diff --git a/apps/site/components.json b/apps/site/components.json new file mode 100644 index 00000000..22dfed03 --- /dev/null +++ b/apps/site/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "@/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils/utils" + } +} \ No newline at end of file diff --git a/apps/site/package.json b/apps/site/package.json index f4a15bd7..27c6dd82 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -10,11 +10,16 @@ }, "dependencies": { "@fireworks-js/react": "^2.10.7", + "@hookform/resolvers": "^3.3.2", "@portabletext/react": "^3.0.11", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-navigation-menu": "^1.1.4", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@react-three/drei": "^9.88.14", @@ -22,13 +27,18 @@ "@sanity/image-url": "^1.0.2", "@types/three": "^0.158.2", "axios": "^1.6.2", + "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "date-fns-tz": "^2.0.0", "framer-motion": "^10.16.14", "lucide-react": "^0.292.0", "next": "13.5.6", "next-sanity": "^5.5.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.49.2", + "tailwind-merge": "^2.1.0", + "tailwindcss-animate": "^1.0.7", "three": "^0.158.0", "tunnel-rat": "^0.1.2", "zod": "^3.22.4" diff --git a/apps/site/src/app/globals.css b/apps/site/src/app/globals.css index 86edb009..ef3ce340 100644 --- a/apps/site/src/app/globals.css +++ b/apps/site/src/app/globals.css @@ -5,6 +5,79 @@ @tailwind components; @tailwind utilities; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + :root { --color-cream: #fffce2; --color-brown: #432810; diff --git a/apps/site/src/app/login/page.tsx b/apps/site/src/app/login/page.tsx new file mode 100644 index 00000000..39cb56a8 --- /dev/null +++ b/apps/site/src/app/login/page.tsx @@ -0,0 +1,96 @@ +"use client"; +import { useRouter } from "next/navigation"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { + Form, + FormItem, + FormControl, + FormDescription, + FormLabel, + FormField, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils/utils"; + +import ButtonStyles from "@/lib/components/Button/Button.module.css"; + +export const revalidate = 60; + +const LOGIN_PATH = "/api/user/login"; + +const formSchema = z.object({ + email: z + .string() + .email({ message: "Sorry, that email address is invalid." }), +}); + +export default function Home() { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + }, + }); + + const onSubmit = (e: z.infer) => { + router.push(LOGIN_PATH); + }; + + return ( +
+
+

+ Login +

+ +
+ + ( + + + Email Address + + + + + + UCI students will log in with UCI SSO. + + + + )} + /> + + + + +
+
+ ); +} diff --git a/apps/site/src/app/schedule/components/BarLoader.tsx b/apps/site/src/app/schedule/components/BarLoader.tsx deleted file mode 100644 index 1dd747a8..00000000 --- a/apps/site/src/app/schedule/components/BarLoader.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { motion, Variants } from "framer-motion"; - -const variants = { - initial: { - scaleY: 0.5, - opacity: 0, - }, - animate: { - scaleY: 1, - opacity: 1, - transition: { - repeat: Infinity, - repeatType: "mirror", - duration: 1, - // ease: "circIn", - }, - }, -} as Variants; - -const BarLoader = () => { - return ( - - - - - - - - ); -}; - -export default BarLoader; diff --git a/apps/site/src/app/schedule/components/ShiftingCountdown/ShiftingCountdown.module.scss b/apps/site/src/app/schedule/components/ShiftingCountdown/ShiftingCountdown.module.scss deleted file mode 100644 index 55a6106e..00000000 --- a/apps/site/src/app/schedule/components/ShiftingCountdown/ShiftingCountdown.module.scss +++ /dev/null @@ -1,11 +0,0 @@ -.text { - text-shadow: 0px 0px 10px #fffce2; - filter: brightness(1.5); -} - -.subtext { - text-shadow: - 0px 0px 10px #ffa700, - 0px 0px 10px #ffda7b; - filter: brightness(3.05); -} diff --git a/apps/site/src/app/schedule/components/ShiftingCountdown/ShiftingCountdown.tsx b/apps/site/src/app/schedule/components/ShiftingCountdown/ShiftingCountdown.tsx deleted file mode 100644 index 73a95055..00000000 --- a/apps/site/src/app/schedule/components/ShiftingCountdown/ShiftingCountdown.tsx +++ /dev/null @@ -1,158 +0,0 @@ -"use client"; - -import { AnimatePresence, motion } from "framer-motion"; -import { useEffect, useRef, useState } from "react"; -import BarLoader from "../BarLoader"; -import clsx from "clsx"; -import { Fireworks } from "@fireworks-js/react"; - -import styles from "./ShiftingCountdown.module.scss"; - -// NOTE: Change this date to whatever date you want to countdown to :) -const HACKING_STARTS = "2024-01-26T18:00:00.000"; -const HACKING_ENDS = "2024-01-28T24:00:00.000"; - -// Testing: -// const HACKING_STARTS = "2023-12-09T21:42:00.000"; -// const HACKING_ENDS = "2023-12-09T21:44:00.000"; - -const SECOND = 1000; -const MINUTE = SECOND * 60; -const HOUR = MINUTE * 60; -const DAY = HOUR * 24; - -const ShiftingCountdown = () => { - const [countdown, setCountdown] = useState({ - days: 0, - hours: 0, - minutes: 0, - seconds: 0, - showTimer: false, - showFireworks: false, - label: "Hacking Starts", - }); - - useEffect(() => { - const intervalRef = setInterval(handleCountdown, 1000); - return () => clearInterval(intervalRef || undefined); - }, []); - - const handleCountdown = () => { - const now = new Date(); - - let end = new Date(HACKING_STARTS); - - if (now >= new Date(HACKING_STARTS)) { - setCountdown((prev) => { - return { ...prev, label: "Hacking Ends" }; - }); - end = new Date(HACKING_ENDS); - } - - const distance = Math.max(+end - +now, 0); - - const days = Math.floor(distance / DAY); - const hours = Math.floor((distance % DAY) / HOUR); - const minutes = Math.floor((distance % HOUR) / MINUTE); - const seconds = Math.floor((distance % MINUTE) / SECOND); - - if (days == 0 && hours == 0 && minutes == 0 && seconds == 0) { - // triggers when HACKING_STARTS timer ends and HACKING_ENDS timer ends - setCountdown((prev) => { - return { ...prev, showFireworks: true }; - }); - } - - setCountdown((prev) => { - return { - ...prev, - days: days, - hours: hours, - minutes: minutes, - seconds: seconds, - }; - }); - setCountdown((prev) => { - return { ...prev, showTimer: true }; - }); - }; - - return ( - <> -
- {!countdown.showTimer ? ( -
- -
- ) : ( - <> - - {countdown.label} - -
- - - - -
- - )} -
- {countdown.showFireworks && ( -
- -
- )} - - ); -}; - -const CountdownItem = ({ num, text }: { num: number; text: string }) => { - return ( - -
- - - {num} - - -
- - {text} - -
- ); -}; - -export default ShiftingCountdown; diff --git a/apps/site/src/app/schedule/page.tsx b/apps/site/src/app/schedule/page.tsx deleted file mode 100644 index e1bcdd51..00000000 --- a/apps/site/src/app/schedule/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import ShiftingCountdown from "./components/ShiftingCountdown/ShiftingCountdown"; - -export const revalidate = 60; - -export default function Schedule() { - return ( - <> -
-
- -
-
- Placeholder for Schedule -
-
- - ); -} diff --git a/apps/site/src/components/ui/button.tsx b/apps/site/src/components/ui/button.tsx new file mode 100644 index 00000000..5d45e8f0 --- /dev/null +++ b/apps/site/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/apps/site/src/components/ui/form.tsx b/apps/site/src/components/ui/form.tsx new file mode 100644 index 00000000..a195f3b5 --- /dev/null +++ b/apps/site/src/components/ui/form.tsx @@ -0,0 +1,177 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form"; + +import { cn } from "@/lib/utils/utils"; +import { Label } from "@/components/ui/label"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +