From be735ba79266eb6409ebb9979b958e664ad80fea Mon Sep 17 00:00:00 2001 From: tayloraj10 Date: Tue, 14 Jul 2026 19:51:48 -0400 Subject: [PATCH 1/2] Add resource review detail page Adds the /reviews/:id page for approving, rejecting, and editing an individual resource revision submission. --- app/routes.ts | 1 + app/routes/authenticated/reviews/detail.tsx | 793 ++++++++++++++++++++ 2 files changed, 794 insertions(+) create mode 100644 app/routes/authenticated/reviews/detail.tsx diff --git a/app/routes.ts b/app/routes.ts index 4ba9f72..dd1e58f 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -9,6 +9,7 @@ export default [ layout("routes/authenticated/_layout.tsx", [ index("routes/authenticated/dashboard.tsx"), route("reviews", "routes/authenticated/reviews/index.tsx"), + route("reviews/:id", "routes/authenticated/reviews/detail.tsx"), route("logout", "routes/authenticated/logout.tsx"), ]), route("auth", "routes/unauthenticated/_layout.tsx", [ diff --git a/app/routes/authenticated/reviews/detail.tsx b/app/routes/authenticated/reviews/detail.tsx new file mode 100644 index 0000000..d6c7acf --- /dev/null +++ b/app/routes/authenticated/reviews/detail.tsx @@ -0,0 +1,793 @@ +import { ArrowBack } from "@mui/icons-material"; +import { + Alert, + Autocomplete, + Box, + Button, + Chip, + MenuItem, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + TextField, + Typography, +} from "@mui/material"; +import { useEffect, useState } from "react"; +import { + type ActionFunction, + data, + Form, + type LoaderFunction, + Link as RouterLink, + redirect, + useActionData, + useFetcher, + useLoaderData, +} from "react-router"; +import { getDatabaseClient } from "~/api/client.server"; +import { getResourceRevisionAPI } from "~/api/resource-revisions/methods"; +import { getResourceEntryAPI } from "~/api/resources/methods"; +import { authMiddleware } from "~/middleware/auth"; +import type { + BathroomTag, + DispenserType, + DistributionType, + EntryType, + FoodType, + ForageTag, + ForageType, + OrganizationType, + ResourceEntry, + ResourceType, + WaterTag, +} from "~/types/ResourceEntry"; +import type { ResourceRevision } from "~/types/ResourceRevision"; +import { statusChipColor } from "~/utils/chipColors"; + +export const middleware = [authMiddleware]; + +const RESOURCE_TYPES: ResourceType[] = ["WATER", "FOOD", "FORAGE", "BATHROOM"]; +const ENTRY_TYPES: EntryType[] = ["OPEN", "RESTRICTED", "UNSURE"]; +const DISPENSER_TYPES: DispenserType[] = [ + "DRINKING_FOUNTAIN", + "BOTTLE_FILLER", + "SINK", + "JUG", + "SODA_MACHINE", + "PITCHER", + "WATER_COOLER", +]; +const WATER_TAGS: WaterTag[] = [ + "WHEELCHAIR_ACCESSIBLE", + "FILTERED", + "BYOB", + "ID_REQUIRED", +]; +const FOOD_TYPES: FoodType[] = ["PERISHABLE", "NON_PERISHABLE", "PREPARED"]; +const DISTRIBUTION_TYPES: DistributionType[] = [ + "EAT_ON_SITE", + "DELIVERY", + "PICKUP", +]; +const ORGANIZATION_TYPES: OrganizationType[] = [ + "GOVERNMENT", + "BUSINESS", + "NON_PROFIT", + "UNSURE", +]; +const FORAGE_TYPES: ForageType[] = [ + "NUT", + "FRUIT", + "LEAVES", + "BARK", + "FLOWERS", +]; +const FORAGE_TAGS: ForageTag[] = ["MEDICINAL", "IN_SEASON", "COMMUNITY_GARDEN"]; +const BATHROOM_TAGS: BathroomTag[] = [ + "WHEELCHAIR_ACCESSIBLE", + "GENDER_NEUTRAL", + "CHANGING_TABLE", + "SINGLE_OCCUPANCY", + "FAMILY", +]; + +const formatValue = (value: unknown): string => { + if (value === null || value === undefined || value === "") { + return "—"; + } + if (typeof value === "object") { + return JSON.stringify(value); + } + return String(value); +}; + +type EditableValues = { + name: string; + resource_type: ResourceType; + entry_type: EntryType | ""; + address: string; + city: string; + state: string; + zip_code: string; + latitude: number; + longitude: number; + description: string; + guidelines: string; + water: { dispenser_type: DispenserType[]; tags: WaterTag[] }; + food: { + food_type: FoodType[]; + distribution_type: DistributionType[]; + organization_type: OrganizationType[]; + organization_name: string; + organization_url: string; + }; + forage: { forage_type: ForageType[]; tags: ForageTag[] }; + bathroom: { tags: BathroomTag[] }; +}; + +const toEditableValues = (revision: ResourceRevision): EditableValues => ({ + name: revision.name ?? "", + resource_type: revision.resource_type, + entry_type: revision.entry_type ?? "", + address: revision.address ?? "", + city: revision.city ?? "", + state: revision.state ?? "", + zip_code: revision.zip_code ?? "", + latitude: revision.latitude, + longitude: revision.longitude, + description: revision.description ?? "", + guidelines: revision.guidelines ?? "", + water: { + dispenser_type: revision.water?.dispenser_type ?? [], + tags: revision.water?.tags ?? [], + }, + food: { + food_type: revision.food?.food_type ?? [], + distribution_type: revision.food?.distribution_type ?? [], + organization_type: revision.food?.organization_type ?? [], + organization_name: revision.food?.organization_name ?? "", + organization_url: revision.food?.organization_url ?? "", + }, + forage: { + forage_type: revision.forage?.forage_type ?? [], + tags: revision.forage?.tags ?? [], + }, + bathroom: { + tags: revision.bathroom?.tags ?? [], + }, +}); + +export const loader: LoaderFunction = async ({ request, params }) => { + const id = Number(params.id); + if (!Number.isInteger(id)) { + throw data("Not found", { status: 404 }); + } + + const { client } = getDatabaseClient(request); + const revisionAPI = getResourceRevisionAPI(client); + const revision = await revisionAPI.getById(id); + + let resource: ResourceEntry | null = null; + try { + const resourceAPI = getResourceEntryAPI(client); + resource = await resourceAPI.getById(String(revision.mapped_resources)); + } catch { + resource = null; + } + + return { revision, resource }; +}; + +export const action: ActionFunction = async ({ request, params }) => { + const id = Number(params.id); + if (!Number.isInteger(id)) { + throw data("Not found", { status: 404 }); + } + + const { client, headers } = getDatabaseClient(request); + const revisionAPI = getResourceRevisionAPI(client); + const contentType = request.headers.get("content-type") || ""; + + if (contentType.includes("application/json")) { + const body = await request.json(); + + if (body.intent !== "save") { + return data({ message: "Unknown action" }, { status: 400 }); + } + + try { + const updated = await revisionAPI.updateFields(id, body.values); + return data({ message: "Changes saved.", ok: true, revision: updated }); + } catch { + return data({ message: "Failed to save changes." }, { status: 400 }); + } + } + + const formData = await request.formData(); + const intent = formData.get("intent"); + + if (intent !== "approve" && intent !== "reject") { + return data({ message: "Unknown action" }, { status: 400 }); + } + + await revisionAPI.updateStatus( + id, + intent === "approve" ? "APPROVED" : "REJECTED", + ); + + return redirect("/reviews", { headers }); +}; + +const ReviewDetail = () => { + const { revision, resource } = useLoaderData<{ + revision: ResourceRevision; + resource: ResourceEntry | null; + }>(); + const actionData = useActionData<{ message?: string }>(); + const fetcher = useFetcher<{ message?: string; ok?: boolean }>(); + + const [values, setValues] = useState(() => + toEditableValues(revision), + ); + + useEffect(() => { + setValues(toEditableValues(revision)); + }, [revision]); + + const isPending = revision.status === "PENDING"; + const isSaving = fetcher.state !== "idle"; + + const setField = ( + key: K, + value: EditableValues[K], + ) => setValues((v) => ({ ...v, [key]: value })); + + const setWater = ( + key: K, + value: EditableValues["water"][K], + ) => setValues((v) => ({ ...v, water: { ...v.water, [key]: value } })); + + const setFood = ( + key: K, + value: EditableValues["food"][K], + ) => setValues((v) => ({ ...v, food: { ...v.food, [key]: value } })); + + const setForage = ( + key: K, + value: EditableValues["forage"][K], + ) => setValues((v) => ({ ...v, forage: { ...v.forage, [key]: value } })); + + const setBathroom = ( + key: K, + value: EditableValues["bathroom"][K], + ) => setValues((v) => ({ ...v, bathroom: { ...v.bathroom, [key]: value } })); + + const handleSave = () => { + const payload: Partial = { + name: values.name || null, + resource_type: values.resource_type, + entry_type: values.entry_type || null, + address: values.address || null, + city: values.city || null, + state: values.state || null, + zip_code: values.zip_code || null, + latitude: values.latitude, + longitude: values.longitude, + description: values.description || null, + guidelines: values.guidelines || null, + water: values.resource_type === "WATER" ? values.water : null, + food: + values.resource_type === "FOOD" + ? { + ...values.food, + organization_name: values.food.organization_name || undefined, + organization_url: values.food.organization_url || undefined, + } + : null, + forage: values.resource_type === "FORAGE" ? values.forage : null, + bathroom: values.resource_type === "BATHROOM" ? values.bathroom : null, + }; + + // biome-ignore lint/suspicious/noExplicitAny: react-router's JsonValue submit-target type isn't exported to cast against + fetcher.submit({ intent: "save", values: payload } as any, { + method: "post", + encType: "application/json", + }); + }; + + return ( + + + + + Back to review queue + + + + + Review proposed edit + + + + + Submitted{" "} + {new Date(revision.date_created).toLocaleString(undefined, { + dateStyle: "medium", + timeStyle: "short", + })} + {revision.creator ? ` by ${revision.creator}` : ""} + + + + + {!resource && ( + + The resource this revision targets (#{revision.mapped_resources}) + could not be found — it may have been deleted. + + )} + + + + + + Field + Current + Proposed + + + + Name + {formatValue(resource?.name)} + + setField("name", e.target.value)} + /> + + + + + + Resource type + + {formatValue(resource?.resource_type)} + + + setField("resource_type", e.target.value as ResourceType) + } + > + {RESOURCE_TYPES.map((type) => ( + + {type} + + ))} + + + + + + Entry type + {formatValue(resource?.entry_type)} + + + setField("entry_type", e.target.value as EntryType) + } + > + {ENTRY_TYPES.map((type) => ( + + {type} + + ))} + + + + + + + Description + + {formatValue(resource?.description)} + + setField("description", e.target.value)} + /> + + + + + Guidelines + {formatValue(resource?.guidelines)} + + setField("guidelines", e.target.value)} + /> + + + + + Address + {formatValue(resource?.address)} + + setField("address", e.target.value)} + /> + + + + + City + {formatValue(resource?.city)} + + setField("city", e.target.value)} + /> + + + + + State + {formatValue(resource?.state)} + + setField("state", e.target.value)} + /> + + + + + Zip code + {formatValue(resource?.zip_code)} + + setField("zip_code", e.target.value)} + /> + + + + + Latitude + {formatValue(resource?.latitude)} + + setField("latitude", Number(e.target.value))} + /> + + + + + Longitude + {formatValue(resource?.longitude)} + + + setField("longitude", Number(e.target.value)) + } + /> + + + + + Hours + {formatValue(resource?.hours)} + {formatValue(revision.hours)} + + + + Images + {formatValue(resource?.images)} + {formatValue(revision.images)} + + +
+
+ + {values.resource_type === "WATER" && ( + + + Water details + + + + + Current + + + {formatValue(resource?.water)} + + + + setWater("dispenser_type", val)} + renderInput={(params) => ( + + )} + /> + setWater("tags", val)} + renderInput={(params) => } + /> + + + + )} + + {values.resource_type === "FOOD" && ( + + + Food details + + + + + Current + + + {formatValue(resource?.food)} + + + + setFood("food_type", val)} + renderInput={(params) => ( + + )} + /> + setFood("distribution_type", val)} + renderInput={(params) => ( + + )} + /> + setFood("organization_type", val)} + renderInput={(params) => ( + + )} + /> + setFood("organization_name", e.target.value)} + /> + setFood("organization_url", e.target.value)} + /> + + + + )} + + {values.resource_type === "FORAGE" && ( + + + Forage details + + + + + Current + + + {formatValue(resource?.forage)} + + + + setForage("forage_type", val)} + renderInput={(params) => ( + + )} + /> + setForage("tags", val)} + renderInput={(params) => } + /> + + + + )} + + {values.resource_type === "BATHROOM" && ( + + + Bathroom details + + + + + Current + + + {formatValue(resource?.bathroom)} + + + + setBathroom("tags", val)} + renderInput={(params) => } + /> + + + + )} + + {fetcher.data?.message && ( + + {fetcher.data.message} + + )} + + {actionData?.message && ( + {actionData.message} + )} + + {isPending && ( + + + +
+ + + + +
+
+ )} +
+ ); +}; + +export default ReviewDetail; From ce7154303744ea0da031cc122edc5b9d9ef386dd Mon Sep 17 00:00:00 2001 From: tayloraj10 Date: Tue, 14 Jul 2026 20:54:56 -0400 Subject: [PATCH 2/2] Convert remaining pages to MUI and remove Tailwind CSS infrastructure Converts the authenticated layout, WaveDivider, ThemeToggle, root.tsx's ErrorBoundary, and the unauthenticated layout from Tailwind classes to MUI, then removes the Tailwind vite plugin, dependency, and CSS directives now that nothing in this branch renders with them. --- app/app.css | 30 ++-- app/components/ThemeToggle.tsx | 19 ++- app/components/WaveDivider.tsx | 36 +++-- app/root.tsx | 12 +- app/routes/authenticated/_layout.tsx | 209 ++++++++++++++++++++----- app/routes/unauthenticated/_layout.tsx | 50 ++++-- package.json | 2 - pnpm-lock.yaml | 207 +----------------------- vite.config.ts | 3 +- 9 files changed, 274 insertions(+), 294 deletions(-) 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/ThemeToggle.tsx b/app/components/ThemeToggle.tsx index c5ee989..206b561 100644 --- a/app/components/ThemeToggle.tsx +++ b/app/components/ThemeToggle.tsx @@ -4,8 +4,8 @@ import { useThemeMode } from "~/theme/ThemeModeProvider"; import { brand, navy } from "~/theme/theme"; // Styling and icon visibility are driven purely by the `.dark` class (via -// theme.applyStyles / Tailwind's dark: variant) rather than the `mode` value -// from React state, so this can never mismatch between SSR and hydration. +// theme.applyStyles) rather than the `mode` value from React state, so this +// can never mismatch between SSR and hydration. export function ThemeToggle() { const { toggleMode } = useThemeMode(); @@ -26,8 +26,19 @@ export function ThemeToggle() { }), })} > - - + ({ + ...theme.applyStyles("dark", { display: "none" }), + })} + /> + ({ + display: "none", + ...theme.applyStyles("dark", { display: "inline-block" }), + })} + /> ); 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/root.tsx b/app/root.tsx index 346688c..e68b313 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,3 +1,4 @@ +import { Box } from "@mui/material"; import { isRouteErrorResponse, Links, @@ -82,14 +83,17 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { } return ( -
+

