Merged in feat/my-stays (pull request #106)

Feat/my stays

Approved-by: Michael Zetterberg
Approved-by: Simon.Emanuelsson
This commit is contained in:
Matilda Landström
2024-04-22 05:12:41 +00:00
committed by Michael Zetterberg
33 changed files with 576 additions and 77 deletions

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null
}

View File

@@ -0,0 +1,17 @@
import { serverClient } from "@/lib/trpc/server"
import { stays } from "@/constants/routes/myPages"
import Breadcrumbs from "@/components/MyPages/Breadcrumbs"
import type { LangParams, PageArgs } from "@/types/params"
export default async function StaysBreadcrumbs({
params,
}: PageArgs<LangParams>) {
const href = stays[params.lang].replace(`/${params.lang}`, "")
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get({
href,
locale: params.lang,
})
return <Breadcrumbs breadcrumbs={breadcrumbs} />
}

View File

@@ -4,7 +4,7 @@ import { serverClient } from "@/lib/trpc/server"
import MaxWidth from "@/components/MaxWidth"
import Overview from "@/components/MyPages/Blocks/Overview"
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
import UpcomingStays from "@/components/MyPages/Blocks/UpcomingStays"
import UpcomingStays from "@/components/MyPages/Blocks/Overview/UpcomingStays"
import styles from "./page.module.css"
@@ -12,10 +12,11 @@ import type { LangParams, PageArgs } from "@/types/params"
export default async function MyPageOverview({ params }: PageArgs<LangParams>) {
const user = await serverClient().user.get()
return (
<MaxWidth className={styles.blocks} tag="main">
<Overview user={user} />
<UpcomingStays lang={params.lang} stays={user.stays} />
<UpcomingStays lang={params.lang} />
<Shortcuts
shortcuts={user.shortcuts}
subtitle={_("The community at your fingertips")}

View File

@@ -0,0 +1,17 @@
.container {
display: flex;
flex-direction: column;
gap: 4.2rem;
padding-left: 2rem;
padding-right: 2rem;
margin-top: 2rem;
}
@media screen and (min-width: 950px) {
.container {
gap: 10rem;
padding-left: 0;
padding-right: 0;
margin: 0;
}
}

View File

@@ -0,0 +1,15 @@
import UpcomingStays from "@/components/MyPages/Blocks/Stays/Upcoming"
import PreviousStays from "@/components/MyPages/Blocks/Stays/Previous"
import styles from "./page.module.css"
import { LangParams, PageArgs } from "@/types/params"
export default async function MyStays({ params }: PageArgs<LangParams>) {
return (
<main className={styles.container}>
<UpcomingStays lang={params.lang} />
<PreviousStays lang={params.lang} />
</main>
)
}

View File

@@ -0,0 +1,38 @@
import { serverClient } from "@/lib/trpc/server"
import Link from "@/components/TempDesignSystem/Link"
import Title from "@/components/MyPages/Title"
import StayCard from "../../Stays/StayCard"
import EmptyUpcomingStaysBlock from "../../Stays/EmptyUpcomingStays"
import styles from "./upcoming.module.css"
import type { LangParams } from "@/types/params"
export default async function UpcomingStays({ lang }: LangParams) {
const stays = await serverClient().user.stays.upcoming({
perPage: 3,
})
return (
<section className={styles.container}>
<header className={styles.header}>
<Title level="h2" as="h5" uppercase>
Your upcoming stays
</Title>
<Link className={styles.link} href="#">
See all
</Link>
</header>
{stays.length ? (
<section className={styles.stays}>
{stays.map((stay) => (
<StayCard key={stay.uid} stay={stay} lang={lang} />
))}
</section>
) : (
<EmptyUpcomingStaysBlock />
)}
</section>
)
}

View File

@@ -1,8 +1,8 @@
.container {
display: grid;
gap: 2.2rem;
margin-right: -2rem;
overflow: hidden;
margin-right: -2rem;
}
.header {
@@ -31,16 +31,15 @@
display: none;
}
@media screen and (max-width: 950px) {
.stays {
padding-right: 2rem;
}
}
@media screen and (min-width: 950px) {
.container {
margin-right: 0;
}
@media screen and (max-width: 950px) {
.stays {
padding-right: 2rem;
}
}
.link {
color: var(--some-black-color, #111);

View File

@@ -14,7 +14,7 @@ export default function Shortcuts({
return (
<section className={styles.shortcuts}>
<header className={styles.header}>
<Title level="h2" as="h4" uppercase>
<Title level="h2" as="h5" uppercase>
{title}
</Title>
<p className={styles.subtitle}>{subtitle}</p>

View File

@@ -0,0 +1,9 @@
.container {
align-items: center;
display: flex;
justify-content: center;
min-height: 25rem;
background-color: var(--some-grey-color, #f2f2f2);
border-radius: 0.8rem;
max-width: var(--max-width);
}

View File

@@ -0,0 +1,12 @@
import Title from "@/components/MyPages/Title"
import styles from "./emptyPreviousStays.module.css"
export default function EmptyPreviousStaysBlock() {
return (
<section className={styles.container}>
<Title level="h3" as="h5" uppercase>
You have no previous stays.
</Title>
</section>
)
}

View File

@@ -0,0 +1,26 @@
.button {
background-color: var(--some-red-color, #ed2027);
}
.link {
text-decoration: none;
}
.redTitle {
color: var(--some-red-color, #ed2027);
display: block;
}
.container {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 25rem;
gap: 2.5rem;
background-color: var(--some-grey-color, #f2f2f2);
border-radius: 0.8rem;
max-width: var(--max-width);
margin-bottom: 0.5rem;
padding: 0 2rem;
}

View File

@@ -0,0 +1,25 @@
import Title from "@/components/MyPages/Title"
import Button from "@/components/TempDesignSystem/Button"
import styles from "./emptyUpcomingStays.module.css"
import Link from "next/link"
export default function EmptyUpcomingStaysBlock() {
return (
<section className={styles.container}>
<Title level="h3" as="h5" uppercase>
You have no upcoming stays.
<span className={styles.redTitle}> Where should you go next?</span>
</Title>
<Button
intent={"primary"}
className={styles.button}
asChild
type="button"
>
<Link className={styles.link} href={"#"} key={"getInspired"}>
Get inspired
</Link>
</Button>
</section>
)
}

View File

@@ -0,0 +1,13 @@
.subtitle {
padding-top: 0.5rem;
padding-bottom: 2.5rem;
margin: 0;
}
@media screen and (min-width: 950px) {
.subtitle {
width: 60%;
padding-top: 2.5rem;
padding-bottom: 5rem;
}
}

View File

@@ -0,0 +1,18 @@
import Title from "@/components/MyPages/Title"
import styles from "./header.module.css"
import { HeaderProps } from "@/types/components/myPages/stays/title"
export default function Header({ title, subtitle }: HeaderProps) {
return (
<header className={styles.header}>
<Title as="h3" weight="semiBold" uppercase>
{title}
</Title>
<Title as="h5" weight="regular" className={styles.subtitle}>
{subtitle}
</Title>
</header>
)
}

View File

@@ -0,0 +1,28 @@
import { serverClient } from "@/lib/trpc/server"
import EmptyPreviousStaysBlock from "../EmptyPreviousStays"
import Header from "../Header"
import StayList from "../StayList"
import styles from "./previous.module.css"
import type { LangParams } from "@/types/params"
export default async function PreviousStays({ lang }: LangParams) {
const stays = await serverClient().user.stays.previous()
return (
<section className={styles.container}>
<Header
title="Your previous stays."
subtitle="Revisit your stays and rekindle those our moments together, with ease."
></Header>
{stays.length ? (
<StayList lang={lang} stays={stays} />
) : (
<EmptyPreviousStaysBlock />
)}
</section>
)
}

View File

@@ -0,0 +1,3 @@
.container {
max-width: var(--max-width);
}

View File

@@ -1,20 +1,18 @@
import { dt } from "@/lib/dt"
import Image from "@/components/Image"
import Title from "@/components/MyPages/Title"
import styles from "./stay.module.css"
import Title from "@/components/MyPages/Title"
import type { LangParams } from "@/types/params"
import type { StayProps } from "@/types/components/myPages/myPage/stays"
import type { StayCardProps } from "@/types/components/myPages/stays/stayCard"
export default function Stay({
dateArrive,
dateDepart,
guests,
hotel,
export default function StayCard({
stay,
lang,
}: StayProps & LangParams) {
showDayCount = false,
}: StayCardProps) {
const { dateArrive, dateDepart, guests, hotel } = stay
const daysUntilArrival = dt(dateArrive).diff(dt(), "days")
const arrival = dt(dateArrive).locale(lang)
const arrivalDate = arrival.format("DD MMM")
@@ -22,12 +20,15 @@ export default function Stay({
const depart = dt(dateDepart).locale(lang)
const departDate = depart.format("DD MMM YYYY")
const departDateTime = depart.format("YYYY-MM-DD")
return (
<article className={styles.stay}>
<div className={styles.imageContainer}>
<div className={styles.badge}>
<time className={styles.time}>In {daysUntilArrival} days</time>
</div>
{showDayCount ? (
<div className={styles.badge}>
<time className={styles.time}>In {daysUntilArrival} days</time>
</div>
) : null}
<Image
alt="Placeholder image flower"
height={73}
@@ -36,7 +37,7 @@ export default function Stay({
/>
</div>
<footer className={styles.footer}>
<Title as="h5" level="h3" uppercase>
<Title as="h5" level="h3" uppercase className={styles.hotel}>
{hotel}
</Title>
<section className={styles.container}>

View File

@@ -38,11 +38,19 @@
border-left: 0.1rem solid var(--some-grey-color, #d9d9d9);
border-right: 0.1rem solid var(--some-grey-color, #d9d9d9);
border-radius: 0 0 0.8rem 0.8rem;
display: flex;
flex-direction: column;
gap: 0.8rem;
display: inline-block;
height: 9rem;
padding: 1.5rem 2rem;
overflow: hidden;
}
.hotel {
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
font-size: 1.5rem;
padding: 0;
margin: 0;
}
.container {

View File

@@ -0,0 +1,28 @@
import Button from "@/components/TempDesignSystem/Button"
import StayCard from "../StayCard"
import styles from "./stayList.module.css"
import { StayListProps } from "@/types/components/myPages/stays/stayList"
export default function StayList({ lang, stays }: StayListProps) {
return (
<section>
<section className={styles.stays}>
{stays.map((stay) => (
<StayCard
key={stay.uid}
stay={stay}
lang={lang}
showDayCount={true}
/>
))}
</section>
<div className={styles.buttonContainer}>
<Button intent="primary" type="button">
Show more
</Button>
</div>
</section>
)
}

View File

@@ -0,0 +1,32 @@
.stays {
display: grid;
row-gap: 1.5rem;
column-gap: 2.2rem;
grid-template-columns: auto;
/* Hide scrollbar IE and Edge */
-ms-overflow-style: none;
/* Hide Scrollbar Firefox */
scrollbar-width: none;
}
/* Hide Scrollbar Chrome, Safari and Opera */
.stays::-webkit-scrollbar {
display: none;
}
.buttonContainer {
display: flex;
justify-content: center;
margin-top: 2rem;
}
@media screen and (min-width: 950px) {
.stays {
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
}
.buttonContainer {
margin-top: 4rem;
}
}

View File

@@ -0,0 +1,29 @@
import { serverClient } from "@/lib/trpc/server"
import EmptyUpcomingStaysBlock from "../EmptyUpcomingStays"
import Header from "../Header"
import StayList from "../StayList"
import styles from "./upcoming.module.css"
import type { LangParams } from "@/types/params"
export default async function UpcomingStays({ lang }: LangParams) {
const stays = await serverClient().user.stays.upcoming()
return (
<section className={styles.container}>
<Header
title="Your upcoming stays."
subtitle="Excited about your next trip? So are we. Below are your upcoming stays
with us, complete with all the details you need to make each visit
perfect. Can't wait to welcome you back, friend!"
></Header>
{stays.length ? (
<StayList lang={lang} stays={stays} />
) : (
<EmptyUpcomingStaysBlock />
)}
</section>
)
}

View File

@@ -0,0 +1,3 @@
.container {
max-width: var(--max-width);
}

View File

@@ -1,31 +0,0 @@
import Link from "next/link"
import Stay from "./Stay"
import Title from "@/components/MyPages/Title"
import styles from "./upcoming.module.css"
import type { LangParams } from "@/types/params"
import type { StaysProps } from "@/types/components/myPages/myPage/stays"
export default function UpcomingStays({
lang,
stays,
}: StaysProps & LangParams) {
return (
<section className={styles.container}>
<header className={styles.header}>
<Title level="h2" as="h4" uppercase>
Your upcoming stays
</Title>
<Link className={styles.link} href="#">
See all
</Link>
</header>
<section className={styles.stays}>
{stays.map((stay) => (
<Stay key={stay.hotel} {...stay} lang={lang} />
))}
</section>
</section>
)
}

View File

@@ -1,4 +1,11 @@
import { benefits, myPages, overview, profile, profileEdit } from "./myPages"
import {
benefits,
myPages,
overview,
profile,
profileEdit,
stays,
} from "./myPages"
/**
* These are routes in code we know requires auth
@@ -11,4 +18,5 @@ export const authRequired = [
...Object.values(overview),
...Object.values(profile),
...Object.values(profileEdit),
...Object.values(stays),
]

View File

@@ -57,3 +57,13 @@ export const benefits = {
no: `${myPages.no}/fordeler`,
sv: `${myPages.sv}/formaner`,
}
/** @type {import('@/types/routes').LangRoute} */
export const stays = {
da: `${myPages.da}/ophold`,
de: `${myPages.de}/aufenthalte`,
en: `${myPages.en}/stays`,
fi: `${myPages.fi}/oleskeluni`,
no: `${myPages.no}/opphold`,
sv: `${myPages.sv}/vistelser`,
}

View File

@@ -1,5 +1,11 @@
import * as api from "@/lib/api"
import { benefits, extendedUser, nextLevelPerks } from "./temp"
import {
benefits,
extendedUser,
nextLevelPerks,
previousStays,
upcomingStays,
} from "./temp"
import {
badRequestError,
forbiddenError,
@@ -7,6 +13,8 @@ import {
unauthorizedError,
} from "@/server/errors/trpc"
import { protectedProcedure, router } from "@/server/trpc"
import { z } from "zod"
import { getUserSchema } from "./output"
function fakingRequest<T>(payload: T): Promise<T> {
@@ -74,4 +82,33 @@ export const userQueryRouter = router({
return await fakingRequest<typeof nextLevelPerks>(nextLevelPerks)
}),
}),
stays: router({
previous: protectedProcedure
.input(
z
.object({
perPage: z.number().min(0).default(6),
page: z.number().min(0).default(0),
})
.default({})
)
.query(async (opts) => {
const { perPage, page } = opts.input
return previousStays.slice(page * perPage, page * perPage + perPage)
}),
upcoming: protectedProcedure
.input(
z
.object({
perPage: z.number().min(0).default(6),
page: z.number().min(0).default(0),
})
.default({})
)
.query(async (opts) => {
const { perPage, page } = opts.input
return upcomingStays.slice(page * perPage, page * perPage + perPage)
}),
}),
})

View File

@@ -113,31 +113,170 @@ export const shortcuts = [
// },
]
export const stays = [
export const previousStays = [
{
uid: "0",
dateArrive: new Date("04 27 2024"),
dateDepart: new Date("04 28 2024"),
guests: 2,
hotel: "Scandic Helsinki Hub",
},
{
uid: "1",
dateArrive: new Date("05 27 2024"),
dateDepart: new Date("05 28 2024"),
guests: 2,
hotel: "Scandic Örebro Central",
},
{
uid: "2",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic Oslo City",
},
{
uid: "3",
dateArrive: new Date("04 27 2024"),
dateDepart: new Date("04 28 2024"),
guests: 2,
hotel: "Scandic Lorem",
},
{
uid: "4",
dateArrive: new Date("05 27 2024"),
dateDepart: new Date("05 28 2024"),
guests: 2,
hotel: "Scandic Ipsum",
},
{
uid: "5",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic Dolor Sin Amet",
},
{
uid: "6",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic Anglais",
},
{
uid: "7",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic Park",
},
{
uid: "8",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic Klara",
},
{
uid: "9",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic Dolor A",
},
{
uid: "10",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic B",
},
{
uid: "11",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic C",
},
{
uid: "12",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic D",
},
{
uid: "13",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic E",
},
{
uid: "14",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic F",
},
{
uid: "15",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic G",
},
]
export const upcomingStays = [
{
uid: "0",
dateArrive: new Date("04 27 2024"),
dateDepart: new Date("04 28 2024"),
guests: 2,
hotel: "Scandic Helsinki Hub",
},
{
uid: "1",
dateArrive: new Date("05 27 2024"),
dateDepart: new Date("05 28 2024"),
guests: 2,
hotel: "Scandic Örebro Central",
},
{
uid: "2",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic Oslo City",
},
{
uid: "3",
dateArrive: new Date("04 27 2024"),
dateDepart: new Date("04 28 2024"),
guests: 2,
hotel: "Scandic Lorem",
},
{
uid: "4",
dateArrive: new Date("05 27 2024"),
dateDepart: new Date("05 28 2024"),
guests: 2,
hotel: "Scandic Ipsum",
},
{
uid: "5",
dateArrive: new Date("06 27 2024"),
dateDepart: new Date("06 28 2024"),
guests: 2,
hotel: "Scandic Dolor Sin Amet",
},
]
export const extendedUser = {
journeys: challenges.journeys,
nights: 14,
shortcuts,
stays,
upcomingStays,
victories: challenges.victories,
}

View File

@@ -0,0 +1,7 @@
export type Stay = {
uid: string
dateArrive: Date
dateDepart: Date
guests: number
hotel: string
}

View File

@@ -1,7 +0,0 @@
import type { Stay, User } from "@/types/user"
export type StaysProps = {
stays: User["stays"]
}
export type StayProps = Stay

View File

@@ -0,0 +1,8 @@
import { Stay } from "../myPage/stay"
import { Lang } from "@/constants/languages"
export type StayCardProps = {
lang: Lang
showDayCount?: boolean
stay: Stay
}

View File

@@ -0,0 +1,7 @@
import { Lang } from "@/constants/languages"
import { Stay } from "../myPage/stay"
export type StayListProps = {
stays: Stay[]
lang: Lang
}

View File

@@ -0,0 +1,4 @@
export type HeaderProps = {
title: string
subtitle: string
}

View File

@@ -12,13 +12,6 @@ type ShortcutLink = {
title: string
}
export type Stay = {
dateArrive: Date
dateDepart: Date
guests: number
hotel: string
}
type Victory = {
tag: string
title: string
@@ -33,6 +26,5 @@ export interface User extends z.infer<typeof getUserSchema> {
journeys: Journey[]
nights: number
shortcuts: ShortcutLink[]
stays: Stay[]
victories: Victory[]
}