Private
Public Access
1
0

feat: pwa functionality

This commit is contained in:
2025-12-30 21:14:27 +01:00
parent 5764905c6c
commit 1a485266ba
7 changed files with 147 additions and 54 deletions

6
app.py
View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/icons/512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

20
static/manifest.json Normal file
View 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
View 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));
});

View File

@@ -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) %}

View File

@@ -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 %}