chore(frontend): format files with prettier

This commit is contained in:
Piotr Oleszczyk 2026-03-03 20:51:34 +01:00
parent 0e7a39836f
commit 067e460dd2
20 changed files with 1615 additions and 1509 deletions

View file

@ -5,85 +5,85 @@
/* ── CSS variable definitions (light / dark) ─────────────────────────────── */
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(240 10% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(240 10% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(240 10% 3.9%);
--primary: hsl(240 5.9% 10%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(240 4.8% 95.9%);
--secondary-foreground: hsl(240 5.9% 10%);
--muted: hsl(240 4.8% 95.9%);
--muted-foreground: hsl(240 3.8% 46.1%);
--accent: hsl(240 4.8% 95.9%);
--accent-foreground: hsl(240 5.9% 10%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(240 5.9% 90%);
--input: hsl(240 5.9% 90%);
--ring: hsl(240 5.9% 10%);
--radius: 0.5rem;
--background: hsl(0 0% 100%);
--foreground: hsl(240 10% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(240 10% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(240 10% 3.9%);
--primary: hsl(240 5.9% 10%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(240 4.8% 95.9%);
--secondary-foreground: hsl(240 5.9% 10%);
--muted: hsl(240 4.8% 95.9%);
--muted-foreground: hsl(240 3.8% 46.1%);
--accent: hsl(240 4.8% 95.9%);
--accent-foreground: hsl(240 5.9% 10%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(240 5.9% 90%);
--input: hsl(240 5.9% 90%);
--ring: hsl(240 5.9% 10%);
--radius: 0.5rem;
}
.dark {
--background: hsl(240 10% 3.9%);
--foreground: hsl(0 0% 98%);
--card: hsl(240 10% 3.9%);
--card-foreground: hsl(0 0% 98%);
--popover: hsl(240 10% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(240 5.9% 10%);
--secondary: hsl(240 3.7% 15.9%);
--secondary-foreground: hsl(0 0% 98%);
--muted: hsl(240 3.7% 15.9%);
--muted-foreground: hsl(240 5% 64.9%);
--accent: hsl(240 3.7% 15.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(240 3.7% 15.9%);
--input: hsl(240 3.7% 15.9%);
--ring: hsl(240 4.9% 83.9%);
--background: hsl(240 10% 3.9%);
--foreground: hsl(0 0% 98%);
--card: hsl(240 10% 3.9%);
--card-foreground: hsl(0 0% 98%);
--popover: hsl(240 10% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(240 5.9% 10%);
--secondary: hsl(240 3.7% 15.9%);
--secondary-foreground: hsl(0 0% 98%);
--muted: hsl(240 3.7% 15.9%);
--muted-foreground: hsl(240 5% 64.9%);
--accent: hsl(240 3.7% 15.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(240 3.7% 15.9%);
--input: hsl(240 3.7% 15.9%);
--ring: hsl(240 4.9% 83.9%);
}
/* ── Map CSS vars → Tailwind v4 design tokens ────────────────────────────── */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
/* ── Base resets ─────────────────────────────────────────────────────────── */
* {
border-color: var(--border);
border-color: var(--border);
}
body {
background-color: var(--background);
color: var(--foreground);
background-color: var(--background);
color: var(--foreground);
}

14
frontend/src/app.d.ts vendored
View file

@ -1,13 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View file

@ -1,11 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -1,6 +1,6 @@
import { paraglideMiddleware } from '$lib/paraglide/server.js';
import type { Handle } from '@sveltejs/kit';
import { paraglideMiddleware } from "$lib/paraglide/server.js";
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
return paraglideMiddleware(event.request, () => resolve(event));
return paraglideMiddleware(event.request, () => resolve(event));
};

View file

@ -1,280 +1,349 @@
import { browser } from '$app/environment';
import { PUBLIC_API_BASE } from '$env/static/public';
import { browser } from "$app/environment";
import { PUBLIC_API_BASE } from "$env/static/public";
import type {
ActiveIngredient,
BatchSuggestion,
GroomingSchedule,
LabResult,
MedicationEntry,
MedicationUsage,
PartOfDay,
Product,
ProductContext,
ProductEffectProfile,
ProductInteraction,
ProductInventory,
Routine,
RoutineSuggestion,
RoutineStep,
SkinConditionSnapshot
} from './types';
ActiveIngredient,
BatchSuggestion,
GroomingSchedule,
LabResult,
MedicationEntry,
MedicationUsage,
PartOfDay,
Product,
ProductContext,
ProductEffectProfile,
ProductInteraction,
ProductInventory,
Routine,
RoutineSuggestion,
RoutineStep,
SkinConditionSnapshot,
} from "./types";
// ─── Core fetch helpers ──────────────────────────────────────────────────────
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
// Server-side uses PUBLIC_API_BASE (e.g. http://localhost:8000).
// Browser-side uses /api so nginx proxies the request on the correct host.
const base = browser ? '/api' : PUBLIC_API_BASE;
const url = `${base}${path}`;
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...init.headers },
...init
});
if (!res.ok) {
const detail = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(detail?.detail ?? res.statusText);
}
if (res.status === 204) return undefined as T;
return res.json();
// Server-side uses PUBLIC_API_BASE (e.g. http://localhost:8000).
// Browser-side uses /api so nginx proxies the request on the correct host.
const base = browser ? "/api" : PUBLIC_API_BASE;
const url = `${base}${path}`;
const res = await fetch(url, {
headers: { "Content-Type": "application/json", ...init.headers },
...init,
});
if (!res.ok) {
const detail = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(detail?.detail ?? res.statusText);
}
if (res.status === 204) return undefined as T;
return res.json();
}
export const api = {
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body: unknown) =>
request<T>(path, { method: 'POST', body: JSON.stringify(body) }),
patch: <T>(path: string, body: unknown) =>
request<T>(path, { method: 'PATCH', body: JSON.stringify(body) }),
del: (path: string) => request<void>(path, { method: 'DELETE' })
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body: unknown) =>
request<T>(path, { method: "POST", body: JSON.stringify(body) }),
patch: <T>(path: string, body: unknown) =>
request<T>(path, { method: "PATCH", body: JSON.stringify(body) }),
del: (path: string) => request<void>(path, { method: "DELETE" }),
};
// ─── Products ────────────────────────────────────────────────────────────────
export interface ProductListParams {
category?: string;
brand?: string;
targets?: string[];
is_medication?: boolean;
is_tool?: boolean;
category?: string;
brand?: string;
targets?: string[];
is_medication?: boolean;
is_tool?: boolean;
}
export function getProducts(params: ProductListParams = {}): Promise<Product[]> {
const q = new URLSearchParams();
if (params.category) q.set('category', params.category);
if (params.brand) q.set('brand', params.brand);
if (params.targets) params.targets.forEach((t) => q.append('targets', t));
if (params.is_medication != null) q.set('is_medication', String(params.is_medication));
if (params.is_tool != null) q.set('is_tool', String(params.is_tool));
const qs = q.toString();
return api.get(`/products${qs ? `?${qs}` : ''}`);
export function getProducts(
params: ProductListParams = {},
): Promise<Product[]> {
const q = new URLSearchParams();
if (params.category) q.set("category", params.category);
if (params.brand) q.set("brand", params.brand);
if (params.targets) params.targets.forEach((t) => q.append("targets", t));
if (params.is_medication != null)
q.set("is_medication", String(params.is_medication));
if (params.is_tool != null) q.set("is_tool", String(params.is_tool));
const qs = q.toString();
return api.get(`/products${qs ? `?${qs}` : ""}`);
}
export const getProduct = (id: string): Promise<Product> => api.get(`/products/${id}`);
export const createProduct = (body: Record<string, unknown>): Promise<Product> =>
api.post('/products', body);
export const updateProduct = (id: string, body: Record<string, unknown>): Promise<Product> =>
api.patch(`/products/${id}`, body);
export const deleteProduct = (id: string): Promise<void> => api.del(`/products/${id}`);
export const getProduct = (id: string): Promise<Product> =>
api.get(`/products/${id}`);
export const createProduct = (
body: Record<string, unknown>,
): Promise<Product> => api.post("/products", body);
export const updateProduct = (
id: string,
body: Record<string, unknown>,
): Promise<Product> => api.patch(`/products/${id}`, body);
export const deleteProduct = (id: string): Promise<void> =>
api.del(`/products/${id}`);
export const getInventory = (productId: string): Promise<ProductInventory[]> =>
api.get(`/products/${productId}/inventory`);
api.get(`/products/${productId}/inventory`);
export const createInventory = (
productId: string,
body: Record<string, unknown>
): Promise<ProductInventory> => api.post(`/products/${productId}/inventory`, body);
export const updateInventory = (id: string, body: Record<string, unknown>): Promise<ProductInventory> =>
api.patch(`/inventory/${id}`, body);
export const deleteInventory = (id: string): Promise<void> => api.del(`/inventory/${id}`);
productId: string,
body: Record<string, unknown>,
): Promise<ProductInventory> =>
api.post(`/products/${productId}/inventory`, body);
export const updateInventory = (
id: string,
body: Record<string, unknown>,
): Promise<ProductInventory> => api.patch(`/inventory/${id}`, body);
export const deleteInventory = (id: string): Promise<void> =>
api.del(`/inventory/${id}`);
export interface ProductParseResponse {
name?: string; brand?: string; line_name?: string; sku?: string; url?: string; barcode?: string;
category?: string; recommended_time?: string; texture?: string; absorption_speed?: string;
leave_on?: boolean; price_tier?: string;
size_ml?: number; full_weight_g?: number; empty_weight_g?: number; pao_months?: number;
inci?: string[]; actives?: ActiveIngredient[];
recommended_for?: string[]; targets?: string[];
contraindications?: string[]; usage_notes?: string;
fragrance_free?: boolean; essential_oils_free?: boolean;
alcohol_denat_free?: boolean; pregnancy_safe?: boolean;
product_effect_profile?: ProductEffectProfile;
ph_min?: number; ph_max?: number;
incompatible_with?: ProductInteraction[]; synergizes_with?: string[];
context_rules?: ProductContext;
min_interval_hours?: number; max_frequency_per_week?: number;
is_medication?: boolean; is_tool?: boolean; needle_length_mm?: number;
name?: string;
brand?: string;
line_name?: string;
sku?: string;
url?: string;
barcode?: string;
category?: string;
recommended_time?: string;
texture?: string;
absorption_speed?: string;
leave_on?: boolean;
price_tier?: string;
size_ml?: number;
full_weight_g?: number;
empty_weight_g?: number;
pao_months?: number;
inci?: string[];
actives?: ActiveIngredient[];
recommended_for?: string[];
targets?: string[];
contraindications?: string[];
usage_notes?: string;
fragrance_free?: boolean;
essential_oils_free?: boolean;
alcohol_denat_free?: boolean;
pregnancy_safe?: boolean;
product_effect_profile?: ProductEffectProfile;
ph_min?: number;
ph_max?: number;
incompatible_with?: ProductInteraction[];
synergizes_with?: string[];
context_rules?: ProductContext;
min_interval_hours?: number;
max_frequency_per_week?: number;
is_medication?: boolean;
is_tool?: boolean;
needle_length_mm?: number;
}
export const parseProductText = (text: string): Promise<ProductParseResponse> =>
api.post('/products/parse-text', { text });
api.post("/products/parse-text", { text });
// ─── Routines ────────────────────────────────────────────────────────────────
export interface RoutineListParams {
from_date?: string;
to_date?: string;
part_of_day?: string;
from_date?: string;
to_date?: string;
part_of_day?: string;
}
export function getRoutines(params: RoutineListParams = {}): Promise<Routine[]> {
const q = new URLSearchParams();
if (params.from_date) q.set('from_date', params.from_date);
if (params.to_date) q.set('to_date', params.to_date);
if (params.part_of_day) q.set('part_of_day', params.part_of_day);
const qs = q.toString();
return api.get(`/routines${qs ? `?${qs}` : ''}`);
export function getRoutines(
params: RoutineListParams = {},
): Promise<Routine[]> {
const q = new URLSearchParams();
if (params.from_date) q.set("from_date", params.from_date);
if (params.to_date) q.set("to_date", params.to_date);
if (params.part_of_day) q.set("part_of_day", params.part_of_day);
const qs = q.toString();
return api.get(`/routines${qs ? `?${qs}` : ""}`);
}
export const getRoutine = (id: string): Promise<Routine> => api.get(`/routines/${id}`);
export const createRoutine = (body: Record<string, unknown>): Promise<Routine> =>
api.post('/routines', body);
export const updateRoutine = (id: string, body: Record<string, unknown>): Promise<Routine> =>
api.patch(`/routines/${id}`, body);
export const deleteRoutine = (id: string): Promise<void> => api.del(`/routines/${id}`);
export const getRoutine = (id: string): Promise<Routine> =>
api.get(`/routines/${id}`);
export const createRoutine = (
body: Record<string, unknown>,
): Promise<Routine> => api.post("/routines", body);
export const updateRoutine = (
id: string,
body: Record<string, unknown>,
): Promise<Routine> => api.patch(`/routines/${id}`, body);
export const deleteRoutine = (id: string): Promise<void> =>
api.del(`/routines/${id}`);
export const addRoutineStep = (routineId: string, body: Record<string, unknown>): Promise<RoutineStep> =>
api.post(`/routines/${routineId}/steps`, body);
export const updateRoutineStep = (stepId: string, body: Record<string, unknown>): Promise<RoutineStep> =>
api.patch(`/routines/steps/${stepId}`, body);
export const addRoutineStep = (
routineId: string,
body: Record<string, unknown>,
): Promise<RoutineStep> => api.post(`/routines/${routineId}/steps`, body);
export const updateRoutineStep = (
stepId: string,
body: Record<string, unknown>,
): Promise<RoutineStep> => api.patch(`/routines/steps/${stepId}`, body);
export const deleteRoutineStep = (stepId: string): Promise<void> =>
api.del(`/routines/steps/${stepId}`);
api.del(`/routines/steps/${stepId}`);
export const suggestRoutine = (body: {
routine_date: string;
part_of_day: PartOfDay;
notes?: string;
include_minoxidil_beard?: boolean;
leaving_home?: boolean;
}): Promise<RoutineSuggestion> => api.post('/routines/suggest', body);
routine_date: string;
part_of_day: PartOfDay;
notes?: string;
include_minoxidil_beard?: boolean;
leaving_home?: boolean;
}): Promise<RoutineSuggestion> => api.post("/routines/suggest", body);
export const suggestBatch = (body: {
from_date: string;
to_date: string;
notes?: string;
include_minoxidil_beard?: boolean;
minimize_products?: boolean;
}): Promise<BatchSuggestion> => api.post('/routines/suggest-batch', body);
from_date: string;
to_date: string;
notes?: string;
include_minoxidil_beard?: boolean;
minimize_products?: boolean;
}): Promise<BatchSuggestion> => api.post("/routines/suggest-batch", body);
export const getGroomingSchedule = (): Promise<GroomingSchedule[]> =>
api.get('/routines/grooming-schedule');
export const createGroomingScheduleEntry = (body: Record<string, unknown>): Promise<GroomingSchedule> =>
api.post('/routines/grooming-schedule', body);
export const updateGroomingScheduleEntry = (id: string, body: Record<string, unknown>): Promise<GroomingSchedule> =>
api.patch(`/routines/grooming-schedule/${id}`, body);
api.get("/routines/grooming-schedule");
export const createGroomingScheduleEntry = (
body: Record<string, unknown>,
): Promise<GroomingSchedule> => api.post("/routines/grooming-schedule", body);
export const updateGroomingScheduleEntry = (
id: string,
body: Record<string, unknown>,
): Promise<GroomingSchedule> =>
api.patch(`/routines/grooming-schedule/${id}`, body);
export const deleteGroomingScheduleEntry = (id: string): Promise<void> =>
api.del(`/routines/grooming-schedule/${id}`);
api.del(`/routines/grooming-schedule/${id}`);
// ─── Health Medications ────────────────────────────────────────────────────
export interface MedicationListParams {
kind?: string;
product_name?: string;
kind?: string;
product_name?: string;
}
export function getMedications(params: MedicationListParams = {}): Promise<MedicationEntry[]> {
const q = new URLSearchParams();
if (params.kind) q.set('kind', params.kind);
if (params.product_name) q.set('product_name', params.product_name);
const qs = q.toString();
return api.get(`/health/medications${qs ? `?${qs}` : ''}`);
export function getMedications(
params: MedicationListParams = {},
): Promise<MedicationEntry[]> {
const q = new URLSearchParams();
if (params.kind) q.set("kind", params.kind);
if (params.product_name) q.set("product_name", params.product_name);
const qs = q.toString();
return api.get(`/health/medications${qs ? `?${qs}` : ""}`);
}
export const getMedication = (id: string): Promise<MedicationEntry> =>
api.get(`/health/medications/${id}`);
export const createMedication = (body: Record<string, unknown>): Promise<MedicationEntry> =>
api.post('/health/medications', body);
api.get(`/health/medications/${id}`);
export const createMedication = (
body: Record<string, unknown>,
): Promise<MedicationEntry> => api.post("/health/medications", body);
export const updateMedication = (
id: string,
body: Record<string, unknown>
id: string,
body: Record<string, unknown>,
): Promise<MedicationEntry> => api.patch(`/health/medications/${id}`, body);
export const deleteMedication = (id: string): Promise<void> =>
api.del(`/health/medications/${id}`);
api.del(`/health/medications/${id}`);
export const getMedicationUsages = (medicationId: string): Promise<MedicationUsage[]> =>
api.get(`/health/medications/${medicationId}/usages`);
export const getMedicationUsages = (
medicationId: string,
): Promise<MedicationUsage[]> =>
api.get(`/health/medications/${medicationId}/usages`);
export const createMedicationUsage = (
medicationId: string,
body: Record<string, unknown>
): Promise<MedicationUsage> => api.post(`/health/medications/${medicationId}/usages`, body);
medicationId: string,
body: Record<string, unknown>,
): Promise<MedicationUsage> =>
api.post(`/health/medications/${medicationId}/usages`, body);
// ─── Health Lab results ────────────────────────────────────────────────────
export interface LabResultListParams {
test_code?: string;
flag?: string;
lab?: string;
from_date?: string;
to_date?: string;
test_code?: string;
flag?: string;
lab?: string;
from_date?: string;
to_date?: string;
}
export function getLabResults(params: LabResultListParams = {}): Promise<LabResult[]> {
const q = new URLSearchParams();
if (params.test_code) q.set('test_code', params.test_code);
if (params.flag) q.set('flag', params.flag);
if (params.lab) q.set('lab', params.lab);
if (params.from_date) q.set('from_date', params.from_date);
if (params.to_date) q.set('to_date', params.to_date);
const qs = q.toString();
return api.get(`/health/lab-results${qs ? `?${qs}` : ''}`);
export function getLabResults(
params: LabResultListParams = {},
): Promise<LabResult[]> {
const q = new URLSearchParams();
if (params.test_code) q.set("test_code", params.test_code);
if (params.flag) q.set("flag", params.flag);
if (params.lab) q.set("lab", params.lab);
if (params.from_date) q.set("from_date", params.from_date);
if (params.to_date) q.set("to_date", params.to_date);
const qs = q.toString();
return api.get(`/health/lab-results${qs ? `?${qs}` : ""}`);
}
export const getLabResult = (id: string): Promise<LabResult> =>
api.get(`/health/lab-results/${id}`);
export const createLabResult = (body: Record<string, unknown>): Promise<LabResult> =>
api.post('/health/lab-results', body);
export const updateLabResult = (id: string, body: Record<string, unknown>): Promise<LabResult> =>
api.patch(`/health/lab-results/${id}`, body);
api.get(`/health/lab-results/${id}`);
export const createLabResult = (
body: Record<string, unknown>,
): Promise<LabResult> => api.post("/health/lab-results", body);
export const updateLabResult = (
id: string,
body: Record<string, unknown>,
): Promise<LabResult> => api.patch(`/health/lab-results/${id}`, body);
export const deleteLabResult = (id: string): Promise<void> =>
api.del(`/health/lab-results/${id}`);
api.del(`/health/lab-results/${id}`);
// ─── Skin ────────────────────────────────────────────────────────────────────
export interface SnapshotListParams {
from_date?: string;
to_date?: string;
overall_state?: string;
from_date?: string;
to_date?: string;
overall_state?: string;
}
export function getSkinSnapshots(params: SnapshotListParams = {}): Promise<SkinConditionSnapshot[]> {
const q = new URLSearchParams();
if (params.from_date) q.set('from_date', params.from_date);
if (params.to_date) q.set('to_date', params.to_date);
if (params.overall_state) q.set('overall_state', params.overall_state);
const qs = q.toString();
return api.get(`/skincare${qs ? `?${qs}` : ''}`);
export function getSkinSnapshots(
params: SnapshotListParams = {},
): Promise<SkinConditionSnapshot[]> {
const q = new URLSearchParams();
if (params.from_date) q.set("from_date", params.from_date);
if (params.to_date) q.set("to_date", params.to_date);
if (params.overall_state) q.set("overall_state", params.overall_state);
const qs = q.toString();
return api.get(`/skincare${qs ? `?${qs}` : ""}`);
}
export const getSkinSnapshot = (id: string): Promise<SkinConditionSnapshot> =>
api.get(`/skincare/${id}`);
export const createSkinSnapshot = (body: Record<string, unknown>): Promise<SkinConditionSnapshot> =>
api.post('/skincare', body);
api.get(`/skincare/${id}`);
export const createSkinSnapshot = (
body: Record<string, unknown>,
): Promise<SkinConditionSnapshot> => api.post("/skincare", body);
export const updateSkinSnapshot = (
id: string,
body: Record<string, unknown>
id: string,
body: Record<string, unknown>,
): Promise<SkinConditionSnapshot> => api.patch(`/skincare/${id}`, body);
export const deleteSkinSnapshot = (id: string): Promise<void> => api.del(`/skincare/${id}`);
export const deleteSkinSnapshot = (id: string): Promise<void> =>
api.del(`/skincare/${id}`);
export interface SkinPhotoAnalysisResponse {
overall_state?: string;
texture?: string;
skin_type?: string;
hydration_level?: number;
sebum_tzone?: number;
sebum_cheeks?: number;
sensitivity_level?: number;
barrier_state?: string;
active_concerns?: string[];
risks?: string[];
priorities?: string[];
notes?: string;
overall_state?: string;
texture?: string;
skin_type?: string;
hydration_level?: number;
sebum_tzone?: number;
sebum_cheeks?: number;
sensitivity_level?: number;
barrier_state?: string;
active_concerns?: string[];
risks?: string[];
priorities?: string[];
notes?: string;
}
export async function analyzeSkinPhotos(files: File[]): Promise<SkinPhotoAnalysisResponse> {
const body = new FormData();
for (const file of files) body.append('photos', file);
const base = browser ? '/api' : PUBLIC_API_BASE;
const res = await fetch(`${base}/skincare/analyze-photos`, { method: 'POST', body });
if (!res.ok) {
const detail = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(detail?.detail ?? res.statusText);
}
return res.json();
export async function analyzeSkinPhotos(
files: File[],
): Promise<SkinPhotoAnalysisResponse> {
const body = new FormData();
for (const file of files) body.append("photos", file);
const base = browser ? "/api" : PUBLIC_API_BASE;
const res = await fetch(`${base}/skincare/analyze-photos`, {
method: "POST",
body,
});
if (!res.ok) {
const detail = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(detail?.detail ?? res.statusText);
}
return res.json();
}

View file

@ -1,17 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View file

@ -7,19 +7,19 @@ import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};

View file

@ -1,7 +1,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
Root,
//
Root as Input,
};

View file

@ -1,7 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
Root,
//
Root as Label,
};

