feat: breadcrumbs for My Pages

This commit is contained in:
Simon Emanuelsson
2024-04-11 18:51:38 +02:00
parent 33b4d1d9fc
commit 38f764e0ff
31 changed files with 228 additions and 87 deletions

View File

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

View File

@@ -0,0 +1,10 @@
import { serverClient } from "@/lib/trpc/server"
import Breadcrumbs from "@/components/MyPages/Breadcrumbs"
export default async function BenefitsBreadcrumbs() {
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get({
href: "/my-pages/benefits",
})
return <Breadcrumbs breadcrumbs={breadcrumbs} />
}

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,10 @@
import { serverClient } from "@/lib/trpc/server"
import Breadcrumbs from "@/components/MyPages/Breadcrumbs"
export default async function OverviewBreadcrumbs() {
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get({
href: "/my-pages/overview",
})
return <Breadcrumbs breadcrumbs={breadcrumbs} />
}

View File

@@ -0,0 +1,10 @@
import { serverClient } from "@/lib/trpc/server"
import Breadcrumbs from "@/components/MyPages/Breadcrumbs"
export default async function MyPagesBreadcrumbs() {
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get({
href: "/my-pages",
})
return <Breadcrumbs breadcrumbs={breadcrumbs} />
}

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,10 @@
import { serverClient } from "@/lib/trpc/server"
import Breadcrumbs from "@/components/MyPages/Breadcrumbs"
export default async function ProfileBreadcrumbs() {
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get({
href: "/my-pages/profile",
})
return <Breadcrumbs breadcrumbs={breadcrumbs} />
}

View File

@@ -0,0 +1,10 @@
import { serverClient } from "@/lib/trpc/server"
import Breadcrumbs from "@/components/MyPages/Breadcrumbs"
export default async function ProfileBreadcrumbs() {
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get({
href: "/my-pages/profile",
})
return <Breadcrumbs breadcrumbs={breadcrumbs} />
}

View File

