Merge branch 'develop' into feature/tracking
This commit is contained in:
4
.env
4
.env
@@ -1,4 +0,0 @@
|
|||||||
# See update-dotenv.mjs
|
|
||||||
AUTH_URL="REPLACE-ON-NETLIFY-BUILD"
|
|
||||||
NEXTAUTH_URL="REPLACE-ON-NETLIFY-BUILD"
|
|
||||||
PUBLIC_URL="REPLACED-ON-NETLIFY-BUILD"
|
|
||||||
67
actions/registerUserBookingFlow.ts
Normal file
67
actions/registerUserBookingFlow.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { parsePhoneNumber } from "libphonenumber-js"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { serviceServerActionProcedure } from "@/server/trpc"
|
||||||
|
|
||||||
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
|
|
||||||
|
const registerUserPayload = z.object({
|
||||||
|
firstName: z.string(),
|
||||||
|
lastName: z.string(),
|
||||||
|
dateOfBirth: z.string(),
|
||||||
|
address: z.object({
|
||||||
|
countryCode: z.string(),
|
||||||
|
zipCode: z.string(),
|
||||||
|
}),
|
||||||
|
email: z.string(),
|
||||||
|
phoneNumber: phoneValidator("Phone is required"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registerUserBookingFlow = serviceServerActionProcedure
|
||||||
|
.input(registerUserPayload)
|
||||||
|
.mutation(async function ({ ctx, input }) {
|
||||||
|
const payload = {
|
||||||
|
...input,
|
||||||
|
language: ctx.lang,
|
||||||
|
phoneNumber: parsePhoneNumber(input.phoneNumber)
|
||||||
|
.formatNational()
|
||||||
|
.replace(/\s+/g, ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Consume the API to register the user as soon as passwordless signup is enabled.
|
||||||
|
// let apiResponse
|
||||||
|
// try {
|
||||||
|
// apiResponse = await api.post(api.endpoints.v1.profile, {
|
||||||
|
// body: payload,
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Unexpected error", error)
|
||||||
|
// return { success: false, error: "Unexpected error" }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!apiResponse.ok) {
|
||||||
|
// const text = await apiResponse.text()
|
||||||
|
// console.error(text)
|
||||||
|
// console.error(
|
||||||
|
// "registerUserBookingFlow api error",
|
||||||
|
// JSON.stringify({
|
||||||
|
// query: input,
|
||||||
|
// error: {
|
||||||
|
// status: apiResponse.status,
|
||||||
|
// statusText: apiResponse.statusText,
|
||||||
|
// error: text,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
// return { success: false, error: "API error" }
|
||||||
|
// }
|
||||||
|
// const json = await apiResponse.json()
|
||||||
|
// console.log("registerUserBookingFlow: json", json)
|
||||||
|
|
||||||
|
return { success: true, data: payload }
|
||||||
|
})
|
||||||
@@ -4,6 +4,7 @@ import { AuthError } from "next-auth"
|
|||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { internalServerError } from "@/server/errors/next"
|
import { internalServerError } from "@/server/errors/next"
|
||||||
|
import { getPublicURL } from "@/server/utils"
|
||||||
|
|
||||||
import { signOut } from "@/auth"
|
import { signOut } from "@/auth"
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ export async function GET(
|
|||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
context: { params: { lang: Lang } }
|
context: { params: { lang: Lang } }
|
||||||
) {
|
) {
|
||||||
|
const publicURL = getPublicURL(request)
|
||||||
|
|
||||||
let redirectTo: string = ""
|
let redirectTo: string = ""
|
||||||
|
|
||||||
const returnUrl = request.headers.get("x-returnurl")
|
const returnUrl = request.headers.get("x-returnurl")
|
||||||
@@ -39,7 +42,7 @@ export async function GET(
|
|||||||
// Make relative URL to absolute URL
|
// Make relative URL to absolute URL
|
||||||
if (redirectTo.startsWith("/")) {
|
if (redirectTo.startsWith("/")) {
|
||||||
console.log(`[logout] make redirectTo absolute, from ${redirectTo}`)
|
console.log(`[logout] make redirectTo absolute, from ${redirectTo}`)
|
||||||
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
|
redirectTo = new URL(redirectTo, publicURL).href
|
||||||
console.log(`[logout] make redirectTo absolute, to ${redirectTo}`)
|
console.log(`[logout] make redirectTo absolute, to ${redirectTo}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { ArrowRightIcon } from "@/components/Icons"
|
|
||||||
import ManagePreferencesButton from "@/components/Profile/ManagePreferencesButton"
|
import ManagePreferencesButton from "@/components/Profile/ManagePreferencesButton"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
|
||||||
import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout"
|
import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout"
|
||||||
@@ -15,7 +17,7 @@ export default function ProfileLayout({
|
|||||||
{profile}
|
{profile}
|
||||||
<Divider color="burgundy" opacity={8} />
|
<Divider color="burgundy" opacity={8} />
|
||||||
{creditCards}
|
{creditCards}
|
||||||
{communication}
|
{env.HIDE_FOR_NEXT_RELEASE ? null : communication}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
|
|
||||||
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
|
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
|
||||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||||
|
import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek"
|
||||||
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
|
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
|
||||||
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
@@ -38,6 +39,7 @@ export default async function StepLayout({
|
|||||||
<Summary />
|
<Summary />
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
<SidePeek hotel={hotel.data.attributes} />
|
||||||
</main>
|
</main>
|
||||||
</EnterDetailsProvider>
|
</EnterDetailsProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
|
|
||||||
|
|
||||||
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
@@ -15,18 +15,20 @@ export default async function SelectRatePage({
|
|||||||
}: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) {
|
}: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|
||||||
const hotelData = await serverClient().hotel.hotelData.get({
|
const [hotelData, roomConfigurations, user] = await Promise.all([
|
||||||
hotelId: searchParams.hotel,
|
serverClient().hotel.hotelData.get({
|
||||||
language: params.lang,
|
hotelId: searchParams.hotel,
|
||||||
include: ["RoomCategories"],
|
language: params.lang,
|
||||||
})
|
include: ["RoomCategories"],
|
||||||
|
}),
|
||||||
const roomConfigurations = await serverClient().hotel.availability.rooms({
|
serverClient().hotel.availability.rooms({
|
||||||
hotelId: parseInt(searchParams.hotel, 10),
|
hotelId: parseInt(searchParams.hotel, 10),
|
||||||
roomStayStartDate: "2024-11-02",
|
roomStayStartDate: "2024-11-02",
|
||||||
roomStayEndDate: "2024-11-03",
|
roomStayEndDate: "2024-11-03",
|
||||||
adults: 1,
|
adults: 1,
|
||||||
})
|
}),
|
||||||
|
getProfileSafely(),
|
||||||
|
])
|
||||||
|
|
||||||
if (!roomConfigurations) {
|
if (!roomConfigurations) {
|
||||||
return "No rooms found" // TODO: Add a proper error message
|
return "No rooms found" // TODO: Add a proper error message
|
||||||
@@ -47,6 +49,7 @@ export default async function SelectRatePage({
|
|||||||
<RoomSelection
|
<RoomSelection
|
||||||
roomConfigurations={roomConfigurations}
|
roomConfigurations={roomConfigurations}
|
||||||
roomCategories={roomCategories ?? []}
|
roomCategories={roomCategories ?? []}
|
||||||
|
user={user}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AuthError } from "next-auth"
|
|||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { internalServerError } from "@/server/errors/next"
|
import { internalServerError } from "@/server/errors/next"
|
||||||
|
import { getPublicURL } from "@/server/utils"
|
||||||
|
|
||||||
import { signIn } from "@/auth"
|
import { signIn } from "@/auth"
|
||||||
|
|
||||||
@@ -11,9 +12,7 @@ export async function GET(
|
|||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
context: { params: { lang: Lang } }
|
context: { params: { lang: Lang } }
|
||||||
) {
|
) {
|
||||||
if (!env.PUBLIC_URL) {
|
const publicURL = getPublicURL(request)
|
||||||
throw internalServerError("No value for env.PUBLIC_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
let redirectHeaders: Headers | undefined = undefined
|
let redirectHeaders: Headers | undefined = undefined
|
||||||
let redirectTo: string
|
let redirectTo: string
|
||||||
@@ -54,7 +53,7 @@ export async function GET(
|
|||||||
// Make relative URL to absolute URL
|
// Make relative URL to absolute URL
|
||||||
if (redirectTo.startsWith("/")) {
|
if (redirectTo.startsWith("/")) {
|
||||||
console.log(`[login] make redirectTo absolute, from ${redirectTo}`)
|
console.log(`[login] make redirectTo absolute, from ${redirectTo}`)
|
||||||
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
|
redirectTo = new URL(redirectTo, publicURL).href
|
||||||
console.log(`[login] make redirectTo absolute, to ${redirectTo}`)
|
console.log(`[login] make redirectTo absolute, to ${redirectTo}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +130,7 @@ export async function GET(
|
|||||||
* because user might choose to do Email link login.
|
* because user might choose to do Email link login.
|
||||||
* */
|
* */
|
||||||
// The `for_origin` param is used to make Curity email login functionality working.
|
// The `for_origin` param is used to make Curity email login functionality working.
|
||||||
for_origin: env.PUBLIC_URL,
|
for_origin: publicURL,
|
||||||
// This is new param set for differentiate between the Magic link login of New web and current web
|
// This is new param set for differentiate between the Magic link login of New web and current web
|
||||||
version: "2",
|
version: "2",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Lang } from "@/constants/languages"
|
|||||||
import { login } from "@/constants/routes/handleAuth"
|
import { login } from "@/constants/routes/handleAuth"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { badRequest, internalServerError } from "@/server/errors/next"
|
import { badRequest, internalServerError } from "@/server/errors/next"
|
||||||
|
import { getPublicURL } from "@/server/utils"
|
||||||
|
|
||||||
import { signIn } from "@/auth"
|
import { signIn } from "@/auth"
|
||||||
|
|
||||||
@@ -12,9 +13,7 @@ export async function GET(
|
|||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
context: { params: { lang: Lang } }
|
context: { params: { lang: Lang } }
|
||||||
) {
|
) {
|
||||||
if (!env.PUBLIC_URL) {
|
const publicURL = getPublicURL(request)
|
||||||
throw internalServerError("No value for env.PUBLIC_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginKey = request.nextUrl.searchParams.get("loginKey")
|
const loginKey = request.nextUrl.searchParams.get("loginKey")
|
||||||
if (!loginKey) {
|
if (!loginKey) {
|
||||||
@@ -44,7 +43,7 @@ export async function GET(
|
|||||||
console.log(
|
console.log(
|
||||||
`[verifymagiclink] make redirectTo absolute, from ${redirectTo}`
|
`[verifymagiclink] make redirectTo absolute, from ${redirectTo}`
|
||||||
)
|
)
|
||||||
redirectTo = new URL(redirectTo, env.PUBLIC_URL).href
|
redirectTo = new URL(redirectTo, publicURL).href
|
||||||
console.log(`[verifymagiclink] make redirectTo absolute, to ${redirectTo}`)
|
console.log(`[verifymagiclink] make redirectTo absolute, to ${redirectTo}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +68,7 @@ export async function GET(
|
|||||||
ui_locales: context.params.lang,
|
ui_locales: context.params.lang,
|
||||||
scope: ["openid", "profile"].join(" "),
|
scope: ["openid", "profile"].join(" "),
|
||||||
loginKey: loginKey,
|
loginKey: loginKey,
|
||||||
for_origin: env.PUBLIC_URL,
|
for_origin: publicURL,
|
||||||
acr_values: "abc",
|
acr_values: "abc",
|
||||||
version: "2",
|
version: "2",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ import { env } from "process"
|
|||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
import { profile } from "@/constants/routes/myPages"
|
import { profile } from "@/constants/routes/myPages"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
import { getPublicURL } from "@/server/utils"
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { lang: string } }
|
{ params }: { params: { lang: string } }
|
||||||
) {
|
) {
|
||||||
|
const publicURL = getPublicURL(request)
|
||||||
|
|
||||||
console.log(`[add-card] callback started`)
|
console.log(`[add-card] callback started`)
|
||||||
const lang = params.lang as Lang
|
const lang = params.lang as Lang
|
||||||
const returnUrl = new URL(`${env.PUBLIC_URL}/${profile[lang ?? Lang.en]}`)
|
const returnUrl = new URL(`${publicURL}/${profile[lang ?? Lang.en]}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const searchParams = request.nextUrl.searchParams
|
const searchParams = request.nextUrl.searchParams
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const e = process.env
|
|
||||||
console.log({ process_env: process.env })
|
|
||||||
|
|
||||||
const urlVar = "PUBLIC_URL"
|
|
||||||
const nextAuthUrlVar = "NEXTAUTH_URL"
|
|
||||||
const nextAuthUrlVar2 = "AUTH_URL"
|
|
||||||
const envTestVar = "ENVTEST"
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
env_url: env.PUBLIC_URL,
|
|
||||||
static_url: process.env.PUBLIC_URL,
|
|
||||||
dynamic_url: e[urlVar],
|
|
||||||
env_envtest: env.ENVTEST,
|
|
||||||
static_envtest: process.env.ENVTEST,
|
|
||||||
dynamic_envtest: e[envTestVar],
|
|
||||||
env_nextauth: env.NEXTAUTH_URL,
|
|
||||||
static_nextauth: process.env.NEXTAUTH_URL,
|
|
||||||
dynamic_nextauth: e[nextAuthUrlVar],
|
|
||||||
env_nextauth2: env.AUTH_URL,
|
|
||||||
static_nextauth2: process.env.AUTH_URL,
|
|
||||||
dynamic_nextauth2: e[nextAuthUrlVar2],
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
|
|
||||||
return NextResponse.json(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
|
||||||
|
|
||||||
export const runtime = "edge"
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { config } from "dotenv"
|
|
||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
config({ path: "./.env" })
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const e = process.env
|
|
||||||
console.log({ process_env: process.env })
|
|
||||||
|
|
||||||
const urlVar = "PUBLIC_URL"
|
|
||||||
const nextAuthUrlVar = "NEXTAUTH_URL"
|
|
||||||
const nextAuthUrlVar2 = "AUTH_URL"
|
|
||||||
const envTestVar = "ENVTEST"
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
env_url: env.PUBLIC_URL,
|
|
||||||
static_url: process.env.PUBLIC_URL,
|
|
||||||
dynamic_url: e[urlVar],
|
|
||||||
env_envtest: env.ENVTEST,
|
|
||||||
static_envtest: process.env.ENVTEST,
|
|
||||||
dynamic_envtest: e[envTestVar],
|
|
||||||
env_nextauth: env.NEXTAUTH_URL,
|
|
||||||
static_nextauth: process.env.NEXTAUTH_URL,
|
|
||||||
dynamic_nextauth: e[nextAuthUrlVar],
|
|
||||||
env_nextauth2: env.AUTH_URL,
|
|
||||||
static_nextauth2: process.env.AUTH_URL,
|
|
||||||
dynamic_nextauth2: e[nextAuthUrlVar2],
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
|
|
||||||
return NextResponse.json(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import "dotenv/config"
|
|
||||||
|
|
||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const e = process.env
|
|
||||||
console.log({ process_env: process.env })
|
|
||||||
|
|
||||||
const urlVar = "PUBLC_URL"
|
|
||||||
const nextAuthUrlVar = "NEXTAUTH_URL"
|
|
||||||
const nextAuthUrlVar2 = "AUTH_URL"
|
|
||||||
const envTestVar = "ENVTEST"
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
env_url: env.PUBLIC_URL,
|
|
||||||
static_url: process.env.PUBLIC_URL,
|
|
||||||
dynamic_url: e[urlVar],
|
|
||||||
env_envtest: env.ENVTEST,
|
|
||||||
static_envtest: process.env.ENVTEST,
|
|
||||||
dynamic_envtest: e[envTestVar],
|
|
||||||
env_nextauth: env.NEXTAUTH_URL,
|
|
||||||
static_nextauth: process.env.NEXTAUTH_URL,
|
|
||||||
dynamic_nextauth: e[nextAuthUrlVar],
|
|
||||||
env_nextauth2: env.AUTH_URL,
|
|
||||||
static_nextauth2: process.env.AUTH_URL,
|
|
||||||
dynamic_nextauth2: e[nextAuthUrlVar2],
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
|
|
||||||
return NextResponse.json(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { config } from "dotenv"
|
|
||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
config({ debug: true, override: true })
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const e = process.env
|
|
||||||
console.log({ process_env: process.env })
|
|
||||||
|
|
||||||
const urlVar = "PUBLC_URL"
|
|
||||||
const nextAuthUrlVar = "NEXTAUTH_URL"
|
|
||||||
const nextAuthUrlVar2 = "AUTH_URL"
|
|
||||||
const envTestVar = "ENVTEST"
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
env_url: env.PUBLIC_URL,
|
|
||||||
static_url: process.env.PUBLIC_URL,
|
|
||||||
dynamic_url: e[urlVar],
|
|
||||||
env_envtest: env.ENVTEST,
|
|
||||||
static_envtest: process.env.ENVTEST,
|
|
||||||
dynamic_envtest: e[envTestVar],
|
|
||||||
env_nextauth: env.NEXTAUTH_URL,
|
|
||||||
static_nextauth: process.env.NEXTAUTH_URL,
|
|
||||||
dynamic_nextauth: e[nextAuthUrlVar],
|
|
||||||
env_nextauth2: env.AUTH_URL,
|
|
||||||
static_nextauth2: process.env.AUTH_URL,
|
|
||||||
dynamic_nextauth2: e[nextAuthUrlVar2],
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
|
|
||||||
return NextResponse.json(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const e = process.env
|
|
||||||
console.log({ process_env: process.env })
|
|
||||||
|
|
||||||
const urlVar = "PUBLIC_URL"
|
|
||||||
const nextAuthUrlVar = "NEXTAUTH_URL"
|
|
||||||
const nextAuthUrlVar2 = "AUTH_URL"
|
|
||||||
const envTestVar = "ENVTEST"
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
env_url: env.PUBLIC_URL,
|
|
||||||
static_url: process.env.PUBLIC_URL,
|
|
||||||
dynamic_url: e[urlVar],
|
|
||||||
env_envtest: env.ENVTEST,
|
|
||||||
static_envtest: process.env.ENVTEST,
|
|
||||||
dynamic_envtest: e[envTestVar],
|
|
||||||
env_nextauth: env.NEXTAUTH_URL,
|
|
||||||
static_nextauth: process.env.NEXTAUTH_URL,
|
|
||||||
dynamic_nextauth: e[nextAuthUrlVar],
|
|
||||||
env_nextauth2: env.AUTH_URL,
|
|
||||||
static_nextauth2: process.env.AUTH_URL,
|
|
||||||
dynamic_nextauth2: e[nextAuthUrlVar2],
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
|
|
||||||
return NextResponse.json(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { config } from "dotenv"
|
|
||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
config({ path: "./.env" })
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const e = process.env
|
|
||||||
console.log({ process_env: process.env })
|
|
||||||
|
|
||||||
const urlVar = "PUBLIC_URL"
|
|
||||||
const nextAuthUrlVar = "NEXTAUTH_URL"
|
|
||||||
const nextAuthUrlVar2 = "AUTH_URL"
|
|
||||||
const envTestVar = "ENVTEST"
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
env_url: env.PUBLIC_URL,
|
|
||||||
static_url: process.env.PUBLIC_URL,
|
|
||||||
dynamic_url: e[urlVar],
|
|
||||||
env_envtest: env.ENVTEST,
|
|
||||||
static_envtest: process.env.ENVTEST,
|
|
||||||
dynamic_envtest: e[envTestVar],
|
|
||||||
env_nextauth: env.NEXTAUTH_URL,
|
|
||||||
static_nextauth: process.env.NEXTAUTH_URL,
|
|
||||||
dynamic_nextauth: e[nextAuthUrlVar],
|
|
||||||
env_nextauth2: env.AUTH_URL,
|
|
||||||
static_nextauth2: process.env.AUTH_URL,
|
|
||||||
dynamic_nextauth2: e[nextAuthUrlVar2],
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
|
|
||||||
return NextResponse.json(values)
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import "dotenv/config"
|
|
||||||
|
|
||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const e = process.env
|
|
||||||
console.log({ process_env: process.env })
|
|
||||||
|
|
||||||
const urlVar = "PUBLIC_URL"
|
|
||||||
const nextAuthUrlVar = "NEXTAUTH_URL"
|
|
||||||
const nextAuthUrlVar2 = "AUTH_URL"
|
|
||||||
const envTestVar = "ENVTEST"
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
env_url: env.PUBLIC_URL,
|
|
||||||
static_url: process.env.PUBLIC_URL,
|
|
||||||
dynamic_url: e[urlVar],
|
|
||||||
env_envtest: env.ENVTEST,
|
|
||||||
static_envtest: process.env.ENVTEST,
|
|
||||||
dynamic_envtest: e[envTestVar],
|
|
||||||
env_nextauth: env.NEXTAUTH_URL,
|
|
||||||
static_nextauth: process.env.NEXTAUTH_URL,
|
|
||||||
dynamic_nextauth: e[nextAuthUrlVar],
|
|
||||||
env_nextauth2: env.AUTH_URL,
|
|
||||||
static_nextauth2: process.env.AUTH_URL,
|
|
||||||
dynamic_nextauth2: e[nextAuthUrlVar2],
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
|
|
||||||
return NextResponse.json(values)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { config } from "dotenv"
|
|
||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
config({ debug: true, override: true })
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const e = process.env
|
|
||||||
console.log({ process_env: process.env })
|
|
||||||
|
|
||||||
const urlVar = "PUBLIC_URL"
|
|
||||||
const nextAuthUrlVar = "NEXTAUTH_URL"
|
|
||||||
const nextAuthUrlVar2 = "AUTH_URL"
|
|
||||||
const envTestVar = "ENVTEST"
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
env_url: env.PUBLIC_URL,
|
|
||||||
static_url: process.env.PUBLIC_URL,
|
|
||||||
dynamic_url: e[urlVar],
|
|
||||||
env_envtest: env.ENVTEST,
|
|
||||||
static_envtest: process.env.ENVTEST,
|
|
||||||
dynamic_envtest: e[envTestVar],
|
|
||||||
env_nextauth: env.NEXTAUTH_URL,
|
|
||||||
static_nextauth: process.env.NEXTAUTH_URL,
|
|
||||||
dynamic_nextauth: e[nextAuthUrlVar],
|
|
||||||
env_nextauth2: env.AUTH_URL,
|
|
||||||
static_nextauth2: process.env.AUTH_URL,
|
|
||||||
dynamic_nextauth2: e[nextAuthUrlVar2],
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
|
|
||||||
return NextResponse.json(values)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { NextResponse } from "next/server"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const e = process.env
|
|
||||||
console.log({ process_env: process.env })
|
|
||||||
|
|
||||||
const urlVar = "PUBLIC_URL"
|
|
||||||
const nextAuthUrlVar = "NEXTAUTH_URL"
|
|
||||||
const nextAuthUrlVar2 = "AUTH_URL"
|
|
||||||
const envTestVar = "ENVTEST"
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
env_url: env.PUBLIC_URL,
|
|
||||||
static_url: process.env.PUBLIC_URL,
|
|
||||||
dynamic_url: e[urlVar],
|
|
||||||
env_envtest: env.ENVTEST,
|
|
||||||
static_envtest: process.env.ENVTEST,
|
|
||||||
dynamic_envtest: e[envTestVar],
|
|
||||||
env_nextauth: env.NEXTAUTH_URL,
|
|
||||||
static_nextauth: process.env.NEXTAUTH_URL,
|
|
||||||
dynamic_nextauth: e[nextAuthUrlVar],
|
|
||||||
env_nextauth2: env.AUTH_URL,
|
|
||||||
static_nextauth2: process.env.AUTH_URL,
|
|
||||||
dynamic_nextauth2: e[nextAuthUrlVar2],
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
|
|
||||||
return NextResponse.json(values)
|
|
||||||
}
|
|
||||||
@@ -6,20 +6,21 @@ import {
|
|||||||
bookingConfirmation,
|
bookingConfirmation,
|
||||||
payment,
|
payment,
|
||||||
} from "@/constants/routes/hotelReservation"
|
} from "@/constants/routes/hotelReservation"
|
||||||
|
import { getPublicURL } from "@/server/utils"
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { lang: string; status: string } }
|
{ params }: { params: { lang: string; status: string } }
|
||||||
): Promise<NextResponse> {
|
): Promise<NextResponse> {
|
||||||
|
const publicURL = getPublicURL(request)
|
||||||
|
|
||||||
console.log(`[payment-callback] callback started`)
|
console.log(`[payment-callback] callback started`)
|
||||||
const lang = params.lang as Lang
|
const lang = params.lang as Lang
|
||||||
const status = params.status
|
const status = params.status
|
||||||
const returnUrl = new URL(`${env.PUBLIC_URL}/${payment[lang]}`)
|
const returnUrl = new URL(`${publicURL}/${payment[lang]}`)
|
||||||
|
|
||||||
if (status === "success") {
|
if (status === "success") {
|
||||||
const confirmationUrl = new URL(
|
const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation[lang]}`)
|
||||||
`${env.PUBLIC_URL}/${bookingConfirmation[lang]}`
|
|
||||||
)
|
|
||||||
console.log(`[payment-callback] redirecting to: ${confirmationUrl}`)
|
console.log(`[payment-callback] redirecting to: ${confirmationUrl}`)
|
||||||
return NextResponse.redirect(confirmationUrl)
|
return NextResponse.redirect(confirmationUrl)
|
||||||
}
|
}
|
||||||
|
|||||||
3
auth.ts
3
auth.ts
@@ -109,6 +109,7 @@ const curityProvider = {
|
|||||||
} satisfies OIDCConfig<User>
|
} satisfies OIDCConfig<User>
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
basePath: "/api/web/auth",
|
||||||
debug: env.NEXTAUTH_DEBUG,
|
debug: env.NEXTAUTH_DEBUG,
|
||||||
providers: [curityProvider],
|
providers: [curityProvider],
|
||||||
redirectProxyUrl: env.NEXTAUTH_REDIRECT_PROXY_URL,
|
redirectProxyUrl: env.NEXTAUTH_REDIRECT_PROXY_URL,
|
||||||
@@ -233,4 +234,4 @@ export const {
|
|||||||
auth,
|
auth,
|
||||||
signIn,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
} = NextAuth(config)
|
} = NextAuth(config)
|
||||||
|
|||||||
7
components/Blocks/Accordion/accordion.module.css
Normal file
7
components/Blocks/Accordion/accordion.module.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.accordion:not(.allVisible) :nth-child(n + 6) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion:not(.allVisible) :nth-child(5) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
53
components/Blocks/Accordion/index.tsx
Normal file
53
components/Blocks/Accordion/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"use client"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import JsonToHtml from "@/components/JsonToHtml"
|
||||||
|
import SectionContainer from "@/components/Section/Container"
|
||||||
|
import SectionHeader from "@/components/Section/Header"
|
||||||
|
import Accordion from "@/components/TempDesignSystem/Accordion"
|
||||||
|
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
||||||
|
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||||
|
|
||||||
|
import styles from "./accordion.module.css"
|
||||||
|
|
||||||
|
import type { AccordionProps } from "@/types/components/blocks/Accordion"
|
||||||
|
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
|
||||||
|
|
||||||
|
export default function AccordionSection({ accordion, title }: AccordionProps) {
|
||||||
|
const showToggleButton = accordion.length > 5
|
||||||
|
const [allAccordionsVisible, setAllAccordionsVisible] =
|
||||||
|
useState(!showToggleButton)
|
||||||
|
|
||||||
|
function toggleAccordions() {
|
||||||
|
setAllAccordionsVisible((state) => !state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionContainer id={HotelHashValues.faq}>
|
||||||
|
{title && <SectionHeader textTransform="uppercase" title={title} />}
|
||||||
|
<Accordion
|
||||||
|
className={`${styles.accordion} ${allAccordionsVisible ? styles.allVisible : ""}`}
|
||||||
|
theme="light"
|
||||||
|
variant="card"
|
||||||
|
>
|
||||||
|
{accordion.map((acc) => (
|
||||||
|
<AccordionItem key={acc.question} title={acc.question}>
|
||||||
|
<JsonToHtml
|
||||||
|
embeds={acc.answer.embedded_itemsConnection.edges}
|
||||||
|
nodes={acc.answer.json?.children[0].children}
|
||||||
|
/>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
{showToggleButton ? (
|
||||||
|
<ShowMoreButton
|
||||||
|
loadMoreData={toggleAccordions}
|
||||||
|
showLess={allAccordionsVisible}
|
||||||
|
textShowMore="See all FAQ"
|
||||||
|
textShowLess="See less FAQ"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</SectionContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,13 +7,27 @@ import TeaserCard from "@/components/TempDesignSystem/TeaserCard"
|
|||||||
|
|
||||||
import type { CardsGridProps } from "@/types/components/blocks/cardsGrid"
|
import type { CardsGridProps } from "@/types/components/blocks/cardsGrid"
|
||||||
import { CardsGridEnum, CardsGridLayoutEnum } from "@/types/enums/cardsGrid"
|
import { CardsGridEnum, CardsGridLayoutEnum } from "@/types/enums/cardsGrid"
|
||||||
|
import type { StackableGridProps } from "../TempDesignSystem/Grids/Stackable/stackable"
|
||||||
|
|
||||||
export default function CardsGrid({
|
export default function CardsGrid({
|
||||||
cards_grid,
|
cards_grid,
|
||||||
firstItem = false,
|
firstItem = false,
|
||||||
}: CardsGridProps) {
|
}: CardsGridProps) {
|
||||||
const columns =
|
let columns: StackableGridProps["columns"]
|
||||||
cards_grid.layout === CardsGridLayoutEnum.THREE_COLUMNS ? 3 : 2
|
|
||||||
|
switch (cards_grid.layout) {
|
||||||
|
case CardsGridLayoutEnum.ONE_COLUMN:
|
||||||
|
columns = 1
|
||||||
|
break
|
||||||
|
case CardsGridLayoutEnum.TWO_COLUMNS:
|
||||||
|
columns = 2
|
||||||
|
break
|
||||||
|
case CardsGridLayoutEnum.THREE_COLUMNS:
|
||||||
|
columns = 3
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
columns = 3
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
import HowItWorks from "@/components/Blocks/DynamicContent/HowItWorks"
|
import HowItWorks from "@/components/Blocks/DynamicContent/HowItWorks"
|
||||||
import LoyaltyLevels from "@/components/Blocks/DynamicContent/LoyaltyLevels"
|
import LoyaltyLevels from "@/components/Blocks/DynamicContent/LoyaltyLevels"
|
||||||
import Overview from "@/components/Blocks/DynamicContent/Overview"
|
import Overview from "@/components/Blocks/DynamicContent/Overview"
|
||||||
@@ -26,7 +28,9 @@ export default async function DynamicContent({
|
|||||||
case DynamicContentEnum.Blocks.components.earn_and_burn:
|
case DynamicContentEnum.Blocks.components.earn_and_burn:
|
||||||
return <EarnAndBurn {...dynamic_content} />
|
return <EarnAndBurn {...dynamic_content} />
|
||||||
case DynamicContentEnum.Blocks.components.expiring_points:
|
case DynamicContentEnum.Blocks.components.expiring_points:
|
||||||
return <ExpiringPoints {...dynamic_content} />
|
return env.HIDE_FOR_NEXT_RELEASE ? null : (
|
||||||
|
<ExpiringPoints {...dynamic_content} />
|
||||||
|
)
|
||||||
case DynamicContentEnum.Blocks.components.how_it_works:
|
case DynamicContentEnum.Blocks.components.how_it_works:
|
||||||
return (
|
return (
|
||||||
<HowItWorks dynamic_content={dynamic_content} firstItem={firstItem} />
|
<HowItWorks dynamic_content={dynamic_content} firstItem={firstItem} />
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import { removeMultipleSlashes } from "@/utils/url"
|
|
||||||
|
|
||||||
import styles from "./uspgrid.module.css"
|
import styles from "./uspgrid.module.css"
|
||||||
|
|
||||||
import { EmbedEnum } from "@/types/requests/utils/embeds"
|
|
||||||
import type { EmbedByUid } from "@/types/transitionTypes/jsontohtml"
|
import type { EmbedByUid } from "@/types/transitionTypes/jsontohtml"
|
||||||
import { RTEItemTypeEnum, RTETypeEnum } from "@/types/transitionTypes/rte/enums"
|
import { RTEItemTypeEnum, RTETypeEnum } from "@/types/transitionTypes/rte/enums"
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import TextCols from "@/components/Blocks/TextCols"
|
|||||||
import UspGrid from "@/components/Blocks/UspGrid"
|
import UspGrid from "@/components/Blocks/UspGrid"
|
||||||
import JsonToHtml from "@/components/JsonToHtml"
|
import JsonToHtml from "@/components/JsonToHtml"
|
||||||
|
|
||||||
|
import AccordionSection from "./Accordion"
|
||||||
import Table from "./Table"
|
import Table from "./Table"
|
||||||
|
|
||||||
import type { BlocksProps } from "@/types/components/blocks"
|
import type { BlocksProps } from "@/types/components/blocks"
|
||||||
@@ -14,6 +15,14 @@ export default function Blocks({ blocks }: BlocksProps) {
|
|||||||
return blocks.map((block, idx) => {
|
return blocks.map((block, idx) => {
|
||||||
const firstItem = idx === 0
|
const firstItem = idx === 0
|
||||||
switch (block.typename) {
|
switch (block.typename) {
|
||||||
|
case BlocksEnums.block.Accordion:
|
||||||
|
return (
|
||||||
|
<AccordionSection
|
||||||
|
accordion={block.accordion.accordions}
|
||||||
|
title={block.accordion.title}
|
||||||
|
key={`${block.typename}-${idx}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case BlocksEnums.block.CardsGrid:
|
case BlocksEnums.block.CardsGrid:
|
||||||
return (
|
return (
|
||||||
<CardsGrid
|
<CardsGrid
|
||||||
@@ -65,6 +74,7 @@ export default function Blocks({ blocks }: BlocksProps) {
|
|||||||
)
|
)
|
||||||
case BlocksEnums.block.UspGrid:
|
case BlocksEnums.block.UspGrid:
|
||||||
return <UspGrid usp_grid={block.usp_grid} />
|
return <UspGrid usp_grid={block.usp_grid} />
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default async function Breadcrumbs() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={breadcrumb.uid} className={styles.listItem}>
|
<li key={breadcrumb.uid} className={styles.listItem}>
|
||||||
<Footnote color="burgundy" textTransform="bold">
|
<Footnote color="burgundy" type="bold">
|
||||||
{breadcrumb.title}
|
{breadcrumb.title}
|
||||||
</Footnote>
|
</Footnote>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import Title from "@/components/TempDesignSystem/Text/Title"
|
|||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { IntroSectionProps } from "./types"
|
|
||||||
|
|
||||||
import styles from "./introSection.module.css"
|
import styles from "./introSection.module.css"
|
||||||
|
|
||||||
|
import type { IntroSectionProps } from "./types"
|
||||||
|
|
||||||
export default async function IntroSection({
|
export default async function IntroSection({
|
||||||
hotelName,
|
hotelName,
|
||||||
hotelDescription,
|
hotelDescription,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import useHotelPageStore from "@/stores/hotel-page"
|
import useHotelPageStore from "@/stores/hotel-page"
|
||||||
@@ -21,7 +20,6 @@ export default function MobileMapToggle() {
|
|||||||
onClick={closeDynamicMap}
|
onClick={closeDynamicMap}
|
||||||
>
|
>
|
||||||
<HouseIcon
|
<HouseIcon
|
||||||
className={styles.icon}
|
|
||||||
color={!isDynamicMapOpen ? "white" : "red"}
|
color={!isDynamicMapOpen ? "white" : "red"}
|
||||||
height={24}
|
height={24}
|
||||||
width={24}
|
width={24}
|
||||||
@@ -34,7 +32,6 @@ export default function MobileMapToggle() {
|
|||||||
onClick={openDynamicMap}
|
onClick={openDynamicMap}
|
||||||
>
|
>
|
||||||
<MapIcon
|
<MapIcon
|
||||||
className={styles.icon}
|
|
||||||
color={isDynamicMapOpen ? "white" : "red"}
|
color={isDynamicMapOpen ? "white" : "red"}
|
||||||
height={24}
|
height={24}
|
||||||
width={24}
|
width={24}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
.mobileToggle {
|
.mobileToggle {
|
||||||
position: fixed;
|
position: sticky;
|
||||||
bottom: var(--Spacing-x5);
|
bottom: var(--Spacing-x5);
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
margin: 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: var(--Spacing-x-half);
|
gap: var(--Spacing-x-half);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
|||||||
|
|
||||||
import styles from "./roomCard.module.css"
|
import styles from "./roomCard.module.css"
|
||||||
|
|
||||||
import type { RoomCardProps } from "@/types/components/hotelPage/roomCard"
|
import type { RoomCardProps } from "@/types/components/hotelPage/room"
|
||||||
|
|
||||||
export function RoomCard({
|
export function RoomCard({
|
||||||
badgeTextTransKey,
|
badgeTextTransKey,
|
||||||
|
|||||||
@@ -3,22 +3,23 @@
|
|||||||
import { useRef, useState } from "react"
|
import { useRef, useState } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { ChevronDownIcon } from "@/components/Icons"
|
|
||||||
import SectionContainer from "@/components/Section/Container"
|
import SectionContainer from "@/components/Section/Container"
|
||||||
import SectionHeader from "@/components/Section/Header"
|
import SectionHeader from "@/components/Section/Header"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Grids from "@/components/TempDesignSystem/Grids"
|
import Grids from "@/components/TempDesignSystem/Grids"
|
||||||
|
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||||
|
|
||||||
import { RoomCard } from "./RoomCard"
|
import { RoomCard } from "./RoomCard"
|
||||||
|
|
||||||
import styles from "./rooms.module.css"
|
import styles from "./rooms.module.css"
|
||||||
|
|
||||||
|
import type { RoomsProps } from "@/types/components/hotelPage/room"
|
||||||
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
|
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
|
||||||
import type { RoomsProps } from "./types"
|
|
||||||
|
|
||||||
export function Rooms({ rooms }: RoomsProps) {
|
export function Rooms({ rooms }: RoomsProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const [allRoomsVisible, setAllRoomsVisible] = useState(false)
|
const showToggleButton = rooms.length > 3
|
||||||
|
const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton)
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const mappedRooms = rooms
|
const mappedRooms = rooms
|
||||||
@@ -42,12 +43,11 @@ export function Rooms({ rooms }: RoomsProps) {
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
|
|
||||||
function handleToggleShowMore() {
|
function handleShowMore() {
|
||||||
if (scrollRef.current && allRoomsVisible) {
|
if (scrollRef.current && allRoomsVisible) {
|
||||||
scrollRef.current.scrollIntoView({ behavior: "smooth" })
|
scrollRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
}
|
}
|
||||||
|
setAllRoomsVisible((state) => !state)
|
||||||
setAllRoomsVisible((previousState) => !previousState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,41 +61,30 @@ export function Rooms({ rooms }: RoomsProps) {
|
|||||||
title={intl.formatMessage({ id: "Rooms" })}
|
title={intl.formatMessage({ id: "Rooms" })}
|
||||||
preamble={null}
|
preamble={null}
|
||||||
/>
|
/>
|
||||||
<Grids.Stackable>
|
<Grids.Stackable
|
||||||
{mappedRooms.map(
|
className={`${styles.grid} ${allRoomsVisible ? styles.allVisible : ""}`}
|
||||||
({ id, images, title, subtitle, popularChoice }, index) => (
|
>
|
||||||
<div
|
{mappedRooms.map(({ id, images, title, subtitle, popularChoice }) => (
|
||||||
key={id}
|
<div key={id}>
|
||||||
className={
|
<RoomCard
|
||||||
!allRoomsVisible && index > 2 ? styles.hiddenRoomCard : ""
|
id={id}
|
||||||
}
|
images={images}
|
||||||
>
|
title={title}
|
||||||
<RoomCard
|
subtitle={subtitle}
|
||||||
id={id}
|
badgeTextTransKey={popularChoice ? "Popular choice" : null}
|
||||||
images={images}
|
/>
|
||||||
title={title}
|
</div>
|
||||||
subtitle={subtitle}
|
))}
|
||||||
badgeTextTransKey={popularChoice ? "Popular choice" : null}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Grids.Stackable>
|
</Grids.Stackable>
|
||||||
|
|
||||||
<div className={styles.ctaContainer}>
|
{showToggleButton ? (
|
||||||
<Button
|
<ShowMoreButton
|
||||||
onClick={handleToggleShowMore}
|
loadMoreData={handleShowMore}
|
||||||
theme="base"
|
showLess={allRoomsVisible}
|
||||||
intent="text"
|
textShowMore="Show more rooms"
|
||||||
variant="icon"
|
textShowLess="Show less rooms"
|
||||||
className={`${styles.showMoreButton} ${allRoomsVisible ? styles.showLess : ""}`}
|
/>
|
||||||
>
|
) : null}
|
||||||
<ChevronDownIcon className={styles.chevron} />
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: allRoomsVisible ? "Show less" : "Show more",
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hiddenRoomCard {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showMoreButton.showLess .chevron {
|
.showMoreButton.showLess .chevron {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid:not(.allVisible) :nth-child(n + 4) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { RoomData } from "@/types/hotel"
|
|
||||||
|
|
||||||
export type RoomsProps = {
|
|
||||||
rooms: RoomData[]
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.stickyWrapper {
|
.stickyWrapper {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--booking-widget-mobile-height);
|
top: var(--booking-widget-mobile-height);
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
background-color: var(--Base-Surface-Subtle-Normal);
|
background-color: var(--Base-Surface-Subtle-Normal);
|
||||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"mapContainer";
|
"mapContainer";
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotelImages {
|
.hotelImages {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import hotelPageParams from "@/constants/routes/hotelPageParams"
|
|||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
|
import AccordionSection from "@/components/Blocks/Accordion"
|
||||||
import SidePeekProvider from "@/components/SidePeekProvider"
|
import SidePeekProvider from "@/components/SidePeekProvider"
|
||||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
@@ -47,6 +48,7 @@ export default async function HotelPage() {
|
|||||||
activitiesCard,
|
activitiesCard,
|
||||||
pointsOfInterest,
|
pointsOfInterest,
|
||||||
facilities,
|
facilities,
|
||||||
|
faq,
|
||||||
} = hotelData
|
} = hotelData
|
||||||
|
|
||||||
const topThreePois = pointsOfInterest.slice(0, 3)
|
const topThreePois = pointsOfInterest.slice(0, 3)
|
||||||
@@ -64,7 +66,7 @@ export default async function HotelPage() {
|
|||||||
<TabNavigation
|
<TabNavigation
|
||||||
restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)}
|
restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)}
|
||||||
hasActivities={!!activitiesCard}
|
hasActivities={!!activitiesCard}
|
||||||
hasFAQ={false}
|
hasFAQ={!!faq}
|
||||||
/>
|
/>
|
||||||
<main className={styles.mainSection}>
|
<main className={styles.mainSection}>
|
||||||
<div id={HotelHashValues.overview} className={styles.introContainer}>
|
<div id={HotelHashValues.overview} className={styles.introContainer}>
|
||||||
@@ -80,6 +82,9 @@ export default async function HotelPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Rooms rooms={roomCategories} />
|
<Rooms rooms={roomCategories} />
|
||||||
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
|
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
|
||||||
|
{faq && (
|
||||||
|
<AccordionSection accordion={faq.accordions} title={faq.title} />
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
{googleMapsApiKey ? (
|
{googleMapsApiKey ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ export default function DatePickerDesktop({
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
mode="range"
|
mode="range"
|
||||||
numberOfMonths={2}
|
numberOfMonths={2}
|
||||||
onSelect={handleOnSelect}
|
onDayClick={handleOnSelect}
|
||||||
pagedNavigation
|
pagedNavigation
|
||||||
required
|
required={false}
|
||||||
selected={selectedDate}
|
selected={selectedDate}
|
||||||
startMonth={currentDate}
|
startMonth={currentDate}
|
||||||
weekStartsOn={1}
|
weekStartsOn={1}
|
||||||
@@ -82,7 +82,7 @@ export default function DatePickerDesktop({
|
|||||||
size="small"
|
size="small"
|
||||||
theme="base"
|
theme="base"
|
||||||
>
|
>
|
||||||
<Caption color="white" textTransform="bold">
|
<Caption color="white" type="bold">
|
||||||
{intl.formatMessage({ id: "Select dates" })}
|
{intl.formatMessage({ id: "Select dates" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default function DatePickerMobile({
|
|||||||
mode="range"
|
mode="range"
|
||||||
/** Showing full year or what's left of it */
|
/** Showing full year or what's left of it */
|
||||||
numberOfMonths={12}
|
numberOfMonths={12}
|
||||||
onSelect={handleOnSelect}
|
onDayClick={handleOnSelect}
|
||||||
required
|
required
|
||||||
selected={selectedDate}
|
selected={selectedDate}
|
||||||
startMonth={startMonth}
|
startMonth={startMonth}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ td.rangeStart[aria-selected="true"] button.dayButton:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton,
|
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton,
|
||||||
td.rangeStart[aria-selected="true"]:not([data-outside="true"])
|
td.rangeStart[aria-selected="true"]:not([data-outside="true"]) button.dayButton,
|
||||||
button.dayButton {
|
td.day[aria-selected="true"] button.dayButton {
|
||||||
background: var(--Primary-Light-On-Surface-Accent);
|
background: var(--Primary-Light-On-Surface-Accent);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--Base-Button-Inverted-Fill-Normal);
|
color: var(--Base-Button-Inverted-Fill-Normal);
|
||||||
@@ -75,6 +75,7 @@ td.rangeMiddle[aria-selected="true"] button.dayButton {
|
|||||||
background: var(--Base-Background-Primary-Normal);
|
background: var(--Base-Background-Primary-Normal);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
color: var(--UI-Text-High-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
td.day[data-disabled="true"],
|
td.day[data-disabled="true"],
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ td.rangeStart[aria-selected="true"] button.dayButton:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton,
|
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton,
|
||||||
td.rangeStart[aria-selected="true"]:not([data-outside="true"])
|
td.rangeStart[aria-selected="true"]:not([data-outside="true"]) button.dayButton,
|
||||||
button.dayButton {
|
td.day[aria-selected="true"] button.dayButton {
|
||||||
background: var(--Primary-Light-On-Surface-Accent);
|
background: var(--Primary-Light-On-Surface-Accent);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--Base-Button-Inverted-Fill-Normal);
|
color: var(--Base-Button-Inverted-Fill-Normal);
|
||||||
@@ -141,6 +141,7 @@ td.rangeMiddle[aria-selected="true"] button.dayButton {
|
|||||||
background: var(--Base-Background-Primary-Normal);
|
background: var(--Base-Background-Primary-Normal);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
color: var(--UI-Text-High-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
td.day[data-disabled="true"],
|
td.day[data-disabled="true"],
|
||||||
|
|||||||
@@ -22,6 +22,11 @@
|
|||||||
|
|
||||||
.hideWrapper {
|
.hideWrapper {
|
||||||
background-color: var(--Main-Grey-White);
|
background-color: var(--Main-Grey-White);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container[data-isopen="true"] .hideWrapper {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1366px) {
|
@media screen and (max-width: 1366px) {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import DatePickerMobile from "./Screen/Mobile"
|
|||||||
|
|
||||||
import styles from "./date-picker.module.css"
|
import styles from "./date-picker.module.css"
|
||||||
|
|
||||||
import type { DateRange } from "react-day-picker"
|
|
||||||
|
|
||||||
import type { DatePickerFormProps } from "@/types/components/datepicker"
|
import type { DatePickerFormProps } from "@/types/components/datepicker"
|
||||||
|
|
||||||
const locales = {
|
const locales = {
|
||||||
@@ -33,6 +31,8 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
const { register, setValue } = useFormContext()
|
const { register, setValue } = useFormContext()
|
||||||
const ref = useRef<HTMLDivElement | null>(null)
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const [isSelectingFrom, setIsSelectingFrom] = useState(true)
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}
|
}
|
||||||
@@ -41,11 +41,29 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
setIsOpen((prevIsOpen) => !prevIsOpen)
|
setIsOpen((prevIsOpen) => !prevIsOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectDate(selected: DateRange) {
|
function handleSelectDate(selected: Date) {
|
||||||
setValue(name, {
|
if (isSelectingFrom) {
|
||||||
from: dt(selected.from).format("YYYY-MM-DD"),
|
setValue(name, {
|
||||||
to: dt(selected.to).format("YYYY-MM-DD"),
|
from: dt(selected).format("YYYY-MM-DD"),
|
||||||
})
|
to: undefined,
|
||||||
|
})
|
||||||
|
setIsSelectingFrom(false)
|
||||||
|
} else {
|
||||||
|
const fromDate = dt(selectedDate.from)
|
||||||
|
const toDate = dt(selected)
|
||||||
|
if (toDate.isAfter(fromDate)) {
|
||||||
|
setValue(name, {
|
||||||
|
from: selectedDate.from,
|
||||||
|
to: toDate.format("YYYY-MM-DD"),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setValue(name, {
|
||||||
|
from: toDate.format("YYYY-MM-DD"),
|
||||||
|
to: selectedDate.from,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setIsSelectingFrom(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -64,7 +82,9 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
|
|||||||
const selectedFromDate = dt(selectedDate.from)
|
const selectedFromDate = dt(selectedDate.from)
|
||||||
.locale(lang)
|
.locale(lang)
|
||||||
.format("ddd D MMM")
|
.format("ddd D MMM")
|
||||||
const selectedToDate = dt(selectedDate.to).locale(lang).format("ddd D MMM")
|
const selectedToDate = !!selectedDate.to
|
||||||
|
? dt(selectedDate.to).locale(lang).format("ddd D MMM")
|
||||||
|
: ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} data-isopen={isOpen} ref={ref}>
|
<div className={styles.container} data-isopen={isOpen} ref={ref}>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function ClearSearchButton({
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<DeleteIcon color="burgundy" height={20} width={20} />
|
<DeleteIcon color="burgundy" height={20} width={20} />
|
||||||
<Caption color="burgundy" textTransform="bold">
|
<Caption color="burgundy" type="bold">
|
||||||
{intl.formatMessage({ id: "Clear searches" })}
|
{intl.formatMessage({ id: "Clear searches" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -48,15 +48,6 @@ export default function Search({ locations }: SearchProps) {
|
|||||||
dispatch({ type: ActionType.CLEAR_HISTORY_LOCATIONS })
|
dispatch({ type: ActionType.CLEAR_HISTORY_LOCATIONS })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOnBlur() {
|
|
||||||
if (!value && state.searchData?.name) {
|
|
||||||
setValue(name, state.searchData.name)
|
|
||||||
// Always need to manually trigger
|
|
||||||
// revalidation when setting value r-h-f
|
|
||||||
trigger()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOnChange(
|
function handleOnChange(
|
||||||
evt: FormEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>
|
evt: FormEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>
|
||||||
) {
|
) {
|
||||||
@@ -138,7 +129,9 @@ export default function Search({ locations }: SearchProps) {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<label {...getLabelProps({ htmlFor: name })} className={styles.label}>
|
<label {...getLabelProps({ htmlFor: name })} className={styles.label}>
|
||||||
<Caption color={isOpen ? "uiTextActive" : "red"}>
|
<Caption color={isOpen ? "uiTextActive" : "red"}>
|
||||||
{intl.formatMessage({ id: "Where to" })}
|
{state.searchData?.type === "hotels"
|
||||||
|
? state.searchData?.relationships?.city?.name
|
||||||
|
: intl.formatMessage({ id: "Where to" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
</label>
|
</label>
|
||||||
<div {...getRootProps({}, { suppressRefError: true })}>
|
<div {...getRootProps({}, { suppressRefError: true })}>
|
||||||
@@ -154,7 +147,6 @@ export default function Search({ locations }: SearchProps) {
|
|||||||
}),
|
}),
|
||||||
...register(name, {
|
...register(name, {
|
||||||
onBlur: function () {
|
onBlur: function () {
|
||||||
handleOnBlur()
|
|
||||||
closeMenu()
|
closeMenu()
|
||||||
},
|
},
|
||||||
onChange: handleOnChange,
|
onChange: handleOnChange,
|
||||||
|
|||||||
@@ -24,8 +24,3 @@
|
|||||||
p {
|
p {
|
||||||
color: var(--UI-Text-Active);
|
color: var(--UI-Text-Active);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container:hover:has(input:not(:active, :focus, :focus-within))
|
|
||||||
input::-webkit-search-cancel-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import { Tooltip } from "@/components/TempDesignSystem/Tooltip"
|
import { Tooltip } from "@/components/TempDesignSystem/Tooltip"
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export default function Voucher() {
|
|||||||
>
|
>
|
||||||
<div className={styles.vouchers}>
|
<div className={styles.vouchers}>
|
||||||
<label>
|
<label>
|
||||||
<Caption color="disabled" textTransform="bold">
|
<Caption color="disabled" type="bold">
|
||||||
{vouchers}
|
{vouchers}
|
||||||
</Caption>
|
</Caption>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
@@ -50,17 +50,17 @@ export default function Voucher() {
|
|||||||
>
|
>
|
||||||
<div className={styles.options}>
|
<div className={styles.options}>
|
||||||
<label className={`${styles.option} ${styles.checkboxVoucher}`}>
|
<label className={`${styles.option} ${styles.checkboxVoucher}`}>
|
||||||
<input type="checkbox" disabled className={styles.checkbox} />
|
<Checkbox name="useVouchers" registerOptions={{ disabled: true }} />
|
||||||
<Caption color="disabled">{useVouchers}</Caption>
|
<Caption color="disabled">{useVouchers}</Caption>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
</label>
|
</label>
|
||||||
<label className={styles.option}>
|
<label className={styles.option}>
|
||||||
<input type="checkbox" disabled className={styles.checkbox} />
|
<Checkbox name="useBonus" registerOptions={{ disabled: true }} />
|
||||||
<Caption color="disabled">{bonus}</Caption>
|
<Caption color="disabled">{bonus}</Caption>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
</label>
|
</label>
|
||||||
<label className={styles.option}>
|
<label className={styles.option}>
|
||||||
<input type="checkbox" disabled className={styles.checkbox} />
|
<Checkbox name="useReward" registerOptions={{ disabled: true }} />
|
||||||
<Caption color="disabled">{reward}</Caption>
|
<Caption color="disabled">{reward}</Caption>
|
||||||
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
.options {
|
.options {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 190px;
|
max-width: 190px;
|
||||||
gap: 0;
|
gap: var(--Spacing-x-half);
|
||||||
}
|
}
|
||||||
.vouchers:hover,
|
.vouchers:hover,
|
||||||
.option:hover {
|
.option:hover {
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
}
|
}
|
||||||
.optionsContainer {
|
.optionsContainer {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.checkboxVoucher {
|
.checkboxVoucher {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -75,8 +75,10 @@
|
|||||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||||
border-radius: var(--Corner-radius-Small);
|
border-radius: var(--Corner-radius-Small);
|
||||||
}
|
}
|
||||||
|
|
||||||
.when:hover,
|
.when:hover,
|
||||||
.rooms:hover,
|
.rooms:hover,
|
||||||
|
.when:has([data-isopen="true"]),
|
||||||
.rooms:has(.input:active, .input:focus, .input:focus-within) {
|
.rooms:has(.input:active, .input:focus, .input:focus-within) {
|
||||||
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
background-color: var(--Base-Surface-Primary-light-Hover-alt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { useState } from "react"
|
||||||
import { useWatch } from "react-hook-form"
|
import { useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import DatePicker from "@/components/DatePicker"
|
import DatePicker from "@/components/DatePicker"
|
||||||
|
import GuestsRoomsPickerForm from "@/components/GuestsRoomsPicker"
|
||||||
import { SearchIcon } from "@/components/Icons"
|
import { SearchIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
import Input from "./Input"
|
|
||||||
import Search from "./Search"
|
import Search from "./Search"
|
||||||
import Voucher from "./Voucher"
|
import Voucher from "./Voucher"
|
||||||
|
|
||||||
@@ -20,7 +21,6 @@ import type { BookingWidgetFormContentProps } from "@/types/components/form/book
|
|||||||
export default function FormContent({
|
export default function FormContent({
|
||||||
locations,
|
locations,
|
||||||
formId,
|
formId,
|
||||||
formState,
|
|
||||||
}: BookingWidgetFormContentProps) {
|
}: BookingWidgetFormContentProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const selectedDate = useWatch({ name: "date" })
|
const selectedDate = useWatch({ name: "date" })
|
||||||
@@ -37,21 +37,21 @@ export default function FormContent({
|
|||||||
<Search locations={locations} />
|
<Search locations={locations} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.when}>
|
<div className={styles.when}>
|
||||||
<Caption color="red" textTransform="bold">
|
<Caption color="red" type="bold">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "booking.nights" },
|
{ id: "booking.nights" },
|
||||||
{ totalNights: nights }
|
{ totalNights: nights > 0 ? nights : 0 }
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
<DatePicker />
|
<DatePicker />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rooms}>
|
<div className={styles.rooms}>
|
||||||
<label>
|
<label>
|
||||||
<Caption color="red" textTransform="bold">
|
<Caption color="red" type="bold">
|
||||||
{rooms}
|
{rooms}
|
||||||
</Caption>
|
</Caption>
|
||||||
</label>
|
</label>
|
||||||
<Input type="text" placeholder={rooms} />
|
<GuestsRoomsPickerForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.voucherContainer}>
|
<div className={styles.voucherContainer}>
|
||||||
@@ -60,17 +60,12 @@ export default function FormContent({
|
|||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<Button
|
<Button
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
disabled={!formState.isValid}
|
|
||||||
form={formId}
|
form={formId}
|
||||||
intent="primary"
|
intent="primary"
|
||||||
theme="base"
|
theme="base"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<Caption
|
<Caption color="white" type="bold" className={styles.buttonText}>
|
||||||
color="white"
|
|
||||||
textTransform="bold"
|
|
||||||
className={styles.buttonText}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Search" })}
|
{intl.formatMessage({ id: "Search" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
<div className={styles.icon}>
|
<div className={styles.icon}>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
.section {
|
.section {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.default {
|
.default {
|
||||||
@@ -35,6 +36,13 @@
|
|||||||
var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
width: min(
|
||||||
|
calc(100dvw - (var(--Spacing-x2) * 2)),
|
||||||
|
var(--max-width-navigation)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.full {
|
.full {
|
||||||
padding: var(--Spacing-x1) 0;
|
padding: var(--Spacing-x1) 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useFormContext } from "react-hook-form"
|
import { useFormContext } from "react-hook-form"
|
||||||
|
|
||||||
|
import { selectHotel, selectRate } from "@/constants/routes/hotelReservation"
|
||||||
|
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
import FormContent from "./FormContent"
|
import FormContent from "./FormContent"
|
||||||
import { bookingWidgetVariants } from "./variants"
|
import { bookingWidgetVariants } from "./variants"
|
||||||
|
|
||||||
@@ -9,11 +13,13 @@ import styles from "./form.module.css"
|
|||||||
|
|
||||||
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
|
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
|
||||||
import type { BookingWidgetFormProps } from "@/types/components/form/bookingwidget"
|
import type { BookingWidgetFormProps } from "@/types/components/form/bookingwidget"
|
||||||
|
import { Location } from "@/types/trpc/routers/hotel/locations"
|
||||||
|
|
||||||
const formId = "booking-widget"
|
const formId = "booking-widget"
|
||||||
|
|
||||||
export default function Form({ locations, type }: BookingWidgetFormProps) {
|
export default function Form({ locations, type }: BookingWidgetFormProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const lang = useLang()
|
||||||
|
|
||||||
const classNames = bookingWidgetVariants({
|
const classNames = bookingWidgetVariants({
|
||||||
type,
|
type,
|
||||||
@@ -23,11 +29,32 @@ export default function Form({ locations, type }: BookingWidgetFormProps) {
|
|||||||
useFormContext<BookingWidgetSchema>()
|
useFormContext<BookingWidgetSchema>()
|
||||||
|
|
||||||
function onSubmit(data: BookingWidgetSchema) {
|
function onSubmit(data: BookingWidgetSchema) {
|
||||||
data.location = JSON.parse(decodeURIComponent(data.location))
|
const locationData: Location = JSON.parse(decodeURIComponent(data.location))
|
||||||
console.log(data)
|
|
||||||
// TODO: Parse data and route accordignly to Select hotel or select room-rate page
|
const bookingFlowPage =
|
||||||
console.log("to be routing")
|
locationData.type == "cities" ? selectHotel[lang] : selectRate[lang]
|
||||||
router.push("/en/hotelreservation/select-hotel")
|
const bookingWidgetParams = new URLSearchParams(data.date)
|
||||||
|
|
||||||
|
if (locationData.type == "cities")
|
||||||
|
bookingWidgetParams.set("city", locationData.name)
|
||||||
|
else bookingWidgetParams.set("hotel", locationData.operaId || "")
|
||||||
|
|
||||||
|
data.rooms.forEach((room, index) => {
|
||||||
|
bookingWidgetParams.set(`room[${index}].adults`, room.adults.toString())
|
||||||
|
|
||||||
|
room.children.forEach((child, childIndex) => {
|
||||||
|
bookingWidgetParams.set(
|
||||||
|
`room[${index}].child[${childIndex}].age`,
|
||||||
|
child.age.toString()
|
||||||
|
)
|
||||||
|
bookingWidgetParams.set(
|
||||||
|
`room[${index}].child[${childIndex}].bed`,
|
||||||
|
child.bed.toString()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
router.push(`${bookingFlowPage}?${bookingWidgetParams.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
import type { Location } from "@/types/trpc/routers/hotel/locations"
|
||||||
|
|
||||||
|
export const guestRoomSchema = z.object({
|
||||||
|
adults: z.number().default(1),
|
||||||
|
children: z.array(
|
||||||
|
z.object({
|
||||||
|
age: z.number().nonnegative(),
|
||||||
|
bed: z.number(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const guestRoomsSchema = z.array(guestRoomSchema)
|
||||||
|
|
||||||
export const bookingWidgetSchema = z.object({
|
export const bookingWidgetSchema = z.object({
|
||||||
bookingCode: z.string(), // Update this as required when working with booking codes component
|
bookingCode: z.string(), // Update this as required when working with booking codes component
|
||||||
date: z.object({
|
date: z.object({
|
||||||
@@ -25,18 +37,7 @@ export const bookingWidgetSchema = z.object({
|
|||||||
{ message: "Required" }
|
{ message: "Required" }
|
||||||
),
|
),
|
||||||
redemption: z.boolean().default(false),
|
redemption: z.boolean().default(false),
|
||||||
rooms: z.array(
|
rooms: guestRoomsSchema,
|
||||||
// This will be updated when working in guests component
|
|
||||||
z.object({
|
|
||||||
adults: z.number().default(1),
|
|
||||||
children: z.array(
|
|
||||||
z.object({
|
|
||||||
age: z.number(),
|
|
||||||
bed: z.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
search: z.string({ coerce: true }).min(1, "Required"),
|
search: z.string({ coerce: true }).min(1, "Required"),
|
||||||
voucher: z.boolean().default(false),
|
voucher: z.boolean().default(false),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function FormContent() {
|
|||||||
const email = `${intl.formatMessage({ id: "Email" })} ${intl.formatMessage({ id: "Address" }).toLowerCase()}`
|
const email = `${intl.formatMessage({ id: "Email" })} ${intl.formatMessage({ id: "Address" }).toLowerCase()}`
|
||||||
const street = intl.formatMessage({ id: "Address" })
|
const street = intl.formatMessage({ id: "Address" })
|
||||||
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
||||||
const password = intl.formatMessage({ id: "Current password" })
|
const currentPassword = intl.formatMessage({ id: "Current password" })
|
||||||
const retypeNewPassword = intl.formatMessage({ id: "Retype new password" })
|
const retypeNewPassword = intl.formatMessage({ id: "Retype new password" })
|
||||||
const zipCode = intl.formatMessage({ id: "Zip code" })
|
const zipCode = intl.formatMessage({ id: "Zip code" })
|
||||||
|
|
||||||
@@ -72,8 +72,10 @@ export default function FormContent() {
|
|||||||
{intl.formatMessage({ id: "Password" })}
|
{intl.formatMessage({ id: "Password" })}
|
||||||
</Body>
|
</Body>
|
||||||
</header>
|
</header>
|
||||||
<Input label={password} name="password" type="password" />
|
<Input label={currentPassword} name="password" type="password" />
|
||||||
<NewPassword />
|
{/* visibilityToggleable set to false as feature is done for signup first */}
|
||||||
|
{/* likely we can remove the prop altogether once signup launches */}
|
||||||
|
<NewPassword visibilityToggleable={false} />
|
||||||
<Input
|
<Input
|
||||||
label={retypeNewPassword}
|
label={retypeNewPassword}
|
||||||
name="retypeNewPassword"
|
name="retypeNewPassword"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const editProfileSchema = z
|
|||||||
),
|
),
|
||||||
|
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
newPassword: passwordValidator(),
|
newPassword: z.literal("").optional().or(passwordValidator()),
|
||||||
retypeNewPassword: z.string().optional(),
|
retypeNewPassword: z.string().optional(),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.dateField}>
|
<div className={styles.dateField}>
|
||||||
<header>
|
<header>
|
||||||
<Caption textTransform="bold">
|
<Caption type="bold">
|
||||||
{intl.formatMessage({ id: "Birth date" })}
|
{intl.formatMessage({ id: "Birth date" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
75
components/GuestsRoomsPicker/AdultSelector/index.tsx
Normal file
75
components/GuestsRoomsPicker/AdultSelector/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useFormContext } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
||||||
|
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import Counter from "../Counter"
|
||||||
|
|
||||||
|
import styles from "./adult-selector.module.css"
|
||||||
|
|
||||||
|
import { BedTypeEnum } from "@/types/components/bookingWidget/enums"
|
||||||
|
import {
|
||||||
|
AdultSelectorProps,
|
||||||
|
Child,
|
||||||
|
} from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||||
|
|
||||||
|
export default function AdultSelector({ roomIndex = 0 }: AdultSelectorProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const adultsLabel = intl.formatMessage({ id: "Adults" })
|
||||||
|
const { setValue } = useFormContext()
|
||||||
|
const { adults, children, childrenInAdultsBed } = useGuestsRoomsStore(
|
||||||
|
(state) => state.rooms[roomIndex]
|
||||||
|
)
|
||||||
|
const increaseAdults = useGuestsRoomsStore((state) => state.increaseAdults)
|
||||||
|
const decreaseAdults = useGuestsRoomsStore((state) => state.decreaseAdults)
|
||||||
|
|
||||||
|
function increaseAdultsCount(roomIndex: number) {
|
||||||
|
if (adults < 6) {
|
||||||
|
increaseAdults(roomIndex)
|
||||||
|
setValue(`rooms.${roomIndex}.adults`, adults + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decreaseAdultsCount(roomIndex: number) {
|
||||||
|
if (adults > 1) {
|
||||||
|
decreaseAdults(roomIndex)
|
||||||
|
setValue(`rooms.${roomIndex}.adults`, adults - 1)
|
||||||
|
if (childrenInAdultsBed > adults) {
|
||||||
|
const toUpdateIndex = children.findIndex(
|
||||||
|
(child: Child) => child.bed == BedTypeEnum.IN_ADULTS_BED
|
||||||
|
)
|
||||||
|
if (toUpdateIndex != -1) {
|
||||||
|
setValue(
|
||||||
|
`rooms.${roomIndex}.children.${toUpdateIndex}.bed`,
|
||||||
|
children[toUpdateIndex].age < 3
|
||||||
|
? BedTypeEnum.IN_CRIB
|
||||||
|
: BedTypeEnum.IN_EXTRA_BED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.container}>
|
||||||
|
<Caption color="uiTextHighContrast" type="bold">
|
||||||
|
{adultsLabel}
|
||||||
|
</Caption>
|
||||||
|
<Counter
|
||||||
|
count={adults}
|
||||||
|
handleOnDecrease={() => {
|
||||||
|
decreaseAdultsCount(roomIndex)
|
||||||
|
}}
|
||||||
|
handleOnIncrease={() => {
|
||||||
|
increaseAdultsCount(roomIndex)
|
||||||
|
}}
|
||||||
|
disableDecrease={adults == 1}
|
||||||
|
disableIncrease={adults == 6}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
140
components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx
Normal file
140
components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useFormContext } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
||||||
|
|
||||||
|
import { ErrorCircleIcon } from "@/components/Icons"
|
||||||
|
import Select from "@/components/TempDesignSystem/Select"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import styles from "./child-selector.module.css"
|
||||||
|
|
||||||
|
import { BedTypeEnum } from "@/types/components/bookingWidget/enums"
|
||||||
|
import {
|
||||||
|
ChildBed,
|
||||||
|
ChildInfoSelectorProps,
|
||||||
|
} from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||||
|
|
||||||
|
export default function ChildInfoSelector({
|
||||||
|
child = { age: -1, bed: -1 },
|
||||||
|
index = 0,
|
||||||
|
roomIndex = 0,
|
||||||
|
}: ChildInfoSelectorProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const ageLabel = intl.formatMessage({ id: "Age" })
|
||||||
|
const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" })
|
||||||
|
const bedLabel = intl.formatMessage({ id: "Bed" })
|
||||||
|
const { setValue, trigger } = useFormContext()
|
||||||
|
const { adults, childrenInAdultsBed } = useGuestsRoomsStore(
|
||||||
|
(state) => state.rooms[roomIndex]
|
||||||
|
)
|
||||||
|
const {
|
||||||
|
isValidated,
|
||||||
|
updateChildAge,
|
||||||
|
updateChildBed,
|
||||||
|
increaseChildInAdultsBed,
|
||||||
|
decreaseChildInAdultsBed,
|
||||||
|
} = useGuestsRoomsStore((state) => ({
|
||||||
|
isValidated: state.isValidated,
|
||||||
|
updateChildAge: state.updateChildAge,
|
||||||
|
updateChildBed: state.updateChildBed,
|
||||||
|
increaseChildInAdultsBed: state.increaseChildInAdultsBed,
|
||||||
|
decreaseChildInAdultsBed: state.decreaseChildInAdultsBed,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ageList = Array.from(Array(13).keys()).map((age) => ({
|
||||||
|
label: `${age}`,
|
||||||
|
value: age,
|
||||||
|
}))
|
||||||
|
|
||||||
|
function updateSelectedAge(age: number) {
|
||||||
|
updateChildAge(age, roomIndex, index)
|
||||||
|
setValue(`rooms.${roomIndex}.children.${index}.age`, age)
|
||||||
|
const availableBedTypes = getAvailableBeds(age)
|
||||||
|
updateSelectedBed(availableBedTypes[0].value)
|
||||||
|
trigger("rooms")
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedBed(bed: number) {
|
||||||
|
if (bed == BedTypeEnum.IN_ADULTS_BED) {
|
||||||
|
increaseChildInAdultsBed(roomIndex)
|
||||||
|
} else if (child.bed == BedTypeEnum.IN_ADULTS_BED) {
|
||||||
|
decreaseChildInAdultsBed(roomIndex)
|
||||||
|
}
|
||||||
|
updateChildBed(bed, roomIndex, index)
|
||||||
|
setValue(`rooms.${roomIndex}.children.${index}.bed`, bed)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allBedTypes: ChildBed[] = [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: "In adults bed" }),
|
||||||
|
value: BedTypeEnum.IN_ADULTS_BED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: "In crib" }),
|
||||||
|
value: BedTypeEnum.IN_CRIB,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: "In extra bed" }),
|
||||||
|
value: BedTypeEnum.IN_EXTRA_BED,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function getAvailableBeds(age: number) {
|
||||||
|
let availableBedTypes: ChildBed[] = []
|
||||||
|
if (age <= 5 && (adults > childrenInAdultsBed || child.bed === 0)) {
|
||||||
|
availableBedTypes.push(allBedTypes[0])
|
||||||
|
}
|
||||||
|
if (age < 3) {
|
||||||
|
availableBedTypes.push(allBedTypes[1])
|
||||||
|
}
|
||||||
|
if (age > 2) {
|
||||||
|
availableBedTypes.push(allBedTypes[2])
|
||||||
|
}
|
||||||
|
return availableBedTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div key={index} className={styles.childInfoContainer}>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
required={true}
|
||||||
|
items={ageList}
|
||||||
|
label={ageLabel}
|
||||||
|
aria-label={ageLabel}
|
||||||
|
value={child.age}
|
||||||
|
onSelect={(key) => {
|
||||||
|
updateSelectedAge(key as number)
|
||||||
|
}}
|
||||||
|
name={`rooms.${roomIndex}.children.${index}.age`}
|
||||||
|
placeholder={ageLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{child.age !== -1 ? (
|
||||||
|
<Select
|
||||||
|
items={getAvailableBeds(child.age)}
|
||||||
|
label={bedLabel}
|
||||||
|
aria-label={bedLabel}
|
||||||
|
value={child.bed}
|
||||||
|
onSelect={(key) => {
|
||||||
|
updateSelectedBed(key as number)
|
||||||
|
}}
|
||||||
|
name={`rooms.${roomIndex}.children.${index}.age`}
|
||||||
|
placeholder={bedLabel}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isValidated && child.age < 0 ? (
|
||||||
|
<Caption color="red" className={styles.error}>
|
||||||
|
<ErrorCircleIcon color="red" />
|
||||||
|
{ageReqdErrMsg}
|
||||||
|
</Caption>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captionBold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.childInfoContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
78
components/GuestsRoomsPicker/ChildSelector/index.tsx
Normal file
78
components/GuestsRoomsPicker/ChildSelector/index.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useFormContext } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
||||||
|
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import Counter from "../Counter"
|
||||||
|
import ChildInfoSelector from "./ChildInfoSelector"
|
||||||
|
|
||||||
|
import styles from "./child-selector.module.css"
|
||||||
|
|
||||||
|
import { BookingWidgetSchema } from "@/types/components/bookingWidget"
|
||||||
|
import { ChildSelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||||
|
|
||||||
|
export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const childrenLabel = intl.formatMessage({ id: "Children" })
|
||||||
|
const { setValue, trigger } = useFormContext<BookingWidgetSchema>()
|
||||||
|
const children = useGuestsRoomsStore(
|
||||||
|
(state) => state.rooms[roomIndex].children
|
||||||
|
)
|
||||||
|
const increaseChildren = useGuestsRoomsStore(
|
||||||
|
(state) => state.increaseChildren
|
||||||
|
)
|
||||||
|
const decreaseChildren = useGuestsRoomsStore(
|
||||||
|
(state) => state.decreaseChildren
|
||||||
|
)
|
||||||
|
|
||||||
|
function increaseChildrenCount(roomIndex: number) {
|
||||||
|
if (children.length < 5) {
|
||||||
|
increaseChildren(roomIndex)
|
||||||
|
setValue(`rooms.${roomIndex}.children.${children.length}`, {
|
||||||
|
age: -1,
|
||||||
|
bed: -1,
|
||||||
|
})
|
||||||
|
trigger("rooms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function decreaseChildrenCount(roomIndex: number) {
|
||||||
|
if (children.length > 0) {
|
||||||
|
const newChildrenList = decreaseChildren(roomIndex)
|
||||||
|
setValue(`rooms.${roomIndex}.children`, newChildrenList)
|
||||||
|
trigger("rooms")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className={styles.container}>
|
||||||
|
<Caption color="uiTextHighContrast" type="bold">
|
||||||
|
{childrenLabel}
|
||||||
|
</Caption>
|
||||||
|
<Counter
|
||||||
|
count={children.length}
|
||||||
|
handleOnDecrease={() => {
|
||||||
|
decreaseChildrenCount(roomIndex)
|
||||||
|
}}
|
||||||
|
handleOnIncrease={() => {
|
||||||
|
increaseChildrenCount(roomIndex)
|
||||||
|
}}
|
||||||
|
disableDecrease={children.length == 0}
|
||||||
|
disableIncrease={children.length == 5}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
{children.map((child, index) => (
|
||||||
|
<ChildInfoSelector
|
||||||
|
roomIndex={roomIndex}
|
||||||
|
index={index}
|
||||||
|
child={child}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
13
components/GuestsRoomsPicker/Counter/counter.module.css
Normal file
13
components/GuestsRoomsPicker/Counter/counter.module.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.counterContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.counterBtn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.counterBtn:not([disabled]) {
|
||||||
|
box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
49
components/GuestsRoomsPicker/Counter/index.tsx
Normal file
49
components/GuestsRoomsPicker/Counter/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { MinusIcon, PlusIcon } from "@/components/Icons"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
|
import styles from "./counter.module.css"
|
||||||
|
|
||||||
|
import { CounterProps } from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||||
|
|
||||||
|
export default function Counter({
|
||||||
|
count,
|
||||||
|
handleOnIncrease,
|
||||||
|
handleOnDecrease,
|
||||||
|
disableIncrease,
|
||||||
|
disableDecrease,
|
||||||
|
}: CounterProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.counterContainer}>
|
||||||
|
<Button
|
||||||
|
className={styles.counterBtn}
|
||||||
|
intent="inverted"
|
||||||
|
onClick={handleOnDecrease}
|
||||||
|
size="small"
|
||||||
|
theme="base"
|
||||||
|
variant="icon"
|
||||||
|
wrapping={true}
|
||||||
|
disabled={disableDecrease}
|
||||||
|
>
|
||||||
|
<MinusIcon color="burgundy" />
|
||||||
|
</Button>
|
||||||
|
<Body color="textHighContrast" textAlign="center">
|
||||||
|
{count}
|
||||||
|
</Body>
|
||||||
|
<Button
|
||||||
|
className={styles.counterBtn}
|
||||||
|
onClick={handleOnIncrease}
|
||||||
|
intent="inverted"
|
||||||
|
variant="icon"
|
||||||
|
theme="base"
|
||||||
|
wrapping={true}
|
||||||
|
size="small"
|
||||||
|
disabled={disableIncrease}
|
||||||
|
>
|
||||||
|
<PlusIcon color="burgundy" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
113
components/GuestsRoomsPicker/GuestsRoomsPicker.tsx
Normal file
113
components/GuestsRoomsPicker/GuestsRoomsPicker.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
"use client"
|
||||||
|
import { useFormContext } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
||||||
|
|
||||||
|
import { CloseLargeIcon, PlusCircleIcon } from "../Icons"
|
||||||
|
import Button from "../TempDesignSystem/Button"
|
||||||
|
import Divider from "../TempDesignSystem/Divider"
|
||||||
|
import Subtitle from "../TempDesignSystem/Text/Subtitle"
|
||||||
|
import { Tooltip } from "../TempDesignSystem/Tooltip"
|
||||||
|
import AdultSelector from "./AdultSelector"
|
||||||
|
import ChildSelector from "./ChildSelector"
|
||||||
|
|
||||||
|
import styles from "./guests-rooms-picker.module.css"
|
||||||
|
|
||||||
|
import { BookingWidgetSchema } from "@/types/components/bookingWidget"
|
||||||
|
import { GuestsRoomsPickerProps } from "@/types/components/bookingWidget/guestsRoomsPicker"
|
||||||
|
|
||||||
|
export default function GuestsRoomsPicker({
|
||||||
|
closePicker,
|
||||||
|
}: GuestsRoomsPickerProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const doneLabel = intl.formatMessage({ id: "Done" })
|
||||||
|
const roomLabel = intl.formatMessage({ id: "Room" })
|
||||||
|
const disabledBookingOptionsHeader = intl.formatMessage({
|
||||||
|
id: "Disabled booking options header",
|
||||||
|
})
|
||||||
|
const disabledBookingOptionsText = intl.formatMessage({
|
||||||
|
id: "Disabled booking options text",
|
||||||
|
})
|
||||||
|
const addRoomLabel = intl.formatMessage({ id: "Add Room" })
|
||||||
|
|
||||||
|
const { getFieldState } = useFormContext<BookingWidgetSchema>()
|
||||||
|
|
||||||
|
const rooms = useGuestsRoomsStore((state) => state.rooms)
|
||||||
|
|
||||||
|
// Not in MVP
|
||||||
|
// const increaseRoom = useGuestsRoomsStore.use.increaseRoom()
|
||||||
|
// const decreaseRoom = useGuestsRoomsStore.use.decreaseRoom()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.pickerContainer}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<button type="button" className={styles.close} onClick={closePicker}>
|
||||||
|
<CloseLargeIcon />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div className={styles.contentContainer}>
|
||||||
|
{rooms.map((room, index) => (
|
||||||
|
<div className={styles.roomContainer} key={index}>
|
||||||
|
<section className={styles.roomDetailsContainer}>
|
||||||
|
<Subtitle type="two" className={styles.roomHeading}>
|
||||||
|
{roomLabel} {index + 1}
|
||||||
|
</Subtitle>
|
||||||
|
<AdultSelector roomIndex={index} />
|
||||||
|
<ChildSelector roomIndex={index} />
|
||||||
|
</section>
|
||||||
|
{/* Not in MVP
|
||||||
|
{index > 0 ? (
|
||||||
|
<Button intent="text" onClick={() => decreaseRoom(index)}>
|
||||||
|
Remove Room
|
||||||
|
</Button>
|
||||||
|
) : null} */}
|
||||||
|
<Divider color="primaryLightSubtle" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<Tooltip
|
||||||
|
heading={disabledBookingOptionsHeader}
|
||||||
|
text={disabledBookingOptionsText}
|
||||||
|
position="bottom"
|
||||||
|
arrow="left"
|
||||||
|
>
|
||||||
|
{rooms.length < 4 ? (
|
||||||
|
<Button
|
||||||
|
intent="text"
|
||||||
|
variant="icon"
|
||||||
|
wrapping
|
||||||
|
disabled
|
||||||
|
theme="base"
|
||||||
|
className={styles.addRoom}
|
||||||
|
>
|
||||||
|
<PlusCircleIcon />
|
||||||
|
{addRoomLabel}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Tooltip>
|
||||||
|
<Button
|
||||||
|
onClick={closePicker}
|
||||||
|
disabled={getFieldState("rooms").invalid}
|
||||||
|
className={styles.hideOnMobile}
|
||||||
|
intent="tertiary"
|
||||||
|
theme="base"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{doneLabel}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={closePicker}
|
||||||
|
disabled={getFieldState("rooms").invalid}
|
||||||
|
className={styles.hideOnDesktop}
|
||||||
|
intent="tertiary"
|
||||||
|
theme="base"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{doneLabel}
|
||||||
|
</Button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
141
components/GuestsRoomsPicker/guests-rooms-picker.module.css
Normal file
141
components/GuestsRoomsPicker/guests-rooms-picker.module.css
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
.container {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
&[data-isopen="true"] {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.roomContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
.roomDetailsContainer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
padding-bottom: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
.hideWrapper {
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
}
|
||||||
|
.roomHeading {
|
||||||
|
margin-bottom: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
margin-top: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1366px) {
|
||||||
|
.hideWrapper {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 100%;
|
||||||
|
transition: top 300ms ease;
|
||||||
|
z-index: 10002;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container[data-isopen="true"] .hideWrapper {
|
||||||
|
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickerContainer {
|
||||||
|
--header-height: 72px;
|
||||||
|
--sticky-button-height: 140px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"header"
|
||||||
|
"content";
|
||||||
|
grid-template-rows: var(--header-height) calc(100dvh - var(--header-height));
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.contentContainer {
|
||||||
|
grid-area: content;
|
||||||
|
overflow-y: scroll;
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
display: grid;
|
||||||
|
grid-area: header;
|
||||||
|
padding: var(--Spacing-x3) var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-self: flex-end;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomContainer {
|
||||||
|
padding: 0 var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
.roomContainer:last-of-type {
|
||||||
|
padding-bottom: calc(var(--sticky-button-height) + 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(255, 255, 255, 0) 7.5%,
|
||||||
|
#ffffff 82.5%
|
||||||
|
);
|
||||||
|
padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x7);
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer .hideOnMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer .addRoom {
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.hideWrapper {
|
||||||
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
left: calc((var(--Spacing-x1) + var(--Spacing-x2)) * -1);
|
||||||
|
max-width: calc(100vw - 20px);
|
||||||
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4));
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer .hideOnDesktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
components/GuestsRoomsPicker/index.tsx
Normal file
80
components/GuestsRoomsPicker/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useGuestsRoomsStore } from "@/stores/guests-rooms"
|
||||||
|
|
||||||
|
import { guestRoomsSchema } from "@/components/Forms/BookingWidget/schema"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
|
import GuestsRoomsPicker from "./GuestsRoomsPicker"
|
||||||
|
|
||||||
|
import styles from "./guests-rooms-picker.module.css"
|
||||||
|
|
||||||
|
export default function GuestsRoomsPickerForm() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore(
|
||||||
|
(state) => ({
|
||||||
|
rooms: state.rooms,
|
||||||
|
adultCount: state.adultCount,
|
||||||
|
childCount: state.childCount,
|
||||||
|
setIsValidated: state.setIsValidated,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null)
|
||||||
|
function handleOnClick() {
|
||||||
|
setIsOpen((prevIsOpen) => !prevIsOpen)
|
||||||
|
}
|
||||||
|
const closePicker = useCallback(() => {
|
||||||
|
const guestRoomsValidData = guestRoomsSchema.safeParse(rooms)
|
||||||
|
if (guestRoomsValidData.success) {
|
||||||
|
setIsOpen(false)
|
||||||
|
setIsValidated(false)
|
||||||
|
} else {
|
||||||
|
setIsValidated(true)
|
||||||
|
}
|
||||||
|
}, [rooms, setIsValidated, setIsOpen])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(evt: Event) {
|
||||||
|
const target = evt.target as HTMLElement
|
||||||
|
if (ref.current && target && !ref.current.contains(target)) {
|
||||||
|
closePicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("click", handleClickOutside)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("click", handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [closePicker])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container} data-isopen={isOpen} ref={ref}>
|
||||||
|
<button className={styles.btn} onClick={handleOnClick} type="button">
|
||||||
|
<Body className={styles.body}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "booking.rooms" },
|
||||||
|
{ totalRooms: rooms.length }
|
||||||
|
)}
|
||||||
|
{", "}
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "booking.adults" },
|
||||||
|
{ totalAdults: adultCount }
|
||||||
|
)}
|
||||||
|
{childCount > 0
|
||||||
|
? ", " +
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "booking.children" },
|
||||||
|
{ totalChildren: childCount }
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</Body>
|
||||||
|
</button>
|
||||||
|
<div aria-modal className={styles.hideWrapper} role="dialog">
|
||||||
|
<GuestsRoomsPicker closePicker={closePicker} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ecoLabel {
|
.ecoLabel {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: auto 1fr;
|
align-items: center;
|
||||||
column-gap: var(--Spacing-x-one-and-half);
|
column-gap: var(--Spacing-x-one-and-half);
|
||||||
grid-column: 2 / 3;
|
grid-column: 2 / 3;
|
||||||
grid-row: 3 / 4;
|
grid-row: 3 / 4;
|
||||||
@@ -24,20 +24,26 @@ export default function Contact({ hotel }: ContactProps) {
|
|||||||
<span className={styles.heading}>
|
<span className={styles.heading}>
|
||||||
{intl.formatMessage({ id: "Address" })}
|
{intl.formatMessage({ id: "Address" })}
|
||||||
</span>
|
</span>
|
||||||
<span>{hotel.address.streetAddress}</span>
|
<span>
|
||||||
<span>{hotel.address.city}</span>
|
{`${hotel.address.streetAddress}, ${hotel.address.city}`}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className={styles.heading}>
|
<span className={styles.heading}>
|
||||||
{intl.formatMessage({ id: "Driving directions" })}
|
{intl.formatMessage({ id: "Driving directions" })}
|
||||||
</span>
|
</span>
|
||||||
<Link href="#">{intl.formatMessage({ id: "Google Maps" })}</Link>
|
<Link href="#" color="peach80">
|
||||||
|
Google Maps
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className={styles.heading}>
|
<span className={styles.heading}>
|
||||||
{intl.formatMessage({ id: "Email" })}
|
{intl.formatMessage({ id: "Email" })}
|
||||||
</span>
|
</span>
|
||||||
<Link href={`mailto:${hotel.contactInformation.email}`}>
|
<Link
|
||||||
|
href={`mailto:${hotel.contactInformation.email}`}
|
||||||
|
color="peach80"
|
||||||
|
>
|
||||||
{hotel.contactInformation.email}
|
{hotel.contactInformation.email}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -45,7 +51,10 @@ export default function Contact({ hotel }: ContactProps) {
|
|||||||
<span className={styles.heading}>
|
<span className={styles.heading}>
|
||||||
{intl.formatMessage({ id: "Contact us" })}
|
{intl.formatMessage({ id: "Contact us" })}
|
||||||
</span>
|
</span>
|
||||||
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
|
<Link
|
||||||
|
href={`tel:${hotel.contactInformation.phoneNumber}`}
|
||||||
|
color="peach80"
|
||||||
|
>
|
||||||
{hotel.contactInformation.phoneNumber}
|
{hotel.contactInformation.phoneNumber}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useWatch } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { privacyPolicy } from "@/constants/currentWebHrefs"
|
||||||
|
|
||||||
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
|
import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox"
|
||||||
|
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||||
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import styles from "./signup.module.css"
|
||||||
|
|
||||||
|
export default function Signup({ name }: { name: string }) {
|
||||||
|
const lang = useLang()
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const [isJoinChecked, setIsJoinChecked] = useState(false)
|
||||||
|
|
||||||
|
const joinValue = useWatch({ name })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// In order to avoid hydration errors the state needs to be set as side effect,
|
||||||
|
// since the join value can come from search params
|
||||||
|
setIsJoinChecked(joinValue)
|
||||||
|
}, [joinValue])
|
||||||
|
|
||||||
|
const list = [
|
||||||
|
{ title: intl.formatMessage({ id: "Earn bonus nights & points" }) },
|
||||||
|
{ title: intl.formatMessage({ id: "Get member benefits & offers" }) },
|
||||||
|
{ title: intl.formatMessage({ id: "Join at no cost" }) },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<CheckboxCard
|
||||||
|
highlightSubtitle
|
||||||
|
list={list}
|
||||||
|
name={name}
|
||||||
|
subtitle={intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "{difference}{amount} {currency}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
amount: "491",
|
||||||
|
currency: "SEK",
|
||||||
|
difference: "-",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
title={intl.formatMessage({ id: "Join Scandic Friends" })}
|
||||||
|
/>
|
||||||
|
{isJoinChecked ? (
|
||||||
|
<div className={styles.additionalFormData}>
|
||||||
|
<div className={styles.dateField}>
|
||||||
|
<header>
|
||||||
|
<Caption type="bold">
|
||||||
|
{intl.formatMessage({ id: "Birth date" })} *
|
||||||
|
</Caption>
|
||||||
|
</header>
|
||||||
|
<DateSelect
|
||||||
|
name="dateOfBirth"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="zipCode"
|
||||||
|
label={intl.formatMessage({ id: "Zip code" })}
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Checkbox name="termsAccepted" registerOptions={{ required: true }}>
|
||||||
|
<Body>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with",
|
||||||
|
})}{" "}
|
||||||
|
<Link
|
||||||
|
variant="underscored"
|
||||||
|
color="peach80"
|
||||||
|
target="_blank"
|
||||||
|
href={privacyPolicy[lang]}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Scandic's Privacy Policy." })}
|
||||||
|
</Link>
|
||||||
|
</Body>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-column: 1/-1;
|
||||||
|
gap: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.additionalFormData {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateField {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
@@ -6,14 +6,16 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import { registerUserBookingFlow } from "@/actions/registerUserBookingFlow"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox"
|
|
||||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
|
||||||
import { detailsSchema, signedInDetailsSchema } from "./schema"
|
import { detailsSchema, signedInDetailsSchema } from "./schema"
|
||||||
|
import Signup from "./Signup"
|
||||||
|
|
||||||
import styles from "./details.module.css"
|
import styles from "./details.module.css"
|
||||||
|
|
||||||
@@ -25,28 +27,30 @@ import type {
|
|||||||
const formID = "enter-details"
|
const formID = "enter-details"
|
||||||
export default function Details({ user }: DetailsProps) {
|
export default function Details({ user }: DetailsProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const list = [
|
|
||||||
{ title: intl.formatMessage({ id: "Earn bonus nights & points" }) },
|
|
||||||
{ title: intl.formatMessage({ id: "Get member benefits & offers" }) },
|
|
||||||
{ title: intl.formatMessage({ id: "Join at no cost" }) },
|
|
||||||
]
|
|
||||||
|
|
||||||
const initialData = useEnterDetailsStore((state) => ({
|
const initialData = useEnterDetailsStore((state) => ({
|
||||||
countryCode: state.data.countryCode,
|
countryCode: state.data.countryCode,
|
||||||
email: state.data.email,
|
email: state.data.email,
|
||||||
firstname: state.data.firstname,
|
firstName: state.data.firstName,
|
||||||
lastname: state.data.lastname,
|
lastName: state.data.lastName,
|
||||||
phoneNumber: state.data.phoneNumber,
|
phoneNumber: state.data.phoneNumber,
|
||||||
|
join: state.data.join,
|
||||||
|
dateOfBirth: state.data.dateOfBirth,
|
||||||
|
zipCode: state.data.zipCode,
|
||||||
|
termsAccepted: state.data.termsAccepted,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const methods = useForm<DetailsSchema>({
|
const methods = useForm<DetailsSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
countryCode: user?.address?.countryCode ?? initialData.countryCode,
|
countryCode: user?.address?.countryCode ?? initialData.countryCode,
|
||||||
email: user?.email ?? initialData.email,
|
email: user?.email ?? initialData.email,
|
||||||
firstname: user?.firstName ?? initialData.firstname,
|
firstName: user?.firstName ?? initialData.firstName,
|
||||||
lastname: user?.lastName ?? initialData.lastname,
|
lastName: user?.lastName ?? initialData.lastName,
|
||||||
phoneNumber: user?.phoneNumber ?? initialData.phoneNumber,
|
phoneNumber: user?.phoneNumber ?? initialData.phoneNumber,
|
||||||
|
//@ts-expect-error: We use a literal for join to be true or false, which does not convert to a boolean
|
||||||
|
join: initialData.join,
|
||||||
|
dateOfBirth: initialData.dateOfBirth,
|
||||||
|
zipCode: initialData.zipCode,
|
||||||
|
termsAccepted: initialData.termsAccepted,
|
||||||
},
|
},
|
||||||
criteriaMode: "all",
|
criteriaMode: "all",
|
||||||
mode: "all",
|
mode: "all",
|
||||||
@@ -56,10 +60,39 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
|
|
||||||
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
||||||
|
|
||||||
|
// const errorMessage = intl.formatMessage({
|
||||||
|
// id: "An error occurred. Please try again.",
|
||||||
|
// })
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(values: DetailsSchema) => {
|
async function (values: DetailsSchema) {
|
||||||
|
if (values.join) {
|
||||||
|
const signupVals = {
|
||||||
|
firstName: values.firstName,
|
||||||
|
lastName: values.lastName,
|
||||||
|
email: values.email,
|
||||||
|
phoneNumber: values.phoneNumber,
|
||||||
|
address: {
|
||||||
|
zipCode: values.zipCode,
|
||||||
|
countryCode: values.countryCode,
|
||||||
|
},
|
||||||
|
dateOfBirth: values.dateOfBirth,
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await registerUserBookingFlow(signupVals)
|
||||||
|
if (!res.success) {
|
||||||
|
// if (res.error) {
|
||||||
|
// toast.error(res.error)
|
||||||
|
// } else {
|
||||||
|
// toast.error(errorMessage)
|
||||||
|
// }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("Signed up user: ", res)
|
||||||
|
}
|
||||||
completeStep(values)
|
completeStep(values)
|
||||||
},
|
},
|
||||||
|
|
||||||
[completeStep]
|
[completeStep]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,14 +110,14 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
onSubmit={methods.handleSubmit(onSubmit)}
|
onSubmit={methods.handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
label={intl.formatMessage({ id: "Firstname" })}
|
label={intl.formatMessage({ id: "First name" })}
|
||||||
name="firstname"
|
name="firstName"
|
||||||
readOnly={!!user}
|
readOnly={!!user}
|
||||||
registerOptions={{ required: true }}
|
registerOptions={{ required: true }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={intl.formatMessage({ id: "Lastname" })}
|
label={intl.formatMessage({ id: "Last name" })}
|
||||||
name="lastname"
|
name="lastName"
|
||||||
readOnly={!!user}
|
readOnly={!!user}
|
||||||
registerOptions={{ required: true }}
|
registerOptions={{ required: true }}
|
||||||
/>
|
/>
|
||||||
@@ -109,26 +142,9 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
readOnly={!!user}
|
readOnly={!!user}
|
||||||
registerOptions={{ required: true }}
|
registerOptions={{ required: true }}
|
||||||
/>
|
/>
|
||||||
|
{user ? null : <Signup name="join" />}
|
||||||
</form>
|
</form>
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
{user ? null : (
|
|
||||||
<CheckboxCard
|
|
||||||
highlightSubtitle
|
|
||||||
list={list}
|
|
||||||
name="join"
|
|
||||||
subtitle={intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: "{difference}{amount} {currency}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
amount: "491",
|
|
||||||
currency: "SEK",
|
|
||||||
difference: "-",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
title={intl.formatMessage({ id: "Join Scandic Friends" })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
disabled={!methods.formState.isValid}
|
disabled={!methods.formState.isValid}
|
||||||
form={formID}
|
form={formID}
|
||||||
|
|||||||
@@ -2,18 +2,49 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { phoneValidator } from "@/utils/phoneValidator"
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
|
|
||||||
export const detailsSchema = z.object({
|
export const baseDetailsSchema = z.object({
|
||||||
countryCode: z.string(),
|
countryCode: z.string(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
firstname: z.string(),
|
firstName: z.string(),
|
||||||
lastname: z.string(),
|
lastName: z.string(),
|
||||||
phoneNumber: phoneValidator(),
|
phoneNumber: phoneValidator(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const notJoinDetailsSchema = baseDetailsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
join: z.literal(false),
|
||||||
|
zipCode: z.string().optional(),
|
||||||
|
dateOfBirth: z.string().optional(),
|
||||||
|
termsAccepted: z.boolean().default(false),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const joinDetailsSchema = baseDetailsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
join: z.literal(true),
|
||||||
|
zipCode: z.string().min(1, { message: "Zip code is required" }),
|
||||||
|
dateOfBirth: z.string(),
|
||||||
|
termsAccepted: z.literal(true, {
|
||||||
|
errorMap: (err, ctx) => {
|
||||||
|
switch (err.code) {
|
||||||
|
case "invalid_literal":
|
||||||
|
return { message: "You must accept the terms and conditions" }
|
||||||
|
}
|
||||||
|
return { message: ctx.defaultError }
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const detailsSchema = z.discriminatedUnion("join", [
|
||||||
|
notJoinDetailsSchema,
|
||||||
|
joinDetailsSchema,
|
||||||
|
])
|
||||||
|
|
||||||
export const signedInDetailsSchema = z.object({
|
export const signedInDetailsSchema = z.object({
|
||||||
countryCode: z.string().optional(),
|
countryCode: z.string().optional(),
|
||||||
email: z.string().email().optional(),
|
email: z.string().email().optional(),
|
||||||
firstname: z.string().optional(),
|
firstName: z.string().optional(),
|
||||||
lastname: z.string().optional(),
|
lastName: z.string().optional(),
|
||||||
phoneNumber: phoneValidator().optional(),
|
phoneNumber: phoneValidator().optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,54 +20,30 @@ export default function SectionAccordion({
|
|||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<SectionAccordionProps>) {
|
}: React.PropsWithChildren<SectionAccordionProps>) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const [isComplete, setIsComplete] = useState(false)
|
|
||||||
const currentStep = useEnterDetailsStore((state) => state.currentStep)
|
const currentStep = useEnterDetailsStore((state) => state.currentStep)
|
||||||
|
const [isComplete, setIsComplete] = useState(false)
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
const isValid = useEnterDetailsStore((state) => state.isValid[step])
|
const isValid = useEnterDetailsStore((state) => state.isValid[step])
|
||||||
|
|
||||||
const navigate = useEnterDetailsStore((state) => state.navigate)
|
const navigate = useEnterDetailsStore((state) => state.navigate)
|
||||||
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
|
||||||
const circleRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const isOpen = currentStep === step
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const content = contentRef.current
|
|
||||||
const circle = circleRef.current
|
|
||||||
if (content) {
|
|
||||||
if (isOpen) {
|
|
||||||
content.style.maxHeight = `${content.scrollHeight}px`
|
|
||||||
} else {
|
|
||||||
content.style.maxHeight = "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (circle) {
|
|
||||||
if (isOpen) {
|
|
||||||
circle.style.backgroundColor = `var(--UI-Text-Placeholder);`
|
|
||||||
} else {
|
|
||||||
circle.style.backgroundColor = `var(--Base-Surface-Subtle-Hover);`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// We need to set the state on mount because of hydration errors
|
// We need to set the state on mount because of hydration errors
|
||||||
setIsComplete(isValid)
|
setIsComplete(isValid)
|
||||||
}, [isValid])
|
}, [isValid])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOpen(currentStep === step)
|
||||||
|
}, [currentStep, step])
|
||||||
|
|
||||||
function onModify() {
|
function onModify() {
|
||||||
navigate(step)
|
navigate(step)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.wrapper} data-open={isOpen}>
|
<section className={styles.wrapper} data-open={isOpen} data-step={step}>
|
||||||
<div className={styles.iconWrapper}>
|
<div className={styles.iconWrapper}>
|
||||||
<div
|
<div className={styles.circle} data-checked={isComplete}>
|
||||||
className={styles.circle}
|
|
||||||
data-checked={isComplete}
|
|
||||||
ref={circleRef}
|
|
||||||
>
|
|
||||||
{isComplete ? (
|
{isComplete ? (
|
||||||
<CheckIcon color="white" height="16" width="16" />
|
<CheckIcon color="white" height="16" width="16" />
|
||||||
) : null}
|
) : null}
|
||||||
@@ -79,6 +55,7 @@ export default function SectionAccordion({
|
|||||||
<Footnote
|
<Footnote
|
||||||
asChild
|
asChild
|
||||||
textTransform="uppercase"
|
textTransform="uppercase"
|
||||||
|
type="label"
|
||||||
color="uiTextPlaceholder"
|
color="uiTextPlaceholder"
|
||||||
>
|
>
|
||||||
<h2>{header}</h2>
|
<h2>{header}</h2>
|
||||||
@@ -105,9 +82,7 @@ export default function SectionAccordion({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.content} ref={contentRef}>
|
<div className={styles.content}>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,12 +22,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x3);
|
gap: var(--Spacing-x3);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
padding-bottom: var(--Spacing-x3);
|
padding-bottom: var(--Spacing-x3);
|
||||||
|
|
||||||
|
transition: 0.4s ease-out;
|
||||||
|
grid-template-rows: 2em 0fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerContainer {
|
.headerContainer {
|
||||||
@@ -70,12 +72,23 @@
|
|||||||
background-color: var(--Base-Surface-Subtle-Hover);
|
background-color: var(--Base-Surface-Subtle-Hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper[data-open="true"] .main {
|
||||||
|
grid-template-rows: 2em 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height 0.4s ease-out;
|
}
|
||||||
max-height: 0;
|
|
||||||
|
@keyframes allowOverflow {
|
||||||
|
0% {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper[data-open="true"] .content {
|
.wrapper[data-open="true"] .content {
|
||||||
max-height: 1000px;
|
animation: allowOverflow 0.4s 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.spacing {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
42
components/HotelReservation/EnterDetails/SidePeek/index.tsx
Normal file
42
components/HotelReservation/EnterDetails/SidePeek/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import Contact from "@/components/HotelReservation/Contact"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
|
import styles from "./enterDetailsSidePeek.module.css"
|
||||||
|
|
||||||
|
import {
|
||||||
|
SidePeekEnum,
|
||||||
|
SidePeekProps,
|
||||||
|
} from "@/types/components/enterDetails/sidePeek"
|
||||||
|
|
||||||
|
export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) {
|
||||||
|
const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek)
|
||||||
|
const close = useEnterDetailsStore((state) => state.closeSidePeek)
|
||||||
|
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<SidePeek
|
||||||
|
contentKey={SidePeekEnum.hotelDetails}
|
||||||
|
title={intl.formatMessage({ id: "About the hotel" })}
|
||||||
|
isOpen={activeSidePeek === SidePeekEnum.hotelDetails}
|
||||||
|
handleClose={close}
|
||||||
|
>
|
||||||
|
<article className={styles.spacing}>
|
||||||
|
<Contact hotel={hotel} />
|
||||||
|
<Divider />
|
||||||
|
<section className={styles.spacing}>
|
||||||
|
<Body>{hotel.hotelContent.texts.descriptions.medium}</Body>
|
||||||
|
|
||||||
|
<Body>{hotel.hotelContent.texts.facilityInformation}</Body>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</SidePeek>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
|
import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek"
|
||||||
|
|
||||||
|
export default function ToggleSidePeek() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const openSidePeek = useEnterDetailsStore((state) => state.openSidePeek)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
openSidePeek(SidePeekEnum.hotelDetails)
|
||||||
|
}}
|
||||||
|
theme="base"
|
||||||
|
size="small"
|
||||||
|
variant="icon"
|
||||||
|
intent="text"
|
||||||
|
wrapping
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "See room details" })}{" "}
|
||||||
|
<ChevronRightSmallIcon
|
||||||
|
color="baseButtonTextOnFillNormal"
|
||||||
|
height={20}
|
||||||
|
width={20}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import { ArrowRightIcon, ChevronRightSmallIcon } from "@/components/Icons"
|
import { ArrowRightIcon } from "@/components/Icons"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import ToggleSidePeek from "./ToggleSidePeek"
|
||||||
|
|
||||||
import styles from "./summary.module.css"
|
import styles from "./summary.module.css"
|
||||||
|
|
||||||
// TEMP
|
// TEMP
|
||||||
@@ -21,7 +22,6 @@ const rooms = [
|
|||||||
export default async function Summary() {
|
export default async function Summary() {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = getLang()
|
const lang = getLang()
|
||||||
|
|
||||||
const fromDate = dt().locale(lang).format("ddd, D MMM")
|
const fromDate = dt().locale(lang).format("ddd, D MMM")
|
||||||
const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM")
|
const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM")
|
||||||
const diff = dt(toDate).diff(fromDate, "days")
|
const diff = dt(toDate).diff(fromDate, "days")
|
||||||
@@ -75,19 +75,8 @@ export default async function Summary() {
|
|||||||
<ArrowRightIcon color="uiTextMediumContrast" height={15} width={15} />
|
<ArrowRightIcon color="uiTextMediumContrast" height={15} width={15} />
|
||||||
{toDate}
|
{toDate}
|
||||||
</Body>
|
</Body>
|
||||||
<Link
|
|
||||||
className={styles.link}
|
<ToggleSidePeek />
|
||||||
color="baseButtonTextOnFillNormal"
|
|
||||||
href="#"
|
|
||||||
variant="icon"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "See room details" })}
|
|
||||||
<ChevronRightSmallIcon
|
|
||||||
color="baseButtonTextOnFillNormal"
|
|
||||||
height={20}
|
|
||||||
width={20}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</header>
|
</header>
|
||||||
<Divider color="primaryLightSubtle" />
|
<Divider color="primaryLightSubtle" />
|
||||||
<div className={styles.addOns}>
|
<div className={styles.addOns}>
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Spacing-x3);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
|
||||||
|
|
||||||
import styles from "./hotelDetailSidePeek.module.css"
|
|
||||||
|
|
||||||
export default function HotelDetailSidePeek() {
|
|
||||||
const intl = useIntl()
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
|
|
||||||
function toggleSidePeek() {
|
|
||||||
setIsOpen(!isOpen)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.buttons}>
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
theme="base"
|
|
||||||
intent="text"
|
|
||||||
wrapping
|
|
||||||
onClick={toggleSidePeek}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "See hotel details",
|
|
||||||
})}
|
|
||||||
<ChevronRightSmallIcon aria-hidden="true" color="burgundy" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
theme="base"
|
|
||||||
intent="text"
|
|
||||||
wrapping
|
|
||||||
onClick={toggleSidePeek}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "Show all amenities",
|
|
||||||
})}
|
|
||||||
<ChevronRightSmallIcon aria-hidden="true" color="burgundy" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<SidePeek
|
|
||||||
contentKey="hotel-detail-side-peek"
|
|
||||||
title="Hotel Details"
|
|
||||||
isOpen={isOpen}
|
|
||||||
handleClose={() => setIsOpen(false)}
|
|
||||||
>
|
|
||||||
<div>TBD</div>
|
|
||||||
</SidePeek>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,6 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
|||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
|
||||||
import HotelDetailSidePeek from "./HotelDetailSidePeek"
|
|
||||||
|
|
||||||
import styles from "./hotelSelectionHeader.module.css"
|
import styles from "./hotelSelectionHeader.module.css"
|
||||||
|
|
||||||
import { HotelSelectionHeaderProps } from "@/types/components/hotelReservation/selectRate/hotelSelectionHeader"
|
import { HotelSelectionHeaderProps } from "@/types/components/hotelReservation/selectRate/hotelSelectionHeader"
|
||||||
@@ -46,7 +44,6 @@ export default function HotelSelectionHeader({
|
|||||||
<Body color="textHighContrast">
|
<Body color="textHighContrast">
|
||||||
{hotel.hotelContent.texts.descriptions.short}
|
{hotel.hotelContent.texts.descriptions.short}
|
||||||
</Body>
|
</Body>
|
||||||
<HotelDetailSidePeek />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
import Contact from "./Contact"
|
import Contact from "../Contact"
|
||||||
|
|
||||||
import styles from "./readMore.module.css"
|
import styles from "./readMore.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function PriceList({
|
|||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<dt>
|
<dt>
|
||||||
<Caption
|
<Caption
|
||||||
textTransform="bold"
|
type="bold"
|
||||||
color={publicLocalPrice ? "uiTextHighContrast" : "disabled"}
|
color={publicLocalPrice ? "uiTextHighContrast" : "disabled"}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({ id: "Standard price" })}
|
{intl.formatMessage({ id: "Standard price" })}
|
||||||
@@ -52,10 +52,7 @@ export default function PriceList({
|
|||||||
|
|
||||||
<div className={styles.priceRow}>
|
<div className={styles.priceRow}>
|
||||||
<dt>
|
<dt>
|
||||||
<Caption
|
<Caption type="bold" color={memberLocalPrice ? "red" : "disabled"}>
|
||||||
textTransform="bold"
|
|
||||||
color={memberLocalPrice ? "red" : "disabled"}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Member price" })}
|
{intl.formatMessage({ id: "Member price" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
</dt>
|
</dt>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export default function FlexibilityOption({
|
|||||||
name,
|
name,
|
||||||
paymentTerm,
|
paymentTerm,
|
||||||
priceInformation,
|
priceInformation,
|
||||||
|
roomType,
|
||||||
|
handleSelectRate,
|
||||||
}: FlexibilityOptionProps) {
|
}: FlexibilityOptionProps) {
|
||||||
const [rootDiv, setRootDiv] = useState<Element | undefined>(undefined)
|
const [rootDiv, setRootDiv] = useState<Element | undefined>(undefined)
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
||||||
@@ -42,9 +44,24 @@ export default function FlexibilityOption({
|
|||||||
|
|
||||||
const { public: publicPrice, member: memberPrice } = product.productType
|
const { public: publicPrice, member: memberPrice } = product.productType
|
||||||
|
|
||||||
|
function onChange() {
|
||||||
|
const rate = {
|
||||||
|
roomType: roomType,
|
||||||
|
priceName: name,
|
||||||
|
public: publicPrice,
|
||||||
|
member: memberPrice,
|
||||||
|
}
|
||||||
|
handleSelectRate(rate)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="rateCode" value={publicPrice?.rateCode} />
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="rateCode"
|
||||||
|
value={publicPrice?.rateCode}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<div className={styles.header} ref={setRef}>
|
<div className={styles.header} ref={setRef}>
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
@@ -73,7 +90,7 @@ export default function FlexibilityOption({
|
|||||||
>
|
>
|
||||||
<Caption
|
<Caption
|
||||||
color="uiTextHighContrast"
|
color="uiTextHighContrast"
|
||||||
textTransform="bold"
|
type="bold"
|
||||||
className={styles.popoverHeading}
|
className={styles.popoverHeading}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
|
import styles from "./rateSummary.module.css"
|
||||||
|
|
||||||
|
import { RateSummaryProps } from "@/types/components/hotelReservation/selectRate/rateSummary"
|
||||||
|
|
||||||
|
export default function RateSummary({
|
||||||
|
rateSummary,
|
||||||
|
isUserLoggedIn,
|
||||||
|
}: RateSummaryProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const priceToShow = isUserLoggedIn ? rateSummary.member : rateSummary.public
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.summary}>
|
||||||
|
<div className={styles.summaryText}>
|
||||||
|
<Subtitle color="uiTextHighContrast">{rateSummary.roomType}</Subtitle>
|
||||||
|
<Body color="uiTextMediumContrast">{rateSummary.priceName}</Body>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryPrice}>
|
||||||
|
<div className={styles.summaryPriceText}>
|
||||||
|
<>
|
||||||
|
<Subtitle color={isUserLoggedIn ? "red" : "uiTextHighContrast"}>
|
||||||
|
{priceToShow?.localPrice.pricePerStay}{" "}
|
||||||
|
{priceToShow?.localPrice.currency}
|
||||||
|
</Subtitle>
|
||||||
|
<Body color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||||
|
{priceToShow?.requestedPrice?.pricePerStay}{" "}
|
||||||
|
{priceToShow?.requestedPrice?.currency}
|
||||||
|
</Body>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" theme="base">
|
||||||
|
{intl.formatMessage({ id: "Continue" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.summary {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryPrice {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x4);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { RateDefinition } from "@/server/routers/hotels/output"
|
import { RateDefinition } from "@/server/routers/hotels/output"
|
||||||
@@ -21,6 +22,7 @@ export default function RoomCard({
|
|||||||
rateDefinitions,
|
rateDefinitions,
|
||||||
roomConfiguration,
|
roomConfiguration,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
|
handleSelectRate,
|
||||||
}: RoomCardProps) {
|
}: RoomCardProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
@@ -112,6 +114,8 @@ export default function RoomCard({
|
|||||||
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
||||||
product={findProductForRate(saveRate)}
|
product={findProductForRate(saveRate)}
|
||||||
priceInformation={getPriceForRate(saveRate)}
|
priceInformation={getPriceForRate(saveRate)}
|
||||||
|
handleSelectRate={handleSelectRate}
|
||||||
|
roomType={roomConfiguration.roomType}
|
||||||
/>
|
/>
|
||||||
<FlexibilityOption
|
<FlexibilityOption
|
||||||
name={intl.formatMessage({ id: "Free rebooking" })}
|
name={intl.formatMessage({ id: "Free rebooking" })}
|
||||||
@@ -119,6 +123,8 @@ export default function RoomCard({
|
|||||||
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
paymentTerm={intl.formatMessage({ id: "Pay now" })}
|
||||||
product={findProductForRate(changeRate)}
|
product={findProductForRate(changeRate)}
|
||||||
priceInformation={getPriceForRate(changeRate)}
|
priceInformation={getPriceForRate(changeRate)}
|
||||||
|
handleSelectRate={handleSelectRate}
|
||||||
|
roomType={roomConfiguration.roomType}
|
||||||
/>
|
/>
|
||||||
<FlexibilityOption
|
<FlexibilityOption
|
||||||
name={intl.formatMessage({ id: "Free cancellation" })}
|
name={intl.formatMessage({ id: "Free cancellation" })}
|
||||||
@@ -126,6 +132,8 @@ export default function RoomCard({
|
|||||||
paymentTerm={intl.formatMessage({ id: "Pay later" })}
|
paymentTerm={intl.formatMessage({ id: "Pay later" })}
|
||||||
product={findProductForRate(flexRate)}
|
product={findProductForRate(flexRate)}
|
||||||
priceInformation={getPriceForRate(flexRate)}
|
priceInformation={getPriceForRate(flexRate)}
|
||||||
|
handleSelectRate={handleSelectRate}
|
||||||
|
roomType={roomConfiguration.roomType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import RateSummary from "./RateSummary"
|
||||||
import RoomCard from "./RoomCard"
|
import RoomCard from "./RoomCard"
|
||||||
|
|
||||||
import styles from "./roomSelection.module.css"
|
import styles from "./roomSelection.module.css"
|
||||||
|
|
||||||
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
|
import { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
|
||||||
export default function RoomSelection({
|
export default function RoomSelection({
|
||||||
roomConfigurations,
|
roomConfigurations,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
|
user,
|
||||||
}: RoomSelectionProps) {
|
}: RoomSelectionProps) {
|
||||||
|
const [rateSummary, setRateSummary] = useState<Rate | null>(null)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
@@ -36,16 +42,14 @@ export default function RoomSelection({
|
|||||||
rateDefinitions={roomConfigurations.rateDefinitions}
|
rateDefinitions={roomConfigurations.rateDefinitions}
|
||||||
roomConfiguration={roomConfiguration}
|
roomConfiguration={roomConfiguration}
|
||||||
roomCategories={roomCategories}
|
roomCategories={roomCategories}
|
||||||
|
handleSelectRate={setRateSummary}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{/* <div className={styles.summary}>
|
{rateSummary && (
|
||||||
This is summary
|
<RateSummary rateSummary={rateSummary} isUserLoggedIn={!!user} />
|
||||||
<Button type="submit" size="small" theme="primaryDark">
|
)}
|
||||||
{intl.formatMessage({ id: "Choose room" })}
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,15 +20,6 @@
|
|||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: white;
|
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 767px) {
|
@media (min-width: 767px) {
|
||||||
.roomList {
|
.roomList {
|
||||||
grid-template-columns: repeat(3, minmax(240px, 1fr));
|
grid-template-columns: repeat(3, minmax(240px, 1fr));
|
||||||
|
|||||||
@@ -6,13 +6,6 @@
|
|||||||
padding: var(--Spacing-x1);
|
padding: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordionItem.light {
|
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
|
||||||
}
|
|
||||||
.accordionItem.subtle {
|
|
||||||
background-color: var(--Base-Background-Primary-Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -48,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding: 0 var(--Spacing-x-one-and-half);
|
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
transition: max-height 0.3s;
|
transition: max-height 0.3s;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { VariantProps } from "class-variance-authority"
|
|||||||
|
|
||||||
import { accordionItemVariants } from "./variants"
|
import { accordionItemVariants } from "./variants"
|
||||||
|
|
||||||
import { IconName } from "@/types/components/icon"
|
import type { IconName } from "@/types/components/icon"
|
||||||
|
|
||||||
export interface AccordionItemProps
|
export interface AccordionItemProps
|
||||||
extends React.HtmlHTMLAttributes<HTMLDetailsElement>,
|
extends React.HtmlHTMLAttributes<HTMLDetailsElement>,
|
||||||
|
|||||||
@@ -5,17 +5,19 @@ import { useRef } from "react"
|
|||||||
import { ChevronDownIcon } from "@/components/Icons"
|
import { ChevronDownIcon } from "@/components/Icons"
|
||||||
import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
|
import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
|
||||||
|
|
||||||
import { AccordionItemProps } from "./accordionItem"
|
|
||||||
import { accordionItemVariants } from "./variants"
|
import { accordionItemVariants } from "./variants"
|
||||||
|
|
||||||
import styles from "./accordionItem.module.css"
|
import styles from "./accordionItem.module.css"
|
||||||
|
|
||||||
|
import type { AccordionItemProps } from "./accordionItem"
|
||||||
|
|
||||||
export default function AccordionItem({
|
export default function AccordionItem({
|
||||||
children,
|
children,
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
theme,
|
theme,
|
||||||
variant,
|
variant,
|
||||||
|
className,
|
||||||
}: AccordionItemProps) {
|
}: AccordionItemProps) {
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
const detailsRef = useRef<HTMLDetailsElement>(null)
|
const detailsRef = useRef<HTMLDetailsElement>(null)
|
||||||
@@ -43,7 +45,7 @@ export default function AccordionItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={accordionItemVariants({ variant, theme })}>
|
<li className={accordionItemVariants({ className, variant, theme })}>
|
||||||
<details ref={detailsRef} onToggle={toggleAccordion}>
|
<details ref={detailsRef} onToggle={toggleAccordion}>
|
||||||
<summary className={styles.summary}>
|
<summary className={styles.summary}>
|
||||||
{IconComp && <IconComp className={styles.icon} color="burgundy" />}
|
{IconComp && <IconComp className={styles.icon} color="burgundy" />}
|
||||||
|
|||||||
@@ -12,3 +12,7 @@
|
|||||||
.accordion.subtle {
|
.accordion.subtle {
|
||||||
background-color: var(--Base-Background-Primary-Normal);
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accordion li:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Children, cloneElement, isValidElement } from "react"
|
import { Children, cloneElement, isValidElement } from "react"
|
||||||
|
|
||||||
import { AccordionItemProps } from "./AccordionItem/accordionItem"
|
import { AccordionItemProps } from "./AccordionItem/accordionItem"
|
||||||
import { AccordionProps } from "./accordion"
|
|
||||||
import { accordionVariants } from "./variants"
|
import { accordionVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { AccordionProps } from "./accordion"
|
||||||
|
|
||||||
export default function Accordion({
|
export default function Accordion({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.themeThree {
|
.themeThree {
|
||||||
--font-color: var(--Tertiary-Light-Surface-Text);
|
--font-color: var(--Tertiary-Light-On-Surface-Text);
|
||||||
--script-color: var(--Tertiary-Light-On-Surface-Accent);
|
--script-color: var(--Tertiary-Light-On-Surface-Accent);
|
||||||
|
|
||||||
background: var(--Tertiary-Light-Surface-Normal);
|
background: var(--Tertiary-Light-Surface-Normal);
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.themePrimaryDim {
|
.themePrimaryDim {
|
||||||
--font-color: var(--Primary-Light-On-Surface-Text);
|
--font-color: var(--Primary-Dim-On-Surface-Text);
|
||||||
--script-color: var(--Primary-Dim-On-Surface-Accent);
|
--script-color: var(--Primary-Dim-On-Surface-Accent);
|
||||||
|
|
||||||
background: var(--Primary-Dim-Surface-Normal);
|
background: var(--Primary-Dim-Surface-Normal);
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
background: var(--UI-Input-Controls-Fill-Selected);
|
background: var(--UI-Input-Controls-Fill-Selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container[data-disabled] .checkbox {
|
||||||
|
border: 1px solid var(--UI-Input-Controls-Border-Disabled);
|
||||||
|
background: var(--UI-Input-Controls-Surface-Disabled);
|
||||||
|
}
|
||||||
|
|
||||||
.checkboxContainer {
|
.checkboxContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -20,13 +25,12 @@
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
border: 2px solid var(--UI-Input-Controls-Border-Normal);
|
border: 1px solid var(--UI-Input-Controls-Border-Normal);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: all 200ms;
|
transition: all 200ms;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 200ms;
|
|
||||||
forced-color-adjust: none;
|
forced-color-adjust: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { InfoCircleIcon } from "@/components/Icons"
|
|||||||
import CheckIcon from "@/components/Icons/Check"
|
import CheckIcon from "@/components/Icons/Check"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
import { CheckboxProps } from "./checkbox"
|
|
||||||
|
|
||||||
import styles from "./checkbox.module.css"
|
import styles from "./checkbox.module.css"
|
||||||
|
|
||||||
|
import { CheckboxProps } from "@/types/components/checkbox"
|
||||||
|
|
||||||
export default function Checkbox({
|
export default function Checkbox({
|
||||||
name,
|
name,
|
||||||
children,
|
children,
|
||||||
@@ -29,6 +29,7 @@ export default function Checkbox({
|
|||||||
isSelected={field.value}
|
isSelected={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
data-testid={name}
|
data-testid={name}
|
||||||
|
isDisabled={registerOptions?.disabled}
|
||||||
>
|
>
|
||||||
{({ isSelected }) => (
|
{({ isSelected }) => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ export default function Card({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={styles.label} data-declined={declined}>
|
<label className={styles.label} data-declined={declined}>
|
||||||
<Caption className={styles.title} textTransform="bold" uppercase>
|
<Caption className={styles.title} type="label" uppercase>
|
||||||
{title}
|
{title}
|
||||||
</Caption>
|
</Caption>
|
||||||
{subtitle ? (
|
{subtitle ? (
|
||||||
<Caption
|
<Caption
|
||||||
className={styles.subtitle}
|
className={styles.subtitle}
|
||||||
color={highlightSubtitle ? "baseTextAccent" : "uiTextHighContrast"}
|
color={highlightSubtitle ? "baseTextAccent" : "uiTextHighContrast"}
|
||||||
textTransform="bold"
|
type="regular"
|
||||||
>
|
>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</Caption>
|
</Caption>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import type { DateProps } from "./date"
|
|||||||
|
|
||||||
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const d = useWatch({ name })
|
const currentValue = useWatch({ name })
|
||||||
const { control, setValue } = useFormContext()
|
const { control, setValue, trigger } = useFormContext()
|
||||||
const { field } = useController({
|
const { field } = useController({
|
||||||
control,
|
control,
|
||||||
name,
|
name,
|
||||||
@@ -47,7 +47,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
}))
|
}))
|
||||||
const years = rangeArray(1900, currentYear - 18)
|
const years = rangeArray(1900, currentYear - 18)
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((year) => ({ value: year, label: `${year}` }))
|
.map((year) => ({ value: year, label: year.toString() }))
|
||||||
|
|
||||||
function createOnSelect(selector: DateName) {
|
function createOnSelect(selector: DateName) {
|
||||||
/**
|
/**
|
||||||
@@ -68,6 +68,8 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
const month = selector === DateName.month ? value : newSegments.month
|
const month = selector === DateName.month ? value : newSegments.month
|
||||||
if (year !== null && month !== null) {
|
if (year !== null && month !== null) {
|
||||||
newSegments.daysInMonth = dt().year(year).month(month).daysInMonth()
|
newSegments.daysInMonth = dt().year(year).month(month).daysInMonth()
|
||||||
|
} else if (month !== null) {
|
||||||
|
newSegments.daysInMonth = dt().month(month).daysInMonth()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +81,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
.set("date", Math.min(newSegments.date!, newSegments.daysInMonth))
|
.set("date", Math.min(newSegments.date!, newSegments.daysInMonth))
|
||||||
|
|
||||||
setValue(name, newDate.format("YYYY-MM-DD"))
|
setValue(name, newDate.format("YYYY-MM-DD"))
|
||||||
|
trigger(name)
|
||||||
}
|
}
|
||||||
setDateSegment(newSegments)
|
setDateSegment(newSegments)
|
||||||
}
|
}
|
||||||
@@ -95,9 +98,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
* date, but we can't check isNan since
|
* date, but we can't check isNan since
|
||||||
* we recieve the date as "1999-01-01"
|
* we recieve the date as "1999-01-01"
|
||||||
*/
|
*/
|
||||||
dateValue = dt(d).isValid() ? parseDate(d) : null
|
dateValue = dt(currentValue).isValid() ? parseDate(currentValue) : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.warn("Known error for parse date in DateSelect: ", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -130,9 +133,10 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
placeholder="DD"
|
placeholder="DD"
|
||||||
required
|
required
|
||||||
tabIndex={3}
|
tabIndex={3}
|
||||||
defaultValue={
|
defaultSelectedKey={
|
||||||
segment.isPlaceholder ? undefined : segment.value
|
segment.isPlaceholder ? undefined : segment.value
|
||||||
}
|
}
|
||||||
|
value={segment.isPlaceholder ? undefined : segment.value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -148,9 +152,10 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
placeholder="MM"
|
placeholder="MM"
|
||||||
required
|
required
|
||||||
tabIndex={2}
|
tabIndex={2}
|
||||||
defaultValue={
|
defaultSelectedKey={
|
||||||
segment.isPlaceholder ? undefined : segment.value
|
segment.isPlaceholder ? undefined : segment.value
|
||||||
}
|
}
|
||||||
|
value={segment.isPlaceholder ? undefined : segment.value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -166,9 +171,10 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|||||||
placeholder="YYYY"
|
placeholder="YYYY"
|
||||||
required
|
required
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
defaultValue={
|
defaultSelectedKey={
|
||||||
segment.isPlaceholder ? undefined : segment.value
|
segment.isPlaceholder ? undefined : segment.value
|
||||||
}
|
}
|
||||||
|
value={segment.isPlaceholder ? undefined : segment.value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user