feat(print): add print-optimized route with server-side portion scaling
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
140
src/routes/recipes/[id]/print/+page.svelte
Normal file
140
src/routes/recipes/[id]/print/+page.svelte
Normal file
@@ -0,0 +1,140 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
function formatQty(q: number | null): string {
|
||||
if (q === null) return '';
|
||||
if (Number.isInteger(q)) return String(q);
|
||||
return q.toLocaleString('de-DE', { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => window.print(), 250);
|
||||
});
|
||||
</script>
|
||||
|
||||
<article class="print">
|
||||
<header>
|
||||
<h1>{data.recipe.title}</h1>
|
||||
<p class="meta">
|
||||
{data.servings} {data.recipe.servings_unit ?? 'Portionen'}
|
||||
{#if data.recipe.prep_time_min}· Vorb. {data.recipe.prep_time_min} min{/if}
|
||||
{#if data.recipe.cook_time_min}· Kochen {data.recipe.cook_time_min} min{/if}
|
||||
{#if data.recipe.source_url}
|
||||
· Quelle: {data.recipe.source_domain}
|
||||
{/if}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{#if data.recipe.image_path}
|
||||
<img src={`/images/${data.recipe.image_path}`} alt="" />
|
||||
{/if}
|
||||
|
||||
<section>
|
||||
<h2>Zutaten</h2>
|
||||
<ul class="ingredients">
|
||||
{#each data.ingredients as ing, i (i)}
|
||||
<li>
|
||||
<span class="qty">
|
||||
{formatQty(ing.quantity)}{#if ing.unit}{' '}{ing.unit}{/if}
|
||||
</span>
|
||||
<span class="name">
|
||||
{ing.name}{#if ing.note}{' '}<em>({ing.note})</em>{/if}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Zubereitung</h2>
|
||||
<ol class="steps">
|
||||
{#each data.recipe.steps as step (step.position)}
|
||||
<li>{step.text}</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{#if data.recipe.source_url}
|
||||
<footer>
|
||||
<p>Quelle: <a href={data.recipe.source_url}>{data.recipe.source_url}</a></p>
|
||||
</footer>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
background: white !important;
|
||||
}
|
||||
:global(header.bar),
|
||||
:global(nav.tabs) {
|
||||
display: none !important;
|
||||
}
|
||||
.print {
|
||||
font-family: Georgia, 'Liberation Serif', serif;
|
||||
color: #111;
|
||||
max-width: 190mm;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin: 0 0 0.4rem;
|
||||
}
|
||||
.meta {
|
||||
color: #555;
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
img {
|
||||
max-width: 50%;
|
||||
float: right;
|
||||
margin: 0 0 1rem 1rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.15rem;
|
||||
border-bottom: 1px solid #bbb;
|
||||
padding-bottom: 0.25rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.ingredients {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
columns: 2;
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
.ingredients li {
|
||||
break-inside: avoid;
|
||||
padding: 0.25rem 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.qty {
|
||||
display: inline-block;
|
||||
min-width: 5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.steps {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
.steps li {
|
||||
margin-bottom: 0.6rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
footer {
|
||||
margin-top: 2rem;
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
@media print {
|
||||
@page {
|
||||
margin: 15mm;
|
||||
}
|
||||
.print {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user