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
437
frontend/messages/en.json
Normal file
437
frontend/messages/en.json
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
{
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_products": "Products",
|
||||
"nav_routines": "Routines",
|
||||
"nav_grooming": "Grooming",
|
||||
"nav_medications": "Medications",
|
||||
"nav_labResults": "Lab Results",
|
||||
"nav_skin": "Skin",
|
||||
"nav_appName": "innercontext",
|
||||
"nav_appSubtitle": "personal health & skincare",
|
||||
|
||||
"common_save": "Save",
|
||||
"common_cancel": "Cancel",
|
||||
"common_add": "Add",
|
||||
"common_edit": "Edit",
|
||||
"common_delete": "Delete",
|
||||
"common_saved": "Saved.",
|
||||
"common_select": "Select",
|
||||
"common_unknown": "Unknown",
|
||||
"common_yes": "Yes",
|
||||
"common_no": "No",
|
||||
"common_unknown_value": "Unknown",
|
||||
"common_optional_notes": "optional",
|
||||
"common_steps": "steps",
|
||||
|
||||
"dashboard_title": "Dashboard",
|
||||
"dashboard_subtitle": "Your recent health & skincare overview",
|
||||
"dashboard_latestSnapshot": "Latest Skin Snapshot",
|
||||
"dashboard_recentRoutines": "Recent Routines",
|
||||
"dashboard_noSnapshots": "No skin snapshots yet.",
|
||||
"dashboard_noRoutines": "No routines in the past 2 weeks.",
|
||||
|
||||
"products_title": "Products",
|
||||
"products_count": "{count} products",
|
||||
"products_addNew": "+ Add product",
|
||||
"products_noProducts": "No products found.",
|
||||
"products_filterAll": "All",
|
||||
"products_filterOwned": "Owned",
|
||||
"products_filterUnowned": "Not owned",
|
||||
"products_colName": "Name",
|
||||
"products_colBrand": "Brand",
|
||||
"products_colTargets": "Targets",
|
||||
"products_colTime": "Time",
|
||||
"products_newTitle": "New Product",
|
||||
"products_backToList": "← Products",
|
||||
"products_createProduct": "Create product",
|
||||
"products_saveChanges": "Save changes",
|
||||
"products_deleteProduct": "Delete product",
|
||||
"products_confirmDelete": "Delete this product?",
|
||||
"products_noInventory": "No inventory packages.",
|
||||
|
||||
"inventory_title": "Inventory packages ({count})",
|
||||
"inventory_addPackage": "+ Add package",
|
||||
"inventory_packageAdded": "Package added.",
|
||||
"inventory_packageUpdated": "Package updated.",
|
||||
"inventory_packageDeleted": "Package deleted.",
|
||||
"inventory_alreadyOpened": "Already opened",
|
||||
"inventory_openedDate": "Opened date",
|
||||
"inventory_finishedDate": "Finished date",
|
||||
"inventory_expiryDate": "Expiry date",
|
||||
"inventory_currentWeight": "Current weight (g)",
|
||||
"inventory_lastWeighed": "Last weighed",
|
||||
"inventory_notes": "Notes",
|
||||
"inventory_badgeOpen": "Open",
|
||||
"inventory_badgeSealed": "Sealed",
|
||||
"inventory_badgeFinished": "Finished",
|
||||
"inventory_exp": "Exp:",
|
||||
"inventory_opened": "Opened:",
|
||||
"inventory_finished": "Finished:",
|
||||
"inventory_remaining": "g remaining",
|
||||
"inventory_weighed": "Weighed:",
|
||||
"inventory_confirmDelete": "Delete this package?",
|
||||
|
||||
"routines_title": "Routines",
|
||||
"routines_count": "{count} routines (last 30 days)",
|
||||
"routines_suggestAI": "Suggest AI routine",
|
||||
"routines_addNew": "+ New routine",
|
||||
"routines_noRoutines": "No routines found.",
|
||||
"routines_newTitle": "New Routine",
|
||||
"routines_backToList": "← Routines",
|
||||
"routines_detailsTitle": "Routine details",
|
||||
"routines_date": "Date *",
|
||||
"routines_amOrPm": "AM or PM *",
|
||||
"routines_notes": "Notes",
|
||||
"routines_notesPlaceholder": "Optional notes",
|
||||
"routines_createRoutine": "Create routine",
|
||||
"routines_deleteRoutine": "Delete routine",
|
||||
"routines_confirmDelete": "Delete this routine?",
|
||||
"routines_steps": "Steps ({count})",
|
||||
"routines_addStep": "+ Add step",
|
||||
"routines_addStepTitle": "Add step",
|
||||
"routines_product": "Product",
|
||||
"routines_selectProduct": "Select product",
|
||||
"routines_dose": "Dose",
|
||||
"routines_dosePlaceholder": "e.g. 2 pumps",
|
||||
"routines_region": "Region",
|
||||
"routines_regionPlaceholder": "e.g. face",
|
||||
"routines_addStepBtn": "Add step",
|
||||
"routines_unknownStep": "Unknown step",
|
||||
"routines_noSteps": "No steps yet.",
|
||||
|
||||
"grooming_title": "Grooming Schedule",
|
||||
"grooming_backToRoutines": "← Routines",
|
||||
"grooming_addEntry": "+ Add entry",
|
||||
"grooming_entryAdded": "Entry added.",
|
||||
"grooming_entryUpdated": "Entry updated.",
|
||||
"grooming_entryDeleted": "Entry deleted.",
|
||||
"grooming_dayOfWeek": "Day of week",
|
||||
"grooming_action": "Action",
|
||||
"grooming_notesOptional": "Notes (optional)",
|
||||
"grooming_notesPlaceholder": "e.g. every 2 weeks",
|
||||
"grooming_noEntries": "No entries yet. Click \"+ Add entry\" to get started.",
|
||||
"grooming_confirmDelete": "Delete this entry?",
|
||||
"grooming_actionShavingRazor": "Razor shaving",
|
||||
"grooming_actionShavingOneblade": "OneBlade shaving",
|
||||
"grooming_actionDermarolling": "Dermarolling",
|
||||
"grooming_dayMonday": "Monday",
|
||||
"grooming_dayTuesday": "Tuesday",
|
||||
"grooming_dayWednesday": "Wednesday",
|
||||
"grooming_dayThursday": "Thursday",
|
||||
"grooming_dayFriday": "Friday",
|
||||
"grooming_daySaturday": "Saturday",
|
||||
"grooming_daySunday": "Sunday",
|
||||
|
||||
"suggest_title": "AI Routine Suggestion",
|
||||
"suggest_backToRoutines": "← Routines",
|
||||
"suggest_singleTab": "Single routine",
|
||||
"suggest_batchTab": "Batch / Vacation",
|
||||
"suggest_singleParams": "Parameters",
|
||||
"suggest_date": "Date",
|
||||
"suggest_timeOfDay": "Time of day",
|
||||
"suggest_contextLabel": "Additional context for AI",
|
||||
"suggest_contextOptional": "(optional)",
|
||||
"suggest_contextPlaceholder": "e.g. party night, focusing on hydration...",
|
||||
"suggest_generateBtn": "Generate suggestion",
|
||||
"suggest_generating": "Generating…",
|
||||
"suggest_proposalTitle": "Suggestion",
|
||||
"suggest_saveRoutine": "Save routine",
|
||||
"suggest_saving": "Saving…",
|
||||
"suggest_regenerate": "Regenerate",
|
||||
"suggest_batchRange": "Date range",
|
||||
"suggest_fromDate": "From",
|
||||
"suggest_toDate": "To (max 14 days)",
|
||||
"suggest_batchContextLabel": "Context / trip purpose",
|
||||
"suggest_batchContextPlaceholder": "e.g. sunny trip to Italy, active mountain vacation...",
|
||||
"suggest_generatePlan": "Generate plan",
|
||||
"suggest_generatingPlan": "Generating plan…",
|
||||
"suggest_planTitle": "Plan ({count} days)",
|
||||
"suggest_saveAllRoutines": "Save all routines",
|
||||
"suggest_amSteps": "steps",
|
||||
"suggest_pmSteps": "steps",
|
||||
"suggest_noAmSteps": "No AM steps.",
|
||||
"suggest_noPmSteps": "No PM steps.",
|
||||
"suggest_errorDefault": "Error generating suggestion.",
|
||||
"suggest_errorBatch": "Error generating plan.",
|
||||
"suggest_errorSave": "Error saving.",
|
||||
"suggest_amMorning": "AM (morning)",
|
||||
"suggest_pmEvening": "PM (evening)",
|
||||
|
||||
"medications_title": "Medications",
|
||||
"medications_count": "{count} entries",
|
||||
"medications_addNew": "+ Add medication",
|
||||
"medications_newTitle": "New medication",
|
||||
"medications_kind": "Kind",
|
||||
"medications_productName": "Product name *",
|
||||
"medications_productNamePlaceholder": "e.g. Vitamin D3",
|
||||
"medications_activeSubstance": "Active substance",
|
||||
"medications_activeSubstancePlaceholder": "e.g. cholecalciferol",
|
||||
"medications_notes": "Notes",
|
||||
"medications_added": "Medication added.",
|
||||
"medications_usages": "{count} usages",
|
||||
"medications_noMedications": "No medications recorded.",
|
||||
"medications_kindPrescription": "Prescription",
|
||||
"medications_kindOtc": "OTC",
|
||||
"medications_kindSupplement": "Supplement",
|
||||
"medications_kindHerbal": "Herbal",
|
||||
"medications_kindOther": "Other",
|
||||
|
||||
"labResults_title": "Lab Results",
|
||||
"labResults_count": "{count} results",
|
||||
"labResults_addNew": "+ Add result",
|
||||
"labResults_newTitle": "New lab result",
|
||||
"labResults_flagFilter": "Flag:",
|
||||
"labResults_flagAll": "All",
|
||||
"labResults_flagNone": "None",
|
||||
"labResults_date": "Date *",
|
||||
"labResults_loincCode": "LOINC code *",
|
||||
"labResults_testName": "Test name",
|
||||
"labResults_testNamePlaceholder": "e.g. Hemoglobin",
|
||||
"labResults_lab": "Lab",
|
||||
"labResults_labPlaceholder": "e.g. LabCorp",
|
||||
"labResults_value": "Value",
|
||||
"labResults_unit": "Unit",
|
||||
"labResults_unitPlaceholder": "e.g. g/dL",
|
||||
"labResults_flag": "Flag",
|
||||
"labResults_added": "Result added.",
|
||||
"labResults_colDate": "Date",
|
||||
"labResults_colTest": "Test",
|
||||
"labResults_colLoinc": "LOINC",
|
||||
"labResults_colValue": "Value",
|
||||
"labResults_colFlag": "Flag",
|
||||
"labResults_colLab": "Lab",
|
||||
"labResults_noResults": "No lab results found.",
|
||||
|
||||
"skin_title": "Skin Snapshots",
|
||||
"skin_count": "{count} snapshots",
|
||||
"skin_addNew": "+ Add snapshot",
|
||||
"skin_aiAnalysisTitle": "AI analysis from photos",
|
||||
"skin_aiUploadText": "Upload 1–3 photos of your skin. AI will pre-fill the form fields below.",
|
||||
"skin_analyzePhotos": "Analyze photos",
|
||||
"skin_analyzing": "Analyzing…",
|
||||
"skin_newSnapshotTitle": "New skin snapshot",
|
||||
"skin_date": "Date *",
|
||||
"skin_overallState": "Overall state",
|
||||
"skin_texture": "Texture",
|
||||
"skin_skinType": "Skin type",
|
||||
"skin_barrierState": "Barrier state",
|
||||
"skin_hydration": "Hydration (1–5)",
|
||||
"skin_sensitivity": "Sensitivity (1–5)",
|
||||
"skin_sebumTzone": "Sebum T-zone (1–5)",
|
||||
"skin_sebumCheeks": "Sebum cheeks (1–5)",
|
||||
"skin_activeConcerns": "Active concerns (comma-separated)",
|
||||
"skin_activeConcernsPlaceholder": "acne, redness, dehydration",
|
||||
"skin_notes": "Notes",
|
||||
"skin_addSnapshot": "Add snapshot",
|
||||
"skin_snapshotAdded": "Snapshot added.",
|
||||
"skin_snapshotUpdated": "Snapshot updated.",
|
||||
"skin_snapshotDeleted": "Snapshot deleted.",
|
||||
"skin_noSnapshots": "No skin snapshots yet.",
|
||||
"skin_hydrationLabel": "Hydration",
|
||||
"skin_sensitivityLabel": "Sensitivity",
|
||||
"skin_barrierLabel": "Barrier",
|
||||
"skin_stateExcellent": "excellent",
|
||||
"skin_stateGood": "good",
|
||||
"skin_stateFair": "fair",
|
||||
"skin_statePoor": "poor",
|
||||
"skin_textureSmooth": "smooth",
|
||||
"skin_textureRough": "rough",
|
||||
"skin_textureFlaky": "flaky",
|
||||
"skin_textureBumpy": "bumpy",
|
||||
"skin_barrierIntact": "intact",
|
||||
"skin_barrierMildly": "mildly compromised",
|
||||
"skin_barrierCompromised": "compromised",
|
||||
"skin_typeDry": "dry",
|
||||
"skin_typeOily": "oily",
|
||||
"skin_typeCombination": "combination",
|
||||
"skin_typeSensitive": "sensitive",
|
||||
"skin_typeNormal": "normal",
|
||||
"skin_typeAcneProne": "acne prone",
|
||||
|
||||
"productForm_aiPrefill": "AI pre-fill",
|
||||
"productForm_aiPrefillText": "Paste product description from a website, ingredient list, or other text. AI will fill in available fields — you can review and correct before saving.",
|
||||
"productForm_pasteText": "Paste product description, INCI ingredients here...",
|
||||
"productForm_parseWithAI": "Fill fields (AI)",
|
||||
"productForm_parsing": "Processing…",
|
||||
"productForm_basicInfo": "Basic info",
|
||||
"productForm_name": "Name *",
|
||||
"productForm_namePlaceholder": "e.g. Hydro Boost Water Gel",
|
||||
"productForm_brand": "Brand *",
|
||||
"productForm_brandPlaceholder": "e.g. Neutrogena",
|
||||
"productForm_lineName": "Line / series",
|
||||
"productForm_lineNamePlaceholder": "e.g. Hydro Boost",
|
||||
"productForm_url": "URL",
|
||||
"productForm_sku": "SKU",
|
||||
"productForm_skuPlaceholder": "e.g. NTR-HB-50",
|
||||
"productForm_barcode": "Barcode / EAN",
|
||||
"productForm_barcodePlaceholder": "e.g. 3614273258975",
|
||||
"productForm_classification": "Classification",
|
||||
"productForm_category": "Category *",
|
||||
"productForm_selectCategory": "Select category",
|
||||
"productForm_time": "Time *",
|
||||
"productForm_timeOptions": "AM / PM / Both",
|
||||
"productForm_timeBoth": "Both",
|
||||
"productForm_leaveOn": "Leave-on *",
|
||||
"productForm_leaveOnYes": "Yes (leave-on)",
|
||||
"productForm_leaveOnNo": "No (rinse-off)",
|
||||
"productForm_texture": "Texture",
|
||||
"productForm_selectTexture": "Select texture",
|
||||
"productForm_absorptionSpeed": "Absorption speed",
|
||||
"productForm_selectSpeed": "Select speed",
|
||||
"productForm_skinProfile": "Skin profile",
|
||||
"productForm_recommendedFor": "Recommended for skin types",
|
||||
"productForm_targetConcerns": "Target concerns",
|
||||
"productForm_contraindications": "Contraindications (one per line)",
|
||||
"productForm_contraindicationsPlaceholder": "e.g. active rosacea flares",
|
||||
"productForm_ingredients": "Ingredients",
|
||||
"productForm_inciList": "INCI list (one ingredient per line)",
|
||||
"productForm_activeIngredients": "Active ingredients",
|
||||
"productForm_addActive": "+ Add active",
|
||||
"productForm_noActives": "No actives added yet.",
|
||||
"productForm_activeName": "Name",
|
||||
"productForm_activePercent": "%",
|
||||
"productForm_activeStrength": "Strength",
|
||||
"productForm_activeIrritation": "Irritation",
|
||||
"productForm_activeFunctions": "Functions",
|
||||
"productForm_effectProfile": "Effect profile (0–5)",
|
||||
"productForm_interactions": "Interactions",
|
||||
"productForm_synergizesWith": "Synergizes with (one per line)",
|
||||
"productForm_incompatibleWith": "Incompatible with",
|
||||
"productForm_addIncompatibility": "+ Add incompatibility",
|
||||
"productForm_noIncompatibilities": "No incompatibilities added.",
|
||||
"productForm_incompTarget": "Target ingredient",
|
||||
"productForm_incompScope": "Scope",
|
||||
"productForm_incompReason": "Reason (optional)",
|
||||
"productForm_incompReasonPlaceholder": "e.g. reduces efficacy",
|
||||
"productForm_incompScopeSelect": "Select…",
|
||||
"productForm_contextRules": "Context rules",
|
||||
"productForm_ctxAfterShaving": "Safe after shaving",
|
||||
"productForm_ctxAfterAcids": "Safe after acids",
|
||||
"productForm_ctxAfterRetinoids": "Safe after retinoids",
|
||||
"productForm_ctxCompromisedBarrier": "Safe with compromised barrier",
|
||||
"productForm_ctxLowUvOnly": "Low UV only (evening/covered)",
|
||||
"productForm_productDetails": "Product details",
|
||||
"productForm_priceTier": "Price tier",
|
||||
"productForm_selectTier": "Select tier",
|
||||
"productForm_sizeMl": "Size (ml)",
|
||||
"productForm_fullWeightG": "Full weight (g)",
|
||||
"productForm_emptyWeightG": "Empty weight (g)",
|
||||
"productForm_paoMonths": "PAO (months)",
|
||||
"productForm_phMin": "pH min",
|
||||
"productForm_phMax": "pH max",
|
||||
"productForm_usageNotes": "Usage notes",
|
||||
"productForm_usageNotesPlaceholder": "e.g. Apply to damp skin, avoid eye area",
|
||||
"productForm_safetyFlags": "Safety flags",
|
||||
"productForm_fragranceFree": "Fragrance-free",
|
||||
"productForm_essentialOilsFree": "Essential oils-free",
|
||||
"productForm_alcoholDenatFree": "Alcohol denat-free",
|
||||
"productForm_pregnancySafe": "Pregnancy safe",
|
||||
"productForm_usageConstraints": "Usage constraints",
|
||||
"productForm_minIntervalHours": "Min interval (hours)",
|
||||
"productForm_maxFrequencyPerWeek": "Max uses per week",
|
||||
"productForm_isMedication": "Is medication",
|
||||
"productForm_isTool": "Is tool (e.g. dermaroller)",
|
||||
"productForm_needleLengthMm": "Needle length (mm, tools only)",
|
||||
"productForm_personalNotes": "Personal notes",
|
||||
"productForm_repurchaseIntent": "Repurchase intent",
|
||||
"productForm_toleranceNotes": "Tolerance notes",
|
||||
"productForm_toleranceNotesPlaceholder": "e.g. Causes mild stinging, fine after 2 weeks",
|
||||
|
||||
"productForm_categoryCleanser": "Cleanser",
|
||||
"productForm_categoryToner": "Toner",
|
||||
"productForm_categoryEssence": "Essence",
|
||||
"productForm_categorySerum": "Serum",
|
||||
"productForm_categoryMoisturizer": "Moisturizer",
|
||||
"productForm_categorySpf": "SPF",
|
||||
"productForm_categoryMask": "Mask",
|
||||
"productForm_categoryExfoliant": "Exfoliant",
|
||||
"productForm_categoryHairTreatment": "Hair treatment",
|
||||
"productForm_categoryTool": "Tool",
|
||||
"productForm_categorySpotTreatment": "Spot treatment",
|
||||
"productForm_categoryOil": "Oil",
|
||||
|
||||
"productForm_textureWatery": "Watery",
|
||||
"productForm_textureGel": "Gel",
|
||||
"productForm_textureEmulsion": "Emulsion",
|
||||
"productForm_textureCream": "Cream",
|
||||
"productForm_textureOil": "Oil",
|
||||
"productForm_textureBalm": "Balm",
|
||||
"productForm_textureFoam": "Foam",
|
||||
"productForm_textureFluid": "Fluid",
|
||||
|
||||
"productForm_absorptionVeryFast": "Very fast",
|
||||
"productForm_absorptionFast": "Fast",
|
||||
"productForm_absorptionModerate": "Moderate",
|
||||
"productForm_absorptionSlow": "Slow",
|
||||
"productForm_absorptionVerySlow": "Very slow",
|
||||
|
||||
"productForm_priceBudget": "Budget",
|
||||
"productForm_priceMid": "Mid",
|
||||
"productForm_pricePremium": "Premium",
|
||||
"productForm_priceLuxury": "Luxury",
|
||||
|
||||
"productForm_skinTypeDry": "dry",
|
||||
"productForm_skinTypeOily": "oily",
|
||||
"productForm_skinTypeCombination": "combination",
|
||||
"productForm_skinTypeSensitive": "sensitive",
|
||||
"productForm_skinTypeNormal": "normal",
|
||||
"productForm_skinTypeAcneProne": "acne prone",
|
||||
|
||||
"productForm_concernAcne": "acne",
|
||||
"productForm_concernRosacea": "rosacea",
|
||||
"productForm_concernHyperpigmentation": "hyperpigmentation",
|
||||
"productForm_concernAging": "aging",
|
||||
"productForm_concernDehydration": "dehydration",
|
||||
"productForm_concernRedness": "redness",
|
||||
"productForm_concernDamagedBarrier": "damaged barrier",
|
||||
"productForm_concernPoreVisibility": "pore visibility",
|
||||
"productForm_concernUnevenTexture": "uneven texture",
|
||||
"productForm_concernHairGrowth": "hair growth",
|
||||
"productForm_concernSebumExcess": "sebum excess",
|
||||
|
||||
"productForm_fnHumectant": "humectant",
|
||||
"productForm_fnEmollient": "emollient",
|
||||
"productForm_fnOcclusive": "occlusive",
|
||||
"productForm_fnExfoliantAha": "AHA exfoliant",
|
||||
"productForm_fnExfoliantBha": "BHA exfoliant",
|
||||
"productForm_fnExfoliantPha": "PHA exfoliant",
|
||||
"productForm_fnRetinoid": "retinoid",
|
||||
"productForm_fnAntioxidant": "antioxidant",
|
||||
"productForm_fnSoothing": "soothing",
|
||||
"productForm_fnBarrierSupport": "barrier support",
|
||||
"productForm_fnBrightening": "brightening",
|
||||
"productForm_fnAntiAcne": "anti-acne",
|
||||
"productForm_fnCeramide": "ceramide",
|
||||
"productForm_fnNiacinamide": "niacinamide",
|
||||
"productForm_fnSunscreen": "sunscreen",
|
||||
"productForm_fnPeptide": "peptide",
|
||||
"productForm_fnHairGrowth": "hair growth stimulant",
|
||||
"productForm_fnPrebiotic": "prebiotic",
|
||||
"productForm_fnVitaminC": "vitamin C",
|
||||
"productForm_fnAntiAging": "anti-aging",
|
||||
|
||||
"productForm_scopeSameStep": "same step",
|
||||
"productForm_scopeSameDay": "same day",
|
||||
"productForm_scopeSamePeriod": "same period",
|
||||
|
||||
"productForm_strengthLow": "1 Low",
|
||||
"productForm_strengthMedium": "2 Medium",
|
||||
"productForm_strengthHigh": "3 High",
|
||||
|
||||
"productForm_effectHydrationImmediate": "Hydration (immediate)",
|
||||
"productForm_effectHydrationLongTerm": "Hydration (long term)",
|
||||
"productForm_effectBarrierRepair": "Barrier repair",
|
||||
"productForm_effectSoothing": "Soothing",
|
||||
"productForm_effectExfoliation": "Exfoliation",
|
||||
"productForm_effectRetinoid": "Retinoid activity",
|
||||
"productForm_effectIrritation": "Irritation risk",
|
||||
"productForm_effectComedogenic": "Comedogenic risk",
|
||||
"productForm_effectBarrierDisruption": "Barrier disruption risk",
|
||||
"productForm_effectDryness": "Dryness risk",
|
||||
"productForm_effectBrightening": "Brightening",
|
||||
"productForm_effectAntiAcne": "Anti-acne",
|
||||
"productForm_effectAntiAging": "Anti-aging",
|
||||
|
||||
"lang_pl": "PL",
|
||||
"lang_en": "EN"
|
||||
}
|
||||
437
frontend/messages/pl.json
Normal file
437
frontend/messages/pl.json
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
{
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_products": "Produkty",
|
||||
"nav_routines": "Rutyny",
|
||||
"nav_grooming": "Pielęgnacja",
|
||||
"nav_medications": "Leki",
|
||||
"nav_labResults": "Wyniki badań",
|
||||
"nav_skin": "Skóra",
|
||||
"nav_appName": "innercontext",
|
||||
"nav_appSubtitle": "zdrowie & pielęgnacja",
|
||||
|
||||
"common_save": "Zapisz",
|
||||
"common_cancel": "Anuluj",
|
||||
"common_add": "Dodaj",
|
||||
"common_edit": "Edytuj",
|
||||
"common_delete": "Usuń",
|
||||
"common_saved": "Zapisano.",
|
||||
"common_select": "Wybierz",
|
||||
"common_unknown": "Nieznane",
|
||||
"common_yes": "Tak",
|
||||
"common_no": "Nie",
|
||||
"common_unknown_value": "Nieznane",
|
||||
"common_optional_notes": "opcjonalnie",
|
||||
"common_steps": "kroków",
|
||||
|
||||
"dashboard_title": "Dashboard",
|
||||
"dashboard_subtitle": "Przegląd zdrowia i pielęgnacji",
|
||||
"dashboard_latestSnapshot": "Ostatni stan skóry",
|
||||
"dashboard_recentRoutines": "Ostatnie rutyny",
|
||||
"dashboard_noSnapshots": "Brak wpisów o stanie skóry.",
|
||||
"dashboard_noRoutines": "Brak rutyno w ciągu ostatnich 2 tygodni.",
|
||||
|
||||
"products_title": "Produkty",
|
||||
"products_count": "{count} produktów",
|
||||
"products_addNew": "+ Dodaj produkt",
|
||||
"products_noProducts": "Nie znaleziono produktów.",
|
||||
"products_filterAll": "Wszystkie",
|
||||
"products_filterOwned": "Posiadane",
|
||||
"products_filterUnowned": "Nieposiadane",
|
||||
"products_colName": "Nazwa",
|
||||
"products_colBrand": "Marka",
|
||||
"products_colTargets": "Cele",
|
||||
"products_colTime": "Pora",
|
||||
"products_newTitle": "Nowy produkt",
|
||||
"products_backToList": "← Produkty",
|
||||
"products_createProduct": "Utwórz produkt",
|
||||
"products_saveChanges": "Zapisz zmiany",
|
||||
"products_deleteProduct": "Usuń produkt",
|
||||
"products_confirmDelete": "Usunąć ten produkt?",
|
||||
"products_noInventory": "Brak opakowań w magazynie.",
|
||||
|
||||
"inventory_title": "Opakowania ({count})",
|
||||
"inventory_addPackage": "+ Dodaj opakowanie",
|
||||
"inventory_packageAdded": "Opakowanie dodane.",
|
||||
"inventory_packageUpdated": "Opakowanie zaktualizowane.",
|
||||
"inventory_packageDeleted": "Opakowanie usunięte.",
|
||||
"inventory_alreadyOpened": "Już otwarte",
|
||||
"inventory_openedDate": "Data otwarcia",
|
||||
"inventory_finishedDate": "Data skończenia",
|
||||
"inventory_expiryDate": "Data ważności",
|
||||
"inventory_currentWeight": "Aktualna waga (g)",
|
||||
"inventory_lastWeighed": "Ostatnie ważenie",
|
||||
"inventory_notes": "Notatki",
|
||||
"inventory_badgeOpen": "Otwarte",
|
||||
"inventory_badgeSealed": "Zamknięte",
|
||||
"inventory_badgeFinished": "Skończone",
|
||||
"inventory_exp": "Wazność:",
|
||||
"inventory_opened": "Otwarto:",
|
||||
"inventory_finished": "Skończono:",
|
||||
"inventory_remaining": "g pozostało",
|
||||
"inventory_weighed": "Ważono:",
|
||||
"inventory_confirmDelete": "Usunąć to opakowanie?",
|
||||
|
||||
"routines_title": "Rutyny",
|
||||
"routines_count": "{count} rutyno (ostatnie 30 dni)",
|
||||
"routines_suggestAI": "Zaproponuj rutynę AI",
|
||||
"routines_addNew": "+ Nowa rutyna",
|
||||
"routines_noRoutines": "Nie znaleziono rutyno.",
|
||||
"routines_newTitle": "Nowa rutyna",
|
||||
"routines_backToList": "← Rutyny",
|
||||
"routines_detailsTitle": "Szczegóły rutyny",
|
||||
"routines_date": "Data *",
|
||||
"routines_amOrPm": "AM lub PM *",
|
||||
"routines_notes": "Notatki",
|
||||
"routines_notesPlaceholder": "Opcjonalne notatki",
|
||||
"routines_createRoutine": "Utwórz rutynę",
|
||||
"routines_deleteRoutine": "Usuń rutynę",
|
||||
"routines_confirmDelete": "Usunąć tę rutynę?",
|
||||
"routines_steps": "Kroki ({count})",
|
||||
"routines_addStep": "+ Dodaj krok",
|
||||
"routines_addStepTitle": "Dodaj krok",
|
||||
"routines_product": "Produkt",
|
||||
"routines_selectProduct": "Wybierz produkt",
|
||||
"routines_dose": "Dawka",
|
||||
"routines_dosePlaceholder": "np. 2 pompki",
|
||||
"routines_region": "Okolica",
|
||||
"routines_regionPlaceholder": "np. twarz",
|
||||
"routines_addStepBtn": "Dodaj krok",
|
||||
"routines_unknownStep": "Nieznany krok",
|
||||
"routines_noSteps": "Brak kroków.",
|
||||
|
||||
"grooming_title": "Harmonogram pielęgnacji",
|
||||
"grooming_backToRoutines": "← Rutyny",
|
||||
"grooming_addEntry": "+ Dodaj wpis",
|
||||
"grooming_entryAdded": "Wpis dodany.",
|
||||
"grooming_entryUpdated": "Wpis zaktualizowany.",
|
||||
"grooming_entryDeleted": "Wpis usunięty.",
|
||||
"grooming_dayOfWeek": "Dzień tygodnia",
|
||||
"grooming_action": "Czynność",
|
||||
"grooming_notesOptional": "Notatki (opcjonalnie)",
|
||||
"grooming_notesPlaceholder": "np. co 2 tygodnie",
|
||||
"grooming_noEntries": "Brak wpisów. Kliknij \"+ Dodaj wpis\", aby zacząć.",
|
||||
"grooming_confirmDelete": "Usunąć ten wpis?",
|
||||
"grooming_actionShavingRazor": "Golenie maszynką",
|
||||
"grooming_actionShavingOneblade": "Golenie OneBlade",
|
||||
"grooming_actionDermarolling": "Dermarolling",
|
||||
"grooming_dayMonday": "Poniedziałek",
|
||||
"grooming_dayTuesday": "Wtorek",
|
||||
"grooming_dayWednesday": "Środa",
|
||||
"grooming_dayThursday": "Czwartek",
|
||||
"grooming_dayFriday": "Piątek",
|
||||
"grooming_daySaturday": "Sobota",
|
||||
"grooming_daySunday": "Niedziela",
|
||||
|
||||
"suggest_title": "Propozycja rutyny AI",
|
||||
"suggest_backToRoutines": "← Rutyny",
|
||||
"suggest_singleTab": "Jedna rutyna",
|
||||
"suggest_batchTab": "Batch / Urlop",
|
||||
"suggest_singleParams": "Parametry",
|
||||
"suggest_date": "Data",
|
||||
"suggest_timeOfDay": "Pora dnia",
|
||||
"suggest_contextLabel": "Dodatkowy kontekst dla AI",
|
||||
"suggest_contextOptional": "(opcjonalny)",
|
||||
"suggest_contextPlaceholder": "np. wieczór imprezowy, skupiam się na nawilżeniu...",
|
||||
"suggest_generateBtn": "Generuj propozycję",
|
||||
"suggest_generating": "Generuję…",
|
||||
"suggest_proposalTitle": "Propozycja",
|
||||
"suggest_saveRoutine": "Zapisz rutynę",
|
||||
"suggest_saving": "Zapisuję…",
|
||||
"suggest_regenerate": "Wygeneruj ponownie",
|
||||
"suggest_batchRange": "Zakres dat",
|
||||
"suggest_fromDate": "Od",
|
||||
"suggest_toDate": "Do (max 14 dni)",
|
||||
"suggest_batchContextLabel": "Kontekst / cel wyjazdu",
|
||||
"suggest_batchContextPlaceholder": "np. słoneczna podróż do Włoch, aktywny urlop górski...",
|
||||
"suggest_generatePlan": "Generuj plan",
|
||||
"suggest_generatingPlan": "Generuję plan…",
|
||||
"suggest_planTitle": "Plan ({count} dni)",
|
||||
"suggest_saveAllRoutines": "Zapisz wszystkie rutyny",
|
||||
"suggest_amSteps": "kroków",
|
||||
"suggest_pmSteps": "kroków",
|
||||
"suggest_noAmSteps": "Brak kroków AM.",
|
||||
"suggest_noPmSteps": "Brak kroków PM.",
|
||||
"suggest_errorDefault": "Błąd podczas generowania.",
|
||||
"suggest_errorBatch": "Błąd podczas generowania planu.",
|
||||
"suggest_errorSave": "Błąd podczas zapisywania.",
|
||||
"suggest_amMorning": "AM (rano)",
|
||||
"suggest_pmEvening": "PM (wieczór)",
|
||||
|
||||
"medications_title": "Leki",
|
||||
"medications_count": "{count} wpisów",
|
||||
"medications_addNew": "+ Dodaj lek",
|
||||
"medications_newTitle": "Nowy lek",
|
||||
"medications_kind": "Rodzaj",
|
||||
"medications_productName": "Nazwa produktu *",
|
||||
"medications_productNamePlaceholder": "np. Witamina D3",
|
||||
"medications_activeSubstance": "Substancja czynna",
|
||||
"medications_activeSubstancePlaceholder": "np. cholekalcyferol",
|
||||
"medications_notes": "Notatki",
|
||||
"medications_added": "Lek dodany.",
|
||||
"medications_usages": "{count} użyć",
|
||||
"medications_noMedications": "Brak leków.",
|
||||
"medications_kindPrescription": "Na receptę",
|
||||
"medications_kindOtc": "OTC (bez recepty)",
|
||||
"medications_kindSupplement": "Suplement",
|
||||
"medications_kindHerbal": "Zioła",
|
||||
"medications_kindOther": "Inne",
|
||||
|
||||
"labResults_title": "Wyniki badań",
|
||||
"labResults_count": "{count} wyników",
|
||||
"labResults_addNew": "+ Dodaj wynik",
|
||||
"labResults_newTitle": "Nowy wynik badania",
|
||||
"labResults_flagFilter": "Flaga:",
|
||||
"labResults_flagAll": "Wszystkie",
|
||||
"labResults_flagNone": "Brak",
|
||||
"labResults_date": "Data *",
|
||||
"labResults_loincCode": "Kod LOINC *",
|
||||
"labResults_testName": "Nazwa badania",
|
||||
"labResults_testNamePlaceholder": "np. Hemoglobina",
|
||||
"labResults_lab": "Laboratorium",
|
||||
"labResults_labPlaceholder": "np. LabCorp",
|
||||
"labResults_value": "Wartość",
|
||||
"labResults_unit": "Jednostka",
|
||||
"labResults_unitPlaceholder": "np. g/dL",
|
||||
"labResults_flag": "Flaga",
|
||||
"labResults_added": "Wynik dodany.",
|
||||
"labResults_colDate": "Data",
|
||||
"labResults_colTest": "Badanie",
|
||||
"labResults_colLoinc": "LOINC",
|
||||
"labResults_colValue": "Wartość",
|
||||
"labResults_colFlag": "Flaga",
|
||||
"labResults_colLab": "Lab",
|
||||
"labResults_noResults": "Nie znaleziono wyników badań.",
|
||||
|
||||
"skin_title": "Stan skóry",
|
||||
"skin_count": "{count} wpisów",
|
||||
"skin_addNew": "+ Dodaj wpis",
|
||||
"skin_aiAnalysisTitle": "Analiza AI ze zdjęć",
|
||||
"skin_aiUploadText": "Prześlij 1–3 zdjęcia skóry. AI wypełni pola formularza poniżej.",
|
||||
"skin_analyzePhotos": "Analizuj zdjęcia",
|
||||
"skin_analyzing": "Analizuję…",
|
||||
"skin_newSnapshotTitle": "Nowy wpis",
|
||||
"skin_date": "Data *",
|
||||
"skin_overallState": "Ogólny stan",
|
||||
"skin_texture": "Tekstura",
|
||||
"skin_skinType": "Typ skóry",
|
||||
"skin_barrierState": "Stan bariery",
|
||||
"skin_hydration": "Nawilżenie (1–5)",
|
||||
"skin_sensitivity": "Wrażliwość (1–5)",
|
||||
"skin_sebumTzone": "Sebum T-zone (1–5)",
|
||||
"skin_sebumCheeks": "Sebum policzki (1–5)",
|
||||
"skin_activeConcerns": "Aktywne problemy (przecinek)",
|
||||
"skin_activeConcernsPlaceholder": "trądzik, zaczerwienienie, odwodnienie",
|
||||
"skin_notes": "Notatki",
|
||||
"skin_addSnapshot": "Dodaj wpis",
|
||||
"skin_snapshotAdded": "Wpis dodany.",
|
||||
"skin_snapshotUpdated": "Wpis zaktualizowany.",
|
||||
"skin_snapshotDeleted": "Wpis usunięty.",
|
||||
"skin_noSnapshots": "Brak wpisów o stanie skóry.",
|
||||
"skin_hydrationLabel": "Nawilżenie",
|
||||
"skin_sensitivityLabel": "Wrażliwość",
|
||||
"skin_barrierLabel": "Bariera",
|
||||
"skin_stateExcellent": "doskonały",
|
||||
"skin_stateGood": "dobry",
|
||||
"skin_stateFair": "przeciętny",
|
||||
"skin_statePoor": "zły",
|
||||
"skin_textureSmooth": "gładka",
|
||||
"skin_textureRough": "szorstka",
|
||||
"skin_textureFlaky": "łuszcząca się",
|
||||
"skin_textureBumpy": "nierówna",
|
||||
"skin_barrierIntact": "nienaruszona",
|
||||
"skin_barrierMildly": "lekko naruszona",
|
||||
"skin_barrierCompromised": "naruszona",
|
||||
"skin_typeDry": "sucha",
|
||||
"skin_typeOily": "tłusta",
|
||||
"skin_typeCombination": "mieszana",
|
||||
"skin_typeSensitive": "wrażliwa",
|
||||
"skin_typeNormal": "normalna",
|
||||
"skin_typeAcneProne": "trądzikowa",
|
||||
|
||||
"productForm_aiPrefill": "Uzupełnienie AI",
|
||||
"productForm_aiPrefillText": "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.",
|
||||
"productForm_pasteText": "Wklej tutaj opis produktu, składniki INCI...",
|
||||
"productForm_parseWithAI": "Uzupełnij pola (AI)",
|
||||
"productForm_parsing": "Przetwarzam…",
|
||||
"productForm_basicInfo": "Informacje podstawowe",
|
||||
"productForm_name": "Nazwa *",
|
||||
"productForm_namePlaceholder": "np. Hydro Boost Water Gel",
|
||||
"productForm_brand": "Marka *",
|
||||
"productForm_brandPlaceholder": "np. Neutrogena",
|
||||
"productForm_lineName": "Linia / seria",
|
||||
"productForm_lineNamePlaceholder": "np. Hydro Boost",
|
||||
"productForm_url": "URL",
|
||||
"productForm_sku": "SKU",
|
||||
"productForm_skuPlaceholder": "np. NTR-HB-50",
|
||||
"productForm_barcode": "Kod kreskowy / EAN",
|
||||
"productForm_barcodePlaceholder": "np. 3614273258975",
|
||||
"productForm_classification": "Klasyfikacja",
|
||||
"productForm_category": "Kategoria *",
|
||||
"productForm_selectCategory": "Wybierz kategorię",
|
||||
"productForm_time": "Pora *",
|
||||
"productForm_timeOptions": "AM / PM / Oba",
|
||||
"productForm_timeBoth": "Oba",
|
||||
"productForm_leaveOn": "Leave-on *",
|
||||
"productForm_leaveOnYes": "Tak (leave-on)",
|
||||
"productForm_leaveOnNo": "Nie (rinse-off)",
|
||||
"productForm_texture": "Tekstura",
|
||||
"productForm_selectTexture": "Wybierz teksturę",
|
||||
"productForm_absorptionSpeed": "Szybkość wchłaniania",
|
||||
"productForm_selectSpeed": "Wybierz szybkość",
|
||||
"productForm_skinProfile": "Profil skóry",
|
||||
"productForm_recommendedFor": "Polecane dla typów skóry",
|
||||
"productForm_targetConcerns": "Problemy docelowe",
|
||||
"productForm_contraindications": "Przeciwwskazania (jedno na linię)",
|
||||
"productForm_contraindicationsPlaceholder": "np. aktywna rosacea",
|
||||
"productForm_ingredients": "Składniki",
|
||||
"productForm_inciList": "Lista INCI (jeden składnik na linię)",
|
||||
"productForm_activeIngredients": "Składniki aktywne",
|
||||
"productForm_addActive": "+ Dodaj aktywny",
|
||||
"productForm_noActives": "Brak składników aktywnych.",
|
||||
"productForm_activeName": "Nazwa",
|
||||
"productForm_activePercent": "%",
|
||||
"productForm_activeStrength": "Siła",
|
||||
"productForm_activeIrritation": "Podrażnienie",
|
||||
"productForm_activeFunctions": "Funkcje",
|
||||
"productForm_effectProfile": "Profil działania (0–5)",
|
||||
"productForm_interactions": "Interakcje",
|
||||
"productForm_synergizesWith": "Synergizuje z (jedno na linię)",
|
||||
"productForm_incompatibleWith": "Niekompatybilny z",
|
||||
"productForm_addIncompatibility": "+ Dodaj niekompatybilność",
|
||||
"productForm_noIncompatibilities": "Brak niekompatybilności.",
|
||||
"productForm_incompTarget": "Składnik docelowy",
|
||||
"productForm_incompScope": "Zakres",
|
||||
"productForm_incompReason": "Powód (opcjonalny)",
|
||||
"productForm_incompReasonPlaceholder": "np. zmniejsza skuteczność",
|
||||
"productForm_incompScopeSelect": "Wybierz…",
|
||||
"productForm_contextRules": "Reguły kontekstu",
|
||||
"productForm_ctxAfterShaving": "Bezpieczny po goleniu",
|
||||
"productForm_ctxAfterAcids": "Bezpieczny po kwasach",
|
||||
"productForm_ctxAfterRetinoids": "Bezpieczny po retinoidach",
|
||||
"productForm_ctxCompromisedBarrier": "Bezpieczny przy naruszonej barierze",
|
||||
"productForm_ctxLowUvOnly": "Tylko przy niskim UV (wieczór/zakrycie)",
|
||||
"productForm_productDetails": "Szczegóły produktu",
|
||||
"productForm_priceTier": "Przedział cenowy",
|
||||
"productForm_selectTier": "Wybierz przedział",
|
||||
"productForm_sizeMl": "Rozmiar (ml)",
|
||||
"productForm_fullWeightG": "Waga pełna (g)",
|
||||
"productForm_emptyWeightG": "Waga pustego (g)",
|
||||
"productForm_paoMonths": "PAO (miesiące)",
|
||||
"productForm_phMin": "pH min",
|
||||
"productForm_phMax": "pH max",
|
||||
"productForm_usageNotes": "Notatki o stosowaniu",
|
||||
"productForm_usageNotesPlaceholder": "np. Nakładaj na wilgotną skórę, unikaj okolic oczu",
|
||||
"productForm_safetyFlags": "Flagi bezpieczeństwa",
|
||||
"productForm_fragranceFree": "Bez zapachów",
|
||||
"productForm_essentialOilsFree": "Bez olejków eterycznych",
|
||||
"productForm_alcoholDenatFree": "Bez alkoholu denat.",
|
||||
"productForm_pregnancySafe": "Bezpieczny w ciąży",
|
||||
"productForm_usageConstraints": "Ograniczenia stosowania",
|
||||
"productForm_minIntervalHours": "Min. przerwa (godziny)",
|
||||
"productForm_maxFrequencyPerWeek": "Max użyć na tydzień",
|
||||
"productForm_isMedication": "To lek",
|
||||
"productForm_isTool": "To narzędzie (np. dermaroller)",
|
||||
"productForm_needleLengthMm": "Długość igły (mm, tylko narzędzia)",
|
||||
"productForm_personalNotes": "Notatki osobiste",
|
||||
"productForm_repurchaseIntent": "Zamiar ponownego zakupu",
|
||||
"productForm_toleranceNotes": "Notatki o tolerancji",
|
||||
"productForm_toleranceNotesPlaceholder": "np. Lekkie pieczenie, ustępuje po 2 tygodniach",
|
||||
|
||||
"productForm_categoryCleanser": "Żel/pianka do mycia",
|
||||
"productForm_categoryToner": "Tonik",
|
||||
"productForm_categoryEssence": "Esencja",
|
||||
"productForm_categorySerum": "Serum",
|
||||
"productForm_categoryMoisturizer": "Krem",
|
||||
"productForm_categorySpf": "SPF",
|
||||
"productForm_categoryMask": "Maska",
|
||||
"productForm_categoryExfoliant": "Peeling",
|
||||
"productForm_categoryHairTreatment": "Pielęgnacja włosów",
|
||||
"productForm_categoryTool": "Narzędzie",
|
||||
"productForm_categorySpotTreatment": "Punkt leczenia",
|
||||
"productForm_categoryOil": "Olejek",
|
||||
|
||||
"productForm_textureWatery": "Wodnista",
|
||||
"productForm_textureGel": "Żel",
|
||||
"productForm_textureEmulsion": "Emulsja",
|
||||
"productForm_textureCream": "Krem",
|
||||
"productForm_textureOil": "Olejek",
|
||||
"productForm_textureBalm": "Balsam",
|
||||
"productForm_textureFoam": "Pianka",
|
||||
"productForm_textureFluid": "Fluid",
|
||||
|
||||
"productForm_absorptionVeryFast": "Bardzo szybkie",
|
||||
"productForm_absorptionFast": "Szybkie",
|
||||
"productForm_absorptionModerate": "Umiarkowane",
|
||||
"productForm_absorptionSlow": "Wolne",
|
||||
"productForm_absorptionVerySlow": "Bardzo wolne",
|
||||
|
||||
"productForm_priceBudget": "Budżetowy",
|
||||
"productForm_priceMid": "Średni",
|
||||
"productForm_pricePremium": "Premium",
|
||||
"productForm_priceLuxury": "Luksusowy",
|
||||
|
||||
"productForm_skinTypeDry": "sucha",
|
||||
"productForm_skinTypeOily": "tłusta",
|
||||
"productForm_skinTypeCombination": "mieszana",
|
||||
"productForm_skinTypeSensitive": "wrażliwa",
|
||||
"productForm_skinTypeNormal": "normalna",
|
||||
"productForm_skinTypeAcneProne": "trądzikowa",
|
||||
|
||||
"productForm_concernAcne": "trądzik",
|
||||
"productForm_concernRosacea": "rosacea",
|
||||
"productForm_concernHyperpigmentation": "przebarwienia",
|
||||
"productForm_concernAging": "starzenie",
|
||||
"productForm_concernDehydration": "odwodnienie",
|
||||
"productForm_concernRedness": "zaczerwienienie",
|
||||
"productForm_concernDamagedBarrier": "naruszona bariera",
|
||||
"productForm_concernPoreVisibility": "widoczność porów",
|
||||
"productForm_concernUnevenTexture": "nierówna tekstura",
|
||||
"productForm_concernHairGrowth": "wzrost włosów",
|
||||
"productForm_concernSebumExcess": "nadmiar sebum",
|
||||
|
||||
"productForm_fnHumectant": "humektant",
|
||||
"productForm_fnEmollient": "emolient",
|
||||
"productForm_fnOcclusive": "okluzja",
|
||||
"productForm_fnExfoliantAha": "peeling AHA",
|
||||
"productForm_fnExfoliantBha": "peeling BHA",
|
||||
"productForm_fnExfoliantPha": "peeling PHA",
|
||||
"productForm_fnRetinoid": "retinoid",
|
||||
"productForm_fnAntioxidant": "antyoksydant",
|
||||
"productForm_fnSoothing": "łagodzący",
|
||||
"productForm_fnBarrierSupport": "wsparcie bariery",
|
||||
"productForm_fnBrightening": "rozjaśniający",
|
||||
"productForm_fnAntiAcne": "przeciwtrądzikowy",
|
||||
"productForm_fnCeramide": "ceramid",
|
||||
"productForm_fnNiacinamide": "niacynamid",
|
||||
"productForm_fnSunscreen": "filtr UV",
|
||||
"productForm_fnPeptide": "peptyd",
|
||||
"productForm_fnHairGrowth": "stymulator wzrostu włosów",
|
||||
"productForm_fnPrebiotic": "prebiotyk",
|
||||
"productForm_fnVitaminC": "witamina C",
|
||||
"productForm_fnAntiAging": "przeciwstarzeniowy",
|
||||
|
||||
"productForm_scopeSameStep": "ten sam krok",
|
||||
"productForm_scopeSameDay": "ten sam dzień",
|
||||
"productForm_scopeSamePeriod": "ten sam okres",
|
||||
|
||||
"productForm_strengthLow": "1 Niskie",
|
||||
"productForm_strengthMedium": "2 Średnie",
|
||||
"productForm_strengthHigh": "3 Wysokie",
|
||||
|
||||
"productForm_effectHydrationImmediate": "Nawilżenie (natychmiastowe)",
|
||||
"productForm_effectHydrationLongTerm": "Nawilżenie (długoterminowe)",
|
||||
"productForm_effectBarrierRepair": "Naprawa bariery",
|
||||
"productForm_effectSoothing": "Łagodzenie",
|
||||
"productForm_effectExfoliation": "Złuszczanie",
|
||||
"productForm_effectRetinoid": "Aktywność retinoidu",
|
||||
"productForm_effectIrritation": "Ryzyko podrażnienia",
|
||||
"productForm_effectComedogenic": "Ryzyko komedogenności",
|
||||
"productForm_effectBarrierDisruption": "Ryzyko naruszenia bariery",
|
||||
"productForm_effectDryness": "Ryzyko przesuszenia",
|
||||
"productForm_effectBrightening": "Rozjaśnienie",
|
||||
"productForm_effectAntiAcne": "Działanie przeciwtrądzikowe",
|
||||
"productForm_effectAntiAging": "Działanie przeciwstarzeniowe",
|
||||
|
||||
"lang_pl": "PL",
|
||||
"lang_en": "EN"
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@inlang/paraglide-js": "^2.13.0",
|
||||
"bits-ui": "^2.16.2",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-svelte": "^0.575.0",
|
||||
|
|
|
|||
263
frontend/pnpm-lock.yaml
generated
263
frontend/pnpm-lock.yaml
generated
|
|
@ -8,9 +8,12 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
'@inlang/paraglide-js':
|
||||
specifier: ^2.13.0
|
||||
version: 2.13.0
|
||||
bits-ui:
|
||||
specifier: ^2.16.2
|
||||
version: 2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)
|
||||
version: 2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
|
|
@ -32,16 +35,16 @@ importers:
|
|||
version: 0.561.0(svelte@5.53.5)
|
||||
'@sveltejs/adapter-node':
|
||||
specifier: ^5.0.0
|
||||
version: 5.5.4(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))
|
||||
version: 5.5.4(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))
|
||||
'@sveltejs/kit':
|
||||
specifier: ^2.50.2
|
||||
version: 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
version: 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@sveltejs/vite-plugin-svelte':
|
||||
specifier: ^6.2.4
|
||||
version: 6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
version: 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
version: 4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
svelte:
|
||||
specifier: ^5.51.0
|
||||
version: 5.53.5
|
||||
|
|
@ -59,7 +62,7 @@ importers:
|
|||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^7.3.1
|
||||
version: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
version: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -228,6 +231,17 @@ packages:
|
|||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@inlang/paraglide-js@2.13.0':
|
||||
resolution: {integrity: sha512-m7JQiTeLC3tY3DusUCc4iRWlsKoMuDLhw4iGhkY0yI96ki7PK42DLsi1kMk8ubSVenKOwgrs7eqQZN1Htvkhew==}
|
||||
hasBin: true
|
||||
|
||||
'@inlang/recommend-sherlock@0.2.1':
|
||||
resolution: {integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==}
|
||||
|
||||
'@inlang/sdk@2.7.0':
|
||||
resolution: {integrity: sha512-yJNBD0o8i29TTJqWX5uDRHxnalDGcsUDctxepzFXsUfkzqGWfiFBxODdxvReqvM2CuKAAOo/kib/F1UcgdYFNQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@internationalized/date@3.11.0':
|
||||
resolution: {integrity: sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==}
|
||||
|
||||
|
|
@ -247,6 +261,13 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@lix-js/sdk@0.4.7':
|
||||
resolution: {integrity: sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@lix-js/server-protocol-schema@0.1.1':
|
||||
resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==}
|
||||
|
||||
'@lucide/svelte@0.561.0':
|
||||
resolution: {integrity: sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==}
|
||||
peerDependencies:
|
||||
|
|
@ -429,6 +450,13 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@sinclair/typebox@0.31.28':
|
||||
resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==}
|
||||
|
||||
'@sqlite.org/sqlite-wasm@3.48.0-build4':
|
||||
resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==}
|
||||
hasBin: true
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
|
|
@ -576,6 +604,9 @@ packages:
|
|||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/node@25.3.3':
|
||||
resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==}
|
||||
|
||||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
|
||||
|
|
@ -591,6 +622,9 @@ packages:
|
|||
resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
array-timsort@1.0.3:
|
||||
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
|
||||
|
||||
axobject-query@4.1.0:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -610,13 +644,36 @@ packages:
|
|||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
commander@11.1.0:
|
||||
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
comment-json@4.5.1:
|
||||
resolution: {integrity: sha512-taEtr3ozUmOB7it68Jll7s0Pwm+aoiHyXKrEC8SEodL4rNpdfDLqa7PfBlrgFoCNNdR8ImL+muti5IGvktJAAg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
commondir@1.0.1:
|
||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||
|
||||
consola@3.4.0:
|
||||
resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==}
|
||||
engines: {node: ^14.18.0 || >=16.10.0}
|
||||
|
||||
cookie@0.6.0:
|
||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
dedent@1.5.1:
|
||||
resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==}
|
||||
peerDependencies:
|
||||
babel-plugin-macros: ^3.1.0
|
||||
peerDependenciesMeta:
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -644,6 +701,11 @@ packages:
|
|||
esm-env@1.2.2:
|
||||
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
||||
|
||||
esprima@4.0.1:
|
||||
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
esrap@2.2.3:
|
||||
resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==}
|
||||
|
||||
|
|
@ -674,6 +736,10 @@ packages:
|
|||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
human-id@4.1.3:
|
||||
resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==}
|
||||
hasBin: true
|
||||
|
||||
inline-style-parser@0.2.7:
|
||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||
|
||||
|
|
@ -694,10 +760,22 @@ packages:
|
|||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
js-sha256@0.11.1:
|
||||
resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==}
|
||||
|
||||
json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
kleur@4.1.5:
|
||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
kysely@0.27.6:
|
||||
resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
lightningcss-android-arm64@1.31.1:
|
||||
resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
|
@ -870,6 +948,11 @@ packages:
|
|||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
sqlite-wasm-kysely@0.3.0:
|
||||
resolution: {integrity: sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==}
|
||||
peerDependencies:
|
||||
kysely: '*'
|
||||
|
||||
style-to-object@1.0.14:
|
||||
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
|
||||
|
||||
|
|
@ -940,6 +1023,24 @@ packages:
|
|||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
unplugin@2.3.11:
|
||||
resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
urlpattern-polyfill@10.1.0:
|
||||
resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==}
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
hasBin: true
|
||||
|
||||
uuid@13.0.0:
|
||||
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
|
||||
hasBin: true
|
||||
|
||||
vite@7.3.1:
|
||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
|
@ -988,6 +1089,9 @@ packages:
|
|||
vite:
|
||||
optional: true
|
||||
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
zimmerframe@1.1.4:
|
||||
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
||||
|
||||
|
|
@ -1082,6 +1186,32 @@ snapshots:
|
|||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@inlang/paraglide-js@2.13.0':
|
||||
dependencies:
|
||||
'@inlang/recommend-sherlock': 0.2.1
|
||||
'@inlang/sdk': 2.7.0
|
||||
commander: 11.1.0
|
||||
consola: 3.4.0
|
||||
json5: 2.2.3
|
||||
unplugin: 2.3.11
|
||||
urlpattern-polyfill: 10.1.0
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
|
||||
'@inlang/recommend-sherlock@0.2.1':
|
||||
dependencies:
|
||||
comment-json: 4.5.1
|
||||
|
||||
'@inlang/sdk@2.7.0':
|
||||
dependencies:
|
||||
'@lix-js/sdk': 0.4.7
|
||||
'@sinclair/typebox': 0.31.28
|
||||
kysely: 0.27.6
|
||||
sqlite-wasm-kysely: 0.3.0(kysely@0.27.6)
|
||||
uuid: 13.0.0
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
|
||||
'@internationalized/date@3.11.0':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.19
|
||||
|
|
@ -1105,6 +1235,20 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@lix-js/sdk@0.4.7':
|
||||
dependencies:
|
||||
'@lix-js/server-protocol-schema': 0.1.1
|
||||
dedent: 1.5.1
|
||||
human-id: 4.1.3
|
||||
js-sha256: 0.11.1
|
||||
kysely: 0.27.6
|
||||
sqlite-wasm-kysely: 0.3.0(kysely@0.27.6)
|
||||
uuid: 10.0.0
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
|
||||
'@lix-js/server-protocol-schema@0.1.1': {}
|
||||
|
||||
'@lucide/svelte@0.561.0(svelte@5.53.5)':
|
||||
dependencies:
|
||||
svelte: 5.53.5
|
||||
|
|
@ -1222,25 +1366,29 @@ snapshots:
|
|||
'@rollup/rollup-win32-x64-msvc@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@sinclair/typebox@0.31.28': {}
|
||||
|
||||
'@sqlite.org/sqlite-wasm@3.48.0-build4': {}
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)':
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
||||
'@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))':
|
||||
'@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))':
|
||||
dependencies:
|
||||
'@rollup/plugin-commonjs': 29.0.0(rollup@4.59.0)
|
||||
'@rollup/plugin-json': 6.1.0(rollup@4.59.0)
|
||||
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.59.0)
|
||||
'@sveltejs/kit': 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@sveltejs/kit': 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
rollup: 4.59.0
|
||||
|
||||
'@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
'@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0)
|
||||
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@types/cookie': 0.6.0
|
||||
acorn: 8.16.0
|
||||
cookie: 0.6.0
|
||||
|
|
@ -1252,26 +1400,26 @@ snapshots:
|
|||
set-cookie-parser: 3.0.1
|
||||
sirv: 3.0.2
|
||||
svelte: 5.53.5
|
||||
vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
'@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
obug: 2.1.1
|
||||
svelte: 5.53.5
|
||||
vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
|
||||
'@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
'@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
deepmerge: 4.3.1
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
svelte: 5.53.5
|
||||
vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vitefu: 1.1.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vitefu: 1.1.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
|
||||
'@swc/helpers@0.5.19':
|
||||
dependencies:
|
||||
|
|
@ -1338,17 +1486,22 @@ snapshots:
|
|||
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1
|
||||
|
||||
'@tailwindcss/vite@4.2.1(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
'@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.2.1
|
||||
'@tailwindcss/oxide': 4.2.1
|
||||
tailwindcss: 4.2.1
|
||||
vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
|
||||
'@types/cookie@0.6.0': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/node@25.3.3':
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
optional: true
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
|
@ -1357,17 +1510,19 @@ snapshots:
|
|||
|
||||
aria-query@5.3.1: {}
|
||||
|
||||
array-timsort@1.0.3: {}
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
||||
bits-ui@2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5):
|
||||
bits-ui@2.16.2(@internationalized/date@3.11.0)(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5):
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.4
|
||||
'@floating-ui/dom': 1.7.5
|
||||
'@internationalized/date': 3.11.0
|
||||
esm-env: 1.2.2
|
||||
runed: 0.35.1(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)
|
||||
runed: 0.35.1(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)
|
||||
svelte: 5.53.5
|
||||
svelte-toolbelt: 0.10.6(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)
|
||||
svelte-toolbelt: 0.10.6(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)
|
||||
tabbable: 6.4.0
|
||||
transitivePeerDependencies:
|
||||
- '@sveltejs/kit'
|
||||
|
|
@ -1378,10 +1533,24 @@ snapshots:
|
|||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
commander@11.1.0: {}
|
||||
|
||||
comment-json@4.5.1:
|
||||
dependencies:
|
||||
array-timsort: 1.0.3
|
||||
core-util-is: 1.0.3
|
||||
esprima: 4.0.1
|
||||
|
||||
commondir@1.0.1: {}
|
||||
|
||||
consola@3.4.0: {}
|
||||
|
||||
cookie@0.6.0: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
dedent@1.5.1: {}
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
|
@ -1426,6 +1595,8 @@ snapshots:
|
|||
|
||||
esm-env@1.2.2: {}
|
||||
|
||||
esprima@4.0.1: {}
|
||||
|
||||
esrap@2.2.3:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
|
@ -1447,6 +1618,8 @@ snapshots:
|
|||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
human-id@4.1.3: {}
|
||||
|
||||
inline-style-parser@0.2.7: {}
|
||||
|
||||
is-core-module@2.16.1:
|
||||
|
|
@ -1465,8 +1638,14 @@ snapshots:
|
|||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
js-sha256@0.11.1: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
kleur@4.1.5: {}
|
||||
|
||||
kysely@0.27.6: {}
|
||||
|
||||
lightningcss-android-arm64@1.31.1:
|
||||
optional: true
|
||||
|
||||
|
|
@ -1603,14 +1782,14 @@ snapshots:
|
|||
esm-env: 1.2.2
|
||||
svelte: 5.53.5
|
||||
|
||||
runed@0.35.1(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5):
|
||||
runed@0.35.1(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
esm-env: 1.2.2
|
||||
lz-string: 1.5.0
|
||||
svelte: 5.53.5
|
||||
optionalDependencies:
|
||||
'@sveltejs/kit': 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@sveltejs/kit': 2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
|
||||
sade@1.8.1:
|
||||
dependencies:
|
||||
|
|
@ -1626,6 +1805,11 @@ snapshots:
|
|||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
sqlite-wasm-kysely@0.3.0(kysely@0.27.6):
|
||||
dependencies:
|
||||
'@sqlite.org/sqlite-wasm': 3.48.0-build4
|
||||
kysely: 0.27.6
|
||||
|
||||
style-to-object@1.0.14:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.7
|
||||
|
|
@ -1644,10 +1828,10 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- picomatch
|
||||
|
||||
svelte-toolbelt@0.10.6(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5):
|
||||
svelte-toolbelt@0.10.6(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5):
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
runed: 0.35.1(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)
|
||||
runed: 0.35.1(@sveltejs/kit@2.53.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)))(svelte@5.53.5)
|
||||
style-to-object: 1.0.14
|
||||
svelte: 5.53.5
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -1704,7 +1888,23 @@ snapshots:
|
|||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1):
|
||||
undici-types@7.18.2:
|
||||
optional: true
|
||||
|
||||
unplugin@2.3.11:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
acorn: 8.16.0
|
||||
picomatch: 4.0.3
|
||||
webpack-virtual-modules: 0.6.2
|
||||
|
||||
urlpattern-polyfill@10.1.0: {}
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
uuid@13.0.0: {}
|
||||
|
||||
vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
|
|
@ -1713,12 +1913,15 @@ snapshots:
|
|||
rollup: 4.59.0
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 25.3.3
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.31.1
|
||||
|
||||
vitefu@1.1.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.31.1)):
|
||||
vitefu@1.1.2(vite@7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)):
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
zimmerframe@1.1.4: {}
|
||||
|
|
|
|||
12
frontend/project.inlang/settings.json
Normal file
12
frontend/project.inlang/settings.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"$schema": "https://inlang.com/schema/project-settings",
|
||||
"baseLocale": "pl",
|
||||
"locales": ["pl", "en"],
|
||||
"modules": [
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
|
||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
|
||||
],
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{locale}.json"
|
||||
}
|
||||
}
|
||||
6
frontend/src/hooks.server.ts
Normal file
6
frontend/src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
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));
|
||||
};
|
||||
15
frontend/src/lib/components/LanguageSwitcher.svelte
Normal file
15
frontend/src/lib/components/LanguageSwitcher.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { getLocale, setLocale } from '$lib/paraglide/runtime.js';
|
||||
</script>
|
||||
|
||||
<div class="flex gap-1 font-mono text-xs text-muted-foreground">
|
||||
<button
|
||||
class="hover:text-foreground transition-colors {getLocale() === 'pl' ? 'font-bold text-foreground' : ''}"
|
||||
onclick={() => setLocale('pl')}
|
||||
>PL</button>
|
||||
<span>|</span>
|
||||
<button
|
||||
class="hover:text-foreground transition-colors {getLocale() === 'en' ? 'font-bold text-foreground' : ''}"
|
||||
onclick={() => setLocale('en')}
|
||||
>EN</button>
|
||||
</div>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { page } from '$app/state';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Dashboard', icon: '🏠' },
|
||||
{ href: '/products', label: 'Products', icon: '🧴' },
|
||||
{ href: '/routines', label: 'Routines', icon: '📋' },
|
||||
{ href: '/routines/grooming-schedule', label: 'Grooming', icon: '🪒' },
|
||||
{ href: '/health/medications', label: 'Medications', icon: '💊' },
|
||||
{ href: '/health/lab-results', label: 'Lab Results', icon: '🔬' },
|
||||
{ href: '/skin', label: 'Skin', icon: '✨' }
|
||||
];
|
||||
const navItems = $derived([
|
||||
{ href: '/', label: m.nav_dashboard(), icon: '🏠' },
|
||||
{ href: '/products', label: m.nav_products(), icon: '🧴' },
|
||||
{ href: '/routines', label: m.nav_routines(), icon: '📋' },
|
||||
{ href: '/routines/grooming-schedule', label: m.nav_grooming(), icon: '🪒' },
|
||||
{ href: '/health/medications', label: m.nav_medications(), icon: '💊' },
|
||||
{ href: '/health/lab-results', label: m["nav_labResults"](), icon: '🔬' },
|
||||
{ href: '/skin', label: m.nav_skin(), icon: '✨' }
|
||||
]);
|
||||
|
||||
function isActive(href: string) {
|
||||
if (href === '/') return page.url.pathname === '/';
|
||||
|
|
@ -30,8 +32,8 @@
|
|||
<!-- Sidebar -->
|
||||
<nav class="w-56 shrink-0 border-r border-border bg-card px-3 py-6">
|
||||
<div class="mb-8 px-3">
|
||||
<h1 class="text-lg font-semibold tracking-tight">innercontext</h1>
|
||||
<p class="text-xs text-muted-foreground">personal health & skincare</p>
|
||||
<h1 class="text-lg font-semibold tracking-tight">{m["nav_appName"]()}</h1>
|
||||
<p class="text-xs text-muted-foreground">{m["nav_appSubtitle"]()}</p>
|
||||
</div>
|
||||
<ul class="space-y-1">
|
||||
{#each navItems as item}
|
||||
|
|
@ -49,6 +51,9 @@
|
|||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="mt-6 px-3">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main content -->
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
|
||||
|
|
@ -13,19 +14,19 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Dashboard — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m.dashboard_title()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Dashboard</h2>
|
||||
<p class="text-muted-foreground">Your recent health & skincare overview</p>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{m.dashboard_title()}</h2>
|
||||
<p class="text-muted-foreground">{m.dashboard_subtitle()}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<!-- Latest skin snapshot -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Latest Skin Snapshot</CardTitle>
|
||||
<CardTitle>{m["dashboard_latestSnapshot"]()}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if data.latestSnapshot}
|
||||
|
|
@ -51,7 +52,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">No skin snapshots yet.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["dashboard_noSnapshots"]()}</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
<!-- Recent routines -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Routines</CardTitle>
|
||||
<CardTitle>{m["dashboard_recentRoutines"]()}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#if data.recentRoutines.length}
|
||||
|
|
@ -76,7 +77,7 @@
|
|||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">No routines in the past 2 weeks.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["dashboard_noRoutines"]()}</p>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
|
|
@ -34,16 +35,16 @@
|
|||
let filterFlag = $derived(data.flag ?? '');
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Lab Results — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m["labResults_title"]()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Lab Results</h2>
|
||||
<p class="text-muted-foreground">{data.results.length} results</p>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{m["labResults_title"]()}</h2>
|
||||
<p class="text-muted-foreground">{m["labResults_count"]({ count: data.results.length })}</p>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => (showForm = !showForm)}>
|
||||
{showForm ? 'Cancel' : '+ Add result'}
|
||||
{showForm ? m.common_cancel() : m["labResults_addNew"]()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -51,12 +52,12 @@
|
|||
<div class="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">{form.error}</div>
|
||||
{/if}
|
||||
{#if form?.created}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Result added.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["labResults_added"]()}</div>
|
||||
{/if}
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm text-muted-foreground">Flag:</span>
|
||||
<span class="text-sm text-muted-foreground">{m["labResults_flagFilter"]()}</span>
|
||||
<Select
|
||||
type="single"
|
||||
value={filterFlag}
|
||||
|
|
@ -64,9 +65,9 @@
|
|||
goto(v ? `/health/lab-results?flag=${v}` : '/health/lab-results');
|
||||
}}
|
||||
>
|
||||
<SelectTrigger class="w-32">{filterFlag || 'All'}</SelectTrigger>
|
||||
<SelectTrigger class="w-32">{filterFlag || m["labResults_flagAll"]()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">All</SelectItem>
|
||||
<SelectItem value="">{m["labResults_flagAll"]()}</SelectItem>
|
||||
{#each flags as f (f)}
|
||||
<SelectItem value={f}>{f}</SelectItem>
|
||||
{/each}
|
||||
|
|
@ -76,40 +77,40 @@
|
|||
|
||||
{#if showForm}
|
||||
<Card>
|
||||
<CardHeader><CardTitle>New lab result</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["labResults_newTitle"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form method="POST" action="?/create" use:enhance class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<Label for="collected_at">Date *</Label>
|
||||
<Label for="collected_at">{m["labResults_date"]()}</Label>
|
||||
<Input id="collected_at" name="collected_at" type="date" required />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="test_code">LOINC code * <span class="text-xs text-muted-foreground">(e.g. 718-7)</span></Label>
|
||||
<Label for="test_code">{m["labResults_loincCode"]()} <span class="text-xs text-muted-foreground">(e.g. 718-7)</span></Label>
|
||||
<Input id="test_code" name="test_code" required placeholder="718-7" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="test_name_original">Test name</Label>
|
||||
<Input id="test_name_original" name="test_name_original" placeholder="e.g. Hemoglobin" />
|
||||
<Label for="test_name_original">{m["labResults_testName"]()}</Label>
|
||||
<Input id="test_name_original" name="test_name_original" placeholder={m["labResults_testNamePlaceholder"]()} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="lab">Lab</Label>
|
||||
<Input id="lab" name="lab" placeholder="e.g. LabCorp" />
|
||||
<Label for="lab">{m["labResults_lab"]()}</Label>
|
||||
<Input id="lab" name="lab" placeholder={m["labResults_labPlaceholder"]()} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="value_num">Value</Label>
|
||||
<Label for="value_num">{m["labResults_value"]()}</Label>
|
||||
<Input id="value_num" name="value_num" type="number" step="any" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="unit_original">Unit</Label>
|
||||
<Input id="unit_original" name="unit_original" placeholder="e.g. g/dL" />
|
||||
<Label for="unit_original">{m["labResults_unit"]()}</Label>
|
||||
<Input id="unit_original" name="unit_original" placeholder={m["labResults_unitPlaceholder"]()} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Flag</Label>
|
||||
<Label>{m["labResults_flag"]()}</Label>
|
||||
<input type="hidden" name="flag" value={selectedFlag} />
|
||||
<Select type="single" value={selectedFlag} onValueChange={(v) => (selectedFlag = v)}>
|
||||
<SelectTrigger>{selectedFlag || 'None'}</SelectTrigger>
|
||||
<SelectTrigger>{selectedFlag || m["labResults_flagNone"]()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">None</SelectItem>
|
||||
<SelectItem value="">{m["labResults_flagNone"]()}</SelectItem>
|
||||
{#each flags as f (f)}
|
||||
<SelectItem value={f}>{f}</SelectItem>
|
||||
{/each}
|
||||
|
|
@ -117,7 +118,7 @@
|
|||
</Select>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button type="submit">Add</Button>
|
||||
<Button type="submit">{m.common_add()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
|
@ -128,12 +129,12 @@
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Test</TableHead>
|
||||
<TableHead>LOINC</TableHead>
|
||||
<TableHead>Value</TableHead>
|
||||
<TableHead>Flag</TableHead>
|
||||
<TableHead>Lab</TableHead>
|
||||
<TableHead>{m["labResults_colDate"]()}</TableHead>
|
||||
<TableHead>{m["labResults_colTest"]()}</TableHead>
|
||||
<TableHead>{m["labResults_colLoinc"]()}</TableHead>
|
||||
<TableHead>{m["labResults_colValue"]()}</TableHead>
|
||||
<TableHead>{m["labResults_colFlag"]()}</TableHead>
|
||||
<TableHead>{m["labResults_colLab"]()}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -165,7 +166,7 @@
|
|||
{:else}
|
||||
<TableRow>
|
||||
<TableCell colspan={6} class="text-center text-muted-foreground py-8">
|
||||
No lab results found.
|
||||
{m["labResults_noResults"]()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
|
|
@ -21,18 +22,26 @@
|
|||
herbal: 'bg-emerald-100 text-emerald-800',
|
||||
other: 'bg-gray-100 text-gray-700'
|
||||
};
|
||||
|
||||
const kindLabels: Record<string, () => string> = {
|
||||
prescription: m["medications_kindPrescription"],
|
||||
otc: m["medications_kindOtc"],
|
||||
supplement: m["medications_kindSupplement"],
|
||||
herbal: m["medications_kindHerbal"],
|
||||
other: m["medications_kindOther"]
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Medications — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m.medications_title()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Medications</h2>
|
||||
<p class="text-muted-foreground">{data.medications.length} entries</p>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{m.medications_title()}</h2>
|
||||
<p class="text-muted-foreground">{m.medications_count({ count: data.medications.length })}</p>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => (showForm = !showForm)}>
|
||||
{showForm ? 'Cancel' : '+ Add medication'}
|
||||
{showForm ? m.common_cancel() : m["medications_addNew"]()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -40,40 +49,40 @@
|
|||
<div class="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">{form.error}</div>
|
||||
{/if}
|
||||
{#if form?.created}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Medication added.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m.medications_added()}</div>
|
||||
{/if}
|
||||
|
||||
{#if showForm}
|
||||
<Card>
|
||||
<CardHeader><CardTitle>New medication</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["medications_newTitle"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form method="POST" action="?/create" use:enhance class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1 col-span-2">
|
||||
<Label>Kind</Label>
|
||||
<Label>{m.medications_kind()}</Label>
|
||||
<input type="hidden" name="kind" value={kind} />
|
||||
<Select type="single" value={kind} onValueChange={(v) => (kind = v)}>
|
||||
<SelectTrigger>{kind}</SelectTrigger>
|
||||
<SelectTrigger>{kindLabels[kind]?.() ?? kind}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each kinds as k (k)}
|
||||
<SelectItem value={k}>{k}</SelectItem>
|
||||
<SelectItem value={k}>{kindLabels[k]?.() ?? k}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="product_name">Product name *</Label>
|
||||
<Input id="product_name" name="product_name" required placeholder="e.g. Vitamin D3" />
|
||||
<Label for="product_name">{m["medications_productName"]()}</Label>
|
||||
<Input id="product_name" name="product_name" required placeholder={m["medications_productNamePlaceholder"]()} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="active_substance">Active substance</Label>
|
||||
<Input id="active_substance" name="active_substance" placeholder="e.g. cholecalciferol" />
|
||||
<Label for="active_substance">{m["medications_activeSubstance"]()}</Label>
|
||||
<Input id="active_substance" name="active_substance" placeholder={m["medications_activeSubstancePlaceholder"]()} />
|
||||
</div>
|
||||
<div class="space-y-1 col-span-2">
|
||||
<Label for="notes">Notes</Label>
|
||||
<Label for="notes">{m.medications_notes()}</Label>
|
||||
<Input id="notes" name="notes" />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<Button type="submit">Add</Button>
|
||||
<Button type="submit">{m.common_add()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
|
@ -86,21 +95,21 @@
|
|||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="rounded-full px-2 py-0.5 text-xs font-medium {kindColors[med.kind] ?? ''}">
|
||||
{med.kind}
|
||||
{kindLabels[med.kind]?.() ?? med.kind}
|
||||
</span>
|
||||
<span class="font-medium">{med.product_name}</span>
|
||||
{#if med.active_substance}
|
||||
<span class="text-sm text-muted-foreground">{med.active_substance}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<Badge variant="secondary">{med.usage_history.length} usages</Badge>
|
||||
<Badge variant="secondary">{m.medications_usages({ count: med.usage_history.length })}</Badge>
|
||||
</div>
|
||||
{#if med.notes}
|
||||
<p class="mt-1 text-sm text-muted-foreground">{med.notes}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">No medications recorded.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["medications_noMedications"]()}</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import type { Product } from '$lib/types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import {
|
||||
|
|
@ -54,15 +55,15 @@
|
|||
const totalCount = $derived(groupedProducts.reduce((s, [, arr]) => s + arr.length, 0));
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Products — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m.products_title()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Products</h2>
|
||||
<p class="text-muted-foreground">{totalCount} products</p>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{m.products_title()}</h2>
|
||||
<p class="text-muted-foreground">{m.products_count({ count: totalCount })}</p>
|
||||
</div>
|
||||
<Button href="/products/new">+ Add product</Button>
|
||||
<Button href="/products/new">{m["products_addNew"]()}</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
|
|
@ -72,7 +73,7 @@
|
|||
size="sm"
|
||||
onclick={() => ownershipFilter = f}
|
||||
>
|
||||
{f === 'all' ? 'All' : f === 'owned' ? 'Owned' : 'Not owned'}
|
||||
{f === 'all' ? m["products_filterAll"]() : f === 'owned' ? m["products_filterOwned"]() : m["products_filterUnowned"]()}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -81,17 +82,17 @@
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Brand</TableHead>
|
||||
<TableHead>Targets</TableHead>
|
||||
<TableHead>Time</TableHead>
|
||||
<TableHead>{m["products_colName"]()}</TableHead>
|
||||
<TableHead>{m["products_colBrand"]()}</TableHead>
|
||||
<TableHead>{m["products_colTargets"]()}</TableHead>
|
||||
<TableHead>{m["products_colTime"]()}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#if totalCount === 0}
|
||||
<TableRow>
|
||||
<TableCell colspan={4} class="text-center text-muted-foreground py-8">
|
||||
No products found.
|
||||
{m["products_noProducts"]()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent } from '$lib/components/ui/card';
|
||||
|
|
@ -20,7 +21,7 @@
|
|||
|
||||
<div class="max-w-2xl space-y-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/products" class="text-sm text-muted-foreground hover:underline">← Products</a>
|
||||
<a href="/products" class="text-sm text-muted-foreground hover:underline">{m["products_backToList"]()}</a>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{product.name}</h2>
|
||||
<Badge variant="outline">{product.category.replace(/_/g, ' ')}</Badge>
|
||||
</div>
|
||||
|
|
@ -29,7 +30,7 @@
|
|||
<div class="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">{form.error}</div>
|
||||
{/if}
|
||||
{#if form?.success}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Saved.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m.common_saved()}</div>
|
||||
{/if}
|
||||
|
||||
<!-- Edit form -->
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
<ProductForm {product} />
|
||||
|
||||
<div class="flex gap-3">
|
||||
<Button type="submit">Save changes</Button>
|
||||
<Button type="submit">{m["products_saveChanges"]()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
@ -46,20 +47,20 @@
|
|||
<!-- Inventory -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Inventory packages ({product.inventory.length})</h3>
|
||||
<h3 class="text-lg font-semibold">{m.inventory_title({ count: product.inventory.length })}</h3>
|
||||
<Button variant="outline" size="sm" onclick={() => (showInventoryForm = !showInventoryForm)}>
|
||||
{showInventoryForm ? 'Cancel' : '+ Add package'}
|
||||
{showInventoryForm ? m.common_cancel() : m["inventory_addPackage"]()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if form?.inventoryAdded}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Package added.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["inventory_packageAdded"]()}</div>
|
||||
{/if}
|
||||
{#if form?.inventoryUpdated}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Package updated.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["inventory_packageUpdated"]()}</div>
|
||||
{/if}
|
||||
{#if form?.inventoryDeleted}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Package deleted.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["inventory_packageDeleted"]()}</div>
|
||||
{/if}
|
||||
|
||||
{#if showInventoryForm}
|
||||
|
|
@ -68,34 +69,34 @@
|
|||
<form method="POST" action="?/addInventory" use:enhance class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
<input type="checkbox" id="add_is_opened" name="is_opened" value="true" class="h-4 w-4" />
|
||||
<Label for="add_is_opened">Already opened</Label>
|
||||
<Label for="add_is_opened">{m["inventory_alreadyOpened"]()}</Label>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_opened_at">Opened date</Label>
|
||||
<Label for="add_opened_at">{m["inventory_openedDate"]()}</Label>
|
||||
<Input id="add_opened_at" name="opened_at" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_finished_at">Finished date</Label>
|
||||
<Label for="add_finished_at">{m["inventory_finishedDate"]()}</Label>
|
||||
<Input id="add_finished_at" name="finished_at" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="expiry_date">Expiry date</Label>
|
||||
<Label for="expiry_date">{m["inventory_expiryDate"]()}</Label>
|
||||
<Input id="expiry_date" name="expiry_date" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="current_weight_g">Current weight (g)</Label>
|
||||
<Label for="current_weight_g">{m["inventory_currentWeight"]()}</Label>
|
||||
<Input id="current_weight_g" name="current_weight_g" type="number" min="0" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_last_weighed_at">Last weighed</Label>
|
||||
<Label for="add_last_weighed_at">{m["inventory_lastWeighed"]()}</Label>
|
||||
<Input id="add_last_weighed_at" name="last_weighed_at" type="date" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="notes">Notes</Label>
|
||||
<Label for="notes">{m.inventory_notes()}</Label>
|
||||
<Input id="notes" name="notes" />
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<Button type="submit" size="sm">Add</Button>
|
||||
<Button type="submit" size="sm">{m.common_add()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
|
@ -109,25 +110,25 @@
|
|||
<div class="flex items-center justify-between px-4 py-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={pkg.is_opened ? 'default' : 'secondary'}>
|
||||
{pkg.is_opened ? 'Open' : 'Sealed'}
|
||||
{pkg.is_opened ? m["inventory_badgeOpen"]() : m["inventory_badgeSealed"]()}
|
||||
</Badge>
|
||||
{#if pkg.finished_at}
|
||||
<Badge variant="outline">Finished</Badge>
|
||||
<Badge variant="outline">{m["inventory_badgeFinished"]()}</Badge>
|
||||
{/if}
|
||||
{#if pkg.expiry_date}
|
||||
<span class="text-muted-foreground">Exp: {pkg.expiry_date.slice(0, 10)}</span>
|
||||
<span class="text-muted-foreground">{m.inventory_exp()} {pkg.expiry_date.slice(0, 10)}</span>
|
||||
{/if}
|
||||
{#if pkg.opened_at}
|
||||
<span class="text-muted-foreground">Opened: {pkg.opened_at.slice(0, 10)}</span>
|
||||
<span class="text-muted-foreground">{m.inventory_opened()} {pkg.opened_at.slice(0, 10)}</span>
|
||||
{/if}
|
||||
{#if pkg.finished_at}
|
||||
<span class="text-muted-foreground">Finished: {pkg.finished_at.slice(0, 10)}</span>
|
||||
<span class="text-muted-foreground">{m.inventory_finished()} {pkg.finished_at.slice(0, 10)}</span>
|
||||
{/if}
|
||||
{#if pkg.current_weight_g}
|
||||
<span class="text-muted-foreground">{pkg.current_weight_g}g remaining</span>
|
||||
<span class="text-muted-foreground">{pkg.current_weight_g}g {m.inventory_remaining()}</span>
|
||||
{/if}
|
||||
{#if pkg.last_weighed_at}
|
||||
<span class="text-muted-foreground">Weighed: {pkg.last_weighed_at.slice(0, 10)}</span>
|
||||
<span class="text-muted-foreground">{m.inventory_weighed()} {pkg.last_weighed_at.slice(0, 10)}</span>
|
||||
{/if}
|
||||
{#if pkg.notes}
|
||||
<span class="text-muted-foreground">{pkg.notes}</span>
|
||||
|
|
@ -139,13 +140,13 @@
|
|||
size="sm"
|
||||
onclick={() => (editingInventoryId = editingInventoryId === pkg.id ? null : pkg.id)}
|
||||
>
|
||||
{editingInventoryId === pkg.id ? 'Cancel' : 'Edit'}
|
||||
{editingInventoryId === pkg.id ? m.common_cancel() : m.common_edit()}
|
||||
</Button>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteInventory"
|
||||
use:enhance
|
||||
onsubmit={(e) => { if (!confirm('Delete this package?')) e.preventDefault(); }}
|
||||
onsubmit={(e) => { if (!confirm(m["inventory_confirmDelete"]())) e.preventDefault(); }}
|
||||
>
|
||||
<input type="hidden" name="inventory_id" value={pkg.id} />
|
||||
<Button type="submit" variant="ghost" size="sm" class="text-destructive hover:text-destructive">×</Button>
|
||||
|
|
@ -176,10 +177,10 @@
|
|||
checked={pkg.is_opened}
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<Label for="edit_is_opened_{pkg.id}">Already opened</Label>
|
||||
<Label for="edit_is_opened_{pkg.id}">{m["inventory_alreadyOpened"]()}</Label>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_opened_at_{pkg.id}">Opened date</Label>
|
||||
<Label for="edit_opened_at_{pkg.id}">{m["inventory_openedDate"]()}</Label>
|
||||
<Input
|
||||
id="edit_opened_at_{pkg.id}"
|
||||
name="opened_at"
|
||||
|
|
@ -188,7 +189,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_finished_at_{pkg.id}">Finished date</Label>
|
||||
<Label for="edit_finished_at_{pkg.id}">{m["inventory_finishedDate"]()}</Label>
|
||||
<Input
|
||||
id="edit_finished_at_{pkg.id}"
|
||||
name="finished_at"
|
||||
|
|
@ -197,7 +198,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_expiry_{pkg.id}">Expiry date</Label>
|
||||
<Label for="edit_expiry_{pkg.id}">{m["inventory_expiryDate"]()}</Label>
|
||||
<Input
|
||||
id="edit_expiry_{pkg.id}"
|
||||
name="expiry_date"
|
||||
|
|
@ -206,7 +207,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_weight_{pkg.id}">Current weight (g)</Label>
|
||||
<Label for="edit_weight_{pkg.id}">{m["inventory_currentWeight"]()}</Label>
|
||||
<Input
|
||||
id="edit_weight_{pkg.id}"
|
||||
name="current_weight_g"
|
||||
|
|
@ -216,7 +217,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_last_weighed_{pkg.id}">Last weighed</Label>
|
||||
<Label for="edit_last_weighed_{pkg.id}">{m["inventory_lastWeighed"]()}</Label>
|
||||
<Input
|
||||
id="edit_last_weighed_{pkg.id}"
|
||||
name="last_weighed_at"
|
||||
|
|
@ -225,17 +226,17 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_notes_{pkg.id}">Notes</Label>
|
||||
<Label for="edit_notes_{pkg.id}">{m.inventory_notes()}</Label>
|
||||
<Input id="edit_notes_{pkg.id}" name="notes" value={pkg.notes ?? ''} />
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<Button type="submit" size="sm">Save</Button>
|
||||
<Button type="submit" size="sm">{m.common_save()}</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => (editingInventoryId = null)}
|
||||
>Cancel</Button>
|
||||
>{m.common_cancel()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -244,7 +245,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">No inventory packages.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["products_noInventory"]()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -257,10 +258,10 @@
|
|||
action="?/delete"
|
||||
use:enhance
|
||||
onsubmit={(e) => {
|
||||
if (!confirm('Delete this product?')) e.preventDefault();
|
||||
if (!confirm(m["products_confirmDelete"]())) e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Button type="submit" variant="destructive" size="sm">Delete product</Button>
|
||||
<Button type="submit" variant="destructive" size="sm">{m["products_deleteProduct"]()}</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import ProductForm from '$lib/components/ProductForm.svelte';
|
||||
|
||||
|
|
@ -15,12 +16,12 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<svelte:head><title>New Product — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m["products_newTitle"]()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="max-w-2xl space-y-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/products" class="text-sm text-muted-foreground hover:underline">← Products</a>
|
||||
<h2 class="text-2xl font-bold tracking-tight">New Product</h2>
|
||||
<a href="/products" class="text-sm text-muted-foreground hover:underline">{m["products_backToList"]()}</a>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{m["products_newTitle"]()}</h2>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
|
|
@ -33,8 +34,8 @@
|
|||
<ProductForm />
|
||||
|
||||
<div class="flex gap-3 pb-6">
|
||||
<Button type="submit">Create product</Button>
|
||||
<Button variant="outline" href="/products">Cancel</Button>
|
||||
<Button type="submit">{m["products_createProduct"]()}</Button>
|
||||
<Button variant="outline" href="/products">{m.common_cancel()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
|
||||
|
|
@ -18,17 +19,17 @@
|
|||
const sortedDates = $derived(Object.keys(byDate).sort((a, b) => b.localeCompare(a)));
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Routines — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m.routines_title()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Routines</h2>
|
||||
<p class="text-muted-foreground">{data.routines.length} routines (last 30 days)</p>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{m.routines_title()}</h2>
|
||||
<p class="text-muted-foreground">{m.routines_count({ count: data.routines.length })}</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button href="/routines/suggest" variant="outline">Zaproponuj rutynę AI</Button>
|
||||
<Button href="/routines/new">+ New routine</Button>
|
||||
<Button href="/routines/suggest" variant="outline">{m["routines_suggestAI"]()}</Button>
|
||||
<Button href="/routines/new">{m["routines_addNew"]()}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -47,7 +48,7 @@
|
|||
<Badge variant={routine.part_of_day === 'am' ? 'default' : 'secondary'}>
|
||||
{routine.part_of_day.toUpperCase()}
|
||||
</Badge>
|
||||
<span class="text-sm">{routine.steps?.length ?? 0} steps</span>
|
||||
<span class="text-sm">{routine.steps?.length ?? 0} {m.common_steps()}</span>
|
||||
</div>
|
||||
{#if routine.notes}
|
||||
<span class="text-sm text-muted-foreground truncate max-w-xs">{routine.notes}</span>
|
||||
|
|
@ -59,6 +60,6 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">No routines found.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["routines_noRoutines"]()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
|
|
@ -24,7 +25,7 @@
|
|||
|
||||
<div class="max-w-2xl space-y-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/routines" class="text-sm text-muted-foreground hover:underline">← Routines</a>
|
||||
<a href="/routines" class="text-sm text-muted-foreground hover:underline">{m["routines_backToList"]()}</a>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{routine.routine_date}</h2>
|
||||
<Badge variant={routine.part_of_day === 'am' ? 'default' : 'secondary'}>
|
||||
{routine.part_of_day.toUpperCase()}
|
||||
|
|
@ -42,26 +43,26 @@
|
|||
<!-- Steps -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Steps ({routine.steps.length})</h3>
|
||||
<h3 class="text-lg font-semibold">{m.routines_steps({ count: routine.steps.length })}</h3>
|
||||
<Button variant="outline" size="sm" onclick={() => (showStepForm = !showStepForm)}>
|
||||
{showStepForm ? 'Cancel' : '+ Add step'}
|
||||
{showStepForm ? m.common_cancel() : m["routines_addStep"]()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if showStepForm}
|
||||
<Card>
|
||||
<CardHeader><CardTitle class="text-base">Add step</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle class="text-base">{m["routines_addStepTitle"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form method="POST" action="?/addStep" use:enhance class="space-y-4">
|
||||
<div class="space-y-1">
|
||||
<Label>Product</Label>
|
||||
<Label>{m.routines_product()}</Label>
|
||||
<input type="hidden" name="product_id" value={selectedProductId} />
|
||||
<Select type="single" value={selectedProductId} onValueChange={(v) => (selectedProductId = v)}>
|
||||
<SelectTrigger>
|
||||
{#if selectedProductId}
|
||||
{products.find((p) => p.id === selectedProductId)?.name ?? 'Select product'}
|
||||
{products.find((p) => p.id === selectedProductId)?.name ?? m["routines_selectProduct"]()}
|
||||
{:else}
|
||||
Select product
|
||||
{m["routines_selectProduct"]()}
|
||||
{/if}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -74,15 +75,15 @@
|
|||
<input type="hidden" name="order_index" value={nextOrderIndex} />
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="dose">Dose</Label>
|
||||
<Input id="dose" name="dose" placeholder="e.g. 2 pumps" />
|
||||
<Label for="dose">{m.routines_dose()}</Label>
|
||||
<Input id="dose" name="dose" placeholder={m["routines_dosePlaceholder"]()} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="region">Region</Label>
|
||||
<Input id="region" name="region" placeholder="e.g. face" />
|
||||
<Label for="region">{m.routines_region()}</Label>
|
||||
<Input id="region" name="region" placeholder={m["routines_regionPlaceholder"]()} />
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" size="sm">Add step</Button>
|
||||
<Button type="submit" size="sm">{m["routines_addStepBtn"]()}</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -102,7 +103,7 @@
|
|||
{:else if step.action_type}
|
||||
<p class="text-sm font-medium">{step.action_type.replace(/_/g, ' ')}</p>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">Unknown step</p>
|
||||
<p class="text-sm text-muted-foreground">{m["routines_unknownStep"]()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if step.dose}
|
||||
|
|
@ -119,14 +120,14 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">No steps yet.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["routines_noSteps"]()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<form method="POST" action="?/delete" use:enhance
|
||||
onsubmit={(e) => { if (!confirm('Delete this routine?')) e.preventDefault(); }}>
|
||||
<Button type="submit" variant="destructive" size="sm">Delete routine</Button>
|
||||
onsubmit={(e) => { if (!confirm(m["routines_confirmDelete"]())) e.preventDefault(); }}>
|
||||
<Button type="submit" variant="destructive" size="sm">{m["routines_deleteRoutine"]()}</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent } from '$lib/components/ui/card';
|
||||
|
|
@ -15,12 +16,22 @@
|
|||
let showAddForm = $state(false);
|
||||
let editingId = $state<string | null>(null);
|
||||
|
||||
const DAY_NAMES = ['Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota', 'Niedziela'];
|
||||
const ACTION_LABELS: Record<GroomingAction, string> = {
|
||||
shaving_razor: 'Golenie maszynką',
|
||||
shaving_oneblade: 'Golenie OneBlade',
|
||||
dermarolling: 'Dermarolling'
|
||||
};
|
||||
const DAY_NAMES = $derived([
|
||||
m["grooming_dayMonday"](),
|
||||
m["grooming_dayTuesday"](),
|
||||
m["grooming_dayWednesday"](),
|
||||
m["grooming_dayThursday"](),
|
||||
m["grooming_dayFriday"](),
|
||||
m["grooming_daySaturday"](),
|
||||
m["grooming_daySunday"]()
|
||||
]);
|
||||
|
||||
const ACTION_LABELS = $derived({
|
||||
shaving_razor: m["grooming_actionShavingRazor"](),
|
||||
shaving_oneblade: m["grooming_actionShavingOneblade"](),
|
||||
dermarolling: m["grooming_actionDermarolling"]()
|
||||
} as Record<GroomingAction, string>);
|
||||
|
||||
const ALL_ACTIONS: GroomingAction[] = ['shaving_razor', 'shaving_oneblade', 'dermarolling'];
|
||||
|
||||
const byDay = $derived(
|
||||
|
|
@ -32,16 +43,16 @@
|
|||
);
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Grooming Schedule — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m.grooming_title()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="max-w-2xl space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<a href="/routines" class="text-sm text-muted-foreground hover:underline">← Routines</a>
|
||||
<h2 class="mt-1 text-2xl font-bold tracking-tight">Grooming Schedule</h2>
|
||||
<a href="/routines" class="text-sm text-muted-foreground hover:underline">{m["grooming_backToRoutines"]()}</a>
|
||||
<h2 class="mt-1 text-2xl font-bold tracking-tight">{m.grooming_title()}</h2>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onclick={() => (showAddForm = !showAddForm)}>
|
||||
{showAddForm ? 'Anuluj' : '+ Dodaj wpis'}
|
||||
{showAddForm ? m.common_cancel() : m["grooming_addEntry"]()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -49,13 +60,13 @@
|
|||
<div class="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">{form.error}</div>
|
||||
{/if}
|
||||
{#if form?.created}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Wpis dodany.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["grooming_entryAdded"]()}</div>
|
||||
{/if}
|
||||
{#if form?.updated}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Wpis zaktualizowany.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["grooming_entryUpdated"]()}</div>
|
||||
{/if}
|
||||
{#if form?.deleted}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Wpis usunięty.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["grooming_entryDeleted"]()}</div>
|
||||
{/if}
|
||||
|
||||
<!-- Add form -->
|
||||
|
|
@ -74,7 +85,7 @@
|
|||
class="grid grid-cols-2 gap-4"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_day">Dzień tygodnia</Label>
|
||||
<Label for="add_day">{m["grooming_dayOfWeek"]()}</Label>
|
||||
<select
|
||||
id="add_day"
|
||||
name="day_of_week"
|
||||
|
|
@ -87,7 +98,7 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="add_action">Czynność</Label>
|
||||
<Label for="add_action">{m.grooming_action()}</Label>
|
||||
<select
|
||||
id="add_action"
|
||||
name="action"
|
||||
|
|
@ -100,13 +111,13 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label for="add_notes">Notatki (opcjonalnie)</Label>
|
||||
<Input id="add_notes" name="notes" placeholder="np. co 2 tygodnie" />
|
||||
<Label for="add_notes">{m["grooming_notesOptional"]()}</Label>
|
||||
<Input id="add_notes" name="notes" placeholder={m["grooming_notesPlaceholder"]()} />
|
||||
</div>
|
||||
<div class="col-span-2 flex gap-2">
|
||||
<Button type="submit" size="sm">Dodaj</Button>
|
||||
<Button type="submit" size="sm">{m.common_add()}</Button>
|
||||
<Button type="button" variant="ghost" size="sm" onclick={() => (showAddForm = false)}>
|
||||
Anuluj
|
||||
{m.common_cancel()}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -116,7 +127,7 @@
|
|||
|
||||
<!-- Entries grouped by day -->
|
||||
{#if schedule.length === 0}
|
||||
<p class="text-sm text-muted-foreground">Brak wpisów. Kliknij "+ Dodaj wpis", aby zacząć.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["grooming_noEntries"]()}</p>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
{#each byDay as { name, entries, day } (day)}
|
||||
|
|
@ -138,19 +149,19 @@
|
|||
size="sm"
|
||||
onclick={() => (editingId = editingId === entry.id ? null : entry.id)}
|
||||
>
|
||||
{editingId === entry.id ? 'Anuluj' : 'Edytuj'}
|
||||
{editingId === entry.id ? m.common_cancel() : m.common_edit()}
|
||||
</Button>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/delete"
|
||||
use:enhance
|
||||
onsubmit={(e) => {
|
||||
if (!confirm('Usunąć ten wpis?')) e.preventDefault();
|
||||
if (!confirm(m["grooming_confirmDelete"]())) e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="id" value={entry.id} />
|
||||
<Button variant="ghost" size="sm" type="submit" class="text-destructive hover:text-destructive">
|
||||
Usuń
|
||||
{m.common_delete()}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -172,7 +183,7 @@
|
|||
>
|
||||
<input type="hidden" name="id" value={entry.id} />
|
||||
<div class="space-y-1">
|
||||
<Label>Dzień tygodnia</Label>
|
||||
<Label>{m["grooming_dayOfWeek"]()}</Label>
|
||||
<select
|
||||
name="day_of_week"
|
||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
|
|
@ -183,7 +194,7 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Czynność</Label>
|
||||
<Label>{m.grooming_action()}</Label>
|
||||
<select
|
||||
name="action"
|
||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
|
|
@ -194,18 +205,13 @@
|
|||
</select>
|
||||
</div>
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label>Notatki</Label>
|
||||
<Input name="notes" value={entry.notes ?? ''} placeholder="opcjonalnie" />
|
||||
<Label>{m.inventory_notes()}</Label>
|
||||
<Input name="notes" value={entry.notes ?? ''} placeholder={m["common_optional_notes"]()} />
|
||||
</div>
|
||||
<div class="col-span-2 flex gap-2">
|
||||
<Button type="submit" size="sm">Zapisz</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => (editingId = null)}
|
||||
>
|
||||
Anuluj
|
||||
<Button type="submit" size="sm">{m.common_save()}</Button>
|
||||
<Button type="button" variant="ghost" size="sm" onclick={() => (editingId = null)}>
|
||||
{m.common_cancel()}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
|
|
@ -11,12 +12,12 @@
|
|||
let partOfDay = $state('am');
|
||||
</script>
|
||||
|
||||
<svelte:head><title>New Routine — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m["routines_newTitle"]()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="max-w-md space-y-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/routines" class="text-sm text-muted-foreground hover:underline">← Routines</a>
|
||||
<h2 class="text-2xl font-bold tracking-tight">New Routine</h2>
|
||||
<a href="/routines" class="text-sm text-muted-foreground hover:underline">{m["routines_backToList"]()}</a>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{m["routines_newTitle"]()}</h2>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
|
|
@ -24,16 +25,16 @@
|
|||
{/if}
|
||||
|
||||
<Card>
|
||||
<CardHeader><CardTitle>Routine details</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["routines_detailsTitle"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form method="POST" use:enhance class="space-y-5">
|
||||
<div class="space-y-2">
|
||||
<Label for="routine_date">Date *</Label>
|
||||
<Label for="routine_date">{m.routines_date()}</Label>
|
||||
<Input id="routine_date" name="routine_date" type="date" value={data.today} required />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>AM or PM *</Label>
|
||||
<Label>{m["routines_amOrPm"]()}</Label>
|
||||
<input type="hidden" name="part_of_day" value={partOfDay} />
|
||||
<Select type="single" value={partOfDay} onValueChange={(v) => (partOfDay = v)}>
|
||||
<SelectTrigger>{partOfDay.toUpperCase()}</SelectTrigger>
|
||||
|
|
@ -45,13 +46,13 @@
|
|||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="notes">Notes</Label>
|
||||
<Input id="notes" name="notes" placeholder="Optional notes" />
|
||||
<Label for="notes">{m.routines_notes()}</Label>
|
||||
<Input id="notes" name="notes" placeholder={m["routines_notesPlaceholder"]()} />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-2">
|
||||
<Button type="submit">Create routine</Button>
|
||||
<Button variant="outline" href="/routines">Cancel</Button>
|
||||
<Button type="submit">{m["routines_createRoutine"]()}</Button>
|
||||
<Button variant="outline" href="/routines">{m.common_cancel()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import type { BatchSuggestion, RoutineSuggestion, SuggestedStep } from '$lib/types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
return `${p.brand} ${p.name}`;
|
||||
}
|
||||
if (step.action_type) return step.action_type.replace(/_/g, ' ');
|
||||
return step.action_notes ?? 'Unknown step';
|
||||
return step.action_notes ?? m.common_unknown();
|
||||
}
|
||||
|
||||
function stepMeta(step: SuggestedStep): string {
|
||||
|
|
@ -64,7 +65,7 @@
|
|||
suggestionPod = result.data.part_of_day as string;
|
||||
errorMsg = null;
|
||||
} else if (result.type === 'failure') {
|
||||
errorMsg = (result.data?.error as string) ?? 'Błąd podczas generowania.';
|
||||
errorMsg = (result.data?.error as string) ?? m["suggest_errorDefault"]();
|
||||
}
|
||||
await update({ reset: false });
|
||||
};
|
||||
|
|
@ -81,7 +82,7 @@
|
|||
for (const d of batch.days) expandedDays.add(d.date);
|
||||
errorMsg = null;
|
||||
} else if (result.type === 'failure') {
|
||||
errorMsg = (result.data?.error as string) ?? 'Błąd podczas generowania planu.';
|
||||
errorMsg = (result.data?.error as string) ?? m["suggest_errorBatch"]();
|
||||
}
|
||||
await update({ reset: false });
|
||||
};
|
||||
|
|
@ -92,19 +93,19 @@
|
|||
return async ({ result, update }: { result: { type: string; data?: Record<string, unknown> }; update: (opts?: { reset?: boolean }) => Promise<void> }) => {
|
||||
loadingSave = false;
|
||||
if (result.type === 'failure') {
|
||||
errorMsg = (result.data?.error as string) ?? 'Błąd podczas zapisywania.';
|
||||
errorMsg = (result.data?.error as string) ?? m["suggest_errorSave"]();
|
||||
}
|
||||
await update({ reset: false });
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Zaproponuj rutynę AI — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m.suggest_title()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="max-w-2xl space-y-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/routines" class="text-sm text-muted-foreground hover:underline">← Rutyny</a>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Propozycja rutyny AI</h2>
|
||||
<a href="/routines" class="text-sm text-muted-foreground hover:underline">{m["suggest_backToRoutines"]()}</a>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{m.suggest_title()}</h2>
|
||||
</div>
|
||||
|
||||
{#if errorMsg}
|
||||
|
|
@ -113,41 +114,41 @@
|
|||
|
||||
<Tabs value="single">
|
||||
<TabsList class="w-full">
|
||||
<TabsTrigger value="single" class="flex-1" onclick={() => { errorMsg = null; }}>Jedna rutyna</TabsTrigger>
|
||||
<TabsTrigger value="batch" class="flex-1" onclick={() => { errorMsg = null; }}>Batch / Urlop</TabsTrigger>
|
||||
<TabsTrigger value="single" class="flex-1" onclick={() => { errorMsg = null; }}>{m["suggest_singleTab"]()}</TabsTrigger>
|
||||
<TabsTrigger value="batch" class="flex-1" onclick={() => { errorMsg = null; }}>{m["suggest_batchTab"]()}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- ── Single tab ─────────────────────────────────────────────────── -->
|
||||
<TabsContent value="single" class="space-y-6 pt-4">
|
||||
<Card>
|
||||
<CardHeader><CardTitle class="text-base">Parametry</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle class="text-base">{m["suggest_singleParams"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form method="POST" action="?/suggest" use:enhance={enhanceSingle} class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="single_date">Data</Label>
|
||||
<Label for="single_date">{m.suggest_date()}</Label>
|
||||
<Input id="single_date" name="routine_date" type="date" value={data.today} required />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Pora dnia</Label>
|
||||
<Label>{m["suggest_timeOfDay"]()}</Label>
|
||||
<input type="hidden" name="part_of_day" value={partOfDay} />
|
||||
<Select type="single" value={partOfDay} onValueChange={(v) => (partOfDay = v as 'am' | 'pm')}>
|
||||
<SelectTrigger>{partOfDay.toUpperCase()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="am">AM (rano)</SelectItem>
|
||||
<SelectItem value="pm">PM (wieczór)</SelectItem>
|
||||
<SelectItem value="am">{m["suggest_amMorning"]()}</SelectItem>
|
||||
<SelectItem value="pm">{m["suggest_pmEvening"]()}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="single_notes">Dodatkowy kontekst dla AI <span class="text-muted-foreground text-xs">(opcjonalny)</span></Label>
|
||||
<Label for="single_notes">{m["suggest_contextLabel"]()} <span class="text-muted-foreground text-xs">{m["suggest_contextOptional"]()}</span></Label>
|
||||
<textarea
|
||||
id="single_notes"
|
||||
name="notes"
|
||||
rows="2"
|
||||
placeholder="np. wieczór imprezowy, skupiam się na nawilżeniu..."
|
||||
placeholder={m["suggest_contextPlaceholder"]()}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
|
@ -155,9 +156,9 @@
|
|||
<Button type="submit" disabled={loadingSingle} class="w-full">
|
||||
{#if loadingSingle}
|
||||
<span class="mr-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></span>
|
||||
Generuję…
|
||||
{m.suggest_generating()}
|
||||
{:else}
|
||||
Generuj propozycję
|
||||
{m["suggest_generateBtn"]()}
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
|
|
@ -167,7 +168,7 @@
|
|||
{#if suggestion}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold">Propozycja</h3>
|
||||
<h3 class="text-lg font-semibold">{m["suggest_proposalTitle"]()}</h3>
|
||||
<Badge variant={suggestionPod === 'am' ? 'default' : 'secondary'}>
|
||||
{suggestionPod.toUpperCase()}
|
||||
</Badge>
|
||||
|
|
@ -206,13 +207,13 @@
|
|||
<Button type="submit" disabled={loadingSave}>
|
||||
{#if loadingSave}
|
||||
<span class="mr-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></span>
|
||||
Zapisuję…
|
||||
{m.suggest_saving()}
|
||||
{:else}
|
||||
Zapisz rutynę
|
||||
{m["suggest_saveRoutine"]()}
|
||||
{/if}
|
||||
</Button>
|
||||
<Button variant="outline" type="submit" form="suggest-single-form" disabled={loadingSingle}>
|
||||
Wygeneruj ponownie
|
||||
{m.suggest_regenerate()}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -222,27 +223,27 @@
|
|||
<!-- ── Batch tab ──────────────────────────────────────────────────── -->
|
||||
<TabsContent value="batch" class="space-y-6 pt-4">
|
||||
<Card>
|
||||
<CardHeader><CardTitle class="text-base">Zakres dat</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle class="text-base">{m["suggest_batchRange"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form id="batch-form" method="POST" action="?/suggestBatch" use:enhance={enhanceBatch} class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="from_date">Od</Label>
|
||||
<Label for="from_date">{m["suggest_fromDate"]()}</Label>
|
||||
<Input id="from_date" name="from_date" type="date" value={data.today} required />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="to_date">Do (max 14 dni)</Label>
|
||||
<Label for="to_date">{m["suggest_toDate"]()}</Label>
|
||||
<Input id="to_date" name="to_date" type="date" value={data.today} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="batch_notes">Kontekst / cel wyjazdu <span class="text-muted-foreground text-xs">(opcjonalny)</span></Label>
|
||||
<Label for="batch_notes">{m["suggest_batchContextLabel"]()} <span class="text-muted-foreground text-xs">{m["suggest_contextOptional"]()}</span></Label>
|
||||
<textarea
|
||||
id="batch_notes"
|
||||
name="notes"
|
||||
rows="2"
|
||||
placeholder="np. słoneczna podróż do Włoch, aktywny urlop górski..."
|
||||
placeholder={m["suggest_batchContextPlaceholder"]()}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
|
@ -250,9 +251,9 @@
|
|||
<Button type="submit" disabled={loadingBatch} class="w-full">
|
||||
{#if loadingBatch}
|
||||
<span class="mr-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></span>
|
||||
Generuję plan…
|
||||
{m["suggest_generatingPlan"]()}
|
||||
{:else}
|
||||
Generuj plan
|
||||
{m["suggest_generatePlan"]()}
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
|
|
@ -261,7 +262,7 @@
|
|||
|
||||
{#if batch}
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold">Plan ({batch.days.length} dni)</h3>
|
||||
<h3 class="text-lg font-semibold">{m["suggest_planTitle"]({ count: batch.days.length })}</h3>
|
||||
|
||||
<!-- Overall reasoning -->
|
||||
{#if batch.overall_reasoning}
|
||||
|
|
@ -285,7 +286,7 @@
|
|||
<span class="font-medium text-sm">{day.date}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
AM {day.am_steps.length} kroków · PM {day.pm_steps.length} kroków
|
||||
AM {day.am_steps.length} {m["suggest_amSteps"]()} · PM {day.pm_steps.length} {m["suggest_pmSteps"]()}
|
||||
</span>
|
||||
<span class="text-muted-foreground">{isOpen ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
|
|
@ -301,7 +302,7 @@
|
|||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge>AM</Badge>
|
||||
<span class="text-xs text-muted-foreground">{day.am_steps.length} kroków</span>
|
||||
<span class="text-xs text-muted-foreground">{day.am_steps.length} {m["suggest_amSteps"]()}</span>
|
||||
</div>
|
||||
{#if day.am_steps.length}
|
||||
<div class="space-y-1">
|
||||
|
|
@ -318,7 +319,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-xs text-muted-foreground">Brak kroków AM.</p>
|
||||
<p class="text-xs text-muted-foreground">{m["suggest_noAmSteps"]()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -326,7 +327,7 @@
|
|||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant="secondary">PM</Badge>
|
||||
<span class="text-xs text-muted-foreground">{day.pm_steps.length} kroków</span>
|
||||
<span class="text-xs text-muted-foreground">{day.pm_steps.length} {m["suggest_pmSteps"]()}</span>
|
||||
</div>
|
||||
{#if day.pm_steps.length}
|
||||
<div class="space-y-1">
|
||||
|
|
@ -343,7 +344,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-xs text-muted-foreground">Brak kroków PM.</p>
|
||||
<p class="text-xs text-muted-foreground">{m["suggest_noPmSteps"]()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -358,13 +359,13 @@
|
|||
<Button type="submit" disabled={loadingSave}>
|
||||
{#if loadingSave}
|
||||
<span class="mr-2 inline-block h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></span>
|
||||
Zapisuję…
|
||||
{m.suggest_saving()}
|
||||
{:else}
|
||||
Zapisz wszystkie rutyny
|
||||
{m["suggest_saveAllRoutines"]()}
|
||||
{/if}
|
||||
</Button>
|
||||
<Button variant="outline" type="submit" form="batch-form" disabled={loadingBatch}>
|
||||
Wygeneruj ponownie
|
||||
{m.suggest_regenerate()}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { analyzeSkinPhotos } from '$lib/api';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
|
|
@ -23,6 +24,35 @@
|
|||
poor: 'bg-red-100 text-red-800'
|
||||
};
|
||||
|
||||
const stateLabels: Record<string, () => string> = {
|
||||
excellent: m["skin_stateExcellent"],
|
||||
good: m["skin_stateGood"],
|
||||
fair: m["skin_stateFair"],
|
||||
poor: m["skin_statePoor"]
|
||||
};
|
||||
|
||||
const textureLabels: Record<string, () => string> = {
|
||||
smooth: m["skin_textureSmooth"],
|
||||
rough: m["skin_textureRough"],
|
||||
flaky: m["skin_textureFlaky"],
|
||||
bumpy: m["skin_textureBumpy"]
|
||||
};
|
||||
|
||||
const barrierLabels: Record<string, () => string> = {
|
||||
intact: m["skin_barrierIntact"],
|
||||
mildly_compromised: m["skin_barrierMildly"],
|
||||
compromised: m["skin_barrierCompromised"]
|
||||
};
|
||||
|
||||
const skinTypeLabels: Record<string, () => string> = {
|
||||
dry: m["skin_typeDry"],
|
||||
oily: m["skin_typeOily"],
|
||||
combination: m["skin_typeCombination"],
|
||||
sensitive: m["skin_typeSensitive"],
|
||||
normal: m["skin_typeNormal"],
|
||||
acne_prone: m["skin_typeAcneProne"]
|
||||
};
|
||||
|
||||
let showForm = $state(false);
|
||||
|
||||
// Create form state (bound to inputs so AI can pre-fill)
|
||||
|
|
@ -112,16 +142,16 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Skin — innercontext</title></svelte:head>
|
||||
<svelte:head><title>{m.skin_title()} — innercontext</title></svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold tracking-tight">Skin Snapshots</h2>
|
||||
<p class="text-muted-foreground">{data.snapshots.length} snapshots</p>
|
||||
<h2 class="text-2xl font-bold tracking-tight">{m.skin_title()}</h2>
|
||||
<p class="text-muted-foreground">{m.skin_count({ count: data.snapshots.length })}</p>
|
||||
</div>
|
||||
<Button variant="outline" onclick={() => (showForm = !showForm)}>
|
||||
{showForm ? 'Cancel' : '+ Add snapshot'}
|
||||
{showForm ? m.common_cancel() : m["skin_addNew"]()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -129,13 +159,13 @@
|
|||
<div class="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">{form.error}</div>
|
||||
{/if}
|
||||
{#if form?.created}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Snapshot added.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["skin_snapshotAdded"]()}</div>
|
||||
{/if}
|
||||
{#if form?.updated}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Snapshot updated.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["skin_snapshotUpdated"]()}</div>
|
||||
{/if}
|
||||
{#if form?.deleted}
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">Snapshot deleted.</div>
|
||||
<div class="rounded-md bg-green-50 px-4 py-3 text-sm text-green-700">{m["skin_snapshotDeleted"]()}</div>
|
||||
{/if}
|
||||
|
||||
{#if showForm}
|
||||
|
|
@ -147,14 +177,14 @@
|
|||
class="flex w-full items-center justify-between text-left"
|
||||
onclick={() => (aiPanelOpen = !aiPanelOpen)}
|
||||
>
|
||||
<CardTitle>AI analysis from photos</CardTitle>
|
||||
<CardTitle>{m["skin_aiAnalysisTitle"]()}</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">
|
||||
Upload 1–3 photos of your skin. AI will pre-fill the form fields below.
|
||||
{m["skin_aiUploadText"]()}
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
|
|
@ -180,7 +210,7 @@
|
|||
onclick={analyzePhotos}
|
||||
disabled={aiLoading || !selectedFiles.length}
|
||||
>
|
||||
{aiLoading ? 'Analyzing…' : 'Analyze photos'}
|
||||
{aiLoading ? m.skin_analyzing() : m["skin_analyzePhotos"]()}
|
||||
</Button>
|
||||
</CardContent>
|
||||
{/if}
|
||||
|
|
@ -188,11 +218,11 @@
|
|||
|
||||
<!-- New snapshot form -->
|
||||
<Card>
|
||||
<CardHeader><CardTitle>New skin snapshot</CardTitle></CardHeader>
|
||||
<CardHeader><CardTitle>{m["skin_newSnapshotTitle"]()}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<form method="POST" action="?/create" use:enhance class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<Label for="snapshot_date">Date *</Label>
|
||||
<Label for="snapshot_date">{m.skin_date()}</Label>
|
||||
<Input
|
||||
id="snapshot_date"
|
||||
name="snapshot_date"
|
||||
|
|
@ -202,114 +232,79 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Overall state</Label>
|
||||
<Label>{m["skin_overallState"]()}</Label>
|
||||
<input type="hidden" name="overall_state" value={overallState} />
|
||||
<Select type="single" value={overallState} onValueChange={(v) => (overallState = v)}>
|
||||
<SelectTrigger>{overallState || 'Select'}</SelectTrigger>
|
||||
<SelectTrigger>{overallState ? stateLabels[overallState]?.() ?? overallState : m.common_select()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each states as s (s)}
|
||||
<SelectItem value={s}>{s}</SelectItem>
|
||||
<SelectItem value={s}>{stateLabels[s]?.() ?? s}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Texture</Label>
|
||||
<Label>{m.skin_texture()}</Label>
|
||||
<input type="hidden" name="texture" value={texture} />
|
||||
<Select type="single" value={texture} onValueChange={(v) => (texture = v)}>
|
||||
<SelectTrigger>{texture || 'Select'}</SelectTrigger>
|
||||
<SelectTrigger>{texture ? textureLabels[texture]?.() ?? texture : m.common_select()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each skinTextures as t (t)}
|
||||
<SelectItem value={t}>{t}</SelectItem>
|
||||
<SelectItem value={t}>{textureLabels[t]?.() ?? t}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Skin type</Label>
|
||||
<Label>{m["skin_skinType"]()}</Label>
|
||||
<input type="hidden" name="skin_type" value={skinType} />
|
||||
<Select type="single" value={skinType} onValueChange={(v) => (skinType = v)}>
|
||||
<SelectTrigger>{skinType ? skinType.replace(/_/g, ' ') : 'Select'}</SelectTrigger>
|
||||
<SelectTrigger>{skinType ? skinTypeLabels[skinType]?.() ?? skinType : m.common_select()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each skinTypes as st (st)}
|
||||
<SelectItem value={st}>{st.replace(/_/g, ' ')}</SelectItem>
|
||||
<SelectItem value={st}>{skinTypeLabels[st]?.() ?? st}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Barrier state</Label>
|
||||
<Label>{m["skin_barrierState"]()}</Label>
|
||||
<input type="hidden" name="barrier_state" value={barrierState} />
|
||||
<Select type="single" value={barrierState} onValueChange={(v) => (barrierState = v)}>
|
||||
<SelectTrigger
|
||||
>{barrierState ? barrierState.replace(/_/g, ' ') : 'Select'}</SelectTrigger
|
||||
>
|
||||
<SelectTrigger>{barrierState ? barrierLabels[barrierState]?.() ?? barrierState : m.common_select()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each barrierStates as b (b)}
|
||||
<SelectItem value={b}>{b.replace(/_/g, ' ')}</SelectItem>
|
||||
<SelectItem value={b}>{barrierLabels[b]?.() ?? b}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="hydration_level">Hydration (1–5)</Label>
|
||||
<Input
|
||||
id="hydration_level"
|
||||
name="hydration_level"
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
bind:value={hydrationLevel}
|
||||
/>
|
||||
<Label for="hydration_level">{m.skin_hydration()}</Label>
|
||||
<Input id="hydration_level" name="hydration_level" type="number" min="1" max="5" bind:value={hydrationLevel} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="sensitivity_level">Sensitivity (1–5)</Label>
|
||||
<Input
|
||||
id="sensitivity_level"
|
||||
name="sensitivity_level"
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
bind:value={sensitivityLevel}
|
||||
/>
|
||||
<Label for="sensitivity_level">{m.skin_sensitivity()}</Label>
|
||||
<Input id="sensitivity_level" name="sensitivity_level" type="number" min="1" max="5" bind:value={sensitivityLevel} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="sebum_tzone">Sebum T-zone (1–5)</Label>
|
||||
<Input
|
||||
id="sebum_tzone"
|
||||
name="sebum_tzone"
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
bind:value={sebumTzone}
|
||||
/>
|
||||
<Label for="sebum_tzone">{m["skin_sebumTzone"]()}</Label>
|
||||
<Input id="sebum_tzone" name="sebum_tzone" type="number" min="1" max="5" bind:value={sebumTzone} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="sebum_cheeks">Sebum cheeks (1–5)</Label>
|
||||
<Input
|
||||
id="sebum_cheeks"
|
||||
name="sebum_cheeks"
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
bind:value={sebumCheeks}
|
||||
/>
|
||||
<Label for="sebum_cheeks">{m["skin_sebumCheeks"]()}</Label>
|
||||
<Input id="sebum_cheeks" name="sebum_cheeks" type="number" min="1" max="5" bind:value={sebumCheeks} />
|
||||
</div>
|
||||
<div class="space-y-1 col-span-2">
|
||||
<Label for="active_concerns">Active concerns (comma-separated)</Label>
|
||||
<Input
|
||||
id="active_concerns"
|
||||
name="active_concerns"
|
||||
placeholder="acne, redness, dehydration"
|
||||
bind:value={activeConcernsRaw}
|
||||
/>
|
||||
<Label for="active_concerns">{m["skin_activeConcerns"]()}</Label>
|
||||
<Input id="active_concerns" name="active_concerns" placeholder={m["skin_activeConcernsPlaceholder"]()} bind:value={activeConcernsRaw} />
|
||||
</div>
|
||||
<div class="space-y-1 col-span-2">
|
||||
<Label for="notes">Notes</Label>
|
||||
<Label for="notes">{m.skin_notes()}</Label>
|
||||
<Input id="notes" name="notes" bind:value={notes} />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<Button type="submit">Add snapshot</Button>
|
||||
<Button type="submit">{m["skin_addSnapshot"]()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
|
@ -333,147 +328,84 @@
|
|||
>
|
||||
<input type="hidden" name="id" value={snap.id} />
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_snapshot_date">Date *</Label>
|
||||
<Input
|
||||
id="edit_snapshot_date"
|
||||
name="snapshot_date"
|
||||
type="date"
|
||||
bind:value={editSnapshotDate}
|
||||
required
|
||||
/>
|
||||
<Label for="edit_snapshot_date">{m.skin_date()}</Label>
|
||||
<Input id="edit_snapshot_date" name="snapshot_date" type="date" bind:value={editSnapshotDate} required />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Overall state</Label>
|
||||
<Label>{m["skin_overallState"]()}</Label>
|
||||
<input type="hidden" name="overall_state" value={editOverallState} />
|
||||
<Select
|
||||
type="single"
|
||||
value={editOverallState}
|
||||
onValueChange={(v) => (editOverallState = v)}
|
||||
>
|
||||
<SelectTrigger>{editOverallState || 'Select'}</SelectTrigger>
|
||||
<Select type="single" value={editOverallState} onValueChange={(v) => (editOverallState = v)}>
|
||||
<SelectTrigger>{editOverallState ? stateLabels[editOverallState]?.() ?? editOverallState : m.common_select()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each states as s (s)}
|
||||
<SelectItem value={s}>{s}</SelectItem>
|
||||
<SelectItem value={s}>{stateLabels[s]?.() ?? s}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Texture</Label>
|
||||
<Label>{m.skin_texture()}</Label>
|
||||
<input type="hidden" name="texture" value={editTexture} />
|
||||
<Select
|
||||
type="single"
|
||||
value={editTexture}
|
||||
onValueChange={(v) => (editTexture = v)}
|
||||
>
|
||||
<SelectTrigger>{editTexture || 'Select'}</SelectTrigger>
|
||||
<Select type="single" value={editTexture} onValueChange={(v) => (editTexture = v)}>
|
||||
<SelectTrigger>{editTexture ? textureLabels[editTexture]?.() ?? editTexture : m.common_select()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each skinTextures as t (t)}
|
||||
<SelectItem value={t}>{t}</SelectItem>
|
||||
<SelectItem value={t}>{textureLabels[t]?.() ?? t}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Skin type</Label>
|
||||
<Label>{m["skin_skinType"]()}</Label>
|
||||
<input type="hidden" name="skin_type" value={editSkinType} />
|
||||
<Select
|
||||
type="single"
|
||||
value={editSkinType}
|
||||
onValueChange={(v) => (editSkinType = v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
>{editSkinType ? editSkinType.replace(/_/g, ' ') : 'Select'}</SelectTrigger
|
||||
>
|
||||
<Select type="single" value={editSkinType} onValueChange={(v) => (editSkinType = v)}>
|
||||
<SelectTrigger>{editSkinType ? skinTypeLabels[editSkinType]?.() ?? editSkinType : m.common_select()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each skinTypes as st (st)}
|
||||
<SelectItem value={st}>{st.replace(/_/g, ' ')}</SelectItem>
|
||||
<SelectItem value={st}>{skinTypeLabels[st]?.() ?? st}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label>Barrier state</Label>
|
||||
<Label>{m["skin_barrierState"]()}</Label>
|
||||
<input type="hidden" name="barrier_state" value={editBarrierState} />
|
||||
<Select
|
||||
type="single"
|
||||
value={editBarrierState}
|
||||
onValueChange={(v) => (editBarrierState = v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
>{editBarrierState
|
||||
? editBarrierState.replace(/_/g, ' ')
|
||||
: 'Select'}</SelectTrigger
|
||||
>
|
||||
<Select type="single" value={editBarrierState} onValueChange={(v) => (editBarrierState = v)}>
|
||||
<SelectTrigger>{editBarrierState ? barrierLabels[editBarrierState]?.() ?? editBarrierState : m.common_select()}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each barrierStates as b (b)}
|
||||
<SelectItem value={b}>{b.replace(/_/g, ' ')}</SelectItem>
|
||||
<SelectItem value={b}>{barrierLabels[b]?.() ?? b}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_hydration_level">Hydration (1–5)</Label>
|
||||
<Input
|
||||
id="edit_hydration_level"
|
||||
name="hydration_level"
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
bind:value={editHydrationLevel}
|
||||
/>
|
||||
<Label for="edit_hydration_level">{m.skin_hydration()}</Label>
|
||||
<Input id="edit_hydration_level" name="hydration_level" type="number" min="1" max="5" bind:value={editHydrationLevel} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_sensitivity_level">Sensitivity (1–5)</Label>
|
||||
<Input
|
||||
id="edit_sensitivity_level"
|
||||
name="sensitivity_level"
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
bind:value={editSensitivityLevel}
|
||||
/>
|
||||
<Label for="edit_sensitivity_level">{m.skin_sensitivity()}</Label>
|
||||
<Input id="edit_sensitivity_level" name="sensitivity_level" type="number" min="1" max="5" bind:value={editSensitivityLevel} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_sebum_tzone">Sebum T-zone (1–5)</Label>
|
||||
<Input
|
||||
id="edit_sebum_tzone"
|
||||
name="sebum_tzone"
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
bind:value={editSebumTzone}
|
||||
/>
|
||||
<Label for="edit_sebum_tzone">{m["skin_sebumTzone"]()}</Label>
|
||||
<Input id="edit_sebum_tzone" name="sebum_tzone" type="number" min="1" max="5" bind:value={editSebumTzone} />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="edit_sebum_cheeks">Sebum cheeks (1–5)</Label>
|
||||
<Input
|
||||
id="edit_sebum_cheeks"
|
||||
name="sebum_cheeks"
|
||||
type="number"
|
||||
min="1"
|
||||
max="5"
|
||||
bind:value={editSebumCheeks}
|
||||
/>
|
||||
<Label for="edit_sebum_cheeks">{m["skin_sebumCheeks"]()}</Label>
|
||||
<Input id="edit_sebum_cheeks" name="sebum_cheeks" type="number" min="1" max="5" bind:value={editSebumCheeks} />
|
||||
</div>
|
||||
<div class="space-y-1 col-span-2">
|
||||
<Label for="edit_active_concerns">Active concerns (comma-separated)</Label>
|
||||
<Input
|
||||
id="edit_active_concerns"
|
||||
name="active_concerns"
|
||||
placeholder="acne, redness, dehydration"
|
||||
bind:value={editActiveConcernsRaw}
|
||||
/>
|
||||
<Label for="edit_active_concerns">{m["skin_activeConcerns"]()}</Label>
|
||||
<Input id="edit_active_concerns" name="active_concerns" placeholder={m["skin_activeConcernsPlaceholder"]()} bind:value={editActiveConcernsRaw} />
|
||||
</div>
|
||||
<div class="space-y-1 col-span-2">
|
||||
<Label for="edit_notes">Notes</Label>
|
||||
<Label for="edit_notes">{m.skin_notes()}</Label>
|
||||
<Input id="edit_notes" name="notes" bind:value={editNotes} />
|
||||
</div>
|
||||
<div class="col-span-2 flex gap-2">
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="button" variant="outline" onclick={() => (editingId = null)}
|
||||
>Cancel</Button
|
||||
>
|
||||
<Button type="submit">{m.common_save()}</Button>
|
||||
<Button type="button" variant="outline" onclick={() => (editingId = null)}>{m.common_cancel()}</Button>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
|
|
@ -482,34 +414,20 @@
|
|||
<span class="font-medium">{snap.snapshot_date}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if snap.overall_state}
|
||||
<span
|
||||
class="rounded-full px-2 py-0.5 text-xs font-medium {stateColors[
|
||||
snap.overall_state
|
||||
] ?? ''}"
|
||||
>
|
||||
{snap.overall_state}
|
||||
<span class="rounded-full px-2 py-0.5 text-xs font-medium {stateColors[snap.overall_state] ?? ''}">
|
||||
{stateLabels[snap.overall_state]?.() ?? snap.overall_state}
|
||||
</span>
|
||||
{/if}
|
||||
{#if snap.texture}
|
||||
<Badge variant="secondary">{snap.texture}</Badge>
|
||||
<Badge variant="secondary">{textureLabels[snap.texture]?.() ?? snap.texture}</Badge>
|
||||
{/if}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => startEdit(snap)}
|
||||
class="h-7 px-2 text-xs"
|
||||
>
|
||||
Edit
|
||||
<Button variant="ghost" size="sm" onclick={() => startEdit(snap)} class="h-7 px-2 text-xs">
|
||||
{m.common_edit()}
|
||||
</Button>
|
||||
<form method="POST" action="?/delete" use:enhance>
|
||||
<input type="hidden" name="id" value={snap.id} />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2 text-xs text-destructive hover:text-destructive"
|
||||
>
|
||||
Delete
|
||||
<Button type="submit" variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive hover:text-destructive">
|
||||
{m.common_delete()}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -517,20 +435,20 @@
|
|||
<div class="grid grid-cols-3 gap-3 text-sm mb-3">
|
||||
{#if snap.hydration_level != null}
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Hydration</p>
|
||||
<p class="text-xs text-muted-foreground">{m["skin_hydrationLabel"]()}</p>
|
||||
<p class="font-medium">{snap.hydration_level}/5</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if snap.sensitivity_level != null}
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Sensitivity</p>
|
||||
<p class="text-xs text-muted-foreground">{m["skin_sensitivityLabel"]()}</p>
|
||||
<p class="font-medium">{snap.sensitivity_level}/5</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if snap.barrier_state}
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">Barrier</p>
|
||||
<p class="font-medium">{snap.barrier_state.replace(/_/g, ' ')}</p>
|
||||
<p class="text-xs text-muted-foreground">{m["skin_barrierLabel"]()}</p>
|
||||
<p class="font-medium">{barrierLabels[snap.barrier_state]?.() ?? snap.barrier_state.replace(/_/g, ' ')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -548,7 +466,7 @@
|
|||
</CardContent>
|
||||
</Card>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">No skin snapshots yet.</p>
|
||||
<p class="text-sm text-muted-foreground">{m["skin_noSnapshots"]()}</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { paraglideVitePlugin } from '@inlang/paraglide-js';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()]
|
||||
plugins: [
|
||||
paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide' }),
|
||||
tailwindcss(),
|
||||
sveltekit()
|
||||
]
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue