innercontext/frontend/src/lib/components/ProductForm.svelte

965 lines
40 KiB
Svelte

<script lang="ts">
import { untrack } from 'svelte';
import type { Product } from '$lib/types';
import type { IngredientFunction } from '$lib/types';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { parseProductText, type ProductParseResponse } from '$lib/api';
import { m } from '$lib/paraglide/messages.js';
let { product }: { product?: Product } = $props();
// ── Enum option lists ─────────────────────────────────────────────────────
const categories = [
'cleanser', 'toner', 'essence', 'serum', 'moisturizer',
'spf', 'mask', 'exfoliant', 'hair_treatment', 'tool', 'spot_treatment', 'oil'
];
const textures = ['watery', 'gel', 'emulsion', 'cream', 'oil', 'balm', 'foam', 'fluid'];
const absorptionSpeeds = ['very_fast', 'fast', 'moderate', 'slow', 'very_slow'];
const skinTypes = ['dry', 'oily', 'combination', 'sensitive', 'normal', 'acne_prone'];
const skinConcerns = [
'acne', 'rosacea', 'hyperpigmentation', 'aging', 'dehydration',
'redness', 'damaged_barrier', 'pore_visibility', 'uneven_texture',
'hair_growth', 'sebum_excess'
];
const ingFunctions: 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'
];
// ── Translated label maps ─────────────────────────────────────────────────
const categoryLabels = $derived<Record<string, string>>({
cleanser: m["productForm_categoryCleanser"](),
toner: m["productForm_categoryToner"](),
essence: m["productForm_categoryEssence"](),
serum: m["productForm_categorySerum"](),
moisturizer: m["productForm_categoryMoisturizer"](),
spf: m["productForm_categorySpf"](),
mask: m["productForm_categoryMask"](),
exfoliant: m["productForm_categoryExfoliant"](),
hair_treatment: m["productForm_categoryHairTreatment"](),
tool: m["productForm_categoryTool"](),
spot_treatment: m["productForm_categorySpotTreatment"](),
oil: m["productForm_categoryOil"]()
});
const textureLabels = $derived<Record<string, string>>({
watery: m["productForm_textureWatery"](),
gel: m["productForm_textureGel"](),
emulsion: m["productForm_textureEmulsion"](),
cream: m["productForm_textureCream"](),
oil: m["productForm_textureOil"](),
balm: m["productForm_textureBalm"](),
foam: m["productForm_textureFoam"](),
fluid: m["productForm_textureFluid"]()
});
const absorptionLabels = $derived<Record<string, string>>({
very_fast: m["productForm_absorptionVeryFast"](),
fast: m["productForm_absorptionFast"](),
moderate: m["productForm_absorptionModerate"](),
slow: m["productForm_absorptionSlow"](),
very_slow: m["productForm_absorptionVerySlow"]()
});
const skinTypeLabels = $derived<Record<string, string>>({
dry: m["productForm_skinTypeDry"](),
oily: m["productForm_skinTypeOily"](),
combination: m["productForm_skinTypeCombination"](),
sensitive: m["productForm_skinTypeSensitive"](),
normal: m["productForm_skinTypeNormal"](),
acne_prone: m["productForm_skinTypeAcneProne"]()
});
const skinConcernLabels = $derived<Record<string, string>>({
acne: m["productForm_concernAcne"](),
rosacea: m["productForm_concernRosacea"](),
hyperpigmentation: m["productForm_concernHyperpigmentation"](),
aging: m["productForm_concernAging"](),
dehydration: m["productForm_concernDehydration"](),
redness: m["productForm_concernRedness"](),
damaged_barrier: m["productForm_concernDamagedBarrier"](),
pore_visibility: m["productForm_concernPoreVisibility"](),
uneven_texture: m["productForm_concernUnevenTexture"](),
hair_growth: m["productForm_concernHairGrowth"](),
sebum_excess: m["productForm_concernSebumExcess"]()
});
const ingFunctionLabels = $derived<Record<string, string>>({
humectant: m["productForm_fnHumectant"](),
emollient: m["productForm_fnEmollient"](),
occlusive: m["productForm_fnOcclusive"](),
exfoliant_aha: m["productForm_fnExfoliantAha"](),
exfoliant_bha: m["productForm_fnExfoliantBha"](),
exfoliant_pha: m["productForm_fnExfoliantPha"](),
retinoid: m["productForm_fnRetinoid"](),
antioxidant: m["productForm_fnAntioxidant"](),
soothing: m["productForm_fnSoothing"](),
barrier_support: m["productForm_fnBarrierSupport"](),
brightening: m["productForm_fnBrightening"](),
anti_acne: m["productForm_fnAntiAcne"](),
ceramide: m["productForm_fnCeramide"](),
niacinamide: m["productForm_fnNiacinamide"](),
sunscreen: m["productForm_fnSunscreen"](),
peptide: m["productForm_fnPeptide"](),
hair_growth_stimulant: m["productForm_fnHairGrowth"](),
prebiotic: m["productForm_fnPrebiotic"](),
vitamin_c: m["productForm_fnVitaminC"](),
anti_aging: m["productForm_fnAntiAging"]()
});
const tristate = $derived([
{ value: '', label: m.common_unknown() },
{ value: 'true', label: m.common_yes() },
{ value: 'false', label: m.common_no() }
]);
function tristateLabel(val: string): string {
return val === '' ? m.common_unknown() : val === 'true' ? m.common_yes() : m.common_no();
}
const effectFields = $derived([
{ key: 'hydration_immediate' as const, label: m["productForm_effectHydrationImmediate"]() },
{ key: 'hydration_long_term' as const, label: m["productForm_effectHydrationLongTerm"]() },
{ key: 'barrier_repair_strength' as const, label: m["productForm_effectBarrierRepair"]() },
{ key: 'soothing_strength' as const, label: m["productForm_effectSoothing"]() },
{ key: 'exfoliation_strength' as const, label: m["productForm_effectExfoliation"]() },
{ key: 'retinoid_strength' as const, label: m["productForm_effectRetinoid"]() },
{ key: 'irritation_risk' as const, label: m["productForm_effectIrritation"]() },
{ key: 'comedogenic_risk' as const, label: m["productForm_effectComedogenic"]() },
{ key: 'barrier_disruption_risk' as const, label: m["productForm_effectBarrierDisruption"]() },
{ key: 'dryness_risk' as const, label: m["productForm_effectDryness"]() },
{ key: 'brightening_strength' as const, label: m["productForm_effectBrightening"]() },
{ key: 'anti_acne_strength' as const, label: m["productForm_effectAntiAcne"]() },
{ key: 'anti_aging_strength' as const, label: m["productForm_effectAntiAging"]() }
]);
// ── Controlled text/number inputs ────────────────────────────────────────
let name = $state(untrack(() => product?.name ?? ''));
let brand = $state(untrack(() => product?.brand ?? ''));
let lineName = $state(untrack(() => product?.line_name ?? ''));
let url = $state(untrack(() => product?.url ?? ''));
let sku = $state(untrack(() => product?.sku ?? ''));
let barcode = $state(untrack(() => product?.barcode ?? ''));
let sizeMl = $state(untrack(() => (product?.size_ml != null ? String(product.size_ml) : '')));
let fullWeightG = $state(untrack(() => (product?.full_weight_g != null ? String(product.full_weight_g) : '')));
let emptyWeightG = $state(untrack(() => (product?.empty_weight_g != null ? String(product.empty_weight_g) : '')));
let paoMonths = $state(untrack(() => (product?.pao_months != null ? String(product.pao_months) : '')));
let phMin = $state(untrack(() => (product?.ph_min != null ? String(product.ph_min) : '')));
let phMax = $state(untrack(() => (product?.ph_max != null ? String(product.ph_max) : '')));
let minIntervalHours = $state(untrack(() => (product?.min_interval_hours != null ? String(product.min_interval_hours) : '')));
let maxFrequencyPerWeek = $state(untrack(() => (product?.max_frequency_per_week != null ? String(product.max_frequency_per_week) : '')));
let needleLengthMm = $state(untrack(() => (product?.needle_length_mm != null ? String(product.needle_length_mm) : '')));
let usageNotes = $state(untrack(() => product?.usage_notes ?? ''));
let inciText = $state(untrack(() => product?.inci?.join('\n') ?? ''));
let contraindicationsText = $state(untrack(() => product?.contraindications?.join('\n') ?? ''));
let recommendedFor = $state<string[]>(untrack(() => [...(product?.recommended_for ?? [])]));
let targetConcerns = $state<string[]>(untrack(() => [...(product?.targets ?? [])]));
let isMedication = $state(untrack(() => product?.is_medication ?? false));
let isTool = $state(untrack(() => product?.is_tool ?? false));
// ── AI pre-fill state ─────────────────────────────────────────────────────
let aiPanelOpen = $state(false);
let aiText = $state('');
let aiLoading = $state(false);
let aiError = $state('');
async function parseWithAi() {
if (!aiText.trim()) return;
aiLoading = true;
aiError = '';
try {
const r = await parseProductText(aiText);
applyAiResult(r);
aiPanelOpen = false;
} catch (e) {
aiError = (e as Error).message;
} finally {
aiLoading = false;
}
}
function applyAiResult(r: ProductParseResponse) {
if (r.name) name = r.name;
if (r.brand) brand = r.brand;
if (r.line_name) lineName = r.line_name;
if (r.url) url = r.url;
if (r.sku) sku = r.sku;
if (r.barcode) barcode = r.barcode;
if (r.usage_notes) usageNotes = r.usage_notes;
if (r.category) category = r.category;
if (r.recommended_time) recommendedTime = r.recommended_time;
if (r.texture) texture = r.texture;
if (r.absorption_speed) absorptionSpeed = r.absorption_speed;
if (r.price_amount != null) priceAmount = String(r.price_amount);
if (r.price_currency) priceCurrency = r.price_currency;
if (r.leave_on != null) leaveOn = String(r.leave_on);
if (r.size_ml != null) sizeMl = String(r.size_ml);
if (r.full_weight_g != null) fullWeightG = String(r.full_weight_g);
if (r.empty_weight_g != null) emptyWeightG = String(r.empty_weight_g);
if (r.pao_months != null) paoMonths = String(r.pao_months);
if (r.ph_min != null) phMin = String(r.ph_min);
if (r.ph_max != null) phMax = String(r.ph_max);
if (r.min_interval_hours != null) minIntervalHours = String(r.min_interval_hours);
if (r.max_frequency_per_week != null) maxFrequencyPerWeek = String(r.max_frequency_per_week);
if (r.needle_length_mm != null) needleLengthMm = String(r.needle_length_mm);
if (r.fragrance_free != null) fragranceFree = String(r.fragrance_free);
if (r.essential_oils_free != null) essentialOilsFree = String(r.essential_oils_free);
if (r.alcohol_denat_free != null) alcoholDenatFree = String(r.alcohol_denat_free);
if (r.pregnancy_safe != null) pregnancySafe = String(r.pregnancy_safe);
if (r.recommended_for?.length) recommendedFor = [...r.recommended_for];
if (r.targets?.length) targetConcerns = [...r.targets];
if (r.is_medication != null) isMedication = r.is_medication;
if (r.is_tool != null) isTool = r.is_tool;
if (r.inci?.length) inciText = r.inci.join('\n');
if (r.contraindications?.length) contraindicationsText = r.contraindications.join('\n');
if (r.actives?.length) {
actives = r.actives.map((a) => ({
name: a.name,
percent: a.percent != null ? String(a.percent) : '',
functions: [...(a.functions ?? [])] as IngredientFunction[],
strength_level: a.strength_level != null ? String(a.strength_level) : '',
irritation_potential: a.irritation_potential != null ? String(a.irritation_potential) : ''
}));
}
if (r.product_effect_profile) {
effectValues = { ...effectValues, ...r.product_effect_profile };
}
if (r.context_rules) {
const cr = r.context_rules;
if (cr.safe_after_shaving != null) ctxAfterShaving = String(cr.safe_after_shaving);
if (cr.safe_after_acids != null) ctxAfterAcids = String(cr.safe_after_acids);
if (cr.safe_after_retinoids != null) ctxAfterRetinoids = String(cr.safe_after_retinoids);
if (cr.safe_with_compromised_barrier != null) ctxCompromisedBarrier = String(cr.safe_with_compromised_barrier);
if (cr.low_uv_only != null) ctxLowUvOnly = String(cr.low_uv_only);
}
}
// ── Select reactive state ─────────────────────────────────────────────────
let category = $state(untrack(() => product?.category ?? ''));
let recommendedTime = $state(untrack(() => product?.recommended_time ?? ''));
let leaveOn = $state(untrack(() => (product?.leave_on != null ? String(product.leave_on) : 'true')));
let texture = $state(untrack(() => product?.texture ?? ''));
let absorptionSpeed = $state(untrack(() => product?.absorption_speed ?? ''));
let priceAmount = $state(untrack(() => (product?.price_amount != null ? String(product.price_amount) : '')));
let priceCurrency = $state(untrack(() => product?.price_currency ?? 'PLN'));
let fragranceFree = $state(
untrack(() => (product?.fragrance_free != null ? String(product.fragrance_free) : ''))
);
let essentialOilsFree = $state(
untrack(() => (product?.essential_oils_free != null ? String(product.essential_oils_free) : ''))
);
let alcoholDenatFree = $state(
untrack(() => (product?.alcohol_denat_free != null ? String(product.alcohol_denat_free) : ''))
);
let pregnancySafe = $state(
untrack(() => (product?.pregnancy_safe != null ? String(product.pregnancy_safe) : ''))
);
let personalRepurchaseIntent = $state(
untrack(() => (product?.personal_repurchase_intent != null ? String(product.personal_repurchase_intent) : ''))
);
// context rules tristate
const cr = untrack(() => product?.context_rules);
let ctxAfterShaving = $state(
cr?.safe_after_shaving != null ? String(cr.safe_after_shaving) : ''
);
let ctxAfterAcids = $state(cr?.safe_after_acids != null ? String(cr.safe_after_acids) : '');
let ctxAfterRetinoids = $state(
cr?.safe_after_retinoids != null ? String(cr.safe_after_retinoids) : ''
);
let ctxCompromisedBarrier = $state(
cr?.safe_with_compromised_barrier != null ? String(cr.safe_with_compromised_barrier) : ''
);
let ctxLowUvOnly = $state(cr?.low_uv_only != null ? String(cr.low_uv_only) : '');
// ── Effect profile ────────────────────────────────────────────────────────
const ep = untrack(() => product?.product_effect_profile);
let effectValues = $state({
hydration_immediate: ep?.hydration_immediate ?? 0,
hydration_long_term: ep?.hydration_long_term ?? 0,
barrier_repair_strength: ep?.barrier_repair_strength ?? 0,
soothing_strength: ep?.soothing_strength ?? 0,
exfoliation_strength: ep?.exfoliation_strength ?? 0,
retinoid_strength: ep?.retinoid_strength ?? 0,
irritation_risk: ep?.irritation_risk ?? 0,
comedogenic_risk: ep?.comedogenic_risk ?? 0,
barrier_disruption_risk: ep?.barrier_disruption_risk ?? 0,
dryness_risk: ep?.dryness_risk ?? 0,
brightening_strength: ep?.brightening_strength ?? 0,
anti_acne_strength: ep?.anti_acne_strength ?? 0,
anti_aging_strength: ep?.anti_aging_strength ?? 0
});
// ── Dynamic actives ───────────────────────────────────────────────────────
type ActiveRow = {
name: string;
percent: string;
functions: IngredientFunction[];
strength_level: string;
irritation_potential: string;
};
let actives: ActiveRow[] = $state(
untrack(() =>
product?.actives?.map((a) => ({
name: a.name,
percent: a.percent != null ? String(a.percent) : '',
functions: [...(a.functions ?? [])] as IngredientFunction[],
strength_level: a.strength_level != null ? String(a.strength_level) : '',
irritation_potential: a.irritation_potential != null ? String(a.irritation_potential) : ''
})) ?? []
)
);
function addActive() {
actives = [
...actives,
{ name: '', percent: '', functions: [], strength_level: '', irritation_potential: '' }
];
}
function removeActive(i: number) {
actives = actives.filter((_, idx) => idx !== i);
}
function toggleFn(i: number, fn: IngredientFunction) {
const cur = actives[i].functions;
actives[i].functions = cur.includes(fn) ? cur.filter((f) => f !== fn) : [...cur, fn];
}
let activesJson = $derived(
JSON.stringify(
actives
.filter((a) => a.name.trim())
.map((a) => ({
name: a.name.trim(),
...(a.percent ? { percent: parseFloat(a.percent) } : {}),
functions: a.functions,
...(a.strength_level ? { strength_level: parseInt(a.strength_level) } : {}),
...(a.irritation_potential
? { irritation_potential: parseInt(a.irritation_potential) }
: {})
}))
)
);
const textareaClass =
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
const selectClass =
'h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-ring';
</script>
<!-- ── AI pre-fill ──────────────────────────────────────────────────────────── -->
<Card>
<CardHeader>
<button type="button" class="flex w-full items-center justify-between text-left"
onclick={() => (aiPanelOpen = !aiPanelOpen)}>
<CardTitle>{m["productForm_aiPrefill"]()}</CardTitle>
<span class="text-sm text-muted-foreground">{aiPanelOpen ? '▲' : '▼'}</span>
</button>
</CardHeader>
{#if aiPanelOpen}
<CardContent class="space-y-3">
<p class="text-sm text-muted-foreground">{m["productForm_aiPrefillText"]()}</p>
<textarea bind:value={aiText} rows="6"
placeholder={m["productForm_pasteText"]()}
class={textareaClass}></textarea>
{#if aiError}
<p class="text-sm text-destructive">{aiError}</p>
{/if}
<Button type="button" onclick={parseWithAi}
disabled={aiLoading || !aiText.trim()}>
{aiLoading ? m["productForm_parsing"]() : m["productForm_parseWithAI"]()}
</Button>
</CardContent>
{/if}
</Card>
<!-- ── Basic info ──────────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_basicInfo"]()}</CardTitle></CardHeader>
<CardContent class="space-y-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="name">{m["productForm_name"]()}</Label>
<Input id="name" name="name" required placeholder={m["productForm_namePlaceholder"]()} bind:value={name} />
</div>
<div class="space-y-2">
<Label for="brand">{m["productForm_brand"]()}</Label>
<Input id="brand" name="brand" required placeholder={m["productForm_brandPlaceholder"]()} bind:value={brand} />
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="line_name">{m["productForm_lineName"]()}</Label>
<Input id="line_name" name="line_name" placeholder={m["productForm_lineNamePlaceholder"]()} bind:value={lineName} />
</div>
<div class="space-y-2">
<Label for="url">{m["productForm_url"]()}</Label>
<Input id="url" name="url" type="url" placeholder="https://…" bind:value={url} />
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="sku">{m["productForm_sku"]()}</Label>
<Input id="sku" name="sku" placeholder={m["productForm_skuPlaceholder"]()} bind:value={sku} />
</div>
<div class="space-y-2">
<Label for="barcode">{m["productForm_barcode"]()}</Label>
<Input id="barcode" name="barcode" placeholder={m["productForm_barcodePlaceholder"]()} bind:value={barcode} />
</div>
</div>
</CardContent>
</Card>
<!-- ── Classification ────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_classification"]()}</CardTitle></CardHeader>
<CardContent>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="col-span-2 space-y-2">
<Label>{m["productForm_category"]()}</Label>
<input type="hidden" name="category" value={category} />
<Select type="single" value={category} onValueChange={(v) => (category = v)}>
<SelectTrigger>{category ? categoryLabels[category] : m["productForm_selectCategory"]()}</SelectTrigger>
<SelectContent>
{#each categories as cat}
<SelectItem value={cat}>{categoryLabels[cat]}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_time"]()}</Label>
<input type="hidden" name="recommended_time" value={recommendedTime} />
<Select type="single" value={recommendedTime} onValueChange={(v) => (recommendedTime = v)}>
<SelectTrigger>
{recommendedTime ? recommendedTime.toUpperCase() : m["productForm_timeOptions"]()}
</SelectTrigger>
<SelectContent>
<SelectItem value="am">AM</SelectItem>
<SelectItem value="pm">PM</SelectItem>
<SelectItem value="both">{m["productForm_timeBoth"]()}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_leaveOn"]()}</Label>
<input type="hidden" name="leave_on" value={leaveOn} />
<Select type="single" value={leaveOn} onValueChange={(v) => (leaveOn = v)}>
<SelectTrigger>{leaveOn === 'true' ? m["productForm_leaveOnYes"]() : m["productForm_leaveOnNo"]()}</SelectTrigger>
<SelectContent>
<SelectItem value="true">{m["productForm_leaveOnYes"]()}</SelectItem>
<SelectItem value="false">{m["productForm_leaveOnNo"]()}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_texture"]()}</Label>
<input type="hidden" name="texture" value={texture} />
<Select type="single" value={texture} onValueChange={(v) => (texture = v)}>
<SelectTrigger>{texture ? textureLabels[texture] : m["productForm_selectTexture"]()}</SelectTrigger>
<SelectContent>
{#each textures as t}
<SelectItem value={t}>{textureLabels[t]}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_absorptionSpeed"]()}</Label>
<input type="hidden" name="absorption_speed" value={absorptionSpeed} />
<Select type="single" value={absorptionSpeed} onValueChange={(v) => (absorptionSpeed = v)}>
<SelectTrigger>{absorptionSpeed ? absorptionLabels[absorptionSpeed] : m["productForm_selectSpeed"]()}</SelectTrigger>
<SelectContent>
{#each absorptionSpeeds as s}
<SelectItem value={s}>{absorptionLabels[s]}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<!-- ── Skin profile ───────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_skinProfile"]()}</CardTitle></CardHeader>
<CardContent class="space-y-4">
<div class="space-y-2">
<Label>{m["productForm_recommendedFor"]()}</Label>
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
{#each skinTypes as st}
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input
type="checkbox"
name="recommended_for"
value={st}
checked={recommendedFor.includes(st)}
onchange={() => {
if (recommendedFor.includes(st))
recommendedFor = recommendedFor.filter((s) => s !== st);
else
recommendedFor = [...recommendedFor, st];
}}
class="rounded border-input"
/>
{skinTypeLabels[st]}
</label>
{/each}
</div>
</div>
<div class="space-y-2">
<Label>{m["productForm_targetConcerns"]()}</Label>
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
{#each skinConcerns as sc}
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input
type="checkbox"
name="targets"
value={sc}
checked={targetConcerns.includes(sc)}
onchange={() => {
if (targetConcerns.includes(sc))
targetConcerns = targetConcerns.filter((s) => s !== sc);
else
targetConcerns = [...targetConcerns, sc];
}}
class="rounded border-input"
/>
{skinConcernLabels[sc]}
</label>
{/each}
</div>
</div>
<div class="space-y-2">
<Label for="contraindications">{m["productForm_contraindications"]()}</Label>
<textarea
id="contraindications"
name="contraindications"
rows="2"
placeholder={m["productForm_contraindicationsPlaceholder"]()}
class={textareaClass}
bind:value={contraindicationsText}
></textarea>
</div>
</CardContent>
</Card>
<!-- ── Ingredients ────────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_ingredients"]()}</CardTitle></CardHeader>
<CardContent class="space-y-6">
<div class="space-y-2">
<Label for="inci">{m["productForm_inciList"]()}</Label>
<textarea
id="inci"
name="inci"
rows="5"
placeholder="Aqua&#10;Glycerin&#10;Niacinamide"
class={textareaClass}
bind:value={inciText}
></textarea>
</div>
<div class="space-y-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<Label>{m["productForm_activeIngredients"]()}</Label>
<Button type="button" variant="outline" size="sm" onclick={addActive}>{m["productForm_addActive"]()}</Button>
</div>
<input type="hidden" name="actives_json" value={activesJson} />
{#each actives as active, i}
<div class="rounded-md border border-border p-3 space-y-3">
<div class="flex items-end gap-2">
<div class="min-w-0 flex-1 space-y-1">
<Label class="text-xs">{m["productForm_activeName"]()}</Label>
<Input
placeholder="e.g. Niacinamide"
bind:value={active.name}
/>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onclick={() => removeActive(i)}
class="h-7 w-7 shrink-0 p-0 text-destructive hover:text-destructive"
></Button>
</div>
<div class="grid grid-cols-3 gap-2">
<div class="space-y-1">
<Label class="text-xs">{m["productForm_activePercent"]()}</Label>
<Input
type="number"
min="0"
max="100"
step="0.01"
placeholder="e.g. 5"
bind:value={active.percent}
/>
</div>
<div class="space-y-1">
<Label class="text-xs">{m["productForm_activeStrength"]()}</Label>
<select class={selectClass} bind:value={active.strength_level}>
<option value=""></option>
<option value="1">{m["productForm_strengthLow"]()}</option>
<option value="2">{m["productForm_strengthMedium"]()}</option>
<option value="3">{m["productForm_strengthHigh"]()}</option>
</select>
</div>
<div class="space-y-1">
<Label class="text-xs">{m["productForm_activeIrritation"]()}</Label>
<select class={selectClass} bind:value={active.irritation_potential}>
<option value=""></option>
<option value="1">{m["productForm_strengthLow"]()}</option>
<option value="2">{m["productForm_strengthMedium"]()}</option>
<option value="3">{m["productForm_strengthHigh"]()}</option>
</select>
</div>
</div>
<div class="space-y-1">
<Label class="text-xs text-muted-foreground">{m["productForm_activeFunctions"]()}</Label>
<div class="grid grid-cols-2 gap-1 sm:grid-cols-4">
{#each ingFunctions as fn}
<label class="flex cursor-pointer items-center gap-1.5 text-xs">
<input
type="checkbox"
checked={active.functions.includes(fn)}
onchange={() => toggleFn(i, fn)}
class="rounded border-input"
/>
{ingFunctionLabels[fn]}
</label>
{/each}
</div>
</div>
</div>
{/each}
{#if actives.length === 0}
<p class="text-sm text-muted-foreground">{m["productForm_noActives"]()}</p>
{/if}
</div>
</CardContent>
</Card>
<!-- ── Effect profile ─────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_effectProfile"]()}</CardTitle></CardHeader>
<CardContent>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
{#each effectFields as field}
{@const key = field.key as keyof typeof effectValues}
<div class="grid grid-cols-[minmax(7rem,10rem)_1fr_1.25rem] items-center gap-3">
<span class="text-xs text-muted-foreground">{field.label}</span>
<input
type="range"
name="effect_{field.key}"
min="0"
max="5"
step="1"
bind:value={effectValues[key]}
class="accent-primary"
/>
<span class="text-center font-mono text-sm">{effectValues[key]}</span>
</div>
{/each}
</div>
</CardContent>
</Card>
<!-- ── Context rules ──────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_contextRules"]()}</CardTitle></CardHeader>
<CardContent>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label>{m["productForm_ctxAfterShaving"]()}</Label>
<input type="hidden" name="ctx_safe_after_shaving" value={ctxAfterShaving} />
<Select type="single" value={ctxAfterShaving} onValueChange={(v) => (ctxAfterShaving = v)}>
<SelectTrigger>{tristateLabel(ctxAfterShaving)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_ctxAfterAcids"]()}</Label>
<input type="hidden" name="ctx_safe_after_acids" value={ctxAfterAcids} />
<Select type="single" value={ctxAfterAcids} onValueChange={(v) => (ctxAfterAcids = v)}>
<SelectTrigger>{tristateLabel(ctxAfterAcids)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_ctxAfterRetinoids"]()}</Label>
<input type="hidden" name="ctx_safe_after_retinoids" value={ctxAfterRetinoids} />
<Select
type="single"
value={ctxAfterRetinoids}
onValueChange={(v) => (ctxAfterRetinoids = v)}
>
<SelectTrigger>{tristateLabel(ctxAfterRetinoids)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_ctxCompromisedBarrier"]()}</Label>
<input type="hidden" name="ctx_safe_with_compromised_barrier" value={ctxCompromisedBarrier} />
<Select
type="single"
value={ctxCompromisedBarrier}
onValueChange={(v) => (ctxCompromisedBarrier = v)}
>
<SelectTrigger>{tristateLabel(ctxCompromisedBarrier)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_ctxLowUvOnly"]()}</Label>
<input type="hidden" name="ctx_low_uv_only" value={ctxLowUvOnly} />
<Select type="single" value={ctxLowUvOnly} onValueChange={(v) => (ctxLowUvOnly = v)}>
<SelectTrigger>{tristateLabel(ctxLowUvOnly)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<!-- ── Product details ────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_productDetails"]()}</CardTitle></CardHeader>
<CardContent class="space-y-4">
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3">
<div class="space-y-2">
<Label for="price_amount">Price</Label>
<Input id="price_amount" name="price_amount" type="number" min="0" step="0.01" placeholder="e.g. 79.99" bind:value={priceAmount} />
</div>
<div class="space-y-2">
<Label for="price_currency">Currency</Label>
<Input id="price_currency" name="price_currency" maxlength={3} placeholder="PLN" bind:value={priceCurrency} />
</div>
<div class="space-y-2">
<Label for="size_ml">{m["productForm_sizeMl"]()}</Label>
<Input id="size_ml" name="size_ml" type="number" min="0" step="0.1" placeholder="e.g. 50" bind:value={sizeMl} />
</div>
<div class="space-y-2">
<Label for="full_weight_g">{m["productForm_fullWeightG"]()}</Label>
<Input id="full_weight_g" name="full_weight_g" type="number" min="0" step="0.1" placeholder="e.g. 120" bind:value={fullWeightG} />
</div>
<div class="space-y-2">
<Label for="empty_weight_g">{m["productForm_emptyWeightG"]()}</Label>
<Input id="empty_weight_g" name="empty_weight_g" type="number" min="0" step="0.1" placeholder="e.g. 30" bind:value={emptyWeightG} />
</div>
<div class="space-y-2">
<Label for="pao_months">{m["productForm_paoMonths"]()}</Label>
<Input id="pao_months" name="pao_months" type="number" min="1" max="60" placeholder="e.g. 12" bind:value={paoMonths} />
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="ph_min">{m["productForm_phMin"]()}</Label>
<Input id="ph_min" name="ph_min" type="number" min="0" max="14" step="0.1" placeholder="e.g. 3.5" bind:value={phMin} />
</div>
<div class="space-y-2">
<Label for="ph_max">{m["productForm_phMax"]()}</Label>
<Input id="ph_max" name="ph_max" type="number" min="0" max="14" step="0.1" placeholder="e.g. 4.5" bind:value={phMax} />
</div>
</div>
<div class="space-y-2">
<Label for="usage_notes">{m["productForm_usageNotes"]()}</Label>
<textarea
id="usage_notes"
name="usage_notes"
rows="2"
placeholder={m["productForm_usageNotesPlaceholder"]()}
class={textareaClass}
bind:value={usageNotes}
></textarea>
</div>
</CardContent>
</Card>
<!-- ── Safety flags ───────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_safetyFlags"]()}</CardTitle></CardHeader>
<CardContent>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label>{m["productForm_fragranceFree"]()}</Label>
<input type="hidden" name="fragrance_free" value={fragranceFree} />
<Select type="single" value={fragranceFree} onValueChange={(v) => (fragranceFree = v)}>
<SelectTrigger>{tristateLabel(fragranceFree)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_essentialOilsFree"]()}</Label>
<input type="hidden" name="essential_oils_free" value={essentialOilsFree} />
<Select
type="single"
value={essentialOilsFree}
onValueChange={(v) => (essentialOilsFree = v)}
>
<SelectTrigger>{tristateLabel(essentialOilsFree)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_alcoholDenatFree"]()}</Label>
<input type="hidden" name="alcohol_denat_free" value={alcoholDenatFree} />
<Select
type="single"
value={alcoholDenatFree}
onValueChange={(v) => (alcoholDenatFree = v)}
>
<SelectTrigger>{tristateLabel(alcoholDenatFree)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label>{m["productForm_pregnancySafe"]()}</Label>
<input type="hidden" name="pregnancy_safe" value={pregnancySafe} />
<Select type="single" value={pregnancySafe} onValueChange={(v) => (pregnancySafe = v)}>
<SelectTrigger>{tristateLabel(pregnancySafe)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<!-- ── Usage constraints ──────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_usageConstraints"]()}</CardTitle></CardHeader>
<CardContent class="space-y-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="min_interval_hours">{m["productForm_minIntervalHours"]()}</Label>
<Input id="min_interval_hours" name="min_interval_hours" type="number" min="0" placeholder="e.g. 24" bind:value={minIntervalHours} />
</div>
<div class="space-y-2">
<Label for="max_frequency_per_week">{m["productForm_maxFrequencyPerWeek"]()}</Label>
<Input id="max_frequency_per_week" name="max_frequency_per_week" type="number" min="1" max="14" placeholder="e.g. 3" bind:value={maxFrequencyPerWeek} />
</div>
</div>
<div class="flex gap-6">
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input type="hidden" name="is_medication" value={String(isMedication)} />
<input
type="checkbox"
checked={isMedication}
onchange={() => (isMedication = !isMedication)}
class="rounded border-input"
/>
{m["productForm_isMedication"]()}
</label>
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input type="hidden" name="is_tool" value={String(isTool)} />
<input
type="checkbox"
checked={isTool}
onchange={() => (isTool = !isTool)}
class="rounded border-input"
/>
{m["productForm_isTool"]()}
</label>
</div>
<div class="space-y-2">
<Label for="needle_length_mm">{m["productForm_needleLengthMm"]()}</Label>
<Input id="needle_length_mm" name="needle_length_mm" type="number" min="0" step="0.01" placeholder="e.g. 0.25" bind:value={needleLengthMm} />
</div>
</CardContent>
</Card>
<!-- ── Personal notes ─────────────────────────────────────────────────────── -->
<Card>
<CardHeader><CardTitle>{m["productForm_personalNotes"]()}</CardTitle></CardHeader>
<CardContent class="space-y-4">
<div class="space-y-2">
<Label>{m["productForm_repurchaseIntent"]()}</Label>
<input type="hidden" name="personal_repurchase_intent" value={personalRepurchaseIntent} />
<Select
type="single"
value={personalRepurchaseIntent}
onValueChange={(v) => (personalRepurchaseIntent = v)}
>
<SelectTrigger>{tristateLabel(personalRepurchaseIntent)}</SelectTrigger>
<SelectContent>
{#each tristate as opt}
<SelectItem value={opt.value}>{opt.label}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="personal_tolerance_notes">{m["productForm_toleranceNotes"]()}</Label>
<textarea
id="personal_tolerance_notes"
name="personal_tolerance_notes"
rows="2"
placeholder={m["productForm_toleranceNotesPlaceholder"]()}
class={textareaClass}
>{product?.personal_tolerance_notes ?? ''}</textarea>
</div>
</CardContent>
</Card>