feat(WEB-170): edit profile view

This commit is contained in:
Simon Emanuelsson
2024-04-11 18:51:38 +02:00
parent 82e4d40203
commit 9396b2c3d5
114 changed files with 3642 additions and 2171 deletions

44
actions/editProfile.ts Normal file
View 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,
}
}
}

View File

@@ -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,
}

View File

@@ -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>

View File

@@ -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>
) )

View File

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

View File

@@ -0,0 +1,5 @@
import CommunicationPreferences from "@/components/MyProfile/CommunicationPreferences"
export default function Communication() {
return <CommunicationPreferences />
}

View File

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

View File

@@ -0,0 +1,5 @@
import CreditCards from "@/components/MyProfile/CreditCards"
export default function CreditCardSlot() {
return <CreditCards />
}

View File

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

View File

@@ -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>
</>
)
} }

View File

@@ -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>
</>
)
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
import MembershipCard from "@/components/MyProfile/MembershipCard"
export default function MembershipCardSlot() {
return <MembershipCard />
}

View File

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

View File

@@ -0,0 +1,5 @@
import Password from "@/components/MyProfile/Password"
export default function PasswordSlot() {
return <Password />
}

View File

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

View File

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

View File

@@ -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} />
}

View File

@@ -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} />
}

View File

@@ -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>
)
}

View File

@@ -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>
)
} }

View File

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

View File

@@ -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>
)
}

View File

@@ -1,3 +0,0 @@
.btn {
position: absolute;
}

View File

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

View File

@@ -0,0 +1,5 @@
import Wishes from "@/components/MyProfile/Wishes"
export default function WishesSlot() {
return <Wishes />
}

View File

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

View File

@@ -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;
}

View File

@@ -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>
) )
} }

View File

@@ -1,10 +0,0 @@
.page {
display: grid;
gap: 3rem;
}
.cards {
display: grid;
gap: 0.4rem;
grid-template-columns: 1fr 1fr;
}

View File

@@ -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>
)
}

View File

@@ -1,9 +0,0 @@
export default function VerifyPage() {
return (
<section>
<header>
<h1>Verify that code already!</h1>
</header>
</section>
)
}

View 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}
/>
)
}

View 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}
/>
)
}

View 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}
/>
)
}

View 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} />
)
}

View 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} />
)
}

View 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"

View File

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

View File

@@ -1 +0,0 @@
.header {}

View File

@@ -1,9 +0,0 @@
import styles from "./header.module.css"
export default function Header({ children }: React.PropsWithChildren) {
return (
<header className={styles.header}>
{children}
</header>
)
}

View File

@@ -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

View File

@@ -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%);
}

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -1,9 +0,0 @@
"use client"
import { createPortal } from 'react-dom';
export default function Portal({ children }: React.PropsWithChildren) {
return createPortal(
children,
document.body
);
};

View File

@@ -1,9 +0,0 @@
import styles from "./title.module.css"
export default function Title({ children }: React.PropsWithChildren) {
return (
<h1 className={styles.heading}>
{children}
</h1>
)
}

View File

@@ -1 +0,0 @@
.heading {}

View File

@@ -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

View File

@@ -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}>

View File

@@ -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,

View File

@@ -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>
</>
)
}

View 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("//+", "/"),
}
})
}

View File

@@ -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>

View File

@@ -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;
} }
} }

View File

@@ -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;
}

View 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

View File

@@ -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

View 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>
)
}

View 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>
</>
)
}

View File

@@ -0,0 +1,5 @@
.form {
display: grid;
gap: 1.8rem;
grid-template-columns: 1fr 1fr;
}

View 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>
)
}

View 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>

View 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>
)
}

View File

@@ -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>
) )
} }

View File

@@ -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);
} }

View File

@@ -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,

View File

@@ -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,

View 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" },
]

View 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);
}

View File

@@ -0,0 +1,7 @@
import type { RegisterOptions } from "react-hook-form"
export type CountryProps = {
name?: string
placeholder?: string
registerOptions?: RegisterOptions
}

View 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>
)
}

View 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>
)
}

View File

@@ -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);
}

View 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
}

View 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;
}

View 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>
}

View 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>
)
}

View File

@@ -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;
}

View File

@@ -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
}

View 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>}
/>
)
}

View 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>
)
}

View 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%);
}

View 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>
}

View 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>
)
}

View 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;
}

View File

@@ -0,0 +1,7 @@
import type { RegisterOptions } from "react-hook-form"
export type PhoneProps = {
name?: string
placeholder?: string
registerOptions?: RegisterOptions
}

View File

@@ -0,0 +1,7 @@
.chevron {
display: flex;
}
div[data-rac][data-open="true"] .chevron {
transform: rotate(180deg);
}

View 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>
)
}

View File

@@ -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,

View File

@@ -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",
}

View File

@@ -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",
}

View File

@@ -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),
] ]

View File

@@ -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
View File

@@ -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"),

View File

@@ -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
View File

@@ -0,0 +1,3 @@
export function _(str: string) {
return str
}

View File

@@ -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

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