Files
ketopath/apps/web/src/app/[locale]/welcome/disclaimer-form.tsx
T
lucianoandClaude Opus 4.7 f6423844da feat: medical disclaimer onboarding + V1 + recipe schema (no migration yet)
Schema changes (require a single migration to apply):
- User gains disclaimerAcceptedAt — null until the user accepts the medical
  disclaimer, blocks /profile until set (PRD §14.3)
- New WeightEntry (PRD §5.2) — weight/measurements/notes encrypted at rest,
  energy/sleep/hunger left in the clear so we can produce aggregate analytics
- New FastEvent (PRD §5.3) — symptoms/notes encrypted, protocol/status/timer
  in the clear; indexed on (userId, startedAt)
- New Ingredient / Recipe / RecipeIngredient (PRD §5.1) — italian keto recipe
  database with macros and exclusion groups, ready for the matchmaking algo
- New MealPlan / MealSlot — generated weekly plan with selected recipe and
  alternatives per (day, meal)
- New enums: FastStatus, MealCategory, Difficulty, MealPlanStatus

Web — disclaimer flow:
- /welcome page (server component, requires auth) lists the 4 PRD-mandated
  points and surfaces the 3 mandatory checkboxes; submit triggers a server
  action that stamps disclaimerAcceptedAt and redirects to /profile
- /profile redirects to /welcome whenever disclaimerAcceptedAt is null,
  so the disclaimer becomes a hard prerequisite for any sensitive page
- New Italian copy under Welcome.* in messages/it.json
- @ketopath/db added to apps/web dependencies (was indirect via @ketopath/auth)

@ketopath/db re-exports the new models and enums.

Run the migration before testing: `pnpm db:migrate --name onboarding_v1_recipes`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:53:15 +02:00

59 lines
1.7 KiB
TypeScript

'use client';
import { useTranslations } from 'next-intl';
import { useState, type FormEvent } from 'react';
import { Button } from '@/components/ui/button';
import { acceptDisclaimer } from './actions';
export function DisclaimerForm() {
const t = useTranslations('Welcome');
const [adult, setAdult] = useState(false);
const [aware, setAware] = useState(false);
const [accepts, setAccepts] = useState(false);
const [pending, setPending] = useState(false);
const allChecked = adult && aware && accepts;
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!allChecked) return;
setPending(true);
await acceptDisclaimer();
}
return (
<form onSubmit={handleSubmit} className="mt-6 space-y-3">
<Checkbox checked={adult} onChange={setAdult} label={t('checkAdult')} />
<Checkbox checked={aware} onChange={setAware} label={t('checkAware')} />
<Checkbox checked={accepts} onChange={setAccepts} label={t('checkAccepts')} />
<Button type="submit" disabled={!allChecked || pending} className="mt-4 w-full">
{pending ? t('submitting') : t('submit')}
</Button>
</form>
);
}
function Checkbox({
checked,
onChange,
label,
}: {
checked: boolean;
onChange: (next: boolean) => void;
label: string;
}) {
return (
<label className="flex cursor-pointer items-start gap-3 text-sm">
<input
type="checkbox"
className="border-input text-primary focus-visible:ring-ring mt-0.5 h-4 w-4 rounded border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
/>
<span>{label}</span>
</label>
);
}