fix: resolve recipe edit modal issues and improve dark mode visibility
All checks were successful
CI / update (push) Successful in 1m15s
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:
@@ -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)} >
|
||||||
|
|||||||
@@ -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)}>
|
||||||
|
|||||||
@@ -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,
|
...englishData,
|
||||||
// Ensure images array exists and matches German images length
|
|
||||||
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,40 +131,10 @@
|
|||||||
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 (!editableEnglish) {
|
|
||||||
// No existing English translation - create from German structure with English base recipe names
|
|
||||||
editableEnglish = {
|
|
||||||
...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) {
|
if (germanIng.type === 'reference' && germanIng.baseRecipeRef) {
|
||||||
// This is a base recipe reference - use English base recipe name
|
|
||||||
const shortName = getShortName(germanIng.baseRecipeRef);
|
const shortName = getShortName(germanIng.baseRecipeRef);
|
||||||
const translation = baseRecipeTranslations.get(shortName);
|
const translation = baseRecipeTranslations.get(shortName);
|
||||||
const englishIng = editableEnglish.ingredients[index];
|
const englishIng = editableEnglish.ingredients[index];
|
||||||
@@ -174,10 +155,11 @@
|
|||||||
// 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 germanIng;
|
return germanIng;
|
||||||
}
|
}
|
||||||
}),
|
});
|
||||||
instructions: germanData.instructions.map((germanInst: any, index: number) => {
|
|
||||||
|
// Update instructions with English base recipe names
|
||||||
|
editableEnglish.instructions = germanData.instructions.map((germanInst: any, index: number) => {
|
||||||
if (germanInst.type === 'reference' && germanInst.baseRecipeRef) {
|
if (germanInst.type === 'reference' && germanInst.baseRecipeRef) {
|
||||||
// This is a base recipe reference - use English base recipe name
|
|
||||||
const shortName = getShortName(germanInst.baseRecipeRef);
|
const shortName = getShortName(germanInst.baseRecipeRef);
|
||||||
const translation = baseRecipeTranslations.get(shortName);
|
const translation = baseRecipeTranslations.get(shortName);
|
||||||
const englishInst = editableEnglish.instructions[index];
|
const englishInst = editableEnglish.instructions[index];
|
||||||
@@ -198,18 +180,19 @@
|
|||||||
// 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
|
// Sync images array - keep existing English alt/caption or initialize empty
|
||||||
images: germanData.images?.map((germanImg: any, index: number) => {
|
editableEnglish.images = germanData.images?.map((germanImg: any, index: number) => {
|
||||||
const existingEnImage = editableEnglish.images?.[index];
|
const existingEnImage = editableEnglish.images?.[index];
|
||||||
return existingEnImage || { alt: '', caption: '' };
|
return existingEnImage || { alt: '', caption: '' };
|
||||||
}) || []
|
}) || [];
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always sync base recipe references when component mounts
|
// Run base recipe check in background (non-blocking)
|
||||||
|
$effect(() => {
|
||||||
syncBaseRecipeReferences();
|
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,9 +242,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
|
||||||
if (editableEnglish) {
|
|
||||||
// Special handling for tags (comma-separated string -> array)
|
// Special handling for tags (comma-separated string -> array)
|
||||||
if (field === 'tags') {
|
if (field === 'tags') {
|
||||||
editableEnglish[field] = value.split(',').map((t: string) => t.trim()).filter((t: string) => t);
|
editableEnglish[field] = value.split(',').map((t: string) => t.trim()).filter((t: string) => t);
|
||||||
@@ -279,13 +257,11 @@
|
|||||||
} else {
|
} else {
|
||||||
editableEnglish[field] = value;
|
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', {
|
||||||
|
detail: {
|
||||||
translatedRecipe: {
|
translatedRecipe: {
|
||||||
...editableEnglish,
|
...editableEnglish,
|
||||||
translationStatus: 'approved',
|
translationStatus: 'approved',
|
||||||
lastTranslated: new Date(),
|
lastTranslated: new Date(),
|
||||||
changedFields: [],
|
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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
let portions_local
|
|
||||||
portions.subscribe((p) => {
|
portions.subscribe((p) => {
|
||||||
portions_local = p
|
portions_local = p;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
season.update(() => data.recipe.season)
|
season.update(() => data.recipe.season);
|
||||||
let season_local
|
let season_local = $state<any>(data.recipe.season);
|
||||||
|
$effect(() => {
|
||||||
season.subscribe((s) => {
|
season.subscribe((s) => {
|
||||||
season_local = s
|
season_local = s;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let img_local = $state<string>('');
|
||||||
import { img } from '$lib/js/img_store';
|
img.update(() => '');
|
||||||
let img_local
|
$effect(() => {
|
||||||
img.update(() => "")
|
|
||||||
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}
|
||||||
|
|||||||
Reference in New Issue
Block a user