diff --git a/app/(app)/alpha/additional-details/_actions.ts b/app/(app)/alpha/additional-details/_actions.ts index cf2a81f5..78d300d3 100644 --- a/app/(app)/alpha/additional-details/_actions.ts +++ b/app/(app)/alpha/additional-details/_actions.ts @@ -16,7 +16,7 @@ import { db } from "@/server/db"; import { user } from "@/server/db/schema"; import { eq } from "drizzle-orm"; -export async function handleFormSlideOneSubmit(dataInput: TypeSlideOneSchema) { +export async function slideOneSubmitAction(dataInput: TypeSlideOneSchema) { const session = await getServerAuthSession(); if (!session || !session.user) { redirect("/get-started"); @@ -47,7 +47,7 @@ export async function handleFormSlideOneSubmit(dataInput: TypeSlideOneSchema) { } } -export async function handleFormSlideTwoSubmit(dataInput: TypeSlideTwoSchema) { +export async function slideTwoSubmitAction(dataInput: TypeSlideTwoSchema) { const session = await getServerAuthSession(); if (!session || !session.user) { redirect("/get-started"); @@ -75,9 +75,7 @@ export async function handleFormSlideTwoSubmit(dataInput: TypeSlideTwoSchema) { } } -export async function handleFormSlideThreeSubmit( - dataInput: TypeSlideThreeSchema, -) { +export async function slideThreeSubmitAction(dataInput: TypeSlideThreeSchema) { const session = await getServerAuthSession(); if (!session || !session.user) { redirect("/get-started"); diff --git a/app/(app)/alpha/additional-details/_client.tsx b/app/(app)/alpha/additional-details/_client.tsx index 635742f3..c8c1d4e6 100644 --- a/app/(app)/alpha/additional-details/_client.tsx +++ b/app/(app)/alpha/additional-details/_client.tsx @@ -23,10 +23,22 @@ import { monthsOptions, } from "@/app/(app)/alpha/additional-details/selectOptions"; import { - handleFormSlideOneSubmit, - handleFormSlideThreeSubmit, - handleFormSlideTwoSubmit, + slideOneSubmitAction, + slideThreeSubmitAction, + slideTwoSubmitAction, } from "./_actions"; +import { Heading, Subheading } from "./components/heading"; +import { Divider } from "./components/divider"; +import { + ErrorMessage, + Field, + Fieldset, + Label, + Legend, +} from "./components/fieldset"; +import { Input } from "./components/input"; +import { Select } from "./components/select"; +import { Button } from "./components/button"; type UserDetails = { username: string; @@ -103,7 +115,7 @@ function SlideOne({ details }: { details: UserDetails }) { const onFormSubmit = async (data: TypeSlideOneSchema) => { try { - const isSuccess = await handleFormSlideOneSubmit(data); + const isSuccess = await slideOneSubmitAction(data); if (isSuccess) { toast.success("Saved"); router.push(`?slide=${2}`, { scroll: false }); @@ -116,107 +128,101 @@ function SlideOne({ details }: { details: UserDetails }) { }; return ( -
-
-
-

- Profile information -

-

This information will be displayed on your profile

+ +
+
+ Profile information + + This information will be displayed on your profile + +
+ +
+ + + + {errors?.firstName && ( + + {errors.firstName.message} + + )} +
-
-
-
- -
- - {errors.firstName &&

{`${errors.firstName.message}`}

} -
-
-
- -
- - {errors.surname &&

{`${errors.surname.message}`}

} -
-
+
+ + + + {errors?.surname && ( + + {errors.surname.message} + + )} + +
-
- -
-
- codu.co/ -
- -
- {errors.username &&

{`${errors.username.message}`}

} -
-
- - - {errors.location &&

{`${errors.location.message}`}

} +
+ + +
+ + codu.co/ + +
-
-
- -
+ {errors?.username && ( + + {errors.username.message} + + )} +
+ +
+ + + + {errors?.location && ( + + {errors.location.message} + + )} + +
+
+ +
+
); @@ -287,7 +293,7 @@ function SlideTwo({ details }: { details: UserDetails }) { const onFormSubmit = async (data: TypeSlideTwoSchema) => { try { - const isSuccess = await handleFormSlideTwoSubmit(data); + const isSuccess = await slideTwoSubmitAction(data); if (isSuccess) { toast.success("Saved"); @@ -301,132 +307,115 @@ function SlideTwo({ details }: { details: UserDetails }) { }; return ( -
-
-
-

- Demographic -

-

This information is private, but helps us improve

+ +
+
+ Demographic + + This information is private, but helps us improve +
+ -
-
-
-
+ {errors.dateOfBirth &&

{`${errors.dateOfBirth.message}`}

} + +
-
- - -
-
+
+ +
); @@ -472,7 +461,7 @@ function SlideThree({ details }: { details: UserDetails }) { if (isError) { try { - const isSuccess = await handleFormSlideThreeSubmit(data); + const isSuccess = await slideThreeSubmitAction(data); if (isSuccess) { toast.success("Saved"); router.push(`/`, { scroll: false }); @@ -486,33 +475,23 @@ function SlideThree({ details }: { details: UserDetails }) { }; return ( -
-
-

- Work and education -

-

- This information is private but helpful to tailor our events and - features. -

-
+ +
+
+ Work and education + + This information is private but helpful to tailor our events and + features. + +
+ +
+ + -
-
-
- - - + {errors.professionalOrStudent && ( -

{`${errors.professionalOrStudent.message}`}

+ + {errors.professionalOrStudent.message} + )} -
+ {getValues("professionalOrStudent") === "Working professional" && ( <> -
- -
- -
- {errors.workplace &&

{`${errors.workplace.message}`}

} -
- -
- -
- -
- {errors.jobTitle &&

{`${errors.jobTitle.message}`}

} -
+ + + + {errors.workplace && ( + + {errors.workplace.message} + + )} + + + + + + {errors.jobTitle && ( + + {errors.jobTitle.message} + + )} + )} {getValues("professionalOrStudent") === "Current student" && ( <> -
- - @@ -591,50 +552,50 @@ function SlideThree({ details }: { details: UserDetails }) { {levelOfStudyOptions.map((level) => ( ))} - + {errors.levelOfStudy && ( -

{`${errors.levelOfStudy.message}`}

+ + {errors.levelOfStudy.message} + + )} + + + + + + {errors.course && ( + + {errors.course.message} + )} -
- -
- -
- -
- {errors.course &&

{`${errors.course.message}`}

} -
+ )} -
+
+
-
- - -
+
+ + +
); diff --git a/app/(app)/alpha/additional-details/components/button.tsx b/app/(app)/alpha/additional-details/components/button.tsx new file mode 100644 index 00000000..b50095d5 --- /dev/null +++ b/app/(app)/alpha/additional-details/components/button.tsx @@ -0,0 +1,216 @@ +import * as Headless from "@headlessui/react"; +import clsx from "clsx"; +import React, { forwardRef } from "react"; +import { Link } from "./link"; + +const styles = { + base: [ + // Base + "relative isolate inline-flex items-center justify-center gap-x-2 rounded-lg border text-base/6 font-semibold", + // Sizing + "px-[calc(theme(spacing[3.5])-1px)] py-[calc(theme(spacing[2.5])-1px)] sm:px-[calc(theme(spacing.3)-1px)] sm:py-[calc(theme(spacing[1.5])-1px)] sm:text-sm/6", + // Focus + "focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500", + // Disabled + "data-[disabled]:opacity-50", + // Icon + "[&>[data-slot=icon]]:-mx-0.5 [&>[data-slot=icon]]:my-0.5 [&>[data-slot=icon]]:size-5 [&>[data-slot=icon]]:shrink-0 [&>[data-slot=icon]]:text-[--btn-icon] [&>[data-slot=icon]]:sm:my-1 [&>[data-slot=icon]]:sm:size-4 forced-colors:[--btn-icon:ButtonText] forced-colors:data-[hover]:[--btn-icon:ButtonText]", + ], + solid: [ + // Optical border, implemented as the button background to avoid corner artifacts + "border-transparent bg-[--btn-border]", + // Dark mode: border is rendered on `after` so background is set to button background + "dark:bg-[--btn-bg]", + // Button background, implemented as foreground layer to stack on top of pseudo-border layer + "before:absolute before:inset-0 before:-z-10 before:rounded-[calc(theme(borderRadius.lg)-1px)] before:bg-[--btn-bg]", + // Drop shadow, applied to the inset `before` layer so it blends with the border + "before:shadow", + // Background color is moved to control and shadow is removed in dark mode so hide `before` pseudo + "dark:before:hidden", + // Dark mode: Subtle white outline is applied using a border + "dark:border-white/5", + // Shim/overlay, inset to match button foreground and used for hover state + highlight shadow + "after:absolute after:inset-0 after:-z-10 after:rounded-[calc(theme(borderRadius.lg)-1px)]", + // Inner highlight shadow + "after:shadow-[shadow:inset_0_1px_theme(colors.white/15%)]", + // White overlay on hover + "after:data-[active]:bg-[--btn-hover-overlay] after:data-[hover]:bg-[--btn-hover-overlay]", + // Dark mode: `after` layer expands to cover entire button + "dark:after:-inset-px dark:after:rounded-lg", + // Disabled + "before:data-[disabled]:shadow-none after:data-[disabled]:shadow-none", + ], + outline: [ + // Base + "border-zinc-950/10 text-zinc-950 data-[active]:bg-zinc-950/[2.5%] data-[hover]:bg-zinc-950/[2.5%]", + // Dark mode + "dark:border-white/15 dark:text-white dark:[--btn-bg:transparent] dark:data-[active]:bg-white/5 dark:data-[hover]:bg-white/5", + // Icon + "[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]", + ], + plain: [ + // Base + "border-transparent text-zinc-950 data-[active]:bg-zinc-950/5 data-[hover]:bg-zinc-950/5", + // Dark mode + "dark:text-white dark:data-[active]:bg-white/10 dark:data-[hover]:bg-white/10", + // Icon + "[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]", + ], + colors: { + "dark/zinc": [ + "text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]", + "dark:text-white dark:[--btn-bg:theme(colors.zinc.600)] dark:[--btn-hover-overlay:theme(colors.white/5%)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]", + ], + light: [ + "text-zinc-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]", + "dark:text-white dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]", + "[--btn-icon:theme(colors.zinc.500)] data-[active]:[--btn-icon:theme(colors.zinc.700)] data-[hover]:[--btn-icon:theme(colors.zinc.700)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]", + ], + "dark/white": [ + "text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]", + "dark:text-zinc-950 dark:[--btn-bg:white] dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)] dark:[--btn-icon:theme(colors.zinc.500)] dark:data-[active]:[--btn-icon:theme(colors.zinc.400)] dark:data-[hover]:[--btn-icon:theme(colors.zinc.400)]", + ], + dark: [ + "text-white [--btn-bg:theme(colors.zinc.900)] [--btn-border:theme(colors.zinc.950/90%)] [--btn-hover-overlay:theme(colors.white/10%)]", + "dark:[--btn-hover-overlay:theme(colors.white/5%)] dark:[--btn-bg:theme(colors.zinc.800)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]", + ], + white: [ + "text-zinc-950 [--btn-bg:white] [--btn-border:theme(colors.zinc.950/10%)] [--btn-hover-overlay:theme(colors.zinc.950/2.5%)] data-[active]:[--btn-border:theme(colors.zinc.950/15%)] data-[hover]:[--btn-border:theme(colors.zinc.950/15%)]", + "dark:[--btn-hover-overlay:theme(colors.zinc.950/5%)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.500)] data-[hover]:[--btn-icon:theme(colors.zinc.500)]", + ], + zinc: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.zinc.600)] [--btn-border:theme(colors.zinc.700/90%)]", + "dark:[--btn-hover-overlay:theme(colors.white/5%)]", + "[--btn-icon:theme(colors.zinc.400)] data-[active]:[--btn-icon:theme(colors.zinc.300)] data-[hover]:[--btn-icon:theme(colors.zinc.300)]", + ], + indigo: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.indigo.500)] [--btn-border:theme(colors.indigo.600/90%)]", + "[--btn-icon:theme(colors.indigo.300)] data-[active]:[--btn-icon:theme(colors.indigo.200)] data-[hover]:[--btn-icon:theme(colors.indigo.200)]", + ], + cyan: [ + "text-cyan-950 [--btn-bg:theme(colors.cyan.300)] [--btn-border:theme(colors.cyan.400/80%)] [--btn-hover-overlay:theme(colors.white/25%)]", + "[--btn-icon:theme(colors.cyan.500)]", + ], + red: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.red.600)] [--btn-border:theme(colors.red.700/90%)]", + "[--btn-icon:theme(colors.red.300)] data-[active]:[--btn-icon:theme(colors.red.200)] data-[hover]:[--btn-icon:theme(colors.red.200)]", + ], + orange: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.orange.500)] [--btn-border:theme(colors.orange.600/90%)]", + "[--btn-icon:theme(colors.orange.300)] data-[active]:[--btn-icon:theme(colors.orange.200)] data-[hover]:[--btn-icon:theme(colors.orange.200)]", + ], + amber: [ + "text-amber-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.amber.400)] [--btn-border:theme(colors.amber.500/80%)]", + "[--btn-icon:theme(colors.amber.600)]", + ], + yellow: [ + "text-yellow-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.yellow.300)] [--btn-border:theme(colors.yellow.400/80%)]", + "[--btn-icon:theme(colors.yellow.600)] data-[active]:[--btn-icon:theme(colors.yellow.700)] data-[hover]:[--btn-icon:theme(colors.yellow.700)]", + ], + lime: [ + "text-lime-950 [--btn-hover-overlay:theme(colors.white/25%)] [--btn-bg:theme(colors.lime.300)] [--btn-border:theme(colors.lime.400/80%)]", + "[--btn-icon:theme(colors.lime.600)] data-[active]:[--btn-icon:theme(colors.lime.700)] data-[hover]:[--btn-icon:theme(colors.lime.700)]", + ], + green: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.green.600)] [--btn-border:theme(colors.green.700/90%)]", + "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]", + ], + emerald: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.emerald.600)] [--btn-border:theme(colors.emerald.700/90%)]", + "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]", + ], + teal: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.teal.600)] [--btn-border:theme(colors.teal.700/90%)]", + "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]", + ], + sky: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.sky.500)] [--btn-border:theme(colors.sky.600/80%)]", + "[--btn-icon:theme(colors.white/60%)] data-[active]:[--btn-icon:theme(colors.white/80%)] data-[hover]:[--btn-icon:theme(colors.white/80%)]", + ], + blue: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.blue.600)] [--btn-border:theme(colors.blue.700/90%)]", + "[--btn-icon:theme(colors.blue.400)] data-[active]:[--btn-icon:theme(colors.blue.300)] data-[hover]:[--btn-icon:theme(colors.blue.300)]", + ], + violet: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.violet.500)] [--btn-border:theme(colors.violet.600/90%)]", + "[--btn-icon:theme(colors.violet.300)] data-[active]:[--btn-icon:theme(colors.violet.200)] data-[hover]:[--btn-icon:theme(colors.violet.200)]", + ], + purple: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.purple.500)] [--btn-border:theme(colors.purple.600/90%)]", + "[--btn-icon:theme(colors.purple.300)] data-[active]:[--btn-icon:theme(colors.purple.200)] data-[hover]:[--btn-icon:theme(colors.purple.200)]", + ], + fuchsia: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.fuchsia.500)] [--btn-border:theme(colors.fuchsia.600/90%)]", + "[--btn-icon:theme(colors.fuchsia.300)] data-[active]:[--btn-icon:theme(colors.fuchsia.200)] data-[hover]:[--btn-icon:theme(colors.fuchsia.200)]", + ], + pink: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.pink.500)] [--btn-border:theme(colors.pink.600/90%)]", + "[--btn-icon:theme(colors.pink.300)] data-[active]:[--btn-icon:theme(colors.pink.200)] data-[hover]:[--btn-icon:theme(colors.pink.200)]", + ], + rose: [ + "text-white [--btn-hover-overlay:theme(colors.white/10%)] [--btn-bg:theme(colors.rose.500)] [--btn-border:theme(colors.rose.600/90%)]", + "[--btn-icon:theme(colors.rose.300)] data-[active]:[--btn-icon:theme(colors.rose.200)] data-[hover]:[--btn-icon:theme(colors.rose.200)]", + ], + }, +}; + +type ButtonProps = ( + | { color?: keyof typeof styles.colors; outline?: never; plain?: never } + | { color?: never; outline: true; plain?: never } + | { color?: never; outline?: never; plain: true } +) & { className?: string; children: React.ReactNode } & ( + | Omit + | Omit, "className"> + ); + +export const Button = forwardRef(function Button( + { color, outline, plain, className, children, ...props }: ButtonProps, + ref: React.ForwardedRef, +) { + const classes = clsx( + className, + styles.base, + outline + ? styles.outline + : plain + ? styles.plain + : clsx(styles.solid, styles.colors[color ?? "dark/zinc"]), + ); + + return "href" in props ? ( + } + > + {children} + + ) : ( + + {children} + + ); +}); + +/** + * Expand the hit area to at least 44×44px on touch devices + */ +export function TouchTarget({ children }: { children: React.ReactNode }) { + return ( + <> +