diff --git a/.env.example b/.env.example index 99d86bd..0d0e0d6 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,2 @@ DATABASE_URL="file:./data.db?connection_limit=1" SESSION_SECRET="super-duper-s3cret" -IMAGE_UPLOAD_FOLDER="/workspaces/mokupona/build/uploads" \ No newline at end of file diff --git a/app/components/dinner-card.tsx b/app/components/dinner-card.tsx index 5e98a21..0337837 100644 --- a/app/components/dinner-card.tsx +++ b/app/components/dinner-card.tsx @@ -1,5 +1,4 @@ import type { Event } from "@prisma/client"; -import type { SerializeFrom } from "@remix-run/node"; import { Link } from "@remix-run/react"; import { Button } from "./ui/button"; @@ -10,7 +9,7 @@ export function DinnerCard({ event, preferredLocale, }: { - event: Event | SerializeFrom; + event: Event; preferredLocale: string; }) { const parsedDate = new Date(event.date); diff --git a/app/components/dinner-view.tsx b/app/components/dinner-view.tsx index 9c4caf1..057facf 100644 --- a/app/components/dinner-view.tsx +++ b/app/components/dinner-view.tsx @@ -1,14 +1,10 @@ -import type { Address, Event } from "@prisma/client"; -import { SerializeFrom } from "@remix-run/node"; - import { AutoLink } from "./auto-link"; +import { loader } from "~/routes/admin.dinners.$dinnerId"; import { getEventImageUrl } from "~/utils/misc"; export interface DinnerViewProps { - event: - | (Event & { address: Address }) - | SerializeFrom; + event: Awaited>["event"]; } export function DinnerView({ event }: DinnerViewProps) { diff --git a/app/components/ui/select.tsx b/app/components/ui/select.tsx index bbeead4..59ceb8f 100644 --- a/app/components/ui/select.tsx +++ b/app/components/ui/select.tsx @@ -17,7 +17,7 @@ const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< - React.ElementRef, + React.ComponentRef, React.ComponentPropsWithoutRef & { className?: string | ClassValue[]; } @@ -39,7 +39,7 @@ const SelectTrigger = React.forwardRef< SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; const SelectScrollUpButton = React.forwardRef< - React.ElementRef, + React.ComponentRef, React.ComponentPropsWithoutRef & { className?: string | ClassValue[]; } @@ -58,7 +58,7 @@ const SelectScrollUpButton = React.forwardRef< SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; const SelectScrollDownButton = React.forwardRef< - React.ElementRef, + React.ComponentRef, React.ComponentPropsWithoutRef & { className?: string | ClassValue[]; } @@ -78,7 +78,7 @@ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; const SelectContent = React.forwardRef< - React.ElementRef, + React.ComponentRef, React.ComponentPropsWithoutRef & { className?: string | ClassValue[]; position: "item-aligned" | "popper"; @@ -113,7 +113,7 @@ const SelectContent = React.forwardRef< SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< - React.ElementRef, + React.ComponentRef, React.ComponentPropsWithoutRef & { className?: string | ClassValue[]; } @@ -127,7 +127,7 @@ const SelectLabel = React.forwardRef< SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< - React.ElementRef, + React.ComponentRef, React.ComponentPropsWithoutRef & { className?: string | ClassValue[]; } @@ -151,7 +151,7 @@ const SelectItem = React.forwardRef< SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< - React.ElementRef, + React.ComponentRef, React.ComponentPropsWithoutRef & { className?: string | ClassValue[]; } diff --git a/app/root.tsx b/app/root.tsx index 76409bd..ac566be 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,10 +1,6 @@ import { HamburgerMenuIcon, InstagramLogoIcon } from "@radix-ui/react-icons"; -import type { - LinksFunction, - LoaderFunctionArgs, - SerializeFrom, -} from "@remix-run/node"; -import { json } from "@remix-run/node"; +import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node"; +import { data } from "@remix-run/node"; import { Form, Link, @@ -36,7 +32,7 @@ import { getToast } from "./utils/toast.server"; import stylesheet from "~/tailwind.css?url"; import { getUserWithRole } from "~/utils/session.server"; -export type RootLoaderData = SerializeFrom; +export type RootLoaderData = typeof loader; export const links: LinksFunction = () => [ { rel: "stylesheet", href: stylesheet }, @@ -74,7 +70,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const domainUrl = getDomainUrl(request); const user = await getUserWithRole(request); const { toast, headers } = await getToast(request); - return json({ user, toast, domainUrl }, { headers: combineHeaders(headers) }); + return data({ user, toast, domainUrl }, { headers: combineHeaders(headers) }); }; export default function App() { @@ -86,7 +82,7 @@ export default function App() { - + diff --git a/app/routes.ts b/app/routes.ts new file mode 100644 index 0000000..8389284 --- /dev/null +++ b/app/routes.ts @@ -0,0 +1,3 @@ +import { flatRoutes } from "@remix-run/fs-routes"; + +export default flatRoutes(); diff --git a/app/routes/admin._index.tsx b/app/routes/admin._index.tsx index 7e61653..3a143dd 100644 --- a/app/routes/admin._index.tsx +++ b/app/routes/admin._index.tsx @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Link } from "@remix-run/react"; import { Button } from "~/components/ui/button"; @@ -7,7 +7,7 @@ import { requireUserWithRole } from "~/utils/session.server"; export async function loader({ request }: LoaderFunctionArgs) { await requireUserWithRole(request, ["moderator", "admin"]); - return json({}); + return {}; } export const meta: MetaFunction = () => { diff --git a/app/routes/admin.dinners.$dinnerId.tsx b/app/routes/admin.dinners.$dinnerId.tsx index 2062a76..e6b47ba 100644 --- a/app/routes/admin.dinners.$dinnerId.tsx +++ b/app/routes/admin.dinners.$dinnerId.tsx @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Form, Link, useLoaderData } from "@remix-run/react"; import invariant from "tiny-invariant"; @@ -17,7 +17,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { if (!event) throw new Response("Not found", { status: 404 }); - return json({ event }); + return { event }; } export const meta: MetaFunction = ({ data }) => { diff --git a/app/routes/admin.dinners.$dinnerId_.edit.tsx b/app/routes/admin.dinners.$dinnerId_.edit.tsx index 854f58a..975ea76 100644 --- a/app/routes/admin.dinners.$dinnerId_.edit.tsx +++ b/app/routes/admin.dinners.$dinnerId_.edit.tsx @@ -6,28 +6,25 @@ import { useForm, } from "@conform-to/react"; import { getZodConstraint, parseWithZod } from "@conform-to/zod"; +import { FileUpload, parseFormData } from "@mjackson/form-data-parser"; import { ActionFunctionArgs, LoaderFunctionArgs, - MaxPartSizeExceededError, MetaFunction, - NodeOnDiskFile, - json, redirect, - unstable_composeUploadHandlers, - unstable_createFileUploadHandler, - unstable_createMemoryUploadHandler, - unstable_parseMultipartFormData, } from "@remix-run/node"; import { Form, useActionData, useLoaderData } from "@remix-run/react"; import invariant from "tiny-invariant"; -import { z } from "zod"; import { Field, SelectField, TextareaField } from "~/components/forms"; import { Button } from "~/components/ui/button"; import { prisma } from "~/db.server"; import { getAddresses } from "~/models/address.server"; import { getEventById, updateEvent } from "~/models/event.server"; +import { + fileStorage, + getStorageKey, +} from "~/utils/dinner-image-storage.server"; import { ClientEventSchema } from "~/utils/event-validation"; import { ServerEventSchema } from "~/utils/event-validation.server"; import { getTimezoneOffset, offsetDate } from "~/utils/misc"; @@ -46,11 +43,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!event) throw new Response("Not found", { status: 404 }); - return json({ + return { validImageTypes, addresses, dinner: event, - }); + }; } export const meta: MetaFunction = ({ data }) => { @@ -71,41 +68,18 @@ export async function action({ request, params }: ActionFunctionArgs) { const { dinnerId } = params; invariant(typeof dinnerId === "string", "Parameter dinnerId is missing"); - const uploadHandler = unstable_composeUploadHandlers( - unstable_createFileUploadHandler({ - directory: process.env.IMAGE_UPLOAD_FOLDER, - }), - unstable_createMemoryUploadHandler(), - ); + const uploadHandler = async (fileUpload: FileUpload) => { + let storageKey = getStorageKey("temporary-key"); + await fileStorage.set(storageKey, fileUpload); + return fileStorage.get(storageKey); + }; - const formData = await unstable_parseMultipartFormData( - request, - async (part) => { - try { - const result = await uploadHandler(part); - return result; - } catch (error) { - if (error instanceof MaxPartSizeExceededError) { - maximumFileSizeExceeded = true; - return new File([], "cover"); - } - throw error; - } - }, - ); + const formData = await parseFormData(request, uploadHandler); const submission = parseWithZod(formData, { schema: (intent) => - schema.superRefine((data, ctx) => { + schema.superRefine((data) => { if (intent !== null) return { ...data }; - if (maximumFileSizeExceeded) { - ctx.addIssue({ - path: ["cover"], - code: z.ZodIssueCode.custom, - message: "File cannot be greater than 3MB", - }); - return; - } }), }); @@ -116,11 +90,11 @@ export async function action({ request, params }: ActionFunctionArgs) { ) { // Remove the uploaded file from disk. // It will be sent again when submitting. - await (submission.payload.cover as unknown as NodeOnDiskFile).remove(); + await fileStorage.remove(getStorageKey("temporary-key")); } if (submission.status !== "success" || !submission.value) { - return json(submission.reply()); + return submission.reply(); } const { title, description, date, slots, price, cover, addressId } = @@ -138,7 +112,7 @@ export async function action({ request, params }: ActionFunctionArgs) { // Remove the file from disk. // It is in the database now. - await (cover as NodeOnDiskFile).remove(); + await fileStorage.remove(getStorageKey("temporary-key")); } const event = await updateEvent(dinnerId, { @@ -167,7 +141,7 @@ export default function DinnersPage() { defaultValue: { title: dinner.title, description: dinner.description, - date: dinner.date.substring(0, 16), + date: dinner.date.toISOString().substring(0, 16), slots: dinner.slots, price: dinner.price, addressId: dinner.addressId, diff --git a/app/routes/admin.dinners.$dinnerId_.signups.tsx b/app/routes/admin.dinners.$dinnerId_.signups.tsx index cf7843e..2b8d927 100644 --- a/app/routes/admin.dinners.$dinnerId_.signups.tsx +++ b/app/routes/admin.dinners.$dinnerId_.signups.tsx @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import invariant from "tiny-invariant"; @@ -27,10 +27,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!event) throw new Response("Not found", { status: 404 }); - return json({ + return { event, responses, - }); + }; } export const meta: MetaFunction = ({ data }) => { diff --git a/app/routes/admin.dinners._index.tsx b/app/routes/admin.dinners._index.tsx index 9186acd..9c845e6 100644 --- a/app/routes/admin.dinners._index.tsx +++ b/app/routes/admin.dinners._index.tsx @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Link, useFetcher, useLoaderData } from "@remix-run/react"; import { Button } from "~/components/ui/button"; @@ -9,7 +9,7 @@ export async function loader({ request }: LoaderFunctionArgs) { await requireUserWithRole(request, ["moderator", "admin"]); const events = await getEvents(); - return json({ events }); + return { events }; } export const meta: MetaFunction = () => { diff --git a/app/routes/admin.dinners.new.tsx b/app/routes/admin.dinners.new.tsx index eea4ff5..7fb5b1d 100644 --- a/app/routes/admin.dinners.new.tsx +++ b/app/routes/admin.dinners.new.tsx @@ -6,27 +6,24 @@ import { useForm, } from "@conform-to/react"; import { getZodConstraint, parseWithZod } from "@conform-to/zod"; +import { parseFormData, type FileUpload } from "@mjackson/form-data-parser"; import { ActionFunctionArgs, LoaderFunctionArgs, - MaxPartSizeExceededError, MetaFunction, - NodeOnDiskFile, - json, redirect, - unstable_composeUploadHandlers, - unstable_createFileUploadHandler, - unstable_createMemoryUploadHandler, - unstable_parseMultipartFormData, } from "@remix-run/node"; import { Form, useActionData, useLoaderData } from "@remix-run/react"; -import { z } from "zod"; import { Field, SelectField, TextareaField } from "~/components/forms"; import { Button } from "~/components/ui/button"; import { prisma } from "~/db.server"; import { getAddresses } from "~/models/address.server"; import { createEvent } from "~/models/event.server"; +import { + fileStorage, + getStorageKey, +} from "~/utils/dinner-image-storage.server"; import { ClientEventSchema } from "~/utils/event-validation"; import { ServerEventSchema } from "~/utils/event-validation.server"; import { getTimezoneOffset, offsetDate } from "~/utils/misc"; @@ -39,10 +36,10 @@ export async function loader({ request }: LoaderFunctionArgs) { const addresses = await getAddresses(); - return json({ + return { validImageTypes, addresses, - }); + }; } export const meta: MetaFunction = () => { @@ -54,44 +51,23 @@ export async function action({ request }: ActionFunctionArgs) { const timeOffset = getTimezoneOffset(request); let maximumFileSizeExceeded = false; - const uploadHandler = unstable_composeUploadHandlers( - unstable_createFileUploadHandler({ - directory: process.env.IMAGE_UPLOAD_FOLDER, - }), - unstable_createMemoryUploadHandler(), - ); + const uploadHandler = async (fileUpload: FileUpload) => { + let storageKey = getStorageKey("temporary-key"); + await fileStorage.set(storageKey, fileUpload); + return fileStorage.get(storageKey); + }; - const formData = await unstable_parseMultipartFormData( - request, - async (part) => { - try { - const result = await uploadHandler(part); - return result; - } catch (error) { - if (error instanceof MaxPartSizeExceededError) { - maximumFileSizeExceeded = true; - return new File([], "cover"); - } - throw error; - } - }, - ); + const formData = await parseFormData(request, uploadHandler); const submission = parseWithZod(formData, { schema: (intent) => - ServerEventSchema.superRefine((data, ctx) => { + ServerEventSchema.superRefine((data) => { if (intent !== null) return { ...data }; - if (maximumFileSizeExceeded) { - ctx.addIssue({ - path: ["cover"], - code: z.ZodIssueCode.custom, - message: "File cannot be greater than 3MB", - }); - return; - } }), }); + console.log(submission.payload); + if ( submission.status !== "success" && submission.payload && @@ -99,11 +75,11 @@ export async function action({ request }: ActionFunctionArgs) { ) { // Remove the uploaded file from disk. // It will be sent again when submitting. - await (submission.payload.cover as unknown as NodeOnDiskFile).remove(); + await fileStorage.remove(getStorageKey("temporary-key")); } if (submission.status !== "success" || !submission.value) { - return json(submission.reply()); + return submission.reply(); } const { title, description, date, slots, price, cover, addressId } = @@ -130,7 +106,7 @@ export async function action({ request }: ActionFunctionArgs) { // Remove the file from disk. // It is in the database now. - await (cover as NodeOnDiskFile).remove(); + await fileStorage.remove(getStorageKey("temporary-key")); return redirect(`/admin/dinners/${event.id}`); } @@ -177,17 +153,21 @@ export default function DinnersPage() { errors={fields.date.errors} /> - - - +
+ + + +
= () => { diff --git a/app/routes/admin.locations.$locationId_.edit.tsx b/app/routes/admin.locations.$locationId_.edit.tsx index 90ee557..7b81698 100644 --- a/app/routes/admin.locations.$locationId_.edit.tsx +++ b/app/routes/admin.locations.$locationId_.edit.tsx @@ -4,7 +4,6 @@ import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, - json, redirect, } from "@remix-run/node"; import { Form, useActionData, useLoaderData } from "@remix-run/react"; @@ -26,9 +25,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!address) throw new Response("Not found", { status: 404 }); - return json({ + return { location: address, - }); + }; } export const meta: MetaFunction = () => { @@ -45,7 +44,7 @@ export async function action({ request, params }: ActionFunctionArgs) { const submission = parseWithZod(formData, { schema: AddressSchema }); if (submission.status !== "success" || !submission.value) { - return json(submission.reply()); + return submission.reply(); } const { streetName, houseNumber, zipCode, city } = submission.value; diff --git a/app/routes/admin.locations._index.tsx b/app/routes/admin.locations._index.tsx index 508d800..b59bf29 100644 --- a/app/routes/admin.locations._index.tsx +++ b/app/routes/admin.locations._index.tsx @@ -1,5 +1,5 @@ import { Address } from "@prisma/client"; -import { LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Link, useFetcher, useLoaderData } from "@remix-run/react"; import { Button } from "~/components/ui/button"; @@ -10,7 +10,7 @@ export async function loader({ request }: LoaderFunctionArgs) { await requireUserWithRole(request, ["moderator", "admin"]); const addresses = await getAddresses(); - return json({ addresses }); + return { addresses }; } export const meta: MetaFunction = () => { diff --git a/app/routes/admin.locations.new.tsx b/app/routes/admin.locations.new.tsx index d9665ed..76edf36 100644 --- a/app/routes/admin.locations.new.tsx +++ b/app/routes/admin.locations.new.tsx @@ -4,7 +4,6 @@ import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, - json, redirect, } from "@remix-run/node"; import { Form, useActionData } from "@remix-run/react"; @@ -18,7 +17,7 @@ import { requireUserWithRole } from "~/utils/session.server"; export async function loader({ request }: LoaderFunctionArgs) { await requireUserWithRole(request, ["moderator", "admin"]); - return json({}); + return {}; } export const meta: MetaFunction = () => { @@ -32,7 +31,7 @@ export async function action({ request }: ActionFunctionArgs) { const submission = parseWithZod(formData, { schema: AddressSchema }); if (submission.status !== "success" || !submission.value) { - return json(submission.reply()); + return submission.reply(); } const { streetName, houseNumber, zipCode, city } = submission.value; diff --git a/app/routes/admin.locations.tsx b/app/routes/admin.locations.tsx index fdcb8e0..199f882 100644 --- a/app/routes/admin.locations.tsx +++ b/app/routes/admin.locations.tsx @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; import { requireUserWithRole } from "~/utils/session.server"; @@ -6,7 +6,7 @@ import { requireUserWithRole } from "~/utils/session.server"; export async function loader({ request }: LoaderFunctionArgs) { await requireUserWithRole(request, ["moderator", "admin"]); - return json({}); + return {}; } export const meta: MetaFunction = () => { diff --git a/app/routes/admin.tsx b/app/routes/admin.tsx index f4bd3ca..e95b519 100644 --- a/app/routes/admin.tsx +++ b/app/routes/admin.tsx @@ -1,11 +1,11 @@ -import { LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; import { requireUserWithRole } from "~/utils/session.server"; export async function loader({ request }: LoaderFunctionArgs) { await requireUserWithRole(request, ["moderator", "admin"]); - return json({}); + return {}; } export const meta: MetaFunction = () => { diff --git a/app/routes/admin.users.$userId_.edit.tsx b/app/routes/admin.users.$userId_.edit.tsx index 4b86baa..1c80bbe 100644 --- a/app/routes/admin.users.$userId_.edit.tsx +++ b/app/routes/admin.users.$userId_.edit.tsx @@ -4,7 +4,6 @@ import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, - json, redirect, } from "@remix-run/node"; import { Form, useActionData, useLoaderData } from "@remix-run/react"; @@ -40,9 +39,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!user) throw new Response("Not found", { status: 404 }); - return json({ + return { user, - }); + }; } export const meta: MetaFunction = () => { @@ -82,7 +81,7 @@ export async function action({ request, params }: ActionFunctionArgs) { !submission.value || !submission.value.roleId ) { - return json(submission.reply()); + return submission.reply(); } const { roleId } = submission.value; diff --git a/app/routes/admin.users._index.tsx b/app/routes/admin.users._index.tsx index 27f045c..cc4ec85 100644 --- a/app/routes/admin.users._index.tsx +++ b/app/routes/admin.users._index.tsx @@ -1,9 +1,4 @@ -import { - LoaderFunctionArgs, - MetaFunction, - SerializeFrom, - json, -} from "@remix-run/node"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Link, useFetcher, useLoaderData } from "@remix-run/react"; import { Button } from "~/components/ui/button"; @@ -24,7 +19,7 @@ export async function loader({ request }: LoaderFunctionArgs) { }, }); - return json({ users }); + return { users }; } export const meta: MetaFunction = () => { @@ -49,7 +44,7 @@ export default function DinnersPage() { ); } -type User = Pick, "users">["users"][number]; +type User = Pick>, "users">["users"][number]; function User({ user }: { user: User }) { const deleteFetcher = useFetcher(); diff --git a/app/routes/admin.users.tsx b/app/routes/admin.users.tsx index 1a3446a..ced9f3e 100644 --- a/app/routes/admin.users.tsx +++ b/app/routes/admin.users.tsx @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, MetaFunction, json } from "@remix-run/node"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; import { requireUserWithRole } from "~/utils/session.server"; @@ -6,7 +6,7 @@ import { requireUserWithRole } from "~/utils/session.server"; export async function loader({ request }: LoaderFunctionArgs) { await requireUserWithRole(request, ["admin"]); - return json({}); + return {}; } export const meta: MetaFunction = () => { diff --git a/app/routes/dinners._index.tsx b/app/routes/dinners._index.tsx index fd510f8..63f7e7d 100644 --- a/app/routes/dinners._index.tsx +++ b/app/routes/dinners._index.tsx @@ -1,4 +1,4 @@ -import { MetaFunction, json } from "@remix-run/node"; +import { MetaFunction } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { DinnerCard } from "~/components/dinner-card"; @@ -11,7 +11,7 @@ export const loader = async () => { }, }); - return json({ events }); + return { events }; }; export const meta: MetaFunction = () => [{ title: "Dinners" }]; diff --git a/app/routes/dinners_.$dinnerId.tsx b/app/routes/dinners_.$dinnerId.tsx index fc43adf..4938cb3 100644 --- a/app/routes/dinners_.$dinnerId.tsx +++ b/app/routes/dinners_.$dinnerId.tsx @@ -9,7 +9,6 @@ import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction, - json, } from "@remix-run/node"; import { Form, useActionData, useLoaderData } from "@remix-run/react"; import invariant from "tiny-invariant"; @@ -73,7 +72,7 @@ export async function loader({ params }: LoaderFunctionArgs) { if (!event) throw new Response("Not found", { status: 404 }); - return json({ event }); + return { event }; } export async function action({ params, request }: ActionFunctionArgs) { @@ -85,7 +84,7 @@ export async function action({ params, request }: ActionFunctionArgs) { const submission = parseWithZod(formData, { schema }); if (submission.status !== "success" || !submission.value) { - return json(submission.reply()); + return submission.reply(); } const { signupPerson, people, comment } = submission.value; diff --git a/app/routes/join.tsx b/app/routes/join.tsx index 95b09f6..2f99aa8 100644 --- a/app/routes/join.tsx +++ b/app/routes/join.tsx @@ -5,7 +5,7 @@ import type { LoaderFunctionArgs, MetaFunction, } from "@remix-run/node"; -import { json, redirect } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; import { Form, Link, useActionData, useSearchParams } from "@remix-run/react"; import { z } from "zod"; @@ -44,7 +44,7 @@ const schema = z export const loader = async ({ request }: LoaderFunctionArgs) => { const userId = await getUserId(request); if (userId) return redirect("/"); - return json({}); + return {}; }; export const action = async ({ request }: ActionFunctionArgs) => { @@ -68,7 +68,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { }); if (submission.status !== "success" || !submission.value) { - return json(submission.reply()); + return submission.reply(); } const redirectTo = safeRedirect(submission.value.redirectTo, "/"); diff --git a/app/routes/login.tsx b/app/routes/login.tsx index b40411b..75bc544 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -5,7 +5,7 @@ import type { LoaderFunctionArgs, MetaFunction, } from "@remix-run/node"; -import { json, redirect } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; import { Form, Link, useActionData, useSearchParams } from "@remix-run/react"; import { z } from "zod"; @@ -34,7 +34,7 @@ const schema = z.object({ export const loader = async ({ request }: LoaderFunctionArgs) => { const userId = await getUserId(request); if (userId) return redirect("/"); - return json({}); + return {}; }; export const action = async ({ request }: ActionFunctionArgs) => { @@ -64,7 +64,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { !submission.value || !submission.value.user ) { - return json(submission.reply()); + return submission.reply(); } const redirectTo = safeRedirect(submission.value.redirectTo, "/"); diff --git a/app/routes/me.tsx b/app/routes/me.tsx index 68b9afa..182587e 100644 --- a/app/routes/me.tsx +++ b/app/routes/me.tsx @@ -1,5 +1,5 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { json, redirect } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { prisma } from "~/db.server"; @@ -23,7 +23,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { if (!user) throw await logout(request); - return json({ user }); + return { user }; }; export default function MeRoute() { diff --git a/app/utils/dinner-image-storage.server.ts b/app/utils/dinner-image-storage.server.ts new file mode 100644 index 0000000..145286e --- /dev/null +++ b/app/utils/dinner-image-storage.server.ts @@ -0,0 +1,16 @@ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { sep } from "node:path"; + +import { LocalFileStorage } from "@mjackson/file-storage/local"; + +const tmpDir = tmpdir(); +const tmpPath = mkdtempSync(`${tmpDir}${sep}`); + +export const fileStorage = new LocalFileStorage( + process.env.IMAGE_UPLOAD_FOLDER || tmpPath, +); + +export function getStorageKey(id: string) { + return `dinner-${id}-cover`; +} diff --git a/app/utils/event-validation.server.ts b/app/utils/event-validation.server.ts index 1efa0ef..7f660df 100644 --- a/app/utils/event-validation.server.ts +++ b/app/utils/event-validation.server.ts @@ -1,4 +1,3 @@ -import { NodeOnDiskFile } from "@remix-run/node"; import { z } from "zod"; export const ServerEventSchema = z.object({ @@ -14,7 +13,7 @@ export const ServerEventSchema = z.object({ .min(0, "Price cannot be less than 0") .int(), cover: z - .instanceof(NodeOnDiskFile, { message: "You must select a file" }) + .instanceof(File, { message: "You must select a file" }) .refine((file) => { return file.size !== 0; }, "You must select a file") diff --git a/cypress/support/create-user.ts b/cypress/support/create-user.ts index 78e15b8..6a6cc5c 100644 --- a/cypress/support/create-user.ts +++ b/cypress/support/create-user.ts @@ -4,14 +4,11 @@ // and it will log out the cookie value you can use to interact with the server // as that new user. -import { installGlobals } from "@remix-run/node"; import { parse } from "cookie"; import { createUser } from "~/models/user.server"; import { createUserSession } from "~/utils/session.server"; -installGlobals(); - async function createAndLogin(email: string) { if (!email) { throw new Error("email required for login"); diff --git a/cypress/support/delete-user.ts b/cypress/support/delete-user.ts index 0ec6510..551e041 100644 --- a/cypress/support/delete-user.ts +++ b/cypress/support/delete-user.ts @@ -4,12 +4,9 @@ // and that user will get deleted import { Prisma } from "@prisma/client"; -import { installGlobals } from "@remix-run/node"; import { prisma } from "~/db.server"; -installGlobals(); - async function deleteUser(email: string) { if (!email) { throw new Error("email required for login"); diff --git a/package-lock.json b/package-lock.json index 86e718e..8940820 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "dependencies": { "@conform-to/react": "^1.2.2", "@conform-to/zod": "^1.2.2", + "@headlessui/react": "^2.2.0", + "@mjackson/file-storage": "^0.3.0", + "@mjackson/form-data-parser": "^0.5.1", "@prisma/client": "^6.1.0", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dropdown-menu": "^2.1.4", @@ -35,6 +38,8 @@ "@eslint/js": "^9.17.0", "@faker-js/faker": "^9.3.0", "@remix-run/dev": "^2.15.2", + "@remix-run/fs-routes": "^2.15.2", + "@remix-run/route-config": "^2.15.2", "@testing-library/cypress": "^10.0.2", "@testing-library/jest-dom": "^6.6.3", "@types/bcryptjs": "^2.4.5", @@ -61,6 +66,7 @@ "postcss": "^8.4.39", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.5", + "remix-flat-routes": "^0.6.5", "sharp": "^0.33.4", "start-server-and-test": "^2.0.9", "tailwindcss": "^3.4.17", @@ -1448,6 +1454,20 @@ "@floating-ui/utils": "^0.2.8" } }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", @@ -1482,6 +1502,24 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@headlessui/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz", + "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.17.1", + "@react-aria/interactions": "^3.21.3", + "@tanstack/react-virtual": "^3.8.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2188,6 +2226,51 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@mjackson/file-storage": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@mjackson/file-storage/-/file-storage-0.3.0.tgz", + "integrity": "sha512-VhVPmLEaHemr9+vGNJWHMI3g4p4tm0evk8U6pVSoFq7Sid3xeOJwXgx6ELK8ewqRZDiMdJQjsnYU+/LVBfSu4w==", + "dependencies": { + "@mjackson/lazy-file": "^3.3.0" + } + }, + "node_modules/@mjackson/form-data-parser": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@mjackson/form-data-parser/-/form-data-parser-0.5.1.tgz", + "integrity": "sha512-2AHKnpjCoHWQmf00X8wD9v4q1zN962j+XfjevtAsNWj8S3GIJLr1/k6khHT4Fl2qno1EaZK1/+kk+geRmbWwfw==", + "dependencies": { + "@mjackson/multipart-parser": "^0.7.2" + } + }, + "node_modules/@mjackson/headers": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@mjackson/headers/-/headers-0.8.0.tgz", + "integrity": "sha512-A6nE9ZKTGX1hTHQOeoYn/f7+xRTgzixHWu9jtZ8zYgZYq30MPtZIRXqEyY8bWaIILr+zWdmUPQg/R/8e+e/8Nw==" + }, + "node_modules/@mjackson/lazy-file": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@mjackson/lazy-file/-/lazy-file-3.3.0.tgz", + "integrity": "sha512-zNZ6BDZddQZzp763XhWHDtKDV520SMNgaZCuHctA8sjPOzLniE1nb4t9/iU9yPGA2r9fdO3Pv1wKLQZEia4XvA==", + "dependencies": { + "mrmime": "^2.0.0" + } + }, + "node_modules/@mjackson/lazy-file/node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@mjackson/multipart-parser": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@mjackson/multipart-parser/-/multipart-parser-0.7.2.tgz", + "integrity": "sha512-Ic7XeOLOoc/ivrQDzDWkkNCTd7bV6gRbXpWQq95xg8OFXZXKglzeqJhGais00HoB/b2cdJ76cJ06jPOyEBs52w==", + "dependencies": { + "@mjackson/headers": "^0.8.0" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.37.3", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.3.tgz", @@ -3054,6 +3137,83 @@ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" }, + "node_modules/@react-aria/focus": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.19.0.tgz", + "integrity": "sha512-hPF9EXoUQeQl1Y21/rbV2H4FdUR2v+4/I0/vB+8U3bT1CJ+1AFj1hc/rqx2DqEwDlEwOHN+E4+mRahQmlybq0A==", + "dependencies": { + "@react-aria/interactions": "^3.22.5", + "@react-aria/utils": "^3.26.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.5.tgz", + "integrity": "sha512-kMwiAD9E0TQp+XNnOs13yVJghiy8ET8L0cbkeuTgNI96sOAp/63EJ1FSrDf17iD8sdjt41LafwX/dKXW9nCcLQ==", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-aria/utils": "^3.26.0", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", + "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.26.0.tgz", + "integrity": "sha512-LkZouGSjjQ0rEqo4XJosS4L3YC/zzQkfRM3KoqK6fUOmUJ9t0jQ09WjiF+uOoG9u+p30AVg3TrZRUWmoTS+koQ==", + "dependencies": { + "@react-aria/ssr": "^3.9.7", + "@react-stately/utils": "^3.10.5", + "@react-types/shared": "^3.26.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.5", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz", + "integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.26.0.tgz", + "integrity": "sha512-6FuPqvhmjjlpEDLTiYx29IJCbCNWPlsyO+ZUmCUXzhUv2ttShOXfw8CmeHWHftT/b2KweAWuzqSlfeXPR76jpw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@remix-run/dev": { "version": "2.15.2", "resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.15.2.tgz", @@ -3180,6 +3340,25 @@ } } }, + "node_modules/@remix-run/fs-routes": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/@remix-run/fs-routes/-/fs-routes-2.15.2.tgz", + "integrity": "sha512-Ozf0ab5OcyJ0OcOHtjO726VptDXMOp+xek6n/Wp3A+IcaY2tmWWChYsw8O0dKZ5Q5BCJv3hlLX99PLadX9wqeg==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@remix-run/dev": "^2.15.2", + "@remix-run/route-config": "^2.15.2", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@remix-run/node": { "version": "2.15.2", "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.15.2.tgz", @@ -3230,6 +3409,27 @@ } } }, + "node_modules/@remix-run/route-config": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/@remix-run/route-config/-/route-config-2.15.2.tgz", + "integrity": "sha512-xAbH3VCgsvUK3YH2INU7yhZMjbw5bGoSaGEGS3k+QdX8a9gWeFKPex+w76ZNjVNVVOMOE+yJVpdN9RrIu0Pfeg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@remix-run/dev": "^2.15.2", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", @@ -3293,6 +3493,33 @@ "node": ">= 0.6" } }, + "node_modules/@remix-run/v1-route-convention": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@remix-run/v1-route-convention/-/v1-route-convention-0.1.4.tgz", + "integrity": "sha512-fVTr9YlNLWfaiM/6Y56sOtcY8x1bBJQHY0sDWO5+Z/vjJ2Ni7fe2fwrzs1jUFciMPXqBQdFGePnkuiYLz3cuUA==", + "dev": true, + "dependencies": { + "minimatch": "^7.4.3" + }, + "peerDependencies": { + "@remix-run/dev": "^1.15.0 || ^2.0.0" + } + }, + "node_modules/@remix-run/v1-route-convention/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@remix-run/web-blob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@remix-run/web-blob/-/web-blob-3.1.0.tgz", @@ -3638,6 +3865,39 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz", + "integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==", + "dependencies": { + "@tanstack/virtual-core": "3.11.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz", + "integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/cypress": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.0.2.tgz", @@ -13700,6 +13960,49 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remix-flat-routes": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/remix-flat-routes/-/remix-flat-routes-0.6.5.tgz", + "integrity": "sha512-VvPak+LCxL4Fm6Kb/nqPLipB71k9p+GXpzRNPVxs9FmCeJ7hxVmQ3HQMpStzuRQyAh0PMkaX6mBiRlCRHTCYHw==", + "dev": true, + "dependencies": { + "@remix-run/v1-route-convention": "^0.1.3", + "fs-extra": "^11.1.1", + "minimatch": "^5.1.0" + }, + "bin": { + "migrate-flat-routes": "dist/cli.js" + }, + "peerDependencies": { + "@remix-run/dev": "^1.15.0 || ^2" + } + }, + "node_modules/remix-flat-routes/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/remix-flat-routes/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -14810,6 +15113,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tailwind-merge": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", diff --git a/package.json b/package.json index 47d14c1..85c8044 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,9 @@ "dependencies": { "@conform-to/react": "^1.2.2", "@conform-to/zod": "^1.2.2", + "@headlessui/react": "^2.2.0", + "@mjackson/file-storage": "^0.3.0", + "@mjackson/form-data-parser": "^0.5.1", "@prisma/client": "^6.1.0", "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dropdown-menu": "^2.1.4", @@ -60,6 +63,8 @@ "@eslint/js": "^9.17.0", "@faker-js/faker": "^9.3.0", "@remix-run/dev": "^2.15.2", + "@remix-run/fs-routes": "^2.15.2", + "@remix-run/route-config": "^2.15.2", "@testing-library/cypress": "^10.0.2", "@testing-library/jest-dom": "^6.6.3", "@types/bcryptjs": "^2.4.5", @@ -86,6 +91,7 @@ "postcss": "^8.4.39", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.5", + "remix-flat-routes": "^0.6.5", "sharp": "^0.33.4", "start-server-and-test": "^2.0.9", "tailwindcss": "^3.4.17", diff --git a/prisma/migrations/20241223151922_price_to_int/migration.sql b/prisma/migrations/20241223151922_price_to_int/migration.sql new file mode 100644 index 0000000..da5eed6 --- /dev/null +++ b/prisma/migrations/20241223151922_price_to_int/migration.sql @@ -0,0 +1,29 @@ +/* + Warnings: + + - You are about to alter the column `price` on the `Event` table. The data in that column could be lost. The data in that column will be cast from `Decimal` to `Int`. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Event" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "date" DATETIME NOT NULL, + "slots" INTEGER NOT NULL, + "price" INTEGER NOT NULL, + "imageId" TEXT NOT NULL, + "addressId" TEXT NOT NULL, + "createdById" TEXT NOT NULL, + CONSTRAINT "Event_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "EventImage" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Event_addressId_fkey" FOREIGN KEY ("addressId") REFERENCES "Address" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "Event_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_Event" ("addressId", "createdById", "date", "description", "id", "imageId", "price", "slots", "title") SELECT "addressId", "createdById", "date", "description", "id", "imageId", "price", "slots", "title" FROM "Event"; +DROP TABLE "Event"; +ALTER TABLE "new_Event" RENAME TO "Event"; +CREATE UNIQUE INDEX "Event_imageId_key" ON "Event"("imageId"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index e5e5c47..e1640d1 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) +# It should be added in your version-control system (e.g., Git) provider = "sqlite" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0ca013f..0df61a4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -60,7 +60,7 @@ model Event { description String date DateTime slots Int - price Decimal + price Int image EventImage @relation(fields: [imageId], references: [id], onDelete: Cascade, onUpdate: Cascade) imageId String @unique diff --git a/test/setup-test-env.ts b/test/setup-test-env.ts index 8f910da..f149f27 100644 --- a/test/setup-test-env.ts +++ b/test/setup-test-env.ts @@ -1,4 +1 @@ -import { installGlobals } from "@remix-run/node"; import "@testing-library/jest-dom/vitest"; - -installGlobals(); diff --git a/vite.config.ts b/vite.config.ts index d4f2e04..9be8e34 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,13 @@ import { vitePlugin as remix } from "@remix-run/dev"; -import { installGlobals } from "@remix-run/node"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; -installGlobals(); +declare module "@remix-run/server-runtime" { + // or cloudflare, deno, etc. + interface Future { + v3_singleFetch: true; + } +} export default defineConfig({ server: { port: 3000 }, @@ -15,6 +19,7 @@ export default defineConfig({ v3_throwAbortReason: true, v3_singleFetch: true, v3_lazyRouteDiscovery: true, + v3_routeConfig: true, }, }), tsconfigPaths(),