diff --git a/app/app.css b/app/app.css index ceb77af..342b3a5 100644 --- a/app/app.css +++ b/app/app.css @@ -1,40 +1,32 @@ -@import 'tailwindcss'; - /* Dark mode is toggled by adding/removing `.dark` on (see ThemeModeProvider), rather than following the OS preference, so it can stay in sync with the MUI theme. */ -@custom-variant dark (&:where(.dark, .dark *)); - -@theme { - --font-sans: - 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', - 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; - /* Water/brand blue, anchored on the PHLASK logo color (#10B6FF = brand-500) */ +:root { + /* Water/brand blue, anchored on the PHLASK logo color (#10B6FF = brand-500). + Mirrored in app/theme/theme.ts (kept in sync manually). */ --color-brand-50: #eafaff; --color-brand-100: #d6f4ff; - --color-brand-200: #ade8ff; --color-brand-300: #6fd6ff; - --color-brand-400: #38c1ff; - --color-brand-500: #10b6ff; - --color-brand-600: #0090de; - --color-brand-700: #0072b3; - --color-brand-800: #045c90; - --color-brand-900: #0a3a5c; /* Deep ocean navy, used for dark-mode surfaces */ --color-navy-950: #071522; --color-navy-900: #0a1929; - --color-navy-800: #10263a; - --color-navy-700: #17344c; --color-navy-600: #1f4560; } html, body { - @apply bg-brand-50 text-navy-900 dark:bg-navy-950 dark:text-brand-50; + background-color: var(--color-brand-50); + color: var(--color-navy-900); color-scheme: light; } +html.dark, +html.dark body { + background-color: var(--color-navy-950); + color: var(--color-brand-50); +} + html.dark { color-scheme: dark; } diff --git a/app/components/WaveDivider.tsx b/app/components/WaveDivider.tsx index 06057c5..9618bfc 100644 --- a/app/components/WaveDivider.tsx +++ b/app/components/WaveDivider.tsx @@ -1,3 +1,5 @@ +import { Box, type SxProps, type Theme } from "@mui/material"; + // Two periods of the same wave back-to-back so a -50% translateX loops seamlessly. const WAVE_PATH = "M0,40 C150,90 350,0 600,40 C850,80 1050,0 1200,40 C1350,80 1550,0 1800,40 C1950,80 2150,0 2400,40 L2400,120 L0,120 Z"; @@ -8,7 +10,7 @@ type WaveDividerProps = { opacity?: number; duration?: number; reverse?: boolean; - className?: string; + sx?: SxProps; }; export function WaveDivider({ @@ -17,27 +19,41 @@ export function WaveDivider({ opacity = 1, duration = 18, reverse = false, - className, + sx, }: WaveDividerProps) { return ( -
- -
+ + ); } diff --git a/app/routes.ts b/app/routes.ts index de7ef64..1a14b97 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -12,5 +12,8 @@ export default [ ]), route("auth", "routes/unauthenticated/_layout.tsx", [ index("routes/unauthenticated/login.tsx"), + route("forgot-password", "routes/unauthenticated/forgot-password.tsx"), + route("confirm", "routes/unauthenticated/confirm.tsx"), + route("reset-password", "routes/unauthenticated/reset-password.tsx"), ]), ] satisfies RouteConfig; diff --git a/app/routes/unauthenticated/_layout.tsx b/app/routes/unauthenticated/_layout.tsx index a855b88..0f74ef2 100644 --- a/app/routes/unauthenticated/_layout.tsx +++ b/app/routes/unauthenticated/_layout.tsx @@ -1,23 +1,133 @@ +import { Box } from "@mui/material"; import { Outlet } from "react-router"; import phlasklogo from "~/assets/PHLASK_v2.svg"; +import { ThemeToggle } from "~/components/ThemeToggle"; +import { WaveDivider } from "~/components/WaveDivider"; +import { brand, navy } from "~/theme/theme"; + +const BUBBLES = [ + { left: "8%", size: 14, delay: 0, duration: 9 }, + { left: "18%", size: 8, delay: 2.5, duration: 7 }, + { left: "32%", size: 20, delay: 1, duration: 11 }, + { left: "62%", size: 10, delay: 3.5, duration: 8 }, + { left: "78%", size: 16, delay: 0.5, duration: 10 }, + { left: "90%", size: 9, delay: 2, duration: 7.5 }, +]; export default function UnauthenticatedLayout() { return ( -
- + ({ + position: "relative", + display: "flex", + minHeight: "100vh", + alignItems: "center", + justifyContent: "center", + overflow: "hidden", + px: 2, + py: 6, + backgroundImage: `linear-gradient(to bottom, ${brand[100]}, ${brand[300]}, ${brand[600]})`, + ...theme.applyStyles("dark", { + backgroundImage: `linear-gradient(to bottom, ${navy[950]}, ${navy[900]}, ${navy[800]})`, + }), + })} + > + + + + + {BUBBLES.map((bubble) => ( + ({ + pointerEvents: "none", + position: "absolute", + bottom: 0, + borderRadius: "9999px", + bgcolor: "rgba(255,255,255,0.4)", + ...theme.applyStyles("dark", { + bgcolor: `${brand[200]}33`, + }), + })} + style={{ + left: bubble.left, + width: bubble.size, + height: bubble.size, + animation: `bubble-rise ${bubble.duration}s ease-in infinite`, + animationDelay: `${bubble.delay}s`, + }} + /> + ))} + + ({ + bottom: 0, + ...theme.applyStyles("dark", { opacity: 0.2 }), + })} + /> + ({ + bottom: 0, + ...theme.applyStyles("dark", { opacity: 0.25 }), + })} + /> + ({ + bottom: 0, + ...theme.applyStyles("dark", { opacity: 0.3 }), + })} + /> -
-
+ + + ({ + display: "inline-flex", + borderRadius: 4, + bgcolor: "rgba(255,255,255,0.8)", + p: 1.5, + boxShadow: 2, + backdropFilter: "blur(4px)", + ...theme.applyStyles("dark", { bgcolor: `${navy[950]}99` }), + })} + > + PHLASK Logo + + + ({ + borderRadius: 4, + border: "1px solid rgba(255,255,255,0.5)", + bgcolor: "rgba(255,255,255,0.75)", + p: 4, + boxShadow: 8, + backdropFilter: "blur(8px)", + ...theme.applyStyles("dark", { + borderColor: `${navy[700]}99`, + bgcolor: `${navy[900]}bf`, + }), + })} + > -
-
-
+ + + ); } diff --git a/app/routes/unauthenticated/confirm.tsx b/app/routes/unauthenticated/confirm.tsx new file mode 100644 index 0000000..4893f05 --- /dev/null +++ b/app/routes/unauthenticated/confirm.tsx @@ -0,0 +1,20 @@ +import { type LoaderFunction, redirect } from "react-router"; +import { getDatabaseClient } from "~/api/client.server"; + +export const loader: LoaderFunction = async ({ request }) => { + const url = new URL(request.url); + const code = url.searchParams.get("code"); + + if (!code) { + return redirect("/auth/forgot-password"); + } + + const { client, headers } = getDatabaseClient(request); + const { error } = await client.auth.exchangeCodeForSession(code); + + if (error) { + return redirect("/auth/forgot-password"); + } + + return redirect("/auth/reset-password", { headers }); +}; diff --git a/app/routes/unauthenticated/forgot-password.tsx b/app/routes/unauthenticated/forgot-password.tsx new file mode 100644 index 0000000..2a68578 --- /dev/null +++ b/app/routes/unauthenticated/forgot-password.tsx @@ -0,0 +1,132 @@ +import { + Button, + Link as MuiLink, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { applySchema } from "composable-functions"; +import { + type ActionFunction, + data, + Link, + type LoaderFunction, + redirect, + useActionData, +} from "react-router"; +import { performMutation, SchemaForm } from "remix-forms"; +import { getDatabaseClient } from "~/api/client.server"; +import { forgotPasswordSchema } from "~/schemas/forgot-password"; + +export const loader: LoaderFunction = async ({ request }) => { + const { client } = getDatabaseClient(request); + + const response = await client.auth.getUser(); + if (response.error) { + return null; + } + + return redirect("/"); +}; + +const mutation = applySchema(forgotPasswordSchema)(async (values) => values); + +export const action: ActionFunction = async ({ request }) => { + const result = await performMutation({ + request, + schema: forgotPasswordSchema, + mutation, + }); + + if (!result.success) { + return data(result, 400); + } + + const { client } = getDatabaseClient(request); + const origin = new URL(request.url).origin; + + await client.auth.resetPasswordForEmail(result.data.email, { + redirectTo: `${origin}/auth/confirm`, + }); + + return data({ sent: true }); +}; + +const ForgotPassword = () => { + const action = useActionData<{ sent?: boolean }>(); + + if (action?.sent) { + return ( + + + + Check your email + + + If an account exists for that email, we've sent a link to reset your + password. + + + + Back to sign in + + + ); + } + + return ( + + + + Reset your password + + + Enter the email associated with your account and we'll send you a link + to reset your password. + + + + + {({ Field, formState, register }) => ( + + + {({ name, errors, required }) => ( + + )} + + + + + + Back to sign in + + + )} + + + ); +}; + +export default ForgotPassword; diff --git a/app/routes/unauthenticated/login.tsx b/app/routes/unauthenticated/login.tsx index 9356b99..72cca10 100644 --- a/app/routes/unauthenticated/login.tsx +++ b/app/routes/unauthenticated/login.tsx @@ -1,8 +1,20 @@ -import { Button, Stack, TextField, Typography } from "@mui/material"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; +import { + Alert, + Button, + IconButton, + InputAdornment, + Link as MuiLink, + Stack, + TextField, + Typography, +} from "@mui/material"; import { applySchema } from "composable-functions"; +import { useState } from "react"; import { type ActionFunction, data, + Link, type LoaderFunction, redirect, useActionData, @@ -40,7 +52,10 @@ export const action: ActionFunction = async ({ context, request }) => { const response = await client.auth.signInWithPassword(result.data); if (response.error) { - return data(response.error, { status: response.error.status }); + return data( + { message: response.error.message }, + { status: response.error.status ?? 400 }, + ); } context.set(userContext, response.data.user); @@ -49,57 +64,96 @@ export const action: ActionFunction = async ({ context, request }) => { }; const Login = () => { - const action = useActionData<{ message: string; status: number }>(); + const action = useActionData<{ message?: string }>(); + const [showPassword, setShowPassword] = useState(false); return ( - - Login + + + + Sign in + + + Enter your credentials to access the PHLask admin dashboard. + + + {({ Field, formState, register }) => ( - + - {({ name, errors, required }) => { - return ( - - ); - }} - - {({ name, errors, required }) => ( )} - - {action?.message} - + + + + {({ name, errors, required }) => ( + + setShowPassword((value) => !value)} + edge="end" + tabIndex={-1} + > + {showPassword ? ( + + ) : ( + + )} + + + ), + }, + }} + /> + )} + + + + Forgot password? + + + + {action?.message && ( + {action.message} + )} diff --git a/app/routes/unauthenticated/reset-password.tsx b/app/routes/unauthenticated/reset-password.tsx new file mode 100644 index 0000000..12ff94d --- /dev/null +++ b/app/routes/unauthenticated/reset-password.tsx @@ -0,0 +1,154 @@ +import { Visibility, VisibilityOff } from "@mui/icons-material"; +import { + Alert, + Button, + IconButton, + InputAdornment, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { applySchema } from "composable-functions"; +import { useState } from "react"; +import { + type ActionFunction, + data, + type LoaderFunction, + redirect, + useActionData, +} from "react-router"; +import { performMutation, SchemaForm } from "remix-forms"; +import { getDatabaseClient } from "~/api/client.server"; +import { resetPasswordSchema } from "~/schemas/reset-password"; + +export const loader: LoaderFunction = async ({ request }) => { + const { client } = getDatabaseClient(request); + + const response = await client.auth.getUser(); + if (response.error) { + return redirect("/auth/forgot-password"); + } + + return null; +}; + +const mutation = applySchema(resetPasswordSchema)(async (values) => values); + +export const action: ActionFunction = async ({ request }) => { + const result = await performMutation({ + request, + schema: resetPasswordSchema, + mutation, + }); + + if (!result.success) { + return data(result, 400); + } + + const { client, headers } = getDatabaseClient(request); + const response = await client.auth.updateUser({ + password: result.data.password, + }); + + if (response.error) { + return data( + { message: response.error.message }, + { status: response.error.status ?? 400 }, + ); + } + + return redirect("/", { headers }); +}; + +const ResetPassword = () => { + const action = useActionData<{ message?: string }>(); + const [showPassword, setShowPassword] = useState(false); + + return ( + + + + Set a new password + + + Choose a new password for your account. + + + + + {({ Field, formState, register }) => ( + + + {({ name, errors, required }) => ( + + setShowPassword((value) => !value)} + edge="end" + tabIndex={-1} + > + {showPassword ? ( + + ) : ( + + )} + + + ), + }, + }} + /> + )} + + + + {({ name, errors, required }) => ( + + )} + + + {action?.message && ( + {action.message} + )} + + + + )} + + + ); +}; + +export default ResetPassword; diff --git a/app/schemas/forgot-password.ts b/app/schemas/forgot-password.ts new file mode 100644 index 0000000..531a179 --- /dev/null +++ b/app/schemas/forgot-password.ts @@ -0,0 +1,5 @@ +import { z } from "zod"; + +export const forgotPasswordSchema = z.object({ + email: z.email().min(1), +}); diff --git a/app/schemas/reset-password.ts b/app/schemas/reset-password.ts new file mode 100644 index 0000000..4e0d3b5 --- /dev/null +++ b/app/schemas/reset-password.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const resetPasswordSchema = z + .object({ + password: z + .string() + .min(8, { error: "Password must be at least 8 characters" }), + confirmPassword: z + .string() + .min(1, { error: "Please confirm your password" }), + }) + .refine((values) => values.password === values.confirmPassword, { + error: "Passwords do not match", + path: ["confirmPassword"], + }); diff --git a/package.json b/package.json index ca30df3..798bbe5 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,10 @@ "devDependencies": { "@biomejs/biome": "2.3.14", "@react-router/dev": "^7.13.0", - "@tailwindcss/vite": "^4.1.18", "@types/node": "^22.19.11", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "lefthook": "^2.1.1", - "tailwindcss": "^4.1.18", "typescript": "^5.9.3", "vite": "^7.3.1", "vite-tsconfig-paths": "^5.1.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f33c4e3..9890669 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,9 +66,6 @@ importers: '@react-router/dev': specifier: ^7.13.0 version: 7.13.0(@react-router/serve@7.13.0(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3))(@types/node@22.19.11)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.30.2)) - '@tailwindcss/vite': - specifier: ^4.1.18 - version: 4.1.18(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.30.2)) '@types/node': specifier: ^22.19.11 version: 22.19.11 @@ -81,9 +78,6 @@ importers: lefthook: specifier: ^2.1.1 version: 2.1.1 - tailwindcss: - specifier: ^4.1.18 - version: 4.1.18 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -831,100 +825,6 @@ packages: resolution: {integrity: sha512-KaIHsO6LU1cuqpYMsprQhfedRJFuTAf4dr8/k4/4Po6urRVXIWzmdY/lHPnf8Mzw2Fh0ueQVQYdaXzOvCBGnGQ==} engines: {node: '>=20.0.0'} - '@tailwindcss/node@4.1.18': - resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} - - '@tailwindcss/oxide-android-arm64@4.1.18': - resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.1.18': - resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.1.18': - resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.1.18': - resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': - resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': - resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': - resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': - resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.1.18': - resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.1.18': - resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': - resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': - resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.1.18': - resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} - engines: {node: '>= 10'} - - '@tailwindcss/vite@4.1.18': - resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 - '@tanstack/react-table@8.21.3': resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} engines: {node: '>=12'} @@ -1135,10 +1035,6 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} - engines: {node: '>=10.13.0'} - error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -1243,9 +1139,6 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1452,9 +1345,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1748,13 +1638,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tailwindcss@4.1.18: - resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} - - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -2585,74 +2468,6 @@ snapshots: - bufferutil - utf-8-validate - '@tailwindcss/node@4.1.18': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.19.0 - jiti: 2.6.1 - lightningcss: 1.30.2 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.1.18 - - '@tailwindcss/oxide-android-arm64@4.1.18': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.1.18': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.1.18': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.1.18': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.1.18': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': - optional: true - - '@tailwindcss/oxide@4.1.18': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-x64': 4.1.18 - '@tailwindcss/oxide-freebsd-x64': 4.1.18 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-x64-musl': 4.1.18 - '@tailwindcss/oxide-wasm32-wasi': 4.1.18 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.30.2))': - dependencies: - '@tailwindcss/node': 4.1.18 - '@tailwindcss/oxide': 4.1.18 - tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.30.2) - '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/table-core': 8.21.3 @@ -2832,7 +2647,8 @@ snapshots: destroy@1.2.0: {} - detect-libc@2.1.2: {} + detect-libc@2.1.2: + optional: true dom-helpers@5.2.1: dependencies: @@ -2851,11 +2667,6 @@ snapshots: encodeurl@2.0.0: {} - enhanced-resolve@5.19.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -3000,8 +2811,6 @@ snapshots: gopd@1.2.0: {} - graceful-fs@4.2.11: {} - has-symbols@1.1.0: {} hasown@2.0.2: @@ -3043,7 +2852,8 @@ snapshots: isbot@5.1.35: {} - jiti@2.6.1: {} + jiti@2.6.1: + optional: true js-tokens@4.0.0: {} @@ -3144,6 +2954,7 @@ snapshots: lightningcss-linux-x64-musl: 1.30.2 lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + optional: true lines-and-columns@1.2.4: {} @@ -3157,10 +2968,6 @@ snapshots: dependencies: yallist: 3.1.1 - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - math-intrinsics@1.1.0: {} media-typer@0.3.0: {} @@ -3453,10 +3260,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tailwindcss@4.1.18: {} - - tapable@2.3.0: {} - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) diff --git a/vite.config.ts b/vite.config.ts index 3f3f041..c4140c9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,9 @@ import { reactRouter } from "@react-router/dev/vite"; -import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ - plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], + plugins: [reactRouter(), tsconfigPaths()], server: { port: 5174, },