Shared + migration: table user_preference (thème clair/sombre/système) (step 1/4)

- Modèle Prisma UserPreference (1-1 avec UserProfile, onDelete Cascade),
  enum ThemePreference (LIGHT/DARK/SYSTEM, défaut SYSTEM)
- packages/shared: THEME_PREFERENCES/ThemePreference, PreferencesView,
  updatePreferencesSchema
This commit is contained in:
Nicolas 2026-08-17 15:55:18 +02:00
parent db427a3f30
commit e3811dc280
5 changed files with 67 additions and 0 deletions

View file

@ -0,0 +1,13 @@
-- CreateEnum
CREATE TYPE "ThemePreference" AS ENUM ('LIGHT', 'DARK', 'SYSTEM');
-- CreateTable
CREATE TABLE "user_preference" (
"user_profile_id" INTEGER NOT NULL,
"theme" "ThemePreference" NOT NULL DEFAULT 'SYSTEM',
CONSTRAINT "user_preference_pkey" PRIMARY KEY ("user_profile_id")
);
-- AddForeignKey
ALTER TABLE "user_preference" ADD CONSTRAINT "user_preference_user_profile_id_fkey" FOREIGN KEY ("user_profile_id") REFERENCES "user_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -98,10 +98,36 @@ model UserProfile {
/// a time — but Prisma models the admin side of a one-to-many FK as a
/// list regardless of that real-world cardinality.
administeredHouses House[] @relation("HouseAdmin")
preferences UserPreference?
@@map("user_profiles")
}
/// Not in the original spec doc — personalization settings (theme for now,
/// meant to grow), one row per profile, created on demand (see
/// `preferences.service.ts`) rather than at signup — same "absent means the
/// default" philosophy as `dietId`/allergies.
enum ThemePreference {
LIGHT
DARK
/// Follow the OS/browser preference — the default. Not "no row yet" (that
/// case is handled in the service layer) but an explicit choice to track
/// the system, distinguishable from a user who hasn't decided yet if this
/// model ever needs that distinction.
SYSTEM
}
model UserPreference {
/// Both the primary key and the FK — a strict 1-1 with UserProfile, no
/// separate auto-incrementing id (a profile has at most one preferences row).
userProfileId Int @id @map("user_profile_id")
theme ThemePreference @default(SYSTEM)
userProfile UserProfile @relation(fields: [userProfileId], references: [id], onDelete: Cascade)
@@map("user_preference")
}
/// Explicit join table for the user_profiles <-> allergy association
/// (documented in the spec as a plain many-to-many, no extra fields).
model UserProfileAllergy {

View file

@ -8,9 +8,11 @@ export * from "./schemas/account.js";
export * from "./schemas/auth.js";
export * from "./schemas/household.js";
export * from "./schemas/planning.js";
export * from "./schemas/preferences.js";
export * from "./schemas/profile.js";
export * from "./tools/assert-is-never.js";
export * from "./types/household.js";
export * from "./types/planning.js";
export * from "./types/preferences.js";
export * from "./types/reference.js";
export * from "./types/user-profile.js";

View file

@ -0,0 +1,11 @@
import { z } from "zod";
import { THEME_PREFERENCES } from "../types/preferences.js";
// See schemas/auth.ts for the shared client/server validation rationale.
/** Payload accepted by `PATCH /preferences`. */
export const updatePreferencesSchema = z.object({
theme: z.enum(THEME_PREFERENCES),
});
/** Inferred TS type for {@link updatePreferencesSchema}'s validated output. */
export type UpdatePreferencesInput = z.infer<typeof updatePreferencesSchema>;

View file

@ -0,0 +1,15 @@
/**
* The 3 values a profile's theme preference can take `SYSTEM` means "no
* explicit choice, follow the OS/browser preference" (see `apps/web`'s
* `ThemeContext`, which maps this to *not* setting the `data-theme`
* attribute at all, letting `_theme.scss`'s `prefers-color-scheme` media
* query take over).
*/
export const THEME_PREFERENCES = ["LIGHT", "DARK", "SYSTEM"] as const;
/** Inferred TS type for one {@link THEME_PREFERENCES} member. */
export type ThemePreference = (typeof THEME_PREFERENCES)[number];
/** A profile's personalization preferences, as returned by `GET /preferences` / `PATCH /preferences`. */
export interface PreferencesView {
theme: ThemePreference;
}