Add hard-to-guess public URLs (/public/<token>) that show a read-only meal status dashboard for each household. Only people with the link can see the data — no login required. - models.py: public_token + public_enabled columns on households, get_household_by_public_token(), generate_public_token(), set_public_enabled() - meals.py: /public/<token> route, /admin/public-link for generate/disable/enable/regenerate - templates/public.html: clean read-only public dashboard - templates/public-not-found.html: 404 for invalid/disabled links - templates/dashboard.html: admin UI with copy/regenerate/disable - static/style.css: public page and URL box styles - i18n.py: 18 new translation keys (en/fr/de)
This commit is contained in:
@@ -114,6 +114,27 @@
|
||||
</form>
|
||||
|
||||
{% if is_admin %}
|
||||
<form method="POST" action="{{ url_for('meals.admin_public_link') }}" class="manage-form">
|
||||
<h4>🔗 {{ t('Public Sharing Link') }}</h4>
|
||||
<p class="hint">{{ t('Share a read-only view of your household\'s meal status. The link is hard to guess — only people with it can see the data.') }}</p>
|
||||
{% if household.public_token and household.public_enabled %}
|
||||
<div class="public-url-box">
|
||||
<code>{{ request.host_url }}public/{{ household.public_token }}</code>
|
||||
<button type="button" class="btn btn-sm" onclick="copyPublicUrl(this)">{{ t('Copy') }}</button>
|
||||
</div>
|
||||
<div class="btn-row" style="margin-top: 8px;">
|
||||
<button type="submit" name="action" value="generate" class="btn btn-sm">{{ t('Regenerate') }}</button>
|
||||
<button type="submit" name="action" value="disable" class="btn btn-sm btn-danger">{{ t('Disable') }}</button>
|
||||
</div>
|
||||
{% elif household.public_token and not household.public_enabled %}
|
||||
<p class="hint">{{ t('Public link is currently disabled.') }}</p>
|
||||
<button type="submit" name="action" value="enable" class="btn btn-sm">{{ t('Enable') }}</button>
|
||||
{% else %}
|
||||
<p class="hint">{{ t('No public link yet. Generate one to share.') }}</p>
|
||||
<button type="submit" name="action" value="generate" class="btn btn-sm">{{ t('Generate Link') }}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ url_for('meals.admin_set_timezone') }}" class="manage-form">
|
||||
<h4>🕐 {{ t('Household Timezone') }}</h4>
|
||||
<p class="hint">{{ t('Used to send reminders at 10:00 (lunch) and 16:00 (dinner) local time.') }}</p>
|
||||
@@ -146,4 +167,22 @@
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyPublicUrl(btn) {
|
||||
var code = btn.parentElement.querySelector('code');
|
||||
if (!code) return;
|
||||
navigator.clipboard.writeText(code.textContent).then(function() {
|
||||
var orig = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
btn.disabled = true;
|
||||
setTimeout(function() { btn.textContent = orig; btn.disabled = false; }, 2000);
|
||||
}).catch(function() {
|
||||
var range = document.createRange();
|
||||
range.selectNode(code);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ t('Not Found — Meal Tracker') }}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>🔗 {{ t('Link Not Found') }}</h2>
|
||||
<p class="empty-state">{{ t("This sharing link is invalid or has been disabled. Please ask the household admin for a new link.") }}</p>
|
||||
<p class="empty-state" style="margin-top: 2rem;"><a href="{{ url_for('auth.login') }}">{{ t('Log in') }}</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
<!doctype html>
|
||||
<html lang="{{ lang }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>{{ household.name }} — {{ t('Meal Tracker') }}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body class="public-page">
|
||||
<nav class="navbar">
|
||||
<span class="brand">🍽 {{ household.name }}</span>
|
||||
<span class="nav-household">{{ viewing_date.isoformat() }}</span>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
<h2 class="public-heading">{{ t("Today's Meals") }}</h2>
|
||||
|
||||
<div class="date-nav">
|
||||
<a class="btn btn-sm" href="?date={{ prev_date.isoformat() }}">{{ t('◀ Prev') }}</a>
|
||||
<span class="date-display">{{ viewing_date.isoformat() }}</span>
|
||||
<a class="btn btn-sm" href="?date={{ next_date.isoformat() }}">{{ t('Next ▶') }}</a>
|
||||
{% if viewing_date != today %}
|
||||
<a class="btn btn-sm" href="?">Today</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not dashboard.users %}
|
||||
<p class="empty-state">{{ t('No users yet.') }}</p>
|
||||
{% else %}
|
||||
<div class="table-wrapper">
|
||||
<table class="meal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('User') }}</th>
|
||||
{% for period in dashboard.periods %}
|
||||
<th class="meal-col">{{ t(period.meal_type|capitalize) }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in dashboard.users %}
|
||||
<tr>
|
||||
<td class="user-cell">
|
||||
{{ user.username }}
|
||||
{% if user.is_admin %}<span class="admin-tag">{{ t('admin') }}</span>{% endif %}
|
||||
</td>
|
||||
{% for period in dashboard.periods %}
|
||||
{% set resp = user.responses[period.meal_type] %}
|
||||
<td class="response-cell status-{{ resp.status }}{% if resp.changed_at %} changed-mind{% endif %}">
|
||||
<span class="badge badge-{{ resp.status }}">{{ t('Home') if resp.status == 'yes' else (t('Out') if resp.status == 'no' else t('—')) }}</span>
|
||||
{% if resp.changed_at %}
|
||||
<div class="changed-badge" title="{{ t('Changed mind at {time}', time=resp.changed_at) }}">↻ {{ resp.changed_at[11:16] }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="public-footer">
|
||||
{{ t('Meal Tracker') }}
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<footer class="lang-footer">
|
||||
{% for code, name in {'en': 'English', 'fr': 'Français', 'de': 'Deutsch'}.items() %}
|
||||
<a href="?lang={{ code }}"
|
||||
class="lang-link{% if lang == code %} lang-active{% endif %}"
|
||||
aria-label="{{ name }}">{{ code.upper() }}</a>
|
||||
{% endfor %}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user