Merged in feat/my-profile-mvp (pull request #102)
Feat/my profile mvp Approved-by: Michael Zetterberg
This commit is contained in:
44
actions/editProfile.ts
Normal file
44
actions/editProfile.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { editProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
|
||||||
|
import { ZodError } from "zod"
|
||||||
|
|
||||||
|
import { type State, Status } from "@/types/components/myPages/myProfile/edit"
|
||||||
|
|
||||||
|
export async function editProfile(_prevState: State, values: FormData) {
|
||||||
|
try {
|
||||||
|
const data = editProfileSchema.parse(Object.fromEntries(values.entries()))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Update profile data when endpoint from
|
||||||
|
* API team is ready
|
||||||
|
*/
|
||||||
|
|
||||||
|
console.info(`EditProfileSchema.Parse Data`)
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "All good!",
|
||||||
|
status: Status.success,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
return {
|
||||||
|
errors: error.issues.map((issue) => ({
|
||||||
|
message: `Server validation: ${issue.message}`,
|
||||||
|
path: issue.path.join("."),
|
||||||
|
})),
|
||||||
|
message: "Invalid form data",
|
||||||
|
status: Status.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`EditProfile Server Action Error`)
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Something went wrong. Please try again.",
|
||||||
|
status: Status.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,86 +1,16 @@
|
|||||||
export const challenges = {
|
export const breadcrumbs = {
|
||||||
journeys: [
|
"/my-pages": [
|
||||||
{
|
{
|
||||||
tag: "After work queen",
|
title: "My Pages",
|
||||||
title: "Try 3 Hotel Bars, Pocket 200 Points",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "Dine & Shine",
|
|
||||||
title: "Visit 3 scandic Restaurants, Earn 150 Points",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
victories: [
|
"/my-pages/profile": [
|
||||||
{
|
{
|
||||||
tag: "Capital Explorer",
|
href: "/my-pages",
|
||||||
title: "Stay in 3 scandic hotels, in three Capitals, Gain 2000 Points",
|
title: "My Pages",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: "Friends Feast",
|
title: "My Profile",
|
||||||
title: "Dine with 3 Buddies, Snag a Free Breakfast",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "Eco Warrior",
|
|
||||||
title: "Choose Green, Get 500 Points",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shortcuts = [
|
|
||||||
{
|
|
||||||
href: "#",
|
|
||||||
title: "My Benefit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "#",
|
|
||||||
title: "Program overview",
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// href: "#",
|
|
||||||
// title: "Scandic Friends shop",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// href: "#",
|
|
||||||
// title: "Fire and safety",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// href: "#",
|
|
||||||
// title: "Our sustainability work",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// href: "#",
|
|
||||||
// title: "How you earn points",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// href: "#",
|
|
||||||
// title: "How you use points",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// href: "#",
|
|
||||||
// title: "Missing points",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// href: "#",
|
|
||||||
// title: "Our term and conditions",
|
|
||||||
// },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const stays = [
|
|
||||||
{
|
|
||||||
dateArrive: new Date("04 27 2024"),
|
|
||||||
dateDepart: new Date("04 28 2024"),
|
|
||||||
guests: 2,
|
|
||||||
hotel: "Scandic Helsinki Hub",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dateArrive: new Date("05 27 2024"),
|
|
||||||
dateDepart: new Date("05 28 2024"),
|
|
||||||
guests: 2,
|
|
||||||
hotel: "Scandic Örebro Central",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dateArrive: new Date("06 27 2024"),
|
|
||||||
dateDepart: new Date("06 28 2024"),
|
|
||||||
guests: 2,
|
|
||||||
hotel: "Scandic Oslo City",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,31 +1,28 @@
|
|||||||
.page {
|
.layout {
|
||||||
--max-width: 101.4rem;
|
--max-width: 101.4rem;
|
||||||
|
--header-height: 4.5rem;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
font-family: var(--ff-fira-sans);
|
font-family: var(--ff-fira-sans);
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: var(--header-height) auto 1fr;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: grid;
|
display: grid;
|
||||||
padding: 0 0 17.5rem;
|
|
||||||
padding-bottom: 7.7rem;
|
padding-bottom: 7.7rem;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
padding-top: 0;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 950px) {
|
@media screen and (min-width: 950px) {
|
||||||
.page {
|
|
||||||
gap: 5.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
gap: 10rem;
|
gap: 10rem;
|
||||||
grid-template-columns: 25rem 1fr;
|
grid-template-columns: 25rem 1fr;
|
||||||
padding-bottom: 17.5rem;
|
padding-bottom: 17.5rem;
|
||||||
padding-left: 2.4rem;
|
padding-left: 2.4rem;
|
||||||
padding-right: 2.4rem;
|
padding-right: 2.4rem;
|
||||||
|
padding-top: 5.8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,63 +1,26 @@
|
|||||||
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
|
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
|
||||||
|
import { breadcrumbs } from "./_constants"
|
||||||
|
|
||||||
|
import Breadcrumbs from "@/components/MyPages/Breadcrumbs"
|
||||||
import Header from "@/components/MyPages/Header"
|
import Header from "@/components/MyPages/Header"
|
||||||
import Sidebar from "@/components/MyPages/Sidebar"
|
import Sidebar from "@/components/MyPages/Sidebar"
|
||||||
|
|
||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||||
import { request } from "@/lib/graphql/request"
|
|
||||||
import {
|
|
||||||
GetNavigationMyPagesData,
|
|
||||||
NavigationItem,
|
|
||||||
MenuItem,
|
|
||||||
PageLink,
|
|
||||||
PageLinkEnum,
|
|
||||||
} from "@/types/requests/myPages/navigation"
|
|
||||||
import { GetNavigationMyPages } from "@/lib/graphql/Query/NavigationMyPages.graphql"
|
|
||||||
|
|
||||||
function getURL(node: PageLink) {
|
|
||||||
switch (node.__typename) {
|
|
||||||
case PageLinkEnum.ContentPage:
|
|
||||||
return node.web.url
|
|
||||||
case PageLinkEnum.AccountPage:
|
|
||||||
case PageLinkEnum.LoyaltyPage:
|
|
||||||
return node.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapMenuItems(navigationItems: NavigationItem[]) {
|
|
||||||
return navigationItems.map(({ item }): MenuItem => {
|
|
||||||
const { node } = item.pageConnection.edges[0]
|
|
||||||
|
|
||||||
return {
|
|
||||||
uid: node.system.uid,
|
|
||||||
url: getURL(node),
|
|
||||||
linkText: item.link_text || node.title,
|
|
||||||
subItems: item.sub_items ? mapMenuItems(item.sub_items) : null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function MyPagesLayout({
|
export default async function MyPagesLayout({
|
||||||
children,
|
children,
|
||||||
params,
|
params,
|
||||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
||||||
const response = await request<GetNavigationMyPagesData>(
|
|
||||||
GetNavigationMyPages,
|
|
||||||
{
|
|
||||||
locale: params.lang,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// navigation_my_pages is of type Single, hence the hard [0]
|
|
||||||
const navigation = response.data.all_navigation_my_pages.items[0]
|
|
||||||
const menuItems = mapMenuItems(navigation.items)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${firaMono.variable} ${firaSans.variable} ${styles.page}`}>
|
<div
|
||||||
|
className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`}
|
||||||
|
>
|
||||||
<Header lang={params.lang} />
|
<Header lang={params.lang} />
|
||||||
|
<Breadcrumbs breadcrumbs={breadcrumbs} lang={params.lang} />
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Sidebar menuItems={menuItems} />
|
<Sidebar lang={params.lang} />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
.blocks {
|
.blocks {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4.2rem;
|
gap: 4.2rem;
|
||||||
max-width: var(--max-width);
|
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { _ } from "@/lib/translation"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
import { challenges, shortcuts, stays } from "./_constants"
|
import MaxWidth from "@/components/MaxWidth"
|
||||||
|
|
||||||
import Overview from "@/components/MyPages/Blocks/Overview"
|
import Overview from "@/components/MyPages/Blocks/Overview"
|
||||||
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
|
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
|
||||||
import UpcomingStays from "@/components/MyPages/Blocks/UpcomingStays"
|
import UpcomingStays from "@/components/MyPages/Blocks/UpcomingStays"
|
||||||
@@ -11,27 +11,16 @@ import styles from "./page.module.css"
|
|||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function MyPage({ params }: PageArgs<LangParams>) {
|
export default async function MyPage({ params }: PageArgs<LangParams>) {
|
||||||
const data = await serverClient().user.get()
|
const user = await serverClient().user.get()
|
||||||
const user = {
|
|
||||||
...data,
|
|
||||||
journeys: challenges.journeys,
|
|
||||||
membershipId: 30812404844732,
|
|
||||||
nights: 14,
|
|
||||||
points: 20720,
|
|
||||||
qualifyingPoints: 5000,
|
|
||||||
shortcuts,
|
|
||||||
stays,
|
|
||||||
victories: challenges.victories,
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.blocks}>
|
<MaxWidth className={styles.blocks} tag="main">
|
||||||
<Overview user={user} />
|
<Overview user={user} />
|
||||||
<UpcomingStays lang={params.lang} stays={user.stays} />
|
<UpcomingStays lang={params.lang} stays={user.stays} />
|
||||||
<Shortcuts
|
<Shortcuts
|
||||||
shortcuts={user.shortcuts}
|
shortcuts={user.shortcuts}
|
||||||
title="Handy Shortcuts"
|
subtitle={_("The community at your fingertips")}
|
||||||
subtitle="The community at your fingertips"
|
title={_("Handy Shortcuts")}
|
||||||
/>
|
/>
|
||||||
</main>
|
</MaxWidth>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import CommunicationPreferences from "@/components/MyProfile/CommunicationPreferences"
|
||||||
|
|
||||||
|
export default function Communication() {
|
||||||
|
return <CommunicationPreferences />
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import CreditCards from "@/components/MyProfile/CreditCards"
|
||||||
|
|
||||||
|
export default function CreditCardSlot() {
|
||||||
|
return <CreditCards />
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client"
|
||||||
|
import { _ } from "@/lib/translation"
|
||||||
|
import { profile } from "@/constants/routes/myPages"
|
||||||
|
import { useProfileStore } from "@/stores/edit-profile"
|
||||||
|
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
|
||||||
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
|
export default function EditProfile({ params }: PageArgs<LangParams>) {
|
||||||
|
const isPending = useProfileStore((store) => store.pending)
|
||||||
|
const isValid = useProfileStore((store) => store.valid)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
aria-label="Cancel"
|
||||||
|
asChild
|
||||||
|
bgcolor="white"
|
||||||
|
form="edit-profile"
|
||||||
|
size="small"
|
||||||
|
type="reset"
|
||||||
|
>
|
||||||
|
<Link href={profile[params.lang]}>{_("Cancel")}</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
bgcolor="quarternary"
|
||||||
|
disabled={!isValid || isPending}
|
||||||
|
form="edit-profile"
|
||||||
|
size="small"
|
||||||
|
type="submit"
|
||||||
|
weight="regular"
|
||||||
|
>
|
||||||
|
{_("Save")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import MembershipCard from "@/components/MyProfile/MembershipCard"
|
||||||
|
|
||||||
|
export default function MembershipCardSlot() {
|
||||||
|
return <MembershipCard />
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Password from "@/components/MyProfile/Password"
|
||||||
|
|
||||||
|
export default function PasswordSlot() {
|
||||||
|
return <Password />
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
|
import EditProfile from "@/components/MyProfile/Profile/Edit"
|
||||||
|
|
||||||
|
export default async function EditProfileSlot() {
|
||||||
|
const user = await serverClient().user.get()
|
||||||
|
return <EditProfile user={user} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
|
import Profile from "@/components/MyProfile/Profile"
|
||||||
|
|
||||||
|
export default async function ProfileInfo() {
|
||||||
|
const user = await serverClient().user.get()
|
||||||
|
return <Profile user={user} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { _ } from "@/lib/translation"
|
||||||
|
import { profileEdit } from "@/constants/routes/myPages"
|
||||||
|
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
|
||||||
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
|
export default function ProfileView({ params }: PageArgs<LangParams>) {
|
||||||
|
return (
|
||||||
|
<Button asChild bgcolor="quarternary" size="small" weight="regular">
|
||||||
|
<Link href={profileEdit[params.lang]}>{_("Edit")}</Link>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Default() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Wishes from "@/components/MyProfile/Wishes"
|
||||||
|
|
||||||
|
export default function WishesSlot() {
|
||||||
|
return <Wishes />
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function EditPage() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.page {
|
||||||
|
display: grid;
|
||||||
|
gap: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btns {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
/* Creates the 16px gap from design */
|
||||||
|
top: -1.6rem;
|
||||||
|
/* Moves itself to top of container to avoid calc */
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.4rem;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
33
app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx
Normal file
33
app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import MaxWidth from "@/components/MaxWidth"
|
||||||
|
|
||||||
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
|
import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout"
|
||||||
|
|
||||||
|
export default function ProfileLayout({
|
||||||
|
communication,
|
||||||
|
creditCards,
|
||||||
|
edit,
|
||||||
|
membershipCard,
|
||||||
|
password,
|
||||||
|
profile,
|
||||||
|
view,
|
||||||
|
wishes,
|
||||||
|
}: React.PropsWithChildren<ProfileLayoutProps>) {
|
||||||
|
return (
|
||||||
|
<MaxWidth className={styles.page} tag="main">
|
||||||
|
<div className={styles.btns}>
|
||||||
|
{edit}
|
||||||
|
{view}
|
||||||
|
</div>
|
||||||
|
{profile}
|
||||||
|
<section className={styles.cards}>
|
||||||
|
{communication}
|
||||||
|
{wishes}
|
||||||
|
{membershipCard}
|
||||||
|
{creditCards}
|
||||||
|
{password}
|
||||||
|
</section>
|
||||||
|
</MaxWidth>
|
||||||
|
)
|
||||||
|
}
|
||||||
14
components/Icons/Calendar.tsx
Normal file
14
components/Icons/Calendar.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Image from "@/components/Image"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function CalendarIcon({ height = 20, width = 20 }: IconProps) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
alt="Calendar Icon"
|
||||||
|
height={height}
|
||||||
|
src="/calendar_month.svg"
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
components/Icons/ChevronDown.tsx
Normal file
17
components/Icons/ChevronDown.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Image from "@/components/Image"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function ChevronDownIcon({
|
||||||
|
height = 20,
|
||||||
|
width = 20,
|
||||||
|
}: IconProps) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
alt="Chevron Down Icon"
|
||||||
|
height={height}
|
||||||
|
src="/chevron-down.svg"
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
14
components/Icons/Email.tsx
Normal file
14
components/Icons/Email.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Image from "@/components/Image"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function EmailIcon({ height = 20, width = 20 }: IconProps) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
alt="Email Icon"
|
||||||
|
height={height}
|
||||||
|
src="/alternate_email.svg"
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
components/Icons/House.tsx
Normal file
9
components/Icons/House.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Image from "@/components/Image"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function HouseIcon({ height = 20, width = 20 }: IconProps) {
|
||||||
|
return (
|
||||||
|
<Image alt="House Icon" height={height} src="/home.svg" width={width} />
|
||||||
|
)
|
||||||
|
}
|
||||||
9
components/Icons/Phone.tsx
Normal file
9
components/Icons/Phone.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Image from "@/components/Image"
|
||||||
|
|
||||||
|
import type { IconProps } from "@/types/components/icon"
|
||||||
|
|
||||||
|
export default function PhoneIcon({ height = 20, width = 20 }: IconProps) {
|
||||||
|
return (
|
||||||
|
<Image alt="Phone Icon" height={height} src="/phone.svg" width={width} />
|
||||||
|
)
|
||||||
|
}
|
||||||
5
components/Icons/index.tsx
Normal file
5
components/Icons/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { default as CalendarIcon } from "./Calendar"
|
||||||
|
export { default as ChevronDownIcon } from "./ChevronDown"
|
||||||
|
export { default as EmailIcon } from "./Email"
|
||||||
|
export { default as HouseIcon } from "./House"
|
||||||
|
export { default as PhoneIcon } from "./Phone"
|
||||||
17
components/MaxWidth/index.tsx
Normal file
17
components/MaxWidth/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import styles from "./maxWidth.module.css"
|
||||||
|
|
||||||
|
import type { MaxWidthProps } from "@/types/components/max-width"
|
||||||
|
|
||||||
|
const maxWidthVariants = cva(styles.container)
|
||||||
|
|
||||||
|
export default function MaxWidth({
|
||||||
|
className,
|
||||||
|
tag = "section",
|
||||||
|
...props
|
||||||
|
}: MaxWidthProps
|
||||||
|
) {
|
||||||
|
const Cmp = tag
|
||||||
|
return <Cmp className={maxWidthVariants({ className })} {...props} />
|
||||||
|
}
|
||||||
4
components/MaxWidth/maxWidth.module.css
Normal file
4
components/MaxWidth/maxWidth.module.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.container {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import Title from "@/components/MyPages/Title"
|
|||||||
|
|
||||||
import styles from "./challenges.module.css"
|
import styles from "./challenges.module.css"
|
||||||
|
|
||||||
import type { ChallengesProps } from "@/types/components/myPages/challenges"
|
import type { ChallengesProps } from "@/types/components/myPages/myPage/challenges"
|
||||||
|
|
||||||
export default function Challenges({ journeys, victories }: ChallengesProps) {
|
export default function Challenges({ journeys, victories }: ChallengesProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Image from "@/components/Image"
|
|||||||
|
|
||||||
import styles from "./friend.module.css"
|
import styles from "./friend.module.css"
|
||||||
|
|
||||||
import type { FriendProps } from "@/types/components/myPages/friend"
|
import type { FriendProps } from "@/types/components/myPages/myPage/friend"
|
||||||
|
|
||||||
export default function Friend({ user }: FriendProps) {
|
export default function Friend({ user }: FriendProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Image from "@/components/Image"
|
|||||||
|
|
||||||
import styles from "./points.module.css"
|
import styles from "./points.module.css"
|
||||||
|
|
||||||
import type { QualifyingPointsProps } from "@/types/components/myPages/qualifyingPoints"
|
import type { QualifyingPointsProps } from "@/types/components/myPages/myPage/qualifyingPoints"
|
||||||
|
|
||||||
export default function QualifyingPoints({ user }: QualifyingPointsProps) {
|
export default function QualifyingPoints({ user }: QualifyingPointsProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Title from "../Title"
|
|||||||
|
|
||||||
import styles from "./totalPoints.module.css"
|
import styles from "./totalPoints.module.css"
|
||||||
|
|
||||||
import type { TotalPointsProps } from "@/types/components/myPages/totalPoints"
|
import type { TotalPointsProps } from "@/types/components/myPages/myPage/totalPoints"
|
||||||
|
|
||||||
export default function TotalPoints({ user }: TotalPointsProps) {
|
export default function TotalPoints({ user }: TotalPointsProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import TotalPoints from "./TotalPoints"
|
|||||||
|
|
||||||
import styles from "./stats.module.css"
|
import styles from "./stats.module.css"
|
||||||
|
|
||||||
import type { StatsProps } from "@/types/components/myPages/stats"
|
import type { StatsProps } from "@/types/components/myPages/myPage/stats"
|
||||||
|
|
||||||
export default function Stats({ user }: StatsProps) {
|
export default function Stats({ user }: StatsProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Title from "@/components/MyPages/Title"
|
|||||||
|
|
||||||
import styles from "./overview.module.css"
|
import styles from "./overview.module.css"
|
||||||
|
|
||||||
import type { OverviewProps } from "@/types/components/myPages/overview"
|
import type { OverviewProps } from "@/types/components/myPages/myPage/overview"
|
||||||
|
|
||||||
export default function Overview({ user }: OverviewProps) {
|
export default function Overview({ user }: OverviewProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Title from "@/components/MyPages/Title"
|
|||||||
|
|
||||||
import styles from "./shortcuts.module.css"
|
import styles from "./shortcuts.module.css"
|
||||||
|
|
||||||
import type { ShortcutsProps } from "@/types/components/myPages/shortcuts"
|
import type { ShortcutsProps } from "@/types/components/myPages/myPage/shortcuts"
|
||||||
|
|
||||||
export default function Shortcuts({
|
export default function Shortcuts({
|
||||||
shortcuts,
|
shortcuts,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Title from "@/components/MyPages/Title"
|
|||||||
import styles from "./stay.module.css"
|
import styles from "./stay.module.css"
|
||||||
|
|
||||||
import type { LangParams } from "@/types/params"
|
import type { LangParams } from "@/types/params"
|
||||||
import type { StayProps } from "@/types/components/myPages/stays"
|
import type { StayProps } from "@/types/components/myPages/myPage/stays"
|
||||||
|
|
||||||
export default function Stay({
|
export default function Stay({
|
||||||
dateArrive,
|
dateArrive,
|
||||||
@@ -36,7 +36,7 @@ export default function Stay({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
<Title as="h4" level="h3" uppercase>
|
<Title as="h5" level="h3" uppercase>
|
||||||
{hotel}
|
{hotel}
|
||||||
</Title>
|
</Title>
|
||||||
<section className={styles.container}>
|
<section className={styles.container}>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
import Link from "next/link"
|
||||||
import Stay from "./Stay"
|
import Stay from "./Stay"
|
||||||
import Title from "@/components/MyPages/Title"
|
import Title from "@/components/MyPages/Title"
|
||||||
|
|
||||||
import styles from "./upcoming.module.css"
|
import styles from "./upcoming.module.css"
|
||||||
|
|
||||||
import type { LangParams } from "@/types/params"
|
import type { LangParams } from "@/types/params"
|
||||||
import type { StaysProps } from "@/types/components/myPages/stays"
|
import type { StaysProps } from "@/types/components/myPages/myPage/stays"
|
||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
export default function UpcomingStays({
|
export default function UpcomingStays({
|
||||||
lang,
|
lang,
|
||||||
|
|||||||
49
components/MyPages/Breadcrumbs/Client.tsx
Normal file
49
components/MyPages/Breadcrumbs/Client.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"use client"
|
||||||
|
import { Fragment } from "react"
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
|
||||||
|
import styles from "./breadcrumbs.module.css"
|
||||||
|
|
||||||
|
import type { BreadcrumbsProps } from "@/types/components/myPages/breadcrumbs"
|
||||||
|
|
||||||
|
export default function ClientBreadcrumbs({ breadcrumbs, lang }: BreadcrumbsProps) {
|
||||||
|
const pathname = usePathname()
|
||||||
|
/** Temp solution until we can get breadcrumbs from CS */
|
||||||
|
const path = pathname.replace(`/${lang}`, '')
|
||||||
|
const currentBreadcrumbs = breadcrumbs?.[path]
|
||||||
|
if (!currentBreadcrumbs?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<li className={styles.listItem}>
|
||||||
|
<span>/</span>
|
||||||
|
</li>
|
||||||
|
{currentBreadcrumbs.map(breadcrumb => {
|
||||||
|
if (breadcrumb.href) {
|
||||||
|
return (
|
||||||
|
<Fragment key={breadcrumb.title}>
|
||||||
|
<li className={styles.listItem}>
|
||||||
|
<Link className={styles.link} href={breadcrumb.href}>
|
||||||
|
{breadcrumb.title}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className={styles.listItem}>
|
||||||
|
<span>/</span>
|
||||||
|
</li>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={styles.listItem} key={breadcrumb.title}>
|
||||||
|
<p className={styles.currentPage}>{breadcrumb.title}</p>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
components/MyPages/Breadcrumbs/breadcrumbs.module.css
Normal file
40
components/MyPages/Breadcrumbs/breadcrumbs.module.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
.breadcrumbs {
|
||||||
|
background-color: var(--some-grey-color, #f2f2f2);
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 0.8rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-top: 3rem;
|
||||||
|
position: sticky;
|
||||||
|
top: var(--header-height);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
justify-content: flex-start;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem,
|
||||||
|
.link {
|
||||||
|
color: var(--some-text-color, #000);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.56rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currentPage {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 950px) {
|
||||||
|
.breadcrumbs {
|
||||||
|
background-color: var(--some-white-color, #fff);
|
||||||
|
padding-left: 2.4rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
components/MyPages/Breadcrumbs/index.tsx
Normal file
21
components/MyPages/Breadcrumbs/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import ClientBreadcrumbs from "./Client"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
|
||||||
|
import styles from "./breadcrumbs.module.css"
|
||||||
|
|
||||||
|
import type { BreadcrumbsProps } from "@/types/components/myPages/breadcrumbs"
|
||||||
|
|
||||||
|
export default function Breadcrumbs({ breadcrumbs, lang }: BreadcrumbsProps) {
|
||||||
|
return (
|
||||||
|
<nav className={styles.breadcrumbs}>
|
||||||
|
<ul className={styles.list}>
|
||||||
|
<li className={styles.listItem}>
|
||||||
|
<Link className={styles.link} href="#">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<ClientBreadcrumbs breadcrumbs={breadcrumbs} lang={lang} />
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
.list {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.4rem;
|
|
||||||
justify-content: flex-start;
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.listItem,
|
|
||||||
.link {
|
|
||||||
color: var(--some-text-color, #000);
|
|
||||||
font-size: 1.4rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.56rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.currentPage {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
import styles from "./breadcrumbs.module.css"
|
|
||||||
|
|
||||||
export default function Breadcrumbs() {
|
|
||||||
return (
|
|
||||||
<nav className={styles.breadcrumbs}>
|
|
||||||
<ul className={styles.list}>
|
|
||||||
<li className={styles.listItem}>
|
|
||||||
<Link className={styles.link} href="#">
|
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className={styles.listItem}>
|
|
||||||
<span>/</span>
|
|
||||||
</li>
|
|
||||||
<li className={styles.listItem}>
|
|
||||||
<p className={styles.currentPage}>My Scandic</p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
.container {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--some-white-color, #fff);
|
background-color: var(--some-white-color, #fff);
|
||||||
@@ -11,16 +5,12 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 3rem;
|
gap: 3rem;
|
||||||
grid-template-columns: 1fr auto auto;
|
grid-template-columns: 1fr auto auto;
|
||||||
height: 7rem;
|
height: var(--header-height);
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs {
|
padding: 0 2rem;
|
||||||
background-color: var(--some-grey-color, #f2f2f2);
|
position: sticky;
|
||||||
display: block;
|
top: 0;
|
||||||
padding-bottom: 0.8rem;
|
z-index: 999;
|
||||||
padding-left: 2rem;
|
|
||||||
padding-top: 3rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 950px) {
|
@media screen and (min-width: 950px) {
|
||||||
@@ -30,13 +20,6 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
gap: 3.2rem;
|
gap: 3.2rem;
|
||||||
grid-template-columns: 1fr 19rem auto auto;
|
grid-template-columns: 1fr 19rem auto auto;
|
||||||
height: 4.5rem;
|
|
||||||
padding: 0 2.4rem;
|
padding: 0 2.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs {
|
|
||||||
background-color: var(--some-white-color, #fff);
|
|
||||||
padding-left: 2.4rem;
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import Breadcrumbs from "./Breadcrumbs"
|
|
||||||
import Hamburger from "./Hamburger"
|
import Hamburger from "./Hamburger"
|
||||||
import LanguageSwitcher from "./LanguageSwitcher"
|
import LanguageSwitcher from "./LanguageSwitcher"
|
||||||
import Logo from "./Logo"
|
import Logo from "./Logo"
|
||||||
@@ -10,16 +9,11 @@ import type { LangParams } from "@/types/params"
|
|||||||
|
|
||||||
export default function Header({ lang }: LangParams) {
|
export default function Header({ lang }: LangParams) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<header className={styles.header}>
|
||||||
<header className={styles.header}>
|
<Logo lang={lang} />
|
||||||
<Logo lang={lang} />
|
<LanguageSwitcher />
|
||||||
<LanguageSwitcher />
|
<User />
|
||||||
<User />
|
<Hamburger />
|
||||||
<Hamburger />
|
</header>
|
||||||
</header>
|
|
||||||
<div className={styles.breadcrumbs}>
|
|
||||||
<Breadcrumbs />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
30
components/MyPages/Sidebar/helpers.ts
Normal file
30
components/MyPages/Sidebar/helpers.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { PageLinkEnum } from "@/types/requests/myPages/navigation"
|
||||||
|
import type {
|
||||||
|
NavigationItem,
|
||||||
|
MenuItem,
|
||||||
|
PageLink,
|
||||||
|
} from "@/types/requests/myPages/navigation"
|
||||||
|
|
||||||
|
function getURL(node: PageLink) {
|
||||||
|
switch (node.__typename) {
|
||||||
|
case PageLinkEnum.ContentPage:
|
||||||
|
return node.web.url
|
||||||
|
case PageLinkEnum.AccountPage:
|
||||||
|
case PageLinkEnum.LoyaltyPage:
|
||||||
|
return node.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapMenuItems(navigationItems: NavigationItem[]) {
|
||||||
|
return navigationItems.map(({ item }): MenuItem => {
|
||||||
|
const { node } = item.pageConnection.edges[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
linkText: item.link_text || node.title,
|
||||||
|
lang: node.system.locale,
|
||||||
|
subItems: item.sub_items ? mapMenuItems(item.sub_items) : null,
|
||||||
|
uid: node.system.uid,
|
||||||
|
url: `/${node.system.locale}/${getURL(node)}`.replaceAll("//+", "/"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,18 +1,32 @@
|
|||||||
"use client"
|
import { mapMenuItems } from "./helpers"
|
||||||
|
import { request } from "@/lib/graphql/request"
|
||||||
|
import { GetNavigationMyPages } from "@/lib/graphql/Query/NavigationMyPages.graphql"
|
||||||
|
|
||||||
|
import { Fragment } from "react"
|
||||||
import { LogOut } from "react-feather"
|
import { LogOut } from "react-feather"
|
||||||
import Link from "../../TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
|
||||||
import styles from "./sidebar.module.css"
|
import styles from "./sidebar.module.css"
|
||||||
import { SidebarProps } from "@/types/requests/myPages/navigation"
|
|
||||||
import { Fragment } from "react"
|
|
||||||
|
|
||||||
export default function Sidebar({ menuItems }: SidebarProps) {
|
import type { GetNavigationMyPagesData } from "@/types/requests/myPages/navigation"
|
||||||
|
import type { SidebarProps } from "@/types/requests/myPages/navigation"
|
||||||
|
|
||||||
|
export default async function Sidebar({ lang }: SidebarProps) {
|
||||||
|
const response = await request<GetNavigationMyPagesData>(
|
||||||
|
GetNavigationMyPages,
|
||||||
|
{
|
||||||
|
locale: lang,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// navigation_my_pages is of type Single, hence the hard [0]
|
||||||
|
const navigation = response.data.all_navigation_my_pages.items[0]
|
||||||
|
const menuItems = mapMenuItems(navigation.items)
|
||||||
return (
|
return (
|
||||||
<aside className={styles.sidebar}>
|
<aside className={styles.sidebar}>
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<Fragment key={item.uid}>
|
<Fragment key={item.uid}>
|
||||||
<Link variant={"sidebar"} href={item.url}>
|
<Link href={item.url} variant="sidebar">
|
||||||
{item.linkText}
|
{item.linkText}
|
||||||
</Link>
|
</Link>
|
||||||
{item.subItems
|
{item.subItems
|
||||||
@@ -21,7 +35,7 @@ export default function Sidebar({ menuItems }: SidebarProps) {
|
|||||||
<Link
|
<Link
|
||||||
key={subItem.uid}
|
key={subItem.uid}
|
||||||
href={subItem.url}
|
href={subItem.url}
|
||||||
variant={"sidebar"}
|
variant="sidebar"
|
||||||
>
|
>
|
||||||
{subItem.linkText}
|
{subItem.linkText}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -31,7 +45,7 @@ export default function Sidebar({ menuItems }: SidebarProps) {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Link className={styles.link} href="/api/auth/signout">
|
<Link href="/api/auth/signout" variant="sidebar">
|
||||||
Log out <LogOut height={16} width={16} />
|
Log out <LogOut height={16} width={16} />
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
display: none;
|
display: none;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 13.2rem;
|
/* Based on header and breadcrumbs height, and gap */
|
||||||
|
top: 14.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
@@ -13,31 +14,6 @@
|
|||||||
padding-left: 4rem;
|
padding-left: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
|
||||||
align-items: center;
|
|
||||||
color: var(--some-text-color, #111);
|
|
||||||
display: flex;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-weight: 400;
|
|
||||||
gap: 0.6rem;
|
|
||||||
line-height: 1.9rem;
|
|
||||||
position: relative;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active::before {
|
|
||||||
bottom: -0.4rem;
|
|
||||||
background-color: var(--some-text-color, #000);
|
|
||||||
content: "";
|
|
||||||
height: 0.2rem;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 950px) {
|
@media screen and (min-width: 950px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -1,41 +1,21 @@
|
|||||||
import { cva } from "class-variance-authority"
|
import { headingVariants } from "./variants"
|
||||||
|
|
||||||
import styles from "./title.module.css"
|
|
||||||
|
|
||||||
import type { HeadingProps } from "@/types/components/myPages/title"
|
import type { HeadingProps } from "@/types/components/myPages/title"
|
||||||
|
|
||||||
const config = {
|
|
||||||
variants: {
|
|
||||||
text: {
|
|
||||||
uppercase: styles.uppercase,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
h1: styles.h1,
|
|
||||||
h2: styles.h2,
|
|
||||||
h3: styles.h3,
|
|
||||||
h4: styles.h4,
|
|
||||||
h5: styles.h5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
type: "h1",
|
|
||||||
},
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const headingStyles = cva(styles.heading, config)
|
|
||||||
|
|
||||||
export default function Title({
|
export default function Title({
|
||||||
as,
|
as,
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
level = "h1",
|
level = "h1",
|
||||||
uppercase = false,
|
uppercase = false,
|
||||||
|
weight,
|
||||||
}: HeadingProps) {
|
}: HeadingProps) {
|
||||||
const Hx = level
|
const Hx = level
|
||||||
const classNames = headingStyles({
|
const classNames = headingVariants({
|
||||||
className,
|
className,
|
||||||
text: uppercase ? "uppercase" : undefined,
|
text: uppercase ? "uppercase" : undefined,
|
||||||
type: as ?? level,
|
type: as ?? level,
|
||||||
|
weight,
|
||||||
})
|
})
|
||||||
return <Hx className={classNames}>{children}</Hx>
|
return <Hx className={classNames}>{children}</Hx>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.heading {
|
.heading {
|
||||||
font-weight: 900;
|
/* font-family: var(--ff-brandon-text); */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,30 @@
|
|||||||
line-height: var(--typography-Title5-Mobile-lineHeight);
|
line-height: var(--typography-Title5-Mobile-lineHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regular {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semiBold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 950px) {
|
@media screen and (min-width: 950px) {
|
||||||
.h1 {
|
.h1 {
|
||||||
font-size: var(--typography-Title1-Desktop-fontSize);
|
font-size: var(--typography-Title1-Desktop-fontSize);
|
||||||
|
|||||||
33
components/MyPages/Title/variants.ts
Normal file
33
components/MyPages/Title/variants.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import styles from "./title.module.css"
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
variants: {
|
||||||
|
text: {
|
||||||
|
uppercase: styles.uppercase,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
h1: styles.h1,
|
||||||
|
h2: styles.h2,
|
||||||
|
h3: styles.h3,
|
||||||
|
h4: styles.h4,
|
||||||
|
h5: styles.h5,
|
||||||
|
h6: styles.h6,
|
||||||
|
},
|
||||||
|
weight: {
|
||||||
|
light: styles.light,
|
||||||
|
regular: styles.regular,
|
||||||
|
medium: styles.medium,
|
||||||
|
semiBold: styles.semiBold,
|
||||||
|
bold: styles.bold,
|
||||||
|
black: styles.black,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
type: "h1",
|
||||||
|
weight: "black",
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const headingVariants = cva(styles.heading, config)
|
||||||
21
components/MyProfile/Card/Title/index.tsx
Normal file
21
components/MyProfile/Card/Title/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { headingVariants } from "./variants"
|
||||||
|
|
||||||
|
import type { HeadingProps } from "@/types/components/myPages/myProfile/card/title"
|
||||||
|
|
||||||
|
export default function Title({
|
||||||
|
as,
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
level = "h1",
|
||||||
|
uppercase = false,
|
||||||
|
weight,
|
||||||
|
}: HeadingProps) {
|
||||||
|
const Hx = level
|
||||||
|
const classNames = headingVariants({
|
||||||
|
className,
|
||||||
|
text: uppercase ? "uppercase" : undefined,
|
||||||
|
type: as ?? level,
|
||||||
|
weight,
|
||||||
|
})
|
||||||
|
return <Hx className={classNames}>{children}</Hx>
|
||||||
|
}
|
||||||
68
components/MyProfile/Card/Title/title.module.css
Normal file
68
components/MyProfile/Card/Title/title.module.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.heading {
|
||||||
|
color: var(--some-black-color, #2E2E2E);
|
||||||
|
/* font-family: var(--ff-brandon-text); */
|
||||||
|
letter-spacing: 6%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uppercase {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
line-height: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h3 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h4 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regular {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semiBold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @media screen and (min-width: 950px) {
|
||||||
|
.h1 {
|
||||||
|
font-size: 3.8rem;
|
||||||
|
line-height: 4.5rem;
|
||||||
|
}
|
||||||
|
} */
|
||||||
33
components/MyProfile/Card/Title/variants.ts
Normal file
33
components/MyProfile/Card/Title/variants.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import styles from "./title.module.css"
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
variants: {
|
||||||
|
text: {
|
||||||
|
uppercase: styles.uppercase,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
h1: styles.h1,
|
||||||
|
h2: styles.h2,
|
||||||
|
h3: styles.h3,
|
||||||
|
h4: styles.h4,
|
||||||
|
h5: styles.h5,
|
||||||
|
h6: styles.h6,
|
||||||
|
},
|
||||||
|
weight: {
|
||||||
|
light: styles.light,
|
||||||
|
regular: styles.regular,
|
||||||
|
medium: styles.medium,
|
||||||
|
semiBold: styles.semiBold,
|
||||||
|
bold: styles.bold,
|
||||||
|
black: styles.black,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
type: "h1",
|
||||||
|
weight: "medium",
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const headingVariants = cva(styles.heading, config)
|
||||||
6
components/MyProfile/Card/card.module.css
Normal file
6
components/MyProfile/Card/card.module.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.card {
|
||||||
|
background-color: var(--some-grey-color, #F2F2F2);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
min-height: 15.6rem;
|
||||||
|
padding: 3.8rem;
|
||||||
|
}
|
||||||
16
components/MyProfile/Card/index.tsx
Normal file
16
components/MyProfile/Card/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import Title from "./Title"
|
||||||
|
|
||||||
|
import styles from "./card.module.css"
|
||||||
|
|
||||||
|
import type { CardProps } from "@/types/components/myPages/myProfile/card/card"
|
||||||
|
|
||||||
|
const cardStyles = cva(styles.card)
|
||||||
|
|
||||||
|
export default function Card({ className, tag = "section", ...props }: CardProps) {
|
||||||
|
const Cmp = tag
|
||||||
|
return <Cmp className={cardStyles({ className })} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
Card.Title = Title
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.container {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
11
components/MyProfile/CommunicationPreferences/index.tsx
Normal file
11
components/MyProfile/CommunicationPreferences/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Card from "@/components/MyProfile/Card"
|
||||||
|
|
||||||
|
import styles from "./com.module.css"
|
||||||
|
|
||||||
|
export default function CommunicationPreferences() {
|
||||||
|
return (
|
||||||
|
<Card className={styles.container}>
|
||||||
|
<Card.Title level="h2" uppercase>My communication preferences</Card.Title>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
components/MyProfile/CreditCards/creditCards.module.css
Normal file
5
components/MyProfile/CreditCards/creditCards.module.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.container {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
11
components/MyProfile/CreditCards/index.tsx
Normal file
11
components/MyProfile/CreditCards/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Card from "@/components/MyProfile/Card"
|
||||||
|
|
||||||
|
import styles from "./creditCards.module.css"
|
||||||
|
|
||||||
|
export default function CreditCards() {
|
||||||
|
return (
|
||||||
|
<Card className={styles.container}>
|
||||||
|
<Card.Title level="h2" uppercase>My credit cards</Card.Title>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
components/MyProfile/Field/field.module.css
Normal file
47
components/MyProfile/Field/field.module.css
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
.container {
|
||||||
|
align-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.4rem 1.7rem;
|
||||||
|
grid-template-areas:
|
||||||
|
"icon label"
|
||||||
|
"icon content";
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--some-white-color, #fff);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
font-family: var(--ff-fira-sans);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 400;
|
||||||
|
grid-area: icon;
|
||||||
|
height: 3rem;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1.9rem;
|
||||||
|
width: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label,
|
||||||
|
.content {
|
||||||
|
font-family: var(--ff-fira-sans);
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -1.5%;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--some-black-color, #404040);
|
||||||
|
grid-area: label;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: var(--some-black-color, #000);
|
||||||
|
grid-area: content;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
26
components/MyProfile/Field/index.tsx
Normal file
26
components/MyProfile/Field/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import styles from "./field.module.css"
|
||||||
|
|
||||||
|
export default function Field(props: React.HtmlHTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div {...props} className={styles.container} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function Icon({ children }: React.PropsWithChildren) {
|
||||||
|
return <span className={styles.icon}>{children}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
function Label(props: React.LabelHTMLAttributes<HTMLLabelElement>) {
|
||||||
|
return <label {...props} className={styles.label} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextLabel({ children }: React.PropsWithChildren) {
|
||||||
|
return <span className={styles.label}>{children}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
function Content({ children }: React.PropsWithChildren) {
|
||||||
|
return <div className={styles.content}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
Field.Content = Content
|
||||||
|
Field.Icon = Icon
|
||||||
|
Field.Label = Label
|
||||||
|
Field.TextLabel = TextLabel
|
||||||
11
components/MyProfile/MembershipCard/index.tsx
Normal file
11
components/MyProfile/MembershipCard/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Card from "@/components/MyProfile/Card"
|
||||||
|
|
||||||
|
import styles from "./membershipCard.module.css"
|
||||||
|
|
||||||
|
export default function MembershipCard() {
|
||||||
|
return (
|
||||||
|
<Card className={styles.container}>
|
||||||
|
<Card.Title level="h2" uppercase>Membership cards</Card.Title>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.container {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
11
components/MyProfile/Password/index.tsx
Normal file
11
components/MyProfile/Password/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Card from "@/components/MyProfile/Card"
|
||||||
|
|
||||||
|
import styles from "./password.module.css"
|
||||||
|
|
||||||
|
export default function Password() {
|
||||||
|
return (
|
||||||
|
<Card className={styles.container}>
|
||||||
|
<Card.Title level="h2" uppercase>Password</Card.Title>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
components/MyProfile/Password/password.module.css
Normal file
5
components/MyProfile/Password/password.module.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.container {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
32
components/MyProfile/Profile/Container.tsx
Normal file
32
components/MyProfile/Profile/Container.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
import Card from "@/components/MyProfile/Card"
|
||||||
|
import Image from "@/components/Image"
|
||||||
|
|
||||||
|
import styles from "./profile.module.css"
|
||||||
|
|
||||||
|
import type { ProfileProps } from "@/types/components/myPages/myProfile/profile"
|
||||||
|
|
||||||
|
const profileStyles = cva(styles.profile)
|
||||||
|
|
||||||
|
export default function Container({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
user,
|
||||||
|
...props
|
||||||
|
}: ProfileProps) {
|
||||||
|
return (
|
||||||
|
<Card className={profileStyles({ className })} {...props}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<Image
|
||||||
|
alt="Account Icon"
|
||||||
|
height={40}
|
||||||
|
src="/account_circle.svg"
|
||||||
|
width={40}
|
||||||
|
/>
|
||||||
|
<Card.Title uppercase>{user.name}</Card.Title>
|
||||||
|
</header>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
126
components/MyProfile/Profile/Edit/Form/Content.tsx
Normal file
126
components/MyProfile/Profile/Edit/Form/Content.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
"use client"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { useFormStatus } from "react-dom"
|
||||||
|
|
||||||
|
import { _ } from "@/lib/translation"
|
||||||
|
import { useProfileStore } from "@/stores/edit-profile"
|
||||||
|
|
||||||
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
EmailIcon,
|
||||||
|
HouseIcon,
|
||||||
|
PhoneIcon,
|
||||||
|
} from "@/components/Icons"
|
||||||
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
|
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||||
|
import Field from "@/components/MyProfile/Field"
|
||||||
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
|
|
||||||
|
import type { EditFormContentProps } from "@/types/components/myPages/myProfile/edit"
|
||||||
|
|
||||||
|
export default function FormContent({ control }: EditFormContentProps) {
|
||||||
|
const { pending } = useFormStatus()
|
||||||
|
const setIsPending = useProfileStore((store) => store.setIsPending)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsPending(pending)
|
||||||
|
}, [pending])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>SE</Field.Icon>
|
||||||
|
<Field.Label htmlFor="country">*{_("Country")}</Field.Label>
|
||||||
|
<Field.Content>
|
||||||
|
<CountrySelect name="country" />
|
||||||
|
</Field.Content>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<CalendarIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.Label htmlFor="dob">*{_("Date of Birth")}</Field.Label>
|
||||||
|
<Field.Content>
|
||||||
|
<DateSelect
|
||||||
|
control={control}
|
||||||
|
name="dob"
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
</Field.Content>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<EmailIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.Label htmlFor="email">*{_("Email")}</Field.Label>
|
||||||
|
<Field.Content>
|
||||||
|
<Input
|
||||||
|
control={control}
|
||||||
|
name="email"
|
||||||
|
placeholder={_("Email")}
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
</Field.Content>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<PhoneIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.Label htmlFor="phone">*{_("Phone")}</Field.Label>
|
||||||
|
<Field.Content>
|
||||||
|
<Phone name="phone" />
|
||||||
|
</Field.Content>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<HouseIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.Label htmlFor="street">*{_("Address")}</Field.Label>
|
||||||
|
<Field.Content>
|
||||||
|
<Input
|
||||||
|
control={control}
|
||||||
|
name="street"
|
||||||
|
placeholder={_("Street 123")}
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
</Field.Content>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<HouseIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.Label htmlFor="city">*{_("City/State")}</Field.Label>
|
||||||
|
<Field.Content>
|
||||||
|
<Input
|
||||||
|
control={control}
|
||||||
|
name="city"
|
||||||
|
placeholder={_("City")}
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
</Field.Content>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<HouseIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.Label htmlFor="zip">*{_("Zip code")}</Field.Label>
|
||||||
|
<Field.Content>
|
||||||
|
<Input
|
||||||
|
control={control}
|
||||||
|
name="zip"
|
||||||
|
placeholder={_("Zip code")}
|
||||||
|
registerOptions={{ required: true }}
|
||||||
|
/>
|
||||||
|
</Field.Content>
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
components/MyProfile/Profile/Edit/Form/form.module.css
Normal file
5
components/MyProfile/Profile/Edit/Form/form.module.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.8rem;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
61
components/MyProfile/Profile/Edit/Form/index.tsx
Normal file
61
components/MyProfile/Profile/Edit/Form/index.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"use client"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useFormState as useReactFormState } from "react-dom"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
|
||||||
|
import { editProfile } from "@/actions/editProfile"
|
||||||
|
import { editProfileSchema, type EditProfileSchema } from "./schema"
|
||||||
|
import { useProfileStore } from "@/stores/edit-profile"
|
||||||
|
|
||||||
|
import FormContent from "./Content"
|
||||||
|
|
||||||
|
import styles from "./form.module.css"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type EditFormProps,
|
||||||
|
type State,
|
||||||
|
} from "@/types/components/myPages/myProfile/edit"
|
||||||
|
|
||||||
|
export default function Form({ user }: EditFormProps) {
|
||||||
|
const isValid = useProfileStore((store) => store.valid)
|
||||||
|
const setValid = useProfileStore((store) => store.setValid)
|
||||||
|
/**
|
||||||
|
* like react, react-hook-form also exports a useFormState hook,
|
||||||
|
* we want to clearly keep them separate by naming.
|
||||||
|
*/
|
||||||
|
const [state, formAction] = useReactFormState<State, FormData>(
|
||||||
|
editProfile,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
const form = useForm<EditProfileSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
country: user.country,
|
||||||
|
city: user.address.city,
|
||||||
|
dob: user.dob,
|
||||||
|
email: user.email,
|
||||||
|
phone: user.phone,
|
||||||
|
street: user.address.street,
|
||||||
|
zip: user.address.zipcode,
|
||||||
|
},
|
||||||
|
criteriaMode: "all",
|
||||||
|
mode: "onTouched",
|
||||||
|
resolver: zodResolver(editProfileSchema),
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isValid !== form.formState.isValid) {
|
||||||
|
setValid(form.formState.isValid)
|
||||||
|
}
|
||||||
|
}, [form.formState.isValid, isValid, setValid])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form action={formAction} className={styles.form} id="edit-profile">
|
||||||
|
<FormContent control={form.control} />
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
components/MyProfile/Profile/Edit/Form/schema.ts
Normal file
29
components/MyProfile/Profile/Edit/Form/schema.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { _ } from "@/lib/translation"
|
||||||
|
import { phoneValidator } from "@/utils/phoneValidator"
|
||||||
|
|
||||||
|
export const editProfileSchema = z.object({
|
||||||
|
city: z
|
||||||
|
.string({ required_error: _("City is required") })
|
||||||
|
.min(1, { message: _("City is required") }),
|
||||||
|
country: z
|
||||||
|
.string({ required_error: _("Country is required") })
|
||||||
|
.min(1, { message: _("Country is required") }),
|
||||||
|
dob: z
|
||||||
|
.string({ required_error: _("Date of Birth is required") })
|
||||||
|
.min(1, { message: _("Date of Birth is required") }),
|
||||||
|
email: z.string().email(),
|
||||||
|
phone: phoneValidator(
|
||||||
|
_("Phone is required"),
|
||||||
|
_("Please enter a valid phone number")
|
||||||
|
),
|
||||||
|
street: z
|
||||||
|
.string({ required_error: _("Address is required") })
|
||||||
|
.min(1, { message: _("Address is required") }),
|
||||||
|
zip: z
|
||||||
|
.string({ required_error: _("Zip code is required") })
|
||||||
|
.min(1, { message: _("Zip code is required") }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type EditProfileSchema = z.infer<typeof editProfileSchema>
|
||||||
12
components/MyProfile/Profile/Edit/index.tsx
Normal file
12
components/MyProfile/Profile/Edit/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Container from "../Container"
|
||||||
|
import Form from "./Form"
|
||||||
|
|
||||||
|
import type { ProfileProps } from "@/types/components/myPages/myProfile/profile"
|
||||||
|
|
||||||
|
export default function EditProfile(props: ProfileProps) {
|
||||||
|
return (
|
||||||
|
<Container {...props}>
|
||||||
|
<Form user={props.user} />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
components/MyProfile/Profile/index.tsx
Normal file
70
components/MyProfile/Profile/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { _ } from "@/lib/translation"
|
||||||
|
|
||||||
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
EmailIcon,
|
||||||
|
HouseIcon,
|
||||||
|
PhoneIcon,
|
||||||
|
} from "@/components/Icons"
|
||||||
|
import Container from "./Container"
|
||||||
|
import Field from "../Field"
|
||||||
|
|
||||||
|
import styles from "./profile.module.css"
|
||||||
|
|
||||||
|
import type { ProfileProps } from "@/types/components/myPages/myProfile/profile"
|
||||||
|
|
||||||
|
export default function Profile(props: ProfileProps) {
|
||||||
|
return (
|
||||||
|
<Container {...props}>
|
||||||
|
<section className={styles.info}>
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>SE</Field.Icon>
|
||||||
|
<Field.TextLabel>{_("Country")}</Field.TextLabel>
|
||||||
|
<Field.Content>Sweden</Field.Content>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<CalendarIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.TextLabel>{_("Date of Birth")}</Field.TextLabel>
|
||||||
|
<Field.Content>27/05/1977</Field.Content>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<EmailIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.TextLabel>{_("Email")}</Field.TextLabel>
|
||||||
|
<Field.Content>f*********@g****.com</Field.Content>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<PhoneIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.TextLabel>{_("Phone number")}</Field.TextLabel>
|
||||||
|
<Field.Content>+46 ******00</Field.Content>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<HouseIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.TextLabel>{_("Address")}</Field.TextLabel>
|
||||||
|
<Field.Content>T***************</Field.Content>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<HouseIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.TextLabel>{_("City/State")}</Field.TextLabel>
|
||||||
|
<Field.Content>S*******</Field.Content>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Field.Icon>
|
||||||
|
<HouseIcon />
|
||||||
|
</Field.Icon>
|
||||||
|
<Field.TextLabel>{_("Zip code")}</Field.TextLabel>
|
||||||
|
<Field.Content>1****</Field.Content>
|
||||||
|
</Field>
|
||||||
|
</section>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
19
components/MyProfile/Profile/profile.module.css
Normal file
19
components/MyProfile/Profile/profile.module.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.profile {
|
||||||
|
display: grid;
|
||||||
|
gap: 3.4rem;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
min-height: 46rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
display: grid;
|
||||||
|
gap: 1.4rem;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.8rem;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
11
components/MyProfile/Wishes/index.tsx
Normal file
11
components/MyProfile/Wishes/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Card from "@/components/MyProfile/Card"
|
||||||
|
|
||||||
|
import styles from "./wishes.module.css"
|
||||||
|
|
||||||
|
export default function Wishes() {
|
||||||
|
return (
|
||||||
|
<Card className={styles.container}>
|
||||||
|
<Card.Title level="h2" uppercase>My wishes</Card.Title>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
components/MyProfile/Wishes/wishes.module.css
Normal file
5
components/MyProfile/Wishes/wishes.module.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.container {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
@@ -72,3 +72,79 @@
|
|||||||
color: var(--some-grey-color, #444343);
|
color: var(--some-grey-color, #444343);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
border-radius: 3rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
height: 2.6rem;
|
||||||
|
line-height: 1.7rem;
|
||||||
|
padding: 0.8rem 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.average {
|
||||||
|
border-radius: 4.7rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
height: 3.2rem;
|
||||||
|
letter-spacing: 1%;
|
||||||
|
line-height: 1.6rem;
|
||||||
|
padding: 0.65rem 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.regular {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semiBold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black {
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background-color: var(--scandic-blue, #02838e);
|
||||||
|
border: 0.1rem solid var(--scandic-blue, #02838e);
|
||||||
|
color: var(--some-white-color, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
background-color: var(--some-black-color, #000);
|
||||||
|
border: 0.1rem solid var(--some-black-color, #000);
|
||||||
|
color: var(--some-white-color, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tertiary {
|
||||||
|
background-color: var(--some-red-color, #d60728);
|
||||||
|
border: 0.1rem solid var(--some-red-color, #d60728);
|
||||||
|
color: var(--some-white-color, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quarternary {
|
||||||
|
background-color: var(--some-grey-color, #727272);
|
||||||
|
border: 0.1rem solid var(--some-black-color, #727272);
|
||||||
|
color: var(--some-white-color, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.white {
|
||||||
|
background-color: var(--some-white-color, #fff);
|
||||||
|
border: 0.1rem solid var(--some-black-color, #000);
|
||||||
|
color: var(--some-black-color, #000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
background-color: var(--some-grey-color, #d9d9d9);
|
||||||
|
color: var(--some-grey-color, #757575);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,16 +8,23 @@ import type { ButtonProps } from "./button"
|
|||||||
|
|
||||||
export default function Button({
|
export default function Button({
|
||||||
asChild = false,
|
asChild = false,
|
||||||
|
bgcolor,
|
||||||
className,
|
className,
|
||||||
variant,
|
disabled,
|
||||||
intent,
|
intent,
|
||||||
|
size,
|
||||||
|
variant,
|
||||||
|
weight,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
return (
|
const classNames = buttonVariants({
|
||||||
<Comp
|
bgcolor,
|
||||||
className={buttonVariants({ className, variant, intent })}
|
className,
|
||||||
{...props}
|
intent,
|
||||||
/>
|
size,
|
||||||
)
|
variant,
|
||||||
|
weight,
|
||||||
|
})
|
||||||
|
return <Comp className={classNames} disabled={disabled} {...props} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,36 @@ import styles from "./button.module.css"
|
|||||||
|
|
||||||
export const buttonVariants = cva(styles.btn, {
|
export const buttonVariants = cva(styles.btn, {
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
bgcolor: {
|
||||||
default: styles.default,
|
primary: styles.primary,
|
||||||
icon: styles.icon,
|
secondary: styles.secondary,
|
||||||
|
tertiary: styles.tertiary,
|
||||||
|
quarternary: styles.quarternary,
|
||||||
|
white: styles.white,
|
||||||
},
|
},
|
||||||
intent: {
|
intent: {
|
||||||
primary: styles.primary,
|
primary: styles.primary,
|
||||||
secondary: styles.secondary,
|
secondary: styles.secondary,
|
||||||
},
|
},
|
||||||
|
size: {
|
||||||
|
small: styles.small,
|
||||||
|
regular: styles.average,
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
default: styles.default,
|
||||||
|
icon: styles.icon,
|
||||||
|
},
|
||||||
|
weight: {
|
||||||
|
light: styles.light,
|
||||||
|
regular: styles.regular,
|
||||||
|
medium: styles.medium,
|
||||||
|
semiBold: styles.semiBold,
|
||||||
|
bold: styles.bold,
|
||||||
|
black: styles.black,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
|
weight: "regular",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
256
components/TempDesignSystem/Form/Country/countries.ts
Normal file
256
components/TempDesignSystem/Form/Country/countries.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
export const countries = [
|
||||||
|
{ name: "Afghanistan", code: "AF" },
|
||||||
|
{ name: "Albania", code: "AL" },
|
||||||
|
{ name: "Algeria", code: "DZ" },
|
||||||
|
{ name: "American Samoa", code: "AS" },
|
||||||
|
{ name: "Andorra", code: "AD" },
|
||||||
|
{ name: "Angola", code: "AO" },
|
||||||
|
{ name: "Anguilla", code: "AI" },
|
||||||
|
{ name: "Antarctica", code: "AQ" },
|
||||||
|
{ name: "Antigua and Barbuda", code: "AG" },
|
||||||
|
{ name: "Argentina", code: "AR" },
|
||||||
|
{ name: "Armenia", code: "AM" },
|
||||||
|
{ name: "Aruba", code: "AW" },
|
||||||
|
{ name: "Australia", code: "AU" },
|
||||||
|
{ name: "Austria", code: "AT" },
|
||||||
|
{ name: "Azerbaijan", code: "AZ" },
|
||||||
|
{ name: "Bahamas", code: "BS" },
|
||||||
|
{ name: "Bahrain", code: "BH" },
|
||||||
|
{ name: "Bangladesh", code: "BD" },
|
||||||
|
{ name: "Barbados", code: "BB" },
|
||||||
|
{ name: "Belarus", code: "BY" },
|
||||||
|
{ name: "Belgium", code: "BE" },
|
||||||
|
{ name: "Belize", code: "BZ" },
|
||||||
|
{ name: "Benin", code: "BJ" },
|
||||||
|
{ name: "Bermuda", code: "BM" },
|
||||||
|
{ name: "Bhutan", code: "BT" },
|
||||||
|
{ name: "Bolivia", code: "BO" },
|
||||||
|
{ name: "Bonaire", code: "BQ" },
|
||||||
|
{ name: "Bosnia and Herzegovina", code: "BA" },
|
||||||
|
{ name: "Botswana", code: "BW" },
|
||||||
|
{ name: "Bouvet Island", code: "BV" },
|
||||||
|
{ name: "Brazil", code: "BR" },
|
||||||
|
{ name: "British Indian Ocean Territory", code: "IO" },
|
||||||
|
{ name: "Brunei Darussalam", code: "BN" },
|
||||||
|
{ name: "Bulgaria", code: "BG" },
|
||||||
|
{ name: "Burkina Faso", code: "BF" },
|
||||||
|
{ name: "Burundi", code: "BI" },
|
||||||
|
{ name: "Cambodia", code: "KH" },
|
||||||
|
{ name: "Cameroon", code: "CM" },
|
||||||
|
{ name: "Canada", code: "CA" },
|
||||||
|
{ name: "Cape Verde", code: "CV" },
|
||||||
|
{ name: "Cayman Islands", code: "KY" },
|
||||||
|
{ name: "Central African Republic", code: "CF" },
|
||||||
|
{ name: "Chad", code: "TD" },
|
||||||
|
{ name: "Chile", code: "CL" },
|
||||||
|
{ name: "China", code: "CN" },
|
||||||
|
{ name: "Christmas Island", code: "CX" },
|
||||||
|
{ name: "Cocos (Keeling) Islands", code: "CC" },
|
||||||
|
{ name: "Colombia", code: "CO" },
|
||||||
|
{ name: "Comoros", code: "KM" },
|
||||||
|
{ name: "Congo", code: "CG" },
|
||||||
|
{ name: "Congo, The Democratic Republic of the", code: "CD" },
|
||||||
|
{ name: "Cook Islands", code: "CK" },
|
||||||
|
{ name: "Costa Rica", code: "CR" },
|
||||||
|
{ name: 'Cote D"Ivoire', code: "CI" },
|
||||||
|
{ name: "Croatia", code: "HR" },
|
||||||
|
{ name: "Cuba", code: "CU" },
|
||||||
|
{ name: "Curacao", code: "CW" },
|
||||||
|
{ name: "Cyprus", code: "CY" },
|
||||||
|
{ name: "Czech Republic", code: "CZ" },
|
||||||
|
{ name: "Denmark", code: "DK" },
|
||||||
|
{ name: "Djibouti", code: "DJ" },
|
||||||
|
{ name: "Dominica", code: "DM" },
|
||||||
|
{ name: "Dominican Republic", code: "DO" },
|
||||||
|
{ name: "Ecuador", code: "EC" },
|
||||||
|
{ name: "Egypt", code: "EG" },
|
||||||
|
{ name: "El Salvador", code: "SV" },
|
||||||
|
{ name: "Equatorial Guinea", code: "GQ" },
|
||||||
|
{ name: "Eritrea", code: "ER" },
|
||||||
|
{ name: "Estonia", code: "EE" },
|
||||||
|
{ name: "Eswatini", code: "SZ" },
|
||||||
|
{ name: "Ethiopia", code: "ET" },
|
||||||
|
{ name: "Falkland Islands (Malvinas)", code: "FK" },
|
||||||
|
{ name: "Faroe Islands", code: "FO" },
|
||||||
|
{ name: "Fiji", code: "FJ" },
|
||||||
|
{ name: "Finland", code: "FI" },
|
||||||
|
{ name: "France", code: "FR" },
|
||||||
|
{ name: "French Guiana", code: "GF" },
|
||||||
|
{ name: "French Polynesia", code: "PF" },
|
||||||
|
{ name: "French Southern Territories", code: "TF" },
|
||||||
|
{ name: "Gabon", code: "GA" },
|
||||||
|
{ name: "Gambia", code: "GM" },
|
||||||
|
{ name: "Georgia", code: "GE" },
|
||||||
|
{ name: "Germany", code: "DE" },
|
||||||
|
{ name: "Ghana", code: "GH" },
|
||||||
|
{ name: "Gibraltar", code: "GI" },
|
||||||
|
{ name: "Greece", code: "GR" },
|
||||||
|
{ name: "Greenland", code: "GL" },
|
||||||
|
{ name: "Grenada", code: "GD" },
|
||||||
|
{ name: "Guadeloupe", code: "GP" },
|
||||||
|
{ name: "Guam", code: "GU" },
|
||||||
|
{ name: "Guatemala", code: "GT" },
|
||||||
|
{ name: "Guernsey", code: "GG" },
|
||||||
|
{ name: "Guinea", code: "GN" },
|
||||||
|
{ name: "Guinea-Bissau", code: "GW" },
|
||||||
|
{ name: "Guyana", code: "GY" },
|
||||||
|
{ name: "Haiti", code: "HT" },
|
||||||
|
{ name: "Heard Island and Mcdonald Islands", code: "HM" },
|
||||||
|
{ name: "Holy See (Vatican City State)", code: "VA" },
|
||||||
|
{ name: "Honduras", code: "HN" },
|
||||||
|
{ name: "Hong Kong", code: "HK" },
|
||||||
|
{ name: "Hungary", code: "HU" },
|
||||||
|
{ name: "Iceland", code: "IS" },
|
||||||
|
{ name: "India", code: "IN" },
|
||||||
|
{ name: "Indonesia", code: "ID" },
|
||||||
|
{ name: "Iran, Islamic Republic Of", code: "IR" },
|
||||||
|
{ name: "Iraq", code: "IQ" },
|
||||||
|
{ name: "Ireland", code: "IE" },
|
||||||
|
{ name: "Isle of Man", code: "IM" },
|
||||||
|
{ name: "Israel", code: "IL" },
|
||||||
|
{ name: "Italy", code: "IT" },
|
||||||
|
{ name: "Ivory Coast", code: "CI" },
|
||||||
|
{ name: "Jamaica", code: "JM" },
|
||||||
|
{ name: "Japan", code: "JP" },
|
||||||
|
{ name: "Jersey", code: "JE" },
|
||||||
|
{ name: "Jordan", code: "JO" },
|
||||||
|
{ name: "Kazakhstan", code: "KZ" },
|
||||||
|
{ name: "Kenya", code: "KE" },
|
||||||
|
{ name: "Kiribati", code: "KI" },
|
||||||
|
{ name: 'Korea, Democratic People"S Republic of', code: "KP" },
|
||||||
|
{ name: "Korea, Republic of", code: "KR" },
|
||||||
|
{ name: "Kuwait", code: "KW" },
|
||||||
|
{ name: "Kyrgyzstan", code: "KG" },
|
||||||
|
{ name: 'Lao People"S Democratic Republic', code: "LA" },
|
||||||
|
{ name: "Laos", code: "LA" },
|
||||||
|
{ name: "Latvia", code: "LV" },
|
||||||
|
{ name: "Lebanon", code: "LB" },
|
||||||
|
{ name: "Lesotho", code: "LS" },
|
||||||
|
{ name: "Liberia", code: "LR" },
|
||||||
|
{ name: "Libyan Arab Jamahiriya", code: "LY" },
|
||||||
|
{ name: "Liechtenstein", code: "LI" },
|
||||||
|
{ name: "Lithuania", code: "LT" },
|
||||||
|
{ name: "Luxembourg", code: "LU" },
|
||||||
|
{ name: "Macao", code: "MO" },
|
||||||
|
{ name: "Macedonia, The Former Yugoslav Republic of", code: "MK" },
|
||||||
|
{ name: "Madagascar", code: "MG" },
|
||||||
|
{ name: "Malawi", code: "MW" },
|
||||||
|
{ name: "Malaysia", code: "MY" },
|
||||||
|
{ name: "Maldives", code: "MV" },
|
||||||
|
{ name: "Mali", code: "ML" },
|
||||||
|
{ name: "Malta", code: "MT" },
|
||||||
|
{ name: "Marshall Islands", code: "MH" },
|
||||||
|
{ name: "Martinique", code: "MQ" },
|
||||||
|
{ name: "Mauritania", code: "MR" },
|
||||||
|
{ name: "Mauritius", code: "MU" },
|
||||||
|
{ name: "Mayotte", code: "YT" },
|
||||||
|
{ name: "Mexico", code: "MX" },
|
||||||
|
{ name: "Micronesia, Federated States of", code: "FM" },
|
||||||
|
{ name: "Moldova, Republic of", code: "MD" },
|
||||||
|
{ name: "Monaco", code: "MC" },
|
||||||
|
{ name: "Mongolia", code: "MN" },
|
||||||
|
{ name: "Montenegro", code: "ME" },
|
||||||
|
{ name: "Montserrat", code: "MS" },
|
||||||
|
{ name: "Morocco", code: "MA" },
|
||||||
|
{ name: "Mozambique", code: "MZ" },
|
||||||
|
{ name: "Myanmar", code: "MM" },
|
||||||
|
{ name: "Namibia", code: "NA" },
|
||||||
|
{ name: "Nauru", code: "NR" },
|
||||||
|
{ name: "Nepal", code: "NP" },
|
||||||
|
{ name: "Netherlands", code: "NL" },
|
||||||
|
{ name: "Netherlands Antilles", code: "AN" },
|
||||||
|
{ name: "New Caledonia", code: "NC" },
|
||||||
|
{ name: "New Zealand", code: "NZ" },
|
||||||
|
{ name: "Nicaragua", code: "NI" },
|
||||||
|
{ name: "Niger", code: "NE" },
|
||||||
|
{ name: "Nigeria", code: "NG" },
|
||||||
|
{ name: "Niue", code: "NU" },
|
||||||
|
{ name: "Norfolk Island", code: "NF" },
|
||||||
|
{ name: "Northern Mariana Islands", code: "MP" },
|
||||||
|
{ name: "Norway", code: "NO" },
|
||||||
|
{ name: "Oman", code: "OM" },
|
||||||
|
{ name: "Pakistan", code: "PK" },
|
||||||
|
{ name: "Palau", code: "PW" },
|
||||||
|
{ name: "Palestinian Territory, Occupied", code: "PS" },
|
||||||
|
{ name: "Panama", code: "PA" },
|
||||||
|
{ name: "Papua New Guinea", code: "PG" },
|
||||||
|
{ name: "Paraguay", code: "PY" },
|
||||||
|
{ name: "Peru", code: "PE" },
|
||||||
|
{ name: "Philippines", code: "PH" },
|
||||||
|
{ name: "Pitcairn", code: "PN" },
|
||||||
|
{ name: "Poland", code: "PL" },
|
||||||
|
{ name: "Portugal", code: "PT" },
|
||||||
|
{ name: "Puerto Rico", code: "PR" },
|
||||||
|
{ name: "Qatar", code: "QA" },
|
||||||
|
{ name: "RWANDA", code: "RW" },
|
||||||
|
{ name: "Reunion", code: "RE" },
|
||||||
|
{ name: "Romania", code: "RO" },
|
||||||
|
{ name: "Russian Federation", code: "RU" },
|
||||||
|
{ name: "Saint Barthelemy", code: "BL" },
|
||||||
|
{ name: "Saint Helena", code: "SH" },
|
||||||
|
{ name: "Saint Kitts and Nevis", code: "KN" },
|
||||||
|
{ name: "Saint Lucia", code: "LC" },
|
||||||
|
{ name: "Saint Martin", code: "MF" },
|
||||||
|
{ name: "Saint Pierre and Miquelon", code: "PM" },
|
||||||
|
{ name: "Saint Vincent and the Grenadines", code: "VC" },
|
||||||
|
{ name: "Samoa", code: "WS" },
|
||||||
|
{ name: "San Marino", code: "SM" },
|
||||||
|
{ name: "Sao Tome and Principe", code: "ST" },
|
||||||
|
{ name: "Saudi Arabia", code: "SA" },
|
||||||
|
{ name: "Senegal", code: "SN" },
|
||||||
|
{ name: "Serbia", code: "RS" },
|
||||||
|
{ name: "Seychelles", code: "SC" },
|
||||||
|
{ name: "Sierra Leone", code: "SL" },
|
||||||
|
{ name: "Singapore", code: "SG" },
|
||||||
|
{ name: "Sint Maarten", code: "SX" },
|
||||||
|
{ name: "Slovakia", code: "SK" },
|
||||||
|
{ name: "Slovenia", code: "SI" },
|
||||||
|
{ name: "Solomon Islands", code: "SB" },
|
||||||
|
{ name: "Somalia", code: "SO" },
|
||||||
|
{ name: "South Africa", code: "ZA" },
|
||||||
|
{ name: "South Georgia and the South Sandwich Islands", code: "GS" },
|
||||||
|
{ name: "South Sudan", code: "SS" },
|
||||||
|
{ name: "Spain", code: "ES" },
|
||||||
|
{ name: "Sri Lanka", code: "LK" },
|
||||||
|
{ name: "Sudan", code: "SD" },
|
||||||
|
{ name: "Suriname", code: "SR" },
|
||||||
|
{ name: "Svalbard and Jan Mayen", code: "SJ" },
|
||||||
|
{ name: "Swaziland", code: "SZ" },
|
||||||
|
{ name: "Sweden", code: "SE" },
|
||||||
|
{ name: "Switzerland", code: "CH" },
|
||||||
|
{ name: "Syrian Arab Republic", code: "SY" },
|
||||||
|
{ name: "Taiwan", code: "TW" },
|
||||||
|
{ name: "Tajikistan", code: "TJ" },
|
||||||
|
{ name: "Tanzania, United Republic of", code: "TZ" },
|
||||||
|
{ name: "Thailand", code: "TH" },
|
||||||
|
{ name: "Timor-Leste", code: "TL" },
|
||||||
|
{ name: "Togo", code: "TG" },
|
||||||
|
{ name: "Tokelau", code: "TK" },
|
||||||
|
{ name: "Tonga", code: "TO" },
|
||||||
|
{ name: "Trinidad and Tobago", code: "TT" },
|
||||||
|
{ name: "Tunisia", code: "TN" },
|
||||||
|
{ name: "Turkey", code: "TR" },
|
||||||
|
{ name: "Turkmenistan", code: "TM" },
|
||||||
|
{ name: "Turks and Caicos Islands", code: "TC" },
|
||||||
|
{ name: "Tuvalu", code: "TV" },
|
||||||
|
{ name: "Uganda", code: "UG" },
|
||||||
|
{ name: "Ukraine", code: "UA" },
|
||||||
|
{ name: "United Arab Emirates", code: "AE" },
|
||||||
|
{ name: "United Kingdom", code: "GB" },
|
||||||
|
{ name: "United States", code: "US" },
|
||||||
|
{ name: "United States Minor Outlying Islands", code: "UM" },
|
||||||
|
{ name: "Uruguay", code: "UY" },
|
||||||
|
{ name: "Uzbekistan", code: "UZ" },
|
||||||
|
{ name: "Vanuatu", code: "VU" },
|
||||||
|
{ name: "Venezuela", code: "VE" },
|
||||||
|
{ name: "Viet Nam", code: "VN" },
|
||||||
|
{ name: "Vietnam", code: "VN" },
|
||||||
|
{ name: "Virgin Islands, British", code: "VG" },
|
||||||
|
{ name: "Virgin Islands, U.S.", code: "VI" },
|
||||||
|
{ name: "Wallis and Futuna", code: "WF" },
|
||||||
|
{ name: "Western Sahara", code: "EH" },
|
||||||
|
{ name: "Yemen", code: "YE" },
|
||||||
|
{ name: "Zambia", code: "ZM" },
|
||||||
|
{ name: "Zimbabwe", code: "ZW" },
|
||||||
|
{ name: "Åland Islands", code: "AX" },
|
||||||
|
]
|
||||||
61
components/TempDesignSystem/Form/Country/country.module.css
Normal file
61
components/TempDesignSystem/Form/Country/country.module.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
.container {
|
||||||
|
--select-border: 2px solid var(--some-black-color, #757575);
|
||||||
|
--select-radius: 0.4rem;
|
||||||
|
--select-width: min(28rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comboBoxContainer {
|
||||||
|
background-color: var(--some-white-color, #fff);
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "content";
|
||||||
|
width: var(--select-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background-color: var(--some-white-color, #fff);
|
||||||
|
border: var(--select-border);
|
||||||
|
border-radius: var(--select-radius);
|
||||||
|
grid-area: content;
|
||||||
|
height: 4rem;
|
||||||
|
padding: 0.8rem 1.6rem;
|
||||||
|
width: var(--select-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input,
|
||||||
|
.listBoxItem {
|
||||||
|
color: var(--some-black-color, #757575);
|
||||||
|
font-family: var(--ff-fira-sans);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -1.5%;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
grid-area: content;
|
||||||
|
height: 100%;
|
||||||
|
justify-self: flex-end;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
background-color: var(--some-white-color, #fff);
|
||||||
|
border: var(--select-border);
|
||||||
|
border-radius: var(--select-radius);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1.6rem 1.6rem 1.6rem 0.8rem;
|
||||||
|
width: var(--select-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listBoxItem {
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listBoxItem[data-selected="true"],
|
||||||
|
.listBoxItem[data-focused="true"] {
|
||||||
|
background-color: rgba(75, 75, 75, 0.2);
|
||||||
|
}
|
||||||
7
components/TempDesignSystem/Form/Country/country.ts
Normal file
7
components/TempDesignSystem/Form/Country/country.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { RegisterOptions } from "react-hook-form"
|
||||||
|
|
||||||
|
export type CountryProps = {
|
||||||
|
name?: string
|
||||||
|
placeholder?: string
|
||||||
|
registerOptions?: RegisterOptions
|
||||||
|
}
|
||||||
109
components/TempDesignSystem/Form/Country/index.tsx
Normal file
109
components/TempDesignSystem/Form/Country/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"use client"
|
||||||
|
import { useController, useFormContext } from "react-hook-form"
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
import { _ } from "@/lib/translation"
|
||||||
|
import { countries } from "./countries"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ComboBox,
|
||||||
|
FieldError,
|
||||||
|
Input,
|
||||||
|
ListBox,
|
||||||
|
ListBoxItem,
|
||||||
|
Popover,
|
||||||
|
type Key,
|
||||||
|
} from "react-aria-components"
|
||||||
|
import { ErrorMessage } from "@hookform/error-message"
|
||||||
|
import SelectChevron from "../SelectChevron"
|
||||||
|
|
||||||
|
import styles from "./country.module.css"
|
||||||
|
|
||||||
|
import type { CountryProps } from "./country"
|
||||||
|
|
||||||
|
export default function CountrySelect({
|
||||||
|
name = "country",
|
||||||
|
placeholder = "Select a country",
|
||||||
|
registerOptions,
|
||||||
|
}: CountryProps) {
|
||||||
|
const divRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [divElement, setDivElement] = useState(divRef.current)
|
||||||
|
const { control, setValue } = useFormContext()
|
||||||
|
const { field } = useController({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
rules: registerOptions,
|
||||||
|
})
|
||||||
|
const [selectedKey, setSelectedKey] = useState(() => {
|
||||||
|
if (field.value) {
|
||||||
|
const selected = countries.find(
|
||||||
|
(country) =>
|
||||||
|
country.name === field.value || country.code === field.value
|
||||||
|
)
|
||||||
|
if (selected) {
|
||||||
|
return selected.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleChange(country: Key) {
|
||||||
|
setSelectedKey(String(country))
|
||||||
|
setValue(name, country)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (divRef.current) {
|
||||||
|
setDivElement(divRef.current)
|
||||||
|
}
|
||||||
|
}, [divRef.current])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container} ref={divRef}>
|
||||||
|
<ComboBox
|
||||||
|
className={styles.select}
|
||||||
|
isRequired={!!registerOptions?.required}
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
onSelectionChange={handleChange}
|
||||||
|
ref={field.ref}
|
||||||
|
selectedKey={selectedKey}
|
||||||
|
>
|
||||||
|
<div className={styles.comboBoxContainer}>
|
||||||
|
<Input className={styles.input} placeholder={_(placeholder)} />
|
||||||
|
<Button className={styles.button}>
|
||||||
|
<SelectChevron />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FieldError>
|
||||||
|
<ErrorMessage name={name} />
|
||||||
|
</FieldError>
|
||||||
|
<Popover
|
||||||
|
className={styles.popover}
|
||||||
|
placement="bottom"
|
||||||
|
shouldFlip={false}
|
||||||
|
/**
|
||||||
|
* react-aria uses portals to render Popover in body
|
||||||
|
* unless otherwise specified. We need it to be contained
|
||||||
|
* by this component to both access css variables assigned
|
||||||
|
* on the container as well as to not overflow it at any time.
|
||||||
|
*/
|
||||||
|
UNSTABLE_portalContainer={divElement ?? undefined}
|
||||||
|
>
|
||||||
|
<ListBox>
|
||||||
|
{countries.map((country, idx) => (
|
||||||
|
<ListBoxItem
|
||||||
|
className={styles.listBoxItem}
|
||||||
|
id={country.name}
|
||||||
|
key={`${country.code}-${idx}`}
|
||||||
|
>
|
||||||
|
{country.name}
|
||||||
|
</ListBoxItem>
|
||||||
|
))}
|
||||||
|
</ListBox>
|
||||||
|
</Popover>
|
||||||
|
</ComboBox>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
82
components/TempDesignSystem/Form/Date/Select/index.tsx
Normal file
82
components/TempDesignSystem/Form/Date/Select/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"use client"
|
||||||
|
import { useEffect, useRef, useState, type FocusEvent } from "react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
ListBox,
|
||||||
|
ListBoxItem,
|
||||||
|
Popover,
|
||||||
|
Select as ReactAriaSelect,
|
||||||
|
SelectValue,
|
||||||
|
type Key,
|
||||||
|
} from "react-aria-components"
|
||||||
|
import SelectChevron from "../../SelectChevron"
|
||||||
|
|
||||||
|
import styles from "./select.module.css"
|
||||||
|
|
||||||
|
import type { SelectProps } from "./select"
|
||||||
|
|
||||||
|
export default function Select({
|
||||||
|
items,
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
onSelect,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
}: SelectProps) {
|
||||||
|
const divRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [divElement, setDivElement] = useState(divRef.current)
|
||||||
|
|
||||||
|
function handleOnSelect(key: Key) {
|
||||||
|
onSelect(key, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (divRef.current) {
|
||||||
|
setDivElement(divRef.current)
|
||||||
|
}
|
||||||
|
}, [divRef.current])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.date} ref={divRef}>
|
||||||
|
<ReactAriaSelect
|
||||||
|
className={styles.select}
|
||||||
|
onSelectionChange={handleOnSelect}
|
||||||
|
placeholder={placeholder}
|
||||||
|
selectedKey={value as Key}
|
||||||
|
>
|
||||||
|
<Label className={styles.label}>{label}</Label>
|
||||||
|
<Button className={styles.input}>
|
||||||
|
<SelectValue />
|
||||||
|
<SelectChevron />
|
||||||
|
</Button>
|
||||||
|
<Popover
|
||||||
|
className={styles.popover}
|
||||||
|
placement="bottom"
|
||||||
|
shouldFlip={false}
|
||||||
|
/**
|
||||||
|
* react-aria uses portals to render Popover in body
|
||||||
|
* unless otherwise specified. We need it to be contained
|
||||||
|
* by this component to both access css variables assigned
|
||||||
|
* on the container as well as to not overflow it at any time.
|
||||||
|
*/
|
||||||
|
UNSTABLE_portalContainer={divElement ?? undefined}
|
||||||
|
>
|
||||||
|
<ListBox className={styles.listBox}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<ListBoxItem
|
||||||
|
key={item}
|
||||||
|
className={styles.listBoxItem}
|
||||||
|
id={item}
|
||||||
|
textValue={String(item)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</ListBoxItem>
|
||||||
|
))}
|
||||||
|
</ListBox>
|
||||||
|
</Popover>
|
||||||
|
</ReactAriaSelect>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
.date {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-family: var(--ff-fira-sans);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--some-white-color, #fff);
|
||||||
|
border: var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--some-black-color, #757575);
|
||||||
|
display: grid;
|
||||||
|
font-family: var(--ff-fira-sans);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 400;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
height: 4rem;
|
||||||
|
letter-spacing: -1.5%;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
padding: 0.8rem 1rem 0.8rem 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
background-color: var(--some-white-color, #fff);
|
||||||
|
border: var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listBox {
|
||||||
|
padding: 1.6rem 1.6rem 1.6rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listBoxItem {
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listBoxItem[data-selected="true"],
|
||||||
|
.listBoxItem[data-focused="true"] {
|
||||||
|
background-color: rgba(75, 75, 75, 0.2);
|
||||||
|
}
|
||||||
16
components/TempDesignSystem/Form/Date/Select/select.ts
Normal file
16
components/TempDesignSystem/Form/Date/Select/select.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Key } from "react-aria-components"
|
||||||
|
|
||||||
|
export const enum DateName {
|
||||||
|
date = "date",
|
||||||
|
month = "month",
|
||||||
|
year = "year",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectProps
|
||||||
|
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onSelect"> {
|
||||||
|
items: number[]
|
||||||
|
label: string
|
||||||
|
name: DateName
|
||||||
|
onSelect: (key: Key, name: DateName) => void
|
||||||
|
placeholder?: string
|
||||||
|
}
|
||||||
30
components/TempDesignSystem/Form/Date/date.module.css
Normal file
30
components/TempDesignSystem/Form/Date/date.module.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.container {
|
||||||
|
--border: 2px solid var(--some-black-color, #757575);
|
||||||
|
--radius: 0.4rem;
|
||||||
|
--width: min(28rem, 100%);
|
||||||
|
|
||||||
|
--width-day: 6rem;
|
||||||
|
--width-month: 6rem;
|
||||||
|
--width-year: 8rem;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
gap: 0.8rem;
|
||||||
|
grid-template-areas: "day month year";
|
||||||
|
grid-template-columns: min(--width-day, 1fr) min(--width-month, 1fr) min(
|
||||||
|
--width-year,
|
||||||
|
2fr
|
||||||
|
);
|
||||||
|
width: var(--width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day {
|
||||||
|
grid-area: day;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month {
|
||||||
|
grid-area: month;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year {
|
||||||
|
grid-area: year;
|
||||||
|
}
|
||||||
9
components/TempDesignSystem/Form/Date/date.ts
Normal file
9
components/TempDesignSystem/Form/Date/date.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Control, RegisterOptions } from "react-hook-form"
|
||||||
|
import type { EditProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
|
||||||
|
|
||||||
|
export interface DateProps
|
||||||
|
extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
|
control: Control<EditProfileSchema>
|
||||||
|
name: keyof EditProfileSchema
|
||||||
|
registerOptions: RegisterOptions<EditProfileSchema>
|
||||||
|
}
|
||||||
120
components/TempDesignSystem/Form/Date/index.tsx
Normal file
120
components/TempDesignSystem/Form/Date/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"use client"
|
||||||
|
import { parseDate } from "@internationalized/date"
|
||||||
|
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||||
|
|
||||||
|
import { _ } from "@/lib/translation"
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
import { rangeArray } from "@/utils/rangeArray"
|
||||||
|
|
||||||
|
import {
|
||||||
|
DateInput,
|
||||||
|
DatePicker,
|
||||||
|
DateSegment,
|
||||||
|
Group,
|
||||||
|
} from "react-aria-components"
|
||||||
|
import Select from "./Select"
|
||||||
|
|
||||||
|
import styles from "./date.module.css"
|
||||||
|
|
||||||
|
import { DateName } from "./Select/select"
|
||||||
|
import type { DateProps } from "./date"
|
||||||
|
import type { Key } from "react-aria-components"
|
||||||
|
|
||||||
|
/** TODO: Get selecting with Enter-key to work */
|
||||||
|
export default function DateSelect({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
registerOptions,
|
||||||
|
}: DateProps) {
|
||||||
|
const d = useWatch({ name })
|
||||||
|
const { setValue } = useFormContext()
|
||||||
|
const { field } = useController({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
rules: registerOptions,
|
||||||
|
})
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const months = rangeArray(1, 12)
|
||||||
|
const years = rangeArray(1900, currentYear).reverse()
|
||||||
|
|
||||||
|
function handleOnSelect(select: Key, selector: DateName) {
|
||||||
|
/**
|
||||||
|
* Months are 0 index based and therefore we
|
||||||
|
* must subtract by 1 to get the selected month
|
||||||
|
*/
|
||||||
|
if (selector === DateName.month) {
|
||||||
|
select = Number(select) - 1
|
||||||
|
}
|
||||||
|
const newDate = dt(d).set(selector, Number(select))
|
||||||
|
setValue(name, newDate.format("YYYY-MM-DD"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
granularity="day"
|
||||||
|
isRequired={!!registerOptions.required}
|
||||||
|
name={name}
|
||||||
|
ref={field.ref}
|
||||||
|
value={parseDate(d)}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<DateInput className={styles.container}>
|
||||||
|
{(segment) => {
|
||||||
|
switch (segment.type) {
|
||||||
|
case "day":
|
||||||
|
let days = []
|
||||||
|
if (segment.maxValue && segment.minValue) {
|
||||||
|
days = rangeArray(segment.minValue, segment.maxValue)
|
||||||
|
} else {
|
||||||
|
days = Array.from(Array(segment.maxValue).keys()).map(
|
||||||
|
(i) => i + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<DateSegment className={styles.day} segment={segment}>
|
||||||
|
<Select
|
||||||
|
items={days}
|
||||||
|
label={_("Day")}
|
||||||
|
name={DateName.date}
|
||||||
|
onSelect={handleOnSelect}
|
||||||
|
placeholder={_("DD")}
|
||||||
|
value={segment.value}
|
||||||
|
/>
|
||||||
|
</DateSegment>
|
||||||
|
)
|
||||||
|
case "month":
|
||||||
|
return (
|
||||||
|
<DateSegment className={styles.month} segment={segment}>
|
||||||
|
<Select
|
||||||
|
items={months}
|
||||||
|
label={_("Month")}
|
||||||
|
name={DateName.month}
|
||||||
|
onSelect={handleOnSelect}
|
||||||
|
placeholder={_("MM")}
|
||||||
|
value={segment.value}
|
||||||
|
/>
|
||||||
|
</DateSegment>
|
||||||
|
)
|
||||||
|
case "year":
|
||||||
|
return (
|
||||||
|
<DateSegment className={styles.year} segment={segment}>
|
||||||
|
<Select
|
||||||
|
items={years}
|
||||||
|
label={_("Year")}
|
||||||
|
name={DateName.year}
|
||||||
|
onSelect={handleOnSelect}
|
||||||
|
placeholder={_("YYYY")}
|
||||||
|
value={segment.value}
|
||||||
|
/>
|
||||||
|
</DateSegment>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
/** DateInput forces return of ReactElement */
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</DateInput>
|
||||||
|
</Group>
|
||||||
|
</DatePicker>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.message {
|
||||||
|
color: var(--some-red-color, #d64242);
|
||||||
|
font-family: var(--ff-fira-sans);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 2.2rem;
|
||||||
|
margin: 0.4rem 0 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { FieldValuesFromFieldErrors } from "@hookform/error-message"
|
||||||
|
import type {
|
||||||
|
FieldErrors,
|
||||||
|
FieldName,
|
||||||
|
FieldValues,
|
||||||
|
Message,
|
||||||
|
MultipleFieldErrors,
|
||||||
|
} from "react-hook-form"
|
||||||
|
|
||||||
|
export type ErrorMessageProps<TFieldErrors> = {
|
||||||
|
errors?: FieldErrors<FieldValues>
|
||||||
|
name: FieldName<FieldValuesFromFieldErrors<TFieldErrors>>
|
||||||
|
message?: Message
|
||||||
|
render?: (data: {
|
||||||
|
message: Message
|
||||||
|
messages?: MultipleFieldErrors
|
||||||
|
}) => React.ReactNode
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user