View file

@ -11,27 +11,27 @@ import GroupHeading from "./select-group-heading.svelte";
import Portal from "./select-portal.svelte";
export {
Root,
Group,
Label,
Item,
Content,
Trigger,
Separator,
ScrollDownButton,
ScrollUpButton,
GroupHeading,
Portal,
//
Root as Select,
Group as SelectGroup,
Label as SelectLabel,
Item as SelectItem,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator,
ScrollDownButton as SelectScrollDownButton,
ScrollUpButton as SelectScrollUpButton,
GroupHeading as SelectGroupHeading,
Portal as SelectPortal,
Root,
Group,
Label,
Item,
Content,
Trigger,
Separator,
ScrollDownButton,
ScrollUpButton,
GroupHeading,
Portal,
//
Root as Select,
Group as SelectGroup,
Label as SelectLabel,
Item as SelectItem,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator,
ScrollDownButton as SelectScrollDownButton,
ScrollUpButton as SelectScrollUpButton,
GroupHeading as SelectGroupHeading,
Portal as SelectPortal,
};

View file

@ -1,7 +1,7 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
Root,
//
Root as Separator,
};

View file

@ -8,21 +8,21 @@ import Header from "./table-header.svelte";
import Row from "./table-row.svelte";
export {
Root,
Body,
Caption,
Cell,
Footer,
Head,
Header,
Row,
//
Root as Table,
Body as TableBody,
Caption as TableCaption,
Cell as TableCell,
Footer as TableFooter,
Head as TableHead,
Header as TableHeader,
Row as TableRow,
Root,
Body,
Caption,
Cell,
Footer,
Head,
Header,
Row,
//
Root as Table,
Body as TableBody,
Caption as TableCaption,
Cell as TableCell,
Footer as TableFooter,
Head as TableHead,
Header as TableHeader,
Row as TableRow,
};

