Files
wtc.waiswuaschd.de/app/templates/recipe_detail.html
T

992 lines
40 KiB
HTML
Executable File
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.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>{{ recipe.title }} – Mein Kochbuch</title>
<link rel="stylesheet" href="/static/cookbook.css?v=4.6.9">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍽️</text></svg>">
</head>
<body class="cookbook-body">
<!-- Navigation Bar -->
<header class="cb-nav">
<a href="{% if is_shared %}#{% else %}/cookbook{% endif %}" class="cb-nav-brand">
<span class="cb-brand-icon">📖</span>
<h1 class="cb-nav-title">{% if is_shared %}Mein Kochbuch{% else %}Kochbuch{% endif %}</h1>
</a>
<!-- Desktop Navigation -->
<div class="cb-nav-actions desktop-nav-actions">
<div class="cb-nav-cookmode-toggle no-print" title="Kochmodus: Verhindert automatisches Sperren des Bildschirms">
<span>👨‍🍳 Kochmodus</span>
<label class="cb-switch cb-switch-sm" for="cook-mode-toggle-nav">
<input type="checkbox" id="cook-mode-toggle-nav" class="cook-mode-switch-input" onchange="toggleCookingMode(this.checked)">
<span class="cb-slider"></span>
</label>
</div>
{% if not is_shared %}
<a href="/cookbook" class="cb-btn cb-btn-outline">
<span>📖</span> <span>Alle Rezepte</span>
</a>
<button type="button" onclick="openShareModal()" class="cb-btn cb-btn-outline" title="Rezept per Link teilen">
<span>🔗</span> <span>Teilen</span>
</button>
<a href="/cookbook/edit/{{ recipe.id }}" class="cb-btn cb-btn-green">
<span>✏️</span> <span>Bearbeiten</span>
</a>
{% endif %}
<button onclick="window.print()" class="cb-btn cb-btn-outline" title="Rezeptseite drucken">
<span>🖨️</span> <span>Drucken</span>
</button>
{% if not is_shared %}
<a href="/logout" class="cb-nav-btn cb-nav-btn-logout" title="Abmelden">
<span>🚪</span> <span>Abmelden</span>
</a>
{% endif %}
</div>
<!-- Mobile Kebab (3-Punkte) Menü -->
<div class="mobile-nav-wrapper">
<button type="button" class="cb-kebab-btn" onclick="toggleMobileMenu(event)" aria-label="Menü öffnen" title="Menü">
<span>⋮</span>
</button>
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
<div class="cb-mobile-item" style="display: flex; justify-content: space-between; align-items: center;" onclick="event.stopPropagation();">
<span>👨‍🍳 Kochmodus (Display an)</span>
<label class="cb-switch cb-switch-sm" for="cook-mode-toggle-mobile">
<input type="checkbox" id="cook-mode-toggle-mobile" class="cook-mode-switch-input" onchange="toggleCookingMode(this.checked)">
<span class="cb-slider"></span>
</label>
</div>
<hr class="cb-mobile-divider">
{% if not is_shared %}
<button type="button" onclick="openShareModal()" class="cb-mobile-item">
<span>🔗</span> <span>Rezept teilen</span>
</button>
<a href="/cookbook" class="cb-mobile-item">
<span>📖</span> <span>Zum Kochbuch</span>
</a>
<a href="/cookbook/edit/{{ recipe.id }}" class="cb-mobile-item">
<span>✏️</span> <span>Rezept bearbeiten</span>
</a>
{% endif %}
<button type="button" onclick="window.print()" class="cb-mobile-item">
<span>🖨️</span> <span>Drucken & PDF</span>
</button>
{% if not is_shared %}
<hr class="cb-mobile-divider">
<a href="/logout" class="cb-mobile-item cb-mobile-logout">
<span>🚪</span> <span>Abmelden</span>
</a>
{% endif %}
</div>
</div>
</header>
<main class="book-wrapper">
<!-- EINHEITLICHE REZEPTSEITE -->
<article class="page recipe-page">
{% if is_shared %}
<!-- GAST FREIGABE-BANNER -->
<div class="recipe-shared-banner no-print">
<div class="recipe-shared-left">
<span class="recipe-shared-icon">📤</span>
<div class="recipe-shared-text-group">
<span class="recipe-shared-main-text">Geteiltes Rezept</span>
<span class="recipe-shared-sub-text">
{% if share.expires_at %}
⏳ {{ share.relative_status }} (Gültig bis {{ share.expires_at_formatted }} Uhr)
{% else %}
✨ Dauerhafter Link (unbegrenzt gültig)
{% endif %}
</span>
</div>
</div>
<div class="recipe-shared-right">
<span class="recipe-shared-badge">Gastansicht</span>
</div>
</div>
{% endif %}
<!-- KOCHMODUS SCHIEBEREGLER BANNER (Mobil, Tablet & Desktop) -->
<div class="recipe-cookmode-banner no-print">
<div class="recipe-cookmode-left">
<span class="cookmode-icon">👨‍🍳</span>
<div class="cookmode-text-group">
<span class="cookmode-main-text">Kochmodus</span>
<span class="cookmode-sub-text" id="cookmode-sub-text">Verhindert, dass das Display während des Kochens sperrt</span>
</div>
</div>
<div class="recipe-cookmode-right">
<label class="cb-switch" for="cook-mode-toggle" title="Kochmodus umschalten">
<input type="checkbox" id="cook-mode-toggle" class="cook-mode-switch-input" onchange="toggleCookingMode(this.checked)">
<span class="cb-slider"></span>
</label>
<span id="cook-mode-badge" class="cookmode-badge">Aus</span>
</div>
</div>
<!-- Header (Ohne Kapitelangabe & ohne Quelle im Titel) -->
<header class="recipe-header">
<h2 class="recipe-title">{{ recipe.title }}</h2>
{% if recipe.subtitle and recipe.subtitle.strip() %}
<div class="recipe-subtitle">{{ recipe.subtitle.strip() }}</div>
{% endif %}
<div class="recipe-meta-row">
{% if recipe.effort %}
<div class="recipe-meta-item">
<span>⏱️</span>
<span>Aufwand:
{% if recipe.effort == 'gering' %}Gering 🟢{% elif recipe.effort == 'hoch' %}Hoch 🔴{% else %}Mittel 🟡{% endif %}
</span>
</div>
{% endif %}
{% if recipe.prep_time %}
<div class="recipe-meta-item">
<span>🔪</span>
<span>Zubereitung: {{ recipe.prep_time }}</span>
</div>
{% endif %}
{% if recipe.cook_time %}
<div class="recipe-meta-item">
<span>🍳</span>
<span>Kochen/Backen: {{ recipe.cook_time }}</span>
</div>
{% endif %}
{% if recipe.portions %}
<div class="recipe-meta-item">
<span>👥</span>
<span>{{ recipe.portions }} Portionen</span>
</div>
{% endif %}
{% if recipe.source and recipe.source.strip() %}
<div class="recipe-meta-item">
<span>📖</span>
<span>Quelle: {{ recipe.source.strip() }}</span>
</div>
{% endif %}
</div>
</header>
<!-- 2-Spalten Layout (Auf Tablets im Querformat als 2-Spalten Kochstation) -->
<div class="recipe-grid">
<!-- Linke Spalte: Foto + Zutaten -->
<aside class="recipe-side">
<div class="recipe-photo-box">
{% if recipe.image_url %}
<img src="{{ recipe.image_url }}" alt="{{ recipe.title }}">
{% else %}
<div class="recipe-photo-empty">
<span>📸</span>
<span>Kein Foto hinterlegt</span>
</div>
{% endif %}
</div>
<!-- Zutatenleiste ohne Portionsangabe (da schon in Kopfzeile) -->
<div class="ingredients-box {% if recipe.has_portion_table %}has-portion-matrix{% endif %}">
<h4 class="section-hand-title">Zutaten</h4>
{% if recipe.has_portion_table and recipe.portion_matrix %}
<!-- Tabelle für 2/4/6/8 Portionen (z. B. Waffeln / Pancakes) -->
<table class="portion-matrix-table">
<thead>
<tr>
<th class="matrix-th-ing">Zutat</th>
<th class="matrix-th-p">2 P.</th>
<th class="matrix-th-p">4 P.</th>
<th class="matrix-th-p">6 P.</th>
<th class="matrix-th-p">8 P.</th>
</tr>
</thead>
<tbody>
{% for row in recipe.portion_matrix %}
{% if row.is_header %}
<tr class="matrix-header-row">
<th colspan="5">{{ row.text }}</th>
</tr>
{% else %}
<tr class="matrix-row">
<td class="matrix-td-name">{{ row.name }}</td>
<td class="matrix-td-amt">{{ row.amounts[2] }}</td>
<td class="matrix-td-amt highlight">{{ row.amounts[4] }}</td>
<td class="matrix-td-amt">{{ row.amounts[6] }}</td>
<td class="matrix-td-amt">{{ row.amounts[8] }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% else %}
<!-- Normale kompakte Zutatenliste (Tippen zum Abhaken beim Kochen) -->
<ul class="ingredients-list">
{% for line in recipe.ingredients.split('\n') %}
{% if line.strip() %}
{% set raw_line = line.strip() %}
{% if raw_line.endswith(':') or raw_line.lower().startswith('für ') %}
<li class="ing-subheading">{{ raw_line }}</li>
{% else %}
<li class="ingredient-item" onclick="toggleIngredientCheck(this)" title="Tippen zum Abhaken">{{ raw_line }}</li>
{% endif %}
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div>
</aside>
<!-- Rechte Spalte: Zubereitung + Notizen + Quelle -->
<section class="recipe-main">
<div>
<h4 class="section-hand-title">Zubereitung</h4>
<div class="instructions-steps">
{% for step in recipe.instructions.split('\n') %}
{% if step.strip() %}
<div class="step-card" onclick="toggleStepCheck(this)" title="Tippen zum Abhaken dieses Schritts">
<span class="step-badge">{{ loop.index }}</span>
<div class="step-text">{{ step.strip() | strip_step_number }}</div>
<span class="step-done-check">✓</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% if recipe.notes and recipe.notes.strip() %}
<div class="notes-box">
<div class="notes-box-title">💡 Tipp & Notiz</div>
<div>{{ recipe.notes.strip() }}</div>
</div>
{% else %}
<div class="notes-box notes-box-empty print-only">
<div class="notes-box-title">💡 Eigene Notizen & Tipps</div>
<div class="notes-blank-lines">
<div class="notes-blank-line"></div>
<div class="notes-blank-line"></div>
</div>
</div>
{% endif %}
</section>
</div>
</article>
{% if not is_shared %}
<!-- SHARE MODAL -->
<div id="share-modal" class="cb-modal-backdrop" onclick="closeShareModal(event)">
<div class="cb-modal-card share-modal-card" onclick="event.stopPropagation()">
<div class="cb-modal-header">
<h3 class="cb-modal-title">
<span>🔗</span> <span>Rezept teilen</span>
</h3>
<button type="button" class="cb-modal-close" onclick="closeShareModal()" aria-label="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<p style="margin-top: 0; color: #4a4a4a; line-height: 1.5; font-size: 0.96rem;">
Erstelle einen Link, um <strong>{{ recipe.title }}</strong> mit Freunden oder Familie zu teilen.
Empfänger können das Rezept direkt ansehen, kochen und ausdrucken – <em>ohne eigenes Login</em>!
</p>
<!-- Neue Freigabe konfigurieren -->
<div class="share-create-box">
<label class="share-label">Gültigkeitsdauer des Links:</label>
<div class="share-expiry-pills">
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="24h">
<span>24 Stunden</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="3d">
<span>3 Tage</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="7d" checked>
<span>7 Tage (Empfohlen)</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="30d">
<span>30 Tage</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="unlimited">
<span>Unbegrenzt</span>
</label>
</div>
<div class="share-input-row" style="margin-top: 14px;">
<input type="text" id="share-label-input" class="share-input" placeholder="Optionale Notiz (z. B. WhatsApp, Familie...)" maxlength="50">
<button type="button" id="btn-create-share" class="cb-btn cb-btn-green" onclick="submitCreateShare()">
<span>✨ Link erstellen</span>
</button>
</div>
</div>
<!-- Neuer erstellter Link (Highlight Box) -->
<div id="share-result-box" class="share-result-box" style="display: none;">
<div class="share-result-header">
<span>✓ Neuer Share-Link bereit:</span>
<span id="share-result-expiry-badge" class="share-status-pill share-badge-active"></span>
</div>
<div class="share-result-input-group">
<input type="text" id="share-result-url" class="share-url-field" readonly onclick="this.select()">
<button type="button" id="btn-copy-share" class="cb-btn cb-btn-green" onclick="copyCurrentShareUrl()">
<span id="copy-btn-icon">📋</span> <span id="copy-btn-text">Kopieren</span>
</button>
<button type="button" id="btn-native-share" class="cb-btn cb-btn-outline" style="display: none;" onclick="triggerNativeShare()">
<span>📤 Teilen</span>
</button>
</div>
<div class="share-result-hint">Jeder mit diesem Link hat für den gewählten Zeitraum vollen Lese- und Kochzugriff.</div>
</div>
<!-- Liste existierender Freigaben -->
<div class="share-existing-section">
<h4 class="share-existing-title">Aktive Freigaben für dieses Rezept</h4>
<div id="share-links-list" class="share-links-list">
<div class="share-links-loading">Lade aktive Links...</div>
</div>
</div>
</div>
<div class="cb-modal-footer" style="padding: 12px 18px; border-top: 1.5px solid #e0ded9; display: flex; justify-content: flex-end; background: #faf6f0;">
<button type="button" class="cb-btn cb-btn-outline" onclick="closeShareModal()">Schließen</button>
</div>
</div>
</div>
{% endif %}
{% if not is_shared %}
<!-- SHARE MODAL -->
<div id="share-modal" class="cb-modal-backdrop" onclick="closeShareModal(event)">
<div class="cb-modal-card share-modal-card" onclick="event.stopPropagation()">
<div class="cb-modal-header">
<h3 class="cb-modal-title">
<span>🔗</span> <span>Rezept teilen</span>
</h3>
<button type="button" class="cb-modal-close" onclick="closeShareModal()" aria-label="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<p style="margin-top: 0; color: #4a4a4a; line-height: 1.5; font-size: 0.96rem;">
Erstelle einen Link, um <strong>{{ recipe.title }}</strong> mit Freunden oder Familie zu teilen.
Empfänger können das Rezept direkt ansehen, kochen und ausdrucken – <em>ohne eigenes Login</em>!
</p>
<!-- Neue Freigabe konfigurieren -->
<div class="share-create-box">
<label class="share-label">Gültigkeitsdauer des Links:</label>
<div class="share-expiry-pills">
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="24h">
<span>24 Stunden</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="3d">
<span>3 Tage</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="7d" checked>
<span>7 Tage (Empfohlen)</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="30d">
<span>30 Tage</span>
</label>
<label class="expiry-pill">
<input type="radio" name="share_expiry" value="unlimited">
<span>Unbegrenzt</span>
</label>
</div>
<div class="share-input-row" style="margin-top: 14px;">
<input type="text" id="share-label-input" class="share-input" placeholder="Optionale Notiz (z. B. WhatsApp, Familie...)" maxlength="50">
<button type="button" id="btn-create-share" class="cb-btn cb-btn-green" onclick="submitCreateShare()">
<span>✨ Link erstellen</span>
</button>
</div>
</div>
<!-- Neuer erstellter Link (Highlight Box) -->
<div id="share-result-box" class="share-result-box" style="display: none;">
<div class="share-result-header">
<span>✓ Neuer Share-Link bereit:</span>
<span id="share-result-expiry-badge" class="share-status-pill share-badge-active"></span>
</div>
<div class="share-result-input-group">
<input type="text" id="share-result-url" class="share-url-field" readonly onclick="this.select()">
<button type="button" id="btn-copy-share" class="cb-btn cb-btn-green" onclick="copyCurrentShareUrl()">
<span id="copy-btn-icon">📋</span> <span id="copy-btn-text">Kopieren</span>
</button>
<button type="button" id="btn-native-share" class="cb-btn cb-btn-outline" style="display: none;" onclick="triggerNativeShare()">
<span>📤 Teilen</span>
</button>
</div>
<div class="share-result-hint">Jeder mit diesem Link hat für den gewählten Zeitraum vollen Lese- und Kochzugriff.</div>
</div>
<!-- Liste existierender Freigaben -->
<div class="share-existing-section">
<h4 class="share-existing-title">Aktive Freigaben für dieses Rezept</h4>
<div id="share-links-list" class="share-links-list">
<div class="share-links-loading">Lade aktive Links...</div>
</div>
</div>
</div>
<div class="cb-modal-footer" style="padding: 12px 18px; border-top: 1.5px solid #e0ded9; display: flex; justify-content: flex-end; background: #faf6f0;">
<button type="button" class="cb-btn cb-btn-outline" onclick="closeShareModal()">Schließen</button>
</div>
</div>
</div>
{% endif %}
</main>
<script>
// ==========================================
// MOBILE NAVIGATION (3-PUNKTE-MENÜ)
// ==========================================
function toggleMobileMenu(e) {
if (e) e.stopPropagation();
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.toggle('is-open');
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.mobile-nav-wrapper')) {
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
}
});
// ==========================================
// KOCHMODUS: SCREEN WAKE LOCK & DISPLAY WACHHALTEN
// ==========================================
let wakeLockSentinel = null;
let isCookModeActive = false;
async function requestWakeLock() {
if ('wakeLock' in navigator) {
try {
wakeLockSentinel = await navigator.wakeLock.request('screen');
wakeLockSentinel.addEventListener('release', () => {
if (!isCookModeActive) {
setCookModeUI(false, true);
}
});
return true;
} catch (err) {
console.warn('Wake Lock Anfrage nicht erfolgreich:', err);
return false;
}
}
return false;
}
async function releaseWakeLock() {
if (wakeLockSentinel !== null) {
try {
await wakeLockSentinel.release();
} catch (e) {}
wakeLockSentinel = null;
}
}
async function toggleCookingMode(active) {
isCookModeActive = active;
try {
localStorage.setItem('wtc_cookmode_active', active ? 'true' : 'false');
} catch (e) {}
// Synchronisiere alle Schalter (Desktop-Nav, Banner, Mobil-Menü)
const switches = document.querySelectorAll('.cook-mode-switch-input');
switches.forEach(sw => { sw.checked = active; });
if (active) {
const supported = await requestWakeLock();
setCookModeUI(true, supported);
showToast(supported ? '✓ Kochmodus an: Display bleibt an 💡' : '✓ Kochmodus aktiv');
} else {
await releaseWakeLock();
setCookModeUI(false, true);
showToast('Kochmodus ausgeschaltet');
}
}
function setCookModeUI(active, supported = true) {
if (active) {
document.body.classList.add('cook-mode-on');
} else {
document.body.classList.remove('cook-mode-on');
}
const badge = document.getElementById('cook-mode-badge');
const subText = document.getElementById('cookmode-sub-text');
if (badge) {
if (active) {
badge.textContent = supported ? 'Aktiv 💡' : 'Aktiv';
} else {
badge.textContent = 'Aus';
}
}
if (subText) {
if (active) {
subText.textContent = supported
? 'Display bleibt während des Kochens dauerhaft eingeschaltet 💡'
: 'Display-Wachhalten nicht vom Browser unterstützt (Interaktive Schritte aktiv)';
} else {
subText.textContent = 'Verhindert, dass das Display während des Kochens sperrt';
}
}
}
// Wenn der Nutzer die App/den Tab wechselt und zurückkommt: Wake Lock erneuern!
document.addEventListener('visibilitychange', async () => {
if (isCookModeActive && document.visibilityState === 'visible') {
await requestWakeLock();
}
});
// Gespeicherten Zustand beim Laden wiederherstellen
window.addEventListener('DOMContentLoaded', () => {
try {
const saved = localStorage.getItem('wtc_cookmode_active') === 'true';
if (saved) {
const sw = document.getElementById('cook-mode-toggle');
if (sw) {
sw.checked = true;
toggleCookingMode(true);
}
}
} catch (e) {}
});
// Interaktive Zutaten-Checkliste (zum Abhaken beim Vorbereiten)
function toggleIngredientCheck(el) {
el.classList.toggle('is-checked');
}
// Interaktiver Schritt-Tracker (zum Markieren erledigter Schritte)
function toggleStepCheck(el) {
el.classList.toggle('is-done');
}
// Toast Benachrichtigung
function showToast(msg) {
let toast = document.getElementById('cook-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'cook-toast';
toast.className = 'cb-toast';
document.body.appendChild(toast);
}
toast.textContent = msg;
toast.style.display = 'flex';
setTimeout(() => {
toast.style.display = 'none';
}, 2500);
}
// ==========================================
// SHARE MODAL & FREIGABELINKS
// ==========================================
let currentShareUrl = '';
function openShareModal() {
const modal = document.getElementById('share-modal');
if (modal) {
modal.classList.add('active');
document.body.classList.add('modal-open');
loadActiveShareLinks();
}
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
}
function closeShareModal(e) {
if (e && e.target && e.target.classList && !e.target.classList.contains('cb-modal-backdrop') && !e.target.classList.contains('cb-modal-close') && e.target.tagName !== 'BUTTON') {
return;
}
const modal = document.getElementById('share-modal');
if (modal) {
modal.classList.remove('active');
document.body.classList.remove('modal-open');
}
}
async function loadActiveShareLinks() {
const listEl = document.getElementById('share-links-list');
if (!listEl) return;
try {
const resp = await fetch('/api/recipes/{{ recipe.id }}/shares');
if (!resp.ok) throw new Error('Fehler beim Laden');
const data = await resp.json();
renderShareLinks(data.shares || []);
} catch (err) {
listEl.innerHTML = '<div class="share-empty-state">Konnte aktive Links nicht laden.</div>';
}
}
function renderShareLinks(shares) {
const listEl = document.getElementById('share-links-list');
if (!listEl) return;
if (!shares || shares.length === 0) {
listEl.innerHTML = '<div class="share-empty-state">Noch keine Share-Links für dieses Rezept erstellt.</div>';
return;
}
listEl.innerHTML = shares.map(s => {
const isExp = s.is_expired;
const statusClass = isExp ? 'share-badge-expired' : 'share-badge-active';
const labelText = s.label ? `<span class="share-link-label">🏷️ ${escapeHtml(s.label)}</span>` : '';
const viewsText = `<span class="share-link-views">👁️ ${s.views_count || 0} ${s.views_count === 1 ? 'Aufruf' : 'Aufrufe'}</span>`;
const canonicalUrl = window.location.origin + '/shared/recipe/' + s.token;
return `
<div class="share-link-card ${isExp ? 'is-expired' : ''}">
<div class="share-card-main">
<div class="share-card-header">
${labelText}
<span class="share-status-pill ${statusClass}">${s.relative_status}</span>
</div>
<div class="share-card-meta">
<span>Erstellt: ${s.created_at || ''}</span>
${viewsText}
</div>
<div class="share-card-url-preview">
<input type="text" class="share-mini-url" value="${canonicalUrl}" readonly onclick="this.select()">
</div>
</div>
<div class="share-card-actions">
${!isExp ? `<button type="button" class="cb-btn cb-btn-sm cb-btn-outline" onclick="copyTextToClipboard('${canonicalUrl}', this)" title="In Zwischenablage kopieren">📋 Kopieren</button>` : ''}
<button type="button" class="cb-btn cb-btn-sm cb-btn-danger" onclick="deleteShareLink(${s.id})" title="Freigabe widerrufen">🗑️ Löschen</button>
</div>
</div>
`;
}).join('');
}
async function submitCreateShare() {
const btn = document.getElementById('btn-create-share');
const expiryRadio = document.querySelector('input[name="share_expiry"]:checked');
const labelInput = document.getElementById('share-label-input');
const expiryVal = expiryRadio ? expiryRadio.value : '7d';
const labelVal = labelInput ? labelInput.value.trim() : '';
if (btn) btn.disabled = true;
try {
const resp = await fetch('/api/recipes/{{ recipe.id }}/shares', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expires_in: expiryVal, label: labelVal })
});
if (!resp.ok) throw new Error('Erstellen fehlgeschlagen');
const data = await resp.json();
const share = data.share;
currentShareUrl = window.location.origin + '/shared/recipe/' + share.token;
const resBox = document.getElementById('share-result-box');
const resUrl = document.getElementById('share-result-url');
const badge = document.getElementById('share-result-expiry-badge');
if (resBox && resUrl) {
resUrl.value = currentShareUrl;
if (badge) badge.textContent = share.relative_status;
resBox.style.display = 'block';
resUrl.select();
}
const nativeBtn = document.getElementById('btn-native-share');
if (nativeBtn && navigator.share) {
nativeBtn.style.display = 'inline-flex';
}
if (labelInput) labelInput.value = '';
showToast('✓ Share-Link erfolgreich erstellt!');
loadActiveShareLinks();
} catch (err) {
showToast('Fehler beim Erstellen des Links');
} finally {
if (btn) btn.disabled = false;
}
}
async function copyCurrentShareUrl() {
const input = document.getElementById('share-result-url');
if (input && input.value) {
await copyTextToClipboard(input.value);
const copyBtnText = document.getElementById('copy-btn-text');
if (copyBtnText) {
copyBtnText.textContent = 'Kopiert! ✓';
setTimeout(() => { copyBtnText.textContent = 'Kopieren'; }, 2000);
}
}
}
async function copyTextToClipboard(text, btnEl) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
showToast('✓ In Zwischenablage kopiert!');
if (btnEl) {
const orig = btnEl.innerHTML;
btnEl.innerHTML = '✓ Kopiert!';
setTimeout(() => { btnEl.innerHTML = orig; }, 2000);
}
} catch (e) {
showToast('Konnte Link nicht automatisch kopieren');
}
}
async function triggerNativeShare() {
if (navigator.share && currentShareUrl) {
try {
await navigator.share({
title: 'Rezept: {{ recipe.title }}',
text: 'Hier ist das Rezept für {{ recipe.title }}:',
url: currentShareUrl
});
} catch (e) {}
}
}
async function deleteShareLink(shareId) {
if (!confirm('Diesen Share-Link wirklich löschen? Empfänger können das Rezept danach nicht mehr über diesen Link aufrufen.')) {
return;
}
try {
const resp = await fetch(`/api/recipes/{{ recipe.id }}/shares/${shareId}`, { method: 'DELETE' });
if (!resp.ok) throw new Error('Fehler beim Löschen');
showToast('Share-Link gelöscht');
loadActiveShareLinks();
const resBox = document.getElementById('share-result-box');
if (resBox) resBox.style.display = 'none';
} catch (err) {
showToast('Konnte Share-Link nicht löschen');
}
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ==========================================
// SHARE MODAL & FREIGABELINKS
// ==========================================
let currentShareUrl = '';
function openShareModal() {
const modal = document.getElementById('share-modal');
if (modal) {
modal.classList.add('active');
document.body.classList.add('modal-open');
loadActiveShareLinks();
}
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
}
function closeShareModal(e) {
if (e && e.target && e.target.classList && !e.target.classList.contains('cb-modal-backdrop') && !e.target.classList.contains('cb-modal-close') && e.target.tagName !== 'BUTTON') {
return;
}
const modal = document.getElementById('share-modal');
if (modal) {
modal.classList.remove('active');
document.body.classList.remove('modal-open');
}
}
async function loadActiveShareLinks() {
const listEl = document.getElementById('share-links-list');
if (!listEl) return;
try {
const resp = await fetch('/api/recipes/{{ recipe.id }}/shares');
if (!resp.ok) throw new Error('Fehler beim Laden');
const data = await resp.json();
renderShareLinks(data.shares || []);
} catch (err) {
listEl.innerHTML = '<div class="share-empty-state">Konnte aktive Links nicht laden.</div>';
}
}
function renderShareLinks(shares) {
const listEl = document.getElementById('share-links-list');
if (!listEl) return;
if (!shares || shares.length === 0) {
listEl.innerHTML = '<div class="share-empty-state">Noch keine Share-Links für dieses Rezept erstellt.</div>';
return;
}
listEl.innerHTML = shares.map(s => {
const isExp = s.is_expired;
const statusClass = isExp ? 'share-badge-expired' : 'share-badge-active';
const labelText = s.label ? `<span class="share-link-label">🏷️ ${escapeHtml(s.label)}</span>` : '';
const viewsText = `<span class="share-link-views">👁️ ${s.views_count || 0} ${s.views_count === 1 ? 'Aufruf' : 'Aufrufe'}</span>`;
const canonicalUrl = window.location.origin + '/shared/recipe/' + s.token;
return `
<div class="share-link-card ${isExp ? 'is-expired' : ''}">
<div class="share-card-main">
<div class="share-card-header">
${labelText}
<span class="share-status-pill ${statusClass}">${s.relative_status}</span>
</div>
<div class="share-card-meta">
<span>Erstellt: ${s.created_at || ''}</span>
${viewsText}
</div>
<div class="share-card-url-preview">
<input type="text" class="share-mini-url" value="${canonicalUrl}" readonly onclick="this.select()">
</div>
</div>
<div class="share-card-actions">
${!isExp ? `<button type="button" class="cb-btn cb-btn-sm cb-btn-outline" onclick="copyTextToClipboard('${canonicalUrl}', this)" title="In Zwischenablage kopieren">📋 Kopieren</button>` : ''}
<button type="button" class="cb-btn cb-btn-sm cb-btn-danger" onclick="deleteShareLink(${s.id})" title="Freigabe widerrufen">🗑️ Löschen</button>
</div>
</div>
`;
}).join('');
}
async function submitCreateShare() {
const btn = document.getElementById('btn-create-share');
const expiryRadio = document.querySelector('input[name="share_expiry"]:checked');
const labelInput = document.getElementById('share-label-input');
const expiryVal = expiryRadio ? expiryRadio.value : '7d';
const labelVal = labelInput ? labelInput.value.trim() : '';
if (btn) btn.disabled = true;
try {
const resp = await fetch('/api/recipes/{{ recipe.id }}/shares', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expires_in: expiryVal, label: labelVal })
});
if (!resp.ok) throw new Error('Erstellen fehlgeschlagen');
const data = await resp.json();
const share = data.share;
currentShareUrl = window.location.origin + '/shared/recipe/' + share.token;
const resBox = document.getElementById('share-result-box');
const resUrl = document.getElementById('share-result-url');
const badge = document.getElementById('share-result-expiry-badge');
if (resBox && resUrl) {
resUrl.value = currentShareUrl;
if (badge) badge.textContent = share.relative_status;
resBox.style.display = 'block';
resUrl.select();
}
const nativeBtn = document.getElementById('btn-native-share');
if (nativeBtn && navigator.share) {
nativeBtn.style.display = 'inline-flex';
}
if (labelInput) labelInput.value = '';
showToast('✓ Share-Link erfolgreich erstellt!');
loadActiveShareLinks();
} catch (err) {
showToast('Fehler beim Erstellen des Links');
} finally {
if (btn) btn.disabled = false;
}
}
async function copyCurrentShareUrl() {
const input = document.getElementById('share-result-url');
if (input && input.value) {
await copyTextToClipboard(input.value);
const copyBtnText = document.getElementById('copy-btn-text');
if (copyBtnText) {
copyBtnText.textContent = 'Kopiert! ✓';
setTimeout(() => { copyBtnText.textContent = 'Kopieren'; }, 2000);
}
}
}
async function copyTextToClipboard(text, btnEl) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
} else {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
showToast('✓ In Zwischenablage kopiert!');
if (btnEl) {
const orig = btnEl.innerHTML;
btnEl.innerHTML = '✓ Kopiert!';
setTimeout(() => { btnEl.innerHTML = orig; }, 2000);
}
} catch (e) {
showToast('Konnte Link nicht automatisch kopieren');
}
}
async function triggerNativeShare() {
if (navigator.share && currentShareUrl) {
try {
await navigator.share({
title: 'Rezept: {{ recipe.title }}',
text: 'Hier ist das Rezept für {{ recipe.title }}:',
url: currentShareUrl
});
} catch (e) {}
}
}
async function deleteShareLink(shareId) {
if (!confirm('Diesen Share-Link wirklich löschen? Empfänger können das Rezept danach nicht mehr über diesen Link aufrufen.')) {
return;
}
try {
const resp = await fetch(`/api/recipes/{{ recipe.id }}/shares/${shareId}`, { method: 'DELETE' });
if (!resp.ok) throw new Error('Fehler beim Löschen');
showToast('Share-Link gelöscht');
loadActiveShareLinks();
const resBox = document.getElementById('share-result-box');
if (resBox) resBox.style.display = 'none';
} catch (err) {
showToast('Konnte Share-Link nicht löschen');
}
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
</script>
</body>
</html>