feat(frontend): add PL/EN i18n using @inlang/paraglide-js v2
- Install @inlang/paraglide-js v2 with Vite plugin and paraglideMiddleware hook - Add messages/pl.json and messages/en.json with ~400 translation keys - Create project.inlang/settings.json (PL as base locale) - Add LanguageSwitcher component (cookie-based, no URL prefix needed) - Replace all hardcoded strings across 14 pages/components with m.*() calls - ProductForm uses derived label maps for all enum types (category, texture, etc.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9524e4df54
commit
99584521a1
22 changed files with 1742 additions and 612 deletions
|
|
@ -8,13 +8,10 @@
|
|||
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();
|
||||
|
||||
function lbl(val: string) {
|
||||
return val.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
// ── Enum option lists ─────────────────────────────────────────────────────
|
||||
|
||||
const categories = [
|
||||
|
|
@ -38,26 +35,127 @@
|
|||
'sunscreen', 'peptide', 'hair_growth_stimulant', 'prebiotic', 'vitamin_c', 'anti_aging'
|
||||
];
|
||||
const interactionScopes: InteractionScope[] = ['same_step', 'same_day', 'same_period'];
|
||||
const tristate = [
|
||||
{ value: '', label: 'Unknown' },
|
||||
{ value: 'true', label: 'Yes' },
|
||||
{ value: 'false', label: 'No' }
|
||||
];
|
||||
const effectFields = [
|
||||
{ key: 'hydration_immediate', label: 'Hydration (immediate)' },
|
||||
{ key: 'hydration_long_term', label: 'Hydration (long term)' },
|
||||
{ key: 'barrier_repair_strength', label: 'Barrier repair' },
|
||||
{ key: 'soothing_strength', label: 'Soothing' },
|
||||
{ key: 'exfoliation_strength', label: 'Exfoliation' },
|
||||
{ key: 'retinoid_strength', label: 'Retinoid activity' },
|
||||
{ key: 'irritation_risk', label: 'Irritation risk' },
|
||||
{ key: 'comedogenic_risk', label: 'Comedogenic risk' },
|
||||
{ key: 'barrier_disruption_risk', label: 'Barrier disruption risk' },
|
||||
{ key: 'dryness_risk', label: 'Dryness risk' },
|
||||
{ key: 'brightening_strength', label: 'Brightening' },
|
||||
{ key: 'anti_acne_strength', label: 'Anti-acne' },
|
||||
{ key: 'anti_aging_strength', label: 'Anti-aging' }
|
||||
] as const;
|
||||
|
||||
// ── 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 priceTierLabels = $derived<Record<string, string>>({
|
||||
budget: m["productForm_priceBudget"](),
|
||||
mid: m["productForm_priceMid"](),
|
||||
premium: m["productForm_pricePremium"](),
|
||||
luxury: m["productForm_priceLuxury"]()
|
||||
});
|
||||
|
||||
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 scopeLabels = $derived<Record<string, string>>({
|
||||
same_step: m["productForm_scopeSameStep"](),
|
||||
same_day: m["productForm_scopeSameDay"](),
|
||||
same_period: m["productForm_scopeSamePeriod"]()
|
||||
});
|
||||
|
||||
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 ────────────────────────────────────────
|
||||
|
||||
|
|
@ -328,25 +426,22 @@
|
|||
<CardHeader>
|
||||
<button type="button" class="flex w-full items-center justify-between text-left"
|
||||
onclick={() => (aiPanelOpen = !aiPanelOpen)}>
|
||||
<CardTitle>AI pre-fill</CardTitle>
|
||||
<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">
|
||||
Wklej opis produktu ze strony, listę składników lub inny tekst.
|
||||
AI uzupełni dostępne pola — możesz je przejrzeć i poprawić przed zapisem.
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">{m["productForm_aiPrefillText"]()}</p>
|
||||
<textarea bind:value={aiText} rows="6"
|
||||
placeholder="Wklej tutaj opis produktu, składniki INCI..."
|
||||
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 ? 'Przetwarzam…' : 'Uzupełnij pola (AI)'}
|
||||
{aiLoading ? m["productForm_parsing"]() : m["productForm_parseWithAI"]()}
|
||||
</Button>
|
||||
</CardContent>
|
||||
{/if}
|
||||
|
|
@ -354,36 +449,36 @@
|
|||
|
||||
<!-- ── Basic info ──────────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Basic info</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_basicInfo"]()}</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Name *</Label>
|
||||
<Input id="name" name="name" required placeholder="e.g. Hydro Boost Water Gel" bind:value={name} />
|
||||
<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">Brand *</Label>
|
||||
<Input id="brand" name="brand" required placeholder="e.g. Neutrogena" bind:value={brand} />
|
||||
<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-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="line_name">Line / series</Label>
|
||||
<Input id="line_name" name="line_name" placeholder="e.g. Hydro Boost" bind:value={lineName} />
|
||||
<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">URL</Label>
|
||||
<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-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="sku">SKU</Label>
|
||||
<Input id="sku" name="sku" placeholder="e.g. NTR-HB-50" bind:value={sku} />
|
||||
<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">Barcode / EAN</Label>
|
||||
<Input id="barcode" name="barcode" placeholder="e.g. 3614273258975" bind:value={barcode} />
|
||||
<Label for="barcode">{m["productForm_barcode"]()}</Label>
|
||||
<Input id="barcode" name="barcode" placeholder={m["productForm_barcodePlaceholder"]()} bind:value={barcode} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -391,70 +486,70 @@
|
|||
|
||||
<!-- ── Classification ────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Classification</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_classification"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2 space-y-2">
|
||||
<Label>Category *</Label>
|
||||
<Label>{m["productForm_category"]()}</Label>
|
||||
<input type="hidden" name="category" value={category} />
|
||||
<Select type="single" value={category} onValueChange={(v) => (category = v)}>
|
||||
<SelectTrigger>{category ? lbl(category) : 'Select category'}</SelectTrigger>
|
||||
<SelectTrigger>{category ? categoryLabels[category] : m["productForm_selectCategory"]()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each categories as cat}
|
||||
<SelectItem value={cat}>{lbl(cat)}</SelectItem>
|
||||
<SelectItem value={cat}>{categoryLabels[cat]}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Time *</Label>
|
||||
<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() : 'AM / PM / Both'}
|
||||
{recommendedTime ? recommendedTime.toUpperCase() : m["productForm_timeOptions"]()}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="am">AM</SelectItem>
|
||||
<SelectItem value="pm">PM</SelectItem>
|
||||
<SelectItem value="both">Both</SelectItem>
|
||||
<SelectItem value="both">{m["productForm_timeBoth"]()}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Leave-on *</Label>
|
||||
<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' ? 'Yes (leave-on)' : 'No (rinse-off)'}</SelectTrigger>
|
||||
<SelectTrigger>{leaveOn === 'true' ? m["productForm_leaveOnYes"]() : m["productForm_leaveOnNo"]()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">Yes (leave-on)</SelectItem>
|
||||
<SelectItem value="false">No (rinse-off)</SelectItem>
|
||||
<SelectItem value="true">{m["productForm_leaveOnYes"]()}</SelectItem>
|
||||
<SelectItem value="false">{m["productForm_leaveOnNo"]()}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Texture</Label>
|
||||
<Label>{m["productForm_texture"]()}</Label>
|
||||
<input type="hidden" name="texture" value={texture} />
|
||||
<Select type="single" value={texture} onValueChange={(v) => (texture = v)}>
|
||||
<SelectTrigger>{texture ? lbl(texture) : 'Select texture'}</SelectTrigger>
|
||||
<SelectTrigger>{texture ? textureLabels[texture] : m["productForm_selectTexture"]()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each textures as t}
|
||||
<SelectItem value={t}>{lbl(t)}</SelectItem>
|
||||
<SelectItem value={t}>{textureLabels[t]}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Absorption speed</Label>
|
||||
<Label>{m["productForm_absorptionSpeed"]()}</Label>
|
||||
<input type="hidden" name="absorption_speed" value={absorptionSpeed} />
|
||||
<Select type="single" value={absorptionSpeed} onValueChange={(v) => (absorptionSpeed = v)}>
|
||||
<SelectTrigger>{absorptionSpeed ? lbl(absorptionSpeed) : 'Select speed'}</SelectTrigger>
|
||||
<SelectTrigger>{absorptionSpeed ? absorptionLabels[absorptionSpeed] : m["productForm_selectSpeed"]()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each absorptionSpeeds as s}
|
||||
<SelectItem value={s}>{lbl(s)}</SelectItem>
|
||||
<SelectItem value={s}>{absorptionLabels[s]}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
@ -465,10 +560,10 @@
|
|||
|
||||
<!-- ── Skin profile ───────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Skin profile</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_skinProfile"]()}</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Recommended for skin types</Label>
|
||||
<Label>{m["productForm_recommendedFor"]()}</Label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
{#each skinTypes as st}
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
|
|
@ -485,14 +580,14 @@
|
|||
}}
|
||||
class="rounded border-input"
|
||||
/>
|
||||
{lbl(st)}
|
||||
{skinTypeLabels[st]}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Target concerns</Label>
|
||||
<Label>{m["productForm_targetConcerns"]()}</Label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
{#each skinConcerns as sc}
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
|
|
@ -509,19 +604,19 @@
|
|||
}}
|
||||
class="rounded border-input"
|
||||
/>
|
||||
{lbl(sc)}
|
||||
{skinConcernLabels[sc]}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="contraindications">Contraindications (one per line)</Label>
|
||||
<Label for="contraindications">{m["productForm_contraindications"]()}</Label>
|
||||
<textarea
|
||||
id="contraindications"
|
||||
name="contraindications"
|
||||
rows="2"
|
||||
placeholder="e.g. active rosacea flares"
|
||||
placeholder={m["productForm_contraindicationsPlaceholder"]()}
|
||||
class={textareaClass}
|
||||
bind:value={contraindicationsText}
|
||||
></textarea>
|
||||
|
|
@ -531,10 +626,10 @@
|
|||
|
||||
<!-- ── Ingredients ────────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Ingredients</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_ingredients"]()}</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<Label for="inci">INCI list (one ingredient per line)</Label>
|
||||
<Label for="inci">{m["productForm_inciList"]()}</Label>
|
||||
<textarea
|
||||
id="inci"
|
||||
name="inci"
|
||||
|
|
@ -547,8 +642,8 @@
|
|||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>Active ingredients</Label>
|
||||
<Button type="button" variant="outline" size="sm" onclick={addActive}>+ Add active</Button>
|
||||
<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} />
|
||||
|
|
@ -557,14 +652,14 @@
|
|||
<div class="rounded-md border border-border p-3 space-y-3">
|
||||
<div class="grid grid-cols-[1fr_100px_120px_120px_auto] gap-2 items-end">
|
||||
<div class="space-y-1">
|
||||
<Label class="text-xs">Name</Label>
|
||||
<Label class="text-xs">{m["productForm_activeName"]()}</Label>
|
||||
<Input
|
||||
placeholder="e.g. Niacinamide"
|
||||
bind:value={active.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label class="text-xs">%</Label>
|
||||
<Label class="text-xs">{m["productForm_activePercent"]()}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
|
|
@ -575,21 +670,21 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label class="text-xs">Strength</Label>
|
||||
<Label class="text-xs">{m["productForm_activeStrength"]()}</Label>
|
||||
<select class={selectClass} bind:value={active.strength_level}>
|
||||
<option value="">—</option>
|
||||
<option value="1">1 Low</option>
|
||||
<option value="2">2 Medium</option>
|
||||
<option value="3">3 High</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">Irritation</Label>
|
||||
<Label class="text-xs">{m["productForm_activeIrritation"]()}</Label>
|
||||
<select class={selectClass} bind:value={active.irritation_potential}>
|
||||
<option value="">—</option>
|
||||
<option value="1">1 Low</option>
|
||||
<option value="2">2 Medium</option>
|
||||
<option value="3">3 High</option>
|
||||
<option value="1">{m["productForm_strengthLow"]()}</option>
|
||||
<option value="2">{m["productForm_strengthMedium"]()}</option>
|
||||
<option value="3">{m["productForm_strengthHigh"]()}</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button
|
||||
|
|
@ -602,7 +697,7 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<Label class="text-xs text-muted-foreground">Functions</Label>
|
||||
<Label class="text-xs text-muted-foreground">{m["productForm_activeFunctions"]()}</Label>
|
||||
<div class="grid grid-cols-4 gap-1">
|
||||
{#each ingFunctions as fn}
|
||||
<label class="flex cursor-pointer items-center gap-1.5 text-xs">
|
||||
|
|
@ -612,7 +707,7 @@
|
|||
onchange={() => toggleFn(i, fn)}
|
||||
class="rounded border-input"
|
||||
/>
|
||||
{lbl(fn)}
|
||||
{ingFunctionLabels[fn]}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -621,7 +716,7 @@
|
|||
{/each}
|
||||
|
||||
{#if actives.length === 0}
|
||||
<p class="text-sm text-muted-foreground">No actives added yet.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["productForm_noActives"]()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -629,7 +724,7 @@
|
|||
|
||||
<!-- ── Effect profile ─────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Effect profile (0–5)</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_effectProfile"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{#each effectFields as field}
|
||||
|
|
@ -654,10 +749,10 @@
|
|||
|
||||
<!-- ── Interactions ───────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Interactions</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_interactions"]()}</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="synergizes_with">Synergizes with (one per line)</Label>
|
||||
<Label for="synergizes_with">{m["productForm_synergizesWith"]()}</Label>
|
||||
<textarea
|
||||
id="synergizes_with"
|
||||
name="synergizes_with"
|
||||
|
|
@ -670,9 +765,9 @@
|
|||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label>Incompatible with</Label>
|
||||
<Label>{m["productForm_incompatibleWith"]()}</Label>
|
||||
<Button type="button" variant="outline" size="sm" onclick={addIncompatible}>
|
||||
+ Add incompatibility
|
||||
{m["productForm_addIncompatibility"]()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -681,21 +776,21 @@
|
|||
{#each incompatibleWith as row, i}
|
||||
<div class="grid grid-cols-[1fr_140px_1fr_auto] gap-2 items-end">
|
||||
<div class="space-y-1">
|
||||
<Label class="text-xs">Target ingredient</Label>
|
||||
<Label class="text-xs">{m["productForm_incompTarget"]()}</Label>
|
||||
<Input placeholder="e.g. Vitamin C" bind:value={row.target} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label class="text-xs">Scope</Label>
|
||||
<Label class="text-xs">{m["productForm_incompScope"]()}</Label>
|
||||
<select class={selectClass} bind:value={row.scope}>
|
||||
<option value="">Select…</option>
|
||||
<option value="">{m["productForm_incompScopeSelect"]()}</option>
|
||||
{#each interactionScopes as s}
|
||||
<option value={s}>{lbl(s)}</option>
|
||||
<option value={s}>{scopeLabels[s]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label class="text-xs">Reason (optional)</Label>
|
||||
<Input placeholder="e.g. reduces efficacy" bind:value={row.reason} />
|
||||
<Label class="text-xs">{m["productForm_incompReason"]()}</Label>
|
||||
<Input placeholder={m["productForm_incompReasonPlaceholder"]()} bind:value={row.reason} />
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -708,7 +803,7 @@
|
|||
{/each}
|
||||
|
||||
{#if incompatibleWith.length === 0}
|
||||
<p class="text-sm text-muted-foreground">No incompatibilities added.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["productForm_noIncompatibilities"]()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -716,16 +811,14 @@
|
|||
|
||||
<!-- ── Context rules ──────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Context rules</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_contextRules"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Safe after shaving</Label>
|
||||
<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>
|
||||
{ctxAfterShaving === '' ? 'Unknown' : ctxAfterShaving === 'true' ? 'Yes' : 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(ctxAfterShaving)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
|
|
@ -733,12 +826,10 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Safe after acids</Label>
|
||||
<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>
|
||||
{ctxAfterAcids === '' ? 'Unknown' : ctxAfterAcids === 'true' ? 'Yes' : 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(ctxAfterAcids)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
|
|
@ -746,16 +837,14 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Safe after retinoids</Label>
|
||||
<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>
|
||||
{ctxAfterRetinoids === '' ? 'Unknown' : ctxAfterRetinoids === 'true' ? 'Yes' : 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(ctxAfterRetinoids)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
|
|
@ -763,20 +852,14 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Safe with compromised barrier</Label>
|
||||
<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>
|
||||
{ctxCompromisedBarrier === ''
|
||||
? 'Unknown'
|
||||
: ctxCompromisedBarrier === 'true'
|
||||
? 'Yes'
|
||||
: 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(ctxCompromisedBarrier)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
|
|
@ -784,12 +867,10 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Low UV only (evening/covered)</Label>
|
||||
<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>
|
||||
{ctxLowUvOnly === '' ? 'Unknown' : ctxLowUvOnly === 'true' ? 'Yes' : 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(ctxLowUvOnly)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
|
|
@ -801,61 +882,61 @@
|
|||
|
||||
<!-- ── Product details ────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Product details</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_productDetails"]()}</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Price tier</Label>
|
||||
<Label>{m["productForm_priceTier"]()}</Label>
|
||||
<input type="hidden" name="price_tier" value={priceTier} />
|
||||
<Select type="single" value={priceTier} onValueChange={(v) => (priceTier = v)}>
|
||||
<SelectTrigger>{priceTier ? lbl(priceTier) : 'Select tier'}</SelectTrigger>
|
||||
<SelectTrigger>{priceTier ? priceTierLabels[priceTier] : m["productForm_selectTier"]()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each priceTiers as p}
|
||||
<SelectItem value={p}>{lbl(p)}</SelectItem>
|
||||
<SelectItem value={p}>{priceTierLabels[p]}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="size_ml">Size (ml)</Label>
|
||||
<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">Full weight (g)</Label>
|
||||
<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">Empty weight (g)</Label>
|
||||
<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">PAO (months)</Label>
|
||||
<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-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="ph_min">pH min</Label>
|
||||
<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">pH max</Label>
|
||||
<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">Usage notes</Label>
|
||||
<Label for="usage_notes">{m["productForm_usageNotes"]()}</Label>
|
||||
<textarea
|
||||
id="usage_notes"
|
||||
name="usage_notes"
|
||||
rows="2"
|
||||
placeholder="e.g. Apply to damp skin, avoid eye area"
|
||||
placeholder={m["productForm_usageNotesPlaceholder"]()}
|
||||
class={textareaClass}
|
||||
bind:value={usageNotes}
|
||||
></textarea>
|
||||
|
|
@ -865,16 +946,14 @@
|
|||
|
||||
<!-- ── Safety flags ───────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Safety flags</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_safetyFlags"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Fragrance-free</Label>
|
||||
<Label>{m["productForm_fragranceFree"]()}</Label>
|
||||
<input type="hidden" name="fragrance_free" value={fragranceFree} />
|
||||
<Select type="single" value={fragranceFree} onValueChange={(v) => (fragranceFree = v)}>
|
||||
<SelectTrigger>
|
||||
{fragranceFree === '' ? 'Unknown' : fragranceFree === 'true' ? 'Yes' : 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(fragranceFree)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
|
|
@ -882,16 +961,14 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Essential oils-free</Label>
|
||||
<Label>{m["productForm_essentialOilsFree"]()}</Label>
|
||||
<input type="hidden" name="essential_oils_free" value={essentialOilsFree} />
|
||||
<Select
|
||||
type="single"
|
||||
value={essentialOilsFree}
|
||||
onValueChange={(v) => (essentialOilsFree = v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
{essentialOilsFree === '' ? 'Unknown' : essentialOilsFree === 'true' ? 'Yes' : 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(essentialOilsFree)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
|
|
@ -899,16 +976,14 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Alcohol denat-free</Label>
|
||||
<Label>{m["productForm_alcoholDenatFree"]()}</Label>
|
||||
<input type="hidden" name="alcohol_denat_free" value={alcoholDenatFree} />
|
||||
<Select
|
||||
type="single"
|
||||
value={alcoholDenatFree}
|
||||
onValueChange={(v) => (alcoholDenatFree = v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
{alcoholDenatFree === '' ? 'Unknown' : alcoholDenatFree === 'true' ? 'Yes' : 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(alcoholDenatFree)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
|
|
@ -916,12 +991,10 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>Pregnancy safe</Label>
|
||||
<Label>{m["productForm_pregnancySafe"]()}</Label>
|
||||
<input type="hidden" name="pregnancy_safe" value={pregnancySafe} />
|
||||
<Select type="single" value={pregnancySafe} onValueChange={(v) => (pregnancySafe = v)}>
|
||||
<SelectTrigger>
|
||||
{pregnancySafe === '' ? 'Unknown' : pregnancySafe === 'true' ? 'Yes' : 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(pregnancySafe)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}<SelectItem value={opt.value}>{opt.label}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
|
|
@ -933,15 +1006,15 @@
|
|||
|
||||
<!-- ── Usage constraints ──────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Usage constraints</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_usageConstraints"]()}</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="min_interval_hours">Min interval (hours)</Label>
|
||||
<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">Max uses per week</Label>
|
||||
<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>
|
||||
|
|
@ -955,7 +1028,7 @@
|
|||
onchange={() => (isMedication = !isMedication)}
|
||||
class="rounded border-input"
|
||||
/>
|
||||
Is medication
|
||||
{m["productForm_isMedication"]()}
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input type="hidden" name="is_tool" value={String(isTool)} />
|
||||
|
|
@ -965,12 +1038,12 @@
|
|||
onchange={() => (isTool = !isTool)}
|
||||
class="rounded border-input"
|
||||
/>
|
||||
Is tool (e.g. dermaroller)
|
||||
{m["productForm_isTool"]()}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="needle_length_mm">Needle length (mm, tools only)</Label>
|
||||
<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>
|
||||
|
|
@ -978,23 +1051,17 @@
|
|||
|
||||
<!-- ── Personal notes ─────────────────────────────────────────────────────── -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Personal notes</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["productForm_personalNotes"]()}</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label>Repurchase intent</Label>
|
||||
<Label>{m["productForm_repurchaseIntent"]()}</Label>
|
||||
<input type="hidden" name="personal_repurchase_intent" value={personalRepurchaseIntent} />
|
||||
<Select
|
||||
type="single"
|
||||
value={personalRepurchaseIntent}
|
||||
onValueChange={(v) => (personalRepurchaseIntent = v)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
{personalRepurchaseIntent === ''
|
||||
? 'Unknown'
|
||||
: personalRepurchaseIntent === 'true'
|
||||
? 'Yes'
|
||||
: 'No'}
|
||||
</SelectTrigger>
|
||||
<SelectTrigger>{tristateLabel(personalRepurchaseIntent)}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each tristate as opt}
|
||||
<SelectItem value={opt.value}>{opt.label}</SelectItem>
|
||||
|
|
@ -1004,12 +1071,12 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="personal_tolerance_notes">Tolerance notes</Label>
|
||||
<Label for="personal_tolerance_notes">{m["productForm_toleranceNotes"]()}</Label>
|
||||
<textarea
|
||||
id="personal_tolerance_notes"
|
||||
name="personal_tolerance_notes"
|
||||
rows="2"
|
||||
placeholder="e.g. Causes mild stinging, fine after 2 weeks"
|
||||
placeholder={m["productForm_toleranceNotesPlaceholder"]()}
|
||||
class={textareaClass}
|
||||
>{product?.personal_tolerance_notes ?? ''}</textarea>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue