Files
mealplanner/frontend/src/views/WeekPlanView.vue
Claude 165acc9d22 feat: German translations for TheMealDB recipes + recipe detail view
- Add GermanTranslator service with 200+ ingredient and meal name mappings
- Translate titles and ingredients when fetching from TheMealDB
- Add click-to-view recipe detail modal on week plan meal cards
- Import RecipeDetail component in WeekPlanView

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:20:08 +00:00

176 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="flex flex-col gap-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-zinc-100">Wochenplan</h1>
<p v-if="store.currentPlan" class="text-zinc-500 text-sm mt-0.5">
{{ formatWeek(store.currentPlan.weekStart) }}
</p>
</div>
<button
@click="handleGenerate"
:disabled="store.generating"
class="flex items-center gap-2 px-4 py-2 rounded-lg bg-accent hover:bg-accent-hover disabled:opacity-60 disabled:cursor-not-allowed text-white text-sm font-medium transition-colors"
>
<svg v-if="store.generating" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<svg v-else class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2" stroke-width="2" />
<circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" />
<circle cx="15.5" cy="8.5" r="1.5" fill="currentColor" />
<circle cx="8.5" cy="15.5" r="1.5" fill="currentColor" />
<circle cx="15.5" cy="15.5" r="1.5" fill="currentColor" />
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
</svg>
{{ store.generating ? 'Generiere...' : 'Neue Woche generieren' }}
</button>
</div>
<!-- Error -->
<div v-if="store.error" class="px-4 py-3 bg-red-950 border border-error rounded-lg text-error text-sm">
{{ store.error }}
</div>
<!-- Loading skeleton -->
<div v-if="store.loading" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div
v-for="i in 7"
:key="i"
class="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden animate-pulse"
>
<div class="h-8 bg-zinc-800" />
<div class="h-40 bg-zinc-800/60" />
<div class="p-4 space-y-2">
<div class="h-4 bg-zinc-800 rounded w-3/4" />
<div class="h-3 bg-zinc-800 rounded w-1/2" />
</div>
<div class="px-4 py-3 border-t border-zinc-800 flex gap-2">
<div class="flex-1 h-8 bg-zinc-800 rounded-lg" />
<div class="flex-1 h-8 bg-zinc-800 rounded-lg" />
</div>
</div>
</div>
<!-- Meal grid -->
<div
v-else-if="store.currentPlan?.entries?.length"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
>
<MealCard
v-for="entry in sortedEntries"
:key="entry.id"
:entry="entry"
:day-name="getDayName(entry.date)"
@swap="openSwapModal(entry)"
@reroll="handleReroll(entry)"
@click="selectedRecipe = entry.recipe"
/>
</div>
<!-- Empty state -->
<div v-else-if="!store.loading" class="flex flex-col items-center justify-center py-24 gap-6">
<div class="p-6 bg-zinc-900 rounded-full border border-zinc-800">
<svg class="w-12 h-12 text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<div class="text-center">
<p class="text-zinc-300 text-lg font-medium">Noch kein Plan erstellt</p>
<p class="text-zinc-500 text-sm mt-1">Generiere einen Wochenplan mit zufälligen Rezepten.</p>
</div>
<button
@click="handleGenerate"
:disabled="store.generating"
class="flex items-center gap-2 px-6 py-3 rounded-lg bg-accent hover:bg-accent-hover disabled:opacity-60 text-white font-medium transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="3" y="3" width="18" height="18" rx="2" stroke-width="2" />
<circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" />
<circle cx="15.5" cy="8.5" r="1.5" fill="currentColor" />
<circle cx="8.5" cy="15.5" r="1.5" fill="currentColor" />
<circle cx="15.5" cy="15.5" r="1.5" fill="currentColor" />
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
</svg>
Jetzt generieren
</button>
</div>
</div>
<!-- Recipe detail modal -->
<RecipeDetail
v-if="selectedRecipe"
:recipe="selectedRecipe"
@close="selectedRecipe = null"
/>
<!-- Swap modal -->
<SwapModal
v-if="swapEntry && recipesStore.recipes.length"
:recipes="recipesStore.recipes"
@close="swapEntry = null"
@select="handleSwapSelect"
/>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import MealCard from '../components/MealCard.vue'
import RecipeDetail from '../components/RecipeDetail.vue'
import SwapModal from '../components/SwapModal.vue'
import { useMealPlanStore, useRecipesStore } from '../stores/mealPlan'
import type { MealPlanEntry, Recipe } from '../stores/mealPlan'
const store = useMealPlanStore()
const recipesStore = useRecipesStore()
const swapEntry = ref<MealPlanEntry | null>(null)
const selectedRecipe = ref<Recipe | null>(null)
const DAY_NAMES = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']
function getDayName(dateStr: string): string {
const d = new Date(dateStr + 'T00:00:00')
return DAY_NAMES[d.getDay()] ?? dateStr
}
const sortedEntries = computed(() => {
return [...(store.currentPlan?.entries ?? [])].sort((a, b) => a.date.localeCompare(b.date))
})
function formatWeek(dateStr: string): string {
const d = new Date(dateStr)
const end = new Date(d)
end.setDate(end.getDate() + 6)
const fmt = (dt: Date) => dt.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })
return `${fmt(d)} ${fmt(end)}.${end.getFullYear()}`
}
async function handleGenerate(): Promise<void> {
await store.generatePlan()
}
function openSwapModal(entry: MealPlanEntry): void {
swapEntry.value = entry
if (!recipesStore.recipes.length) {
recipesStore.fetchRecipes()
}
}
async function handleSwapSelect(recipeId: string): Promise<void> {
if (!swapEntry.value) return
await store.swapMeal(swapEntry.value.id, recipeId)
swapEntry.value = null
}
async function handleReroll(entry: MealPlanEntry): Promise<void> {
await store.rerollMeal(entry.id)
}
onMounted(() => {
store.fetchCurrentPlan()
})
</script>