From 009090b381c6c3a61d6aab56ca56305e7472c510 Mon Sep 17 00:00:00 2001 From: Arnaud Nelissen Date: Mon, 15 Jun 2026 17:27:01 +0200 Subject: [PATCH] feat: running timers, symptom log, search, PDF export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dashboard: ActiveTimers widget — live h:mm:ss for any in-progress timed event (breastfeed, nap, sleep, walk…), Terminer button patches endedAt - dashboard: reorganised quick-log groups; Santé group (Température, Traitement, Symptôme) - events: SYMPTOM type — tag chips (Fièvre, Reflux, Colique, Éruption, Toux, Congestion, Diarrhée, Vomissement, Irritabilité, Pleurs excessifs), stored in metadata.tags - prisma: migration adding SYMPTOM to EventType enum - search: GET /api/search (events by notes ILIKE, journal by content ILIKE), /search page with debounced input + keyword highlight - nav: Recherche link added to LINKS + More drawer - growth: PDF export button — generates formatted table with all measurements, zebra rows, downloads croissance-{name}.pdf Co-Authored-By: Claude Sonnet 4.6 --- .../20260615135137_init/migration.sql | 156 ++++++++++++++++ .../20260615200000_add_symptom/migration.sql | 1 + prisma/schema.prisma | 1 + public/sw.js | 1 + public/swe-worker-5c72df51bb1f6ee0.js | 1 + public/workbox-f8dc152a.js | 1 + src/app/(app)/dashboard/page.tsx | 70 +++++++- src/app/(app)/growth/page.tsx | 72 +++++++- src/app/(app)/search/page.tsx | 169 ++++++++++++++++++ src/app/(app)/timeline/page.tsx | 4 + src/app/api/search/route.ts | 36 ++++ src/components/event-modal.tsx | 31 ++++ src/components/nav.tsx | 3 + src/lib/event-config.ts | 13 +- 14 files changed, 549 insertions(+), 10 deletions(-) create mode 100644 prisma/migrations/20260615135137_init/migration.sql create mode 100644 prisma/migrations/20260615200000_add_symptom/migration.sql create mode 100644 public/sw.js create mode 100644 public/swe-worker-5c72df51bb1f6ee0.js create mode 100644 public/workbox-f8dc152a.js create mode 100644 src/app/(app)/search/page.tsx create mode 100644 src/app/api/search/route.ts diff --git a/prisma/migrations/20260615135137_init/migration.sql b/prisma/migrations/20260615135137_init/migration.sql new file mode 100644 index 0000000..ff07e75 --- /dev/null +++ b/prisma/migrations/20260615135137_init/migration.sql @@ -0,0 +1,156 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "EventType" ADD VALUE 'PUMP'; +ALTER TYPE "EventType" ADD VALUE 'BATH'; +ALTER TYPE "EventType" ADD VALUE 'WALK'; +ALTER TYPE "EventType" ADD VALUE 'TUMMY_TIME'; + +-- AlterTable +ALTER TABLE "Family" ADD COLUMN "pinnedNote" TEXT, +ADD COLUMN "pinnedNoteAuthor" TEXT, +ADD COLUMN "pinnedNoteUpdatedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "forcedLogoutAt" TIMESTAMP(3); + +-- CreateTable +CREATE TABLE "EventTemplate" ( + "id" TEXT NOT NULL, + "familyId" TEXT NOT NULL, + "label" TEXT NOT NULL, + "type" "EventType" NOT NULL, + "metadata" JSONB, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "EventTemplate_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "JournalNote" ( + "id" TEXT NOT NULL, + "babyId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "date" TIMESTAMP(3) NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "JournalNote_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Milestone" ( + "id" TEXT NOT NULL, + "babyId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "date" TIMESTAMP(3) NOT NULL, + "notes" TEXT, + "icon" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Milestone_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Vaccination" ( + "id" TEXT NOT NULL, + "babyId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "date" TIMESTAMP(3), + "dueDate" TIMESTAMP(3), + "notes" TEXT, + "done" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Vaccination_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "userName" TEXT NOT NULL, + "action" TEXT NOT NULL, + "targetId" TEXT, + "familyId" TEXT, + "detail" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MedicationProfile" ( + "id" TEXT NOT NULL, + "familyId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "molecule" TEXT, + "defaultDose" TEXT, + "unit" TEXT NOT NULL DEFAULT 'mL', + "intervalHours" DOUBLE PRECISION NOT NULL DEFAULT 6, + "minIntervalHours" DOUBLE PRECISION, + "isPreset" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "MedicationProfile_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MilkStock" ( + "id" TEXT NOT NULL, + "babyId" TEXT NOT NULL, + "volume" DOUBLE PRECISION NOT NULL, + "storedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" TIMESTAMP(3) NOT NULL, + "location" TEXT NOT NULL DEFAULT 'fridge', + "notes" TEXT, + "used" BOOLEAN NOT NULL DEFAULT false, + "usedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "MilkStock_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MedicationReminder" ( + "id" TEXT NOT NULL, + "babyId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "dose" TEXT, + "unit" TEXT, + "intervalHours" DOUBLE PRECISION NOT NULL, + "startAt" TIMESTAMP(3) NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "lastSentAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "MedicationReminder_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "EventTemplate" ADD CONSTRAINT "EventTemplate_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "Family"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JournalNote" ADD CONSTRAINT "JournalNote_babyId_fkey" FOREIGN KEY ("babyId") REFERENCES "Baby"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "JournalNote" ADD CONSTRAINT "JournalNote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Milestone" ADD CONSTRAINT "Milestone_babyId_fkey" FOREIGN KEY ("babyId") REFERENCES "Baby"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Vaccination" ADD CONSTRAINT "Vaccination_babyId_fkey" FOREIGN KEY ("babyId") REFERENCES "Baby"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MilkStock" ADD CONSTRAINT "MilkStock_babyId_fkey" FOREIGN KEY ("babyId") REFERENCES "Baby"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MedicationReminder" ADD CONSTRAINT "MedicationReminder_babyId_fkey" FOREIGN KEY ("babyId") REFERENCES "Baby"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260615200000_add_symptom/migration.sql b/prisma/migrations/20260615200000_add_symptom/migration.sql new file mode 100644 index 0000000..7658ad9 --- /dev/null +++ b/prisma/migrations/20260615200000_add_symptom/migration.sql @@ -0,0 +1 @@ +ALTER TYPE "EventType" ADD VALUE 'SYMPTOM'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f23ab56..9c96d58 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -154,6 +154,7 @@ enum EventType { BATH WALK TUMMY_TIME + SYMPTOM } model GrowthLog { diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..a34f08c --- /dev/null +++ b/public/sw.js @@ -0,0 +1 @@ +if(!self.define){let e,a={};const s=(s,i)=>(s=new URL(s+".js",i).href,a[s]||new Promise(a=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=a,document.head.appendChild(e)}else e=s,importScripts(s),a()}).then(()=>{let e=a[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(i,c)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(a[t])return;let n={};const r=e=>s(e,t),u={module:{uri:t},exports:n,require:r};a[t]=Promise.all(i.map(e=>u[e]||r(e))).then(e=>(c(...e),n))}}define(["./workbox-f8dc152a"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/7CMmehOzCXmlb0GhNG3Nx/_buildManifest.js",revision:"fe9554b067c65252b2059caa6078412a"},{url:"/_next/static/7CMmehOzCXmlb0GhNG3Nx/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/1561.e204774584b3e0ef.js",revision:"e204774584b3e0ef"},{url:"/_next/static/chunks/273acdc0.283b2d17506d0f00.js",revision:"283b2d17506d0f00"},{url:"/_next/static/chunks/4736.b0622d48175ceed5.js",revision:"b0622d48175ceed5"},{url:"/_next/static/chunks/5314-b5a538801baf20b5.js",revision:"b5a538801baf20b5"},{url:"/_next/static/chunks/5345-141c8937ca7b54b8.js",revision:"141c8937ca7b54b8"},{url:"/_next/static/chunks/5361-4edd618fa44a8306.js",revision:"4edd618fa44a8306"},{url:"/_next/static/chunks/6053-5eb18196066296f6.js",revision:"5eb18196066296f6"},{url:"/_next/static/chunks/6186-5e0cf09d4f710302.js",revision:"5e0cf09d4f710302"},{url:"/_next/static/chunks/6257-6fc728f92672038a.js",revision:"6fc728f92672038a"},{url:"/_next/static/chunks/667-c511e1ad9eeb042e.js",revision:"c511e1ad9eeb042e"},{url:"/_next/static/chunks/6851-cc6493e75cd6826b.js",revision:"cc6493e75cd6826b"},{url:"/_next/static/chunks/7099.ec5bac2c7b80e60d.js",revision:"ec5bac2c7b80e60d"},{url:"/_next/static/chunks/7452-a88904d17378b330.js",revision:"a88904d17378b330"},{url:"/_next/static/chunks/842-8579c6e86dde1473.js",revision:"8579c6e86dde1473"},{url:"/_next/static/chunks/8541-129e64d892fafd20.js",revision:"129e64d892fafd20"},{url:"/_next/static/chunks/9145-f2b3b231066b6754.js",revision:"f2b3b231066b6754"},{url:"/_next/static/chunks/92-b50fa1f77a909e6a.js",revision:"b50fa1f77a909e6a"},{url:"/_next/static/chunks/9232-777ef94ca5cb5dd2.js",revision:"777ef94ca5cb5dd2"},{url:"/_next/static/chunks/9313-e24b0598817cdbd6.js",revision:"e24b0598817cdbd6"},{url:"/_next/static/chunks/9e784b99.65159d4b3aca8bca.js",revision:"65159d4b3aca8bca"},{url:"/_next/static/chunks/afa2f26f-e021ee6d637e13db.js",revision:"e021ee6d637e13db"},{url:"/_next/static/chunks/app/(app)/admin/page-56e8e88d2ad907d7.js",revision:"56e8e88d2ad907d7"},{url:"/_next/static/chunks/app/(app)/calendar/page-1ca9c34e66fe1045.js",revision:"1ca9c34e66fe1045"},{url:"/_next/static/chunks/app/(app)/dashboard/page-7093866a02397d3f.js",revision:"7093866a02397d3f"},{url:"/_next/static/chunks/app/(app)/doctor-notes/page-de6e0a1c69544c3e.js",revision:"de6e0a1c69544c3e"},{url:"/_next/static/chunks/app/(app)/growth/page-004fef7d912fe40d.js",revision:"004fef7d912fe40d"},{url:"/_next/static/chunks/app/(app)/layout-857e0658fb257bd8.js",revision:"857e0658fb257bd8"},{url:"/_next/static/chunks/app/(app)/medications/page-bc153349fb4ef1e4.js",revision:"bc153349fb4ef1e4"},{url:"/_next/static/chunks/app/(app)/milestones/page-4b87fc1b4124e2b4.js",revision:"4b87fc1b4124e2b4"},{url:"/_next/static/chunks/app/(app)/milk/page-00a224682b28979b.js",revision:"00a224682b28979b"},{url:"/_next/static/chunks/app/(app)/notes/page-68fc3de845807982.js",revision:"68fc3de845807982"},{url:"/_next/static/chunks/app/(app)/photos/page-c99ff8cc7c139fbf.js",revision:"c99ff8cc7c139fbf"},{url:"/_next/static/chunks/app/(app)/reminders/page-c005b4f27edba8d0.js",revision:"c005b4f27edba8d0"},{url:"/_next/static/chunks/app/(app)/settings/page-c8bd881adb189618.js",revision:"c8bd881adb189618"},{url:"/_next/static/chunks/app/(app)/setup/page-6425879bdfecf589.js",revision:"6425879bdfecf589"},{url:"/_next/static/chunks/app/(app)/stats/page-f5fa19acce9c23db.js",revision:"f5fa19acce9c23db"},{url:"/_next/static/chunks/app/(app)/timeline/page-d774707d8ece4910.js",revision:"d774707d8ece4910"},{url:"/_next/static/chunks/app/(app)/vaccinations/page-6805af0a754c6f04.js",revision:"6805af0a754c6f04"},{url:"/_next/static/chunks/app/_global-error/page-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/_not-found/page-2ddcf070539afa15.js",revision:"2ddcf070539afa15"},{url:"/_next/static/chunks/app/api/admin/audit/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/backup/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/config/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/families/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/family/%5Bid%5D/reset-invite/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/stats/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/test-email/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/test-push/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/users/%5Bid%5D/force-logout/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/users/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/admin/users/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/auth/%5B...nextauth%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/auth/forgot-password/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/auth/reset-password/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/baby/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/baby/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/cron/daily-digest/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/cron/growth-alert/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/cron/milk-expiry/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/cron/weekly-summary/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/doctor-notes/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/doctor-notes/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/event-templates/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/event-templates/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/events/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/events/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/export/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/family/pinned-note/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/growth/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/growth/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/invite/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/invite/send-email/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/journal/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/journal/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/medication-profiles/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/medication-profiles/last-intakes/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/medication-profiles/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/milestones/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/milestones/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/milk/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/milk/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/notify/push/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/register/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/reminders/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/reminders/check/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/reminders/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/upload/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/user/pushover/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/vaccinations/%5Bid%5D/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/api/vaccinations/route-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/app/auth/forgot-password/page-1b473876ab8d296f.js",revision:"1b473876ab8d296f"},{url:"/_next/static/chunks/app/auth/invite/%5Bcode%5D/page-8053ba0438b9f290.js",revision:"8053ba0438b9f290"},{url:"/_next/static/chunks/app/auth/login/page-87001056033db7cc.js",revision:"87001056033db7cc"},{url:"/_next/static/chunks/app/auth/register/page-ad086e59a1327788.js",revision:"ad086e59a1327788"},{url:"/_next/static/chunks/app/auth/reset-password/page-415cf97a3556f61c.js",revision:"415cf97a3556f61c"},{url:"/_next/static/chunks/app/layout-45d38ab66299dd10.js",revision:"45d38ab66299dd10"},{url:"/_next/static/chunks/app/offline/page-0159cc23b1a07aa7.js",revision:"0159cc23b1a07aa7"},{url:"/_next/static/chunks/app/page-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/badf541d.6151e2ad1a381f0b.js",revision:"6151e2ad1a381f0b"},{url:"/_next/static/chunks/c132bf7d.5dbabc63d2725b8a.js",revision:"5dbabc63d2725b8a"},{url:"/_next/static/chunks/framework-1ca0551e6ffbddf2.js",revision:"1ca0551e6ffbddf2"},{url:"/_next/static/chunks/main-app-a358c96d1bdc6263.js",revision:"a358c96d1bdc6263"},{url:"/_next/static/chunks/main-eacdf743a3244659.js",revision:"eacdf743a3244659"},{url:"/_next/static/chunks/next/dist/client/components/builtin/app-error-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/next/dist/client/components/builtin/forbidden-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/next/dist/client/components/builtin/global-error-72bb508065cc2a6d.js",revision:"72bb508065cc2a6d"},{url:"/_next/static/chunks/next/dist/client/components/builtin/not-found-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/next/dist/client/components/builtin/unauthorized-7f8a5ce10e44ab40.js",revision:"7f8a5ce10e44ab40"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-ac4935854903fff4.js",revision:"ac4935854903fff4"},{url:"/_next/static/css/120aecdaeae4ec3c.css",revision:"120aecdaeae4ec3c"},{url:"/apple-touch-icon.png",revision:"ccd2d5bd7f7dcd4c7c0741a503d715b5"},{url:"/file.svg",revision:"d09f95206c3fa0bb9bd9fefabfd0ea71"},{url:"/globe.svg",revision:"2aaafa6a49b6563925fe440891e32717"},{url:"/icon-192.png",revision:"a89c78ebd4f94c27fee6e5e7dba20e83"},{url:"/icon-512.png",revision:"987207de7517e7779ed9d8d9c41f0fe2"},{url:"/icon.svg",revision:"7f77fb2aae66c013f1a0547bb9165181"},{url:"/manifest.json",revision:"0c8ab6fe0c902a038f66693f14b2c04a"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/swe-worker-5c72df51bb1f6ee0.js",revision:"76fdd3369f623a3edcf74ce2200bfdd0"},{url:"/vercel.svg",revision:"c0af2f507b369b085b35ef4bbe3bcf1e"},{url:"/window.svg",revision:"a2760511c65806022ad20adf74370ff3"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({response:e})=>e&&"opaqueredirect"===e.type?new Response(e.body,{status:200,statusText:"OK",headers:e.headers}):e}]}),"GET"),e.registerRoute(/^https?.*/,new e.NetworkFirst({cacheName:"grow-api-cache",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:200,maxAgeSeconds:86400})]}),"GET"),self.__WB_DISABLE_DEV_LOGS=!0}); diff --git a/public/swe-worker-5c72df51bb1f6ee0.js b/public/swe-worker-5c72df51bb1f6ee0.js new file mode 100644 index 0000000..4bd776a --- /dev/null +++ b/public/swe-worker-5c72df51bb1f6ee0.js @@ -0,0 +1 @@ +(()=>{"use strict";self.onmessage=async e=>{switch(e.data.type){case"__START_URL_CACHE__":{let t=e.data.url,a=await fetch(t);if(!a.redirected)return(await caches.open("start-url")).put(t,a);return Promise.resolve()}case"__FRONTEND_NAV_CACHE__":{let t=e.data.url,a=await caches.open("pages");if(await a.match(t,{ignoreSearch:!0}))return;let s=await fetch(t);if(!s.ok)return;if(a.put(t,s.clone()),e.data.shouldCacheAggressively&&s.headers.get("Content-Type")?.includes("text/html"))try{let e=await s.text(),t=[],a=await caches.open("static-style-assets"),r=await caches.open("next-static-js-assets"),c=await caches.open("static-js-assets");for(let[s,r]of e.matchAll(//g))/rel=['"]stylesheet['"]/.test(s)&&t.push(a.match(r).then(e=>e?Promise.resolve():a.add(r)));for(let[,a]of e.matchAll(//g)){let e=/\/_next\/static.+\.js$/i.test(a)?r:c;t.push(e.match(a).then(t=>t?Promise.resolve():e.add(a)))}return await Promise.all(t)}catch{}return Promise.resolve()}default:return Promise.resolve()}}})(); \ No newline at end of file diff --git a/public/workbox-f8dc152a.js b/public/workbox-f8dc152a.js new file mode 100644 index 0000000..17544b4 --- /dev/null +++ b/public/workbox-f8dc152a.js @@ -0,0 +1 @@ +define(["exports"],function(t){"use strict";try{self["workbox:core:7.0.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:7.0.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class i{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class r extends i{constructor(t,e,s){super(({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)},e,s)}}class a{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)})}addCacheListener(){self.addEventListener("message",t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map(e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})}));t.waitUntil(s),t.ports&&t.ports[0]&&s.then(()=>t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let a=r&&r.handler;const o=t.method;if(!a&&this.i.has(o)&&(a=this.i.get(o)),!a)return;let c;try{c=a.handle({url:s,request:t,event:e,params:i})}catch(t){c=Promise.reject(t)}const h=r&&r.catchHandler;return c instanceof Promise&&(this.o||h)&&(c=c.catch(async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:i})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n})),c}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const i=this.t.get(s.method)||[];for(const r of i){let i;const a=r.match({url:t,sameOrigin:e,request:s,event:n});if(a)return i=a,(Array.isArray(i)&&0===i.length||a.constructor===Object&&0===Object.keys(a).length||"boolean"==typeof a)&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let o;const c=()=>(o||(o=new a,o.addFetchListener(),o.addCacheListener()),o);function h(t,e,n){let a;if("string"==typeof t){const s=new URL(t,location.href);a=new i(({url:t})=>t.href===s.href,e,n)}else if(t instanceof RegExp)a=new r(t,e,n);else if("function"==typeof t)a=new i(t,e,n);else{if(!(t instanceof i))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});a=t}return c().registerRoute(a),a}try{self["workbox:strategies:7.0.0"]&&_()}catch(t){}const u={cacheWillUpdate:async({response:t})=>200===t.status||0===t.status?t:null},l={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},f=t=>[l.prefix,t,l.suffix].filter(t=>t&&t.length>0).join("-"),w=t=>t||f(l.precache),d=t=>t||f(l.runtime);function p(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class y{constructor(){this.promise=new Promise((t,e)=>{this.resolve=t,this.reject=e})}}const m=new Set;function g(t){return"string"==typeof t?new Request(t):t}class R{constructor(t,e){this.h={},Object.assign(this,e),this.event=e.event,this.u=t,this.l=new y,this.p=[],this.m=[...t.plugins],this.R=new Map;for(const t of this.m)this.R.set(t,{});this.event.waitUntil(this.l.promise)}async fetch(t){const{event:e}=this;let n=g(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const i=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const r=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.u.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:r,response:t});return t}catch(t){throw i&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:i.clone(),request:r.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=g(t);let s;const{cacheName:n,matchOptions:i}=this.u,r=await this.getCacheKey(e,"read"),a=Object.assign(Object.assign({},i),{cacheName:n});s=await caches.match(r,a);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(t,e){const n=g(t);var i;await(i=0,new Promise(t=>setTimeout(t,i)));const r=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(a=r.url,new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var a;const o=await this.v(e);if(!o)return!1;const{cacheName:c,matchOptions:h}=this.u,u=await self.caches.open(c),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const i=p(e.url,s);if(e.url===i)return t.match(e,n);const r=Object.assign(Object.assign({},n),{ignoreSearch:!0}),a=await t.keys(e,r);for(const e of a)if(i===p(e.url,s))return t.match(e,n)}(u,r.clone(),["__WB_REVISION__"],h):null;try{await u.put(r,l?o.clone():o)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of m)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:c,oldResponse:f,newResponse:o.clone(),request:r,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.h[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=g(await t({mode:e,request:n,event:this.event,params:this.params}));this.h[s]=n}return this.h[s]}hasCallback(t){for(const e of this.u.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.u.plugins)if("function"==typeof e[t]){const s=this.R.get(e),n=n=>{const i=Object.assign(Object.assign({},n),{state:s});return e[t](i)};yield n}}waitUntil(t){return this.p.push(t),t}async doneWaiting(){let t;for(;t=this.p.shift();)await t}destroy(){this.l.resolve(null)}async v(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class v{constructor(t={}){this.cacheName=d(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,i=new R(this,{event:e,request:s,params:n}),r=this.q(i,s,e);return[r,this.D(r,i,s,e)]}async q(t,e,n){let i;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(i=await this.U(e,t),!i||"error"===i.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const r of t.iterateCallbacks("handlerDidError"))if(i=await r({error:s,event:n,request:e}),i)break;if(!i)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))i=await s({event:n,request:e,response:i});return i}async D(t,e,s,n){let i,r;try{i=await t}catch(r){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:i}),await e.doneWaiting()}catch(t){t instanceof Error&&(r=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:i,error:r}),e.destroy(),r)throw r}}function b(t){t.then(()=>{})}function q(){return q=Object.assign?Object.assign.bind():function(t){for(var e=1;e(t[e]=s,!0),has:(t,e)=>t instanceof IDBTransaction&&("done"===e||"store"===e)||e in t};function O(t){return t!==IDBDatabase.prototype.transaction||"objectStoreNames"in IDBTransaction.prototype?(U||(U=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey])).includes(t)?function(...e){return t.apply(T(this),e),B(x.get(this))}:function(...e){return B(t.apply(T(this),e))}:function(e,...s){const n=t.call(T(this),e,...s);return L.set(n,e.sort?e.sort():[e]),B(n)}}function k(t){return"function"==typeof t?O(t):(t instanceof IDBTransaction&&function(t){if(I.has(t))return;const e=new Promise((e,s)=>{const n=()=>{t.removeEventListener("complete",i),t.removeEventListener("error",r),t.removeEventListener("abort",r)},i=()=>{e(),n()},r=()=>{s(t.error||new DOMException("AbortError","AbortError")),n()};t.addEventListener("complete",i),t.addEventListener("error",r),t.addEventListener("abort",r)});I.set(t,e)}(t),e=t,(D||(D=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction])).some(t=>e instanceof t)?new Proxy(t,N):t);var e}function B(t){if(t instanceof IDBRequest)return function(t){const e=new Promise((e,s)=>{const n=()=>{t.removeEventListener("success",i),t.removeEventListener("error",r)},i=()=>{e(B(t.result)),n()},r=()=>{s(t.error),n()};t.addEventListener("success",i),t.addEventListener("error",r)});return e.then(e=>{e instanceof IDBCursor&&x.set(e,t)}).catch(()=>{}),C.set(e,t),e}(t);if(E.has(t))return E.get(t);const e=k(t);return e!==t&&(E.set(t,e),C.set(e,t)),e}const T=t=>C.get(t);const M=["get","getKey","getAll","getAllKeys","count"],P=["put","add","delete","clear"],W=new Map;function j(t,e){if(!(t instanceof IDBDatabase)||e in t||"string"!=typeof e)return;if(W.get(e))return W.get(e);const s=e.replace(/FromIndex$/,""),n=e!==s,i=P.includes(s);if(!(s in(n?IDBIndex:IDBObjectStore).prototype)||!i&&!M.includes(s))return;const r=async function(t,...e){const r=this.transaction(t,i?"readwrite":"readonly");let a=r.store;return n&&(a=a.index(e.shift())),(await Promise.all([a[s](...e),i&&r.done]))[0]};return W.set(e,r),r}N=(t=>q({},t,{get:(e,s,n)=>j(e,s)||t.get(e,s,n),has:(e,s)=>!!j(e,s)||t.has(e,s)}))(N);try{self["workbox:expiration:7.0.0"]&&_()}catch(t){}const S="cache-entries",K=t=>{const e=new URL(t,location.href);return e.hash="",e.href};class A{constructor(t){this._=null,this.I=t}L(t){const e=t.createObjectStore(S,{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1})}C(t){this.L(t),this.I&&function(t,{blocked:e}={}){const s=indexedDB.deleteDatabase(t);e&&s.addEventListener("blocked",t=>e(t.oldVersion,t)),B(s).then(()=>{})}(this.I)}async setTimestamp(t,e){const s={url:t=K(t),timestamp:e,cacheName:this.I,id:this.N(t)},n=(await this.getDb()).transaction(S,"readwrite",{durability:"relaxed"});await n.store.put(s),await n.done}async getTimestamp(t){const e=await this.getDb(),s=await e.get(S,this.N(t));return null==s?void 0:s.timestamp}async expireEntries(t,e){const s=await this.getDb();let n=await s.transaction(S).store.index("timestamp").openCursor(null,"prev");const i=[];let r=0;for(;n;){const s=n.value;s.cacheName===this.I&&(t&&s.timestamp=e?i.push(n.value):r++),n=await n.continue()}const a=[];for(const t of i)await s.delete(S,t.id),a.push(t.url);return a}N(t){return this.I+"|"+K(t)}async getDb(){return this._||(this._=await function(t,e,{blocked:s,upgrade:n,blocking:i,terminated:r}={}){const a=indexedDB.open(t,e),o=B(a);return n&&a.addEventListener("upgradeneeded",t=>{n(B(a.result),t.oldVersion,t.newVersion,B(a.transaction),t)}),s&&a.addEventListener("blocked",t=>s(t.oldVersion,t.newVersion,t)),o.then(t=>{r&&t.addEventListener("close",()=>r()),i&&t.addEventListener("versionchange",t=>i(t.oldVersion,t.newVersion,t))}).catch(()=>{}),o}("workbox-expiration",1,{upgrade:this.C.bind(this)})),this._}}class F{constructor(t,e={}){this.O=!1,this.k=!1,this.B=e.maxEntries,this.T=e.maxAgeSeconds,this.M=e.matchOptions,this.I=t,this.P=new A(t)}async expireEntries(){if(this.O)return void(this.k=!0);this.O=!0;const t=this.T?Date.now()-1e3*this.T:0,e=await this.P.expireEntries(t,this.B),s=await self.caches.open(this.I);for(const t of e)await s.delete(t,this.M);this.O=!1,this.k&&(this.k=!1,b(this.expireEntries()))}async updateTimestamp(t){await this.P.setTimestamp(t,Date.now())}async isURLExpired(t){if(this.T){const e=await this.P.getTimestamp(t),s=Date.now()-1e3*this.T;return void 0===e||e{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class V{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.W.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.W=t}}let J,Q;async function z(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const i=t.clone(),r={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},a=e?e(r):r,o=function(){if(void 0===J){const t=new Response("");if("body"in t)try{new Response(t.body),J=!0}catch(t){J=!1}J=!1}return J}()?i.body:await i.blob();return new Response(o,a)}class X extends v{constructor(t={}){t.cacheName=w(t.cacheName),super(t),this.j=!1!==t.fallbackToNetwork,this.plugins.push(X.copyRedirectedCacheableResponsesPlugin)}async U(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.S(t,e):await this.K(t,e))}async K(t,e){let n;const i=e.params||{};if(!this.j)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=i.integrity,r=t.integrity,a=!r||r===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?r||s:void 0})),s&&a&&"no-cors"!==t.mode&&(this.A(),await e.cachePut(t,n.clone()))}return n}async S(t,e){this.A();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}A(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==X.copyRedirectedCacheableResponsesPlugin&&(n===X.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(X.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}X.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},X.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await z(t):t};class Y{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.F=new Map,this.H=new Map,this.$=new Map,this.u=new X({cacheName:w(t),plugins:[...e,new V({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.u}precache(t){this.addToCacheList(t),this.G||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.G=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:i}=$(n),r="string"!=typeof n&&n.revision?"reload":"default";if(this.F.has(i)&&this.F.get(i)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.F.get(i),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.$.has(t)&&this.$.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:i});this.$.set(t,n.integrity)}if(this.F.set(i,t),this.H.set(i,r),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return H(t,async()=>{const e=new G;this.strategy.plugins.push(e);for(const[e,s]of this.F){const n=this.$.get(s),i=this.H.get(e),r=new Request(e,{integrity:n,cache:i,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}})}activate(t){return H(t,async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.F.values()),n=[];for(const i of e)s.has(i.url)||(await t.delete(i),n.push(i.url));return{deletedURLs:n}})}getURLsToCacheKeys(){return this.F}getCachedURLs(){return[...this.F.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.F.get(e.href)}getIntegrityForCacheKey(t){return this.$.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}const Z=()=>(Q||(Q=new Y),Q);class tt extends i{constructor(t,e){super(({request:s})=>{const n=t.getURLsToCacheKeys();for(const i of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:i}={}){const r=new URL(t,location.href);r.hash="",yield r.href;const a=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some(t=>t.test(s))&&t.searchParams.delete(s);return t}(r,e);if(yield a.href,s&&a.pathname.endsWith("/")){const t=new URL(a.href);t.pathname+=s,yield t.href}if(n){const t=new URL(a.href);t.pathname+=".html",yield t.href}if(i){const t=i({url:r});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(i);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}},t.strategy)}}t.ExpirationPlugin=class{constructor(t={}){this.cachedResponseWillBeUsed=async({event:t,request:e,cacheName:s,cachedResponse:n})=>{if(!n)return null;const i=this.V(n),r=this.J(s);b(r.expireEntries());const a=r.updateTimestamp(e.url);if(t)try{t.waitUntil(a)}catch(t){}return i?n:null},this.cacheDidUpdate=async({cacheName:t,request:e})=>{const s=this.J(t);await s.updateTimestamp(e.url),await s.expireEntries()},this.X=t,this.T=t.maxAgeSeconds,this.Y=new Map,t.purgeOnQuotaError&&function(t){m.add(t)}(()=>this.deleteCacheAndMetadata())}J(t){if(t===d())throw new s("expire-custom-caches-only");let e=this.Y.get(t);return e||(e=new F(t,this.X),this.Y.set(t,e)),e}V(t){if(!this.T)return!0;const e=this.Z(t);if(null===e)return!0;return e>=Date.now()-1e3*this.T}Z(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async deleteCacheAndMetadata(){for(const[t,e]of this.Y)await self.caches.delete(t),await e.delete();this.Y=new Map}},t.NetworkFirst=class extends v{constructor(t={}){super(t),this.plugins.some(t=>"cacheWillUpdate"in t)||this.plugins.unshift(u),this.tt=t.networkTimeoutSeconds||0}async U(t,e){const n=[],i=[];let r;if(this.tt){const{id:s,promise:a}=this.et({request:t,logs:n,handler:e});r=s,i.push(a)}const a=this.st({timeoutId:r,request:t,logs:n,handler:e});i.push(a);const o=await e.waitUntil((async()=>await e.waitUntil(Promise.race(i))||await a)());if(!o)throw new s("no-response",{url:t.url});return o}et({request:t,logs:e,handler:s}){let n;return{promise:new Promise(e=>{n=setTimeout(async()=>{e(await s.cacheMatch(t))},1e3*this.tt)}),id:n}}async st({timeoutId:t,request:e,logs:s,handler:n}){let i,r;try{r=await n.fetchAndCachePut(e)}catch(t){t instanceof Error&&(i=t)}return t&&clearTimeout(t),!i&&r||(r=await n.cacheMatch(e)),r}},t.cleanupOutdatedCaches=function(){self.addEventListener("activate",t=>{const e=w();t.waitUntil((async(t,e="-precache-")=>{const s=(await self.caches.keys()).filter(s=>s.includes(e)&&s.includes(self.registration.scope)&&s!==t);return await Promise.all(s.map(t=>self.caches.delete(t))),s})(e).then(t=>{}))})},t.clientsClaim=function(){self.addEventListener("activate",()=>self.clients.claim())},t.precacheAndRoute=function(t,e){!function(t){Z().precache(t)}(t),function(t){const e=Z();h(new tt(e,t))}(e)},t.registerRoute=h}); diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index e5a785f..c7b367e 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -15,12 +15,16 @@ import { useSession } from "next-auth/react"; const QUICK_LOG_GROUPS: { label: string; types: EventType[] }[] = [ { label: "Alimentation & Soins", - types: ["BREASTFEED", "BOTTLE", "PUMP", "DIAPER_WET", "DIAPER_STOOL", "MEDICATION", "TEMPERATURE"], + types: ["BREASTFEED", "BOTTLE", "PUMP", "DIAPER_WET", "DIAPER_STOOL"], }, { label: "Sommeil & Activités", types: ["NAP", "NIGHT_SLEEP", "BATH", "WALK", "TUMMY_TIME"], }, + { + label: "Santé", + types: ["TEMPERATURE", "MEDICATION", "SYMPTOM"], + }, ]; interface Event { @@ -64,12 +68,73 @@ function getEventSummary(event: Event): string { if (event.type === "DIAPER_STOOL") return String(meta.color ?? ""); if (event.type === "TEMPERATURE") return meta.value ? `${meta.value}°${meta.unit ?? "C"}` : ""; if (event.type === "MEDICATION") return String(meta.name ?? ""); + if (event.type === "SYMPTOM") { const tags = (meta.tags as string[] | undefined) ?? []; return tags.join(", "); } if ((event.type === "NAP" || event.type === "NIGHT_SLEEP") && event.endedAt) { return formatDuration(new Date(event.startedAt), new Date(event.endedAt)); } return ""; } +function formatElapsed(startedAt: string, now: Date): string { + const ms = Math.max(0, now.getTime() - new Date(startedAt).getTime()); + const h = Math.floor(ms / 3600000); + const m = Math.floor((ms % 3600000) / 60000); + const s = Math.floor((ms % 60000) / 1000); + if (h > 0) return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + return `${m}:${s.toString().padStart(2, "0")}`; +} + +function ActiveTimers({ events, onStop }: { events: Event[]; onStop: () => void }) { + const [now, setNow] = useState(() => new Date()); + const active = events.filter((e) => !e.endedAt && EVENT_CONFIG[e.type]?.hasTimer); + + useEffect(() => { + if (active.length === 0) return; + const id = setInterval(() => setNow(new Date()), 1000); + return () => clearInterval(id); + }, [active.length]); + + if (active.length === 0) return null; + + return ( +
+

En cours

+ {active.map((ev) => { + const cfg = EVENT_CONFIG[ev.type]; + return ( +
+
+ +
+
+

{cfg.label}

+

+ Depuis {format(new Date(ev.startedAt), "HH:mm")} +

+
+ + {formatElapsed(ev.startedAt, now)} + + +
+ ); + })} +
+ ); +} + function QuickNoteWidget({ babyId }: { babyId: string }) { const qc = useQueryClient(); const todayStr = format(new Date(), "yyyy-MM-dd"); @@ -512,6 +577,9 @@ export default function DashboardPage() { {/* Handoff summary */} + {/* Active timers */} + qc.invalidateQueries({ queryKey: ["events"] })} /> + {/* Pinned note + Quick note */}
diff --git a/src/app/(app)/growth/page.tsx b/src/app/(app)/growth/page.tsx index f5daa4c..fe7224f 100644 --- a/src/app/(app)/growth/page.tsx +++ b/src/app/(app)/growth/page.tsx @@ -11,7 +11,7 @@ import { import { useBaby } from "@/contexts/baby-context"; import { format, subDays } from "date-fns"; import { fr } from "date-fns/locale"; -import { Plus, Pencil, Trash2 } from "lucide-react"; +import { Plus, Pencil, Trash2, FileDown } from "lucide-react"; interface GrowthLog { id: string; @@ -241,6 +241,53 @@ export default function GrowthPage() { qc.invalidateQueries({ queryKey: ["growth"] }); } + function exportPDF() { + import("jspdf").then(({ default: jsPDF }) => { + const doc = new jsPDF(); + const pageW = doc.internal.pageSize.getWidth(); + + doc.setFontSize(18); + doc.setTextColor(30, 41, 59); + doc.text("Courbe de croissance", 14, 20); + + doc.setFontSize(11); + doc.setTextColor(100, 116, 139); + doc.text(`${baby!.name} · né${baby!.gender === "F" ? "e" : ""} le ${format(new Date(baby!.birthDate), "d MMMM yyyy", { locale: fr })}`, 14, 28); + doc.text(`Généré le ${format(new Date(), "d MMMM yyyy", { locale: fr })}`, 14, 34); + + // Table header + const COL = [14, 55, 95, 130, 155]; + const headers = ["Date", "Poids (kg)", "Taille (cm)", "PC (cm)", "Notes"]; + let y = 46; + + doc.setFillColor(238, 242, 255); + doc.rect(14, y - 5, pageW - 28, 9, "F"); + doc.setFontSize(9); + doc.setTextColor(79, 70, 229); + headers.forEach((h, i) => doc.text(h, COL[i], y)); + y += 8; + + doc.setFontSize(9); + const sorted = [...logs].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + sorted.forEach((log, idx) => { + if (y > 270) { doc.addPage(); y = 20; } + if (idx % 2 === 0) { + doc.setFillColor(248, 250, 252); + doc.rect(14, y - 4, pageW - 28, 7, "F"); + } + doc.setTextColor(30, 41, 59); + doc.text(format(new Date(log.date), "d MMM yyyy", { locale: fr }), COL[0], y); + doc.text(log.weight ? (log.weight / 1000).toFixed(3) : "—", COL[1], y); + doc.text(log.height ? String(log.height) : "—", COL[2], y); + doc.text(log.headCirc ? String(log.headCirc) : "—", COL[3], y); + if (log.notes) doc.text(log.notes.slice(0, 30), COL[4], y); + y += 7; + }); + + doc.save(`croissance-${baby!.name.toLowerCase()}.pdf`); + }); + } + const latest = logs.at(-1); const rangeOptions: { label: string; value: TimeRange }[] = [ @@ -257,13 +304,22 @@ export default function GrowthPage() {

Courbes de croissance

{baby?.name}

- +
+ {logs.length > 0 && ( + + )} + +
{/* Add measurement form */} diff --git a/src/app/(app)/search/page.tsx b/src/app/(app)/search/page.tsx new file mode 100644 index 0000000..c6d352b --- /dev/null +++ b/src/app/(app)/search/page.tsx @@ -0,0 +1,169 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { EVENT_CONFIG, EventType, formatDuration } from "@/lib/event-config"; +import { EventIcon } from "@/components/event-icon"; +import { useBaby } from "@/contexts/baby-context"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; +import { Search, FileText, StickyNote } from "lucide-react"; + +interface SearchEvent { + id: string; + type: EventType; + startedAt: string; + endedAt?: string; + notes?: string; + metadata?: Record; + user: { id: string; name: string }; +} + +interface SearchNote { + id: string; + date: string; + content: string; + user: { id: string; name: string }; +} + +function eventSummary(ev: SearchEvent): string { + const meta = ev.metadata ?? {}; + if (ev.type === "BREASTFEED") { + const side = meta.breastSide === "left" ? "gauche" : meta.breastSide === "right" ? "droite" : "les deux"; + if (ev.endedAt) return `${formatDuration(new Date(ev.startedAt), new Date(ev.endedAt))} · ${side}`; + return side; + } + if (ev.type === "BOTTLE") return meta.volume ? `${meta.volume} mL` : ""; + if (ev.type === "TEMPERATURE") return meta.value ? `${meta.value}°${meta.unit ?? "C"}` : ""; + if (ev.type === "MEDICATION") return String(meta.name ?? ""); + if (ev.type === "SYMPTOM") return ((meta.tags as string[]) ?? []).join(", "); + if ((ev.type === "NAP" || ev.type === "NIGHT_SLEEP") && ev.endedAt) + return formatDuration(new Date(ev.startedAt), new Date(ev.endedAt)); + return ""; +} + +function highlight(text: string, q: string): React.ReactNode { + if (!q || !text) return text; + const idx = text.toLowerCase().indexOf(q.toLowerCase()); + if (idx === -1) return text; + return ( + <> + {text.slice(0, idx)} + {text.slice(idx, idx + q.length)} + {text.slice(idx + q.length)} + + ); +} + +export default function SearchPage() { + const { selectedBaby } = useBaby(); + const [q, setQ] = useState(""); + const [debouncedQ, setDebouncedQ] = useState(""); + const inputRef = useRef(null); + + useEffect(() => { inputRef.current?.focus(); }, []); + + useEffect(() => { + const t = setTimeout(() => setDebouncedQ(q), 300); + return () => clearTimeout(t); + }, [q]); + + const { data, isFetching } = useQuery<{ events: SearchEvent[]; notes: SearchNote[] }>({ + queryKey: ["search", selectedBaby?.id, debouncedQ], + queryFn: () => + fetch(`/api/search?babyId=${selectedBaby!.id}&q=${encodeURIComponent(debouncedQ)}`).then((r) => r.json()), + enabled: !!selectedBaby?.id && debouncedQ.length >= 2, + placeholderData: { events: [], notes: [] }, + }); + + const events = data?.events ?? []; + const notes = data?.notes ?? []; + const hasResults = events.length > 0 || notes.length > 0; + const searched = debouncedQ.length >= 2; + + return ( +
+

Recherche

+ +
+ + setQ(e.target.value)} + placeholder="Rechercher dans les notes et événements…" + className="w-full pl-10 pr-4 py-3 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400" + /> + {isFetching && ( +
+ )} +
+ + {!searched && ( +

Saisissez au moins 2 caractères

+ )} + + {searched && !hasResults && !isFetching && ( +

Aucun résultat pour « {debouncedQ} »

+ )} + + {events.length > 0 && ( +
+

+ + Événements · {events.length} +

+
+ {events.map((ev) => { + const cfg = EVENT_CONFIG[ev.type]; + const summary = eventSummary(ev); + return ( +
+
+ +
+
+
+ {cfg.label} + {summary && {summary}} +
+ {ev.notes && ( +

+ {highlight(ev.notes, debouncedQ)} +

+ )} +

+ {format(new Date(ev.startedAt), "d MMM yyyy HH:mm", { locale: fr })} · {ev.user.name} +

+
+
+ ); + })} +
+
+ )} + + {notes.length > 0 && ( +
+

+ + Journal · {notes.length} +

+
+ {notes.map((note) => ( +
+

+ {format(new Date(note.date), "d MMMM yyyy", { locale: fr })} · {note.user.name} +

+

+ {highlight(note.content, debouncedQ)} +

+
+ ))} +
+
+ )} +
+ ); +} diff --git a/src/app/(app)/timeline/page.tsx b/src/app/(app)/timeline/page.tsx index dca1b3e..229680e 100644 --- a/src/app/(app)/timeline/page.tsx +++ b/src/app/(app)/timeline/page.tsx @@ -57,6 +57,10 @@ function getEventSummary(event: Event): string { if (event.type === "DIAPER_STOOL") return String(meta.color ?? ""); if (event.type === "TEMPERATURE") return meta.value ? `${meta.value}°${meta.unit ?? "C"}` : ""; if (event.type === "MEDICATION") return String(meta.name ?? ""); + if (event.type === "SYMPTOM") { + const tags = (meta.tags as string[] | undefined) ?? []; + return tags.length > 0 ? tags.join(", ") : ""; + } if ((event.type === "NAP" || event.type === "NIGHT_SLEEP") && event.endedAt) { return formatDuration(new Date(event.startedAt), new Date(event.endedAt)); } diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts new file mode 100644 index 0000000..30ebcb6 --- /dev/null +++ b/src/app/api/search/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET(req: Request) { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + + const { searchParams } = new URL(req.url); + const q = searchParams.get("q")?.trim() ?? ""; + const babyId = searchParams.get("babyId"); + + if (!babyId || q.length < 2) return NextResponse.json({ events: [], notes: [] }); + + const [events, notes] = await Promise.all([ + prisma.event.findMany({ + where: { + babyId, + OR: [ + { notes: { contains: q, mode: "insensitive" } }, + ], + }, + orderBy: { startedAt: "desc" }, + take: 30, + include: { user: { select: { id: true, name: true } } }, + }), + prisma.journalNote.findMany({ + where: { babyId, content: { contains: q, mode: "insensitive" } }, + orderBy: { date: "desc" }, + take: 20, + include: { user: { select: { id: true, name: true } } }, + }), + ]); + + return NextResponse.json({ events, notes }); +} diff --git a/src/components/event-modal.tsx b/src/components/event-modal.tsx index 4100e7e..c87beec 100644 --- a/src/components/event-modal.tsx +++ b/src/components/event-modal.tsx @@ -38,6 +38,7 @@ interface FormData { medUnit?: string; temperature?: string; tempUnit?: "C" | "F"; + symptomTags?: string[]; } interface MedProfile { @@ -116,6 +117,7 @@ export function EventModal({ type, babyId, onClose, onSaved, initialEvent }: Pro medUnit: (meta.unit as string) ?? undefined, temperature: meta.value != null ? String(meta.value) : undefined, tempUnit: (meta.unit as "C" | "F") ?? "C", + symptomTags: (meta.tags as string[]) ?? [], }); const [timerRunning, setTimerRunning] = useState(false); @@ -310,6 +312,7 @@ export function EventModal({ type, babyId, onClose, onSaved, initialEvent }: Pro } } if (type === "TEMPERATURE") { metadata.value = form.temperature ? parseFloat(form.temperature) : null; metadata.unit = form.tempUnit; } + if (type === "SYMPTOM") { metadata.tags = form.symptomTags ?? []; } if (photoUrl) metadata.photo = photoUrl; const toUTC = (local: string) => local ? new Date(local).toISOString() : null; @@ -784,6 +787,34 @@ export function EventModal({ type, babyId, onClose, onSaved, initialEvent }: Pro
)} + {/* Symptom tags */} + {type === "SYMPTOM" && ( +
+ +
+ {["Fièvre", "Reflux", "Colique", "Éruption", "Toux", "Congestion", "Diarrhée", "Vomissement", "Irritabilité", "Pleurs excessifs"].map((tag) => { + const active = (form.symptomTags ?? []).includes(tag); + return ( + + ); + })} +
+
+ )} + {/* Time fields — split date + time for iOS Safari compatibility */}
diff --git a/src/components/nav.tsx b/src/components/nav.tsx index 5fbb7b5..30b69e4 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -28,6 +28,7 @@ import { Milk, Pill, ImageIcon, + Search, } from "lucide-react"; const LINKS = [ @@ -44,6 +45,7 @@ const LINKS = [ { href: "/vaccinations", label: "Vaccins", Icon: Syringe }, { href: "/medications", label: "Médicaments", Icon: Pill }, { href: "/reminders", label: "Rappels", Icon: Bell }, + { href: "/search", label: "Recherche", Icon: Search }, { href: "/settings", label: "Réglages", Icon: Settings }, ]; @@ -52,6 +54,7 @@ const DRAWER_GROUPS: { label: string; hrefs: string[] }[] = [ { label: "Suivi", hrefs: ["/calendar", "/stats", "/milk"] }, { label: "Santé", hrefs: ["/medications", "/reminders", "/vaccinations", "/doctor-notes"] }, { label: "Vie du bébé", hrefs: ["/photos", "/milestones", "/notes", "/growth"] }, + { label: "Outils", hrefs: ["/search"] }, ]; const PRIMARY_HREFS = ["/dashboard", "/timeline", "/growth", "/settings"]; diff --git a/src/lib/event-config.ts b/src/lib/event-config.ts index ebd3c8b..6b27134 100644 --- a/src/lib/event-config.ts +++ b/src/lib/event-config.ts @@ -10,7 +10,8 @@ export type EventType = | "TEMPERATURE" | "BATH" | "WALK" - | "TUMMY_TIME"; + | "TUMMY_TIME" + | "SYMPTOM"; export const EVENT_CONFIG: Record< EventType, @@ -145,6 +146,16 @@ export const EVENT_CONFIG: Record< hasDuration: true, hasTimer: true, }, + SYMPTOM: { + label: "Symptôme", + icon: "Activity", + color: "#dc2626", + colorClass: "text-red-600", + bgClass: "bg-red-50", + borderClass: "border-red-200", + hasDuration: false, + hasTimer: false, + }, }; export const ALL_EVENT_TYPES = Object.keys(EVENT_CONFIG) as EventType[];