Merged in feat/get-profile (pull request #124)

feat(WEB-169, WEB-203, WEB-204): get profile data from API

Approved-by: Michael Zetterberg
This commit is contained in:
Simon.Emanuelsson
2024-04-18 11:57:35 +00:00
committed by Michael Zetterberg
44 changed files with 632 additions and 607 deletions

View File

@@ -1,4 +1,5 @@
ADOBE_SCRIPT_SRC="" ADOBE_SCRIPT_SRC=""
API_BASEURL="https://tstapi.scandichotels.com"
CMS_ACCESS_TOKEN="" CMS_ACCESS_TOKEN=""
CMS_API_KEY="" CMS_API_KEY=""
CMS_ENVIRONMENT="development" CMS_ENVIRONMENT="development"

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
"use client"
import type { ErrorPage } from "@/types/next/error"
export default function MyPageOverviewError({ error }: ErrorPage) {
console.error(error)
return <h1>Error happened, overview</h1>
}

View File

@@ -10,7 +10,7 @@ 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 MyPageOverview({ params }: PageArgs<LangParams>) {
const user = await serverClient().user.get() const user = await serverClient().user.get()
return ( return (
<MaxWidth className={styles.blocks} tag="main"> <MaxWidth className={styles.blocks} tag="main">

View File

@@ -1,8 +1,5 @@
import { serverClient } from "@/lib/trpc/server"
import EditProfile from "@/components/MyProfile/Profile/Edit" import EditProfile from "@/components/MyProfile/Profile/Edit"
export default async function EditProfileSlot() { export default function EditProfileSlot() {
const user = await serverClient().user.get() return <EditProfile />
return <EditProfile user={user} />
} }

View File

@@ -0,0 +1,8 @@
"use client"
import type { ErrorPage } from "@/types/next/error"
export default function ProfileError({ error }: ErrorPage) {
console.error(error)
return <h1>Error happened, Profile</h1>
}

View File

@@ -1,8 +1,5 @@
import { serverClient } from "@/lib/trpc/server"
import Profile from "@/components/MyProfile/Profile" import Profile from "@/components/MyProfile/Profile"
export default async function ProfileInfo() { export default function ProfileInfo() {
const user = await serverClient().user.get() return <Profile />
return <Profile user={user} />
} }

24
auth.ts
View File

@@ -18,7 +18,7 @@ const customProvider = {
authorization: { authorization: {
url: `${env.CURITY_ISSUER_USER}/oauth/v2/authorize`, url: `${env.CURITY_ISSUER_USER}/oauth/v2/authorize`,
params: { params: {
scope: ["openid"], scope: ["openid", "profile"].join(" "),
/** /**
* The `acr_values` param is used to make Curity display the proper login * The `acr_values` param is used to make Curity display the proper login
* page for Scandic. Without the parameter Curity presents some choices * page for Scandic. Without the parameter Curity presents some choices
@@ -53,9 +53,9 @@ export const config = {
strategy: "jwt", strategy: "jwt",
}, },
callbacks: { callbacks: {
async signIn(...args) { async signIn() {
console.log("****** SIGN IN *******") console.log("****** SIGN IN *******")
console.log(args) console.log(arguments)
console.log("****** END - SIGN IN *******") console.log("****** END - SIGN IN *******")
return true return true
}, },
@@ -68,6 +68,7 @@ export const config = {
if (session.user) { if (session.user) {
return { return {
...session, ...session,
token,
user: { user: {
...session.user, ...session.user,
id: token.sub, id: token.sub,
@@ -107,22 +108,27 @@ export const config = {
console.log("****** END - AUTHORIZED *******") console.log("****** END - AUTHORIZED *******")
return true return true
}, },
async jwt({ session, token, trigger }) { async jwt({ session, token, trigger, account }) {
console.log("****** JWT *******") console.log("****** JWT *******")
console.log({ session, token, trigger }) console.log({ session, token, trigger, account })
console.log("****** END - JWT *******") console.log("****** END - JWT *******")
if (account) {
return {
access_token: account.access_token,
}
}
return token return token
}, },
}, },
events: { events: {
async signIn(...args) { async signIn() {
console.log("#### SIGNIN EVENT ARGS ######") console.log("#### SIGNIN EVENT ARGS ######")
console.log(args) console.log(arguments)
console.log("#### END - SIGNIN EVENT ARGS ######") console.log("#### END - SIGNIN EVENT ARGS ######")
}, },
async session(...args) { async session() {
console.log("#### SESSION EVENT ARGS ######") console.log("#### SESSION EVENT ARGS ######")
console.log(args) console.log(arguments)
console.log("#### END - SESSION EVENT ARGS ######") console.log("#### END - SESSION EVENT ARGS ######")
}, },
}, },

View File

@@ -5,11 +5,9 @@ import Image from "@/components/Image"
import type { User } from "@/types/user" import type { User } from "@/types/user"
export default function CopyButton({ export default function CopyButton({ membership }: Pick<User, "membership">) {
membershipId,
}: Pick<User, "membershipId">) {
function handleCopy() { function handleCopy() {
console.log(`COPIED! (${membershipId})`) console.log(`COPIED! (${membership.membershipNumber})`)
} }
return ( return (

View File

@@ -17,7 +17,9 @@ export default function Friend({ user }: FriendProps) {
/> />
<h3 className={styles.name}>{user.name}</h3> <h3 className={styles.name}>{user.name}</h3>
<div className={styles.membershipContainer}> <div className={styles.membershipContainer}>
<p className={styles.membershipId}>{user.membershipId}</p> <p className={styles.membershipId}>
{user.membership.membershipNumber}
</p>
</div> </div>
</section> </section>
) )

View File

@@ -1,37 +0,0 @@
import Divider from "@/components/TempDesignSystem/Divider"
import Image from "@/components/Image"
import styles from "./points.module.css"
import type { QualifyingPointsProps } from "@/types/components/myPages/myPage/qualifyingPoints"
export default function QualifyingPoints({ user }: QualifyingPointsProps) {
return (
<div className={styles.qualifyingPoints}>
<h4 className={styles.title}>Progress</h4>
<Divider variant="dotted" />
<div className={styles.container}>
<div className={styles.points}>
<Image
alt="Arrow Up Icon"
height={24}
src="/_static/icons/arrow_upward.svg"
width={24}
/>
<p className={styles.point}>{user.qualifyingPoints}</p>
<h5 className={styles.pointTitle}>Qualifying points</h5>
</div>
<div className={styles.points}>
<Image
alt="Arrow Up Icon"
height={24}
src="/_static/icons/arrow_upward.svg"
width={24}
/>
<p className={styles.point}>{user.nights}</p>
<h5 className={styles.pointTitle}>Nights</h5>
</div>
</div>
</div>
)
}

View File

@@ -1,56 +0,0 @@
.qualifyingPoints {
display: none;
}
.title {
color: var(--some-grey-color, #000);
/* font-family: var(--ff-brandon-text); */
font-size: 1.5rem;
font-weight: 500;
letter-spacing: 0.6%;
line-height: 1.7rem;
margin: 0 0 0.7rem;
}
@media screen and (min-width: 950px) {
.qualifyingPoints {
display: block;
}
.container {
display: grid;
gap: 3.8rem;
grid-template-columns: auto 1fr;
margin-top: 1.5rem;
}
.title {
color: var(--some-grey-color, #4f4f4f);
font-size: 1.2rem;
}
.points {
align-items: center;
display: grid;
gap: 1rem;
grid-template-columns: auto 1fr;
}
.point {
/* font-family: var(--ff-brandon-text); */
font-size: 2.7rem;
font-weight: 900;
line-height: 2.7rem;
margin: 0;
}
.pointTitle {
font-family: var(--ff-fira-sans);
font-size: 1.2rem;
font-weight: 400;
letter-spacing: 0.6%;
line-height: 1.4rem;
grid-column: 1/-1;
margin: 0;
}
}

View File

@@ -10,7 +10,7 @@ export default function TotalPoints({ user }: TotalPointsProps) {
<div> <div>
<Title>Total Points</Title> <Title>Total Points</Title>
<Divider className={styles.divider} variant="dotted" /> <Divider className={styles.divider} variant="dotted" />
<p className={styles.points}>{user.points}</p> <p className={styles.points}>{user.membership.currentPoints}</p>
</div> </div>
) )
} }

View File

@@ -5,7 +5,7 @@ import Image from "@/components/Image"
import styles from "./profile.module.css" import styles from "./profile.module.css"
import type { ProfileProps } from "@/types/components/myPages/myProfile/profile" import type { ContainerProps } from "@/types/components/myPages/myProfile/container"
const profileStyles = cva(styles.profile) const profileStyles = cva(styles.profile)
@@ -14,7 +14,7 @@ export default function Container({
className, className,
user, user,
...props ...props
}: ProfileProps) { }: ContainerProps) {
return ( return (
<Card className={profileStyles({ className })} {...props}> <Card className={profileStyles({ className })} {...props}>
<header className={styles.header}> <header className={styles.header}>

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useEffect } from "react" import { useEffect } from "react"
import { useFormStatus } from "react-dom" import { useFormStatus } from "react-dom"
import { useWatch } from "react-hook-form"
import { _ } from "@/lib/translation" import { _ } from "@/lib/translation"
import { useProfileStore } from "@/stores/edit-profile" import { useProfileStore } from "@/stores/edit-profile"
@@ -22,6 +23,7 @@ import type { EditFormContentProps } from "@/types/components/myPages/myProfile/
export default function FormContent({ control }: EditFormContentProps) { export default function FormContent({ control }: EditFormContentProps) {
const { pending } = useFormStatus() const { pending } = useFormStatus()
const setIsPending = useProfileStore((store) => store.setIsPending) const setIsPending = useProfileStore((store) => store.setIsPending)
const country = useWatch({ name: "address.country" })
useEffect(() => { useEffect(() => {
setIsPending(pending) setIsPending(pending)
@@ -30,10 +32,10 @@ export default function FormContent({ control }: EditFormContentProps) {
return ( return (
<> <>
<Field> <Field>
<Field.Icon>SE</Field.Icon> <Field.Icon>{country}</Field.Icon>
<Field.Label htmlFor="country">*{_("Country")}</Field.Label> <Field.Label htmlFor="address.country">*{_("Country")}</Field.Label>
<Field.Content> <Field.Content>
<CountrySelect name="country" /> <CountrySelect name="address.country" />
</Field.Content> </Field.Content>
</Field> </Field>
@@ -72,9 +74,9 @@ export default function FormContent({ control }: EditFormContentProps) {
<Field.Icon> <Field.Icon>
<PhoneIcon /> <PhoneIcon />
</Field.Icon> </Field.Icon>
<Field.Label htmlFor="phone">*{_("Phone")}</Field.Label> <Field.Label htmlFor="phoneNumber">*{_("Phone")}</Field.Label>
<Field.Content> <Field.Content>
<Phone name="phone" /> <Phone countrySelectName="address.country" name="phoneNumber" />
</Field.Content> </Field.Content>
</Field> </Field>
@@ -82,12 +84,14 @@ export default function FormContent({ control }: EditFormContentProps) {
<Field.Icon> <Field.Icon>
<HouseIcon /> <HouseIcon />
</Field.Icon> </Field.Icon>
<Field.Label htmlFor="street">*{_("Address")}</Field.Label> <Field.Label htmlFor="address.streetAddress">
*{_("Address")}
</Field.Label>
<Field.Content> <Field.Content>
<Input <Input
aria-label={_("Street")} aria-label={_("Street")}
control={control} control={control}
name="street" name="address.streetAddress"
placeholder={_("Street 123")} placeholder={_("Street 123")}
registerOptions={{ required: true }} registerOptions={{ required: true }}
/> />
@@ -98,12 +102,12 @@ export default function FormContent({ control }: EditFormContentProps) {
<Field.Icon> <Field.Icon>
<HouseIcon /> <HouseIcon />
</Field.Icon> </Field.Icon>
<Field.Label htmlFor="city">*{_("City/State")}</Field.Label> <Field.Label htmlFor="address.city">*{_("City/State")}</Field.Label>
<Field.Content> <Field.Content>
<Input <Input
aria-label={_("City")} aria-label={_("City")}
control={control} control={control}
name="city" name="address.city"
placeholder={_("City")} placeholder={_("City")}
registerOptions={{ required: true }} registerOptions={{ required: true }}
/> />
@@ -114,12 +118,12 @@ export default function FormContent({ control }: EditFormContentProps) {
<Field.Icon> <Field.Icon>
<HouseIcon /> <HouseIcon />
</Field.Icon> </Field.Icon>
<Field.Label htmlFor="zip">*{_("Zip code")}</Field.Label> <Field.Label htmlFor="address.zipCode">*{_("Zip code")}</Field.Label>
<Field.Content> <Field.Content>
<Input <Input
aria-label={_("Zip code")} aria-label={_("Zip code")}
control={control} control={control}
name="zip" name="address.zipCode"
placeholder={_("Zip code")} placeholder={_("Zip code")}
registerOptions={{ required: true }} registerOptions={{ required: true }}
/> />

View File

@@ -30,15 +30,7 @@ export default function Form({ user }: EditFormProps) {
) )
const form = useForm<EditProfileSchema>({ const form = useForm<EditProfileSchema>({
defaultValues: { defaultValues: user,
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", criteriaMode: "all",
mode: "onTouched", mode: "onTouched",
resolver: zodResolver(editProfileSchema), resolver: zodResolver(editProfileSchema),

View File

@@ -4,26 +4,22 @@ import { _ } from "@/lib/translation"
import { phoneValidator } from "@/utils/phoneValidator" import { phoneValidator } from "@/utils/phoneValidator"
export const editProfileSchema = z.object({ export const editProfileSchema = z.object({
city: z "address.country": z
.string({ required_error: _("City is required") })
.min(1, { message: _("City is required") }),
country: z
.string({ required_error: _("Country is required") }) .string({ required_error: _("Country is required") })
.min(1, { message: _("Country is required") }), .min(1, { message: _("Country is required") }),
"address.city": z.string().optional(),
"address.streetAddress": z.string().optional(),
"address.zipCode": z
.string({ required_error: _("Zip code is required") })
.min(1, { message: _("Zip code is required") }),
dob: z dob: z
.string({ required_error: _("Date of Birth is required") }) .string({ required_error: _("Date of Birth is required") })
.min(1, { message: _("Date of Birth is required") }), .min(1, { message: _("Date of Birth is required") }),
email: z.string().email(), email: z.string().email(),
phone: phoneValidator( phoneNumber: phoneValidator(
_("Phone is required"), _("Phone is required"),
_("Please enter a valid phone number") _("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> export type EditProfileSchema = z.infer<typeof editProfileSchema>

View File

@@ -1,12 +1,13 @@
import { serverClient } from "@/lib/trpc/server"
import Container from "../Container" import Container from "../Container"
import Form from "./Form" import Form from "./Form"
import type { ProfileProps } from "@/types/components/myPages/myProfile/profile" export default async function EditProfile() {
const user = await serverClient().user.get()
export default function EditProfile(props: ProfileProps) {
return ( return (
<Container {...props}> <Container user={user}>
<Form user={props.user} /> <Form user={user} />
</Container> </Container>
) )
} }

View File

@@ -1,4 +1,5 @@
import { _ } from "@/lib/translation" import { _ } from "@/lib/translation"
import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
import { import {
CalendarIcon, CalendarIcon,
@@ -11,22 +12,27 @@ 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 { serverClient } from "@/lib/trpc/server"
export default function Profile(props: ProfileProps) { export default async function Profile() {
const user = await serverClient().user.get()
const countryName = countries.find(
(country) => country.code === user.address.country
)
return ( return (
<Container {...props}> <Container user={user}>
<section className={styles.info}> <section className={styles.info}>
<Field> <Field>
<Field.Icon>SE</Field.Icon> <Field.Icon>{user.address.country}</Field.Icon>
<Field.TextLabel>{_("Country")}</Field.TextLabel> <Field.TextLabel>{_("Country")}</Field.TextLabel>
<Field.Content>Sweden</Field.Content> <Field.Content>{countryName?.name}</Field.Content>
</Field> </Field>
<Field> <Field>
<Field.Icon> <Field.Icon>
<CalendarIcon /> <CalendarIcon />
</Field.Icon> </Field.Icon>
<Field.TextLabel>{_("Date of Birth")}</Field.TextLabel> <Field.TextLabel>{_("Date of Birth")}</Field.TextLabel>
{/* TODO: Get this from user when API team adds it to payload */}
<Field.Content>27/05/1977</Field.Content> <Field.Content>27/05/1977</Field.Content>
</Field> </Field>
<Field> <Field>
@@ -34,35 +40,36 @@ export default function Profile(props: ProfileProps) {
<EmailIcon /> <EmailIcon />
</Field.Icon> </Field.Icon>
<Field.TextLabel>{_("Email")}</Field.TextLabel> <Field.TextLabel>{_("Email")}</Field.TextLabel>
<Field.Content>f*********@g****.com</Field.Content> <Field.Content>{user.email}</Field.Content>
</Field> </Field>
<Field> <Field>
<Field.Icon> <Field.Icon>
<PhoneIcon /> <PhoneIcon />
</Field.Icon> </Field.Icon>
<Field.TextLabel>{_("Phone number")}</Field.TextLabel> <Field.TextLabel>{_("Phone number")}</Field.TextLabel>
<Field.Content>+46 ******00</Field.Content> <Field.Content>{user.phoneNumber}</Field.Content>
</Field> </Field>
<Field> <Field>
<Field.Icon> <Field.Icon>
<HouseIcon /> <HouseIcon />
</Field.Icon> </Field.Icon>
<Field.TextLabel>{_("Address")}</Field.TextLabel> <Field.TextLabel>{_("Address")}</Field.TextLabel>
<Field.Content>T***************</Field.Content> <Field.Content>{user.address.streetAddress || "-"}</Field.Content>
</Field> </Field>
<Field> <Field>
<Field.Icon> <Field.Icon>
<HouseIcon /> <HouseIcon />
</Field.Icon> </Field.Icon>
<Field.TextLabel>{_("City/State")}</Field.TextLabel> <Field.TextLabel>{_("City/State")}</Field.TextLabel>
<Field.Content>S*******</Field.Content> {/* TODO: Get this from user when API team adds it to payload */}
<Field.Content>{user.address.city || "-"}</Field.Content>
</Field> </Field>
<Field> <Field>
<Field.Icon> <Field.Icon>
<HouseIcon /> <HouseIcon />
</Field.Icon> </Field.Icon>
<Field.TextLabel>{_("Zip code")}</Field.TextLabel> <Field.TextLabel>{_("Zip code")}</Field.TextLabel>
<Field.Content>1****</Field.Content> <Field.Content>{user.address.zipCode}</Field.Content>
</Field> </Field>
</section> </section>
</Container> </Container>

View File

@@ -1,256 +1,261 @@
export const countries = [ export const countriesMap = {
{ name: "Afghanistan", code: "AF" }, Afghanistan: "AF",
{ name: "Albania", code: "AL" }, Albania: "AL",
{ name: "Algeria", code: "DZ" }, Algeria: "DZ",
{ name: "American Samoa", code: "AS" }, "American Samoa": "AS",
{ name: "Andorra", code: "AD" }, Andorra: "AD",
{ name: "Angola", code: "AO" }, Angola: "AO",
{ name: "Anguilla", code: "AI" }, Anguilla: "AI",
{ name: "Antarctica", code: "AQ" }, Antarctica: "AQ",
{ name: "Antigua and Barbuda", code: "AG" }, "Antigua and Barbuda": "AG",
{ name: "Argentina", code: "AR" }, Argentina: "AR",
{ name: "Armenia", code: "AM" }, Armenia: "AM",
{ name: "Aruba", code: "AW" }, Aruba: "AW",
{ name: "Australia", code: "AU" }, Australia: "AU",
{ name: "Austria", code: "AT" }, Austria: "AT",
{ name: "Azerbaijan", code: "AZ" }, Azerbaijan: "AZ",
{ name: "Bahamas", code: "BS" }, Bahamas: "BS",
{ name: "Bahrain", code: "BH" }, Bahrain: "BH",
{ name: "Bangladesh", code: "BD" }, Bangladesh: "BD",
{ name: "Barbados", code: "BB" }, Barbados: "BB",
{ name: "Belarus", code: "BY" }, Belarus: "BY",
{ name: "Belgium", code: "BE" }, Belgium: "BE",
{ name: "Belize", code: "BZ" }, Belize: "BZ",
{ name: "Benin", code: "BJ" }, Benin: "BJ",
{ name: "Bermuda", code: "BM" }, Bermuda: "BM",
{ name: "Bhutan", code: "BT" }, Bhutan: "BT",
{ name: "Bolivia", code: "BO" }, Bolivia: "BO",
{ name: "Bonaire", code: "BQ" }, Bonaire: "BQ",
{ name: "Bosnia and Herzegovina", code: "BA" }, "Bosnia and Herzegovina": "BA",
{ name: "Botswana", code: "BW" }, Botswana: "BW",
{ name: "Bouvet Island", code: "BV" }, "Bouvet Island": "BV",
{ name: "Brazil", code: "BR" }, Brazil: "BR",
{ name: "British Indian Ocean Territory", code: "IO" }, "British Indian Ocean Territory": "IO",
{ name: "Brunei Darussalam", code: "BN" }, "Brunei Darussalam": "BN",
{ name: "Bulgaria", code: "BG" }, Bulgaria: "BG",
{ name: "Burkina Faso", code: "BF" }, "Burkina Faso": "BF",
{ name: "Burundi", code: "BI" }, Burundi: "BI",
{ name: "Cambodia", code: "KH" }, Cambodia: "KH",
{ name: "Cameroon", code: "CM" }, Cameroon: "CM",
{ name: "Canada", code: "CA" }, Canada: "CA",
{ name: "Cape Verde", code: "CV" }, "Cape Verde": "CV",
{ name: "Cayman Islands", code: "KY" }, "Cayman Islands": "KY",
{ name: "Central African Republic", code: "CF" }, "Central African Republic": "CF",
{ name: "Chad", code: "TD" }, Chad: "TD",
{ name: "Chile", code: "CL" }, Chile: "CL",
{ name: "China", code: "CN" }, China: "CN",
{ name: "Christmas Island", code: "CX" }, "Christmas Island": "CX",
{ name: "Cocos (Keeling) Islands", code: "CC" }, "Cocos (Keeling) Islands": "CC",
{ name: "Colombia", code: "CO" }, Colombia: "CO",
{ name: "Comoros", code: "KM" }, Comoros: "KM",
{ name: "Congo", code: "CG" }, Congo: "CG",
{ name: "Congo, The Democratic Republic of the", code: "CD" }, "Congo, The Democratic Republic of the": "CD",
{ name: "Cook Islands", code: "CK" }, "Cook Islands": "CK",
{ name: "Costa Rica", code: "CR" }, "Costa Rica": "CR",
{ name: 'Cote D"Ivoire', code: "CI" }, 'Cote D"Ivoire': "CI",
{ name: "Croatia", code: "HR" }, Croatia: "HR",
{ name: "Cuba", code: "CU" }, Cuba: "CU",
{ name: "Curacao", code: "CW" }, Curacao: "CW",
{ name: "Cyprus", code: "CY" }, Cyprus: "CY",
{ name: "Czech Republic", code: "CZ" }, "Czech Republic": "CZ",
{ name: "Denmark", code: "DK" }, Denmark: "DK",
{ name: "Djibouti", code: "DJ" }, Djibouti: "DJ",
{ name: "Dominica", code: "DM" }, Dominica: "DM",
{ name: "Dominican Republic", code: "DO" }, "Dominican Republic": "DO",
{ name: "Ecuador", code: "EC" }, Ecuador: "EC",
{ name: "Egypt", code: "EG" }, Egypt: "EG",
{ name: "El Salvador", code: "SV" }, "El Salvador": "SV",
{ name: "Equatorial Guinea", code: "GQ" }, "Equatorial Guinea": "GQ",
{ name: "Eritrea", code: "ER" }, Eritrea: "ER",
{ name: "Estonia", code: "EE" }, Estonia: "EE",
{ name: "Eswatini", code: "SZ" }, Eswatini: "SZ",
{ name: "Ethiopia", code: "ET" }, Ethiopia: "ET",
{ name: "Falkland Islands (Malvinas)", code: "FK" }, "Falkland Islands (Malvinas)": "FK",
{ name: "Faroe Islands", code: "FO" }, "Faroe Islands": "FO",
{ name: "Fiji", code: "FJ" }, Fiji: "FJ",
{ name: "Finland", code: "FI" }, Finland: "FI",
{ name: "France", code: "FR" }, France: "FR",
{ name: "French Guiana", code: "GF" }, "French Guiana": "GF",
{ name: "French Polynesia", code: "PF" }, "French Polynesia": "PF",
{ name: "French Southern Territories", code: "TF" }, "French Southern Territories": "TF",
{ name: "Gabon", code: "GA" }, Gabon: "GA",
{ name: "Gambia", code: "GM" }, Gambia: "GM",
{ name: "Georgia", code: "GE" }, Georgia: "GE",
{ name: "Germany", code: "DE" }, Germany: "DE",
{ name: "Ghana", code: "GH" }, Ghana: "GH",
{ name: "Gibraltar", code: "GI" }, Gibraltar: "GI",
{ name: "Greece", code: "GR" }, Greece: "GR",
{ name: "Greenland", code: "GL" }, Greenland: "GL",
{ name: "Grenada", code: "GD" }, Grenada: "GD",
{ name: "Guadeloupe", code: "GP" }, Guadeloupe: "GP",
{ name: "Guam", code: "GU" }, Guam: "GU",
{ name: "Guatemala", code: "GT" }, Guatemala: "GT",
{ name: "Guernsey", code: "GG" }, Guernsey: "GG",
{ name: "Guinea", code: "GN" }, Guinea: "GN",
{ name: "Guinea-Bissau", code: "GW" }, "Guinea-Bissau": "GW",
{ name: "Guyana", code: "GY" }, Guyana: "GY",
{ name: "Haiti", code: "HT" }, Haiti: "HT",
{ name: "Heard Island and Mcdonald Islands", code: "HM" }, "Heard Island and Mcdonald Islands": "HM",
{ name: "Holy See (Vatican City State)", code: "VA" }, "Holy See (Vatican City State)": "VA",
{ name: "Honduras", code: "HN" }, Honduras: "HN",
{ name: "Hong Kong", code: "HK" }, "Hong Kong": "HK",
{ name: "Hungary", code: "HU" }, Hungary: "HU",
{ name: "Iceland", code: "IS" }, Iceland: "IS",
{ name: "India", code: "IN" }, India: "IN",
{ name: "Indonesia", code: "ID" }, Indonesia: "ID",
{ name: "Iran, Islamic Republic Of", code: "IR" }, "Iran, Islamic Republic Of": "IR",
{ name: "Iraq", code: "IQ" }, Iraq: "IQ",
{ name: "Ireland", code: "IE" }, Ireland: "IE",
{ name: "Isle of Man", code: "IM" }, "Isle of Man": "IM",
{ name: "Israel", code: "IL" }, Israel: "IL",
{ name: "Italy", code: "IT" }, Italy: "IT",
{ name: "Ivory Coast", code: "CI" }, "Ivory Coast": "CI",
{ name: "Jamaica", code: "JM" }, Jamaica: "JM",
{ name: "Japan", code: "JP" }, Japan: "JP",
{ name: "Jersey", code: "JE" }, Jersey: "JE",
{ name: "Jordan", code: "JO" }, Jordan: "JO",
{ name: "Kazakhstan", code: "KZ" }, Kazakhstan: "KZ",
{ name: "Kenya", code: "KE" }, Kenya: "KE",
{ name: "Kiribati", code: "KI" }, Kiribati: "KI",
{ name: 'Korea, Democratic People"S Republic of', code: "KP" }, 'Korea, Democratic People"S Republic of': "KP",
{ name: "Korea, Republic of", code: "KR" }, "Korea, Republic of": "KR",
{ name: "Kuwait", code: "KW" }, Kuwait: "KW",
{ name: "Kyrgyzstan", code: "KG" }, Kyrgyzstan: "KG",
{ name: 'Lao People"S Democratic Republic', code: "LA" }, 'Lao People"S Democratic Republic': "LA",
{ name: "Laos", code: "LA" }, Laos: "LA",
{ name: "Latvia", code: "LV" }, Latvia: "LV",
{ name: "Lebanon", code: "LB" }, Lebanon: "LB",
{ name: "Lesotho", code: "LS" }, Lesotho: "LS",
{ name: "Liberia", code: "LR" }, Liberia: "LR",
{ name: "Libyan Arab Jamahiriya", code: "LY" }, "Libyan Arab Jamahiriya": "LY",
{ name: "Liechtenstein", code: "LI" }, Liechtenstein: "LI",
{ name: "Lithuania", code: "LT" }, Lithuania: "LT",
{ name: "Luxembourg", code: "LU" }, Luxembourg: "LU",
{ name: "Macao", code: "MO" }, Macao: "MO",
{ name: "Macedonia, The Former Yugoslav Republic of", code: "MK" }, "Macedonia, The Former Yugoslav Republic of": "MK",
{ name: "Madagascar", code: "MG" }, Madagascar: "MG",
{ name: "Malawi", code: "MW" }, Malawi: "MW",
{ name: "Malaysia", code: "MY" }, Malaysia: "MY",
{ name: "Maldives", code: "MV" }, Maldives: "MV",
{ name: "Mali", code: "ML" }, Mali: "ML",
{ name: "Malta", code: "MT" }, Malta: "MT",
{ name: "Marshall Islands", code: "MH" }, "Marshall Islands": "MH",
{ name: "Martinique", code: "MQ" }, Martinique: "MQ",
{ name: "Mauritania", code: "MR" }, Mauritania: "MR",
{ name: "Mauritius", code: "MU" }, Mauritius: "MU",
{ name: "Mayotte", code: "YT" }, Mayotte: "YT",
{ name: "Mexico", code: "MX" }, Mexico: "MX",
{ name: "Micronesia, Federated States of", code: "FM" }, "Micronesia, Federated States of": "FM",
{ name: "Moldova, Republic of", code: "MD" }, "Moldova, Republic of": "MD",
{ name: "Monaco", code: "MC" }, Monaco: "MC",
{ name: "Mongolia", code: "MN" }, Mongolia: "MN",
{ name: "Montenegro", code: "ME" }, Montenegro: "ME",
{ name: "Montserrat", code: "MS" }, Montserrat: "MS",
{ name: "Morocco", code: "MA" }, Morocco: "MA",
{ name: "Mozambique", code: "MZ" }, Mozambique: "MZ",
{ name: "Myanmar", code: "MM" }, Myanmar: "MM",
{ name: "Namibia", code: "NA" }, Namibia: "NA",
{ name: "Nauru", code: "NR" }, Nauru: "NR",
{ name: "Nepal", code: "NP" }, Nepal: "NP",
{ name: "Netherlands", code: "NL" }, Netherlands: "NL",
{ name: "Netherlands Antilles", code: "AN" }, "Netherlands Antilles": "AN",
{ name: "New Caledonia", code: "NC" }, "New Caledonia": "NC",
{ name: "New Zealand", code: "NZ" }, "New Zealand": "NZ",
{ name: "Nicaragua", code: "NI" }, Nicaragua: "NI",
{ name: "Niger", code: "NE" }, Niger: "NE",
{ name: "Nigeria", code: "NG" }, Nigeria: "NG",
{ name: "Niue", code: "NU" }, Niue: "NU",
{ name: "Norfolk Island", code: "NF" }, "Norfolk Island": "NF",
{ name: "Northern Mariana Islands", code: "MP" }, "Northern Mariana Islands": "MP",
{ name: "Norway", code: "NO" }, Norway: "NO",
{ name: "Oman", code: "OM" }, Oman: "OM",
{ name: "Pakistan", code: "PK" }, Pakistan: "PK",
{ name: "Palau", code: "PW" }, Palau: "PW",
{ name: "Palestinian Territory, Occupied", code: "PS" }, "Palestinian Territory, Occupied": "PS",
{ name: "Panama", code: "PA" }, Panama: "PA",
{ name: "Papua New Guinea", code: "PG" }, "Papua New Guinea": "PG",
{ name: "Paraguay", code: "PY" }, Paraguay: "PY",
{ name: "Peru", code: "PE" }, Peru: "PE",
{ name: "Philippines", code: "PH" }, Philippines: "PH",
{ name: "Pitcairn", code: "PN" }, Pitcairn: "PN",
{ name: "Poland", code: "PL" }, Poland: "PL",
{ name: "Portugal", code: "PT" }, Portugal: "PT",
{ name: "Puerto Rico", code: "PR" }, "Puerto Rico": "PR",
{ name: "Qatar", code: "QA" }, Qatar: "QA",
{ name: "RWANDA", code: "RW" }, RWANDA: "RW",
{ name: "Reunion", code: "RE" }, Reunion: "RE",
{ name: "Romania", code: "RO" }, Romania: "RO",
{ name: "Russian Federation", code: "RU" }, "Russian Federation": "RU",
{ name: "Saint Barthelemy", code: "BL" }, "Saint Barthelemy": "BL",
{ name: "Saint Helena", code: "SH" }, "Saint Helena": "SH",
{ name: "Saint Kitts and Nevis", code: "KN" }, "Saint Kitts and Nevis": "KN",
{ name: "Saint Lucia", code: "LC" }, "Saint Lucia": "LC",
{ name: "Saint Martin", code: "MF" }, "Saint Martin": "MF",
{ name: "Saint Pierre and Miquelon", code: "PM" }, "Saint Pierre and Miquelon": "PM",
{ name: "Saint Vincent and the Grenadines", code: "VC" }, "Saint Vincent and the Grenadines": "VC",
{ name: "Samoa", code: "WS" }, Samoa: "WS",
{ name: "San Marino", code: "SM" }, "San Marino": "SM",
{ name: "Sao Tome and Principe", code: "ST" }, "Sao Tome and Principe": "ST",
{ name: "Saudi Arabia", code: "SA" }, "Saudi Arabia": "SA",
{ name: "Senegal", code: "SN" }, Senegal: "SN",
{ name: "Serbia", code: "RS" }, Serbia: "RS",
{ name: "Seychelles", code: "SC" }, Seychelles: "SC",
{ name: "Sierra Leone", code: "SL" }, "Sierra Leone": "SL",
{ name: "Singapore", code: "SG" }, Singapore: "SG",
{ name: "Sint Maarten", code: "SX" }, "Sint Maarten": "SX",
{ name: "Slovakia", code: "SK" }, Slovakia: "SK",
{ name: "Slovenia", code: "SI" }, Slovenia: "SI",
{ name: "Solomon Islands", code: "SB" }, "Solomon Islands": "SB",
{ name: "Somalia", code: "SO" }, Somalia: "SO",
{ name: "South Africa", code: "ZA" }, "South Africa": "ZA",
{ name: "South Georgia and the South Sandwich Islands", code: "GS" }, "South Georgia and the South Sandwich Islands": "GS",
{ name: "South Sudan", code: "SS" }, "South Sudan": "SS",
{ name: "Spain", code: "ES" }, Spain: "ES",
{ name: "Sri Lanka", code: "LK" }, "Sri Lanka": "LK",
{ name: "Sudan", code: "SD" }, Sudan: "SD",
{ name: "Suriname", code: "SR" }, Suriname: "SR",
{ name: "Svalbard and Jan Mayen", code: "SJ" }, "Svalbard and Jan Mayen": "SJ",
{ name: "Swaziland", code: "SZ" }, Swaziland: "SZ",
{ name: "Sweden", code: "SE" }, Sweden: "SE",
{ name: "Switzerland", code: "CH" }, Switzerland: "CH",
{ name: "Syrian Arab Republic", code: "SY" }, "Syrian Arab Republic": "SY",
{ name: "Taiwan", code: "TW" }, Taiwan: "TW",
{ name: "Tajikistan", code: "TJ" }, Tajikistan: "TJ",
{ name: "Tanzania, United Republic of", code: "TZ" }, "Tanzania, United Republic of": "TZ",
{ name: "Thailand", code: "TH" }, Thailand: "TH",
{ name: "Timor-Leste", code: "TL" }, "Timor-Leste": "TL",
{ name: "Togo", code: "TG" }, Togo: "TG",
{ name: "Tokelau", code: "TK" }, Tokelau: "TK",
{ name: "Tonga", code: "TO" }, Tonga: "TO",
{ name: "Trinidad and Tobago", code: "TT" }, "Trinidad and Tobago": "TT",
{ name: "Tunisia", code: "TN" }, Tunisia: "TN",
{ name: "Turkey", code: "TR" }, Turkey: "TR",
{ name: "Turkmenistan", code: "TM" }, Turkmenistan: "TM",
{ name: "Turks and Caicos Islands", code: "TC" }, "Turks and Caicos Islands": "TC",
{ name: "Tuvalu", code: "TV" }, Tuvalu: "TV",
{ name: "Uganda", code: "UG" }, Uganda: "UG",
{ name: "Ukraine", code: "UA" }, Ukraine: "UA",
{ name: "United Arab Emirates", code: "AE" }, "United Arab Emirates": "AE",
{ name: "United Kingdom", code: "GB" }, "United Kingdom": "GB",
{ name: "United States", code: "US" }, "United States": "US",
{ name: "United States Minor Outlying Islands", code: "UM" }, "United States Minor Outlying Islands": "UM",
{ name: "Uruguay", code: "UY" }, Uruguay: "UY",
{ name: "Uzbekistan", code: "UZ" }, Uzbekistan: "UZ",
{ name: "Vanuatu", code: "VU" }, Vanuatu: "VU",
{ name: "Venezuela", code: "VE" }, Venezuela: "VE",
{ name: "Viet Nam", code: "VN" }, "Viet Nam": "VN",
{ name: "Vietnam", code: "VN" }, Vietnam: "VN",
{ name: "Virgin Islands, British", code: "VG" }, "Virgin Islands, British": "VG",
{ name: "Virgin Islands, U.S.", code: "VI" }, "Virgin Islands, U.S.": "VI",
{ name: "Wallis and Futuna", code: "WF" }, "Wallis and Futuna": "WF",
{ name: "Western Sahara", code: "EH" }, "Western Sahara": "EH",
{ name: "Yemen", code: "YE" }, Yemen: "YE",
{ name: "Zambia", code: "ZM" }, Zambia: "ZM",
{ name: "Zimbabwe", code: "ZW" }, Zimbabwe: "ZW",
{ name: "Åland Islands", code: "AX" }, "Åland Islands": "AX",
] } as const
export const countries = Object.keys(countriesMap).map((country) => ({
code: countriesMap[country as keyof typeof countriesMap],
name: country as keyof typeof countriesMap,
}))

View File

@@ -35,21 +35,8 @@ export default function CountrySelect({
name, name,
rules: registerOptions, 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) { function handleChange(country: Key) {
setSelectedKey(String(country))
setValue(name, country) setValue(name, country)
} }
@@ -69,7 +56,7 @@ export default function CountrySelect({
onBlur={field.onBlur} onBlur={field.onBlur}
onSelectionChange={handleChange} onSelectionChange={handleChange}
ref={field.ref} ref={field.ref}
selectedKey={selectedKey} selectedKey={field.value}
> >
<div className={styles.comboBoxContainer}> <div className={styles.comboBoxContainer}>
<Input <Input
@@ -101,7 +88,7 @@ export default function CountrySelect({
<ListBoxItem <ListBoxItem
aria-label={country.name} aria-label={country.name}
className={styles.listBoxItem} className={styles.listBoxItem}
id={country.name} id={country.code}
key={`${country.code}-${idx}`} key={`${country.code}-${idx}`}
> >
{country.name} {country.name}

View File

@@ -12,7 +12,8 @@ import styles from "./phone.module.css"
import type { PhoneProps } from "./phone" import type { PhoneProps } from "./phone"
export default function Phone({ export default function Phone({
name = "phone", countrySelectName = "country",
name = "phoneNumber",
placeholder = "", placeholder = "",
registerOptions = { registerOptions = {
required: true, required: true,
@@ -20,11 +21,11 @@ export default function Phone({
}: PhoneProps) { }: PhoneProps) {
const phoneRef = useRef<PhoneInputRefType>(null) const phoneRef = useRef<PhoneInputRefType>(null)
const { control, formState } = useFormContext() const { control, formState } = useFormContext()
const countryValue = useWatch({ name: "country" }) const countryValue = useWatch({ name: countrySelectName })
const defaultCountry = getCountry({ const defaultCountry = getCountry({
countries: defaultCountries, countries: defaultCountries,
field: "name", field: "iso2",
value: countryValue, value: String(countryValue).toLowerCase(),
}) })
/** /**
* Holds the previous selected country to be able to update * Holds the previous selected country to be able to update
@@ -44,13 +45,13 @@ export default function Phone({
(country: string) => { (country: string) => {
const selectedCountry = getCountry({ const selectedCountry = getCountry({
countries: defaultCountries, countries: defaultCountries,
field: "name", field: "iso2",
value: country, value: country.toLowerCase(),
}) })
if (selectedCountry) { if (selectedCountry) {
phoneRef.current?.setCountry(selectedCountry.iso2) phoneRef.current?.setCountry(selectedCountry.iso2)
prevSelectedCountry.current = country prevSelectedCountry.current = country.toLowerCase()
} }
}, },
[phoneRef.current, prevSelectedCountry.current] [phoneRef.current, prevSelectedCountry.current]
@@ -63,8 +64,8 @@ export default function Phone({
if (prevSelectedCountry.current !== countryValue) { if (prevSelectedCountry.current !== countryValue) {
const selectedCountryPrev = getCountry({ const selectedCountryPrev = getCountry({
countries: defaultCountries, countries: defaultCountries,
field: "name", field: "iso2",
value: prevSelectedCountry.current, value: prevSelectedCountry.current.toLowerCase(),
}) })
if ( if (
field.value.replace("+", "") === selectedCountryPrev?.dialCode field.value.replace("+", "") === selectedCountryPrev?.dialCode

View File

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

2
env/server.ts vendored
View File

@@ -10,6 +10,7 @@ export const env = createEnv({
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(),
API_BASEURL: z.string(),
BUILD_ID: z.string().default("64rYXBu8o2eHp0Jf"), BUILD_ID: z.string().default("64rYXBu8o2eHp0Jf"),
CMS_ACCESS_TOKEN: z.string(), CMS_ACCESS_TOKEN: z.string(),
CMS_API_KEY: z.string(), CMS_API_KEY: z.string(),
@@ -39,6 +40,7 @@ export const env = createEnv({
emptyStringAsUndefined: true, emptyStringAsUndefined: true,
runtimeEnv: { runtimeEnv: {
ADOBE_SCRIPT_SRC: process.env.ADOBE_SCRIPT_SRC, ADOBE_SCRIPT_SRC: process.env.ADOBE_SCRIPT_SRC,
API_BASEURL: process.env.API_BASEURL,
BUILD_ID: process.env.BUILD_ID, BUILD_ID: process.env.BUILD_ID,
CMS_ACCESS_TOKEN: process.env.CMS_ACCESS_TOKEN, CMS_ACCESS_TOKEN: process.env.CMS_ACCESS_TOKEN,
CMS_API_KEY: process.env.CMS_API_KEY, CMS_API_KEY: process.env.CMS_API_KEY,

10
lib/api/endpoints.ts Normal file
View File

@@ -0,0 +1,10 @@
/**
* Nested enum requires namespace
*/
export namespace endpoints {
export const enum v0 {
profile = "profile/v0/Profile",
}
}
export type Endpoint = endpoints.v0

60
lib/api/index.ts Normal file
View File

@@ -0,0 +1,60 @@
import merge from "deepmerge"
import { env } from "@/env/server"
import type {
RequestOptionsWithJSONBody,
RequestOptionsWithOutBody,
} from "@/types/fetch"
import type { Endpoint } from "./endpoints"
export { endpoints } from "./endpoints"
const defaultOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
},
mode: "cors",
}
export async function get(
endpoint: Endpoint,
options: RequestOptionsWithOutBody
) {
defaultOptions.method = "GET"
return fetch(`${env.API_BASEURL}/${endpoint}`, merge(defaultOptions, options))
}
export async function patch(
endpoint: Endpoint,
options: RequestOptionsWithJSONBody
) {
const { body, ...requestOptions } = options
defaultOptions.body = JSON.stringify(body)
defaultOptions.method = "PATCH"
return fetch(
`${env.API_BASEURL}/${endpoint}`,
merge(defaultOptions, requestOptions)
)
}
export async function post(
endpoint: Endpoint,
options: RequestOptionsWithJSONBody
) {
const { body, ...requestOptions } = options
defaultOptions.body = JSON.stringify(body)
defaultOptions.method = "POST"
return fetch(
`${env.API_BASEURL}/${endpoint}`,
merge(defaultOptions, requestOptions)
)
}
export async function remove(
endpoint: Endpoint,
options: RequestOptionsWithOutBody
) {
defaultOptions.method = "DELETE"
return fetch(`${env.API_BASEURL}/${endpoint}`, merge(defaultOptions, options))
}

View File

@@ -4,11 +4,11 @@ import { useState } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { httpBatchLink } from "@trpc/client" import { httpBatchLink } from "@trpc/client"
import { env } from "@/env/client" import { env } from "@/env/client"
import { api } from "./client" import { trpc } from "./client"
import { transformer } from "@/server/transformer" import { transformer } from "@/server/transformer"
function initializeTrpcClient() { function initializeTrpcClient() {
return api.createClient({ return trpc.createClient({
links: [ links: [
httpBatchLink({ httpBatchLink({
transformer, transformer,
@@ -25,8 +25,8 @@ export default function TrpcProvider({ children }: React.PropsWithChildren) {
const [queryClient] = useState(() => new QueryClient({})) const [queryClient] = useState(() => new QueryClient({}))
const [trpcClient] = useState(() => initializeTrpcClient()) const [trpcClient] = useState(() => initializeTrpcClient())
return ( return (
<api.Provider client={trpcClient} queryClient={queryClient}> <trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</api.Provider> </trpc.Provider>
) )
} }

View File

@@ -2,4 +2,4 @@ import { createTRPCReact } from "@trpc/react-query"
import type { AppRouter } from "@/server" import type { AppRouter } from "@/server"
export const api = createTRPCReact<AppRouter>({}) export const trpc = createTRPCReact<AppRouter>({})

9
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"@trpc/server": "^11.0.0-next-beta.318", "@trpc/server": "^11.0.0-next-beta.318",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"deepmerge": "^4.3.1",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"graphql-request": "^6.1.0", "graphql-request": "^6.1.0",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
@@ -4091,6 +4092,14 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true "dev": true
}, },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/defer-to-connect": { "node_modules/defer-to-connect": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",

View File

@@ -31,6 +31,7 @@
"@trpc/server": "^11.0.0-next-beta.318", "@trpc/server": "^11.0.0-next-beta.318",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"deepmerge": "^4.3.1",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"graphql-request": "^6.1.0", "graphql-request": "^6.1.0",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",

View File

@@ -11,12 +11,10 @@ export function unauthorizedError() {
}) })
} }
export function internalServerError() { export function forbiddenError() {
return new TRPCError({ return new TRPCError({
code: TRPC_ERROR_CODES_BY_NUMBER[ code: TRPC_ERROR_CODES_BY_NUMBER[TRPC_ERROR_CODES_BY_KEY.FORBIDDEN],
TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR message: `You do not have permission!`,
],
message: `Internal Server Error!`,
}) })
} }
@@ -26,3 +24,12 @@ export function badRequestError(msg = "Bad request!") {
message: msg, message: msg,
}) })
} }
export function internalServerError() {
return new TRPCError({
code: TRPC_ERROR_CODES_BY_NUMBER[
TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR
],
message: `Internal Server Error!`,
})
}

View File

@@ -1,27 +1,24 @@
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
import { z } from "zod" import { z } from "zod"
/**
* Return value from jsonplaceholder.com/users/1
* Add proper user object expectation when fetching
* from Scandic API
*/
export const getUserSchema = z.object({ export const getUserSchema = z.object({
address: z.object({ address: z.object({
city: z.string(), city: z.string().optional(),
geo: z.object({}), country: z.nativeEnum(countriesMap),
street: z.string(), streetAddress: z.string().optional(),
suite: z.string(), zipCode: z.string(),
zipcode: z.string(),
}),
company: z.object({
bs: z.string(),
catchPhrase: z.string(),
name: z.string(),
}), }),
email: z.string().email(), email: z.string().email(),
id: z.number(), gender: z.string(),
name: z.string(), name: z.string(),
phone: z.string(), language: z.string(),
username: z.string(), lastName: z.string(),
website: z.string(), membership: z.object({
currentPoints: z.number(),
expirationDate: z.string(),
membershipNumber: z.string(),
memberSince: z.string(),
}),
phoneNumber: z.string(),
profileId: z.string(),
}) })

View File

@@ -1,112 +1,77 @@
import { badRequestError, internalServerError } from "@/server/errors/trpc" import * as api from "@/lib/api"
import { benefits, extendedUser, nextLevelPerks } from "./temp"
import {
badRequestError,
forbiddenError,
internalServerError,
unauthorizedError,
} from "@/server/errors/trpc"
import { protectedProcedure, router } from "@/server/trpc" import { protectedProcedure, router } from "@/server/trpc"
import { getUserSchema } from "./output" import { getUserSchema } from "./output"
import { extendedUser } from "./temp" function fakingRequest<T>(payload: T): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(payload)
}, 1500)
})
}
export const userQueryRouter = router({ export const userQueryRouter = router({
get: protectedProcedure.query(async function (opts) { get: protectedProcedure.query(async function ({ ctx }) {
// TODO: Make request to get user data from Scandic API try {
const response = await fetch( const apiResponse = await api.get(api.endpoints.v0.profile, {
"https://jsonplaceholder.typicode.com/users/1",
{
cache: "no-store", cache: "no-store",
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
})
if (!apiResponse.ok) {
switch (apiResponse.status) {
case 400:
throw badRequestError()
case 401:
throw unauthorizedError()
case 403:
throw forbiddenError()
default:
throw internalServerError()
}
} }
)
if (!response.ok) { const apiJson = await apiResponse.json()
if (!apiJson.data?.length) {
throw internalServerError()
}
const verifiedData = getUserSchema.safeParse(apiJson.data[0].attributes)
if (!verifiedData.success) {
console.info(`Get User - Verified Data Error`)
console.error(verifiedData.error)
throw badRequestError()
}
return {
...extendedUser,
...verifiedData.data,
firstName: verifiedData.data.name,
name: `${verifiedData.data.name} ${verifiedData.data.lastName}`,
}
} catch (error) {
console.info(`GEt User Error`)
console.error(error)
throw internalServerError() throw internalServerError()
} }
const json = await response.json()
const validJson = getUserSchema.safeParse(json)
if (!validJson.success) {
throw badRequestError()
}
const [firstname, lastname] = validJson.data.name.split(" ")
const [phone] = validJson.data.phone.split(" ")
return {
...validJson.data,
firstname,
lastname,
phone,
...extendedUser,
}
}), }),
benefits: router({ benefits: router({
current: protectedProcedure.query(async function (opts) { current: protectedProcedure.query(async function (opts) {
// TODO: Make request to get user data from Scandic API // TODO: Make request to get user data from Scandic API
return await fakingRequest<typeof benefits>(benefits)
const currentBenefits = [
{
id: 1,
value: "€5 voucher",
explanation: "to spend in bar & restaurant for each night",
subtitle:
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
href: "#",
},
{
id: 2,
value: "Breakfast to go",
explanation: "for early birds, when staying",
subtitle:
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
href: "#",
},
{
id: 3,
value: "15% discount",
explanation: "in the restaurant & the bar",
subtitle:
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
href: "#",
},
]
const response = currentBenefits
return response
// if (!response.ok) {
// throw internalServerError()
// }
// const json = await response.json()
// const validJson = getUserSchema.parse(json)
// if (!validJson) {
// throw badRequestError()
// }
// return validJson
}), }),
nextLevel: protectedProcedure.query(async function (opts) { nextLevel: protectedProcedure.query(async function (opts) {
// TODO: Make request to get user data from Scandic API // TODO: Make request to get user data from Scandic API
return await fakingRequest<typeof nextLevelPerks>(nextLevelPerks)
const nextLevelPerks = [
{
id: 1,
explanation: "Free soft drink voucher for the kids when staying",
},
{
id: 2,
explanation: "Free early check in",
},
{
id: 3,
explanation: "25% extra bonus points on each stay",
},
]
const response = { nextLevel: "Close Friend", perks: nextLevelPerks }
return response
// if (!response.ok) {
// throw internalServerError()
// }
// const json = await response.json()
// const validJson = getUserSchema.parse(json)
// if (!validJson) {
// throw badRequestError()
// }
// return validJson
}), }),
}), }),
}) })

View File

@@ -1,5 +1,32 @@
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
export const benefits = [
{
id: 1,
value: "€5 voucher",
explanation: "to spend in bar & restaurant for each night",
subtitle:
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
href: "#",
},
{
id: 2,
value: "Breakfast to go",
explanation: "for early birds, when staying",
subtitle:
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
href: "#",
},
{
id: 3,
value: "15% discount",
explanation: "in the restaurant & the bar",
subtitle:
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
href: "#",
},
]
export const challenges = { export const challenges = {
journeys: [ journeys: [
{ {
@@ -27,6 +54,26 @@ export const challenges = {
], ],
} }
export const nextLevelPerks = {
nextLevel: "Close Friend",
perks: [
{
id: 1,
explanation: "Free soft drink voucher for the kids when staying",
},
{
id: 2,
explanation: "Free early check in",
},
{
id: 3,
explanation: "25% extra bonus points on each stay",
},
],
}
export const shortcuts = [ export const shortcuts = [
{ {
href: "#", href: "#",
@@ -88,13 +135,9 @@ export const stays = [
] ]
export const extendedUser = { export const extendedUser = {
country: "United States",
dob: dt("1977-07-05").format("YYYY-MM-DD"), dob: dt("1977-07-05").format("YYYY-MM-DD"),
journeys: challenges.journeys, journeys: challenges.journeys,
membershipId: 30812404844732,
nights: 14, nights: 14,
points: 20720,
qualifyingPoints: 5000,
shortcuts, shortcuts,
stays, stays,
victories: challenges.victories, victories: challenges.victories,

View File

@@ -14,17 +14,16 @@ export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure.use(async function (opts) { export const protectedProcedure = t.procedure.use(async function (opts) {
const authRequired = opts.meta?.authRequired ?? true const authRequired = opts.meta?.authRequired ?? true
const session = await opts.ctx.auth() const session = await opts.ctx.auth()
if (authRequired) {
if (!session?.user) { if (!authRequired && env.NODE_ENV === "development") {
throw unauthorizedError() console.info(
} `❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌`
} else { )
if (env.NODE_ENV === "development") { console.info(`path: ${opts.path} | type: ${opts.type}`)
console.info( }
`❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌`
) if (!session?.user) {
console.info(`path: ${opts.path} | type: ${opts.type}`) throw unauthorizedError()
}
} }
return opts.next({ return opts.next({

11
types/auth.d.ts vendored
View File

@@ -1,4 +1,4 @@
import "next-auth" import type { JWT } from "next-auth/jwt"
// Module augmentation // Module augmentation
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment // https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
@@ -20,10 +20,7 @@ declare module "next-auth" {
/** /**
* Returned by `useSession`, `auth`, contains information about the active session. * Returned by `useSession`, `auth`, contains information about the active session.
*/ */
interface Session {} interface Session {
} token: JWT
}
declare module "next-auth/jwt" {
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */
interface JWT {}
} }

View File

@@ -0,0 +1,6 @@
import type { CardProps } from "./card/card"
import type { User } from "@/types/user"
export interface ContainerProps extends CardProps {
user: User
}

View File

@@ -1,5 +0,0 @@
import type { User } from "@/types/user"
export interface ProfileProps extends React.HTMLAttributes<HTMLElement> {
user: User
}

7
types/fetch.ts Normal file
View File

@@ -0,0 +1,7 @@
export interface RequestOptionsWithJSONBody
extends Omit<RequestInit, "body" | "method"> {
body: Record<string, unknown>
}
export interface RequestOptionsWithOutBody
extends Omit<RequestInit, "body" | "method"> {}

8
types/jwt.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
// Module augmentation
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
declare module "next-auth/jwt" {
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */
interface JWT {
access_token: string
}
}

8
types/next/error.ts Normal file
View File

@@ -0,0 +1,8 @@
interface NextError extends Error {
digest?: string
}
export interface ErrorPage {
error: NextError
reset: () => void
}

View File

@@ -24,16 +24,15 @@ type Victory = {
title: string title: string
} }
/**
* All extended field needs to be added by API team to response or
* we have to get the values from elsewhere
*/
export interface User extends z.infer<typeof getUserSchema> { export interface User extends z.infer<typeof getUserSchema> {
country: string
dob: string dob: string
firstname: string firstName: string
journeys: Journey[] journeys: Journey[]
lastname: string
membershipId: number
nights: number nights: number
points: number
qualifyingPoints: number
shortcuts: ShortcutLink[] shortcuts: ShortcutLink[]
stays: Stay[] stays: Stay[]
victories: Victory[] victories: Victory[]