refactor: zod validation and pr comments

This commit is contained in:
Christel Westerberg
2024-04-29 14:00:24 +02:00
parent 9f0b044daa
commit d9f1470eb7
31 changed files with 222 additions and 207 deletions

View File

@@ -1,5 +1,4 @@
.layout { .layout {
--max-width: 101.4rem;
--header-height: 4.5rem; --header-height: 4.5rem;
display: grid; display: grid;

View File

@@ -1,6 +1,4 @@
.layout { .layout {
--max-width: 101.4rem;
display: grid; display: grid;
font-family: var(--ff-fira-sans); font-family: var(--ff-fira-sans);
background-color: var(--Brand-Coffee-Subtle); background-color: var(--Brand-Coffee-Subtle);

View File

@@ -26,5 +26,6 @@
gap: 6.4rem; gap: 6.4rem;
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
grid-column: 2 / -1;
} }
} }

View File

@@ -26,13 +26,8 @@ export default async function LoyaltyPage({
return ( return (
<section className={styles.content}> <section className={styles.content}>
<aside> {loyaltyPage.sidebar ? <Sidebar blocks={loyaltyPage.sidebar} /> : null}
{loyaltyPage.sidebar
? loyaltyPage.sidebar.map((block, i) => (
<Sidebar key={i} block={block} />
))
: null}
</aside>
<MaxWidth className={styles.blocks} tag="main"> <MaxWidth className={styles.blocks} tag="main">
<Blocks blocks={loyaltyPage.blocks} /> <Blocks blocks={loyaltyPage.blocks} />
</MaxWidth> </MaxWidth>

View File

@@ -1,5 +1,6 @@
:root { :root {
font-size: 62.5%; font-size: 62.5%;
--max-width: 113.5rem;
} }
* { * {

View File

@@ -5,7 +5,7 @@ import Title from "@/components/Title"
import styles from "./cardGrid.module.css" import styles from "./cardGrid.module.css"
import { CardGridProps, CardProps } from "@/types/components/loyalty/blocks" import { CardGridProps } from "@/types/components/loyalty/blocks"
export default function CardGrid({ card_grid }: CardGridProps) { export default function CardGrid({ card_grid }: CardGridProps) {
return ( return (
@@ -27,17 +27,15 @@ export default function CardGrid({ card_grid }: CardGridProps) {
</header> </header>
<div className={styles.cardContainer}> <div className={styles.cardContainer}>
{card_grid.cards.map((card, i) => ( {card_grid.cards.map((card, i) => (
<CardWrapper key={`${card.title}+${i}`} card={card} /> <div className={styles.cardWrapper} key={`${card.title}+${i}`}>
<Card
subtitle={card.subtitle}
title={card.title}
link={card.link}
/>
</div>
))} ))}
</div> </div>
</section> </section>
) )
} }
function CardWrapper({ card }: CardProps) {
return (
<div className={styles.cardWrapper}>
<Card subtitle={card.subtitle} title={card.title} link={card.link} />
</div>
)
}

View File

@@ -4,10 +4,10 @@ import styles from "./howItWorks.module.css"
export default function HowItWorks() { export default function HowItWorks() {
return ( return (
<div className={styles.container}> <section className={styles.container}>
<Title level="h3" uppercase> <Title level="h3" uppercase>
How it works Placeholder How it works Placeholder
</Title> </Title>
</div> </section>
) )
} }

View File

@@ -33,7 +33,7 @@ export default async function LoyaltyLevels() {
function LevelCard({ level }: LevelCardProps) { function LevelCard({ level }: LevelCardProps) {
return ( return (
<div className={styles.card}> <article className={styles.card}>
<Title level="h4">{level.tier}</Title> <Title level="h4">{level.tier}</Title>
<Image src={level.logo} alt={level.name} width={140} height={54} /> <Image src={level.logo} alt={level.name} width={140} height={54} />
<p className={styles.qualifications}> <p className={styles.qualifications}>
@@ -45,6 +45,6 @@ function LevelCard({ level }: LevelCardProps) {
{benefit} {benefit}
</p> </p>
))} ))}
</div> </article>
) )
} }

View File

@@ -8,7 +8,7 @@
.titleContainer { .titleContainer {
display: grid; display: grid;
grid-template-areas: "title link"; grid-template-areas: "title link" "subtitle subtitle";
grid-template-columns: 1fr max-content; grid-template-columns: 1fr max-content;
padding-bottom: 0.8rem; padding-bottom: 0.8rem;
} }
@@ -25,4 +25,5 @@
.subtitle { .subtitle {
margin: 0; margin: 0;
grid-area: subtitle;
} }

View File

@@ -32,25 +32,23 @@ export default function DynamicContent({
}: DynamicContentProps) { }: DynamicContentProps) {
return ( return (
<section className={styles.container}> <section className={styles.container}>
<header> <header className={styles.titleContainer}>
<div className={styles.titleContainer}> {dynamicContent.title && (
{dynamicContent.title && ( <Title
<Title as="h3"
as="h3" level="h2"
level="h2" className={styles.title}
className={styles.title} weight="semiBold"
weight="semiBold" uppercase
uppercase >
> {dynamicContent.title}
{dynamicContent.title} </Title>
</Title> )}
)} {dynamicContent.link ? (
{dynamicContent.link ? ( <Link className={styles.link} href={dynamicContent.link.href}>
<Link className={styles.link} href={dynamicContent.link.href}> {dynamicContent.link.text}
{dynamicContent.link.text} </Link>
</Link> ) : null}
) : null}
</div>
{dynamicContent.subtitle && ( {dynamicContent.subtitle && (
<Title <Title
as="h5" as="h5"

View File

@@ -13,10 +13,12 @@ export function Blocks({ blocks }: BlocksProps) {
return <CardGrid card_grid={block.card_grid} /> return <CardGrid card_grid={block.card_grid} />
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent: case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
return ( return (
<JsonToHtml <section>
nodes={block.content.content.json.children} <JsonToHtml
embeds={block.content.content.embedded_itemsConnection.edges} nodes={block.content.content.json.children}
/> embeds={block.content.content.embedded_itemsConnection.edges}
/>
</section>
) )
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent: case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
return <DynamicContentBlock dynamicContent={block.dynamic_content} /> return <DynamicContentBlock dynamicContent={block.dynamic_content} />

View File

@@ -5,13 +5,9 @@ import { getValueFromContactConfig } from "@/utils/contactConfig"
import styles from "./contactRow.module.css" import styles from "./contactRow.module.css"
import type { ContactFields } from "@/types/requests/contactConfig" import type { ContactRowProps } from "@/types/components/loyalty/sidebar"
export default async function ContactRow({ export default async function ContactRow({ contact }: ContactRowProps) {
contact,
}: {
contact: ContactFields
}) {
const data = await serverClient().contentstack.contactConfig.get({ const data = await serverClient().contentstack.contactConfig.get({
lang: Lang.en, lang: Lang.en,
}) })

View File

@@ -19,7 +19,7 @@ export default async function Contact({ contactBlock }: ContactProps) {
case JoinLoyaltyContactTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact: case JoinLoyaltyContactTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContactBlockContactContact:
return ( return (
<ContactRow <ContactRow
key={`${contact.display_text}-i`} key={`${contact.display_text}-${i}`}
contact={contact} contact={contact}
/> />
) )

View File

@@ -28,7 +28,7 @@ export default function JoinLoyaltyContact({ block }: JoinLoyaltyContactProps) {
<span>{_("Join Scandic Friends")}</span> <span>{_("Join Scandic Friends")}</span>
</Button> </Button>
<div className={styles.linkContainer}> <div className={styles.linkContainer}>
<Link href="/login" className={styles.logoutLink}> <Link href="/login" className={styles.loginLink}>
{_("Already a friend?")} <br /> {_("Already a friend?")} <br />
{_("Click here to log in")} {_("Click here to log in")}
</Link> </Link>

View File

@@ -21,7 +21,7 @@
margin: 0; margin: 0;
} }
.logoutLink { .loginLink {
text-decoration: none; text-decoration: none;
color: var(--some-black-color, #2e2e2e); color: var(--some-black-color, #2e2e2e);
font-size: 1.2rem; font-size: 1.2rem;

View File

@@ -7,20 +7,26 @@ import styles from "./sidebar.module.css"
import { SidebarProps } from "@/types/components/loyalty/sidebar" import { SidebarProps } from "@/types/components/loyalty/sidebar"
import { SidebarTypenameEnum } from "@/types/requests/loyaltyPage" import { SidebarTypenameEnum } from "@/types/requests/loyaltyPage"
export default function SidebarLoyalty({ block }: SidebarProps) { export default function SidebarLoyalty({ blocks }: SidebarProps) {
switch (block.__typename) { return (
case SidebarTypenameEnum.LoyaltyPageSidebarContent: <aside>
return ( {blocks.map((block) => {
<section className={styles.content}> switch (block.__typename) {
<JsonToHtml case SidebarTypenameEnum.LoyaltyPageSidebarContent:
embeds={block.content.content.embedded_itemsConnection.edges} return (
nodes={block.content.content.json.children} <section className={styles.content}>
/> <JsonToHtml
</section> embeds={block.content.content.embedded_itemsConnection.edges}
) nodes={block.content.content.json.children}
case SidebarTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContact: />
return <JoinLoyaltyContact block={block.join_loyalty_contact} /> </section>
default: )
return null case SidebarTypenameEnum.LoyaltyPageSidebarJoinLoyaltyContact:
} return <JoinLoyaltyContact block={block.join_loyalty_contact} />
default:
return null
}
})}
</aside>
)
} }

View File

@@ -1,9 +1,5 @@
.content { @media screen and (max-width: 950px) {
padding: 0 1.6rem;
}
@media screen and (min-width: 950px) {
.content { .content {
padding: 0; padding: 0 1.6rem;
} }
} }

View File

@@ -15,7 +15,7 @@ export default function Card({
openInNewTab = false, openInNewTab = false,
}: CardProps) { }: CardProps) {
return ( return (
<div className={styles.linkCard}> <article className={styles.linkCard}>
{title ? ( {title ? (
<Title level="h3" weight="semiBold"> <Title level="h3" weight="semiBold">
{title} {title}
@@ -33,6 +33,6 @@ export default function Card({
</Link> </Link>
</Button> </Button>
) : null} ) : null}
</div> </article>
) )
} }

View File

@@ -7,8 +7,8 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
all_loyalty_page(where: { url: $url, locale: $locale }) { all_loyalty_page(where: { url: $url, locale: $locale }) {
items { items {
blocks { blocks {
__typename
... on LoyaltyPageBlocksDynamicContent { ... on LoyaltyPageBlocksDynamicContent {
__typename
dynamic_content { dynamic_content {
title title
subtitle subtitle
@@ -28,7 +28,6 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
} }
} }
... on LoyaltyPageBlocksCardGrid { ... on LoyaltyPageBlocksCardGrid {
__typename
card_grid { card_grid {
title title
subtitle subtitle
@@ -51,7 +50,6 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
} }
} }
... on LoyaltyPageBlocksContent { ... on LoyaltyPageBlocksContent {
__typename
content { content {
content { content {
json json
@@ -70,8 +68,8 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
} }
title title
sidebar { sidebar {
__typename
... on LoyaltyPageSidebarJoinLoyaltyContact { ... on LoyaltyPageSidebarJoinLoyaltyContact {
__typename
join_loyalty_contact { join_loyalty_contact {
title title
preamble preamble
@@ -87,7 +85,6 @@ query GetLoyaltyPage($locale: String!, $url: String!) {
} }
} }
... on LoyaltyPageSidebarContent { ... on LoyaltyPageSidebarContent {
__typename
content { content {
content { content {
json json

View File

@@ -0,0 +1,5 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
export const getConfigInput = z.object({ lang: z.nativeEnum(Lang) })

View File

@@ -0,0 +1,60 @@
import { z } from "zod"
// Help me write this zod schema based on the type ContactConfig
export const validateContactConfigSchema = z.object({
all_contact_config: z.object({
items: z.array(
z.object({
email: z.object({
name: z.string().nullable(),
address: z.string().nullable(),
}),
email_loyalty: z.object({
name: z.string().nullable(),
address: z.string().nullable(),
}),
mailing_address: z.object({
zip: z.string().nullable(),
street: z.string().nullable(),
name: z.string().nullable(),
city: z.string().nullable(),
country: z.string().nullable(),
}),
phone: z.object({
number: z.string().nullable(),
name: z.string().nullable(),
}),
phone_loyalty: z.object({
number: z.string().nullable(),
name: z.string().nullable(),
}),
visiting_address: z.object({
zip: z.string().nullable(),
country: z.string().nullable(),
city: z.string().nullable(),
street: z.string().nullable(),
}),
})
),
}),
})
export enum ContactFieldGroupsEnum {
email = "email",
email_loyalty = "email_loyalty",
mailing_address = "mailing_address",
phone = "phone",
phone_loyalty = "phone_loyalty",
visiting_address = "visiting_address",
}
export type ContactFieldGroups = keyof typeof ContactFieldGroupsEnum
export type ContactConfigData = z.infer<typeof validateContactConfigSchema>
export type ContactConfig = ContactConfigData["all_contact_config"]["items"][0]
export type ContactFields = {
display_text?: string
contact_field: string
}

View File

@@ -1,32 +1,34 @@
import { z } from "zod" import { GetContactConfig } from "@/lib/graphql/Query/ContactConfig.graphql"
import { request } from "@/lib/graphql/request"
import { badRequestError } from "@/server/errors/trpc" import { badRequestError } from "@/server/errors/trpc"
import { publicProcedure, router } from "@/server/trpc" import { publicProcedure, router } from "@/server/trpc"
import { request } from "@/lib/graphql/request"
import { Lang } from "@/constants/languages"
import GetContactConfig from "@/lib/graphql/Query/ContactConfig.graphql" import { getConfigInput } from "./input"
import { type ContactConfigData, validateContactConfigSchema } from "./output"
import type { GetContactConfigData } from "@/types/requests/contactConfig"
export const contactConfigQueryRouter = router({ export const contactConfigQueryRouter = router({
get: publicProcedure get: publicProcedure.input(getConfigInput).query(async ({ input }) => {
.input(z.object({ lang: z.nativeEnum(Lang) })) try {
.query(async ({ input }) => { const contactConfig = await request<ContactConfigData>(GetContactConfig, {
const contactConfig = await request<GetContactConfigData>( locale: input.lang,
GetContactConfig, })
{
locale: input.lang,
},
{
tags: [`contact-config-${input.lang}`],
}
)
if (contactConfig.data && contactConfig.data.all_contact_config.total) { if (!contactConfig.data) {
return contactConfig.data.all_contact_config.items[0] throw badRequestError()
} }
const validatedContactConfigConfig =
validateContactConfigSchema.safeParse(contactConfig.data)
if (!validatedContactConfigConfig.success) {
console.error(validatedContactConfigConfig.error)
throw badRequestError()
}
return validatedContactConfigConfig.data.all_contact_config.items[0]
} catch (e) {
console.log(e)
throw badRequestError() throw badRequestError()
}), }
}),
}) })

View File

@@ -1,8 +1,8 @@
import { router } from "@/server/trpc" import { router } from "@/server/trpc"
import { breadcrumbsRouter } from "./breadcrumbs" import { breadcrumbsRouter } from "./breadcrumbs"
import { loyaltyPageRouter } from "./loyaltyPage"
import { contactConfigRouter } from "./contactConfig" import { contactConfigRouter } from "./contactConfig"
import { loyaltyPageRouter } from "./loyaltyPage"
export const contentstackRouter = router({ export const contentstackRouter = router({
breadcrumbs: breadcrumbsRouter, breadcrumbs: breadcrumbsRouter,

View File

@@ -31,6 +31,7 @@ export const loyaltyPageQueryRouter = router({
) )
if (!validatedLoyaltyPage.success) { if (!validatedLoyaltyPage.success) {
console.error(validatedLoyaltyPage.error)
throw badRequestError() throw badRequestError()
} }
@@ -68,15 +69,13 @@ export const loyaltyPageQueryRouter = router({
cards: block.card_grid.cards.map((card) => { cards: block.card_grid.cards.map((card) => {
return { return {
...card, ...card,
link: link: card.referenceConnection.totalCount
card.referenceConnection.totalCount > 0 ? {
? { href: card.referenceConnection.edges[0].node.url,
href: card.referenceConnection.edges[0].node title:
.url, card.referenceConnection.edges[0].node.title,
title: }
card.referenceConnection.edges[0].node.title, : undefined,
}
: undefined,
} }
}), }),
}, },
@@ -86,17 +85,16 @@ export const loyaltyPageQueryRouter = router({
...block, ...block,
dynamic_content: { dynamic_content: {
...block.dynamic_content, ...block.dynamic_content,
link: link: block.dynamic_content.link.pageConnection.totalCount
block.dynamic_content.link.pageConnection.totalCount > 0 ? {
? { text: block.dynamic_content.link.text,
text: block.dynamic_content.link.text, href: block.dynamic_content.link.pageConnection
href: block.dynamic_content.link.pageConnection .edges[0].node.url,
.edges[0].node.url, title:
title: block.dynamic_content.link.pageConnection.edges[0]
block.dynamic_content.link.pageConnection.edges[0] .node.title,
.node.title, }
} : undefined,
: undefined,
}, },
} }
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent: case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:

View File

@@ -1,11 +1,12 @@
import { initTRPC } from "@trpc/server" import { initTRPC } from "@trpc/server"
import { env } from "@/env/server" import { env } from "@/env/server"
import { transformer } from "./transformer"
import { unauthorizedError } from "./errors/trpc"
import type { Context } from "./context" import { unauthorizedError } from "./errors/trpc"
import { transformer } from "./transformer"
import type { Meta } from "@/types/trpc/meta" import type { Meta } from "@/types/trpc/meta"
import type { Context } from "./context"
const t = initTRPC.context<Context>().meta<Meta>().create({ transformer }) const t = initTRPC.context<Context>().meta<Meta>().create({ transformer })

View File

@@ -1,7 +1,6 @@
import { import {
Block, Block,
CardGrid, CardGrid,
CardGridCard,
DynamicContent, DynamicContent,
RteBlockContent, RteBlockContent,
} from "@/server/routers/contentstack/loyaltyPage/output" } from "@/server/routers/contentstack/loyaltyPage/output"
@@ -18,8 +17,6 @@ export type DynamicComponentProps = {
component: DynamicContent["dynamic_content"]["component"] component: DynamicContent["dynamic_content"]["component"]
} }
export type CardProps = { card: CardGridCard }
export type CardGridProps = Pick<CardGrid, "card_grid"> export type CardGridProps = Pick<CardGrid, "card_grid">
export type Content = { content: RteBlockContent["content"]["content"] } export type Content = { content: RteBlockContent["content"]["content"] }

View File

@@ -1,10 +1,11 @@
import { ContactFields } from "@/server/routers/contentstack/contactConfig/output"
import { import {
JoinLoyaltyContact, JoinLoyaltyContact,
Sidebar, Sidebar,
} from "@/server/routers/contentstack/loyaltyPage/output" } from "@/server/routers/contentstack/loyaltyPage/output"
export type SidebarProps = { export type SidebarProps = {
block: Sidebar blocks: Sidebar[]
} }
export type JoinLoyaltyContactProps = { export type JoinLoyaltyContactProps = {
@@ -14,3 +15,7 @@ export type JoinLoyaltyContactProps = {
export type ContactProps = { export type ContactProps = {
contactBlock: JoinLoyaltyContact["join_loyalty_contact"]["contact"] contactBlock: JoinLoyaltyContact["join_loyalty_contact"]["contact"]
} }
export type ContactRowProps = {
contact: ContactFields
}

View File

@@ -1,53 +0,0 @@
import { AllRequestResponse } from "./utils/all"
export type ContactConfig = {
email: {
name?: string
address?: string
}
email_loyalty: {
name?: string
address?: string
}
mailing_address: {
zip?: string
street?: string
name?: string
city?: string
country?: string
}
phone: {
number?: string
name?: string
}
phone_loyalty: {
number?: string
name?: string
}
visiting_address: {
zip?: string
country?: string
city?: string
street?: string
}
}
export enum ContactFieldGroupsEnum {
email = "email",
email_loyalty = "email_loyalty",
mailing_address = "mailing_address",
phone = "phone",
phone_loyalty = "phone_loyalty",
visiting_address = "visiting_address",
}
export type ContactFieldGroups = keyof typeof ContactFieldGroupsEnum
export type GetContactConfigData = {
all_contact_config: AllRequestResponse<ContactConfig>
}
export type ContactFields = {
display_text?: string
contact_field: string
}

View File

@@ -1,11 +1,13 @@
export type GetContentTypeUidType = { import z from "zod"
all_content_page: {
total: number export const validateContentTypeUid = z.object({
} all_content_page: z.object({
all_loyalty_page: { total: z.number(),
total: number }),
} all_loyalty_page: z.object({
all_current_blocks_page: { total: z.number(),
total: number }),
} all_current_blocks_page: z.object({
} total: z.number(),
}),
})

View File

@@ -1,7 +1,7 @@
import { import {
ContactConfig, ContactConfig,
ContactFieldGroups, ContactFieldGroups,
} from "@/types/requests/contactConfig" } from "@/server/routers/contentstack/contactConfig/output"
export function getValueFromContactConfig( export function getValueFromContactConfig(
keyString: string, keyString: string,
@@ -16,4 +16,5 @@ export function getValueFromContactConfig(
return fieldGroup[key] return fieldGroup[key]
} }
return undefined
} }

View File

@@ -1,10 +1,10 @@
import { DocumentNode } from "graphql" import { DocumentNode, print } from "graphql"
import { Lang } from "@/constants/languages" import { Lang } from "@/constants/languages"
import { env } from "@/env/server" import { env } from "@/env/server"
import GetContentTypeUid from "@/lib/graphql/Query/ContentTypeUid.graphql" import { GetContentTypeUid } from "@/lib/graphql/Query/ContentTypeUid.graphql"
import type { GetContentTypeUidType } from "@/types/requests/contentTypeUid" import { validateContentTypeUid } from "@/types/requests/contentTypeUid"
export enum PageTypeEnum { export enum PageTypeEnum {
CurrentBlocksPage = "CurrentBlocksPage", CurrentBlocksPage = "CurrentBlocksPage",
@@ -16,7 +16,6 @@ export async function getContentTypeByPathName(
pathNameWithoutLang: string, pathNameWithoutLang: string,
lang = Lang.en lang = Lang.en
) { ) {
const print = (await import("graphql/language/printer")).print
const result = await fetch(env.CMS_URL, { const result = await fetch(env.CMS_URL, {
method: "POST", method: "POST",
headers: { headers: {
@@ -33,7 +32,17 @@ export async function getContentTypeByPathName(
}) })
const pageTypeData = await result.json() const pageTypeData = await result.json()
const pageType = pageTypeData.data as GetContentTypeUidType
const validatedContentTypeUid = validateContentTypeUid.safeParse(
pageTypeData.data
)
if (!validatedContentTypeUid.success) {
console.error(validatedContentTypeUid.error)
return null
}
const pageType = validatedContentTypeUid.data
if (pageType.all_content_page.total) { if (pageType.all_content_page.total) {
return PageTypeEnum.ContentPage return PageTypeEnum.ContentPage