moved to server
Update readme.md Signed-off-by: rasmus <rsbendtsen@gmail.com> Update readme.md Update Dockerfile Update Dockerfile
This commit is contained in:
87
templates/base.html
Normal file
87
templates/base.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Madplaner</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900 font-sans">
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="max-w-5xl mx-auto mt-4 p-4 rounded-xl {% if category == 'error' %}bg-red-100 text-red-700 border border-red-200{% else %}bg-green-100 text-green-700 border border-green-200{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<nav class="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-stone-100 no-print">
|
||||
<div class="max-w-6xl mx-auto px-6 h-20 flex items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-2 group">
|
||||
<div class="w-8 h-8 bg-emerald-800 rounded-lg flex items-center justify-center transition-transform group-hover:rotate-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xl text-emerald-800 italic font-serif">Madplaner</span>
|
||||
</a>
|
||||
|
||||
<div class="hidden md:flex items-center gap-8">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="/dashboard" class="text-sm font-medium text-stone-600 hover:text-emerald-800 transition-colors">Dashboard</a>
|
||||
<div class="h-4 w-px bg-stone-200"></div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm text-stone-400 font-light">Hej, {{ current_user.username }}</span>
|
||||
<a href="/logout" class="text-sm font-medium bg-stone-100 text-stone-900 px-4 py-2 rounded-full hover:bg-stone-200 transition-all">Sign Out</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/login" class="text-sm font-medium text-stone-600 hover:text-stone-900">Login</a>
|
||||
<a href="/register" class="text-sm font-medium bg-emerald-800 text-white px-6 py-2.5 rounded-full hover:bg-emerald-900 shadow-sm transition-all">Join Free</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="md:hidden text-stone-900">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16m-7 6h7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container mx-auto p-6">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
<script>
|
||||
function showToast(message, type = 'success') {
|
||||
const container = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
|
||||
// Nordic Palette for Toasts
|
||||
const styles = type === 'success'
|
||||
? 'bg-white border-emerald-500 text-stone-800'
|
||||
: 'bg-white border-rose-500 text-stone-800';
|
||||
|
||||
const icon = type === 'success'
|
||||
? '<span class="text-emerald-500 text-lg">✓</span>'
|
||||
: '<span class="text-rose-500 text-lg">✕</span>';
|
||||
|
||||
toast.className = `toast-animate-in pointer-events-auto flex items-center gap-4 px-6 py-4 rounded-2xl shadow-xl border-l-4 ${styles} min-w-[300px]`;
|
||||
|
||||
toast.innerHTML = `
|
||||
${icon}
|
||||
<p class="text-sm font-medium tracking-tight">${message}</p>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Auto-remove after 4 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.replace('toast-animate-in', 'toast-animate-out');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 4000);
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
204
templates/dashboard.html
Normal file
204
templates/dashboard.html
Normal file
@@ -0,0 +1,204 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-6xl mx-auto px-6 py-12">
|
||||
<div class="mb-12">
|
||||
<h1 class="text-4xl font-light text-stone-900 tracking-tight">
|
||||
Hej, <span class="font-serif italic text-emerald-800">{{ current_user.username }}</span>
|
||||
</h1>
|
||||
<p class="text-stone-500 font-light mt-2">Let’s design a menu for the days ahead.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<div class="mb-6 flex items-center gap-3 px-1">
|
||||
<div class="w-8 h-8 bg-emerald-50 rounded-lg flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-emerald-700" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-xl font-medium text-stone-800 tracking-tight">Personal preferences</h2>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-8 md:p-10 rounded-[2rem] shadow-sm border border-stone-100">
|
||||
<form action="{{ url_for('generate') }}" method="POST" class="space-y-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
|
||||
{% set fields = [
|
||||
('people_count', 'Number of People', [('1 adult', '1 adult'), ('2 adults', '2 adults'), ('Family
|
||||
of 4', 'Family of 4')]),
|
||||
('budget_level', 'Budget Level', [('Low', 'Low'), ('Medium', 'Medium'), ('High', 'High')]),
|
||||
('max_cooking_time', 'Max Cooking Time', [('20', '20 min'), ('30', '30 min'), ('45', '45 min'),
|
||||
('60', '60 min')]),
|
||||
('skill_level', 'Cooking Skill', [('Beginner (simple steps)', 'Beginner'), ('Intermediate
|
||||
(comfortable with techniques)', 'Intermediate'), ('Expert (advanced methods)', 'Advanced')]),
|
||||
('dietary_preference', 'Dietary Preference', [('Standard (Omnivore)', 'Standard'),
|
||||
('Vegetarian', 'Vegetarian'), ('Vegan', 'Vegan'), ('Pescatarian', 'Pescatarian')]),
|
||||
('dietary_focus', 'Dietary Focus', [('Balanced', 'Balanced'), ('High Protein', 'High Protein'),
|
||||
('Low Carb', 'Low Carb'), ('Kid-Friendly', 'Kid-Friendly')])
|
||||
] %}
|
||||
|
||||
{% for name, label, options in fields %}
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-stone-400 uppercase tracking-widest mb-3">{{
|
||||
label }}</label>
|
||||
<select name="{{ name }}"
|
||||
class="w-full px-4 py-3 bg-stone-50 border border-stone-200 rounded-xl text-stone-700 focus:ring-2 focus:ring-emerald-800/20 focus:border-emerald-800 outline-none transition-all appearance-none cursor-pointer">
|
||||
{% for val, display in options %}
|
||||
<option value="{{ val }}" {% if val=='2 adults' or val=='Medium' or val=='30' or
|
||||
val=='Intermediate (comfortable with techniques)' or val=='Standard (Omnivore)' or
|
||||
val=='Balanced' %}selected{% endif %}>{{ display }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="pt-8 border-t border-stone-50 space-y-6">
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-semibold text-stone-400 uppercase tracking-widest mb-3">Meal
|
||||
Strategy</label>
|
||||
<select name="meal_strategy"
|
||||
class="w-full px-4 py-3 bg-stone-50 border border-stone-200 rounded-xl text-stone-700 focus:ring-2 focus:ring-emerald-800/20 focus:border-emerald-800 outline-none transition-all appearance-none cursor-pointer">
|
||||
<option value="Maximize ingredient reuse" selected>Maximize Reuse (Zero Waste)</option>
|
||||
<option value="Variety-focused">Maximum Variety</option>
|
||||
<option value="Quick & Easy">Quick & Easy</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-semibold text-stone-400 uppercase tracking-widest mb-3">Fridge
|
||||
Clear-out</label>
|
||||
<input type="text" name="fridge_items" placeholder="e.g. 2 Carrots, half a cabbage..."
|
||||
class="w-full px-4 py-4 border-2 border-emerald-50/50 rounded-xl bg-emerald-50/30 text-stone-700 focus:border-emerald-800/30 focus:bg-white outline-none transition-all placeholder:text-stone-300">
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-xs font-semibold text-stone-400 uppercase tracking-widest mb-3">Output
|
||||
Language</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="relative flex-1 cursor-pointer group">
|
||||
<input type="radio" name="output_language" value="English" checked
|
||||
class="peer sr-only">
|
||||
<div
|
||||
class="flex items-center justify-center gap-3 p-3 bg-stone-50 border border-stone-200 rounded-xl peer-checked:border-emerald-800 peer-checked:bg-emerald-50 transition-all">
|
||||
<span class="text-2xl">🇬🇧</span>
|
||||
<span
|
||||
class="text-sm font-medium text-stone-600 group-hover:text-stone-900">English</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="relative flex-1 cursor-pointer group">
|
||||
<input type="radio" name="output_language" value="Danish" class="peer sr-only">
|
||||
<div
|
||||
class="flex items-center justify-center gap-3 p-3 bg-stone-50 border border-stone-200 rounded-xl peer-checked:border-emerald-800 peer-checked:bg-emerald-50 transition-all">
|
||||
<span class="text-2xl">🇩🇰</span>
|
||||
<span
|
||||
class="text-sm font-medium text-stone-600 group-hover:text-stone-900">Dansk</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn"
|
||||
class="w-full bg-emerald-800 text-white font-medium py-5 rounded-2xl shadow-lg hover:bg-emerald-700 hover:-translate-y-0.5 transition-all duration-300 flex items-center justify-center gap-3">
|
||||
<span id="btnText">Generate Personalized Plan</span>
|
||||
|
||||
<div id="btnLoader" class="hidden">
|
||||
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
|
||||
stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-1">
|
||||
<div class="mb-6 flex items-center gap-3 px-1">
|
||||
<div class="w-8 h-8 bg-stone-100 rounded-lg flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-stone-600" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-xl font-medium text-stone-800 tracking-tight">Recent Activity</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 max-h-[800px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
{% for plan in plans %}
|
||||
<div
|
||||
class="bg-white p-6 rounded-2xl border border-stone-100 shadow-sm hover:shadow-md hover:border-emerald-100 transition-all group">
|
||||
<h3 class="text-stone-800 font-medium leading-snug group-hover:text-emerald-800 transition-colors">
|
||||
{{ plan.description }}</h3>
|
||||
<div class="flex justify-between items-center mt-6">
|
||||
<span class="text-[10px] font-bold text-stone-300 uppercase tracking-widest">Plan #{{ plan.id
|
||||
}}</span>
|
||||
<a href="{{ url_for('view_plan', plan_id=plan.id) }}"
|
||||
class="text-xs font-semibold text-emerald-800 flex items-center gap-1 hover:gap-2 transition-all">
|
||||
View Plan <span>→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-16 bg-stone-50/50 rounded-3xl border-2 border-dashed border-stone-100">
|
||||
<p class="text-stone-400 font-light italic text-sm px-4">Your culinary history will appear here.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Subtle scrollbar for history sidebar */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #e7e5e4;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #d6d3d1;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 1. Identify the form and button elements
|
||||
const planForm = document.querySelector('form[action="{{ url_for("generate") }}"]');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const btnText = document.getElementById('btnText');
|
||||
const btnLoader = document.getElementById('btnLoader');
|
||||
|
||||
// 2. Add submission logic
|
||||
if (planForm && submitBtn) {
|
||||
planForm.addEventListener('submit', function () {
|
||||
// Disable button to prevent double-clicks
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Trigger Nordic-style loading animations
|
||||
submitBtn.classList.add('opacity-80', 'cursor-not-allowed', 'animate-pulse');
|
||||
|
||||
// Update text and show the SVG spinner
|
||||
if (btnText) btnText.innerText = 'Generating your plan...';
|
||||
if (btnLoader) btnLoader.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
111
templates/index.html
Normal file
111
templates/index.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-6xl mx-auto px-6 py-12 md:py-24">
|
||||
<div class="flex flex-col items-center text-center space-y-8">
|
||||
<!-- <div class="inline-flex items-center gap-2 bg-stone-100 text-stone-600 px-4 py-1.5 rounded-full text-xs font-medium tracking-widest uppercase border border-stone-200">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
||||
</span>
|
||||
MiMo-V2 Intelligence
|
||||
</div> -->
|
||||
|
||||
<h1 class="text-5xl md:text-7xl font-light text-stone-900 leading-tight tracking-tight">
|
||||
Dinner, simplified <br>
|
||||
<span class="font-serif italic text-emerald-800">by design.</span>
|
||||
</h1>
|
||||
|
||||
<p class="max-w-xl text-lg text-stone-500 font-light leading-relaxed">
|
||||
A minimalist approach to meal planning. Clear out your fridge, reduce waste, and regain your evenings with AI-tailored dinner plans.
|
||||
</p>
|
||||
|
||||
<div class="pt-4">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="/dashboard" class="bg-stone-900 text-stone-50 px-10 py-4 rounded-full font-medium transition-all hover:bg-stone-800 hover:shadow-xl active:scale-95 flex items-center gap-3">
|
||||
Open Dashboard
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="flex flex-col sm:flex-row gap-4 items-center">
|
||||
<a href="/register" class="bg-emerald-800 text-white px-10 py-4 rounded-full font-medium shadow-sm hover:bg-emerald-900 transition-all active:scale-95">
|
||||
Get Started
|
||||
</a>
|
||||
<a href="/login" class="text-stone-600 hover:text-stone-900 px-8 py-4 font-medium transition-colors">
|
||||
Sign In
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-32 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 border-t border-stone-100 pt-16">
|
||||
<div class="group space-y-4">
|
||||
<div class="w-12 h-12 bg-amber-50 rounded-2xl flex items-center justify-center text-amber-700 group-hover:bg-amber-100 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-stone-800 tracking-tight">Purely Personal</h3>
|
||||
<p class="text-stone-500 text-sm leading-relaxed font-light">
|
||||
Whether you're Keto, Vegan, want kid-friendly meals or gourmet meals, every recipe is filtered for your unique needs.
|
||||
</p>
|
||||
</div>
|
||||
<div class="group space-y-4">
|
||||
<div class="w-12 h-12 bg-emerald-50 rounded-2xl flex items-center justify-center text-emerald-700 group-hover:bg-emerald-100 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-stone-800 tracking-tight">Zero Waste</h3>
|
||||
<p class="text-stone-500 text-sm leading-relaxed font-light">
|
||||
Our "Fridge Clear-out" logic prioritizes what you already have, reducing food waste and grocery bills.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="group space-y-4">
|
||||
<div class="w-12 h-12 bg-orange-50 rounded-2xl flex items-center justify-center text-orange-700 group-hover:bg-orange-100 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-stone-800 tracking-tight">Smart Matrix</h3>
|
||||
<p class="text-stone-500 text-sm leading-relaxed font-light">
|
||||
Instantly visualize how ingredients are reused across your week with our automated usage matrix.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="group space-y-4">
|
||||
<div class="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-700 group-hover:bg-blue-100 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-stone-800 tracking-tight">Time-Conscious</h3>
|
||||
<p class="text-stone-500 text-sm leading-relaxed font-light">
|
||||
From 20-minute rapid meals to gourmet weekend dinners, we plan around your schedule.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<footer>
|
||||
{% if source_link %}
|
||||
<div class="source-link">
|
||||
<a href="{{ source_link }}" target="_blank" rel="noopener">
|
||||
View Source Code
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Adding a soft background gradient for that 'Nordic Fog' feel */
|
||||
body {
|
||||
background-color: #fdfdfc;
|
||||
background-image: radial-gradient(#e5e7eb 0.5px, transparent 0.5px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
18
templates/login.html
Normal file
18
templates/login.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto bg-white p-8 rounded-2xl shadow-lg mt-10">
|
||||
<h2 class="text-2xl font-bold mb-6">Welcome Back</h2>
|
||||
<form method="POST">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1">Username</label>
|
||||
<input type="text" name="username" class="w-full p-3 border rounded-lg" required>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-1">Password</label>
|
||||
<input type="password" name="password" class="w-full p-3 border rounded-lg" required>
|
||||
</div>
|
||||
<button type="submit" class="w-full bg-green-600 text-white py-3 rounded-lg font-bold">Login</button>
|
||||
</form>
|
||||
<p class="mt-4 text-center text-sm">Don't have an account? <a href="/register" class="text-green-600">Sign up</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
144
templates/plan_result.html
Normal file
144
templates/plan_result.html
Normal file
@@ -0,0 +1,144 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-5xl mx-auto px-4 py-6">
|
||||
<div class="flex justify-between items-center mb-8 no-print">
|
||||
<a href="{{ url_for('dashboard') }}" class="text-gray-500 hover:text-green-600 transition flex items-center gap-2">
|
||||
<span class="text-xl">←</span> Dashboard
|
||||
</a>
|
||||
<div class="flex gap-3">
|
||||
<button onclick="window.print()" class="bg-black text-white px-5 py-2 rounded-xl font-bold text-sm shadow-lg hover:bg-gray-800 transition">
|
||||
Print Full Plan 🖨️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex border-b border-gray-200 mb-8 no-print sticky top-0 bg-gray-50 z-10 py-2 overflow-x-auto gap-2">
|
||||
<button onclick="showTab('summary')" class="tab-btn active-tab px-6 py-2 font-bold text-green-600 border-b-2 border-green-600 whitespace-nowrap">Overview</button>
|
||||
<button onclick="showTab('shopping')" class="tab-btn px-6 py-2 text-gray-500 hover:text-green-600 whitespace-nowrap">Shopping List 🛒</button>
|
||||
<button onclick="showTab('recipes')" class="tab-btn px-6 py-2 text-gray-500 hover:text-green-600 whitespace-nowrap">Recipes 👨🍳</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-8 rounded-3xl shadow-xl border border-gray-100 min-h-[60vh]">
|
||||
<div id="summary" class="tab-content prose prose-slate max-w-none">
|
||||
{{ plan.summary | safe }}
|
||||
</div>
|
||||
|
||||
<div id="shopping" class="tab-content hidden prose prose-slate max-w-none">
|
||||
{{ plan.shopping | safe }}
|
||||
</div>
|
||||
|
||||
<div id="recipes" class="tab-content hidden prose prose-slate max-w-none">
|
||||
{{ plan.recipes | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Styling for the interactive shopping list */
|
||||
.active-tab { color: #059669 !important; border-bottom: 2px solid #059669 !important; }
|
||||
|
||||
input[type="checkbox"]:checked + .checkbox-text {
|
||||
text-decoration: line-through;
|
||||
color: #9ca3af;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#shopping ul { list-style-type: none; padding-left: 0; }
|
||||
#shopping li { margin-bottom: 0.5rem; }
|
||||
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.tab-content { display: block !important; margin-bottom: 50px; page-break-after: always; }
|
||||
.bg-white { box-shadow: none !important; border: none !important; }
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<style>
|
||||
/* 1. Your existing tab and checkbox styles */
|
||||
.active-tab { color: #059669 !important; border-bottom: 2px solid #059669 !important; }
|
||||
|
||||
input[type="checkbox"]:checked + .checkbox-text {
|
||||
text-decoration: line-through;
|
||||
color: #9ca3af;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#shopping ul { list-style-type: none; padding-left: 0; }
|
||||
#shopping li { margin-bottom: 0.5rem; }
|
||||
|
||||
/* 2. PASTE THE NEW TABLE STYLES HERE */
|
||||
.prose table {
|
||||
width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
border: 1px solid #e5e7eb !important;
|
||||
margin-top: 1.5rem !important;
|
||||
border-radius: 0.75rem !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.prose th {
|
||||
background-color: #f8fafc !important;
|
||||
padding: 0.75rem !important;
|
||||
border: 1px solid #e5e7eb !important;
|
||||
text-transform: uppercase !important;
|
||||
font-size: 0.75rem !important;
|
||||
letter-spacing: 0.05em !important;
|
||||
}
|
||||
|
||||
.prose td {
|
||||
padding: 0.75rem !important;
|
||||
border: 1px solid #e5e7eb !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.prose td:has(span.matrix-x),
|
||||
.prose td:empty {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 3. Your existing print logic stays at the bottom */
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.tab-content { display: block !important; margin-bottom: 50px; page-break-after: always; }
|
||||
.bg-white { box-shadow: none !important; border: none !important; }
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script>
|
||||
// 1. Tab Switching Logic
|
||||
function showTab(tabId) {
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active-tab', 'font-bold', 'text-green-600', 'border-b-2', 'border-green-600'));
|
||||
|
||||
document.getElementById(tabId).classList.remove('hidden');
|
||||
event.currentTarget.classList.add('active-tab', 'font-bold', 'text-green-600', 'border-b-2', 'border-green-600');
|
||||
}
|
||||
|
||||
// 2. Render Markdown and Add Interactive Elements
|
||||
window.onload = function() {
|
||||
// Render each section
|
||||
document.querySelectorAll('.tab-content').forEach(div => {
|
||||
const rawContent = div.textContent.trim();
|
||||
div.innerHTML = marked.parse(rawContent);
|
||||
});
|
||||
|
||||
// Add checkboxes to shopping list
|
||||
const listItems = document.querySelectorAll('#shopping li');
|
||||
listItems.forEach(item => {
|
||||
const text = item.innerHTML;
|
||||
item.innerHTML = `
|
||||
<label class="flex items-center space-x-3 cursor-pointer p-2 hover:bg-gray-50 rounded-lg group">
|
||||
<input type="checkbox" class="w-5 h-5 rounded border-gray-300 text-green-600 focus:ring-green-500 transition cursor-pointer">
|
||||
<span class="text-gray-700 checkbox-text">${text}</span>
|
||||
</label>
|
||||
`;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
66
templates/register.html
Normal file
66
templates/register.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto bg-white p-8 rounded-2xl shadow-lg mt-10">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-3xl font-extrabold text-gray-900">Opret bruger</h2>
|
||||
<p class="text-gray-500 mt-2">Få madplaner tilpasset dine ønsker i dag.</p>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" id="registerForm">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-semibold mb-2 text-gray-700">Username</label>
|
||||
<input type="text" name="username" placeholder="e.g. HealthyEats2024"
|
||||
class="w-full p-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-green-500 focus:outline-none transition" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-semibold mb-2 text-gray-700">Password</label>
|
||||
<input type="password" id="password" name="password" placeholder="••••••••"
|
||||
class="w-full p-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-green-500 focus:outline-none transition" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-semibold mb-2 text-gray-700">Confirm Password</label>
|
||||
<input type="password" id="confirm_password" placeholder="••••••••"
|
||||
class="w-full p-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-green-500 focus:outline-none transition" required>
|
||||
<p id="error-msg" class="text-red-500 text-xs mt-2 hidden">Passwords do not match!</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="regBtn" class="w-full bg-green-600 hover:bg-green-500 text-white py-3 rounded-xl font-bold shadow-md transition-all active:scale-95">
|
||||
Create Account
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 pt-6 border-t border-gray-100 text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Already have an account?
|
||||
<a href="/login" class="text-green-600 font-bold hover:underline">Log in</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('registerForm');
|
||||
const password = document.getElementById('password');
|
||||
const confirm = document.getElementById('confirm_password');
|
||||
const errorMsg = document.getElementById('error-msg');
|
||||
|
||||
form.onsubmit = function(e) {
|
||||
if (password.value !== confirm.value) {
|
||||
e.preventDefault();
|
||||
errorMsg.classList.remove('hidden');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user