diff --git a/app/components/listboxes.tsx b/app/components/listboxes.tsx index bdb0049..1f2e3d6 100644 --- a/app/components/listboxes.tsx +++ b/app/components/listboxes.tsx @@ -8,27 +8,27 @@ const listboxButtonClassName = const listBoxOptionsClassname = 'z-50 absolute mt-1 max-h-60 w-full overflow-auto rounded-md py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm border-input border border-1' -interface HorseData { +interface AnimalData { id: string name: string } -interface HorseListboxProps { - horses: HorseData[] +interface AnimalListboxProps { + animals: AnimalData[] name: string - defaultValues?: HorseData[] + defaultValues?: AnimalData[] error: boolean } -export function HorseListbox({ - horses, +export function AnimalListbox({ + animals, name, defaultValues = [], error, -}: HorseListboxProps) { - const initialValues = horses.filter(horse => { +}: AnimalListboxProps) { + const initialValues = animals.filter(animal => { for (const value of defaultValues) { - if (value.id == horse.id) { + if (value.id == animal.id) { return true } } @@ -51,7 +51,7 @@ export function HorseListbox({ aria-invalid={error ? true : undefined} > - {selected.map(horse => horse.name).join(', ')} + {selected.map(animal => animal.name).join(', ')} @@ -65,22 +65,22 @@ export function HorseListbox({ leaveTo="opacity-0" > - {horses.map((horse, horseIdx) => ( + {animals.map((animal, animalIdx) => ( `relative cursor-default select-none py-2 pl-10 pr-4 ${active ? 'bg-teal-600 text-white' : 'bg-background text-primary'}` } - value={horse} + value={animal} > {({ selected, active }) => ( <> - {horse.name} + {animal.name} {selected ? ( (null) + const userIsAdmin = user?.roles.find(r => r.name === 'admin') + + const navLinkClass = ({ isActive }: { isActive: boolean }) => + `flex items-center gap-3 rounded-md px-3 py-2 text-body-sm font-medium transition-colors ${ + isActive + ? 'bg-sidebar-active text-sidebar-active-foreground' + : 'text-sidebar-foreground hover:bg-sidebar-border' + }` + + return ( + + ) +} diff --git a/app/components/ui/data_table.tsx b/app/components/ui/data_table.tsx index 7b97dc6..d304d4c 100644 --- a/app/components/ui/data_table.tsx +++ b/app/components/ui/data_table.tsx @@ -20,11 +20,15 @@ import { Button } from '~/components/ui/button.tsx' interface DataTableProps { columns: ColumnDef[] data: TData[] + emptyMessage?: string + emptyDescription?: string } export function DataTable({ columns, data, + emptyMessage = 'No results found', + emptyDescription, }: DataTableProps) { const table = useReactTable({ data, @@ -80,11 +84,18 @@ export function DataTable({ )) ) : ( - - No results. + +
+
โ€”
+

+ {emptyMessage} +

+ {emptyDescription && ( +

+ {emptyDescription} +

+ )} +
)} diff --git a/app/data.ts b/app/data.ts index bc8719d..67f238d 100644 --- a/app/data.ts +++ b/app/data.ts @@ -1,10 +1,10 @@ import { Prisma } from '@prisma/client' export const siteName = 'The Barn Volunteer Portal' -export const siteEmailAddress = 'hello@email.trottrack.org' +export const siteEmailAddress = 'hello@thebarnaz.com' export const siteEmailAddressWithName = - siteName + ' ' -export const siteBaseUrl = 'https://thebarn.trottrack.org' + siteName + ' ' +export const siteBaseUrl = 'https://thebarnaz.com' export const volunteerTypes = [ { @@ -12,28 +12,28 @@ export const volunteerTypes = [ field: 'cleaningCrew', reqField: 'cleaningCrewReq', description: - 'Cleaning crew volunteers help clean all pastures and stalls in the barn, check automatic waterers, sweep the feed room and tack room, and handle other miscellaneous cleaning jobs. No prior experience with horses is required.', + 'Cleaning crew volunteers help maintain the facility, check waterers, sweep common areas, and handle other miscellaneous cleaning tasks. No prior experience with animals is required.', }, { displayName: 'side walkers', field: 'sideWalkers', reqField: 'sideWalkersReq', description: - 'Side walkers walk alongside students helping to support them during lessons.No prior experience with horses needed. Must be able to walk on uneven surfaces.', + 'Side walkers walk alongside participants helping to support them during sessions. No prior experience with animals needed. Must be able to walk on uneven surfaces.', }, { displayName: 'lesson assistants', field: 'lessonAssistants', reqField: 'lessonAssistantsReq', description: - 'Lesson assistants should have 1+ years of experience with horses. They must be able to groom and tack horses, and to communicate effectively with both students and instructors.', + 'Lesson assistants should have 1+ years of experience with the animals. They assist instructors and communicate effectively with both participants and staff.', }, { - displayName: 'horse leaders', - field: 'horseLeaders', - reqField: 'horseLeadersReq', + displayName: 'animal handlers', + field: 'animalHandlers', + reqField: 'animalHandlersReq', description: - 'Leads horses during lessons. Should have 1+ years of experiences with horses, and must be able to walk on uneven surfaces.', + 'Animal handlers guide and manage animals during sessions. Should have 1+ years of experience with animals, and must be able to walk on uneven surfaces.', }, ] as const @@ -49,7 +49,7 @@ export interface UserData { yearsOfExperience: number | null } -export interface HorseData { +export interface AnimalData { id: string name: string imageId: string | null @@ -60,9 +60,9 @@ export interface HorseData { cooldownEndDate: Date | null } -export interface HorseAssignment { +export interface AnimalAssignment { userId: string - horseId: string + animalId: string } export interface CalEvent { @@ -72,28 +72,28 @@ export interface CalEvent { end: Date instructors: UserData[] - horses: HorseData[] + animals: AnimalData[] cleaningCrewReq: number lessonAssistantsReq: number - horseLeadersReq: number + animalHandlersReq: number sideWalkersReq: number cleaningCrew: UserData[] lessonAssistants: UserData[] - horseLeaders: UserData[] + animalHandlers: UserData[] sideWalkers: UserData[] } const EventWithAllRelations = Prisma.validator()({ include: { - horses: true, + animals: true, instructors: true, cleaningCrew: true, lessonAssistants: true, - horseLeaders: true, + animalHandlers: true, sideWalkers: true, - horseAssignments: true, + animalAssignments: true, }, }) @@ -102,11 +102,11 @@ export type EventWithAllRelations = Prisma.EventGetPayload< > const EventWithVolunteers = Prisma.validator()({ include: { - horses: true, + animals: true, instructors: true, cleaningCrew: true, lessonAssistants: true, - horseLeaders: true, + animalHandlers: true, sideWalkers: true, }, }) diff --git a/app/root.tsx b/app/root.tsx index f8ec336..2123cfa 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,10 +1,3 @@ -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuPortal, - DropdownMenuTrigger, -} from '~/components/ui/dropdown-menu.tsx' import { cssBundleHref } from '@remix-run/css-bundle' import { json, @@ -14,7 +7,6 @@ import { type V2_MetaFunction, } from '@remix-run/node' import { - Form, Link, Links, LiveReload, @@ -23,7 +15,6 @@ import { Scripts, ScrollRestoration, useLoaderData, - useSubmit, } from '@remix-run/react' import { withSentry } from '@sentry/remix' import { ThemeSwitch, useTheme } from './routes/resources+/theme/index.tsx' @@ -35,16 +26,16 @@ import { ClientHintCheck, getHints } from './utils/client-hints.tsx' import { prisma } from './utils/db.server.ts' import { getEnv } from './utils/env.server.ts' import { Button } from '~/components/ui/button.tsx' -import { combineHeaders, getDomainUrl, getUserImgSrc } from './utils/misc.ts' +import { combineHeaders, getDomainUrl } from './utils/misc.ts' import { useNonce } from './utils/nonce-provider.ts' import { makeTimings, time } from './utils/timing.server.ts' -import { useOptionalUser, useUser } from './utils/user.ts' -import { useRef } from 'react' -import { Icon, href as iconsHref } from './components/ui/icon.tsx' +import { useOptionalUser } from './utils/user.ts' +import { href as iconsHref } from './components/ui/icon.tsx' import { Confetti } from './components/confetti.tsx' import { getFlashSession } from './utils/flash-session.server.ts' import { useToast } from './utils/useToast.tsx' import { Toaster } from './components/ui/toaster.tsx' +import { Sidebar } from './components/sidebar.tsx' export const links: LinksFunction = () => { return [ @@ -77,8 +68,8 @@ export const links: LinksFunction = () => { export const meta: V2_MetaFunction = () => { return [ - { title: 'The Barn - Volunteer Portal' }, - { name: 'description', content: 'Equestrian Volunteer Coordinator' }, + { title: 'The Barn' }, + { name: 'description', content: 'Volunteer scheduling for animal-assisted therapy nonprofits' }, ] } @@ -151,32 +142,6 @@ function App() { const theme = useTheme() useToast(data.flash?.toast) - const userIsAdmin = user?.roles.find(role => role.name === 'admin') - - let nav = ( - - ) - if (user) { - nav = ( -
-
- - {userIsAdmin ? : null} -
-
- -
-
- ) - } - return ( @@ -186,39 +151,36 @@ function App() { - -
- -
- -
- -
- -
- -
Equestrian
-
Volunteer Scheduler
- -
- - Terms of Service - - - Privacy Policy - + + {user ? ( + // Authenticated: sidebar layout +
+ +
+ +
- - -
- -
+ ) : ( + // Unauthenticated: top-nav layout +
+
+
+ + The Barn + +
+ + +
+
+
+
+ +
+
+ )} @@ -235,102 +197,3 @@ function App() { ) } export default withSentry(App) - -function UserDropdown() { - const user = useUser() - const submit = useSubmit() - const formRef = useRef(null) - return ( - - - - - - - - - - Profile - - - - { - event.preventDefault() - submit(formRef.current) - }} - > -
- -
-
-
-
-
- ) -} - -function AdminDropdown() { - return ( - - - - - - - - - - Users - - - - - - - Horses - - - - - - - Email - - - - - - - ) -} diff --git a/app/routes/_auth+/login.tsx b/app/routes/_auth+/login.tsx index 2221863..ad01a50 100644 --- a/app/routes/_auth+/login.tsx +++ b/app/routes/_auth+/login.tsx @@ -41,20 +41,29 @@ export default function LoginPage() { const redirectTo = searchParams.get('redirectTo') || '/' return ( -
-
-
-

Welcome back!

-

- Please enter your details. -

+
+ {/* Left brand panel */} +
+ + {/* Right form panel */} +
+
+
+

Welcome back

+

+ Sign in to your account to continue. +

+
+ + {data.unverified ? ( + + ) : ( + + )}
- - {data.unverified ? ( - - ) : ( - - )}
) diff --git a/app/routes/_auth+/signup/index.tsx b/app/routes/_auth+/signup/index.tsx index 1d41897..bd9fc57 100644 --- a/app/routes/_auth+/signup/index.tsx +++ b/app/routes/_auth+/signup/index.tsx @@ -162,7 +162,7 @@ export default function SignupRoute() { return (
-

Let's saddle up!

+

Let's get started!

Please enter your email and the secret password given to you by the volunteer coordinator. diff --git a/app/routes/_marketing+/index.tsx b/app/routes/_marketing+/index.tsx index 6460760..7cf76ed 100644 --- a/app/routes/_marketing+/index.tsx +++ b/app/routes/_marketing+/index.tsx @@ -1,107 +1,117 @@ import type { V2_MetaFunction } from '@remix-run/node' -import { horseMountains, ohack } from './logos/logos.ts' import { Link } from '@remix-run/react' +import { Calendar, Users, Footprints, Building2 } from 'lucide-react' import { Button } from '~/components/ui/button.tsx' import { useOptionalUser } from '~/utils/user.ts' -// Get siteName from data.ts import { siteName } from '~/data.ts' - -export const meta: V2_MetaFunction = () => { - +export const meta: V2_MetaFunction = () => { return [ - { - title: siteName, - }, - { - property: "og:title", - content: siteName, - }, - { - name: "description", - content: `Welcome to ${siteName}, the premier equestrian training facility.`, - }, - { - name: "og:description", - content: `Welcome to ${siteName}, the premier equestrian training facility.`, - }, + { title: `${siteName} โ€” Volunteer Scheduling for Nonprofits` }, { - name: "og:image", - content: "/img/calendar-icon-with-horse-at-grand-canyon-using-arizona-flag-colors.jpeg", + name: 'description', + content: + 'The Barn helps animal-assisted therapy nonprofits coordinate volunteers, manage animals, and schedule events โ€” all in one place.', }, - ]; -}; + ] +} + +const features = [ + { + icon: , + title: 'Event Scheduling', + description: + 'Create and manage therapy sessions, assign animals, and set volunteer slots with a visual calendar.', + }, + { + icon: , + title: 'Volunteer Management', + description: + 'Volunteers self-register for roles. Admins see who is coming, assign handlers, and send bulk emails.', + }, + { + icon: , + title: 'Animal Tracking', + description: + 'Track your animals, manage cooldown periods between events, and prevent scheduling conflicts automatically.', + }, + { + icon: , + title: 'Multi-Org Support', + description: + 'Each organization gets its own isolated workspace. Data never crosses between organizations.', + }, +] export default function Index() { const user = useOptionalUser() + return ( -

-
-
-
-
- -
-
-
-

- - The Barn: Volunteer Portal - -

-

- Equestrian Volunteer Scheduling Application -

-
- {user ? ( - - ) : ( - - )} -
-
-
+
+ {/* Hero */} +
+ + Volunteer Scheduling for Nonprofits + +

+ Coordinate volunteers.{' '} + Simplify scheduling. +

+

+ The Barn gives your organization a dedicated space to manage events, + track animals, and keep volunteers in sync โ€” with zero spreadsheets. +

+
+ {user ? ( + + ) : ( + <> + + + + )}
+
-
-
-
- - Trot Track - {' '} - is built by: - +
+

+ Everything your org needs +

+
+ {features.map(f => ( + +
{f.icon}
+

{f.title}

+

{f.description}

+
+ ))}
-
-
+ + + {/* CTA strip */} +
+

Ready to get started?

+

+ Set up your organization in minutes. No credit card required. +

+ {!user && ( + + )} +
+
) } - diff --git a/app/routes/_marketing+/org-signup.tsx b/app/routes/_marketing+/org-signup.tsx new file mode 100644 index 0000000..a14ab7a --- /dev/null +++ b/app/routes/_marketing+/org-signup.tsx @@ -0,0 +1,272 @@ +import { conform, useForm } from '@conform-to/react' +import { getFieldsetConstraint, parse } from '@conform-to/zod' +import { + json, + redirect, + type DataFunctionArgs, + type V2_MetaFunction, +} from '@remix-run/node' +import { + Form, + Link, + useActionData, + useFormAction, + useNavigation, +} from '@remix-run/react' +import { z } from 'zod' +import { ErrorList, Field } from '~/components/forms.tsx' +import { StatusButton } from '~/components/ui/status-button.tsx' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '~/components/ui/select.tsx' +import { Label } from '~/components/ui/label.tsx' +import { Separator } from '~/components/ui/separator.tsx' +import { requireAnonymous, signupOrg, authenticator } from '~/utils/auth.server.ts' +import { commitSession, getSession } from '~/utils/session.server.ts' +import { + nameSchema, + passwordSchema, + usernameSchema, + emailSchema, +} from '~/utils/user-validation.ts' +import { siteName } from '~/data.ts' +import { prisma } from '~/utils/db.server.ts' + +const orgSignupSchema = z + .object({ + orgName: z + .string() + .min(2, { message: 'Organization name is too short' }) + .max(80, { message: 'Organization name is too long' }), + animalType: z.string().min(1, { message: 'Please select an animal type' }), + name: nameSchema, + username: usernameSchema, + email: emailSchema, + password: passwordSchema, + confirmPassword: passwordSchema, + }) + .superRefine(({ confirmPassword, password }, ctx) => { + if (confirmPassword !== password) { + ctx.addIssue({ + path: ['confirmPassword'], + code: 'custom', + message: 'The passwords did not match', + }) + } + }) + +function slugify(name: string) { + return name + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .slice(0, 60) +} + +export async function loader({ request }: DataFunctionArgs) { + await requireAnonymous(request) + return json({}) +} + +export const meta: V2_MetaFunction = () => { + return [{ title: `Register Your Organization | ${siteName}` }] +} + +export async function action({ request }: DataFunctionArgs) { + await requireAnonymous(request) + const formData = await request.formData() + const submission = await parse(formData, { + schema: orgSignupSchema.superRefine(async ({ username, email, orgName }, ctx) => { + const existingUser = await prisma.user.findFirst({ + where: { OR: [{ username }, { email }] }, + select: { id: true, username: true, email: true }, + }) + if (existingUser?.username === username) { + ctx.addIssue({ + path: ['username'], + code: 'custom', + message: 'A user already exists with this username', + }) + } + if (existingUser?.email === email) { + ctx.addIssue({ + path: ['email'], + code: 'custom', + message: 'A user already exists with this email', + }) + } + const slug = slugify(orgName) + const existingOrg = await prisma.organization.findUnique({ + where: { slug }, + select: { id: true }, + }) + if (existingOrg) { + ctx.addIssue({ + path: ['orgName'], + code: 'custom', + message: 'An organization with a similar name already exists', + }) + } + }), + async: true, + }) + + if (submission.intent !== 'submit') { + return json({ status: 'idle', submission } as const) + } + if (!submission.value) { + return json({ status: 'error', submission } as const, { status: 400 }) + } + + const { orgName, animalType, name, username, email, password } = + submission.value + const orgSlug = slugify(orgName) + + const session = await signupOrg({ + orgName, + orgSlug, + animalType, + email, + username, + name, + password, + }) + + const cookieSession = await getSession(request.headers.get('cookie')) + cookieSession.set(authenticator.sessionKey, session.id) + + return redirect('/calendar', { + headers: { + 'Set-Cookie': await commitSession(cookieSession, { + expires: session.expirationDate, + }), + }, + }) +} + +export default function OrgSignup() { + const actionData = useActionData() + const navigation = useNavigation() + const formAction = useFormAction() + + const isSubmitting = + navigation.state === 'submitting' && navigation.formAction === formAction + + const [form, fields] = useForm({ + id: 'org-signup', + constraint: getFieldsetConstraint(orgSignupSchema), + lastSubmission: actionData?.submission, + shouldRevalidate: 'onBlur', + }) + + return ( +
+ {/* Left image panel */} +
+ + {/* Right form panel */} +
+
+
+

Register Your Organization

+

+ Create your organization account and start coordinating volunteers. +

+
+ +
+
+

Organization Details

+
+ +
+ + + {fields.animalType.errors ? ( + + ) : null} +
+
+
+ + + +
+

Admin Account

+
+ + + + + +
+
+ + + +
+

+ Already have an account?{' '} + + Log in + +

+ + Create Organization + +
+ +
+
+
+ ) +} diff --git a/app/routes/_marketing+/privacy.tsx b/app/routes/_marketing+/privacy.tsx index 3a1fee2..7ca600a 100644 --- a/app/routes/_marketing+/privacy.tsx +++ b/app/routes/_marketing+/privacy.tsx @@ -3,16 +3,16 @@ import { type V2_MetaFunction } from '@remix-run/node' export const meta: V2_MetaFunction = () => { return [ { - title: 'Privacy Policy | TrotTrack.org', + title: 'Privacy Policy | thebarnaz.com', }, { property: 'og:title', - content: 'Privacy Policy | TrotTrack.org', + content: 'Privacy Policy | thebarnaz.com', }, { name: 'description', content: - 'Read our Privacy Policy for TrotTrack.org, a nonprofit equestrian volunteer system that connects volunteers with equestrian organizations.', + 'Read our Privacy Policy for thebarnaz.com, a nonprofit animal volunteer management system that connects volunteers with animal-assisted therapy organizations.', }, ] } diff --git a/app/routes/_marketing+/tos.tsx b/app/routes/_marketing+/tos.tsx index 51dd935..ca0439b 100644 --- a/app/routes/_marketing+/tos.tsx +++ b/app/routes/_marketing+/tos.tsx @@ -3,16 +3,16 @@ import { type V2_MetaFunction } from '@remix-run/node' export const meta: V2_MetaFunction = () => { return [ { - title: 'Terms of Service | TrotTrack.org', + title: 'Terms of Service | thebarnaz.com', }, { property: 'og:title', - content: 'Terms of Service | TrotTrack.org', + content: 'Terms of Service | thebarnaz.com', }, { name: 'description', content: - 'Read or Terms of Service for TrotTrack.org, a nonprofit equestrian volunteer system that connects volunteers with equestrian organizations.', + 'Read our Terms of Service for thebarnaz.com, a nonprofit animal volunteer management system that connects volunteers with animal-assisted therapy organizations.', }, ] } @@ -28,7 +28,7 @@ export default function TermsOfServiceRoute() {

1. Acceptance of Terms

- These terms of service govern your use of TrotTrack.org. By accessing + These terms of service govern your use of thebarnaz.com. By accessing or using our website, you acknowledge that you have read, understood, and agree to be bound by these terms.

@@ -36,8 +36,8 @@ export default function TermsOfServiceRoute() {

2. Description of Services

- TrotTrack.org is a nonprofit equestrian volunteer system that connects - volunteers with equestrian organizations. Our services include + thebarnaz.com is a nonprofit animal volunteer management system that connects + volunteers with animal-assisted therapy organizations. Our services include providing a platform for volunteers to find and sign up for volunteer opportunities, and for organizations to manage their volunteer programs. @@ -45,7 +45,7 @@ export default function TermsOfServiceRoute() {

3. User Responsibilities

-

As a user of TrotTrack.org, you agree to:

+

As a user of thebarnaz.com, you agree to:

  • Provide accurate and complete information when creating an account @@ -57,25 +57,25 @@ export default function TermsOfServiceRoute() {
  • Respect the privacy and rights of other users
  • Not engage in any activity that may disrupt or interfere with the - proper functioning of TrotTrack.org + proper functioning of thebarnaz.com

4. Intellectual Property

- All content and materials on TrotTrack.org, including but not limited + All content and materials on thebarnaz.com, including but not limited to text, graphics, logos, images, and software, are the property of - TrotTrack.org or its licensors and are protected by intellectual + thebarnaz.com or its licensors and are protected by intellectual property laws. You may not reproduce, distribute, modify, or create derivative works of any content without prior written consent from - TrotTrack.org. + thebarnaz.com.

5. Limitation of Liability

- TrotTrack.org and its affiliates shall not be liable for any direct, + thebarnaz.com and its affiliates shall not be liable for any direct, indirect, incidental, special, or consequential damages arising out of or in connection with your use of the website or services.

@@ -84,11 +84,11 @@ export default function TermsOfServiceRoute() {

6. Governing Law

These terms of service shall be governed by and construed in - accordance with the laws of the jurisdiction in which TrotTrack.org + accordance with the laws of the jurisdiction in which thebarnaz.com operates.

- By using TrotTrack.org, you agree to these terms of service. If you do + By using thebarnaz.com, you agree to these terms of service. If you do not agree with any part of these terms, please do not use our website or services.

diff --git a/app/routes/admin+/_horses+/horses.delete.$horseId.tsx b/app/routes/admin+/_animals+/animals.delete.$animalId.tsx similarity index 74% rename from app/routes/admin+/_horses+/horses.delete.$horseId.tsx rename to app/routes/admin+/_animals+/animals.delete.$animalId.tsx index a3e9828..f01187b 100644 --- a/app/routes/admin+/_horses+/horses.delete.$horseId.tsx +++ b/app/routes/admin+/_animals+/animals.delete.$animalId.tsx @@ -20,6 +20,7 @@ import { } from '@remix-run/react' import { json, type DataFunctionArgs } from '@remix-run/node' import { requireAdmin } from '~/utils/permissions.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { prisma } from '~/utils/db.server.ts' import invariant from 'tiny-invariant' import { conform, useForm } from '@conform-to/react' @@ -31,35 +32,37 @@ import { z } from 'zod' export const loader = async ({ request, params }: DataFunctionArgs) => { await requireAdmin(request) - invariant(params.horseId, 'Missing horse id') - const horse = await prisma.horse.findUnique({ where: { id: params.horseId } }) - if (!horse) { + const { orgId } = await requireOrgMember(request) + invariant(params.animalId, 'Missing animal id') + const animal = await prisma.animal.findFirst({ where: { id: params.animalId, orgId } }) + if (!animal) { throw new Response('not found', { status: 404 }) } - return json({ horse }) + return json({ animal }) } -export const deleteHorseFormSchema = z.object({ +export const deleteAnimalFormSchema = z.object({ name: z .string() .min(1, { message: - 'You must enter the name of this horse to delete it from the database.', + 'You must enter the name of this animal to delete it from the database.', }), }) export async function action({ request, params }: DataFunctionArgs) { await requireAdmin(request) - invariant(params.horseId, 'Missing horse id') + const { orgId } = await requireOrgMember(request) + invariant(params.animalId, 'Missing animal id') const formData = await request.formData() - const horse = await prisma.horse.findUnique({ where: { id: params.horseId } }) - if (!horse) { + const animal = await prisma.animal.findFirst({ where: { id: params.animalId, orgId } }) + if (!animal) { throw new Response('not found', { status: 404 }) } const submission = await parse(formData, { async: true, - schema: deleteHorseFormSchema.superRefine(async ({ name }, ctx) => { - if (horse.name != name) { + schema: deleteAnimalFormSchema.superRefine(async ({ name }, ctx) => { + if (animal.name != name) { ctx.addIssue({ path: ['name'], code: 'custom', @@ -82,26 +85,26 @@ export async function action({ request, params }: DataFunctionArgs) { ) } - let deletedHorse + let deletedAnimal try { - deletedHorse = await prisma.horse.delete({ - where: { id: params.horseId }, + deletedAnimal = await prisma.animal.delete({ + where: { id: params.animalId }, }) } catch { - return redirectWithToast('/admin/horses', { + return redirectWithToast('/admin/animals', { title: 'Error', variant: 'destructive', - description: 'Failed to delete horse', + description: 'Failed to delete animal', }) } - return redirectWithToast('/admin/horses', { + return redirectWithToast('/admin/animals', { title: 'Success', - description: `Deleted horse ${deletedHorse.name}`, + description: `Deleted animal ${deletedAnimal.name}`, }) } -export default function DeleteHorse() { +export default function DeleteAnimal() { const data = useLoaderData() || {} const actionData = useActionData() const [open, setOpen] = useState(true) @@ -120,7 +123,7 @@ export default function DeleteHorse() { navigate('..', { preventScrollReset: true }) } const [form, fields] = useForm({ - id: 'edit-horse', + id: 'delete-animal', lastSubmission: actionData?.submission, shouldRevalidate: 'onSubmit', }) @@ -132,9 +135,9 @@ export default function DeleteHorse() { onPointerDownOutside={dismissModal} > - Delete Horse + Delete Animal - Are you sure you want to remove {data.horse?.name} from the + Are you sure you want to remove {data.animal?.name} from the database? This will affect all associated events and assignments. @@ -142,7 +145,7 @@ export default function DeleteHorse() { { await requireAdmin(request) - invariant(params.horseId, 'Missing horse id') - const horse = await prisma.horse.findUnique({ where: { id: params.horseId } }) - if (!horse) { + const { orgId } = await requireOrgMember(request) + invariant(params.animalId, 'Missing animal id') + const animal = await prisma.animal.findFirst({ where: { id: params.animalId, orgId } }) + if (!animal) { throw new Response('not found', { status: 404 }) } - return json({ horse }) + return json({ animal }) } export async function action({ request, params }: DataFunctionArgs) { await requireAdmin(request) - invariant(params.horseId, 'Missing horse id') + const { orgId } = await requireOrgMember(request) + invariant(params.animalId, 'Missing animal id') const formData = await request.formData() const submission = await parse(formData, { async: true, - schema: horseFormSchema, + schema: animalFormSchema, }) if (submission.intent !== 'submit') { @@ -79,8 +82,12 @@ export async function action({ request, params }: DataFunctionArgs) { cooldownEndDate, } = submission.value - const updatedHorse = await prisma.horse.update({ - where: { id: params.horseId }, + const existingAnimal = await prisma.animal.findFirst({ where: { id: params.animalId, orgId } }) + if (!existingAnimal) { + throw new Response('not found', { status: 404 }) + } + const updatedAnimal = await prisma.animal.update({ + where: { id: params.animalId }, data: { name, status, @@ -91,47 +98,44 @@ export async function action({ request, params }: DataFunctionArgs) { }, }) - if (!updatedHorse) { - return redirectWithToast(`/admin/horses`, { + if (!updatedAnimal) { + return redirectWithToast(`/admin/animals`, { title: `Error`, variant: 'destructive', - description: `Failed to update horse`, + description: `Failed to update animal`, }) } if (cooldown && cooldownStartDate && cooldownEndDate) { - // Get events horseID is registered for - const horseEvents = await prisma.event.findMany({ + const animalEvents = await prisma.event.findMany({ where: { - horses: { + animals: { some: { - id: updatedHorse.id, + id: updatedAnimal.id, }, }, }, }) - // Compare event dates to cooldown dates and gather events with conflicts const conflictEvents = [] - if (horseEvents) { - for (const e of horseEvents) { + if (animalEvents) { + for (const e of animalEvents) { if ( cooldownStartDate <= e.start && - e.start < add(cooldownEndDate, { days: 1 }) // checks that event is before midnight of cooldownEndDate + e.start < add(cooldownEndDate, { days: 1 }) ) { conflictEvents.push(e) } } } - // Remove horse from events with conflicts if (conflictEvents.length > 0) { for (const e of conflictEvents) { await prisma.event.update({ where: { id: e.id }, data: { - horses: { - disconnect: { id: updatedHorse.id }, + animals: { + disconnect: { id: updatedAnimal.id }, }, }, }) @@ -144,13 +148,13 @@ export async function action({ request, params }: DataFunctionArgs) { } } - return redirectWithToast(`/admin/horses`, { + return redirectWithToast(`/admin/animals`, { title: `Success`, - description: `Updated ${updatedHorse.name}`, + description: `Updated ${updatedAnimal.name}`, }) } -export default function EditHorse() { +export default function EditAnimal() { const data = useLoaderData() || {} const actionData = useActionData() const [open, setOpen] = useState(true) @@ -169,31 +173,27 @@ export default function EditHorse() { navigate('..', { preventScrollReset: true }) } const [form, fields] = useForm({ - id: 'edit-horse', + id: 'edit-animal', lastSubmission: actionData?.submission, defaultValue: { - name: data.horse?.name, - status: data.horse?.status, - notes: data.horse?.notes, - cooldownStartDate: data.horse?.cooldownStartDate - ? format(new Date(data.horse.cooldownStartDate), 'yyyy-MM-dd') + name: data.animal?.name, + status: data.animal?.status, + notes: data.animal?.notes, + cooldownStartDate: data.animal?.cooldownStartDate + ? format(new Date(data.animal.cooldownStartDate), 'yyyy-MM-dd') : null, - cooldownEndDate: data.horse?.cooldownEndDate - ? format(new Date(data.horse.cooldownEndDate), 'yyyy-MM-dd') + cooldownEndDate: data.animal?.cooldownEndDate + ? format(new Date(data.animal.cooldownEndDate), 'yyyy-MM-dd') : null, }, shouldRevalidate: 'onSubmit', onSubmit: dismissModal, }) - /** - * If there is returned actionData (form validation errors), - * use that checked state, otherwise use the boolean from the DB - */ const cooldown = actionData ? actionData.submission.payload?.cooldown === 'on' ? true : false - : data.horse?.cooldown + : data.animal?.cooldown const [cooldownChecked, setCooldownChecked] = useState(cooldown) const conflictEvents = actionData?.conflictEvents ?? null @@ -204,9 +204,9 @@ export default function EditHorse() { onPointerDownOutside={dismissModal} > - Edit Horse: {data.horse?.name} + Edit Animal: {data.animal?.name} - Edit this horse using this form. Click save to save your changes. + Edit this animal using this form. Click save to save your changes.
@@ -288,7 +288,7 @@ export default function EditHorse() { - Horse removed from {conflictEvents.length}{' '} + Animal removed from {conflictEvents.length}{' '} {conflictEvents.length === 1 ? 'event' : 'events'} diff --git a/app/routes/admin+/_horses+/horses.tsx b/app/routes/admin+/_animals+/animals.tsx similarity index 75% rename from app/routes/admin+/_horses+/horses.tsx rename to app/routes/admin+/_animals+/animals.tsx index 781c0e9..8435524 100644 --- a/app/routes/admin+/_horses+/horses.tsx +++ b/app/routes/admin+/_animals+/animals.tsx @@ -10,6 +10,7 @@ import { } from '~/remix.ts' import { prisma } from '~/utils/db.server.ts' import { requireAdmin } from '~/utils/permissions.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { DataTable } from '~/components/ui/data_table.tsx' import { z } from 'zod' import { @@ -31,7 +32,7 @@ import { useResetCallback } from '~/utils/misc.ts' import { useToast } from '~/components/ui/use-toast.ts' import { type ColumnDef } from '@tanstack/react-table' -import { type Horse } from '@prisma/client' +import { type Animal } from '@prisma/client' import { formatRelative } from 'date-fns' import { Icon } from '~/components/ui/icon.tsx' import { @@ -48,10 +49,11 @@ import { optionalDateTimeZoneSchema, } from '~/utils/zod-extensions.ts' -export const horseFormSchema = z +export const animalFormSchema = z .object({ _action: z.enum(['create', 'update']), name: z.string().min(1, { message: 'Name is required' }), + species: z.string().optional(), notes: z.string().optional(), status: z.string().optional(), cooldown: checkboxSchema(), @@ -59,10 +61,6 @@ export const horseFormSchema = z cooldownEndDate: optionalDateTimeZoneSchema, }) .refine( - /** - * This makes sure that if cooldown is checked, there is both a start and end date, - * and if cooldown is not checked, there are no start and end dates - */ schema => (schema.cooldownStartDate === null && schema.cooldownEndDate === null && @@ -78,9 +76,6 @@ export const horseFormSchema = z }, ) .refine( - /** - * Checks that end date is after or equal to start date - */ schema => { const { cooldownStartDate, cooldownEndDate } = schema if (cooldownStartDate && cooldownEndDate) { @@ -88,26 +83,35 @@ export const horseFormSchema = z } else return true }, { - message: "End date must not be before start date." - } + message: 'End date must not be before start date.', + }, ) export const loader = async ({ request }: DataFunctionArgs) => { await requireAdmin(request) - return json(await prisma.horse.findMany()) + const { orgId } = await requireOrgMember(request) + return json(await prisma.animal.findMany({ where: { orgId } })) } -export default function Horses() { +export default function Animals() { const data = useLoaderData() return ( -
-

Horses

-
- -
-
- +
+
+
+

Animals

+

+ {data.length} animal{data.length !== 1 ? 's' : ''} in your roster +

+
+
+
) @@ -115,17 +119,20 @@ export default function Horses() { export const action = async ({ request }: ActionArgs) => { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) const formData = await request.formData() - const submission = parse(formData, { schema: horseFormSchema }) + const submission = parse(formData, { schema: animalFormSchema }) if (!submission.value) { return json({ status: 'error', submission } as const, { status: 400 }) } - await prisma.horse.create({ + await prisma.animal.create({ data: { name: submission.value.name, + species: submission.value.species ?? 'horse', notes: submission.value.notes, status: submission.value.status, + orgId, }, }) @@ -138,7 +145,7 @@ export const action = async ({ request }: ActionArgs) => { ) } -function CreateHorseDialog() { +function CreateAnimalDialog() { const [open, setOpen] = useState(false) const actionData = useActionData() const { toast } = useToast() @@ -149,15 +156,15 @@ function CreateHorseDialog() { if (actionData.status == 'ok') { toast({ title: 'Success', - description: `Created horse "${actionData.submission?.value?.name}".`, + description: `Added animal "${actionData.submission?.value?.name}".`, }) setOpen(false) } else { if (actionData.submission.value?._action == 'create') { toast({ variant: 'destructive', - title: 'Error creating horse', - description: 'Failed to create horse. There was an unexpected error.', + title: 'Error adding animal', + description: 'Failed to add animal. There was an unexpected error.', }) } } @@ -166,16 +173,16 @@ function CreateHorseDialog() { return ( - - Register new horse + Add new animal - Fill out this form to add a new horse to the database. + Fill out this form to add a new animal to the roster. @@ -183,6 +190,9 @@ function CreateHorseDialog() { + + + @@ -198,11 +208,15 @@ function CreateHorseDialog() { ) } -export const columns: ColumnDef[] = [ +export const columns: ColumnDef[] = [ { accessorKey: 'name', header: 'name', }, + { + accessorKey: 'species', + header: 'species', + }, { accessorKey: 'notes', header: 'notes', diff --git a/app/routes/admin+/_email+/email.tsx b/app/routes/admin+/_email+/email.tsx index 8389a13..2124630 100644 --- a/app/routes/admin+/_email+/email.tsx +++ b/app/routes/admin+/_email+/email.tsx @@ -8,6 +8,7 @@ import { } from '@remix-run/react' import { z } from 'zod' import { requireAdmin } from '~/utils/permissions.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { useToast } from '~/components/ui/use-toast.ts' import { checkboxSchema } from '~/utils/zod-extensions.ts' import { useResetCallback } from '~/utils/misc.ts' @@ -29,7 +30,7 @@ const emailFormSchema = z .object({ allVolunteers: checkboxSchema(), lessonAssistant: checkboxSchema(), - horseLeader: checkboxSchema(), + animalHandler: checkboxSchema(), instructor: checkboxSchema(), subject: z .string() @@ -49,6 +50,7 @@ export const loader = async ({ request }: LoaderArgs) => { export async function action({ request, params }: DataFunctionArgs) { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) const formData = await request.formData() const submission = parse(formData, { schema: emailFormSchema }) if (!submission.value) { @@ -60,12 +62,12 @@ export async function action({ request, params }: DataFunctionArgs) { const roles = [ 'allVolunteers', 'lessonAssistant', - 'horseLeader', + 'animalHandler', 'instructor', ] const selectedRoles = roles.filter(role => submission.payload[role] === 'on') - const recipients = await getRecipientsFromRoles(selectedRoles) - const upcomingEvents = await getUpcomingEvents(5); + const recipients = await getRecipientsFromRoles(selectedRoles, orgId) + const upcomingEvents = await getUpcomingEvents(5, orgId); if (recipients.length === 0) { return json( @@ -151,15 +153,21 @@ export default function Email() { }) return ( -
-

Email

-
- +
+
+
+

Email

+

+ Send bulk emails to volunteers by role. +

+
+
+
-
) } -async function getUpcomingEvents(limit: number) { +async function getUpcomingEvents(limit: number, orgId: string) { const events = await prisma.event.findMany({ where: { + orgId, start: { gt: new Date() }, // Don't include private events in upcoming events isPrivate: false, @@ -255,10 +263,10 @@ async function getUpcomingEvents(limit: number) { } -async function getRecipientsFromRoles(roles: string[]) { +async function getRecipientsFromRoles(roles: string[], orgId: string) { const recipients = new Set() if (roles.includes('allVolunteers')) { - const users = await prisma.user.findMany() + const users = await prisma.user.findMany({ where: { orgId } }) users .filter(user => user.mailingList) .map(user => user.email) @@ -266,7 +274,7 @@ async function getRecipientsFromRoles(roles: string[]) { } else { for (let role of roles) { const users = await prisma.user.findMany({ - where: { roles: { some: { name: role } } }, + where: { orgId, roles: { some: { name: role } } }, }) users .filter(user => user.mailingList) @@ -275,7 +283,7 @@ async function getRecipientsFromRoles(roles: string[]) { // Include admin on all emails const admin = await prisma.user.findMany({ - where: { roles: { some: { name: 'admin' } } }, + where: { orgId, roles: { some: { name: 'admin' } } }, }) admin .filter(user => user.mailingList) diff --git a/app/routes/admin+/_users+/users.delete.$userId.tsx b/app/routes/admin+/_users+/users.delete.$userId.tsx index f939270..e554c70 100644 --- a/app/routes/admin+/_users+/users.delete.$userId.tsx +++ b/app/routes/admin+/_users+/users.delete.$userId.tsx @@ -20,6 +20,7 @@ import { } from '@remix-run/react' import { json, type DataFunctionArgs } from '@remix-run/node' import { requireAdmin } from '~/utils/permissions.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { prisma } from '~/utils/db.server.ts' import invariant from 'tiny-invariant' import { conform, useForm } from '@conform-to/react' @@ -31,8 +32,9 @@ import { z } from 'zod' export const loader = async ({ request, params }: DataFunctionArgs) => { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) invariant(params.userId, 'Missing user id') - const user = await prisma.user.findUnique({ where: { id: params.userId } }) + const user = await prisma.user.findFirst({ where: { id: params.userId, orgId } }) if (!user) { throw new Response('not found', { status: 404 }) } @@ -50,9 +52,10 @@ export const deleteUserFormSchema = z.object({ export async function action({ request, params }: DataFunctionArgs) { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) invariant(params.userId, 'Missing user id') const formData = await request.formData() - const user = await prisma.user.findUnique({ where: { id: params.userId } }) + const user = await prisma.user.findFirst({ where: { id: params.userId, orgId } }) if (!user) { throw new Response('not found', { status: 404 }) } diff --git a/app/routes/admin+/_users+/users.edit.$userId.tsx b/app/routes/admin+/_users+/users.edit.$userId.tsx index c5a13fa..bcd6e8a 100644 --- a/app/routes/admin+/_users+/users.edit.$userId.tsx +++ b/app/routes/admin+/_users+/users.edit.$userId.tsx @@ -26,6 +26,7 @@ import { } from '@remix-run/react' import { json, type DataFunctionArgs } from '@remix-run/node' import { requireAdmin } from '~/utils/permissions.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { prisma } from '~/utils/db.server.ts' import invariant from 'tiny-invariant' import { conform, useFieldset, useForm } from '@conform-to/react' @@ -58,15 +59,16 @@ const editUserSchema = z.object({ yearsOfExperience: yearsOfExperienceSchema, isInstructor: checkboxSchema(), isLessonAssistant: checkboxSchema(), - isHorseLeader: checkboxSchema(), + isAnimalHandler: checkboxSchema(), notes: z.string().optional(), }) export const loader = async ({ request, params }: DataFunctionArgs) => { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) invariant(params.userId, 'Missing user id') - const user = await prisma.user.findUnique({ - where: { id: params.userId }, + const user = await prisma.user.findFirst({ + where: { id: params.userId, orgId }, include: { roles: true }, }) if (!user) { @@ -77,6 +79,7 @@ export const loader = async ({ request, params }: DataFunctionArgs) => { export async function action({ request, params }: DataFunctionArgs) { await requireAdmin(request) + await requireOrgMember(request) invariant(params.userId, 'Missing user id') const formData = await request.formData() const submission = await parse(formData, { @@ -106,7 +109,7 @@ export async function action({ request, params }: DataFunctionArgs) { height, yearsOfExperience, isInstructor, - isHorseLeader, + isAnimalHandler, isLessonAssistant, notes, } = submission.value @@ -118,10 +121,10 @@ export async function action({ request, params }: DataFunctionArgs) { } else { roleDisconnectArray.push({ name: 'instructor' }) } - if (isHorseLeader) { - roleConnectArray.push({ name: 'horseLeader' }) + if (isAnimalHandler) { + roleConnectArray.push({ name: 'animalHandler' }) } else { - roleDisconnectArray.push({ name: 'horseLeader' }) + roleDisconnectArray.push({ name: 'animalHandler' }) } if (isLessonAssistant) { roleConnectArray.push({ name: 'lessonAssistant' }) @@ -213,14 +216,14 @@ export default function EditUser() { const { heightFeet, heightInches } = useFieldset(form.ref, fields.height) let isLessonAssistant = false - let isHorseLeader = false + let isAnimalHandler = false let isInstructor = false for (const role of data.user?.roles) { if (role.name === 'lessonAssistant') { isLessonAssistant = true } - if (role.name === 'horseLeader') { - isHorseLeader = true + if (role.name === 'animalHandler') { + isAnimalHandler = true } if (role.name === 'instructor') { isInstructor = true @@ -348,7 +351,7 @@ export default function EditUser() { className="col-span-6 sm:col-span-3" labelProps={{ htmlFor: fields.yearsOfExperience.id, - children: 'Years of experience with horses', + children: 'Years of experience with animals', }} inputProps={{ ...conform.input(fields.yearsOfExperience), @@ -413,16 +416,16 @@ export default function EditUser() { />
diff --git a/app/routes/admin+/_users+/users.promote.$userId.tsx b/app/routes/admin+/_users+/users.promote.$userId.tsx index 77a5e56..32e8fe2 100644 --- a/app/routes/admin+/_users+/users.promote.$userId.tsx +++ b/app/routes/admin+/_users+/users.promote.$userId.tsx @@ -18,6 +18,7 @@ import { useNavigation, } from '@remix-run/react' import { requireAdmin } from '~/utils/permissions.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { prisma } from '~/utils/db.server.ts' import { StatusButton } from '~/components/ui/status-button.tsx' import { Button } from '~/components/ui/button.tsx' @@ -30,9 +31,10 @@ import { redirectWithToast } from '~/utils/flash-session.server.ts' export const loader = async ({ request, params }: DataFunctionArgs) => { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) invariant(params.userId, 'Missing user id') - const user = await prisma.user.findUnique({ - where: { id: params.userId }, + const user = await prisma.user.findFirst({ + where: { id: params.userId, orgId }, include: { roles: true, }, @@ -49,6 +51,7 @@ const promoteSchema = z.object({ export const action = async ({ request, params }: DataFunctionArgs) => { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) invariant(params.userId, 'Missing user id') const formData = await request.formData() const submission = parse(formData, { schema: promoteSchema }) @@ -68,6 +71,9 @@ export const action = async ({ request, params }: DataFunctionArgs) => { }) } + const targetUser = await prisma.user.findFirst({ where: { id: params.userId, orgId } }) + if (!targetUser) throw new Response('not found', { status: 404 }) + let user if (submission.value._action === 'promote') { user = await prisma.user.update({ @@ -139,7 +145,7 @@ export default function PromotionModal() { Proceeding will {userIsAdmin ? " revoke this user's " : ' give this user '} - permission to edit user, horse, and event data. + permission to edit user, animal, and event data. diff --git a/app/routes/admin+/_users+/users.tsx b/app/routes/admin+/_users+/users.tsx index 336b990..a4967f9 100644 --- a/app/routes/admin+/_users+/users.tsx +++ b/app/routes/admin+/_users+/users.tsx @@ -1,6 +1,8 @@ +import { useState } from 'react' import { type LoaderArgs, json, useLoaderData, Outlet, Link } from '~/remix.ts' import { prisma } from '~/utils/db.server.ts' import { requireAdmin } from '~/utils/permissions.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { DataTable } from '~/components/ui/data_table.tsx' import { type ColumnDef } from '@tanstack/react-table' @@ -21,17 +23,61 @@ import { SetSignupPasswordForm } from '~/routes/resources+/signup_password.tsx' export const loader = async ({ request }: LoaderArgs) => { await requireAdmin(request) - return json(await prisma.user.findMany({ include: { roles: true } })) + const { orgId } = await requireOrgMember(request) + return json(await prisma.user.findMany({ where: { orgId }, include: { roles: true } })) } export default function Users() { const data = useLoaderData() + const [search, setSearch] = useState('') + + const filtered = data.filter(u => { + const q = search.toLowerCase() + return ( + !q || + u.name?.toLowerCase().includes(q) || + u.email.toLowerCase().includes(q) || + u.username.toLowerCase().includes(q) + ) + }) + return ( -
-

Users

-
- - +
+
+
+

Volunteers

+

+ {data.length} member{data.length !== 1 ? 's' : ''} in your organization +

+
+
+ + {/* Filter bar */} +
+
+ setSearch(e.target.value)} + className="w-full rounded-md border border-input bg-background px-3 py-2 text-body-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + /> +
+ {search && ( + + {filtered.length} result{filtered.length !== 1 ? 's' : ''} + + )} +
+ + +
+
@@ -96,9 +142,9 @@ export const columns: ColumnDef[] = [ }, }, { - header: 'horse leader', + header: 'animal handler', accessorFn: (row) => { - const hasRole = row.roles.find(r => r.name === 'horseLeader') + const hasRole = row.roles.find(r => r.name === 'animalHandler') return hasRole ? 'Yes' : 'No' }, }, diff --git a/app/routes/admin+/index.tsx b/app/routes/admin+/index.tsx new file mode 100644 index 0000000..6624efa --- /dev/null +++ b/app/routes/admin+/index.tsx @@ -0,0 +1,7 @@ +import { redirect, type DataFunctionArgs } from '@remix-run/node' +import { requireAdmin } from '~/utils/permissions.server.ts' + +export async function loader({ request }: DataFunctionArgs) { + await requireAdmin(request) + throw redirect('/admin/users') +} diff --git a/app/routes/calendar+/$eventId.delete.tsx b/app/routes/calendar+/$eventId.delete.tsx index 899e592..3556ad4 100644 --- a/app/routes/calendar+/$eventId.delete.tsx +++ b/app/routes/calendar+/$eventId.delete.tsx @@ -20,6 +20,7 @@ import { } from '@remix-run/react' import { json, type DataFunctionArgs } from '@remix-run/node' import { requireAdmin } from '~/utils/permissions.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { prisma } from '~/utils/db.server.ts' import invariant from 'tiny-invariant' import { conform, useForm } from '@conform-to/react' @@ -31,8 +32,9 @@ import { z } from 'zod' export const loader = async ({ request, params }: DataFunctionArgs) => { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) invariant(params.eventId, 'Missing event id') - const event = await prisma.event.findUnique({ where: { id: params.eventId } }) + const event = await prisma.event.findFirst({ where: { id: params.eventId, orgId } }) if (!event) { throw new Response('not found', { status: 404 }) } @@ -50,9 +52,10 @@ export const deleteEventFormSchema = z.object({ export async function action({ request, params }: DataFunctionArgs) { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) invariant(params.eventId, 'Missing event id') const formData = await request.formData() - const event = await prisma.event.findUnique({ where: { id: params.eventId } }) + const event = await prisma.event.findFirst({ where: { id: params.eventId, orgId } }) if (!event) { throw new Response('not found', { status: 404 }) } diff --git a/app/routes/calendar+/$eventId.edit.tsx b/app/routes/calendar+/$eventId.edit.tsx index c3b0ed9..db64414 100644 --- a/app/routes/calendar+/$eventId.edit.tsx +++ b/app/routes/calendar+/$eventId.edit.tsx @@ -23,7 +23,8 @@ import { import { Icon } from '~/components/ui/icon.tsx' import { prisma } from '~/utils/db.server.ts' import { requireAdmin } from '~/utils/permissions.server.ts' -import { HorseListbox, InstructorListbox } from '~/components/listboxes.tsx' +import { requireOrgMember } from '~/utils/auth.server.ts' +import { AnimalListbox, InstructorListbox } from '~/components/listboxes.tsx' import { addMinutes, differenceInMinutes, format, add } from 'date-fns' import { redirectWithToast } from '~/utils/flash-session.server.ts' import { conform, useForm } from '@conform-to/react' @@ -44,16 +45,17 @@ import { useResetCallback } from '~/utils/misc.ts' export const loader = async ({ request, params }: DataFunctionArgs) => { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) invariant(params.eventId, 'Missing event id') const instructors = await prisma.user.findMany({ - where: { roles: { some: { name: 'instructor' } } }, + where: { orgId, roles: { some: { name: 'instructor' } } }, }) - const horses = await prisma.horse.findMany() - const event = await prisma.event.findUnique({ - where: { id: params.eventId }, + const animals = await prisma.animal.findMany({ where: { orgId } }) + const event = await prisma.event.findFirst({ + where: { id: params.eventId, orgId }, include: { - horses: true, + animals: true, instructors: true, }, }) @@ -61,10 +63,10 @@ export const loader = async ({ request, params }: DataFunctionArgs) => { if (!event) { throw new Response('not found', { status: 404 }) } - return json({ event, horses, instructors }) + return json({ event, animals, instructors }) } -const horseSchema = z.object({ +const animalSchema = z.object({ id: z.string(), name: z.string(), cooldownStartDate: optionalDateSchema, @@ -83,17 +85,18 @@ const editEventSchema = z.object({ title: z.string().min(1, 'Title is required'), startDate: z.coerce.date(), duration: z.coerce.number().gt(0), - horses: z.array(horseSchema).optional(), + animals: z.array(animalSchema).optional(), instructor: instructorSchema, cleaningCrewReq: z.coerce.number().gt(-1), lessonAssistantsReq: z.coerce.number().gt(-1), sideWalkersReq: z.coerce.number().gt(-1), - horseLeadersReq: z.coerce.number().gt(-1), + animalHandlersReq: z.coerce.number().gt(-1), isPrivate: checkboxSchema(), }) export async function action({ request, params }: DataFunctionArgs) { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) invariant(params.eventId, 'Missing event id') const formData = await request.formData() const submission = parse(formData, { @@ -121,27 +124,27 @@ export async function action({ request, params }: DataFunctionArgs) { if (instructorId) { instructorData = [{ id: instructorId }] } - const horseIds = submission.value.horses?.map(e => { + const animalIds = submission.value.animals?.map(e => { return { id: e.id } }) - // check that horses selected are not in cooldown period - if (submission.value.horses) { - const selectedHorsesArray = submission.value.horses - const errorHorses = selectedHorsesArray.filter(horse => { - if (horse.cooldownStartDate && horse.cooldownEndDate) { + // check that animals selected are not in cooldown period + if (submission.value.animals) { + const selectedAnimalsArray = submission.value.animals + const errorAnimals = selectedAnimalsArray.filter(animal => { + if (animal.cooldownStartDate && animal.cooldownEndDate) { return ( - horse.cooldownStartDate <= start && - start < add(horse.cooldownEndDate, { days: 1 }) + animal.cooldownStartDate <= start && + start < add(animal.cooldownEndDate, { days: 1 }) ) } else return false }) - const listOfHorses = errorHorses.map(h => h.name).join(', ') - if (errorHorses.length > 0) { + const listOfAnimals = errorAnimals.map(a => a.name).join(', ') + if (errorAnimals.length > 0) { return json({ - status: 'horse-error', + status: 'animal-error', submission, - message: listOfHorses, + message: listOfAnimals, } as const) } } @@ -149,10 +152,13 @@ export async function action({ request, params }: DataFunctionArgs) { const cleaningCrewReq = submission.value.cleaningCrewReq const lessonAssistantsReq = submission.value.lessonAssistantsReq const sideWalkersReq = submission.value.sideWalkersReq - const horseLeadersReq = submission.value.horseLeadersReq + const animalHandlersReq = submission.value.animalHandlersReq const isPrivate = submission.value.isPrivate + const existingEvent = await prisma.event.findFirst({ where: { id: params.eventId, orgId } }) + if (!existingEvent) throw new Response('not found', { status: 404 }) + const updatedEvent = await prisma.event.update({ where: { id: params.eventId, @@ -164,13 +170,13 @@ export async function action({ request, params }: DataFunctionArgs) { instructors: { set: instructorData, }, - horses: { - set: horseIds ?? [], + animals: { + set: animalIds ?? [], }, cleaningCrewReq, lessonAssistantsReq, sideWalkersReq, - horseLeadersReq, + animalHandlersReq, isPrivate, }, }) @@ -224,10 +230,10 @@ export default function EventEditor() { ? format(new Date(data.event.start), "yyyy-MM-dd'T'HH:mm:00") : '', duration: defaultDuration, - horses: data.event?.horses, + animals: data.event?.animals, instructor: data.event?.instructors[0], cleaningCrewReq: data.event?.cleaningCrewReq, - horseLeadersReq: data.event?.horseLeadersReq, + animalHandlersReq: data.event?.animalHandlersReq, sideWalkersReq: data.event?.sideWalkersReq, lessonAssistantsReq: data.event?.lessonAssistantsReq, }, @@ -238,11 +244,11 @@ export default function EventEditor() { if (!actionData) { return } - if (actionData.status === 'horse-error') { + if (actionData.status === 'animal-error') { toast({ variant: 'destructive', title: - 'The following horses are scheduled for cooldown on the selected dates:', + 'The following animals are scheduled for cooldown on the selected dates:', description: actionData.message, }) } @@ -298,12 +304,12 @@ export default function EventEditor() {
- - Animals +
@@ -354,14 +360,14 @@ export default function EventEditor() { { @@ -62,7 +65,7 @@ export const action = async ({ request, params }: ActionArgs) => { const formData = await request.formData() const submission = parse(formData, { schema: () => { - return assignHorseSchema + return assignAnimalSchema }, }) @@ -79,10 +82,10 @@ export const action = async ({ request, params }: ActionArgs) => { invariant(params.eventId, 'Expected params.eventId') const eventId = params.eventId const userId = submission.value.user - const horseId = submission.value.horse + const animalId = submission.value.animal - if (horseId === 'none') { - await prisma.horseAssignment.delete({ + if (animalId === 'none') { + await prisma.animalAssignment.delete({ where: { eventId_userId: { eventId, @@ -101,7 +104,7 @@ export const action = async ({ request, params }: ActionArgs) => { ) } - await prisma.horseAssignment.upsert({ + await prisma.animalAssignment.upsert({ where: { eventId_userId: { eventId, @@ -109,8 +112,8 @@ export const action = async ({ request, params }: ActionArgs) => { }, }, update: { - horse: { - connect: { id: horseId }, + animal: { + connect: { id: animalId }, }, }, create: { @@ -120,8 +123,8 @@ export const action = async ({ request, params }: ActionArgs) => { volunteer: { connect: { id: userId }, }, - horse: { - connect: { id: horseId }, + animal: { + connect: { id: animalId }, }, }, }) @@ -188,20 +191,20 @@ export default function () { ) })}
-
Horses:
+
Animals:
- {event.horses.map(horse => { + {event.animals.map(animal => { return ( - +
{horse.name} -
{horse.name}
+
{animal.name}
-
+ ) })}
@@ -267,7 +270,7 @@ export function VolunteerSection({ interface VolunteerListItemProps { user?: UserData event: CalEvent & { - horseAssignments: HorseAssignment[] + animalAssignments: AnimalAssignment[] } } @@ -290,20 +293,20 @@ function VolunteerListItem({ const isPlaceholder = user.id === 'placeholder' const assignmentFetcher = useFetcher() - let assignedHorseId = 'none' - let assignedHorseImageId = '' - let assignedHorse = null - for (const assignment of event.horseAssignments) { + let assignedAnimalId = 'none' + let assignedAnimalImageId = '' + let assignedAnimal = null + for (const assignment of event.animalAssignments) { if (assignment.userId === user.id) { - assignedHorseId = assignment.horseId + assignedAnimalId = assignment.animalId } } - if (assignedHorseId != 'none') { - for (const horse of event.horses) { - if (horse.id === assignedHorseId && horse.imageId) { - assignedHorseImageId = horse.imageId - assignedHorse = horse + if (assignedAnimalId != 'none') { + for (const animal of event.animals) { + if (animal.id === assignedAnimalId && animal.imageId) { + assignedAnimalImageId = animal.imageId + assignedAnimal = animal } } } @@ -317,7 +320,7 @@ function VolunteerListItem({ assignmentFetcher.submit( { user: user.id, - horse: target.value, + animal: target.value, }, { method: 'post' }, ) @@ -357,19 +360,19 @@ function VolunteerListItem({
{isSubmitting ? ( ๐ŸŒ€ - ) : assignedHorse ? ( - + ) : assignedAnimal ? ( + horse - + ) : ( horse )} @@ -377,14 +380,14 @@ function VolunteerListItem({ @@ -394,31 +397,31 @@ function VolunteerListItem({ ) } -interface HorseInfoPopoverProps { +interface AnimalInfoPopoverProps { children: React.ReactNode - horse: HorseData + animal: AnimalData } -function HorseInfoPopover({ children, horse }: HorseInfoPopoverProps) { +function AnimalInfoPopover({ children, animal }: AnimalInfoPopoverProps) { return ( {children} -
{horse.name}
+
{animal.name}
horse
Status: - {horse.status} + {animal.status}
Notes: - {horse.notes} + {animal.notes}
@@ -448,8 +451,8 @@ function VolunteerInfoPopover({
horse
Age: diff --git a/app/routes/calendar+/index.tsx b/app/routes/calendar+/index.tsx index b6b57e0..cdf2944 100644 --- a/app/routes/calendar+/index.tsx +++ b/app/routes/calendar+/index.tsx @@ -1,24 +1,19 @@ import { Link, Form, json, useLoaderData, useActionData } from '~/remix.ts' import type { ActionArgs, LoaderArgs } from '~/remix.ts' -import { Calendar, dateFnsLocalizer } from 'react-big-calendar' import format from 'date-fns/format/index.js' import parse from 'date-fns/parse/index.js' -import startOfWeek from 'date-fns/startOfWeek/index.js' -import getDay from 'date-fns/getDay/index.js' -import enUS from 'date-fns/locale/en-US/index.js' -import '~/styles/react-big-calendar.css' import { Icon } from '~/components/ui/icon.tsx' import { volunteerTypes, type UserData, - type HorseData, + type AnimalData, type EventWithVolunteers, } from '~/data.ts' -import { useMemo, useState } from 'react' +import { useState } from 'react' import { prisma } from '~/utils/db.server.ts' -import { requireUserId } from '~/utils/auth.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { useUser } from '~/utils/user.ts' import { Button } from '~/components/ui/button.tsx' import { @@ -44,7 +39,7 @@ import { import { parse as formParse } from '@conform-to/zod' import { z } from 'zod' -import { HorseListbox, InstructorListbox } from '~/components/listboxes.tsx' +import { AnimalListbox, InstructorListbox } from '~/components/listboxes.tsx' import { addMinutes, isAfter } from 'date-fns' import { useFetcher, useFormAction, useNavigation } from '@remix-run/react' import { useResetCallback } from '~/utils/misc.ts' @@ -68,41 +63,30 @@ import { Separator } from '~/components/ui/separator.tsx' import { CheckboxField, Field, DatePickerField } from '~/components/forms.tsx' import { checkboxSchema, optionalDateSchema } from '~/utils/zod-extensions.ts' import { - horseDateConflicts, - renderHorseConflictMessage, + animalDateConflicts, + renderAnimalConflictMessage, } from '~/utils/cooldown-functions.ts' import { EventAgenda } from '~/components/EventAgenda.tsx' -const locales = { - 'en-US': enUS, -} - -const localizer = dateFnsLocalizer({ - format, - parse, - startOfWeek, - getDay, - locales, -}) export const loader = async ({ request }: LoaderArgs) => { - await requireUserId(request) + const { orgId } = await requireOrgMember(request) const isAdmin = await userHasAdminPermissions(request) const instructors = await prisma.user.findMany({ - where: { roles: { some: { name: 'instructor' } } }, + where: { orgId, roles: { some: { name: 'instructor' } } }, }) - let eventsWhere: { isPrivate?: boolean } = { isPrivate: false } + let eventsWhere: { orgId: string; isPrivate?: boolean } = { orgId, isPrivate: false } if (isAdmin) delete eventsWhere.isPrivate let events = await prisma.event.findMany({ where: eventsWhere, include: { - horses: true, + animals: true, instructors: true, cleaningCrew: true, lessonAssistants: true, sideWalkers: true, - horseLeaders: true, + animalHandlers: true, }, }) @@ -118,12 +102,12 @@ export const loader = async ({ request }: LoaderArgs) => { return json({ events, - horses: await prisma.horse.findMany(), + animals: await prisma.animal.findMany({ where: { orgId } }), instructors, }) } -const horseSchema = z.object({ +const animalSchema = z.object({ id: z.string(), name: z.string(), cooldownStartDate: optionalDateSchema, @@ -147,17 +131,18 @@ const createEventSchema = z.object({ .string() .regex(new RegExp(/^\d{2}:\d{2}$/g), 'Invalid start time'), duration: z.coerce.number().gt(0), - horses: z.array(horseSchema).optional(), + animals: z.array(animalSchema).optional(), instructor: instructorSchema, cleaningCrewReq: z.coerce.number().gt(-1), lessonAssistantsReq: z.coerce.number().gt(-1), sideWalkersReq: z.coerce.number().gt(-1), - horseLeadersReq: z.coerce.number().gt(-1), + animalHandlersReq: z.coerce.number().gt(-1), isPrivate: checkboxSchema(), }) export async function action({ request }: ActionArgs) { await requireAdmin(request) + const { orgId } = await requireOrgMember(request) const body = await request.formData() const submission = formParse(body, { schema: () => { @@ -182,7 +167,7 @@ export async function action({ request }: ActionArgs) { const cleaningCrewReq = submission.value.cleaningCrewReq const lessonAssistantsReq = submission.value.lessonAssistantsReq const sideWalkersReq = submission.value.sideWalkersReq - const horseLeadersReq = submission.value.horseLeadersReq + const animalHandlersReq = submission.value.animalHandlersReq const isPrivate = submission.value.isPrivate const dateTimesArray = datesArray.map(date => { @@ -192,27 +177,27 @@ export async function action({ request }: ActionArgs) { return { start, end } }) - // Check that horses selected are not in cooldown period - const horses = submission.value.horses - if (horses) { - interface horseDateConflict { + // Check that animals selected are not in cooldown period + const animals = submission.value.animals + if (animals) { + interface animalDateConflict { name: String conflictingDatesArr: Array } - let errorHorseArr: Array = [] - horses.forEach(horse => { - const conflicts = horseDateConflicts( - horse, + let errorAnimalArr: Array = [] + animals.forEach(animal => { + const conflicts = animalDateConflicts( + animal, dateTimesArray.map(date => date.start), ) - if (conflicts) errorHorseArr.push(conflicts) + if (conflicts) errorAnimalArr.push(conflicts) }) - const message = renderHorseConflictMessage(errorHorseArr) + const message = renderAnimalConflictMessage(errorAnimalArr) - if (errorHorseArr.length > 0) { + if (errorAnimalArr.length > 0) { return json({ - status: 'horse-error', + status: 'animal-error', submission, message, } as const) @@ -224,7 +209,7 @@ export async function action({ request }: ActionArgs) { if (instructorId) { instructorData = [{ id: instructorId }] } - const horseIds = submission.value.horses?.map(e => { + const animalIds = submission.value.animals?.map(e => { return { id: e.id } }) @@ -236,16 +221,17 @@ export async function action({ request }: ActionArgs) { title, start: dateTime.start, end: dateTime.end, + orgId, instructors: { connect: instructorData, }, - horses: { - connect: horseIds ?? [], + animals: { + connect: animalIds ?? [], }, cleaningCrewReq, lessonAssistantsReq, sideWalkersReq, - horseLeadersReq, + animalHandlersReq, isPrivate, }, }), @@ -265,7 +251,7 @@ export async function action({ request }: ActionArgs) { export default function Schedule() { const data = useLoaderData() var events = data.events - const horses = data.horses + const animals = data.animals const instructors = data.instructors const user = useUser() const userIsAdmin = user.roles.find(role => role.name === 'admin') @@ -279,7 +265,7 @@ export default function Schedule() { event.start.valueOf() > new Date().valueOf() && (event.cleaningCrewReq > event.cleaningCrew.length || event.lessonAssistantsReq > event.lessonAssistants.length || - event.horseLeadersReq > event.horseLeaders.length || + event.animalHandlersReq > event.animalHandlers.length || event.sideWalkersReq > event.sideWalkers.length) ) }) @@ -289,53 +275,57 @@ export default function Schedule() { setRegisterOpen(!registerOpen) } - const components = useMemo( - () => ({ - agenda: { - event: EventAgenda, - }, - }), - [], - ) + const upcomingCount = events.filter(e => e.start.valueOf() > new Date().valueOf()).length + const needsHelpCount = eventsThatNeedHelp.length return ( -
-

Calendar

-
- setFilterFlag(!filterFlag)} - id="filter" - /> - +
+ {/* Page header */} +
+
+

Calendar

+

+ Schedule events and manage volunteer assignments +

+
+ {userIsAdmin ? ( + + ) : null}
- {userIsAdmin ? ( - - ) : null} + {/* Stat cards */} +
+
+

+ Upcoming Events +

+

{upcomingCount}

+
+
+

+ Need Volunteers +

+

0 ? 'text-amber-600' : 'text-green-600'}`}> + {needsHelpCount} +

+
+
+ setFilterFlag(!filterFlag)} + id="filter" + /> + +
+
-
- + - `Cleaning Crew: ${event.cleaningCrew.length} / ${event.cleaningCrewReq}\nSidewalkers: ${event.sideWalkers.length} / ${event.sideWalkersReq}\nLesson Assistants: ${event.lessonAssistants.length} / ${event.lessonAssistantsReq}\nHorse Leaders: ${event.horseLeaders.length} / ${event.horseLeadersReq}` - } - startAccessor="start" - endAccessor="end" onSelectEvent={handleSelectEvent} - style={{ - height: '95%', - width: '95%', - backgroundColor: 'white', - color: 'black', - padding: 20, - borderRadius: '1.5rem', - }} - components={components} - defaultView="agenda" />
@@ -349,6 +339,83 @@ export default function Schedule() { ) } +interface CustomAgendaProps { + events: EventWithVolunteers[] + onSelectEvent: (event: EventWithVolunteers) => void +} + +function CustomAgenda({ events, onSelectEvent }: CustomAgendaProps) { + // Group events by date, sorted by start time + const grouped = (() => { + const sorted = [...events].sort( + (a, b) => new Date(a.start).valueOf() - new Date(b.start).valueOf(), + ) + const groups: { date: Date; key: string; events: EventWithVolunteers[] }[] = + [] + for (const event of sorted) { + const key = format(new Date(event.start), 'yyyy-MM-dd') + const existing = groups.find(g => g.key === key) + if (existing) { + existing.events.push(event) + } else { + groups.push({ date: new Date(event.start), key, events: [event] }) + } + } + return groups + })() + + if (grouped.length === 0) { + return ( +
+

No upcoming events

+
+ ) + } + + return ( +
+ {/* Column headers */} +
+ Date + Time + Event +
+ {/* Scrollable body */} +
+ {grouped.map(({ date, key, events: dayEvents }) => ( +
+ {dayEvents.map((event, idx) => ( +
onSelectEvent(event)} + className={`flex cursor-pointer items-start hover:bg-muted/20 ${ + idx < dayEvents.length - 1 + ? 'border-b border-border/40' + : '' + }`} + > + {/* Date โ€” only on first event of the group */} +
+ {idx === 0 ? format(date, 'EEE MMM d') : ''} +
+ {/* Time */} +
+ {format(new Date(event.start), 'h:mm aaa')} โ€“{' '} + {format(new Date(event.end), 'h:mm aaa')} +
+ {/* Event content */} +
+ +
+
+ ))} +
+ ))} +
+
+ ) +} + interface RegistrationProps { events: EventWithVolunteers[] selectedEventId: string @@ -360,8 +427,8 @@ function RegistrationDialogue({ selectedEventId, events }: RegistrationProps) { const userIsAdmin = user.roles.find(role => role.name === 'admin') const userIsLessonAssistant = user.roles.find(role => role.name === 'lessonAssistant') != undefined - const userIsHorseLeader = - user.roles.find(role => role.name === 'horseLeader') != undefined + const userIsAnimalHandler = + user.roles.find(role => role.name === 'animalHandler') != undefined const isSubmitting = registrationFetcher.state === 'submitting' @@ -390,7 +457,7 @@ function RegistrationDialogue({ selectedEventId, events }: RegistrationProps) { const helpNeeded = calEvent.cleaningCrewReq > calEvent.cleaningCrew.length || calEvent.lessonAssistantsReq > calEvent.lessonAssistants.length || - calEvent.horseLeadersReq > calEvent.horseLeaders.length || + calEvent.animalHandlersReq > calEvent.animalHandlers.length || calEvent.sideWalkersReq > calEvent.sideWalkers.length const now = new Date() @@ -461,8 +528,8 @@ function RegistrationDialogue({ selectedEventId, events }: RegistrationProps) { let hasPermissions = true if (volunteerType.field == 'lessonAssistants') { hasPermissions = userIsLessonAssistant - } else if (volunteerType.field == 'horseLeaders') { - hasPermissions = userIsHorseLeader + } else if (volunteerType.field == 'animalHandlers') { + hasPermissions = userIsAnimalHandler } return ( @@ -563,11 +630,11 @@ function RegistrationDialogue({ selectedEventId, events }: RegistrationProps) { } interface CreateEventDialogProps { - horses: HorseData[] + animals: AnimalData[] instructors: UserData[] } -function CreateEventDialog({ horses, instructors }: CreateEventDialogProps) { +function CreateEventDialog({ animals, instructors }: CreateEventDialogProps) { const [open, setOpen] = useState(false) return ( @@ -588,7 +655,7 @@ function CreateEventDialog({ horses, instructors }: CreateEventDialogProps) { setOpen(false)} /> @@ -607,7 +674,7 @@ interface EventFormProps extends CreateEventDialogProps { } function CreateEventForm({ - horses, + animals, instructors, doneCallback, }: EventFormProps) { @@ -627,7 +694,7 @@ function CreateEventForm({ lastSubmission: actionData?.submission, defaultValue: { cleaningCrewReq: 0, - horseLeadersReq: 0, + animalHandlersReq: 0, sideWalkersReq: 0, lessonAssistantsReq: 0, }, @@ -646,11 +713,11 @@ function CreateEventForm({ if (doneCallback) { doneCallback() } - } else if (actionData.status === 'horse-error') { + } else if (actionData.status === 'animal-error') { toast({ variant: 'destructive', title: - 'The following horses are scheduled for cooldown on the selected dates:', + 'The following animals are scheduled for cooldown on the selected dates:', description: actionData.message, }) } else { @@ -710,11 +777,11 @@ function CreateEventForm({
- - Animals +
@@ -763,15 +830,15 @@ function CreateEventForm({ +

No Organization Found

+

+ Your account is not associated with any organization. Ask your + organization admin to add you, or register a new organization. +

+
+ + +
+
+ ) +} diff --git a/app/routes/resources+/event-register.tsx b/app/routes/resources+/event-register.tsx index b88cd54..ee1d162 100644 --- a/app/routes/resources+/event-register.tsx +++ b/app/routes/resources+/event-register.tsx @@ -1,5 +1,5 @@ import { z } from 'zod' -import { requireUserId } from '~/utils/auth.server.ts' +import { requireOrgMember } from '~/utils/auth.server.ts' import { parse } from '@conform-to/zod' import { json, type DataFunctionArgs } from '~/remix.ts' import { prisma } from '~/utils/db.server.ts' @@ -20,7 +20,7 @@ const volunteerTypes = [ 'cleaningCrew', 'lessonAssistants', 'sideWalkers', - 'horseLeaders', + 'animalHandlers', ] as const const EventRegistrationSchema = z.object({ @@ -30,7 +30,7 @@ const EventRegistrationSchema = z.object({ }) export async function action({ request }: DataFunctionArgs) { - const userId = await requireUserId(request) + const { userId, orgId } = await requireOrgMember(request) const formData = await request.formData() const submission = parse(formData, { schema: EventRegistrationSchema, @@ -55,6 +55,10 @@ export async function action({ request }: DataFunctionArgs) { } if (submission.value._action === 'unregister') { + // Verify the event belongs to the user's org before mutating + const eventCheck = await prisma.event.findFirst({ where: { id: submission.value.eventId, orgId } }) + if (!eventCheck) throw json({ error: 'Event not found' }, { status: 404 }) + const event = await prisma.event.update({ where: { id: submission.value.eventId, @@ -88,6 +92,7 @@ export async function action({ request }: DataFunctionArgs) { event: event, role: submission.value.role, action: 'unregister', + orgId, }) return json( { @@ -103,12 +108,16 @@ export async function action({ request }: DataFunctionArgs) { throw json({ error: 'Missing permissions' }, { status: 403 }) } } - if (submission.value.role == 'horseLeaders') { - if (!user.roles.find(role => role.name === 'horseLeader')) { + if (submission.value.role == 'animalHandlers') { + if (!user.roles.find(role => role.name === 'animalHandler')) { throw json({ error: 'Missing permissions' }, { status: 403 }) } } + // Verify the event belongs to the user's org before mutating + const eventOrgCheck = await prisma.event.findFirst({ where: { id: submission.value.eventId, orgId } }) + if (!eventOrgCheck) throw json({ error: 'Event not found' }, { status: 404 }) + const event = await prisma.event.update({ where: { id: submission.value.eventId, @@ -162,6 +171,7 @@ export async function action({ request }: DataFunctionArgs) { event: event, role: submission.value.role, action: 'register', + orgId, }) return json( @@ -221,14 +231,16 @@ async function notifyAdmins({ role, action, user, + orgId, }: { event: Event - role: 'cleaningCrew' | 'lessonAssistants' | 'sideWalkers' | 'horseLeaders' + role: 'cleaningCrew' | 'lessonAssistants' | 'sideWalkers' | 'animalHandlers' action: 'register' | 'unregister' user: User + orgId: string }) { const admins = await prisma.user.findMany({ - where: { roles: { some: { name: 'admin' } } }, + where: { orgId, roles: { some: { name: 'admin' } } }, }) for (const admin of admins) { diff --git a/app/routes/resources+/login.tsx b/app/routes/resources+/login.tsx index 4bddac2..df5d8a5 100644 --- a/app/routes/resources+/login.tsx +++ b/app/routes/resources+/login.tsx @@ -11,7 +11,7 @@ import { authenticator } from '~/utils/auth.server.ts' import { prisma } from '~/utils/db.server.ts' import { CheckboxField, ErrorList, Field } from '~/components/forms.tsx' import { commitSession, getSession } from '~/utils/session.server.ts' -import { passwordSchema, usernameSchema } from '~/utils/user-validation.ts' +import { passwordSchema } from '~/utils/user-validation.ts' import { checkboxSchema } from '~/utils/zod-extensions.ts' import { twoFAVerificationType } from '../settings+/profile.two-factor.tsx' import { unverifiedSessionKey } from './verify.tsx' @@ -19,8 +19,10 @@ import { StatusButton } from '~/components/ui/status-button.tsx' const ROUTE_PATH = '/resources/login' +const usernameOrEmailSchema = z.string().min(3).max(254) + export const loginFormSchema = z.object({ - username: usernameSchema, + username: usernameOrEmailSchema, password: passwordSchema, redirectTo: z.string().optional(), remember: checkboxSchema(), @@ -80,6 +82,13 @@ export async function action({ request }: DataFunctionArgs) { select: { id: true }, }) + const userWithRoles = await prisma.user.findUnique({ + where: { id: session.userId }, + select: { roles: { select: { name: true } } }, + }) + const isAdmin = userWithRoles?.roles.some( + r => r.name === 'admin' || r.name === 'superAdmin', + ) const cookieSession = await getSession(request.headers.get('cookie')) const keyToSet = user2FA ? unverifiedSessionKey : authenticator.sessionKey cookieSession.set(keyToSet, sessionId) @@ -91,10 +100,13 @@ export async function action({ request }: DataFunctionArgs) { }), }, } - if (user2FA || !redirectTo) { + if (user2FA) { return json({ status: 'success', submission } as const, responseInit) } else { - throw redirect(safeRedirect(redirectTo), responseInit) + const finalRedirect = isAdmin && (!redirectTo || redirectTo === '/') + ? '/admin' + : safeRedirect(redirectTo || '/') + throw redirect(finalRedirect, responseInit) } } @@ -128,7 +140,7 @@ export function InlineLogin({ {...form.props} > diff --git a/app/routes/resources+/registration-emails.server.tsx b/app/routes/resources+/registration-emails.server.tsx index 2ab47cf..b355e55 100644 --- a/app/routes/resources+/registration-emails.server.tsx +++ b/app/routes/resources+/registration-emails.server.tsx @@ -9,7 +9,7 @@ export function RegistrationEmail({ role, }: { event: Event - role: 'cleaningCrew' | 'lessonAssistants' | 'sideWalkers' | 'horseLeaders' + role: 'cleaningCrew' | 'lessonAssistants' | 'sideWalkers' | 'animalHandlers' }) { let roleName for (let v of volunteerTypes) { @@ -49,7 +49,7 @@ export function RegistrationNoticeForAdmins({ user, }: { event: Event - role: 'cleaningCrew' | 'lessonAssistants' | 'sideWalkers' | 'horseLeaders', + role: 'cleaningCrew' | 'lessonAssistants' | 'sideWalkers' | 'animalHandlers', action: 'register' | 'unregister'; user: User; }) { diff --git a/app/routes/resources+/theme/index.tsx b/app/routes/resources+/theme/index.tsx index a8ba00d..5ba1b83 100644 --- a/app/routes/resources+/theme/index.tsx +++ b/app/routes/resources+/theme/index.tsx @@ -79,9 +79,8 @@ export function ThemeSwitch({ }, }) - const mode = userPreference ?? 'system' - const nextMode = - mode === 'system' ? 'light' : mode === 'light' ? 'dark' : 'system' + const mode = userPreference ?? 'light' + const nextMode = mode === 'light' ? 'dark' : 'light' const modeLabel = { light: ( @@ -93,11 +92,6 @@ export function ThemeSwitch({ Dark ), - system: ( - - System - - ), } return ( diff --git a/app/routes/resources+/unregistration-emails.server.tsx b/app/routes/resources+/unregistration-emails.server.tsx index 18e0ede..34ede74 100644 --- a/app/routes/resources+/unregistration-emails.server.tsx +++ b/app/routes/resources+/unregistration-emails.server.tsx @@ -9,7 +9,7 @@ export function UnregistrationEmail({ role, }: { event: Event - role: 'cleaningCrew' | 'lessonAssistants' | 'sideWalkers' | 'horseLeaders' + role: 'cleaningCrew' | 'lessonAssistants' | 'sideWalkers' | 'animalHandlers' }) { let roleName for (let v of volunteerTypes) { diff --git a/app/routes/settings+/profile.tsx b/app/routes/settings+/profile.tsx index 6275f18..e2de1b9 100644 --- a/app/routes/settings+/profile.tsx +++ b/app/routes/settings+/profile.tsx @@ -382,7 +382,7 @@ export default function EditUserProfile() { className="col-span-6 sm:col-span-3" labelProps={{ htmlFor: fields.yearsOfExperience.id, - children: 'Years of experience with horses', + children: 'Years of experience with animals', }} inputProps={{ ...conform.input(fields.yearsOfExperience), diff --git a/app/routes/super-admin+/orgs.tsx b/app/routes/super-admin+/orgs.tsx new file mode 100644 index 0000000..998da4e --- /dev/null +++ b/app/routes/super-admin+/orgs.tsx @@ -0,0 +1,76 @@ +import { json, type DataFunctionArgs } from '@remix-run/node' +import { useLoaderData } from '@remix-run/react' +import { requireSuperAdmin } from '~/utils/permissions.server.ts' +import { prisma } from '~/utils/db.server.ts' +import { format } from 'date-fns' + +export const loader = async ({ request }: DataFunctionArgs) => { + await requireSuperAdmin(request) + const orgs = await prisma.organization.findMany({ + include: { + _count: { select: { users: true, animals: true, events: true } }, + }, + orderBy: { createdAt: 'desc' }, + }) + return json({ orgs }) +} + +export default function SuperAdminOrgs() { + const { orgs } = useLoaderData() + + return ( +
+

All Organizations

+
+ + + + + + + + + + + + + + + {orgs.map(org => ( + + + + + + + + + + + ))} + +
NameSlugAnimal TypeUsersAnimalsEventsStatusCreated
{org.name}{org.slug}{org.animalType}{org._count.users}{org._count.animals}{org._count.events} + + {org.isActive ? 'Active' : 'Inactive'} + + + {format(new Date(org.createdAt), 'MMM d, yyyy')} +
+ {orgs.length === 0 && ( +
+ No organizations found. +
+ )} +
+

+ {orgs.length} organization{orgs.length !== 1 ? 's' : ''} total +

+
+ ) +} diff --git a/app/styles/react-big-calendar.css b/app/styles/react-big-calendar.css index be280d6..b3527fa 100644 --- a/app/styles/react-big-calendar.css +++ b/app/styles/react-big-calendar.css @@ -498,6 +498,7 @@ button.rbc-input::-moz-focus-inner { .rbc-agenda-view table.rbc-agenda-table tbody > tr + tr { border-top: 1px solid #ddd; } + .rbc-agenda-view table.rbc-agenda-table thead > tr > th { padding: 3px 5px; text-align: left; @@ -829,3 +830,40 @@ button.rbc-input::-moz-focus-inner { } /*# sourceMappingURL=react-big-calendar.css.map */ + +/* Dark mode overrides */ +.dark .rbc-off-range { color: #666; } +.dark .rbc-off-range-bg { background: #1a1a2e; } +.dark .rbc-today { background-color: #1e3a5f; } +.dark .rbc-toolbar button { color: hsl(210 40% 98%); border-color: hsl(216 34% 30%); } +.dark .rbc-toolbar button:active, +.dark .rbc-toolbar button.rbc-active { background-color: hsl(216 34% 25%); border-color: hsl(216 34% 40%); } +.dark .rbc-toolbar button:focus, +.dark .rbc-toolbar button:hover { color: hsl(210 40% 98%); background-color: hsl(216 34% 22%); border-color: hsl(216 34% 35%); } +.dark .rbc-header { border-bottom-color: hsl(216 34% 17%); } +.dark .rbc-header + .rbc-header { border-left-color: hsl(216 34% 17%); } +.dark .rbc-month-view { border-color: hsl(216 34% 17%); } +.dark .rbc-month-row + .rbc-month-row { border-top-color: hsl(216 34% 17%); } +.dark .rbc-day-bg + .rbc-day-bg { border-left-color: hsl(216 34% 17%); } +.dark .rbc-agenda-view table.rbc-agenda-table { border-color: hsl(216 34% 17%); } +.dark .rbc-agenda-view table.rbc-agenda-table tbody > tr > td + td { border-left-color: hsl(216 34% 17%); } +.dark .rbc-agenda-view table.rbc-agenda-table tbody > tr + tr { border-top-color: hsl(216 34% 17%); } +.dark .rbc-agenda-view table.rbc-agenda-table thead > tr > th { border-bottom-color: hsl(216 34% 17%); } +.dark .rbc-time-view { border-color: hsl(216 34% 17%); } +.dark .rbc-time-header.rbc-overflowing { border-right-color: hsl(216 34% 17%); } +.dark .rbc-time-header > .rbc-row:first-child { border-bottom-color: hsl(216 34% 17%); } +.dark .rbc-time-header-content { border-left-color: hsl(216 34% 17%); } +.dark .rbc-time-content { border-top-color: hsl(216 34% 17%); } +.dark .rbc-time-content > * + * > * { border-left-color: hsl(216 34% 17%); } +.dark .rbc-timeslot-group { border-bottom-color: hsl(216 34% 17%); } +.dark .rbc-day-slot .rbc-time-slot { border-top-color: hsl(222 47% 14%); } +.dark .rbc-day-slot .rbc-event, +.dark .rbc-day-slot .rbc-background-event { border-color: hsl(210 60% 40%); } +.dark .rbc-time-view-resources .rbc-time-gutter, +.dark .rbc-time-view-resources .rbc-time-header-gutter { background-color: hsl(222.2 84% 4.9%); border-right-color: hsl(216 34% 17%); } +.dark .rbc-overlay { background-color: hsl(224 71% 4%); border-color: hsl(216 34% 17%); } +.dark .rbc-overlay-header { border-bottom-color: hsl(216 34% 17%); } +.dark .rbc-show-more { color: #58a6e8; } +.dark .rbc-show-more:hover, +.dark .rbc-show-more:focus { color: #82c0ff; } +.dark .rbc-time-view .rbc-allday-cell + .rbc-allday-cell { border-left-color: hsl(216 34% 17%); } diff --git a/app/styles/tailwind.css b/app/styles/tailwind.css index 615def7..a170c8b 100644 --- a/app/styles/tailwind.css +++ b/app/styles/tailwind.css @@ -38,6 +38,12 @@ --radius: 0.5rem; + --color-sidebar: 240 5% 96%; + --color-sidebar-foreground: 240 5.9% 10%; + --color-sidebar-border: 240 5.9% 90%; + --color-sidebar-active: 239 84.2% 67.1%; + --color-sidebar-active-foreground: 0 0% 100%; + --color-brand-primary: 217 91% 60%; --color-brand-primary-muted: 208 56% 44%; --color-brand-secondary: 40 100% 62%; @@ -93,6 +99,12 @@ --color-destructive-foreground: 0 85.7% 97.3%; --color-ring: 217 32.6% 17.5%; + + --color-sidebar: 240 5.9% 10%; + --color-sidebar-foreground: 210 40% 98%; + --color-sidebar-border: 240 3.7% 15.9%; + --color-sidebar-active: 239 84.2% 67.1%; + --color-sidebar-active-foreground: 0 0% 100%; } } diff --git a/app/utils/auth.server.ts b/app/utils/auth.server.ts index 1e24240..95360ce 100644 --- a/app/utils/auth.server.ts +++ b/app/utils/auth.server.ts @@ -49,6 +49,22 @@ authenticator.use( FormStrategy.name, ) +export async function requireOrgMember( + request: Request, + { redirectTo }: { redirectTo?: string | null } = {}, +) { + const userId = await requireUserId(request, { redirectTo }) + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { orgId: true }, + }) + if (!user?.orgId) { + // User exists but has no org โ€” send them to pick/create one + throw redirect('/org-setup') + } + return { userId, orgId: user.orgId } +} + export async function requireUserId( request: Request, { redirectTo }: { redirectTo?: string | null } = {}, @@ -161,11 +177,13 @@ export async function getPasswordHash(password: string) { } export async function verifyLogin( - username: User['username'], + usernameOrEmail: string, password: Password['hash'], ) { - const userWithPassword = await prisma.user.findUnique({ - where: { username }, + const userWithPassword = await prisma.user.findFirst({ + where: { + OR: [{ username: usernameOrEmail }, { email: usernameOrEmail }], + }, select: { id: true, password: { select: { hash: true } } }, }) @@ -182,6 +200,55 @@ export async function verifyLogin( return { id: userWithPassword.id } } +export async function signupOrg({ + orgName, + orgSlug, + animalType, + email, + username, + name, + password, +}: { + orgName: string + orgSlug: string + animalType: string + email: string + username: string + name: string + password: string +}) { + const hashedPassword = await getPasswordHash(password) + + const adminRole = await prisma.role.findFirst({ where: { name: 'admin' } }) + + const session = await prisma.session.create({ + data: { + expirationDate: new Date(Date.now() + SESSION_EXPIRATION_TIME), + user: { + create: { + email, + username, + name, + password: { + create: { hash: hashedPassword }, + }, + roles: adminRole ? { connect: { id: adminRole.id } } : undefined, + org: { + create: { + name: orgName, + slug: orgSlug, + animalType, + }, + }, + }, + }, + }, + select: { id: true, expirationDate: true, userId: true }, + }) + + return session +} + export async function verifySignupPassword(password: string) { const signUpPassword = await prisma.signupPassword.findFirst(); diff --git a/app/utils/cooldown-functions.ts b/app/utils/cooldown-functions.ts index 8744e49..bce1f50 100644 --- a/app/utils/cooldown-functions.ts +++ b/app/utils/cooldown-functions.ts @@ -1,42 +1,42 @@ import { add, format } from 'date-fns' -interface Horse { +interface Animal { id: String name: String cooldownStartDate?: Date | undefined cooldownEndDate?: Date | undefined } -export function isCooldownDateConflict(horse: Horse, date: Date) { - if (horse.cooldownStartDate && horse.cooldownEndDate) { +export function isCooldownDateConflict(animal: Animal, date: Date) { + if (animal.cooldownStartDate && animal.cooldownEndDate) { if ( - horse.cooldownStartDate <= date && - date < add(horse.cooldownEndDate, { days: 1 }) + animal.cooldownStartDate <= date && + date < add(animal.cooldownEndDate, { days: 1 }) ) return true } return false } -export function horseDateConflicts(horse: Horse, datesArr: Array) { +export function animalDateConflicts(animal: Animal, datesArr: Array) { let conflictingDatesArr = datesArr.filter(date => - isCooldownDateConflict(horse, date), + isCooldownDateConflict(animal, date), ) if (conflictingDatesArr.length > 0) { - return { name: horse.name, conflictingDatesArr } + return { name: animal.name, conflictingDatesArr } } else return null } -export function renderHorseConflictMessage( - horseArr: Array<{ name: String; conflictingDatesArr: Array }>, +export function renderAnimalConflictMessage( + animalArr: Array<{ name: String; conflictingDatesArr: Array }>, ) { let message = '' - horseArr.forEach(horse => { - const datesString = horse.conflictingDatesArr - .sort((a:Date, b:Date) => a.valueOf() - b.valueOf()) + animalArr.forEach(animal => { + const datesString = animal.conflictingDatesArr + .sort((a: Date, b: Date) => a.valueOf() - b.valueOf()) .map(date => format(date, 'PP')) .join(', ') - message = message + `${horse.name} (${datesString}), ` + message = message + `${animal.name} (${datesString}), ` }) - return message.slice(0, -2); + return message.slice(0, -2) } diff --git a/app/utils/misc.ts b/app/utils/misc.ts index c54e7d9..3035d52 100644 --- a/app/utils/misc.ts +++ b/app/utils/misc.ts @@ -6,8 +6,8 @@ export function getUserImgSrc(imageId?: string | null) { return imageId ? `/resources/file/${imageId}` : `/img/user.png` } -export function getHorseImgSrc(imageId?: string | null) { - return imageId ? `/resources/file/${imageId}` : `/img/horse.png` +export function getAnimalImgSrc(imageId?: string | null) { + return imageId ? `/resources/file/${imageId}` : `/img/animal.png` } export function getErrorMessage(error: unknown) { diff --git a/app/utils/permissions.server.ts b/app/utils/permissions.server.ts index 0d4d8b4..c3d368c 100644 --- a/app/utils/permissions.server.ts +++ b/app/utils/permissions.server.ts @@ -34,3 +34,11 @@ export async function userHasPermissions(name: string, request: Request) { export async function userHasAdminPermissions(request: Request) { return userHasPermissions('admin', request) } + +export async function requireSuperAdmin(request: Request) { + return requireUserWithPermission('superAdmin', request) +} + +export async function userIsSuperAdmin(request: Request) { + return userHasPermissions('superAdmin', request) +} diff --git a/package-lock.json b/package-lock.json index 6275e1b..04794a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2109,425 +2109,782 @@ "get-tsconfig": "^4.4.0" } }, - "node_modules/@esbuild/darwin-arm64": { + "node_modules/@esbuild/android-arm": { "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], "engines": { "node": ">=12" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.2.0", + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "license": "Apache-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.4.0", + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.0.3", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "(MIT OR CC0-1.0)", + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/@eslint/js": { - "version": "8.43.0", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/@faker-js/faker": { - "version": "8.0.2", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0", - "npm": ">=6.14.13" + "node": ">=12" } }, - "node_modules/@floating-ui/core": { - "version": "1.2.6", - "license": "MIT" - }, - "node_modules/@floating-ui/dom": { - "version": "1.2.8", + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.2.6" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.0.0", + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.2.7" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@headlessui/react": { - "version": "1.7.15", "license": "MIT", - "dependencies": { - "client-only": "^0.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" + "node": ">=12" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.10.0" + "node": ">=12" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=12" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/@jest/expect-utils": { - "version": "29.4.3", + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "jest-get-type": "^29.4.3" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" } }, - "node_modules/@jest/schemas": { - "version": "29.4.3", + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.2.0", + "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.25.16" + "eslint-visitor-keys": "^3.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@jest/types": { - "version": "29.4.3", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.4.0", "dev": true, "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.4.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", + "node_modules/@eslint/eslintrc": { + "version": "2.0.3", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.20.0", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "type-fest": "^0.20.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.43.0", + "dev": true, "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", + "node_modules/@faker-js/faker": { + "version": "8.0.2", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", + "node_modules/@floating-ui/core": { + "version": "1.2.6", "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", + "node_modules/@floating-ui/dom": { + "version": "1.2.8", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@floating-ui/core": "^1.2.6" } }, - "node_modules/@jspm/core": { - "version": "2.0.1", - "dev": true - }, - "node_modules/@mswjs/cookies": { - "version": "0.2.2", - "dev": true, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.0", "license": "MIT", "dependencies": { - "@types/set-cookie-parser": "^2.4.0", - "set-cookie-parser": "^2.4.6" + "@floating-ui/dom": "^1.2.7" }, - "engines": { - "node": ">=14" + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@mswjs/interceptors": { - "version": "0.17.8", + "node_modules/@gar/promisify": { + "version": "1.1.3", "dev": true, + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "1.7.15", "license": "MIT", "dependencies": { - "@open-draft/until": "^1.0.3", - "@types/debug": "^4.1.7", - "@xmldom/xmldom": "^0.8.3", - "debug": "^4.3.3", - "headers-polyfill": "^3.1.0", - "outvariant": "^1.2.1", - "strict-event-emitter": "^0.2.4", - "web-encoding": "^1.1.5" + "client-only": "^0.0.1" }, "engines": { - "node": ">=14" + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" } }, - "node_modules/@mswjs/interceptors/node_modules/strict-event-emitter": { - "version": "0.2.8", + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "events": "^3.3.0" + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" } }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", "dev": true, - "license": "MIT", - "dependencies": { - "eslint-scope": "5.1.1" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jspm/core": { + "version": "2.0.1", + "dev": true + }, + "node_modules/@mswjs/cookies": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.0", + "set-cookie-parser": "^2.4.6" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.17.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/until": "^1.0.3", + "@types/debug": "^4.1.7", + "@xmldom/xmldom": "^0.8.3", + "debug": "^4.3.3", + "headers-polyfill": "^3.1.0", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.2.4", + "web-encoding": "^1.1.5" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@mswjs/interceptors/node_modules/strict-event-emitter": { + "version": "0.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "events": "^3.3.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -3814,975 +4171,1332 @@ } } }, - "node_modules/@remix-run/dev/node_modules/@esbuild/darwin-arm64": { + "node_modules/@remix-run/dev/node_modules/@esbuild/android-arm": { "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz", + "integrity": "sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], "engines": { "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/brace-expansion": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "node_modules/@remix-run/dev/node_modules/@esbuild/android-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz", + "integrity": "sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/chalk": { - "version": "4.1.2", + "node_modules/@remix-run/dev/node_modules/@esbuild/android-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz", + "integrity": "sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/esbuild": { + "node_modules/@remix-run/dev/node_modules/@esbuild/darwin-arm64": { "version": "0.17.6", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.17.6", - "@esbuild/android-arm64": "0.17.6", - "@esbuild/android-x64": "0.17.6", - "@esbuild/darwin-arm64": "0.17.6", - "@esbuild/darwin-x64": "0.17.6", - "@esbuild/freebsd-arm64": "0.17.6", - "@esbuild/freebsd-x64": "0.17.6", - "@esbuild/linux-arm": "0.17.6", - "@esbuild/linux-arm64": "0.17.6", - "@esbuild/linux-ia32": "0.17.6", - "@esbuild/linux-loong64": "0.17.6", - "@esbuild/linux-mips64el": "0.17.6", - "@esbuild/linux-ppc64": "0.17.6", - "@esbuild/linux-riscv64": "0.17.6", - "@esbuild/linux-s390x": "0.17.6", - "@esbuild/linux-x64": "0.17.6", - "@esbuild/netbsd-x64": "0.17.6", - "@esbuild/openbsd-x64": "0.17.6", - "@esbuild/sunos-x64": "0.17.6", - "@esbuild/win32-arm64": "0.17.6", - "@esbuild/win32-ia32": "0.17.6", - "@esbuild/win32-x64": "0.17.6" } }, - "node_modules/@remix-run/dev/node_modules/execa": { - "version": "5.1.1", + "node_modules/@remix-run/dev/node_modules/@esbuild/darwin-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz", + "integrity": "sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/fs-extra": { - "version": "10.1.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz", + "integrity": "sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/get-port": { - "version": "5.1.1", + "node_modules/@remix-run/dev/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz", + "integrity": "sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/human-signals": { - "version": "2.1.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-arm": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz", + "integrity": "sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10.17.0" + "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/is-stream": { - "version": "2.0.1", + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz", + "integrity": "sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/minimatch": { - "version": "9.0.1", + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz", + "integrity": "sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/npm-run-path": { - "version": "4.0.1", + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-loong64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz", + "integrity": "sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/pidtree": { - "version": "0.6.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz", + "integrity": "sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10" + "node": ">=12" } }, - "node_modules/@remix-run/dev/node_modules/strip-final-newline": { - "version": "2.0.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz", + "integrity": "sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/@remix-run/eslint-config": { - "version": "1.18.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz", + "integrity": "sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.8", - "@babel/eslint-parser": "^7.21.8", - "@babel/preset-react": "^7.18.6", - "@rushstack/eslint-patch": "^1.2.0", - "@typescript-eslint/eslint-plugin": "^5.59.0", - "@typescript-eslint/parser": "^5.59.0", - "eslint-import-resolver-node": "0.3.7", - "eslint-import-resolver-typescript": "^3.5.4", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^26.9.0", - "eslint-plugin-jest-dom": "^4.0.3", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-testing-library": "^5.10.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.0", - "react": "^17.0.0 || ^18.0.0", - "typescript": "^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=12" } }, - "node_modules/@remix-run/express": { - "version": "1.18.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-s390x": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz", + "integrity": "sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@remix-run/node": "1.18.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "express": "^4.17.1" + "node": ">=12" } }, - "node_modules/@remix-run/node": { - "version": "1.18.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/linux-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz", + "integrity": "sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@remix-run/server-runtime": "1.18.0", - "@remix-run/web-fetch": "^4.3.4", - "@remix-run/web-file": "^3.0.2", - "@remix-run/web-stream": "^1.0.3", - "@web3-storage/multipart-parser": "^1.0.0", - "abort-controller": "^3.0.0", - "cookie-signature": "^1.1.0", - "source-map-support": "^0.5.21", - "stream-slice": "^0.1.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" + "node": ">=12" } }, - "node_modules/@remix-run/react": { - "version": "1.18.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz", + "integrity": "sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@remix-run/router": "1.7.0", - "react-router-dom": "6.14.0" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "node": ">=12" } }, - "node_modules/@remix-run/router": { - "version": "1.7.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz", + "integrity": "sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=14" + "node": ">=12" } }, - "node_modules/@remix-run/serve": { - "version": "1.18.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/sunos-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz", + "integrity": "sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@remix-run/express": "1.18.0", - "@remix-run/node": "1.18.0", - "compression": "^1.7.4", - "express": "^4.17.1", - "morgan": "^1.10.0" - }, - "bin": { - "remix-serve": "dist/cli.js" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=14.0.0" + "node": ">=12" } }, - "node_modules/@remix-run/server-runtime": { - "version": "1.18.0", + "node_modules/@remix-run/dev/node_modules/@esbuild/win32-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz", + "integrity": "sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@remix-run/router": "1.7.0", - "@types/cookie": "^0.4.1", - "@web3-storage/multipart-parser": "^1.0.0", - "cookie": "^0.4.1", - "set-cookie-parser": "^2.4.8", - "source-map": "^0.7.3" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14.0.0" + "node": ">=12" } }, - "node_modules/@remix-run/server-runtime/node_modules/@types/cookie": { - "version": "0.4.1", - "license": "MIT" - }, - "node_modules/@remix-run/server-runtime/node_modules/cookie": { - "version": "0.4.2", + "node_modules/@remix-run/dev/node_modules/@esbuild/win32-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz", + "integrity": "sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@remix-run/web-blob": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "@remix-run/web-stream": "^1.0.0", - "web-encoding": "1.1.5" + "node": ">=12" } }, - "node_modules/@remix-run/web-fetch": { - "version": "4.3.4", + "node_modules/@remix-run/dev/node_modules/@esbuild/win32-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz", + "integrity": "sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@remix-run/web-blob": "^3.0.4", - "@remix-run/web-form-data": "^3.0.3", - "@remix-run/web-stream": "^1.0.3", - "@web3-storage/multipart-parser": "^1.0.0", - "abort-controller": "^3.0.0", - "data-uri-to-buffer": "^3.0.1", - "mrmime": "^1.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^10.17 || >=12.3" - } - }, - "node_modules/@remix-run/web-file": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "@remix-run/web-blob": "^3.0.3" + "node": ">=12" } }, - "node_modules/@remix-run/web-form-data": { - "version": "3.0.4", + "node_modules/@remix-run/dev/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, "license": "MIT", "dependencies": { - "web-encoding": "1.1.5" + "balanced-match": "^1.0.0" } }, - "node_modules/@remix-run/web-stream": { - "version": "1.0.3", + "node_modules/@remix-run/dev/node_modules/chalk": { + "version": "4.1.2", + "dev": true, "license": "MIT", "dependencies": { - "web-streams-polyfill": "^3.1.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@restart/hooks": { - "version": "0.4.9", + "node_modules/@remix-run/dev/node_modules/esbuild": { + "version": "0.17.6", + "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "dequal": "^2.0.2" + "bin": { + "esbuild": "bin/esbuild" }, - "peerDependencies": { - "react": ">=16.8.0" + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.6", + "@esbuild/android-arm64": "0.17.6", + "@esbuild/android-x64": "0.17.6", + "@esbuild/darwin-arm64": "0.17.6", + "@esbuild/darwin-x64": "0.17.6", + "@esbuild/freebsd-arm64": "0.17.6", + "@esbuild/freebsd-x64": "0.17.6", + "@esbuild/linux-arm": "0.17.6", + "@esbuild/linux-arm64": "0.17.6", + "@esbuild/linux-ia32": "0.17.6", + "@esbuild/linux-loong64": "0.17.6", + "@esbuild/linux-mips64el": "0.17.6", + "@esbuild/linux-ppc64": "0.17.6", + "@esbuild/linux-riscv64": "0.17.6", + "@esbuild/linux-s390x": "0.17.6", + "@esbuild/linux-x64": "0.17.6", + "@esbuild/netbsd-x64": "0.17.6", + "@esbuild/openbsd-x64": "0.17.6", + "@esbuild/sunos-x64": "0.17.6", + "@esbuild/win32-arm64": "0.17.6", + "@esbuild/win32-ia32": "0.17.6", + "@esbuild/win32-x64": "0.17.6" } }, - "node_modules/@rollup/pluginutils": { - "version": "4.2.1", + "node_modules/@remix-run/dev/node_modules/execa": { + "version": "5.1.1", "dev": true, "license": "MIT", "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">= 8.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.2.0", + "node_modules/@remix-run/dev/node_modules/fs-extra": { + "version": "10.1.0", "dev": true, - "license": "MIT" - }, - "node_modules/@selderee/plugin-htmlparser2": { - "version": "0.10.0", "license": "MIT", "dependencies": { - "domhandler": "^5.0.3", - "selderee": "^0.10.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, - "funding": { - "url": "https://ko-fi.com/killymxi" + "engines": { + "node": ">=12" } }, - "node_modules/@sentry-internal/tracing": { - "version": "7.54.0", + "node_modules/@remix-run/dev/node_modules/get-port": { + "version": "5.1.1", + "dev": true, "license": "MIT", - "dependencies": { - "@sentry/core": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "tslib": "^1.9.3" - }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@sentry-internal/tracing/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" + "node_modules/@remix-run/dev/node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } }, - "node_modules/@sentry/browser": { - "version": "7.54.0", + "node_modules/@remix-run/dev/node_modules/is-stream": { + "version": "2.0.1", + "dev": true, "license": "MIT", - "dependencies": { - "@sentry-internal/tracing": "7.54.0", - "@sentry/core": "7.54.0", - "@sentry/replay": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "tslib": "^1.9.3" - }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@sentry/browser/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/cli": { - "version": "2.2.0", - "hasInstallScript": true, - "license": "BSD-3-Clause", + "node_modules/@remix-run/dev/node_modules/minimatch": { + "version": "9.0.1", + "dev": true, + "license": "ISC", "dependencies": { - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.7", - "npmlog": "^6.0.1", - "progress": "^2.0.3", - "proxy-from-env": "^1.1.0", - "which": "^2.0.2" - }, - "bin": { - "sentry-cli": "bin/sentry-cli" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 12" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@sentry/core": { - "version": "7.54.0", + "node_modules/@remix-run/dev/node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, "license": "MIT", "dependencies": { - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "tslib": "^1.9.3" + "path-key": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/@sentry/core/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/node": { - "version": "7.54.0", + "node_modules/@remix-run/dev/node_modules/pidtree": { + "version": "0.6.0", + "dev": true, "license": "MIT", - "dependencies": { - "@sentry-internal/tracing": "7.54.0", - "@sentry/core": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "cookie": "^0.4.1", - "https-proxy-agent": "^5.0.0", - "lru_map": "^0.3.3", - "tslib": "^1.9.3" + "bin": { + "pidtree": "bin/pidtree.js" }, "engines": { - "node": ">=8" + "node": ">=0.10" } }, - "node_modules/@sentry/node/node_modules/cookie": { - "version": "0.4.2", + "node_modules/@remix-run/dev/node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/@sentry/node/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/react": { - "version": "7.54.0", + "node_modules/@remix-run/eslint-config": { + "version": "1.18.0", + "dev": true, "license": "MIT", "dependencies": { - "@sentry/browser": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "hoist-non-react-statics": "^3.3.2", - "tslib": "^1.9.3" + "@babel/core": "^7.21.8", + "@babel/eslint-parser": "^7.21.8", + "@babel/preset-react": "^7.18.6", + "@rushstack/eslint-patch": "^1.2.0", + "@typescript-eslint/eslint-plugin": "^5.59.0", + "@typescript-eslint/parser": "^5.59.0", + "eslint-import-resolver-node": "0.3.7", + "eslint-import-resolver-typescript": "^3.5.4", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^26.9.0", + "eslint-plugin-jest-dom": "^4.0.3", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^5.10.2" }, "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "peerDependencies": { - "react": "15.x || 16.x || 17.x || 18.x" + "eslint": "^8.0.0", + "react": "^17.0.0 || ^18.0.0", + "typescript": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@sentry/react/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/remix": { - "version": "7.54.0", + "node_modules/@remix-run/express": { + "version": "1.18.0", "license": "MIT", "dependencies": { - "@sentry/cli": "2.2.0", - "@sentry/core": "7.54.0", - "@sentry/node": "7.54.0", - "@sentry/react": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0", - "tslib": "^1.9.3", - "yargs": "^17.6.0" - }, - "bin": { - "sentry-upload-sourcemaps": "scripts/sentry-upload-sourcemaps.js" + "@remix-run/node": "1.18.0" }, "engines": { - "node": ">=14" + "node": ">=14.0.0" }, "peerDependencies": { - "@remix-run/node": "1.x", - "@remix-run/react": "1.x", - "react": "16.x || 17.x || 18.x" + "express": "^4.17.1" } }, - "node_modules/@sentry/remix/node_modules/cliui": { - "version": "8.0.1", - "license": "ISC", + "node_modules/@remix-run/node": { + "version": "1.18.0", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "@remix-run/server-runtime": "1.18.0", + "@remix-run/web-fetch": "^4.3.4", + "@remix-run/web-file": "^3.0.2", + "@remix-run/web-stream": "^1.0.3", + "@web3-storage/multipart-parser": "^1.0.0", + "abort-controller": "^3.0.0", + "cookie-signature": "^1.1.0", + "source-map-support": "^0.5.21", + "stream-slice": "^0.1.2" }, "engines": { - "node": ">=12" + "node": ">=14.0.0" } }, - "node_modules/@sentry/remix/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sentry/remix/node_modules/yargs": { - "version": "17.7.2", + "node_modules/@remix-run/react": { + "version": "1.18.0", "license": "MIT", "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "@remix-run/router": "1.7.0", + "react-router-dom": "6.14.0" }, "engines": { - "node": ">=12" + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@sentry/remix/node_modules/yargs-parser": { - "version": "21.1.1", - "license": "ISC", + "node_modules/@remix-run/router": { + "version": "1.7.0", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" } }, - "node_modules/@sentry/replay": { - "version": "7.54.0", + "node_modules/@remix-run/serve": { + "version": "1.18.0", + "dev": true, "license": "MIT", "dependencies": { - "@sentry/core": "7.54.0", - "@sentry/types": "7.54.0", - "@sentry/utils": "7.54.0" + "@remix-run/express": "1.18.0", + "@remix-run/node": "1.18.0", + "compression": "^1.7.4", + "express": "^4.17.1", + "morgan": "^1.10.0" + }, + "bin": { + "remix-serve": "dist/cli.js" }, "engines": { - "node": ">=12" - } - }, - "node_modules/@sentry/types": { - "version": "7.54.0", - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, - "node_modules/@sentry/utils": { - "version": "7.54.0", + "node_modules/@remix-run/server-runtime": { + "version": "1.18.0", "license": "MIT", "dependencies": { - "@sentry/types": "7.54.0", - "tslib": "^1.9.3" + "@remix-run/router": "1.7.0", + "@types/cookie": "^0.4.1", + "@web3-storage/multipart-parser": "^1.0.0", + "cookie": "^0.4.1", + "set-cookie-parser": "^2.4.8", + "source-map": "^0.7.3" }, "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, - "node_modules/@sentry/utils/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, - "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "dev": true, + "node_modules/@remix-run/server-runtime/node_modules/@types/cookie": { + "version": "0.4.1", "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "dev": true, + "node_modules/@remix-run/server-runtime/node_modules/cookie": { + "version": "0.4.2", "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "node": ">= 0.6" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "dev": true, + "node_modules/@remix-run/web-blob": { + "version": "3.0.4", "license": "MIT", "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", - "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", - "dependencies": { - "lodash.castarray": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" + "@remix-run/web-stream": "^1.0.0", + "web-encoding": "1.1.5" } }, - "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "node_modules/@remix-run/web-fetch": { + "version": "4.3.4", + "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "@remix-run/web-blob": "^3.0.4", + "@remix-run/web-form-data": "^3.0.3", + "@remix-run/web-stream": "^1.0.3", + "@web3-storage/multipart-parser": "^1.0.0", + "abort-controller": "^3.0.0", + "data-uri-to-buffer": "^3.0.1", + "mrmime": "^1.0.0" }, "engines": { - "node": ">=4" + "node": "^10.17 || >=12.3" } }, - "node_modules/@tanstack/react-table": { - "version": "8.9.2", + "node_modules/@remix-run/web-file": { + "version": "3.0.2", "license": "MIT", "dependencies": { - "@tanstack/table-core": "8.9.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" + "@remix-run/web-blob": "^3.0.3" } }, - "node_modules/@tanstack/table-core": { - "version": "8.9.2", + "node_modules/@remix-run/web-form-data": { + "version": "3.0.4", "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "dependencies": { + "web-encoding": "1.1.5" } }, - "node_modules/@testing-library/jest-dom": { - "version": "5.16.5", - "dev": true, + "node_modules/@remix-run/web-stream": { + "version": "1.0.3", "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.0.1", - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=8", - "npm": ">=6", - "yarn": ">=1" + "web-streams-polyfill": "^3.1.1" } }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "dev": true, + "node_modules/@restart/hooks": { + "version": "0.4.9", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "dequal": "^2.0.2" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" + "peerDependencies": { + "react": ">=16.8.0" } }, - "node_modules/@total-typescript/ts-reset": { - "version": "0.4.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/acorn": { - "version": "4.0.6", + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "*" + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.1", + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", "dev": true, "license": "MIT" }, - "node_modules/@types/bcryptjs": { - "version": "2.4.2", + "node_modules/@rushstack/eslint-patch": { + "version": "1.2.0", "dev": true, "license": "MIT" }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.4", - "dev": true, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.10.0", "license": "MIT", "dependencies": { - "@types/node": "*" + "domhandler": "^5.0.3", + "selderee": "^0.10.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" } }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "dev": true, + "node_modules/@sentry-internal/tracing": { + "version": "7.54.0", "license": "MIT", "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "@sentry/core": "7.54.0", + "@sentry/types": "7.54.0", + "@sentry/utils": "7.54.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "dev": true, + "node_modules/@sentry-internal/tracing/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@sentry/browser": { + "version": "7.54.0", "license": "MIT", "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" + "@sentry-internal/tracing": "7.54.0", + "@sentry/core": "7.54.0", + "@sentry/replay": "7.54.0", + "@sentry/types": "7.54.0", + "@sentry/utils": "7.54.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/chai": { - "version": "4.3.5", - "dev": true, - "license": "MIT" + "node_modules/@sentry/browser/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" }, - "node_modules/@types/chai-subset": { - "version": "1.3.3", - "dev": true, - "license": "MIT", + "node_modules/@sentry/cli": { + "version": "2.2.0", + "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { - "@types/chai": "*" + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "npmlog": "^6.0.1", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 12" } }, - "node_modules/@types/compression": { - "version": "1.7.2", - "dev": true, + "node_modules/@sentry/core": { + "version": "7.54.0", "license": "MIT", "dependencies": { - "@types/express": "*" + "@sentry/types": "7.54.0", + "@sentry/utils": "7.54.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/connect": { - "version": "3.4.35", - "dev": true, + "node_modules/@sentry/core/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@sentry/node": { + "version": "7.54.0", "license": "MIT", "dependencies": { - "@types/node": "*" + "@sentry-internal/tracing": "7.54.0", + "@sentry/core": "7.54.0", + "@sentry/types": "7.54.0", + "@sentry/utils": "7.54.0", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/cookie": { - "version": "0.5.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/date-arithmetic": { - "version": "4.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/debug": { - "version": "4.1.8", - "dev": true, + "node_modules/@sentry/node/node_modules/cookie": { + "version": "0.4.2", "license": "MIT", - "dependencies": { - "@types/ms": "*" + "engines": { + "node": ">= 0.6" } }, - "node_modules/@types/eslint": { - "version": "8.40.2", - "dev": true, + "node_modules/@sentry/node/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@sentry/react": { + "version": "7.54.0", "license": "MIT", "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" + "@sentry/browser": "7.54.0", + "@sentry/types": "7.54.0", + "@sentry/utils": "7.54.0", + "hoist-non-react-statics": "^3.3.2", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "15.x || 16.x || 17.x || 18.x" } }, - "node_modules/@types/estree": { - "version": "1.0.0", - "dev": true, - "license": "MIT" + "node_modules/@sentry/react/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" }, - "node_modules/@types/estree-jsx": { - "version": "0.0.1", - "dev": true, + "node_modules/@sentry/remix": { + "version": "7.54.0", "license": "MIT", "dependencies": { - "@types/estree": "*" + "@sentry/cli": "2.2.0", + "@sentry/core": "7.54.0", + "@sentry/node": "7.54.0", + "@sentry/react": "7.54.0", + "@sentry/types": "7.54.0", + "@sentry/utils": "7.54.0", + "tslib": "^1.9.3", + "yargs": "^17.6.0" + }, + "bin": { + "sentry-upload-sourcemaps": "scripts/sentry-upload-sourcemaps.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@remix-run/node": "1.x", + "@remix-run/react": "1.x", + "react": "16.x || 17.x || 18.x" } }, - "node_modules/@types/express": { - "version": "4.17.17", - "dev": true, - "license": "MIT", + "node_modules/@sentry/remix/node_modules/cliui": { + "version": "8.0.1", + "license": "ISC", "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.33", - "dev": true, + "node_modules/@sentry/remix/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" + }, + "node_modules/@sentry/remix/node_modules/yargs": { + "version": "17.7.2", "license": "MIT", "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@types/fs-extra": { - "version": "11.0.1", - "dev": true, + "node_modules/@sentry/remix/node_modules/yargs-parser": { + "version": "21.1.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry/replay": { + "version": "7.54.0", "license": "MIT", "dependencies": { - "@types/jsonfile": "*", - "@types/node": "*" + "@sentry/core": "7.54.0", + "@sentry/types": "7.54.0", + "@sentry/utils": "7.54.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@types/glob": { - "version": "8.1.0", - "dev": true, + "node_modules/@sentry/types": { + "version": "7.54.0", "license": "MIT", - "dependencies": { - "@types/minimatch": "^5.1.2", - "@types/node": "*" + "engines": { + "node": ">=8" } }, - "node_modules/@types/hast": { - "version": "2.3.4", - "dev": true, + "node_modules/@sentry/utils": { + "version": "7.54.0", "license": "MIT", "dependencies": { - "@types/unist": "*" + "@sentry/types": "7.54.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "dev": true, - "license": "MIT" + "node_modules/@sentry/utils/node_modules/tslib": { + "version": "1.14.1", + "license": "0BSD" }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", + "node_modules/@sinclair/typebox": { + "version": "0.25.24", "dev": true, "license": "MIT" }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", + "node_modules/@sindresorhus/is": { + "version": "4.6.0", "dev": true, "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", "dev": true, "license": "MIT", "dependencies": { - "@types/istanbul-lib-report": "*" + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "node_modules/@types/jest": { - "version": "29.4.0", - "dev": true, + "node_modules/@tailwindcss/typography": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", + "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.9.2", "license": "MIT", "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "@tanstack/table-core": "8.9.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" } }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, + "node_modules/@tanstack/table-core": { + "version": "8.9.2", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "29.4.3", + "node_modules/@testing-library/jest-dom": { + "version": "5.16.5", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.4.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@total-typescript/ts-reset": { + "version": "0.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/acorn": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.5.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/date-arithmetic": { + "version": "4.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.40.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "0.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.17", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.33", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "11.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "2.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@types/jest/node_modules/react-is": { @@ -5224,16 +5938,373 @@ "vite-node": "^0.28.5" } }, - "node_modules/@vanilla-extract/integration/node_modules/@esbuild/darwin-arm64": { + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-arm": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz", + "integrity": "sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz", + "integrity": "sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/android-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz", + "integrity": "sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.6", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/darwin-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz", + "integrity": "sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz", + "integrity": "sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz", + "integrity": "sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-arm": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz", + "integrity": "sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz", + "integrity": "sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz", + "integrity": "sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-loong64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz", + "integrity": "sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz", + "integrity": "sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz", + "integrity": "sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz", + "integrity": "sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-s390x": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz", + "integrity": "sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/linux-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz", + "integrity": "sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz", + "integrity": "sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz", + "integrity": "sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/sunos-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz", + "integrity": "sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz", + "integrity": "sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz", + "integrity": "sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@vanilla-extract/integration/node_modules/@esbuild/win32-x64": { "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz", + "integrity": "sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">=12" @@ -19113,11 +20184,158 @@ "get-tsconfig": "^4.4.0" } }, + "@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "dev": true, + "optional": true + }, "@esbuild/darwin-arm64": { "version": "0.17.19", "dev": true, "optional": true }, + "@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "dev": true, + "optional": true + }, "@eslint-community/eslint-utils": { "version": "4.2.0", "requires": { @@ -20048,11 +21266,158 @@ "xdm": "^2.0.0" }, "dependencies": { + "@esbuild/android-arm": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz", + "integrity": "sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz", + "integrity": "sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz", + "integrity": "sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==", + "dev": true, + "optional": true + }, "@esbuild/darwin-arm64": { "version": "0.17.6", "dev": true, "optional": true }, + "@esbuild/darwin-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz", + "integrity": "sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz", + "integrity": "sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz", + "integrity": "sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz", + "integrity": "sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz", + "integrity": "sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz", + "integrity": "sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz", + "integrity": "sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz", + "integrity": "sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz", + "integrity": "sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz", + "integrity": "sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz", + "integrity": "sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz", + "integrity": "sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz", + "integrity": "sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz", + "integrity": "sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz", + "integrity": "sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz", + "integrity": "sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz", + "integrity": "sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz", + "integrity": "sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==", + "dev": true, + "optional": true + }, "brace-expansion": { "version": "2.0.1", "dev": true, @@ -21023,11 +22388,158 @@ "vite-node": "^0.28.5" }, "dependencies": { + "@esbuild/android-arm": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz", + "integrity": "sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz", + "integrity": "sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz", + "integrity": "sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==", + "dev": true, + "optional": true + }, "@esbuild/darwin-arm64": { "version": "0.17.6", "dev": true, "optional": true }, + "@esbuild/darwin-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz", + "integrity": "sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz", + "integrity": "sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz", + "integrity": "sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz", + "integrity": "sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz", + "integrity": "sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz", + "integrity": "sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz", + "integrity": "sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz", + "integrity": "sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz", + "integrity": "sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz", + "integrity": "sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz", + "integrity": "sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz", + "integrity": "sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz", + "integrity": "sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz", + "integrity": "sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz", + "integrity": "sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz", + "integrity": "sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz", + "integrity": "sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz", + "integrity": "sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==", + "dev": true, + "optional": true + }, "esbuild": { "version": "0.17.6", "dev": true, diff --git a/prisma/migrations/20260313023719_add_organization_model/migration.sql b/prisma/migrations/20260313023719_add_organization_model/migration.sql new file mode 100644 index 0000000..f83ada2 --- /dev/null +++ b/prisma/migrations/20260313023719_add_organization_model/migration.sql @@ -0,0 +1,86 @@ +-- CreateTable +CREATE TABLE "Organization" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "animalType" TEXT NOT NULL DEFAULT 'mixed', + "description" TEXT, + "logoUrl" TEXT, + "website" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT NOT NULL, + "username" TEXT NOT NULL, + "name" TEXT, + "phone" TEXT, + "mailingList" BOOLEAN NOT NULL DEFAULT true, + "orgId" TEXT, + "birthdate" DATETIME, + "height" INTEGER, + "yearsOfExperience" INTEGER, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "imageId" TEXT, + "lastLogin" DATETIME DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "User_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organization" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "User_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image" ("fileId") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_User" ("birthdate", "createdAt", "email", "height", "id", "imageId", "lastLogin", "mailingList", "name", "notes", "phone", "updatedAt", "username", "yearsOfExperience") SELECT "birthdate", "createdAt", "email", "height", "id", "imageId", "lastLogin", "mailingList", "name", "notes", "phone", "updatedAt", "username", "yearsOfExperience" FROM "User"; +DROP TABLE "User"; +ALTER TABLE "new_User" RENAME TO "User"; +CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); +CREATE UNIQUE INDEX "User_imageId_key" ON "User"("imageId"); +CREATE TABLE "new_Event" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "start" DATETIME NOT NULL, + "end" DATETIME NOT NULL, + "orgId" TEXT, + "cleaningCrewReq" INTEGER NOT NULL DEFAULT 0, + "lessonAssistantsReq" INTEGER NOT NULL DEFAULT 0, + "sideWalkersReq" INTEGER NOT NULL DEFAULT 0, + "horseLeadersReq" INTEGER NOT NULL DEFAULT 0, + "isPrivate" BOOLEAN NOT NULL DEFAULT false, + CONSTRAINT "Event_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organization" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Event" ("cleaningCrewReq", "end", "horseLeadersReq", "id", "isPrivate", "lessonAssistantsReq", "sideWalkersReq", "start", "title") SELECT "cleaningCrewReq", "end", "horseLeadersReq", "id", "isPrivate", "lessonAssistantsReq", "sideWalkersReq", "start", "title" FROM "Event"; +DROP TABLE "Event"; +ALTER TABLE "new_Event" RENAME TO "Event"; +CREATE UNIQUE INDEX "Event_id_key" ON "Event"("id"); +CREATE TABLE "new_Horse" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "notes" TEXT, + "status" TEXT, + "updatedAt" DATETIME NOT NULL, + "cooldown" BOOLEAN NOT NULL DEFAULT false, + "cooldownStartDate" DATETIME, + "cooldownEndDate" DATETIME, + "orgId" TEXT, + "imageId" TEXT, + CONSTRAINT "Horse_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organization" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Horse_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image" ("fileId") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Horse" ("cooldown", "cooldownEndDate", "cooldownStartDate", "id", "imageId", "name", "notes", "status", "updatedAt") SELECT "cooldown", "cooldownEndDate", "cooldownStartDate", "id", "imageId", "name", "notes", "status", "updatedAt" FROM "Horse"; +DROP TABLE "Horse"; +ALTER TABLE "new_Horse" RENAME TO "Horse"; +CREATE UNIQUE INDEX "Horse_id_key" ON "Horse"("id"); +CREATE UNIQUE INDEX "Horse_imageId_key" ON "Horse"("imageId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; + +-- CreateIndex +CREATE UNIQUE INDEX "Organization_id_key" ON "Organization"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Organization_slug_key" ON "Organization"("slug"); diff --git a/prisma/migrations/20260313030000_rename_horse_to_animal/migration.sql b/prisma/migrations/20260313030000_rename_horse_to_animal/migration.sql new file mode 100644 index 0000000..49271fe --- /dev/null +++ b/prisma/migrations/20260313030000_rename_horse_to_animal/migration.sql @@ -0,0 +1,133 @@ +-- DropIndex +DROP INDEX "Horse_imageId_key"; + +-- DropIndex +DROP INDEX "Horse_id_key"; + +-- DropIndex +DROP INDEX "HorseAssignment_eventId_userId_key"; + +-- DropIndex +DROP INDEX "HorseAssignment_id_key"; + +-- DropIndex +DROP INDEX "_EventToHorse_B_index"; + +-- DropIndex +DROP INDEX "_EventToHorse_AB_unique"; + +-- DropIndex +DROP INDEX "_horseLeader_B_index"; + +-- DropIndex +DROP INDEX "_horseLeader_AB_unique"; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "Horse"; +PRAGMA foreign_keys=on; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "HorseAssignment"; +PRAGMA foreign_keys=on; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "_EventToHorse"; +PRAGMA foreign_keys=on; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "_horseLeader"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "Animal" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "species" TEXT NOT NULL DEFAULT 'horse', + "notes" TEXT, + "status" TEXT, + "updatedAt" DATETIME NOT NULL, + "cooldown" BOOLEAN NOT NULL DEFAULT false, + "cooldownStartDate" DATETIME, + "cooldownEndDate" DATETIME, + "orgId" TEXT, + "imageId" TEXT, + CONSTRAINT "Animal_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organization" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Animal_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image" ("fileId") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AnimalAssignment" ( + "id" TEXT NOT NULL PRIMARY KEY, + "eventId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "animalId" TEXT NOT NULL, + CONSTRAINT "AnimalAssignment_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "AnimalAssignment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "AnimalAssignment_animalId_fkey" FOREIGN KEY ("animalId") REFERENCES "Animal" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_AnimalToEvent" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_AnimalToEvent_A_fkey" FOREIGN KEY ("A") REFERENCES "Animal" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_AnimalToEvent_B_fkey" FOREIGN KEY ("B") REFERENCES "Event" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_animalHandler" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_animalHandler_A_fkey" FOREIGN KEY ("A") REFERENCES "Event" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_animalHandler_B_fkey" FOREIGN KEY ("B") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Event" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "start" DATETIME NOT NULL, + "end" DATETIME NOT NULL, + "orgId" TEXT, + "cleaningCrewReq" INTEGER NOT NULL DEFAULT 0, + "lessonAssistantsReq" INTEGER NOT NULL DEFAULT 0, + "sideWalkersReq" INTEGER NOT NULL DEFAULT 0, + "animalHandlersReq" INTEGER NOT NULL DEFAULT 0, + "isPrivate" BOOLEAN NOT NULL DEFAULT false, + CONSTRAINT "Event_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Organization" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Event" ("cleaningCrewReq", "end", "id", "isPrivate", "lessonAssistantsReq", "orgId", "sideWalkersReq", "start", "title") SELECT "cleaningCrewReq", "end", "id", "isPrivate", "lessonAssistantsReq", "orgId", "sideWalkersReq", "start", "title" FROM "Event"; +DROP TABLE "Event"; +ALTER TABLE "new_Event" RENAME TO "Event"; +CREATE UNIQUE INDEX "Event_id_key" ON "Event"("id"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; + +-- CreateIndex +CREATE UNIQUE INDEX "Animal_id_key" ON "Animal"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Animal_imageId_key" ON "Animal"("imageId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AnimalAssignment_id_key" ON "AnimalAssignment"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "AnimalAssignment_eventId_userId_key" ON "AnimalAssignment"("eventId", "userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "_AnimalToEvent_AB_unique" ON "_AnimalToEvent"("A", "B"); + +-- CreateIndex +CREATE INDEX "_AnimalToEvent_B_index" ON "_AnimalToEvent"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_animalHandler_AB_unique" ON "_animalHandler"("A", "B"); + +-- CreateIndex +CREATE INDEX "_animalHandler_B_index" ON "_animalHandler"("B"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b1db976..5419114 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,6 +7,25 @@ generator client { provider = "prisma-client-js" } +model Organization { + id String @id @unique @default(cuid()) + name String + slug String @unique + animalType String @default("mixed") + description String? + logoUrl String? + website String? + isActive Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + users User[] + animals Animal[] + events Event[] +} + + model File { id String @id @unique @default(cuid()) blob Bytes @@ -26,8 +45,8 @@ model Image { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - user User? - horse Horse? + user User? + animal Animal? } model Role { @@ -59,6 +78,9 @@ model User { phone String? mailingList Boolean @default(true) + org Organization? @relation(fields: [orgId], references: [id]) + orgId String? + birthdate DateTime? height Int? yearsOfExperience Int? @@ -76,13 +98,13 @@ model User { lastLogin DateTime? @default(now()) - Instructor Event[] @relation("instructor") - cleaningCrew Event[] @relation("cleaningCrew") - lessonAssistant Event[] @relation("lessonAssistant") - sideWalker Event[] @relation("sideWalker") - horseLeader Event[] @relation("horseLeader") + Instructor Event[] @relation("instructor") + cleaningCrew Event[] @relation("cleaningCrew") + lessonAssistant Event[] @relation("lessonAssistant") + sideWalker Event[] @relation("sideWalker") + animalHandler Event[] @relation("animalHandler") - horseAssignments HorseAssignment[] + animalAssignments AnimalAssignment[] } model Password { @@ -128,9 +150,10 @@ model Session { expirationDate DateTime } -model Horse { +model Animal { id String @id @unique @default(cuid()) name String + species String @default("horse") notes String? status String? updatedAt DateTime @updatedAt @@ -138,11 +161,14 @@ model Horse { cooldownStartDate DateTime? cooldownEndDate DateTime? + org Organization? @relation(fields: [orgId], references: [id]) + orgId String? + image Image? @relation(fields: [imageId], references: [fileId]) imageId String? @unique events Event[] - HorseAssignment HorseAssignment[] + AnimalAssignment AnimalAssignment[] } model Event { @@ -152,6 +178,9 @@ model Event { start DateTime end DateTime + org Organization? @relation(fields: [orgId], references: [id]) + orgId String? + instructors User[] @relation("instructor") cleaningCrew User[] @relation("cleaningCrew") @@ -163,24 +192,24 @@ model Event { sideWalkers User[] @relation("sideWalker") sideWalkersReq Int @default(0) - horseLeaders User[] @relation("horseLeader") - horseLeadersReq Int @default(0) + animalHandlers User[] @relation("animalHandler") + animalHandlersReq Int @default(0) - horses Horse[] + animals Animal[] - horseAssignments HorseAssignment[] + animalAssignments AnimalAssignment[] isPrivate Boolean @default(false) } -model HorseAssignment { +model AnimalAssignment { id String @id @unique @default(cuid()) event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) volunteer User @relation(fields: [userId], references: [id], onDelete: Cascade) - horse Horse @relation(fields: [horseId], references: [id], onDelete: Cascade) + animal Animal @relation(fields: [animalId], references: [id], onDelete: Cascade) eventId String userId String - horseId String + animalId String @@unique([eventId, userId]) } diff --git a/prisma/seed.ts b/prisma/seed.ts index e8ef0fa..c875108 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -3,7 +3,7 @@ import { faker } from '@faker-js/faker' import { createPassword, createUser, - createHorse, + createAnimal, createEvent, } from 'tests/db-utils.ts' import { prisma } from '~/utils/db.server.ts' @@ -19,6 +19,19 @@ async function seed() { deleteAllData() console.timeEnd('๐Ÿงน Cleaned up the database...') + console.time(`๐Ÿ  Created default organization...`) + const defaultOrg = await prisma.organization.create({ + data: { + name: 'Tumbling T Ranch', + slug: 'tumbling-t-ranch', + animalType: 'horses', + description: 'Equestrian therapy nonprofit for individuals with disabilities', + website: 'https://www.thebarnaz.com', + isActive: true, + }, + }) + console.timeEnd(`๐Ÿ  Created default organization...`) + console.time(`๐Ÿ‘‘ Created admin role/permission...`) const adminRole = await prisma.role.create({ data: { @@ -30,6 +43,17 @@ async function seed() { }) console.timeEnd(`๐Ÿ‘‘ Created admin role/permission...`) + console.time(`๐ŸŒ Created superAdmin role/permission...`) + const superAdminRole = await prisma.role.create({ + data: { + name: 'superAdmin', + permissions: { + create: { name: 'superAdmin' }, + }, + }, + }) + console.timeEnd(`๐ŸŒ Created superAdmin role/permission...`) + console.time(`Created lesson assistant role/permission...`) const lessonAssistantRole = await prisma.role.create({ data: { @@ -41,16 +65,16 @@ async function seed() { }) console.timeEnd(`Created lesson assistant role/permission...`) - console.time(`Created horse leader role/permission...`) - const horseLeaderRole = await prisma.role.create({ + console.time(`Created animal handler role/permission...`) + const animalHandlerRole = await prisma.role.create({ data: { - name: 'horseLeader', + name: 'animalHandler', permissions: { - create: { name: 'horseLeader' }, + create: { name: 'animalHandler' }, }, }, }) - console.timeEnd(`Created horse leader role/permission...`) + console.timeEnd(`Created animal handler role/permission...`) console.time(`Created instructor role/permission...`) const instructorRole = await prisma.role.create({ @@ -71,6 +95,7 @@ async function seed() { const user = await prisma.user.create({ data: { ...userData, + org: { connect: { id: defaultOrg.id } }, password: { create: createPassword(userData.username), }, @@ -101,6 +126,7 @@ async function seed() { email: 'kody@kcd.dev', username: 'kody', name: 'Kody', + org: { connect: { id: defaultOrg.id } }, roles: { connect: { id: adminRole.id } }, image: { create: { @@ -133,6 +159,7 @@ async function seed() { email: 'bob@not.admin', username: 'bob', name: 'Bob', + org: { connect: { id: defaultOrg.id } }, image: { create: { contentType: 'image/png', @@ -163,6 +190,7 @@ async function seed() { email: 'isabelle@is.instructor', username: 'isabelle', name: 'Isabelle', + org: { connect: { id: defaultOrg.id } }, roles: { connect: { id: instructorRole.id } }, image: { create: { @@ -191,10 +219,11 @@ async function seed() { console.time(`๐Ÿด Created ${totalHorses} horses...`) const horses = await Promise.all( Array.from({ length: totalHorses }, async (_, index) => { - const horseData = createHorse() - const horse = await prisma.horse.create({ + const animalData = createAnimal() + const animal = await prisma.animal.create({ data: { - ...horseData, + ...animalData, + org: { connect: { id: defaultOrg.id } }, image: { create: { contentType: 'image/jpeg', @@ -209,7 +238,7 @@ async function seed() { }, }, }) - return horse + return animal }), ) console.timeEnd(`๐Ÿด Created ${totalHorses} horses...`) @@ -230,6 +259,7 @@ async function seed() { const event = await prisma.event.create({ data: { ...eventData, + org: { connect: { id: defaultOrg.id } }, }, }) return event @@ -237,13 +267,13 @@ async function seed() { ) console.timeEnd(`๐Ÿ“… Created a few events in the current month`) - console.time(`Setting signup password to "horses are cool"`) + console.time(`Setting signup password to "animals are cool"`) await prisma.signupPassword.create({ data: { - hash: await getPasswordHash('horses are cool'), + hash: await getPasswordHash('animals are cool'), } }) - console.timeEnd(`Setting signup password to "horses are cool"`) + console.timeEnd(`Setting signup password to "animals are cool"`) console.timeEnd(`๐ŸŒฑ Database has been seeded`) diff --git a/public/img/animal.png b/public/img/animal.png new file mode 100644 index 0000000..d416562 Binary files /dev/null and b/public/img/animal.png differ diff --git a/public/img/login-bg.jpg b/public/img/login-bg.jpg new file mode 100644 index 0000000..26a2a20 Binary files /dev/null and b/public/img/login-bg.jpg differ diff --git a/public/img/signup-bg.jpg b/public/img/signup-bg.jpg new file mode 100644 index 0000000..f742f68 Binary files /dev/null and b/public/img/signup-bg.jpg differ diff --git a/server/index.ts b/server/index.ts index 2bd9b33..38ab006 100644 --- a/server/index.ts +++ b/server/index.ts @@ -105,9 +105,13 @@ app.use( 'script-src': [ "'strict-dynamic'", "'self'", + // Allow Remix's dev LiveReload inline script (only in development) + MODE === 'development' + ? "'sha256-2DiiCtmfI72XrwYfK/Ss8TmqyUz4JmjPyki8vD75hYU='" + : null, // @ts-expect-error (_, res) => `'nonce-${res.locals.cspNonce}'`, - ], + ].filter(Boolean), 'script-src-attr': [ // @ts-expect-error (_, res) => `'nonce-${res.locals.cspNonce}'`, diff --git a/tailwind.config.ts b/tailwind.config.ts index 7a73e4a..118a4c7 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -74,7 +74,14 @@ export default { DEFAULT: 'hsl(var(--color-card))', foreground: 'hsl(var(--color-card-foreground))', }, - day: { + sidebar: { + DEFAULT: 'hsl(var(--color-sidebar))', + foreground: 'hsl(var(--color-sidebar-foreground))', + border: 'hsl(var(--color-sidebar-border))', + active: 'hsl(var(--color-sidebar-active))', + 'active-foreground': 'hsl(var(--color-sidebar-active-foreground))', + }, + day: { 100: 'hsl(var(--color-day-100))', 200: 'hsl(var(--color-day-200))', 300: 'hsl(var(--color-day-300))', diff --git a/tests/db-utils.ts b/tests/db-utils.ts index 63d1e64..0263faa 100644 --- a/tests/db-utils.ts +++ b/tests/db-utils.ts @@ -21,10 +21,10 @@ export function createUser() { .replace(/[^a-z0-9_]/g, '_') const notes = [ - 'Great with horses. A real horse whisperer.', - 'Very enthusiastic, does well with more active horses.', - 'Very gentle, works well with timid horses. ', - 'Still a bit afraid of horses, needs some support from others.', + 'Great with animals. Very calm and patient.', + 'Very enthusiastic, does well with more active animals.', + 'Very gentle, works well with timid animals.', + 'Still a bit nervous around animals, needs some support from others.', ] return { @@ -45,7 +45,7 @@ export function createPassword(username: string = faker.internet.userName()) { } } -export function createHorse() { +export function createAnimal() { const name = faker.person.firstName() const exampleStatuses = [ @@ -57,11 +57,11 @@ export function createHorse() { "Tired. Don't schedule for consecutive events.", ] const exampleNotes = [ - 'A little ornery; needs experienced, careful riders and handlers.', - 'Very easy going. Good for beginner handlers and riders.', - 'Easily spooked, riders and handlers need to be aware of their surroundings.', + 'A little ornery; needs experienced, careful handlers.', + 'Very easy going. Good for beginner handlers.', + 'Easily startled, handlers need to be aware of their surroundings.', 'Very social. Needs a firm handler.', - 'Is a very big and active horse.', + 'Very active and energetic.', ] const notes = exampleNotes[Math.floor(Math.random() * exampleNotes.length)] @@ -75,12 +75,15 @@ export function createHorse() { } } +/** @deprecated Use createAnimal instead */ +export const createHorse = createAnimal + export async function createEvent(start: Date) { const volunteers = await prisma.user.findMany({ where: { roles: { none: {} }}, }) - const allHorses = await prisma.horse.findMany() - const horses = faker.helpers.arrayElements(allHorses, { min: 3, max: 5 }) + const allAnimals = await prisma.animal.findMany() + const animals = faker.helpers.arrayElements(allAnimals, { min: 3, max: 5 }) const duration = faker.helpers.arrayElement([30, 60, 90]) const instructors = await prisma.user.findMany({ @@ -117,14 +120,14 @@ export async function createEvent(start: Date) { instructors: { connect: { id: instructor.id }, }, - horses: { - connect: horses.map(horse => { - return { id: horse.id } + animals: { + connect: animals.map(animal => { + return { id: animal.id } }), }, cleaningCrewReq: reqs[0], lessonAssistantsReq: reqs[1], - horseLeadersReq: reqs[2], + animalHandlersReq: reqs[2], sideWalkersReq: reqs[3], cleaningCrew: { @@ -133,7 +136,7 @@ export async function createEvent(start: Date) { lessonAssistants: { connect: assignments[1], }, - horseLeaders: { + animalHandlers: { connect: assignments[2], }, sideWalkers: {