fix: resolve recipe edit modal issues and improve dark mode visibility
All checks were successful
CI / update (push) Successful in 1m15s

- Migrate TranslationApproval and edit page to Svelte 5 runes ($props, $state, $derived)
- Fix empty modal issue by eagerly initializing editableEnglish from germanData
- Fix modal state isolation by adding language-specific modal IDs (en/de)
- Resolve cross-contamination where English modals opened German ingredient/instruction editors
- Improve button icon visibility in dark mode by setting white fill color
- Replace createEventDispatcher with callback props for Svelte 5 compatibility
This commit is contained in:
2026-01-10 10:47:55 +01:00
parent 1628f8ba23
commit 2c370363f5
5 changed files with 249 additions and 233 deletions

View File

@@ -191,7 +191,7 @@ function editItemFromReference(list_index: number, position: 'before' | 'after',
ingredient_index: "", ingredient_index: "",
}; };
const modal_el = document.querySelector("#edit_ingredient_modal") as HTMLDialogElement; const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`) as HTMLDialogElement;
if (modal_el) { if (modal_el) {
modal_el.showModal(); modal_el.showModal();
} }
@@ -214,7 +214,7 @@ function openAddToReferenceModal(list_index: number, position: 'before' | 'after
list_index: "", list_index: "",
ingredient_index: "", ingredient_index: "",
}; };
const modal_el = document.querySelector("#edit_ingredient_modal") as HTMLDialogElement; const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`) as HTMLDialogElement;
if (modal_el) { if (modal_el) {
modal_el.showModal(); modal_el.showModal();
} }
@@ -231,12 +231,12 @@ function get_sublist_index(sublist_name, list){
export function show_modal_edit_subheading_ingredient(list_index){ export function show_modal_edit_subheading_ingredient(list_index){
edit_heading.name = ingredients[list_index].name edit_heading.name = ingredients[list_index].name
edit_heading.list_index = list_index edit_heading.list_index = list_index
const el = document.querySelector('#edit_subheading_ingredient_modal') const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`)
el.showModal() el.showModal()
} }
export function edit_subheading_and_close_modal(){ export function edit_subheading_and_close_modal(){
ingredients[edit_heading.list_index].name = edit_heading.name ingredients[edit_heading.list_index].name = edit_heading.name
const el = document.querySelector('#edit_subheading_ingredient_modal') const el = document.querySelector(`#edit_subheading_ingredient_modal-${lang}`)
el.close() el.close()
} }
@@ -286,7 +286,7 @@ export function show_modal_edit_ingredient(list_index, ingredient_index){
edit_ingredient.list_index = list_index edit_ingredient.list_index = list_index
edit_ingredient.ingredient_index = ingredient_index edit_ingredient.ingredient_index = ingredient_index
edit_ingredient.sublist = ingredients[list_index].name edit_ingredient.sublist = ingredients[list_index].name
const modal_el = document.querySelector("#edit_ingredient_modal"); const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`);
modal_el.showModal(); modal_el.showModal();
} }
export function edit_ingredient_and_close_modal(){ export function edit_ingredient_and_close_modal(){
@@ -301,7 +301,7 @@ export function edit_ingredient_and_close_modal(){
editing: false, editing: false,
item_index: -1 item_index: -1
}; };
const modal_el = document.querySelector("#edit_ingredient_modal") as HTMLDialogElement; const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`) as HTMLDialogElement;
if (modal_el) { if (modal_el) {
setTimeout(() => modal_el.close(), 0); setTimeout(() => modal_el.close(), 0);
} }
@@ -341,7 +341,7 @@ export function edit_ingredient_and_close_modal(){
} }
ingredients[edit_ingredient.list_index].name = edit_ingredient.sublist ingredients[edit_ingredient.list_index].name = edit_ingredient.sublist
} }
const modal_el = document.querySelector("#edit_ingredient_modal") as HTMLDialogElement; const modal_el = document.querySelector(`#edit_ingredient_modal-${lang}`) as HTMLDialogElement;
if (modal_el) { if (modal_el) {
// Defer closing to next tick to ensure all bindings are updated // Defer closing to next tick to ensure all bindings are updated
setTimeout(() => modal_el.close(), 0); setTimeout(() => modal_el.close(), 0);
@@ -887,7 +887,7 @@ h3{
</button> </button>
</div> </div>
</div> </div>
<dialog id=edit_ingredient_modal oncancel={handleIngredientModalCancel}> <dialog id="edit_ingredient_modal-{lang}" oncancel={handleIngredientModalCancel}>
<h2>{t[lang].editIngredient}</h2> <h2>{t[lang].editIngredient}</h2>
<div class=adder> <div class=adder>
<input class=category type="text" bind:value={edit_ingredient.sublist} placeholder={t[lang].categoryOptional}> <input class=category type="text" bind:value={edit_ingredient.sublist} placeholder={t[lang].categoryOptional}>
@@ -902,7 +902,7 @@ h3{
</div> </div>
</dialog> </dialog>
<dialog id=edit_subheading_ingredient_modal> <dialog id="edit_subheading_ingredient_modal-{lang}">
<h2>{t[lang].renameCategory}</h2> <h2>{t[lang].renameCategory}</h2>
<div class=heading_wrapper> <div class=heading_wrapper>
<input class=heading type="text" bind:value={edit_heading.name} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} > <input class=heading type="text" bind:value={edit_heading.name} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} >

View File

@@ -186,7 +186,7 @@ function editStepFromReference(list_index: number, position: 'before' | 'after',
step_index: 0, step_index: 0,
}; };
const modal_el = document.querySelector("#edit_step_modal") as HTMLDialogElement; const modal_el = document.querySelector(`#edit_step_modal-${lang}`) as HTMLDialogElement;
if (modal_el) { if (modal_el) {
modal_el.showModal(); modal_el.showModal();
} }
@@ -207,7 +207,7 @@ function openAddToReferenceModal(list_index: number, position: 'before' | 'after
list_index: 0, list_index: 0,
step_index: 0, step_index: 0,
}; };
const modal_el = document.querySelector("#edit_step_modal") as HTMLDialogElement; const modal_el = document.querySelector(`#edit_step_modal-${lang}`) as HTMLDialogElement;
if (modal_el) { if (modal_el) {
modal_el.showModal(); modal_el.showModal();
} }
@@ -270,7 +270,7 @@ export function show_modal_edit_step(list_index, step_index){
} }
edit_step.list_index = list_index edit_step.list_index = list_index
edit_step.step_index = step_index edit_step.step_index = step_index
const modal_el = document.querySelector("#edit_step_modal"); const modal_el = document.querySelector(`#edit_step_modal-${lang}`);
modal_el.showModal(); modal_el.showModal();
} }
@@ -286,7 +286,7 @@ export function edit_step_and_close_modal(){
editing: false, editing: false,
step_index: -1 step_index: -1
}; };
const modal_el = document.querySelector("#edit_step_modal") as HTMLDialogElement; const modal_el = document.querySelector(`#edit_step_modal-${lang}`) as HTMLDialogElement;
if (modal_el) { if (modal_el) {
setTimeout(() => modal_el.close(), 0); setTimeout(() => modal_el.close(), 0);
} }
@@ -315,7 +315,7 @@ export function edit_step_and_close_modal(){
// Normal edit behavior // Normal edit behavior
instructions[edit_step.list_index].steps[edit_step.step_index] = edit_step.step instructions[edit_step.list_index].steps[edit_step.step_index] = edit_step.step
} }
const modal_el = document.querySelector("#edit_step_modal") as HTMLDialogElement; const modal_el = document.querySelector(`#edit_step_modal-${lang}`) as HTMLDialogElement;
if (modal_el) { if (modal_el) {
// Defer closing to next tick to ensure all bindings are updated // Defer closing to next tick to ensure all bindings are updated
setTimeout(() => modal_el.close(), 0); setTimeout(() => modal_el.close(), 0);
@@ -325,7 +325,7 @@ export function edit_step_and_close_modal(){
export function show_modal_edit_subheading_step(list_index){ export function show_modal_edit_subheading_step(list_index){
edit_heading.name = instructions[list_index].name edit_heading.name = instructions[list_index].name
edit_heading.list_index = list_index edit_heading.list_index = list_index
const el = document.querySelector('#edit_subheading_steps_modal') const el = document.querySelector(`#edit_subheading_steps_modal-${lang}`)
el.showModal() el.showModal()
} }
@@ -942,7 +942,7 @@ h3{
</div> </div>
</div> </div>
<dialog id=edit_step_modal oncancel={handleStepModalCancel}> <dialog id="edit_step_modal-{lang}" oncancel={handleStepModalCancel}>
<h2>{t[lang].editStep}</h2> <h2>{t[lang].editStep}</h2>
<div class=adder> <div class=adder>
<input class=category type="text" bind:value={edit_step.name} placeholder={t[lang].subcategoryOptional} onkeydown={(event) => do_on_key(event, 'Enter', false , edit_step_and_close_modal)}> <input class=category type="text" bind:value={edit_step.name} placeholder={t[lang].subcategoryOptional} onkeydown={(event) => do_on_key(event, 'Enter', false , edit_step_and_close_modal)}>
@@ -955,7 +955,7 @@ h3{
</div> </div>
</dialog> </dialog>
<dialog id=edit_subheading_steps_modal> <dialog id="edit_subheading_steps_modal-{lang}">
<h2>{t[lang].renameCategory}</h2> <h2>{t[lang].renameCategory}</h2>
<div class=heading_wrapper> <div class=heading_wrapper>
<input class="heading" type="text" bind:value={edit_heading.name} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_steps_and_close_modal)}> <input class="heading" type="text" bind:value={edit_heading.name} onkeydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_steps_and_close_modal)}>

View File

@@ -1,22 +1,38 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { TranslatedRecipeType } from '$types/types'; import type { TranslatedRecipeType } from '$types/types';
import TranslationFieldComparison from './TranslationFieldComparison.svelte'; import TranslationFieldComparison from './TranslationFieldComparison.svelte';
import CreateIngredientList from './CreateIngredientList.svelte'; import CreateIngredientList from './CreateIngredientList.svelte';
import CreateStepList from './CreateStepList.svelte'; import CreateStepList from './CreateStepList.svelte';
import GenerateAltTextButton from './GenerateAltTextButton.svelte'; import GenerateAltTextButton from './GenerateAltTextButton.svelte';
export let germanData: any; interface Props {
export let englishData: TranslatedRecipeType | null = null; germanData: any;
export let changedFields: string[] = []; englishData?: TranslatedRecipeType | null;
export let isEditMode: boolean = false; // true when editing existing recipe changedFields?: string[];
isEditMode?: boolean;
oldRecipeData?: any;
onapproved?: (event: CustomEvent) => void;
onskipped?: () => void;
oncancelled?: () => void;
onforceFullRetranslation?: () => void;
}
const dispatch = createEventDispatcher(); let {
germanData,
englishData = null,
changedFields = [],
isEditMode = false,
oldRecipeData = null,
onapproved,
onskipped,
oncancelled,
onforceFullRetranslation
}: Props = $props();
type TranslationState = 'idle' | 'translating' | 'preview' | 'approved' | 'error'; type TranslationState = 'idle' | 'translating' | 'preview' | 'approved' | 'error';
let translationState: TranslationState = englishData ? 'preview' : 'idle'; let translationState = $state<TranslationState>(englishData ? 'preview' : 'idle');
let errorMessage: string = ''; let errorMessage = $state('');
let validationErrors: string[] = []; let validationErrors = $state<string[]>([]);
// Helper function to initialize images array for English translation // Helper function to initialize images array for English translation
function initializeImagesArray(germanImages: any[]): any[] { function initializeImagesArray(germanImages: any[]): any[] {
@@ -27,22 +43,26 @@
})); }));
} }
// Editable English data (clone of englishData or initialized from germanData) // Eagerly initialize editableEnglish from germanData if no English translation exists
let editableEnglish: any = englishData ? { let editableEnglish = $state<any>(
...englishData, englishData ? {
// Ensure images array exists and matches German images length ...englishData,
images: englishData.images || initializeImagesArray(germanData.images || []) images: englishData.images || initializeImagesArray(germanData.images || [])
} : null; } : {
...germanData,
// Store old recipe data for granular change detection translationStatus: 'pending',
export let oldRecipeData: any = null; ingredients: JSON.parse(JSON.stringify(germanData.ingredients || [])),
instructions: JSON.parse(JSON.stringify(germanData.instructions || [])),
images: initializeImagesArray(germanData.images || [])
}
);
// Translation metadata (tracks which items were re-translated) // Translation metadata (tracks which items were re-translated)
let translationMetadata: any = null; let translationMetadata = $state<any>(null);
// Track base recipes that need translation // Track base recipes that need translation
let untranslatedBaseRecipes: { shortName: string, name: string }[] = []; let untranslatedBaseRecipes = $state<{ shortName: string, name: string }[]>([]);
let checkingBaseRecipes = false; let checkingBaseRecipes = $state(false);
// Sync base recipe references from German to English // Sync base recipe references from German to English
async function syncBaseRecipeReferences() { async function syncBaseRecipeReferences() {
@@ -74,17 +94,8 @@
} }
}); });
// If no base recipes in German, just initialize editableEnglish from German data if needed // If no base recipes in German, we're done
if (germanBaseRecipeShortNames.size === 0) { if (germanBaseRecipeShortNames.size === 0) {
if (!editableEnglish) {
editableEnglish = {
...germanData,
translationStatus: 'pending',
ingredients: JSON.parse(JSON.stringify(germanData.ingredients || [])),
instructions: JSON.parse(JSON.stringify(germanData.instructions || [])),
images: editableEnglish?.images || initializeImagesArray(germanData.images || [])
};
}
checkingBaseRecipes = false; checkingBaseRecipes = false;
return; return;
} }
@@ -120,96 +131,68 @@
return; return;
} }
// Now merge German base recipe references into editableEnglish // Merge German base recipe references into editableEnglish
// This works for both new translations and existing translations // Update ingredients with English base recipe names
editableEnglish.ingredients = germanData.ingredients.map((germanIng: any, index: number) => {
if (germanIng.type === 'reference' && germanIng.baseRecipeRef) {
const shortName = getShortName(germanIng.baseRecipeRef);
const translation = baseRecipeTranslations.get(shortName);
const englishIng = editableEnglish.ingredients[index];
if (!editableEnglish) { // If English already has this reference at same position, keep it
// No existing English translation - create from German structure with English base recipe names if (englishIng?.type === 'reference' && englishIng.baseRecipeRef === germanIng.baseRecipeRef) {
editableEnglish = { return englishIng;
...germanData, }
translationStatus: 'pending',
ingredients: JSON.parse(JSON.stringify(germanData.ingredients || [])).map((ing: any) => {
if (ing.type === 'reference' && ing.baseRecipeRef) {
const shortName = getShortName(ing.baseRecipeRef);
const translation = baseRecipeTranslations.get(shortName);
return translation ? { ...ing, name: translation.enName } : ing;
}
return ing;
}),
instructions: JSON.parse(JSON.stringify(germanData.instructions || [])).map((inst: any) => {
if (inst.type === 'reference' && inst.baseRecipeRef) {
const shortName = getShortName(inst.baseRecipeRef);
const translation = baseRecipeTranslations.get(shortName);
return translation ? { ...inst, name: translation.enName } : inst;
}
return inst;
}),
images: initializeImagesArray(germanData.images || [])
};
} else {
// Existing English translation - merge German structure with English translations
// Use German structure but keep English translations where they exist
editableEnglish = {
...editableEnglish,
ingredients: germanData.ingredients.map((germanIng: any, index: number) => {
if (germanIng.type === 'reference' && germanIng.baseRecipeRef) {
// This is a base recipe reference - use English base recipe name
const shortName = getShortName(germanIng.baseRecipeRef);
const translation = baseRecipeTranslations.get(shortName);
const englishIng = editableEnglish.ingredients[index];
// If English already has this reference at same position, keep it // Otherwise, create new reference with English base recipe name
if (englishIng?.type === 'reference' && englishIng.baseRecipeRef === germanIng.baseRecipeRef) { return translation ? { ...germanIng, name: translation.enName } : germanIng;
return englishIng; } else {
} // Regular ingredient section - keep existing English translation if it exists
const englishIng = editableEnglish.ingredients[index];
if (englishIng && englishIng.type !== 'reference') {
return englishIng;
}
// If no English translation exists, use German structure (will be translated later)
return germanIng;
}
});
// Otherwise, create new reference with English base recipe name // Update instructions with English base recipe names
return translation ? { ...germanIng, name: translation.enName } : germanIng; editableEnglish.instructions = germanData.instructions.map((germanInst: any, index: number) => {
} else { if (germanInst.type === 'reference' && germanInst.baseRecipeRef) {
// Regular ingredient section - keep existing English translation if it exists const shortName = getShortName(germanInst.baseRecipeRef);
const englishIng = editableEnglish.ingredients[index]; const translation = baseRecipeTranslations.get(shortName);
if (englishIng && englishIng.type !== 'reference') { const englishInst = editableEnglish.instructions[index];
return englishIng;
}
// If no English translation exists, use German structure (will be translated later)
return germanIng;
}
}),
instructions: germanData.instructions.map((germanInst: any, index: number) => {
if (germanInst.type === 'reference' && germanInst.baseRecipeRef) {
// This is a base recipe reference - use English base recipe name
const shortName = getShortName(germanInst.baseRecipeRef);
const translation = baseRecipeTranslations.get(shortName);
const englishInst = editableEnglish.instructions[index];
// If English already has this reference at same position, keep it // If English already has this reference at same position, keep it
if (englishInst?.type === 'reference' && englishInst.baseRecipeRef === germanInst.baseRecipeRef) { if (englishInst?.type === 'reference' && englishInst.baseRecipeRef === germanInst.baseRecipeRef) {
return englishInst; return englishInst;
} }
// Otherwise, create new reference with English base recipe name // Otherwise, create new reference with English base recipe name
return translation ? { ...germanInst, name: translation.enName } : germanInst; return translation ? { ...germanInst, name: translation.enName } : germanInst;
} else { } else {
// Regular instruction section - keep existing English translation if it exists // Regular instruction section - keep existing English translation if it exists
const englishInst = editableEnglish.instructions[index]; const englishInst = editableEnglish.instructions[index];
if (englishInst && englishInst.type !== 'reference') { if (englishInst && englishInst.type !== 'reference') {
return englishInst; return englishInst;
} }
// If no English translation exists, use German structure (will be translated later) // If no English translation exists, use German structure (will be translated later)
return germanInst; return germanInst;
} }
}), });
// Sync images array - keep existing English alt/caption or initialize empty
images: germanData.images?.map((germanImg: any, index: number) => { // Sync images array - keep existing English alt/caption or initialize empty
const existingEnImage = editableEnglish.images?.[index]; editableEnglish.images = germanData.images?.map((germanImg: any, index: number) => {
return existingEnImage || { alt: '', caption: '' }; const existingEnImage = editableEnglish.images?.[index];
}) || [] return existingEnImage || { alt: '', caption: '' };
}; }) || [];
}
} }
// Always sync base recipe references when component mounts // Run base recipe check in background (non-blocking)
syncBaseRecipeReferences(); $effect(() => {
syncBaseRecipeReferences();
});
// Handle auto-translate button click // Handle auto-translate button click
async function handleAutoTranslate() { async function handleAutoTranslate() {
@@ -251,9 +234,6 @@
translationState = 'preview'; translationState = 'preview';
// Notify parent component
dispatch('translated', { translatedRecipe: editableEnglish });
} catch (error: any) { } catch (error: any) {
console.error('Translation error:', error); console.error('Translation error:', error);
translationState = 'error'; translationState = 'error';
@@ -262,30 +242,26 @@
} }
// Handle field changes from TranslationFieldComparison components // Handle field changes from TranslationFieldComparison components
function handleFieldChange(event: CustomEvent) { function handleFieldChange(value: string, field: string) {
const { field, value } = event.detail; // Special handling for tags (comma-separated string -> array)
if (editableEnglish) { if (field === 'tags') {
// Special handling for tags (comma-separated string -> array) editableEnglish[field] = value.split(',').map((t: string) => t.trim()).filter((t: string) => t);
if (field === 'tags') { }
editableEnglish[field] = value.split(',').map((t: string) => t.trim()).filter((t: string) => t); // Handle nested fields (e.g., baking.temperature, fermentation.bulk)
else if (field.includes('.')) {
const [parent, child] = field.split('.');
if (!editableEnglish[parent]) {
editableEnglish[parent] = {};
} }
// Handle nested fields (e.g., baking.temperature, fermentation.bulk) editableEnglish[parent][child] = value;
else if (field.includes('.')) { } else {
const [parent, child] = field.split('.'); editableEnglish[field] = value;
if (!editableEnglish[parent]) {
editableEnglish[parent] = {};
}
editableEnglish[parent][child] = value;
} else {
editableEnglish[field] = value;
}
editableEnglish = editableEnglish; // Trigger reactivity
} }
} }
// Create add_info object for CreateStepList that references editableEnglish properties // Create add_info object for CreateStepList that references editableEnglish properties
// This allows CreateStepList to modify the values directly // This allows CreateStepList to modify the values directly
$: englishAddInfo = editableEnglish ? { let englishAddInfo = $derived({
get preparation() { return editableEnglish.preparation || ''; }, get preparation() { return editableEnglish.preparation || ''; },
set preparation(value) { editableEnglish.preparation = value; }, set preparation(value) { editableEnglish.preparation = value; },
fermentation: { fermentation: {
@@ -321,7 +297,7 @@
set total_time(value) { editableEnglish.total_time = value; }, set total_time(value) { editableEnglish.total_time = value; },
get cooking() { return editableEnglish.cooking || ''; }, get cooking() { return editableEnglish.cooking || ''; },
set cooking(value) { editableEnglish.cooking = value; }, set cooking(value) { editableEnglish.cooking = value; },
} : null; });
// Handle approval // Handle approval
function handleApprove() { function handleApprove() {
@@ -343,31 +319,39 @@
} }
translationState = 'approved'; translationState = 'approved';
dispatch('approved', { onapproved?.(new CustomEvent('approved', {
translatedRecipe: { detail: {
...editableEnglish, translatedRecipe: {
translationStatus: 'approved', ...editableEnglish,
lastTranslated: new Date(), translationStatus: 'approved',
changedFields: [], lastTranslated: new Date(),
changedFields: [],
}
} }
}); }));
} }
// Handle skip translation // Handle skip translation
function handleSkip() { function handleSkip() {
dispatch('skipped'); onskipped?.();
} }
// Handle cancel // Handle cancel
function handleCancel() { function handleCancel() {
translationState = 'idle'; translationState = 'idle';
editableEnglish = null; editableEnglish = {
dispatch('cancelled'); ...germanData,
translationStatus: 'pending',
ingredients: JSON.parse(JSON.stringify(germanData.ingredients || [])),
instructions: JSON.parse(JSON.stringify(germanData.instructions || [])),
images: initializeImagesArray(germanData.images || [])
};
oncancelled?.();
} }
// Handle force full retranslation // Handle force full retranslation
function handleForceFullRetranslation() { function handleForceFullRetranslation() {
dispatch('forceFullRetranslation'); onforceFullRetranslation?.();
} }
// Get status badge color // Get status badge color
@@ -463,6 +447,16 @@
} }
} }
/* Fix button icon visibility in dark mode */
@media (prefers-color-scheme: dark) {
.list-wrapper :global(svg) {
fill: white !important;
}
.list-wrapper :global(.button_arrow) {
fill: var(--nord4) !important;
}
}
.column-header { .column-header {
font-weight: 700; font-weight: 700;
font-size: 1.1rem; font-size: 1.1rem;
@@ -687,7 +681,7 @@ button:disabled {
englishValue={editableEnglish?.name || ''} englishValue={editableEnglish?.name || ''}
fieldName="name" fieldName="name"
readonly={false} readonly={false}
on:change={handleFieldChange} onchange={(value) => handleFieldChange(value, 'name')}
/> />
</div> </div>
@@ -698,7 +692,7 @@ button:disabled {
englishValue={editableEnglish?.short_name || ''} englishValue={editableEnglish?.short_name || ''}
fieldName="short_name" fieldName="short_name"
readonly={false} readonly={false}
on:change={handleFieldChange} onchange={(value) => handleFieldChange(value, 'short_name')}
/> />
</div> </div>
@@ -710,7 +704,7 @@ button:disabled {
fieldName="description" fieldName="description"
readonly={false} readonly={false}
multiline={true} multiline={true}
on:change={handleFieldChange} onchange={(value) => handleFieldChange(value, 'description')}
/> />
</div> </div>
@@ -721,7 +715,7 @@ button:disabled {
englishValue={editableEnglish?.category || ''} englishValue={editableEnglish?.category || ''}
fieldName="category" fieldName="category"
readonly={false} readonly={false}
on:change={handleFieldChange} onchange={(value) => handleFieldChange(value, 'category')}
/> />
</div> </div>
@@ -733,7 +727,7 @@ button:disabled {
englishValue={editableEnglish.tags.join(', ')} englishValue={editableEnglish.tags.join(', ')}
fieldName="tags" fieldName="tags"
readonly={false} readonly={false}
on:change={handleFieldChange} onchange={(value) => handleFieldChange(value, 'tags')}
/> />
</div> </div>
{/if} {/if}
@@ -747,7 +741,7 @@ button:disabled {
fieldName="preamble" fieldName="preamble"
readonly={false} readonly={false}
multiline={true} multiline={true}
on:change={handleFieldChange} onchange={(value) => handleFieldChange(value, 'preamble')}
/> />
</div> </div>
{/if} {/if}
@@ -761,7 +755,7 @@ button:disabled {
fieldName="note" fieldName="note"
readonly={false} readonly={false}
multiline={true} multiline={true}
on:change={handleFieldChange} onchange={(value) => handleFieldChange(value, 'note')}
/> />
</div> </div>
{/if} {/if}
@@ -774,7 +768,7 @@ button:disabled {
englishValue={editableEnglish.portions} englishValue={editableEnglish.portions}
fieldName="portions" fieldName="portions"
readonly={false} readonly={false}
on:change={handleFieldChange} onchange={(value) => handleFieldChange(value, 'portions')}
/> />
</div> </div>
{/if} {/if}
@@ -875,7 +869,7 @@ button:disabled {
fieldName="addendum" fieldName="addendum"
readonly={false} readonly={false}
multiline={true} multiline={true}
on:change={handleFieldChange} onchange={(value) => handleFieldChange(value, 'addendum')}
/> />
</div> </div>
{/if} {/if}

View File

@@ -1,21 +1,27 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; interface Props {
label: string;
germanValue: string;
englishValue: string;
fieldName: string;
readonly?: boolean;
multiline?: boolean;
onchange?: (value: string) => void;
}
export let label: string; let {
export let germanValue: string; label,
export let englishValue: string; germanValue,
export let fieldName: string; englishValue,
export let readonly: boolean = false; fieldName,
export let multiline: boolean = false; readonly = false,
multiline = false,
const dispatch = createEventDispatcher(); onchange
}: Props = $props();
function handleInput(event: Event) { function handleInput(event: Event) {
const target = event.target as HTMLInputElement | HTMLTextAreaElement; const target = event.target as HTMLInputElement | HTMLTextAreaElement;
dispatch('change', { onchange?.(target.value);
field: fieldName,
value: target.value
});
} }
</script> </script>

View File

@@ -8,53 +8,64 @@
import '$lib/css/nordtheme.css' import '$lib/css/nordtheme.css'
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import EditRecipeNote from '$lib/components/EditRecipeNote.svelte'; import EditRecipeNote from '$lib/components/EditRecipeNote.svelte';
import type { PageData } from './$types';
import CardAdd from '$lib/components/CardAdd.svelte';
import CreateIngredientList from '$lib/components/CreateIngredientList.svelte';
import CreateStepList from '$lib/components/CreateStepList.svelte';
import { season } from '$lib/js/season_store';
import { portions } from '$lib/js/portions_store';
import { img } from '$lib/js/img_store';
export let data: PageData; let { data }: { data: PageData } = $props();
let preamble = data.recipe.preamble
let addendum = data.recipe.addendum let preamble = $state(data.recipe.preamble);
let image_preview_url="https://bocken.org/static/rezepte/thumb/" + (data.recipe.images?.[0]?.mediapath || `${data.recipe.short_name}.webp`); let addendum = $state(data.recipe.addendum);
let note = data.recipe.note let image_preview_url = $state("https://bocken.org/static/rezepte/thumb/" + (data.recipe.images?.[0]?.mediapath || `${data.recipe.short_name}.webp`));
let note = $state(data.recipe.note);
// Translation workflow state // Translation workflow state
let showTranslationWorkflow = false; let showTranslationWorkflow = $state(false);
let translationData: any = data.recipe.translations?.en || null; let translationData = $state<any>(data.recipe.translations?.en || null);
let changedFields: string[] = []; let changedFields = $state<string[]>([]);
// Store original recipe data for change detection // Store original recipe data for change detection
const originalRecipe = JSON.parse(JSON.stringify(data.recipe)); const originalRecipe = JSON.parse(JSON.stringify(data.recipe));
import { season } from '$lib/js/season_store'; portions.update(() => data.recipe.portions);
import { portions } from '$lib/js/portions_store'; let portions_local = $state<any>(data.recipe.portions);
$effect(() => {
portions.update(() => data.recipe.portions) portions.subscribe((p) => {
let portions_local portions_local = p;
portions.subscribe((p) => {
portions_local = p
}); });
season.update(() => data.recipe.season)
let season_local
season.subscribe((s) => {
season_local = s
}); });
season.update(() => data.recipe.season);
let season_local = $state<any>(data.recipe.season);
$effect(() => {
season.subscribe((s) => {
season_local = s;
});
});
import { img } from '$lib/js/img_store'; let img_local = $state<string>('');
let img_local img.update(() => '');
img.update(() => "") $effect(() => {
img.subscribe((i) => { img.subscribe((i) => {
img_local = i}); img_local = i;
});
});
let old_short_name = data.recipe.short_name let old_short_name = $state(data.recipe.short_name);
export let card_data ={ let card_data = $state({
icon: data.recipe.icon, icon: data.recipe.icon,
category: data.recipe.category, category: data.recipe.category,
name: data.recipe.name, name: data.recipe.name,
description: data.recipe.description, description: data.recipe.description,
tags: data.recipe.tags, tags: data.recipe.tags,
} });
export let add_info ={
let add_info = $state({
preparation: data.recipe.preparation, preparation: data.recipe.preparation,
fermentation: { fermentation: {
bulk: data.recipe.fermentation.bulk, bulk: data.recipe.fermentation.bulk,
@@ -67,23 +78,17 @@
}, },
total_time: data.recipe.total_time, total_time: data.recipe.total_time,
cooking: data.recipe.cooking, cooking: data.recipe.cooking,
} });
let images = data.recipe.images let images = $state(data.recipe.images);
let short_name = data.recipe.short_name let short_name = $state(data.recipe.short_name);
let datecreated = data.recipe.datecreated let datecreated = $state(data.recipe.datecreated);
let datemodified = new Date() let datemodified = $state(new Date());
let isBaseRecipe = data.recipe.isBaseRecipe || false let isBaseRecipe = $state(data.recipe.isBaseRecipe || false);
import type { PageData } from './$types'; let ingredients = $state(data.recipe.ingredients);
import CardAdd from '$lib/components/CardAdd.svelte'; let instructions = $state(data.recipe.instructions);
import CreateIngredientList from '$lib/components/CreateIngredientList.svelte';
export let ingredients = data.recipe.ingredients
import CreateStepList from '$lib/components/CreateStepList.svelte';
export let instructions = data.recipe.instructions
function get_season(){ function get_season(){
@@ -389,6 +394,17 @@ input:focus-visible
flex-direction: column; flex-direction: column;
} }
} }
/* Fix button icon visibility in dark mode */
@media (prefers-color-scheme: dark) {
.list_wrapper :global(svg) {
fill: white !important;
}
.list_wrapper :global(.button_arrow) {
fill: var(--nord4) !important;
}
}
h1{ h1{
text-align: center; text-align: center;
margin-bottom: 2rem; margin-bottom: 2rem;
@@ -609,10 +625,10 @@ button.action_button{
oldRecipeData={originalRecipe} oldRecipeData={originalRecipe}
{changedFields} {changedFields}
isEditMode={true} isEditMode={true}
on:approved={handleTranslationApproved} onapproved={handleTranslationApproved}
on:skipped={handleTranslationSkipped} onskipped={handleTranslationSkipped}
on:cancelled={handleTranslationCancelled} oncancelled={handleTranslationCancelled}
on:forceFullRetranslation={forceFullRetranslation} onforceFullRetranslation={forceFullRetranslation}
/> />
</div> </div>
{/if} {/if}