feat(WEB-170): edit profile view
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,114 +1,16 @@
|
|||||||
export const breadcrumbs = {
|
export const breadcrumbs = {
|
||||||
"/my-pages": [
|
"/my-pages": [
|
||||||
{
|
{
|
||||||
title: "My Pages"
|
title: "My Pages",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"/my-pages/profile": [
|
"/my-pages/profile": [
|
||||||
{
|
{
|
||||||
href: "/my-pages",
|
href: "/my-pages",
|
||||||
title: "My Pages"
|
title: "My Pages",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "My Profile",
|
title: "My Profile",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const challenges = {
|
|
||||||
journeys: [
|
|
||||||
{
|
|
||||||
tag: "After work queen",
|
|
||||||
title: "Try 3 Hotel Bars, Pocket 200 Points",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "Dine & Shine",
|
|
||||||
title: "Visit 3 scandic Restaurants, Earn 150 Points",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
victories: [
|
|
||||||
{
|
|
||||||
tag: "Capital Explorer",
|
|
||||||
title: "Stay in 3 scandic hotels, in three Capitals, Gain 2000 Points",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: "Friends Feast",
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const extendedUser = {
|
|
||||||
journeys: challenges.journeys,
|
|
||||||
membershipId: 30812404844732,
|
|
||||||
nights: 14,
|
|
||||||
points: 20720,
|
|
||||||
qualifyingPoints: 5000,
|
|
||||||
shortcuts,
|
|
||||||
stays,
|
|
||||||
victories: challenges.victories,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,53 +8,11 @@ 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
|
<div
|
||||||
className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`}
|
className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`}
|
||||||
@@ -62,7 +20,7 @@ export default async function MyPagesLayout({
|
|||||||
<Header lang={params.lang} />
|
<Header lang={params.lang} />
|
||||||
<Breadcrumbs breadcrumbs={breadcrumbs} 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 @@
|
|||||||
|
import { _ } from "@/lib/translation"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
import { extendedUser } from "./_constants"
|
|
||||||
|
|
||||||
import MaxWidth from "@/components/MaxWidth"
|
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"
|
||||||
@@ -12,19 +11,15 @@ 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,
|
|
||||||
...extendedUser,
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<MaxWidth className={styles.blocks} tag="main">
|
<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")}
|
||||||
/>
|
/>
|
||||||
</MaxWidth>
|
</MaxWidth>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export default function Default() {
|
export default function Default() {
|
||||||
return null
|
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 />
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export default function DefaultEdit() {
|
export default function Default() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
import Button from "@/components/TempDesignSystem/Button";
|
export default function Default() {
|
||||||
|
return null
|
||||||
export default function EditProfile() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button form="edit-profile" type="reset">Cancel</Button>
|
|
||||||
<Button form="edit-profile" type="submit">Save</Button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import Modal from "@/components/Modal";
|
|
||||||
|
|
||||||
export default function VerifyCode() {
|
|
||||||
return (
|
|
||||||
<Modal>
|
|
||||||
<Modal.Header>
|
|
||||||
<Modal.Title>Verify Code</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,3 @@
|
|||||||
import Button from "@/components/TempDesignSystem/Button";
|
export default function Default() {
|
||||||
import Link from "@/components/TempDesignSystem/Link";
|
return null
|
||||||
|
|
||||||
import styles from "./view.module.css"
|
|
||||||
|
|
||||||
import type { LangParams, PageArgs } from "@/types/params";
|
|
||||||
|
|
||||||
export default function ProfileView({ params }: PageArgs<LangParams>) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
bgcolor="quarternary"
|
|
||||||
className={styles.btn}
|
|
||||||
size="small"
|
|
||||||
weight="regular"
|
|
||||||
>
|
|
||||||
<Link href={`/${params.lang}/my-pages/profile/verify`}>
|
|
||||||
Edit
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.btn {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,16 +1,33 @@
|
|||||||
type ProfileLayoutProps = React.PropsWithChildren<{
|
import MaxWidth from "@/components/MaxWidth"
|
||||||
edit: React.ReactNode
|
|
||||||
verifyCode: React.ReactNode
|
|
||||||
view: React.ReactNode
|
|
||||||
}>
|
|
||||||
|
|
||||||
export default function ProfileLayout({ children, edit, verifyCode, view }: ProfileLayoutProps) {
|
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 (
|
return (
|
||||||
<>
|
<MaxWidth className={styles.page} tag="main">
|
||||||
{edit}
|
<div className={styles.btns}>
|
||||||
{view}
|
{edit}
|
||||||
{children}
|
{view}
|
||||||
{verifyCode}
|
</div>
|
||||||
</>
|
{profile}
|
||||||
|
<section className={styles.cards}>
|
||||||
|
{communication}
|
||||||
|
{wishes}
|
||||||
|
{membershipCard}
|
||||||
|
{creditCards}
|
||||||
|
{password}
|
||||||
|
</section>
|
||||||
|
</MaxWidth>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
.page {
|
|
||||||
display: grid;
|
|
||||||
gap: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.4rem;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { serverClient } from "@/lib/trpc/server";
|
|
||||||
import { extendedUser } from "../_constants";
|
|
||||||
|
|
||||||
import CommunicationPreferences from "@/components/MyProfile/CommunicationPreferences";
|
|
||||||
import CreditCards from "@/components/MyProfile/CreditCards";
|
|
||||||
import MaxWidth from "@/components/MaxWidth";
|
|
||||||
import MembershipCard from "@/components/MyProfile/MembershipCard";
|
|
||||||
import Password from "@/components/MyProfile/Password";
|
|
||||||
import Profile from "@/components/MyProfile/Profile";
|
|
||||||
import Wishes from "@/components/MyProfile/Wishes";
|
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
|
||||||
import Modal from "@/components/Modal";
|
|
||||||
|
|
||||||
export default async function MyProfile() {
|
|
||||||
const data = await serverClient().user.get()
|
|
||||||
const user = {
|
|
||||||
...data,
|
|
||||||
...extendedUser,
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<MaxWidth className={styles.page} tag="main">
|
|
||||||
<Modal>
|
|
||||||
<h1>HALLÅ ELLER!?!</h1>
|
|
||||||
</Modal>
|
|
||||||
<Profile user={user} />
|
|
||||||
<section className={styles.cards}>
|
|
||||||
<CommunicationPreferences />
|
|
||||||
<Wishes />
|
|
||||||
<MembershipCard />
|
|
||||||
<CreditCards />
|
|
||||||
<Password />
|
|
||||||
</section>
|
|
||||||
</MaxWidth>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export default function VerifyPage() {
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<header>
|
|
||||||
<h1>Verify that code already!</h1>
|
|
||||||
</header>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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"
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
.container {
|
.container {
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
}
|
position: relative;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
.header {}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import styles from "./header.module.css"
|
|
||||||
|
|
||||||
export default function Header({ children }: React.PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<header className={styles.header}>
|
|
||||||
{children}
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { forwardRef } from "react"
|
|
||||||
|
|
||||||
import styles from "./modal.module.css"
|
|
||||||
|
|
||||||
const Modal = forwardRef<HTMLDivElement, React.PropsWithChildren>(
|
|
||||||
function ({ children }, ref) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.modal}
|
|
||||||
ref={ref}
|
|
||||||
role="dialog"
|
|
||||||
tabIndex={-1}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Modal.displayName = "Modal"
|
|
||||||
|
|
||||||
export default Modal
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
.modal {
|
|
||||||
background-color: var(--some-white-color, #F2F2F2);
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
left: 50%;
|
|
||||||
outline: none;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 3.5rem 7rem;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import { useCallback, useLayoutEffect } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
import { useHandleKeyPress } from "@/hooks/useHandleKeyPress";
|
|
||||||
|
|
||||||
import styles from "./overlay.module.css"
|
|
||||||
|
|
||||||
export default function Overlay({ children }: React.PropsWithChildren) {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const handleOnClose = useCallback(() => {
|
|
||||||
return router.back()
|
|
||||||
}, [router])
|
|
||||||
|
|
||||||
const handleOnEscape = useCallback((evt: KeyboardEvent) => {
|
|
||||||
if (evt.code === "Escape") {
|
|
||||||
handleOnClose()
|
|
||||||
}
|
|
||||||
}, [handleOnClose])
|
|
||||||
|
|
||||||
useHandleKeyPress(handleOnEscape)
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
// Get original body overflow
|
|
||||||
const originalStyle = window.getComputedStyle(document.body).overflow;
|
|
||||||
// Prevent scrolling on mount
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
// Re-enable scrolling when component unmounts
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = originalStyle;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.overlay}
|
|
||||||
onClick={handleOnClose}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
.overlay {
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
|
|
||||||
export default function Portal({ children }: React.PropsWithChildren) {
|
|
||||||
return createPortal(
|
|
||||||
children,
|
|
||||||
document.body
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import styles from "./title.module.css"
|
|
||||||
|
|
||||||
export default function Title({ children }: React.PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<h1 className={styles.heading}>
|
|
||||||
{children}
|
|
||||||
</h1>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.heading {}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import Header from "./Header"
|
|
||||||
import UiModal from "./Modal"
|
|
||||||
import Overlay from "./Overlay"
|
|
||||||
import Portal from "./Portal"
|
|
||||||
import Title from "./Title"
|
|
||||||
|
|
||||||
export default function Modal({ children }: React.PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<Portal>
|
|
||||||
<Overlay>
|
|
||||||
<UiModal>
|
|
||||||
{children}
|
|
||||||
</UiModal>
|
|
||||||
</Overlay>
|
|
||||||
</Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Modal.Header = Header
|
|
||||||
Modal.Title = Title
|
|
||||||
@@ -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,3 +1,4 @@
|
|||||||
|
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"
|
||||||
|
|
||||||
@@ -5,7 +6,6 @@ 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/myPage/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,
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import { usePathname } from "next/navigation"
|
|
||||||
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
|
||||||
|
|
||||||
import type { LangParams } from "@/types/params"
|
|
||||||
|
|
||||||
export default function ClientSidebar({ lang }: LangParams) {
|
|
||||||
const pathname = usePathname()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
currentPath={pathname}
|
|
||||||
href={`/${lang}/my-pages`}
|
|
||||||
variant="sidebar"
|
|
||||||
>
|
|
||||||
My Pages
|
|
||||||
</Link>
|
|
||||||
<Link currentPath={pathname} href="#" variant="sidebar">
|
|
||||||
My Stays
|
|
||||||
</Link>
|
|
||||||
<Link currentPath={pathname} href="#" variant="sidebar">
|
|
||||||
My Points
|
|
||||||
</Link>
|
|
||||||
<Link currentPath={pathname} href="#" variant="sidebar">
|
|
||||||
My Benefits
|
|
||||||
</Link>
|
|
||||||
{/* <Link currentPath={pathname} href="#" variant="sidebar">
|
|
||||||
My Challenges
|
|
||||||
</Link>
|
|
||||||
<Link currentPath={pathname} href="#" variant="sidebar">
|
|
||||||
My Favourites
|
|
||||||
</Link> */}
|
|
||||||
<Link currentPath={pathname} href="#" variant="sidebar">
|
|
||||||
About Scandic Friends
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
currentPath={pathname}
|
|
||||||
href={`/${lang}/my-pages/profile`}
|
|
||||||
variant="sidebar"
|
|
||||||
>
|
|
||||||
My Profile
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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,37 +1,51 @@
|
|||||||
|
import { mapMenuItems } from "./helpers"
|
||||||
|
import { request } from "@/lib/graphql/request"
|
||||||
|
import { GetNavigationMyPages } from "@/lib/graphql/Query/NavigationMyPages.graphql"
|
||||||
|
|
||||||
import { Fragment } from "react"
|
import { Fragment } from "react"
|
||||||
import { LogOut } from "react-feather"
|
import { LogOut } from "react-feather"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
|
||||||
import styles from "./sidebar.module.css"
|
import styles from "./sidebar.module.css"
|
||||||
|
|
||||||
|
import type { GetNavigationMyPagesData } from "@/types/requests/myPages/navigation"
|
||||||
import type { SidebarProps } from "@/types/requests/myPages/navigation"
|
import type { SidebarProps } from "@/types/requests/myPages/navigation"
|
||||||
|
|
||||||
export default function Sidebar({ menuItems }: SidebarProps) {
|
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
|
||||||
? item.subItems.map((subItem) => {
|
? item.subItems.map((subItem) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={subItem.uid}
|
key={subItem.uid}
|
||||||
href={subItem.url}
|
href={subItem.url}
|
||||||
variant={"sidebar"}
|
variant="sidebar"
|
||||||
>
|
>
|
||||||
{subItem.linkText}
|
{subItem.linkText}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
</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,6 +2,7 @@
|
|||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
display: none;
|
display: none;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
/* Based on header and breadcrumbs height, and gap */
|
||||||
top: 14.6rem;
|
top: 14.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,4 +18,4 @@
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"icon label"
|
"icon label"
|
||||||
"icon content";
|
"icon content";
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--some-white-color, #FFF);
|
background-color: var(--some-white-color, #fff);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-family: var(--ff-fira-sans);
|
font-family: var(--ff-fira-sans);
|
||||||
@@ -42,4 +43,5 @@
|
|||||||
color: var(--some-black-color, #000);
|
color: var(--some-black-color, #000);
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
font-size: 1.8rem;
|
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
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import styles from "./lai.module.css"
|
|
||||||
|
|
||||||
export default function LabelAndIcon({ children }: React.PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Icon({ children }: React.PropsWithChildren) {
|
|
||||||
return <span className={styles.icon}>{children}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
function Label({ children }: React.PropsWithChildren) {
|
|
||||||
return <span className={styles.label}>{children}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
function Content({ children }: React.PropsWithChildren) {
|
|
||||||
return <span className={styles.content}>{children}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
LabelAndIcon.Icon = Icon
|
|
||||||
LabelAndIcon.Label = Label
|
|
||||||
LabelAndIcon.Content = Content
|
|
||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,108 +1,70 @@
|
|||||||
import { cva } from "class-variance-authority"
|
import { _ } from "@/lib/translation"
|
||||||
|
|
||||||
import Card from "@/components/MyProfile/Card"
|
import {
|
||||||
import Image from "@/components/Image"
|
CalendarIcon,
|
||||||
import LabelAndIcon from "../LabelAndIcon"
|
EmailIcon,
|
||||||
|
HouseIcon,
|
||||||
|
PhoneIcon,
|
||||||
|
} from "@/components/Icons"
|
||||||
|
import Container from "./Container"
|
||||||
|
import Field from "../Field"
|
||||||
|
|
||||||
import styles from "./profile.module.css"
|
import styles from "./profile.module.css"
|
||||||
|
|
||||||
import type { ProfileProps } from "@/types/components/myPages/myProfile/profile"
|
import type { ProfileProps } from "@/types/components/myPages/myProfile/profile"
|
||||||
|
|
||||||
const profileStyles = cva(styles.profile)
|
export default function Profile(props: ProfileProps) {
|
||||||
|
|
||||||
export default function Profile({ className, user, ...props }: ProfileProps) {
|
|
||||||
return (
|
return (
|
||||||
<Card className={profileStyles({ className })} {...props}>
|
<Container {...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>
|
|
||||||
<section className={styles.info}>
|
<section className={styles.info}>
|
||||||
<LabelAndIcon>
|
<Field>
|
||||||
<LabelAndIcon.Icon>SE</LabelAndIcon.Icon>
|
<Field.Icon>SE</Field.Icon>
|
||||||
<LabelAndIcon.Label>Country</LabelAndIcon.Label>
|
<Field.TextLabel>{_("Country")}</Field.TextLabel>
|
||||||
<LabelAndIcon.Content>Sweden</LabelAndIcon.Content>
|
<Field.Content>Sweden</Field.Content>
|
||||||
</LabelAndIcon>
|
</Field>
|
||||||
<LabelAndIcon>
|
<Field>
|
||||||
<LabelAndIcon.Icon>
|
<Field.Icon>
|
||||||
<Image
|
<CalendarIcon />
|
||||||
alt="Calendar Icon"
|
</Field.Icon>
|
||||||
height={20}
|
<Field.TextLabel>{_("Date of Birth")}</Field.TextLabel>
|
||||||
src="/calendar_month.svg"
|
<Field.Content>27/05/1977</Field.Content>
|
||||||
width={20}
|
</Field>
|
||||||
/>
|
<Field>
|
||||||
</LabelAndIcon.Icon>
|
<Field.Icon>
|
||||||
<LabelAndIcon.Label>Date of Birth</LabelAndIcon.Label>
|
<EmailIcon />
|
||||||
<LabelAndIcon.Content>27/05/1977</LabelAndIcon.Content>
|
</Field.Icon>
|
||||||
</LabelAndIcon>
|
<Field.TextLabel>{_("Email")}</Field.TextLabel>
|
||||||
<LabelAndIcon>
|
<Field.Content>f*********@g****.com</Field.Content>
|
||||||
<LabelAndIcon.Icon>
|
</Field>
|
||||||
<Image
|
<Field>
|
||||||
alt="Email Icon"
|
<Field.Icon>
|
||||||
height={20}
|
<PhoneIcon />
|
||||||
src="/alternate_email.svg"
|
</Field.Icon>
|
||||||
width={20}
|
<Field.TextLabel>{_("Phone number")}</Field.TextLabel>
|
||||||
/>
|
<Field.Content>+46 ******00</Field.Content>
|
||||||
</LabelAndIcon.Icon>
|
</Field>
|
||||||
<LabelAndIcon.Label>Email</LabelAndIcon.Label>
|
<Field>
|
||||||
<LabelAndIcon.Content>f*********@g****.com</LabelAndIcon.Content>
|
<Field.Icon>
|
||||||
</LabelAndIcon>
|
<HouseIcon />
|
||||||
<LabelAndIcon>
|
</Field.Icon>
|
||||||
<LabelAndIcon.Icon>
|
<Field.TextLabel>{_("Address")}</Field.TextLabel>
|
||||||
<Image
|
<Field.Content>T***************</Field.Content>
|
||||||
alt="Cellphone Icon"
|
</Field>
|
||||||
height={20}
|
<Field>
|
||||||
src="/phone.svg"
|
<Field.Icon>
|
||||||
width={20}
|
<HouseIcon />
|
||||||
/>
|
</Field.Icon>
|
||||||
</LabelAndIcon.Icon>
|
<Field.TextLabel>{_("City/State")}</Field.TextLabel>
|
||||||
<LabelAndIcon.Label>Phone number</LabelAndIcon.Label>
|
<Field.Content>S*******</Field.Content>
|
||||||
<LabelAndIcon.Content>+46 ******00</LabelAndIcon.Content>
|
</Field>
|
||||||
</LabelAndIcon>
|
<Field>
|
||||||
<LabelAndIcon>
|
<Field.Icon>
|
||||||
<LabelAndIcon.Icon>
|
<HouseIcon />
|
||||||
<Image
|
</Field.Icon>
|
||||||
alt="House Icon"
|
<Field.TextLabel>{_("Zip code")}</Field.TextLabel>
|
||||||
height={20}
|
<Field.Content>1****</Field.Content>
|
||||||
src="/home.svg"
|
</Field>
|
||||||
width={20}
|
|
||||||
/>
|
|
||||||
</LabelAndIcon.Icon>
|
|
||||||
<LabelAndIcon.Label>Address</LabelAndIcon.Label>
|
|
||||||
<LabelAndIcon.Content>T***************</LabelAndIcon.Content>
|
|
||||||
</LabelAndIcon>
|
|
||||||
<LabelAndIcon>
|
|
||||||
<LabelAndIcon.Icon>
|
|
||||||
<Image
|
|
||||||
alt="House Icon"
|
|
||||||
height={20}
|
|
||||||
src="/home.svg"
|
|
||||||
width={20}
|
|
||||||
/>
|
|
||||||
</LabelAndIcon.Icon>
|
|
||||||
<LabelAndIcon.Label>City/State</LabelAndIcon.Label>
|
|
||||||
<LabelAndIcon.Content>S*******</LabelAndIcon.Content>
|
|
||||||
</LabelAndIcon>
|
|
||||||
<LabelAndIcon>
|
|
||||||
<LabelAndIcon.Icon>
|
|
||||||
<Image
|
|
||||||
alt="House Icon"
|
|
||||||
height={20}
|
|
||||||
src="/home.svg"
|
|
||||||
width={20}
|
|
||||||
/>
|
|
||||||
</LabelAndIcon.Icon>
|
|
||||||
<LabelAndIcon.Label>Zip code</LabelAndIcon.Label>
|
|
||||||
<LabelAndIcon.Content>1****</LabelAndIcon.Content>
|
|
||||||
</LabelAndIcon>
|
|
||||||
</section>
|
</section>
|
||||||
</Card>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,36 +115,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
background-color: var(--scandic-blue, #02838E);
|
background-color: var(--scandic-blue, #02838e);
|
||||||
border: 0.1rem solid var(--scandic-blue, #02838E);
|
border: 0.1rem solid var(--scandic-blue, #02838e);
|
||||||
color: var(--some-white-color, #FFF);
|
color: var(--some-white-color, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
background-color: var(--some-black-color, #000);
|
background-color: var(--some-black-color, #000);
|
||||||
border: 0.1rem solid var(--some-black-color, #000);
|
border: 0.1rem solid var(--some-black-color, #000);
|
||||||
color: var(--some-white-color, #FFF);
|
color: var(--some-white-color, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tertiary {
|
.tertiary {
|
||||||
background-color: var(--some-red-color, #D60728);
|
background-color: var(--some-red-color, #d60728);
|
||||||
border: 0.1rem solid var(--some-red-color, #D60728);
|
border: 0.1rem solid var(--some-red-color, #d60728);
|
||||||
color: var(--some-white-color, #FFF);
|
color: var(--some-white-color, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quarternary {
|
.quarternary {
|
||||||
background-color: var(--some-grey-color, #727272);
|
background-color: var(--some-grey-color, #727272);
|
||||||
border: 0.1rem solid var(--some-black-color, #727272);
|
border: 0.1rem solid var(--some-black-color, #727272);
|
||||||
color: var(--some-white-color, #FFF);
|
color: var(--some-white-color, #fff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.white {
|
.white {
|
||||||
background-color: var(--some-white-color, #FFF);
|
background-color: var(--some-white-color, #fff);
|
||||||
border: 0.1rem solid var(--some-black-color, #000);
|
border: 0.1rem solid var(--some-black-color, #000);
|
||||||
color: var(--some-black-color, #000);
|
color: var(--some-black-color, #000);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.btn:disabled {
|
||||||
background-color: var(--some-grey-color, #D9D9D9);
|
background-color: var(--some-grey-color, #d9d9d9);
|
||||||
color: var(--some-grey-color, #757575);
|
color: var(--some-grey-color, #757575);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default function Button({
|
|||||||
bgcolor,
|
bgcolor,
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
|
intent,
|
||||||
size,
|
size,
|
||||||
variant,
|
variant,
|
||||||
weight,
|
weight,
|
||||||
@@ -20,7 +21,7 @@ export default function Button({
|
|||||||
const classNames = buttonVariants({
|
const classNames = buttonVariants({
|
||||||
bgcolor,
|
bgcolor,
|
||||||
className,
|
className,
|
||||||
disabled,
|
intent,
|
||||||
size,
|
size,
|
||||||
variant,
|
variant,
|
||||||
weight,
|
weight,
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ export const buttonVariants = cva(styles.btn, {
|
|||||||
quarternary: styles.quarternary,
|
quarternary: styles.quarternary,
|
||||||
white: styles.white,
|
white: styles.white,
|
||||||
},
|
},
|
||||||
|
intent: {
|
||||||
|
primary: styles.primary,
|
||||||
|
secondary: styles.secondary,
|
||||||
|
},
|
||||||
size: {
|
size: {
|
||||||
small: styles.small,
|
small: styles.small,
|
||||||
regular: styles.average,
|
regular: styles.average,
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
18
components/TempDesignSystem/Form/ErrorMessage/index.tsx
Normal file
18
components/TempDesignSystem/Form/ErrorMessage/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
||||||
|
|
||||||
|
import styles from "./error.module.css"
|
||||||
|
|
||||||
|
import type { ErrorMessageProps } from "./errorMessage"
|
||||||
|
|
||||||
|
export default function ErrorMessage<T>({
|
||||||
|
errors,
|
||||||
|
name,
|
||||||
|
}: ErrorMessageProps<T>) {
|
||||||
|
return (
|
||||||
|
<RHFErrorMessage
|
||||||
|
errors={errors}
|
||||||
|
name={name}
|
||||||
|
render={({ message }) => <p className={styles.message}>{message}</p>}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
44
components/TempDesignSystem/Form/Input/index.tsx
Normal file
44
components/TempDesignSystem/Form/Input/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"use client"
|
||||||
|
import { useController } from "react-hook-form"
|
||||||
|
|
||||||
|
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||||
|
import { Input as AriaInput, TextField } from "react-aria-components"
|
||||||
|
|
||||||
|
import styles from "./input.module.css"
|
||||||
|
|
||||||
|
import type { InputProps } from "./input"
|
||||||
|
|
||||||
|
export default function Input({
|
||||||
|
control,
|
||||||
|
disabled,
|
||||||
|
name,
|
||||||
|
placeholder,
|
||||||
|
registerOptions,
|
||||||
|
type = "text",
|
||||||
|
}: InputProps) {
|
||||||
|
const { field, fieldState, formState } = useController({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
rules: registerOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
defaultValue={field.value}
|
||||||
|
isDisabled={disabled ?? field.disabled}
|
||||||
|
isInvalid={fieldState.invalid}
|
||||||
|
isRequired={!!registerOptions?.required}
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
onChange={field.onChange}
|
||||||
|
type={type}
|
||||||
|
>
|
||||||
|
<AriaInput
|
||||||
|
className={styles.input}
|
||||||
|
placeholder={placeholder}
|
||||||
|
ref={field.ref}
|
||||||
|
/>
|
||||||
|
<ErrorMessage errors={formState.errors} name={name} />
|
||||||
|
</TextField>
|
||||||
|
)
|
||||||
|
}
|
||||||
13
components/TempDesignSystem/Form/Input/input.module.css
Normal file
13
components/TempDesignSystem/Form/Input/input.module.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.input {
|
||||||
|
border: 0.2rem solid var(--some-black-color, #757575);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
color: var(--some-black-color, #757575);
|
||||||
|
font-family: var(--ff-fira-sans);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 400;
|
||||||
|
height: 4rem;
|
||||||
|
letter-spacing: -1.5%;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
padding: 0.8rem 1.6rem;
|
||||||
|
width: min(28rem, 100%);
|
||||||
|
}
|
||||||
9
components/TempDesignSystem/Form/Input/input.ts
Normal file
9
components/TempDesignSystem/Form/Input/input.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { EditProfileSchema } from "@/components/MyProfile/Profile/Edit/Form/schema"
|
||||||
|
import { Control, RegisterOptions } from "react-hook-form"
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
control: Control<EditProfileSchema>
|
||||||
|
name: keyof EditProfileSchema
|
||||||
|
registerOptions?: RegisterOptions<EditProfileSchema>
|
||||||
|
}
|
||||||
97
components/TempDesignSystem/Form/Phone/index.tsx
Normal file
97
components/TempDesignSystem/Form/Phone/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"use client"
|
||||||
|
import "react-international-phone/style.css"
|
||||||
|
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||||
|
import { useCallback, useEffect, useRef } from "react"
|
||||||
|
import { defaultCountries, getCountry } from "react-international-phone"
|
||||||
|
|
||||||
|
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||||
|
import { PhoneInput, type PhoneInputRefType } from "react-international-phone"
|
||||||
|
|
||||||
|
import styles from "./phone.module.css"
|
||||||
|
|
||||||
|
import type { PhoneProps } from "./phone"
|
||||||
|
|
||||||
|
export default function Phone({
|
||||||
|
name = "phone",
|
||||||
|
placeholder = "",
|
||||||
|
registerOptions = {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}: PhoneProps) {
|
||||||
|
const phoneRef = useRef<PhoneInputRefType>(null)
|
||||||
|
const { control, formState } = useFormContext()
|
||||||
|
const countryValue = useWatch({ name: "country" })
|
||||||
|
const defaultCountry = getCountry({
|
||||||
|
countries: defaultCountries,
|
||||||
|
field: "name",
|
||||||
|
value: countryValue,
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Holds the previous selected country to be able to update
|
||||||
|
* countrycode based on country select field.
|
||||||
|
* Since PhoneInput inputs the countrys dialcode (country code) upon
|
||||||
|
* selection, we need to check if the current value is just
|
||||||
|
* the previously selected countrys dialcode number.
|
||||||
|
*/
|
||||||
|
const prevSelectedCountry = useRef<string | undefined>(countryValue)
|
||||||
|
const { field } = useController({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
rules: registerOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleCountrySelectForPhone = useCallback(
|
||||||
|
(country: string) => {
|
||||||
|
const selectedCountry = getCountry({
|
||||||
|
countries: defaultCountries,
|
||||||
|
field: "name",
|
||||||
|
value: country,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selectedCountry) {
|
||||||
|
phoneRef.current?.setCountry(selectedCountry.iso2)
|
||||||
|
prevSelectedCountry.current = country
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[phoneRef.current, prevSelectedCountry.current]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (countryValue) {
|
||||||
|
if (field.value) {
|
||||||
|
if (prevSelectedCountry.current) {
|
||||||
|
if (prevSelectedCountry.current !== countryValue) {
|
||||||
|
const selectedCountryPrev = getCountry({
|
||||||
|
countries: defaultCountries,
|
||||||
|
field: "name",
|
||||||
|
value: prevSelectedCountry.current,
|
||||||
|
})
|
||||||
|
if (
|
||||||
|
field.value.replace("+", "") === selectedCountryPrev?.dialCode
|
||||||
|
) {
|
||||||
|
handleCountrySelectForPhone(countryValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleCountrySelectForPhone(countryValue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleCountrySelectForPhone(countryValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [countryValue, prevSelectedCountry.current])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.phone}>
|
||||||
|
<PhoneInput
|
||||||
|
{...field}
|
||||||
|
className={styles.input}
|
||||||
|
defaultCountry={defaultCountry?.iso2 ?? "se"}
|
||||||
|
placeholder={placeholder}
|
||||||
|
preferredCountries={["de", "dk", "fi", "no", "se", "gb"]}
|
||||||
|
ref={phoneRef}
|
||||||
|
/>
|
||||||
|
<ErrorMessage errors={formState.errors} name={name} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
components/TempDesignSystem/Form/Phone/phone.module.css
Normal file
25
components/TempDesignSystem/Form/Phone/phone.module.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.phone {
|
||||||
|
--react-international-phone-border-color: var(--some-black-color, #757575);
|
||||||
|
--react-international-phone-border-radius: 0.4rem;
|
||||||
|
--react-international-phone-font-size: 1.6rem;
|
||||||
|
--react-international-phone-height: 4rem;
|
||||||
|
--react-international-phone-text-color: color:
|
||||||
|
var(--some-black-color, #757575);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone :global(.react-international-phone-input-container) {
|
||||||
|
display: grid;
|
||||||
|
/* r-i-p sets their width dynamically and doesn't respect the width property of its parent */
|
||||||
|
grid-template-columns: 4.7rem minmax(20.3rem, 1fr);
|
||||||
|
width: min(28rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* react-international-phone only exposes variables to change border-color */
|
||||||
|
.phone :global(.react-international-phone-country-selector-button),
|
||||||
|
.phone :global(.react-international-phone-input) {
|
||||||
|
border-width: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone :global(.react-international-phone-input) {
|
||||||
|
padding: 0.8rem 1.6rem;
|
||||||
|
}
|
||||||
7
components/TempDesignSystem/Form/Phone/phone.ts
Normal file
7
components/TempDesignSystem/Form/Phone/phone.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { RegisterOptions } from "react-hook-form"
|
||||||
|
|
||||||
|
export type PhoneProps = {
|
||||||
|
name?: string
|
||||||
|
placeholder?: string
|
||||||
|
registerOptions?: RegisterOptions
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.chevron {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[data-rac][data-open="true"] .chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
11
components/TempDesignSystem/Form/SelectChevron/index.tsx
Normal file
11
components/TempDesignSystem/Form/SelectChevron/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { ChevronDownIcon } from "@/components/Icons"
|
||||||
|
|
||||||
|
import styles from "./chevron.module.css"
|
||||||
|
|
||||||
|
export default function SelectChevron() {
|
||||||
|
return (
|
||||||
|
<span aria-hidden="true" className={styles.chevron}>
|
||||||
|
<ChevronDownIcon height={24} width={24} />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
|
||||||
import { linkVariants } from "./variants"
|
import { linkVariants } from "./variants"
|
||||||
|
|
||||||
import NextLink from "next/link"
|
import NextLink from "next/link"
|
||||||
|
|
||||||
import type { LinkProps } from "./link"
|
import type { LinkProps } from "./link"
|
||||||
import { usePathname } from "next/navigation"
|
|
||||||
|
|
||||||
export default function Link({
|
export default function Link({
|
||||||
className,
|
className,
|
||||||
@@ -14,10 +14,7 @@ export default function Link({
|
|||||||
variant,
|
variant,
|
||||||
...props
|
...props
|
||||||
}: LinkProps) {
|
}: LinkProps) {
|
||||||
const currentPageSlug = `/${usePathname()
|
const currentPageSlug = usePathname()
|
||||||
.split("/")
|
|
||||||
.filter((v) => v)
|
|
||||||
.at(-1)}`
|
|
||||||
const isActive = currentPageSlug === href
|
const isActive = currentPageSlug === href
|
||||||
const classNames = linkVariants({
|
const classNames = linkVariants({
|
||||||
active: isActive,
|
active: isActive,
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
export const pageNames = {
|
|
||||||
da: "mine-sider",
|
|
||||||
de: "mein-profil",
|
|
||||||
en: "my-pages",
|
|
||||||
fi: "minun-sivujani",
|
|
||||||
no: "mine-sider",
|
|
||||||
sv: "mina-sidor",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const profilePageNames = {
|
|
||||||
da: "mine-sider/profil",
|
|
||||||
de: "mein-profil/profil",
|
|
||||||
en: "my-pages/profile",
|
|
||||||
fi: "minun-sivujani/profil",
|
|
||||||
no: "mine-sider/profil",
|
|
||||||
sv: "mina-sidor/profil",
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { Lang } from "@/types/lang"
|
|
||||||
|
|
||||||
export const pageNames: Record<Lang, string> = {
|
|
||||||
da: "mine-sider",
|
|
||||||
de: "mein-profil",
|
|
||||||
en: "my-pages",
|
|
||||||
fi: "minun-sivujani",
|
|
||||||
no: "mine-sider",
|
|
||||||
sv: "mina-sidor",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const profilePageNames: Record<Lang, string> = {
|
|
||||||
da: "mine-sider/profil",
|
|
||||||
de: "mein-profil/profil",
|
|
||||||
en: "my-pages/profile",
|
|
||||||
fi: "minun-sivujani/profil",
|
|
||||||
no: "mine-sider/profil",
|
|
||||||
sv: "mina-sidor/profil",
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { benefits, myPages, profile } from "./myPages"
|
import { benefits, myPages, profile, profileEdit } from "./myPages"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are routes in code we know requires auth
|
* These are routes in code we know requires auth
|
||||||
@@ -6,7 +6,8 @@ import { benefits, myPages, profile } from "./myPages"
|
|||||||
* Some of these are rewritten in next.config.js
|
* Some of these are rewritten in next.config.js
|
||||||
*/
|
*/
|
||||||
export const authRequired = [
|
export const authRequired = [
|
||||||
|
...Object.values(benefits),
|
||||||
...Object.values(myPages),
|
...Object.values(myPages),
|
||||||
...Object.values(profile),
|
...Object.values(profile),
|
||||||
...Object.values(benefits),
|
...Object.values(profileEdit),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,14 +17,25 @@ export const myPages = {
|
|||||||
sv: "/sv/mina-sidor",
|
sv: "/sv/mina-sidor",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** TODO: Update to relevant paths */
|
||||||
/** @type {import('@/types/routes').LangRoute} */
|
/** @type {import('@/types/routes').LangRoute} */
|
||||||
export const profile = {
|
export const profile = {
|
||||||
da: `${myPages.da}/profil-da`,
|
da: `${myPages.da}/profil`,
|
||||||
de: `${myPages.de}/profile-de`,
|
de: `${myPages.de}/profil`,
|
||||||
en: `${myPages.en}/profile-en`,
|
en: `${myPages.en}/profile`,
|
||||||
fi: `${myPages.fi}/profile-fi`,
|
fi: `${myPages.fi}/profiili`,
|
||||||
no: `${myPages.no}/profile-no`,
|
no: `${myPages.no}/profil`,
|
||||||
sv: `${myPages.sv}/profile-sv`,
|
sv: `${myPages.sv}/profil`,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('@/types/routes').LangRoute} */
|
||||||
|
export const profileEdit = {
|
||||||
|
da: `${profile.da}/edit`,
|
||||||
|
de: `${profile.de}/edit`,
|
||||||
|
en: `${profile.en}/edit`,
|
||||||
|
fi: `${profile.fi}/edit`,
|
||||||
|
no: `${profile.no}/edit`,
|
||||||
|
sv: `${profile.sv}/edit`,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {import('@/types/routes').LangRoute} */
|
/** @type {import('@/types/routes').LangRoute} */
|
||||||
|
|||||||
1
env/server.ts
vendored
1
env/server.ts
vendored
@@ -2,6 +2,7 @@ import { createEnv } from "@t3-oss/env-nextjs"
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const env = createEnv({
|
export const env = createEnv({
|
||||||
|
isServer: typeof window === "undefined" || "Deno" in window,
|
||||||
server: {
|
server: {
|
||||||
ADOBE_SCRIPT_SRC: z.string().optional(),
|
ADOBE_SCRIPT_SRC: z.string().optional(),
|
||||||
BUILD_ID: z.string().default("64rYXBu8o2eHp0Jf"),
|
BUILD_ID: z.string().default("64rYXBu8o2eHp0Jf"),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ fragment CurrentBlocksPageLink on CurrentBlocksPage {
|
|||||||
|
|
||||||
fragment AccountPageLink on AccountPage {
|
fragment AccountPageLink on AccountPage {
|
||||||
system {
|
system {
|
||||||
|
locale
|
||||||
uid
|
uid
|
||||||
}
|
}
|
||||||
title
|
title
|
||||||
@@ -17,6 +18,7 @@ fragment AccountPageLink on AccountPage {
|
|||||||
|
|
||||||
fragment LoyaltyPageLink on LoyaltyPage {
|
fragment LoyaltyPageLink on LoyaltyPage {
|
||||||
system {
|
system {
|
||||||
|
locale
|
||||||
uid
|
uid
|
||||||
}
|
}
|
||||||
title
|
title
|
||||||
@@ -25,6 +27,7 @@ fragment LoyaltyPageLink on LoyaltyPage {
|
|||||||
|
|
||||||
fragment ContentPageLink on ContentPage {
|
fragment ContentPageLink on ContentPage {
|
||||||
system {
|
system {
|
||||||
|
locale
|
||||||
uid
|
uid
|
||||||
}
|
}
|
||||||
web {
|
web {
|
||||||
|
|||||||
3
lib/translation.ts
Normal file
3
lib/translation.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function _(str: string) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import createJiti from "jiti"
|
import createJiti from "jiti"
|
||||||
|
|
||||||
import { login } from "./constants/routes/handleAuth.js"
|
import { login } from "./constants/routes/handleAuth.js"
|
||||||
import { myPages, profile, benefits } from "./constants/routes/myPages.js"
|
import { benefits, myPages, profile, profileEdit } from "./constants/routes/myPages.js"
|
||||||
|
|
||||||
const jiti = createJiti(new URL(import.meta.url).pathname)
|
const jiti = createJiti(new URL(import.meta.url).pathname)
|
||||||
jiti("./env/server")
|
jiti("./env/server")
|
||||||
@@ -49,23 +49,6 @@ const nextConfig = {
|
|||||||
rewrites() {
|
rewrites() {
|
||||||
return {
|
return {
|
||||||
beforeFiles: [
|
beforeFiles: [
|
||||||
{ source: login.da, destination: "/da/login" },
|
|
||||||
{ source: login.de, destination: "/de/login" },
|
|
||||||
{ source: login.fi, destination: "/fi/login" },
|
|
||||||
{ source: login.no, destination: "/no/login" },
|
|
||||||
{ source: login.sv, destination: "/sv/login" },
|
|
||||||
|
|
||||||
{ source: myPages.da, destination: "/da/my-pages" },
|
|
||||||
{ source: myPages.de, destination: "/de/my-pages" },
|
|
||||||
{ source: myPages.fi, destination: "/fi/my-pages" },
|
|
||||||
{ source: myPages.no, destination: "/no/my-pages" },
|
|
||||||
{ source: myPages.sv, destination: "/sv/my-pages" },
|
|
||||||
|
|
||||||
{ source: profile.da, destination: "/da/my-pages/profile" },
|
|
||||||
{ source: profile.de, destination: "/de/my-pages/profile" },
|
|
||||||
{ source: profile.fi, destination: "/fi/my-pages/profile" },
|
|
||||||
{ source: profile.no, destination: "/no/my-pages/profile" },
|
|
||||||
{ source: profile.sv, destination: "/sv/my-pages/profile" },
|
|
||||||
{
|
{
|
||||||
source: benefits.da,
|
source: benefits.da,
|
||||||
destination: "/da/my-pages/benefits",
|
destination: "/da/my-pages/benefits",
|
||||||
@@ -86,6 +69,30 @@ const nextConfig = {
|
|||||||
source: benefits.sv,
|
source: benefits.sv,
|
||||||
destination: "/sv/my-pages/benefits",
|
destination: "/sv/my-pages/benefits",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{ source: login.da, destination: "/da/login" },
|
||||||
|
{ source: login.de, destination: "/de/login" },
|
||||||
|
{ source: login.fi, destination: "/fi/login" },
|
||||||
|
{ source: login.no, destination: "/no/login" },
|
||||||
|
{ source: login.sv, destination: "/sv/login" },
|
||||||
|
|
||||||
|
{ source: myPages.da, destination: "/da/my-pages" },
|
||||||
|
{ source: myPages.de, destination: "/de/my-pages" },
|
||||||
|
{ source: myPages.fi, destination: "/fi/my-pages" },
|
||||||
|
{ source: myPages.no, destination: "/no/my-pages" },
|
||||||
|
{ source: myPages.sv, destination: "/sv/my-pages" },
|
||||||
|
|
||||||
|
{ source: profile.da, destination: "/da/my-pages/profile" },
|
||||||
|
{ source: profile.de, destination: "/de/my-pages/profile" },
|
||||||
|
{ source: profile.fi, destination: "/fi/my-pages/profile" },
|
||||||
|
{ source: profile.no, destination: "/no/my-pages/profile" },
|
||||||
|
{ source: profile.sv, destination: "/sv/my-pages/profile" },
|
||||||
|
|
||||||
|
{ source: profileEdit.da, destination: "/da/my-pages/profile/edit" },
|
||||||
|
{ source: profileEdit.de, destination: "/de/my-pages/profile/edit" },
|
||||||
|
{ source: profileEdit.fi, destination: "/fi/my-pages/profile/edit" },
|
||||||
|
{ source: profileEdit.no, destination: "/no/my-pages/profile/edit" },
|
||||||
|
{ source: profileEdit.sv, destination: "/sv/my-pages/profile/edit" },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
2970
package-lock.json
generated
2970
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user