From 39ea1ae5aa1beec06f9a62676819c8caa40bc860 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Fri, 10 Jul 2026 03:05:55 -0500 Subject: [PATCH] feat(pets): add profile and medical records --- src/app/pets/[id]/edit/page.tsx | 17 +++ src/app/pets/[id]/page.tsx | 33 +++++ src/app/pets/new/page.tsx | 12 ++ .../pets/components/medical-records.tsx | 121 ++++++++++++++++++ src/modules/pets/components/pet-form.tsx | 81 ++++++++++++ src/modules/pets/server/actions.ts | 59 +++++++++ 6 files changed, 323 insertions(+) create mode 100644 src/app/pets/[id]/edit/page.tsx create mode 100644 src/app/pets/[id]/page.tsx create mode 100644 src/app/pets/new/page.tsx create mode 100644 src/modules/pets/components/medical-records.tsx create mode 100644 src/modules/pets/components/pet-form.tsx diff --git a/src/app/pets/[id]/edit/page.tsx b/src/app/pets/[id]/edit/page.tsx new file mode 100644 index 0000000..d5f51d8 --- /dev/null +++ b/src/app/pets/[id]/edit/page.tsx @@ -0,0 +1,17 @@ +import { notFound } from "next/navigation"; +import { DetailBackLink } from "@/components/detail-back-link"; +import { PetForm } from "@/modules/pets/components/pet-form"; +import { getPet } from "@/modules/pets/server/queries"; + +export default async function EditPetPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const pet = await getPet(id); + if (!pet) notFound(); + return ( +
+ +

Edit pet

+ +
+ ); +} diff --git a/src/app/pets/[id]/page.tsx b/src/app/pets/[id]/page.tsx new file mode 100644 index 0000000..b8303d3 --- /dev/null +++ b/src/app/pets/[id]/page.tsx @@ -0,0 +1,33 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { DetailBackLink } from "@/components/detail-back-link"; +import { getPet } from "@/modules/pets/server/queries"; +import { MedicalRecords } from "@/modules/pets/components/medical-records"; + +export default async function PetPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const pet = await getPet(id); + if (!pet) notFound(); + return ( +
+ +
+
+

{pet.name}

+

+ {[pet.species, pet.breed].filter(Boolean).join(" · ")} +

+
+ + Edit + +
+
+

Profile

+ {pet.birthDate &&

Born {pet.birthDate}

} + {pet.notes &&

{pet.notes}

} + +
+
+ ); +} diff --git a/src/app/pets/new/page.tsx b/src/app/pets/new/page.tsx new file mode 100644 index 0000000..380e21e --- /dev/null +++ b/src/app/pets/new/page.tsx @@ -0,0 +1,12 @@ +import { DetailBackLink } from "@/components/detail-back-link"; +import { PetForm } from "@/modules/pets/components/pet-form"; + +export default function NewPetPage() { + return ( +
+ +

Add pet

+ +
+ ); +} diff --git a/src/modules/pets/components/medical-records.tsx b/src/modules/pets/components/medical-records.tsx new file mode 100644 index 0000000..dc905f0 --- /dev/null +++ b/src/modules/pets/components/medical-records.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useTransition } from "react"; +import type { PetDetailDto } from "../server/queries"; +import { createAppointment, createPrescription, createVaccination } from "../server/actions"; + +export function MedicalRecords({ pet }: { pet: PetDetailDto }) { + const [pending, startTransition] = useTransition(); + function add(form: HTMLFormElement, action: () => Promise) { + startTransition(async () => { + await action(); + form.reset(); + location.reload(); + }); + } + return ( +
+
+

Appointments

+
{ + event.preventDefault(); + const form = event.currentTarget; + const data = new FormData(form); + add(form, () => + createAppointment({ + petId: pet.id, + title: String(data.get("title")), + appointmentAt: String(data.get("appointmentAt")), + clinic: String(data.get("clinic")) || null, + reminderOffsets: [30], + }), + ); + }} + className="flex flex-wrap gap-2" + > + + + + +
+
    + {pet.appointments.map((item) => ( +
  • + {item.title} — {new Date(item.appointmentAt).toLocaleString()} +
  • + ))} +
+
+
+

Vaccinations

+
{ + event.preventDefault(); + const form = event.currentTarget; + const data = new FormData(form); + add(form, () => + createVaccination({ + petId: pet.id, + name: String(data.get("name")), + administeredOn: String(data.get("administeredOn")), + dueOn: String(data.get("dueOn")) || null, + }), + ); + }} + className="flex flex-wrap gap-2" + > + + + + +
+
    + {pet.vaccinations.map((item) => ( +
  • + {item.name} — {item.administeredOn} +
  • + ))} +
+
+
+

Prescriptions

+
{ + event.preventDefault(); + const form = event.currentTarget; + const data = new FormData(form); + add(form, () => + createPrescription({ + petId: pet.id, + medication: String(data.get("medication")), + dosage: String(data.get("dosage")), + expiresOn: String(data.get("expiresOn")) || null, + active: true, + }), + ); + }} + className="flex flex-wrap gap-2" + > + + + + +
+
    + {pet.prescriptions.map((item) => ( +
  • + {item.medication} — {item.dosage} +
  • + ))} +
+
+
+ ); +} diff --git a/src/modules/pets/components/pet-form.tsx b/src/modules/pets/components/pet-form.tsx new file mode 100644 index 0000000..be3fa4b --- /dev/null +++ b/src/modules/pets/components/pet-form.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import type { PetDetailDto } from "../server/queries"; +import { createPet, updatePet } from "../server/actions"; +import { Input } from "@/components/ui/input"; + +export function PetForm({ pet }: { pet?: PetDetailDto }) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + function submit(event: React.FormEvent) { + event.preventDefault(); + const data = new FormData(event.currentTarget); + const input = { + name: String(data.get("name") ?? ""), + species: String(data.get("species") ?? ""), + breed: String(data.get("breed") ?? "") || null, + birthDate: String(data.get("birthDate") ?? "") || null, + notes: String(data.get("notes") ?? "") || null, + images: pet?.images ?? [], + primaryImageUrl: pet?.primaryImageUrl ?? null, + }; + setError(null); + startTransition(async () => { + try { + if (pet) { + await updatePet({ id: pet.id, ...input }); + router.push(`/pets/${pet.id}`); + } else { + const created = await createPet(input); + router.push(`/pets/${created.id}`); + } + router.refresh(); + } catch { + setError("Could not save pet."); + } + }); + } + + return ( +
+ + + + +