View file

@ -4,13 +4,13 @@ import List from "./tabs-list.svelte";
import Trigger from "./tabs-trigger.svelte";
export {
Root,
Content,
List,
Trigger,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
Root,
Content,
List,
Trigger,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};

View file

@ -1,308 +1,335 @@
// ─── Enums ──────────────────────────────────────────────────────────────────
export type AbsorptionSpeed = 'very_fast' | 'fast' | 'moderate' | 'slow' | 'very_slow';
export type BarrierState = 'intact' | 'mildly_compromised' | 'compromised';
export type DayTime = 'am' | 'pm' | 'both';
export type GroomingAction = 'shaving_razor' | 'shaving_oneblade' | 'dermarolling';
export type AbsorptionSpeed =
| "very_fast"
| "fast"
| "moderate"
| "slow"
| "very_slow";
export type BarrierState = "intact" | "mildly_compromised" | "compromised";
export type DayTime = "am" | "pm" | "both";
export type GroomingAction =
| "shaving_razor"
| "shaving_oneblade"
| "dermarolling";
export type IngredientFunction =
| 'humectant'
| 'emollient'
| 'occlusive'
| 'exfoliant_aha'
| 'exfoliant_bha'
| 'exfoliant_pha'
| 'retinoid'
| 'antioxidant'
| 'soothing'
| 'barrier_support'
| 'brightening'
| 'anti_acne'
| 'ceramide'
| 'niacinamide'
| 'sunscreen'
| 'peptide'
| 'hair_growth_stimulant'
| 'prebiotic'
| 'vitamin_c'
| 'anti_aging';
export type InteractionScope = 'same_step' | 'same_day' | 'same_period';
export type MedicationKind = 'prescription' | 'otc' | 'supplement' | 'herbal' | 'other';
export type OverallSkinState = 'excellent' | 'good' | 'fair' | 'poor';
export type PartOfDay = 'am' | 'pm';
export type PriceTier = 'budget' | 'mid' | 'premium' | 'luxury';
| "humectant"
| "emollient"
| "occlusive"
| "exfoliant_aha"
| "exfoliant_bha"
| "exfoliant_pha"
| "retinoid"
| "antioxidant"
| "soothing"
| "barrier_support"
| "brightening"
| "anti_acne"
| "ceramide"
| "niacinamide"
| "sunscreen"
| "peptide"
| "hair_growth_stimulant"
| "prebiotic"
| "vitamin_c"
| "anti_aging";
export type InteractionScope = "same_step" | "same_day" | "same_period";
export type MedicationKind =
| "prescription"
| "otc"
| "supplement"
| "herbal"
| "other";
export type OverallSkinState = "excellent" | "good" | "fair" | "poor";
export type PartOfDay = "am" | "pm";
export type PriceTier = "budget" | "mid" | "premium" | "luxury";
export type ProductCategory =
| 'cleanser'
| 'toner'
| 'essence'
| 'serum'
| 'moisturizer'
| 'spf'
| 'mask'
| 'exfoliant'
| 'hair_treatment'
| 'tool'
| 'spot_treatment'
| 'oil';
export type ResultFlag = 'N' | 'ABN' | 'POS' | 'NEG' | 'L' | 'H';
| "cleanser"
| "toner"
| "essence"
| "serum"
| "moisturizer"
| "spf"
| "mask"
| "exfoliant"
| "hair_treatment"
| "tool"
| "spot_treatment"
| "oil";
export type ResultFlag = "N" | "ABN" | "POS" | "NEG" | "L" | "H";
export type SkinConcern =
| 'acne'
| 'rosacea'
| 'hyperpigmentation'
| 'aging'
| 'dehydration'
| 'redness'
| 'damaged_barrier'
| 'pore_visibility'
| 'uneven_texture'
| 'hair_growth'
| 'sebum_excess';
export type SkinTexture = 'smooth' | 'rough' | 'flaky' | 'bumpy';
export type SkinType = 'dry' | 'oily' | 'combination' | 'sensitive' | 'normal' | 'acne_prone';
| "acne"
| "rosacea"
| "hyperpigmentation"
| "aging"
| "dehydration"
| "redness"
| "damaged_barrier"
| "pore_visibility"
| "uneven_texture"
| "hair_growth"
| "sebum_excess";
export type SkinTexture = "smooth" | "rough" | "flaky" | "bumpy";
export type SkinType =
| "dry"
| "oily"
| "combination"
| "sensitive"
| "normal"
| "acne_prone";
export type StrengthLevel = 1 | 2 | 3;
export type TextureType = 'watery' | 'gel' | 'emulsion' | 'cream' | 'oil' | 'balm' | 'foam' | 'fluid';
export type TextureType =
| "watery"
| "gel"
| "emulsion"
| "cream"
| "oil"
| "balm"
| "foam"
| "fluid";
// ─── Product types ───────────────────────────────────────────────────────────
export interface ActiveIngredient {
name: string;
percent?: number;
functions: IngredientFunction[];
strength_level?: StrengthLevel;
irritation_potential?: StrengthLevel;
name: string;
percent?: number;
functions: IngredientFunction[];
strength_level?: StrengthLevel;
irritation_potential?: StrengthLevel;
}
export interface ProductEffectProfile {
hydration_immediate: number;
hydration_long_term: number;
barrier_repair_strength: number;
soothing_strength: number;
exfoliation_strength: number;
retinoid_strength: number;
irritation_risk: number;
comedogenic_risk: number;
barrier_disruption_risk: number;
dryness_risk: number;
brightening_strength: number;
anti_acne_strength: number;
anti_aging_strength: number;
hydration_immediate: number;
hydration_long_term: number;
barrier_repair_strength: number;
soothing_strength: number;
exfoliation_strength: number;
retinoid_strength: number;
irritation_risk: number;
comedogenic_risk: number;
barrier_disruption_risk: number;
dryness_risk: number;
brightening_strength: number;
anti_acne_strength: number;
anti_aging_strength: number;
}
export interface ProductInteraction {
target: string;
scope: InteractionScope;
reason?: string;
target: string;
scope: InteractionScope;
reason?: string;
}
export interface ProductContext {
safe_after_shaving?: boolean;
safe_after_acids?: boolean;
safe_after_retinoids?: boolean;
safe_with_compromised_barrier?: boolean;
low_uv_only?: boolean;
safe_after_shaving?: boolean;
safe_after_acids?: boolean;
safe_after_retinoids?: boolean;
safe_with_compromised_barrier?: boolean;
low_uv_only?: boolean;
}
export interface ProductInventory {
id: string;
product_id: string;
is_opened: boolean;
opened_at?: string;
finished_at?: string;
expiry_date?: string;
current_weight_g?: number;
last_weighed_at?: string;
notes?: string;
created_at: string;
product?: Product;
id: string;
product_id: string;
is_opened: boolean;
opened_at?: string;
finished_at?: string;
expiry_date?: string;
current_weight_g?: number;
last_weighed_at?: string;
notes?: string;
created_at: string;
product?: Product;
}
export interface Product {
id: string;
name: string;
brand: string;
line_name?: string;
sku?: string;
url?: string;
barcode?: string;
category: ProductCategory;
recommended_time: DayTime;
texture?: TextureType;
absorption_speed?: AbsorptionSpeed;
leave_on: boolean;
price_tier?: PriceTier;
size_ml?: number;
full_weight_g?: number;
empty_weight_g?: number;
pao_months?: number;
inci: string[];
actives?: ActiveIngredient[];
recommended_for: SkinType[];
targets: SkinConcern[];
contraindications: string[];
usage_notes?: string;
fragrance_free?: boolean;
essential_oils_free?: boolean;
alcohol_denat_free?: boolean;
pregnancy_safe?: boolean;
product_effect_profile: ProductEffectProfile;
ph_min?: number;
ph_max?: number;
incompatible_with?: ProductInteraction[];
synergizes_with?: string[];
context_rules?: ProductContext;
min_interval_hours?: number;
max_frequency_per_week?: number;
is_medication: boolean;
is_tool: boolean;
needle_length_mm?: number;
personal_tolerance_notes?: string;
personal_repurchase_intent?: boolean;
created_at: string;
updated_at: string;
inventory: ProductInventory[];
id: string;
name: string;
brand: string;
line_name?: string;
sku?: string;
url?: string;
barcode?: string;
category: ProductCategory;
recommended_time: DayTime;
texture?: TextureType;
absorption_speed?: AbsorptionSpeed;
leave_on: boolean;
price_tier?: PriceTier;
size_ml?: number;
full_weight_g?: number;
empty_weight_g?: number;
pao_months?: number;
inci: string[];
actives?: ActiveIngredient[];
recommended_for: SkinType[];
targets: SkinConcern[];
contraindications: string[];
usage_notes?: string;
fragrance_free?: boolean;
essential_oils_free?: boolean;
alcohol_denat_free?: boolean;
pregnancy_safe?: boolean;
product_effect_profile: ProductEffectProfile;
ph_min?: number;
ph_max?: number;
incompatible_with?: ProductInteraction[];
synergizes_with?: string[];
context_rules?: ProductContext;
min_interval_hours?: number;
max_frequency_per_week?: number;
is_medication: boolean;
is_tool: boolean;
needle_length_mm?: number;
personal_tolerance_notes?: string;
personal_repurchase_intent?: boolean;
created_at: string;
updated_at: string;
inventory: ProductInventory[];
}
// ─── Routine types ───────────────────────────────────────────────────────────
export interface RoutineStep {
id: string;
routine_id: string;
product_id?: string;
order_index: number;
action_type?: GroomingAction;
action_notes?: string;
dose?: string;
region?: string;
product?: Product;
id: string;
routine_id: string;
product_id?: string;
order_index: number;
action_type?: GroomingAction;
action_notes?: string;
dose?: string;
region?: string;
product?: Product;
}
export interface Routine {
id: string;
routine_date: string;
part_of_day: PartOfDay;
notes?: string;
created_at: string;
updated_at: string;
steps?: RoutineStep[];
id: string;
routine_date: string;
part_of_day: PartOfDay;
notes?: string;
created_at: string;
updated_at: string;
steps?: RoutineStep[];
}
export interface GroomingSchedule {
id: string;
day_of_week: number;
action: GroomingAction;
notes?: string;
id: string;
day_of_week: number;
action: GroomingAction;
notes?: string;
}
export interface SuggestedStep {
product_id?: string;
action_type?: GroomingAction;
action_notes?: string;
dose?: string;
region?: string;
product_id?: string;
action_type?: GroomingAction;
action_notes?: string;
dose?: string;
region?: string;
}
export interface RoutineSuggestion {
steps: SuggestedStep[];
reasoning: string;
steps: SuggestedStep[];
reasoning: string;
}
export interface DayPlan {
date: string;
am_steps: SuggestedStep[];
pm_steps: SuggestedStep[];
reasoning: string;
date: string;
am_steps: SuggestedStep[];
pm_steps: SuggestedStep[];
reasoning: string;
}
export interface BatchSuggestion {
days: DayPlan[];
overall_reasoning: string;
days: DayPlan[];
overall_reasoning: string;
}
// ─── Shopping suggestion types ───────────────────────────────────────────────
export interface ProductSuggestion {
category: string;
product_type: string;
key_ingredients: string[];
target_concerns: string[];
why_needed: string;
recommended_time: string;
frequency: string;
category: string;
product_type: string;
key_ingredients: string[];
target_concerns: string[];
why_needed: string;
recommended_time: string;
frequency: string;
}
export interface ShoppingSuggestionResponse {
suggestions: ProductSuggestion[];
reasoning: string;
suggestions: ProductSuggestion[];
reasoning: string;
}
// ─── Health types ────────────────────────────────────────────────────────────
export interface MedicationUsage {
record_id: string;
medication_record_id: string;
dose_value?: number;
dose_unit?: string;
frequency?: string;
schedule_text?: string;
as_needed: boolean;
valid_from: string;
valid_to?: string;
source_file?: string;
notes?: string;
created_at: string;
updated_at: string;
record_id: string;
medication_record_id: string;
dose_value?: number;
dose_unit?: string;
frequency?: string;
schedule_text?: string;
as_needed: boolean;
valid_from: string;
valid_to?: string;
source_file?: string;
notes?: string;
created_at: string;
updated_at: string;
}
export interface MedicationEntry {
record_id: string;
kind: MedicationKind;
product_name: string;
active_substance?: string;
formulation?: string;
route?: string;
source_file?: string;
notes?: string;
created_at: string;
updated_at: string;
usage_history: MedicationUsage[];
record_id: string;
kind: MedicationKind;
product_name: string;
active_substance?: string;
formulation?: string;
route?: string;
source_file?: string;
notes?: string;
created_at: string;
updated_at: string;
usage_history: MedicationUsage[];
}
export interface LabResult {
record_id: string;
collected_at: string;
test_code: string;
test_name_original?: string;
test_name_loinc?: string;
value_num?: number;
value_text?: string;
value_bool?: boolean;
unit_original?: string;
unit_ucum?: string;
ref_low?: number;
ref_high?: number;
ref_text?: string;
flag?: ResultFlag;
lab?: string;
source_file?: string;
notes?: string;
created_at: string;
updated_at: string;
record_id: string;
collected_at: string;
test_code: string;
test_name_original?: string;
test_name_loinc?: string;
value_num?: number;
value_text?: string;
value_bool?: boolean;
unit_original?: string;
unit_ucum?: string;
ref_low?: number;
ref_high?: number;
ref_text?: string;
flag?: ResultFlag;
lab?: string;
source_file?: string;
notes?: string;
created_at: string;
updated_at: string;
}
// ─── Skin types ──────────────────────────────────────────────────────────────
export interface SkinConditionSnapshot {
id: string;
snapshot_date: string;
overall_state?: OverallSkinState;
skin_type?: SkinType;
texture?: SkinTexture;
hydration_level?: number;
sebum_tzone?: number;
sebum_cheeks?: number;
sensitivity_level?: number;
barrier_state?: BarrierState;
active_concerns: SkinConcern[];
risks: string[];
priorities: string[];
notes?: string;
created_at: string;
id: string;
snapshot_date: string;
overall_state?: OverallSkinState;
skin_type?: SkinType;
texture?: SkinTexture;
hydration_level?: number;
sebum_tzone?: number;
sebum_cheeks?: number;
sensitivity_level?: number;
barrier_state?: BarrierState;
active_concerns: SkinConcern[];
risks: string[];
priorities: string[];
notes?: string;
created_at: string;
}