diff --git a/app/components/loading-overlay..tsx b/app/components/loading-overlay..tsx new file mode 100644 index 0000000..650c702 --- /dev/null +++ b/app/components/loading-overlay..tsx @@ -0,0 +1,17 @@ +import type { ReactNode } from "react"; +import { useNavigation } from "react-router"; +import { useSpinDelay } from "spin-delay"; + +export function LoadingOverlay({ children }: { children?: ReactNode }) { + const navigation = useNavigation(); + const isLoading = navigation.state === "loading"; + const isSearching = new URLSearchParams(navigation.location?.search).has("q"); + + const shouldShow = useSpinDelay(isLoading && !isSearching); + + return ( +
+ {children} +
+ ); +} diff --git a/app/routes/_auth.join.tsx b/app/routes/_auth.join.tsx index bf796d7..1eda5d5 100644 --- a/app/routes/_auth.join.tsx +++ b/app/routes/_auth.join.tsx @@ -3,6 +3,7 @@ import { getZodConstraint, parseWithZod } from "@conform-to/zod"; import { data, Form, Link, useSearchParams } from "react-router"; import { z } from "zod"; import { ErrorList } from "~/components/forms"; +import { Logo } from "~/components/logo"; import { Button } from "~/components/ui/button"; import { Card, @@ -128,95 +129,98 @@ export default function Component({ actionData }: Route.ComponentProps) { const [searchParams] = useSearchParams(); return ( - - - -

Sign Up

-
- - Enter your information to create an account - -
- -
-
-
- - - -
-
+
+ + + + +

Sign Up

+
+ + Enter your information to create an account + +
+ + +
- +
+
+
+ + + +
+
+ + + +
+
- +
+
+ + + +
+ +
-
- - - -
-
- - - -
- - -
- -

- Already have an account?{" "} - - Sign in - -

- - + +

+ Already have an account?{" "} + + Sign in + +

+ + +
); } diff --git a/app/routes/_auth.login.tsx b/app/routes/_auth.login.tsx index 5b56771..fb87923 100644 --- a/app/routes/_auth.login.tsx +++ b/app/routes/_auth.login.tsx @@ -3,6 +3,7 @@ import { getZodConstraint, parseWithZod } from "@conform-to/zod"; import { data, Form, Link, useSearchParams } from "react-router"; import { z } from "zod"; import { ErrorList } from "~/components/forms"; +import { Logo } from "~/components/logo"; import { Button } from "~/components/ui/button"; import { Card, @@ -88,57 +89,60 @@ export default function Component({ actionData }: Route.ComponentProps) { const [searchParams] = useSearchParams(); return ( - - - -

Login

-
- - Enter your email below to login to your account - -
- -
-
-
- - - +
+ + + + +

Login

+
+ + Enter your details below to login to your account + +
+ + +
+
+ + + +
+
+ + + +
+ +
-
- - - -
- - -
- -

- Don't have an account?{" "} - - Sign up - -

- - + +

+ Don't have an account?{" "} + + Sign up + +

+ + +
); } diff --git a/app/routes/_dashboard.contacts.$contactId._index.tsx b/app/routes/_dashboard.contacts.$contactId._index.tsx index ed20a99..024e431 100644 --- a/app/routes/_dashboard.contacts.$contactId._index.tsx +++ b/app/routes/_dashboard.contacts.$contactId._index.tsx @@ -1,7 +1,6 @@ -import { invariantResponse } from "@epic-web/invariant"; import { Pencil1Icon } from "@radix-ui/react-icons"; import { format, formatDistanceStrict } from "date-fns"; -import { Form, Link } from "react-router"; +import { data, Form, Link } from "react-router"; import { EmptyState } from "~/components/empty-state"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; @@ -26,11 +25,11 @@ export async function loader({ request, params }: Route.LoaderArgs) { }, where: { id: params.contactId, userId }, }); - invariantResponse( - contact, - `No contact with the id "${params.contactId}" exists.`, - { status: 404 }, - ); + if (!contact) { + throw data(`No contact with the id "${params.contactId}" exists.`, { + status: 404, + }); + } return { contact }; } diff --git a/app/routes/_dashboard.contacts.$contactId.notes.tsx b/app/routes/_dashboard.contacts.$contactId.notes.tsx index 910567b..7354d79 100644 --- a/app/routes/_dashboard.contacts.$contactId.notes.tsx +++ b/app/routes/_dashboard.contacts.$contactId.notes.tsx @@ -1,5 +1,4 @@ import { parseWithZod } from "@conform-to/zod"; -import { invariantResponse } from "@epic-web/invariant"; import { DotsHorizontalIcon, UpdateIcon } from "@radix-ui/react-icons"; import { compareAsc, format, isToday, isYesterday } from "date-fns"; import { useState } from "react"; @@ -39,11 +38,11 @@ export async function action({ request, params }: Route.ActionArgs) { select: { id: true }, where: { id: params.contactId, userId }, }); - invariantResponse( - contact, - `No contact with the id "${params.contactId}" exists.`, - { status: 404 }, - ); + if (!contact) { + throw data(`No contact with the id "${params.contactId}" exists.`, { + status: 404, + }); + } const formData = await request.formData(); @@ -56,6 +55,7 @@ export async function action({ request, params }: Route.ActionArgs) { } const { text, date } = submission.value; + await db.note.create({ select: { id: true }, data: { text, date, contact: { connect: { id: params.contactId } } }, diff --git a/app/routes/_dashboard.contacts.$contactId.notes_.$noteId.edit.tsx b/app/routes/_dashboard.contacts.$contactId.notes_.$noteId.edit.tsx index 101c835..2907f35 100644 --- a/app/routes/_dashboard.contacts.$contactId.notes_.$noteId.edit.tsx +++ b/app/routes/_dashboard.contacts.$contactId.notes_.$noteId.edit.tsx @@ -1,5 +1,4 @@ import { parseWithZod } from "@conform-to/zod"; -import { invariantResponse } from "@epic-web/invariant"; import { ChevronLeftIcon, TrashIcon } from "@radix-ui/react-icons"; import { data, Form, Link, redirect, useNavigation } from "react-router"; import { NoteForm, NoteFormSchema } from "~/components/note-form"; @@ -20,9 +19,11 @@ export async function loader({ params }: Route.LoaderArgs) { select: { text: true, date: true }, where: { id: params.noteId }, }); - invariantResponse(note, `No note with the id "${params.noteId}" exists.`, { - status: 404, - }); + if (!note) { + throw data(`No note with the id "${params.noteId}" exists.`, { + status: 404, + }); + } return { note }; } @@ -34,42 +35,51 @@ export async function action({ request, params }: Route.ActionArgs) { select: { id: true }, where: { id: params.contactId, userId }, }); - invariantResponse( - contact, - `No contact with the id "${params.contactId}" exists.`, - { status: 404 }, - ); + if (!contact) { + throw data(`No contact with the id "${params.contactId}" exists.`, { + status: 404, + }); + } const note = await db.note.findUnique({ select: { id: true }, where: { id: params.noteId }, }); - invariantResponse(note, `No note with the id "${params.noteId}" exists.`, { - status: 404, - }); + if (!note) { + throw data(`No note with the id "${params.noteId}" exists.`, { + status: 404, + }); + } const formData = await request.formData(); - if (formData.get("intent") === "deleteNote") { - await db.note.delete({ - select: { id: true }, - where: { id: params.noteId }, - }); - } else { - const submission = parseWithZod(formData, { schema: NoteFormSchema }); - if (submission.status !== "success") { - return data( - { result: submission.reply() }, - { status: submission.status === "error" ? 400 : 200 }, - ); + switch (formData.get("intent")) { + case "editNote": { + const submission = parseWithZod(formData, { schema: NoteFormSchema }); + if (submission.status !== "success") { + return data( + { result: submission.reply() }, + { status: submission.status === "error" ? 400 : 200 }, + ); + } + + const updates = submission.value; + await db.note.update({ + select: { id: true }, + data: updates, + where: { id: params.noteId }, + }); + + break; } + case "deleteNote": { + await db.note.delete({ + select: { id: true }, + where: { id: params.noteId }, + }); - const updates = submission.value; - await db.note.update({ - select: { id: true }, - data: updates, - where: { id: params.noteId }, - }); + break; + } } throw redirect(`/contacts/${params.contactId}/notes`); diff --git a/app/routes/_dashboard.contacts.$contactId.tsx b/app/routes/_dashboard.contacts.$contactId.tsx index 87d3fa7..47f2ab6 100644 --- a/app/routes/_dashboard.contacts.$contactId.tsx +++ b/app/routes/_dashboard.contacts.$contactId.tsx @@ -8,6 +8,7 @@ import { TrashIcon, } from "@radix-ui/react-icons"; import { + data, Form, Link, NavLink, @@ -67,39 +68,40 @@ export async function action({ request, params }: Route.ActionArgs) { select: { id: true }, where: { id: params.contactId, userId }, }); - invariantResponse( - contact, - `No contact with the id "${params.contactId}" exists.`, - { status: 404 }, - ); + if (!contact) { + throw data(`No contact with the id "${params.contactId}" exists.`, { + status: 404, + }); + } const formData = await request.formData(); - if (formData.get("intent") === "favorite") { - const favorite = formData.get("favorite"); + switch (formData.get("intent")) { + case "favoriteContact": { + const favorite = formData.get("favorite"); - await db.contact.update({ - select: { id: true }, - data: { favorite: favorite === "true" }, - where: { id: params.contactId, userId }, - }); - - return { ok: true }; - } + await db.contact.update({ + select: { id: true }, + data: { favorite: favorite === "true" }, + where: { id: params.contactId, userId }, + }); - if (formData.get("intent") === "delete") { - await db.contact.delete({ - select: { id: true }, - where: { id: params.contactId, userId }, - }); + return { ok: true }; + } + case "deleteContact": { + await db.contact.delete({ + select: { id: true }, + where: { id: params.contactId, userId }, + }); - return redirect("/contacts"); + return redirect("/contacts"); + } + default: { + throw data(`Invalid intent: ${formData.get("intent") ?? "Missing"}`, { + status: 400, + }); + } } - - invariantResponse( - false, - `Invalid intent: ${formData.get("intent") ?? "Missing"}`, - ); } export function ErrorBoundary() { @@ -186,7 +188,7 @@ export default function Component({ loaderData }: Route.ComponentProps) { } }} > - +