@@ -1,24 +1,23 @@
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts" import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
import { breadcrumbs } from "./_constants"
import Breadcrumbs from "@/components/MyPages/Breadcrumbs"
import Header from "@/components/MyPages/Header" import Header from "@/components/MyPages/Header"
import Sidebar from "@/components/MyPages/Sidebar" import Sidebar from "@/components/MyPages/Sidebar"
import styles from "./layout.module.css" import styles from "./layout.module.css"
import type { LangParams, LayoutArgs } from "@/types/params" import type { MyPagesLayoutProps } from "@/types/components/myPages/layout"
export default async function MyPagesLayout({ export default async function MyPagesLayout({
breadcrumbs,
children, children,
params, params,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) { }: React.PropsWithChildren<MyPagesLayoutProps>) {
return ( return (
<div <div
className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`} className={`${firaMono.variable} ${firaSans.variable} ${styles.layout}`}
> >
<Header lang={params.lang} /> <Header lang={params.lang} />
<Breadcrumbs breadcrumbs={breadcrumbs} lang={params.lang} /> {breadcrumbs}
<div className={styles.content}> <div className={styles.content}>
<Sidebar lang={params.lang} /> <Sidebar lang={params.lang} />
{children} {children}

View File

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

View File

@@ -0,0 +1,9 @@
import styles from "./breadcrumbs.module.css"
export default function Breadcrumb({ children }: React.PropsWithChildren) {
return (
<li className={styles.listItem}>
<p className={styles.currentPage}>{children}</p>
</li>
)
}

View File

@@ -0,0 +1,17 @@
import Link from "@/components/TempDesignSystem/Link"
import styles from "./breadcrumbs.module.css"
export default function BreadcrumbsWithLink({
children,
href,
}: React.PropsWithChildren<{ href: string }>) {
return (
<li className={styles.listItem}>
<Link className={styles.link} href={href}>
{children}
</Link>
<span aria-hidden="true">/</span>
</li>
)
}

View File

@@ -1,49 +0,0 @@
"use client"
import { Fragment } from "react"
import { usePathname } from "next/navigation"
import Link from "@/components/TempDesignSystem/Link"
import styles from "./breadcrumbs.module.css"
import type { BreadcrumbsProps } from "@/types/components/myPages/breadcrumbs"
export default function ClientBreadcrumbs({ breadcrumbs, lang }: BreadcrumbsProps) {
const pathname = usePathname()
/** Temp solution until we can get breadcrumbs from CS */
const path = pathname.replace(`/${lang}`, '')
const currentBreadcrumbs = breadcrumbs?.[path]
if (!currentBreadcrumbs?.length) {
return null
}
return (
<>
<li className={styles.listItem}>
<span>/</span>
</li>
{currentBreadcrumbs.map(breadcrumb => {
if (breadcrumb.href) {
return (
<Fragment key={breadcrumb.title}>
<li className={styles.listItem}>
<Link className={styles.link} href={breadcrumb.href}>
{breadcrumb.title}
</Link>
</li>
<li className={styles.listItem}>
<span>/</span>
</li>
</Fragment>
)
}
return (
<li className={styles.listItem} key={breadcrumb.title}>
<p className={styles.currentPage}>{breadcrumb.title}</p>
</li>
)
})}
</>
)
}

View File

@@ -27,6 +27,11 @@
line-height: 1.56rem; line-height: 1.56rem;
} }
.listItem {
display: flex;
gap: 0.4rem;
}
.currentPage { .currentPage {
margin: 0; margin: 0;
} }
@@ -37,4 +42,4 @@
padding-left: 2.4rem; padding-left: 2.4rem;
padding-top: 2rem; padding-top: 2rem;
} }
} }

View File

@@ -1,20 +1,31 @@
import ClientBreadcrumbs from "./Client" import { _ } from "@/lib/translation"
import Link from "@/components/TempDesignSystem/Link"
import Breadcrumb from "./Breadcrumb"
import BreadcrumbsWithLink from "./BreadcrumbWithLink"
import styles from "./breadcrumbs.module.css" import styles from "./breadcrumbs.module.css"
import type { BreadcrumbsProps } from "@/types/components/myPages/breadcrumbs" import type { BreadcrumbsProps } from "@/types/components/myPages/breadcrumbs"
export default function Breadcrumbs({ breadcrumbs, lang }: BreadcrumbsProps) { export default function Breadcrumbs({ breadcrumbs }: BreadcrumbsProps) {
return ( return (
<nav className={styles.breadcrumbs}> <nav className={styles.breadcrumbs}>
<ul className={styles.list}> <ul className={styles.list}>
<li className={styles.listItem}> <BreadcrumbsWithLink href="#">{_("Home")}</BreadcrumbsWithLink>
<Link className={styles.link} href="#"> {breadcrumbs.map((breadcrumb) => {
Home if (breadcrumb.href) {
</Link> return (
</li> <BreadcrumbsWithLink
<ClientBreadcrumbs breadcrumbs={breadcrumbs} lang={lang} /> key={breadcrumb.title}
href={breadcrumb.href}
>
{breadcrumb.title}
</BreadcrumbsWithLink>
)
}
return <Breadcrumb>{breadcrumb.title}</Breadcrumb>
})}
</ul> </ul>
</nav> </nav>
) )

View File

@@ -24,7 +24,7 @@ export function mapMenuItems(navigationItems: NavigationItem[]) {
lang: node.system.locale, lang: node.system.locale,
subItems: item.sub_items ? mapMenuItems(item.sub_items) : null, subItems: item.sub_items ? mapMenuItems(item.sub_items) : null,
uid: node.system.uid, uid: node.system.uid,
url: `/${node.system.locale}/${getURL(node)}`.replaceAll("//+", "/"), url: `/${node.system.locale}/${getURL(node)}`.replaceAll(/\/\/+/g, "/"),
} }
}) })
} }

View File

@@ -30,21 +30,22 @@ export default async function Sidebar({ lang }: SidebarProps) {
</Title> </Title>
{menuItems.map((item) => ( {menuItems.map((item) => (
<Fragment key={item.uid}> <Fragment key={item.uid}>
<Link href={item.url} variant="sidebar"> <Link href={item.url} partialMatch 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" partialMatch
> variant="sidebar"
{subItem.linkText} >
</Link> {subItem.linkText}
) </Link>
}) )
})
: null} : null}
</Fragment> </Fragment>
))} ))}

View File

@@ -10,12 +10,16 @@ import type { LinkProps } from "./link"
export default function Link({ export default function Link({
className, className,
href, href,
partialMatch = false,
size, size,
variant, variant,
...props ...props
}: LinkProps) { }: LinkProps) {
const currentPageSlug = usePathname() const currentPageSlug = usePathname()
const isActive = currentPageSlug === href let isActive = currentPageSlug === href
if (partialMatch && !isActive) {
isActive = currentPageSlug.startsWith(href)
}
const classNames = linkVariants({ const classNames = linkVariants({
active: isActive, active: isActive,
className, className,

View File

@@ -4,6 +4,7 @@ import type { VariantProps } from "class-variance-authority"
export interface LinkProps export interface LinkProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement>, extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
VariantProps<typeof linkVariants> { VariantProps<typeof linkVariants> {
href: string href: string
partialMatch?: boolean
} }

5
env/server.ts vendored
View File

@@ -2,6 +2,11 @@ import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod" import { z } from "zod"
export const env = createEnv({ export const env = createEnv({
/**
* Due to t3-env only checking typeof window === "undefined"
* and Netlify running Deno, window is never "undefined"
* https://github.com/t3-oss/t3-env/issues/154
*/
isServer: typeof window === "undefined" || "Deno" in window, isServer: typeof window === "undefined" || "Deno" in window,
server: { server: {
ADOBE_SCRIPT_SRC: z.string().optional(), ADOBE_SCRIPT_SRC: z.string().optional(),

View File

@@ -1,9 +1,11 @@
import { router } from "./trpc" import { router } from "./trpc"
/** Routers */ /** Routers */
import { contentstackRouter } from "./routers/contentstack"
import { userRouter } from "./routers/user" import { userRouter } from "./routers/user"
export const appRouter = router({ export const appRouter = router({
contentstack: contentstackRouter,
user: userRouter, user: userRouter,
}) })

View File

@@ -0,0 +1,5 @@
import { mergeRouters } from "@/server/trpc"
import { breadcrumbsQueryRouter } from "./query"
export const breadcrumbsRouter = mergeRouters(breadcrumbsQueryRouter)

View File

@@ -0,0 +1,8 @@
import { z } from "zod"
export const getBreadcrumbsSchema = z.array(
z.object({
href: z.string().optional(),
title: z.string(),
})
)

View File

@@ -0,0 +1,43 @@
import { z } from "zod"
import { badRequestError } from "@/server/errors/trpc"
import { getBreadcrumbsSchema } from "./output"
import { publicProcedure, router } from "@/server/trpc"
const rootMyPagesBreadcrumb = {
href: "/en/my-pages",
title: "My Pages",
}
enum paths {
"/my-pages",
"/my-pages/benefits",
"/my-pages/overview",
"/my-pages/profile",
}
const keys = Object.keys(paths) as [keyof typeof paths]
const possibleBreadcrumbs: Record<string, { title: string; href?: string }[]> =
{
"/my-pages": [
{
title: rootMyPagesBreadcrumb.title,
},
],
"/my-pages/benefits": [rootMyPagesBreadcrumb, { title: "Benefits" }],
"/my-pages/overview": [rootMyPagesBreadcrumb, { title: "Overview" }],
"/my-pages/profile": [rootMyPagesBreadcrumb, { title: "Profile" }],
}
export const breadcrumbsQueryRouter = router({
get: publicProcedure.input(z.object({ href: z.enum(keys) })).query((opts) => {
const breadcrumbs = possibleBreadcrumbs[opts.input.href]
const validatedBreadcrumbs = getBreadcrumbsSchema.safeParse(breadcrumbs)
if (validatedBreadcrumbs.success) {
return breadcrumbs
}
throw badRequestError()
}),
})

View File

@@ -0,0 +1,7 @@
import { router } from "@/server/trpc"
import { breadcrumbsRouter } from "./breadcrumbs"
export const contentstackRouter = router({
breadcrumbs: breadcrumbsRouter,
})

View File

@@ -12,7 +12,7 @@ interface EditProfileActions {
export interface EditProfileStore export interface EditProfileStore
extends EditProfileActions, extends EditProfileActions,
EditProfileState {} EditProfileState { }
export const useProfileStore = create<EditProfileStore>()((set) => ({ export const useProfileStore = create<EditProfileStore>()((set) => ({
pending: false, pending: false,

5
types/breadcrumbs.ts Normal file
View File

@@ -0,0 +1,5 @@
import { z } from "zod"
import { getBreadcrumbsSchema } from "@/server/routers/contentstack/breadcrumbs/output"
export interface Breadcrumbs extends z.infer<typeof getBreadcrumbsSchema> {}

View File

@@ -1,10 +1,5 @@
import type { LangParams } from "@/types/params" import type { Breadcrumbs } from "@/types/breadcrumbs"
type Breadcrumb = { export type BreadcrumbsProps = {
href?: string breadcrumbs: Breadcrumbs
title: string
} }
export type BreadcrumbsProps = LangParams & {
breadcrumbs: Record<string, Breadcrumb[]>
}

View File

@@ -0,0 +1,5 @@
import type { LangParams, LayoutArgs } from "@/types/params"
export interface MyPagesLayoutProps extends LayoutArgs<LangParams> {
breadcrumbs: React.ReactNode
}