Merge branch 'pwa'
This commit is contained in:
6
app.py
6
app.py
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from flask import Flask, render_template, redirect, url_for, request, flash
|
from flask import Flask, render_template, redirect, url_for, request, flash, send_from_directory
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_login import (
|
from flask_login import (
|
||||||
LoginManager,
|
LoginManager,
|
||||||
@@ -58,7 +58,11 @@ class MealPlan(db.Model):
|
|||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(user_id))
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
|
# PWA service worker
|
||||||
|
|
||||||
|
@app.route('/sw.js')
|
||||||
|
def serve_sw():
|
||||||
|
return send_from_directory('static', 'sw.js', mimetype='application/javascript')
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
|
|||||||
BIN
static/icons/192.png
Normal file
BIN
static/icons/192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
static/icons/512.png
Normal file
BIN
static/icons/512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
20
static/manifest.json
Normal file
20
static/manifest.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "Madplaner AI",
|
||||||
|
"short_name": "Madplaner",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#2c3e50",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/static/icons/192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/icons/512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
20
static/sw.js
Normal file
20
static/sw.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// static/sw.js
|
||||||
|
const CACHE_NAME = 'madplaner-install';
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
// Clear out any old caches from previous versions
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((names) => {
|
||||||
|
return Promise.all(names.map(name => caches.delete(name)));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Network-only fetch: No caching, just live data
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
event.respondWith(fetch(event.request));
|
||||||
|
});
|
||||||
@@ -6,7 +6,19 @@
|
|||||||
<title>Madplaner</title>
|
<title>Madplaner</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
|
||||||
|
<meta name="theme-color" content="#2c3e50">
|
||||||
|
<link rel="apple-touch-icon" href="/static/icons/192.png">
|
||||||
|
<link rel="icon" href="https://fav.farm/%F0%9F%8D%B4">
|
||||||
|
<!-- cache busting for serivce worker -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}?v=1.1">
|
||||||
</head>
|
</head>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then(() => console.log("Service Worker Registered"));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<body class="bg-gray-50 text-gray-900 font-sans">
|
<body class="bg-gray-50 text-gray-900 font-sans">
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
|||||||
@@ -2,26 +2,20 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-6xl mx-auto px-6 py-12 md:py-24">
|
<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="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">
|
<h1 class="text-5xl md:text-7xl font-light text-stone-900 leading-tight tracking-tight">
|
||||||
Dinner, simplified <br>
|
Dinner, simplified <br>
|
||||||
<span class="font-serif italic text-emerald-800">by design.</span>
|
<span class="font-serif italic text-emerald-800">by design.</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="max-w-xl text-lg text-stone-500 font-light leading-relaxed">
|
<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.
|
A minimalist approach to meal planning. Clear out your fridge, reduce waste, and regain your evenings with
|
||||||
|
AI-tailored dinner plans.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
{% if current_user.is_authenticated %}
|
{% 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">
|
<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
|
Open Dashboard
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||||
@@ -29,7 +23,8 @@
|
|||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="flex flex-col sm:flex-row gap-4 items-center">
|
<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">
|
<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
|
Get Started
|
||||||
</a>
|
</a>
|
||||||
<a href="/login" class="text-stone-600 hover:text-stone-900 px-8 py-4 font-medium transition-colors">
|
<a href="/login" class="text-stone-600 hover:text-stone-900 px-8 py-4 font-medium transition-colors">
|
||||||
@@ -48,10 +43,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold text-stone-800 tracking-tight">Purely Personal</h3>
|
<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">
|
<p class="text-stone-500 text-sm leading-relaxed font-light">Custom filters for Keto, Vegan, or family-friendly gourmet needs.</p>
|
||||||
Whether you're Keto, Vegan, want kid-friendly meals or gourmet meals, every recipe is filtered for your unique needs.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="group space-y-4">
|
<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">
|
<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">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@@ -59,9 +53,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold text-stone-800 tracking-tight">Zero Waste</h3>
|
<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">
|
<p class="text-stone-500 text-sm leading-relaxed font-light">Fridge clear-out logic that prioritizes what you already own.</p>
|
||||||
Our "Fridge Clear-out" logic prioritizes what you already have, reducing food waste and grocery bills.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="group space-y-4">
|
<div class="group space-y-4">
|
||||||
@@ -71,9 +63,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold text-stone-800 tracking-tight">Smart Matrix</h3>
|
<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">
|
<p class="text-stone-500 text-sm leading-relaxed font-light">Instantly visualize ingredient reuse across your week.</p>
|
||||||
Instantly visualize how ingredients are reused across your week with our automated usage matrix.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="group space-y-4">
|
<div class="group space-y-4">
|
||||||
@@ -83,29 +73,76 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-semibold text-stone-800 tracking-tight">Time-Conscious</h3>
|
<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">
|
<p class="text-stone-500 text-sm leading-relaxed font-light">From 20-minute rapid meals to gourmet weekend dinners.</p>
|
||||||
From 20-minute rapid meals to gourmet weekend dinners, we plan around your schedule.
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="install-banner" class="hidden mt-12 p-6 rounded-3xl items-center justify-between gap-4 border border-white/40 bg-white/30 backdrop-blur-md shadow-xl ring-1 ring-stone-900/5 transition-opacity duration-500">
|
||||||
|
<div class="flex items-center space-x-4 text-left">
|
||||||
|
<div class="p-3 bg-emerald-600 rounded-2xl text-white">
|
||||||
|
<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="2" d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<div>
|
||||||
|
<h3 class="text-stone-800 font-semibold tracking-tight">Madplaner App</h3>
|
||||||
|
<p class="text-stone-500 text-xs">Add to home screen for the best experience.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="install-button" class="bg-emerald-600 text-white px-6 py-2.5 rounded-full text-sm font-medium hover:bg-emerald-800 transition-all active:scale-95">
|
||||||
|
Install
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="mt-24 border-t border-stone-100 pt-8 flex flex-col md:flex-row justify-between items-center gap-4 text-stone-400 text-xs">
|
||||||
|
|
||||||
{% if source_link %}
|
{% if source_link %}
|
||||||
<div class="source-link">
|
<a href="{{ source_link }}" target="_blank" rel="noopener" class="flex items-center gap-2 hover:text-emerald-800 transition-colors group">
|
||||||
<a href="{{ source_link }}" target="_blank" rel="noopener">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-50 group-hover:opacity-100" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
View Source Code
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium underline decoration-stone-200 underline-offset-4 group-hover:decoration-emerald-800">Source Code</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Adding a soft background gradient for that 'Nordic Fog' feel */
|
|
||||||
body {
|
body {
|
||||||
background-color: #fdfdfc;
|
background-color: #fdfdfc;
|
||||||
background-image: radial-gradient(#e5e7eb 0.5px, transparent 0.5px);
|
background-image: radial-gradient(#e5e7eb 0.5px, transparent 0.5px);
|
||||||
background-size: 40px 40px;
|
background-size: 40px 40px;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
@media (display-mode: standalone) {
|
||||||
|
#install-banner { display: none !important; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let deferredPrompt;
|
||||||
|
const installBanner = document.getElementById('install-banner');
|
||||||
|
|
||||||
|
window.addEventListener('beforeinstallprompt', (e) => {
|
||||||
|
console.log('Install prompt captured');
|
||||||
|
e.preventDefault();
|
||||||
|
deferredPrompt = e;
|
||||||
|
|
||||||
|
// Ensure banner is visible and styled
|
||||||
|
installBanner.classList.remove('hidden');
|
||||||
|
installBanner.classList.add('flex');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('install-button')?.addEventListener('click', async () => {
|
||||||
|
if (!deferredPrompt) return;
|
||||||
|
|
||||||
|
installBanner.classList.add('hidden');
|
||||||
|
await deferredPrompt.prompt();
|
||||||
|
|
||||||
|
const { outcome } = await deferredPrompt.userChoice;
|
||||||
|
console.log(`User response: ${outcome}`);
|
||||||
|
deferredPrompt = null;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user