{message}

{details}

{stack && ( -
+        
           {stack}
-        
+
)} -
+ ); } diff --git a/app/routes/authenticated/_layout.tsx b/app/routes/authenticated/_layout.tsx index f2b77f5..959568c 100644 --- a/app/routes/authenticated/_layout.tsx +++ b/app/routes/authenticated/_layout.tsx @@ -1,76 +1,207 @@ import { Dashboard, Logout, RateReview } from "@mui/icons-material"; +import { Box } from "@mui/material"; +import { styled } from "@mui/material/styles"; import { Link, NavLink, 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"; export const action = () => {}; -const navLinkClass = ({ isActive }: { isActive: boolean }) => - `flex items-center gap-2.5 rounded-xl px-4 py-2.5 text-sm font-medium transition-colors ${ - isActive - ? "bg-brand-50 text-brand-700 dark:bg-navy-800 dark:text-brand-300" - : "text-navy-600 hover:bg-brand-50 dark:text-brand-100/80 dark:hover:bg-navy-800" - }`; +const SidebarNavLink = styled(NavLink)(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: theme.spacing(1.25), + borderRadius: 12, + padding: `${theme.spacing(1.25)} ${theme.spacing(2)}`, + fontSize: "0.875rem", + fontWeight: 500, + textDecoration: "none", + color: navy[600], + transition: "color 0.2s ease, background-color 0.2s ease", + "&:hover": { backgroundColor: brand[50] }, + "&.active": { + color: brand[700], + backgroundColor: brand[50], + }, + ...theme.applyStyles("dark", { + color: `${brand[100]}cc`, + "&:hover": { backgroundColor: navy[800] }, + "&.active": { + color: brand[300], + backgroundColor: navy[800], + }, + }), +})); export default function DashboardLayout() { return ( -
- + + + + -
-
+ + -
-
-
+ + + ); } diff --git a/app/routes/unauthenticated/_layout.tsx b/app/routes/unauthenticated/_layout.tsx index a855b88..2503c6b 100644 --- a/app/routes/unauthenticated/_layout.tsx +++ b/app/routes/unauthenticated/_layout.tsx @@ -1,23 +1,49 @@ +import { Box } from "@mui/material"; import { Outlet } from "react-router"; import phlasklogo from "~/assets/PHLASK_v2.svg"; export default function UnauthenticatedLayout() { return ( -
- + + + -
-
+ + -
-
-
+ + + ); } 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, },