Convert to Flask app with password-protected admin panel

Links now live in data/links.json instead of hardcoded HTML, editable
via /admin (add/rename/reorder/delete sections and links, per-link
color and half-width side-by-side layout). New pages can be added
without code changes. Docker image only needs rebuilding on code
changes now, not on content changes (data/ is volume-mounted).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Sonnet 5 committed 2026-07-23 22:50:05 +02:00
1 parent 56dc37843a
commit 61c4701115
13 files changed
+797 -173

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
ADMIN_PASSWORD=change-me
SECRET_KEY=change-me-to-a-long-random-string
COOKIE_SECURE=true
+3
View File
@@ -0,0 +1,3 @@
.env
__pycache__/
*.pyc
+249
View File
@@ -0,0 +1,249 @@
import json
import os
import re
import secrets
from functools import wraps
from pathlib import Path
from flask import Flask, abort, redirect, render_template, request, session, url_for
app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32)
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["SESSION_COOKIE_SECURE"] = os.environ.get("COOKIE_SECURE", "true").lower() != "false"
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD")
DATA_FILE = Path(os.environ.get("DATA_FILE", "data/links.json"))
COLORS = ["blue", "green", "orange", "pink"]
def load_data():
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
def save_data(data):
tmp = DATA_FILE.with_suffix(".tmp")
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
tmp.replace(DATA_FILE)
def login_required(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not session.get("admin"):
return redirect(url_for("admin_dashboard"))
return view(*args, **kwargs)
return wrapped
def get_page_or_404(data, page_key):
if page_key not in data:
abort(404)
return data[page_key]
def get_section_or_404(page, section_id):
for section in page["sections"]:
if section["id"] == section_id:
return section
abort(404)
@app.route("/")
def index():
data = load_data()
return render_template("page.html", page=data["index"])
@app.route("/<slug>.html")
def page(slug):
data = load_data()
page_data = data.get(slug)
if not page_data:
abort(404)
return render_template("page.html", page=page_data)
@app.route("/admin", methods=["GET"])
def admin_dashboard():
if not session.get("admin"):
return render_template("admin_login.html", error=None)
data = load_data()
return render_template("admin.html", data=data, colors=COLORS)
@app.route("/admin/login", methods=["POST"])
def admin_login():
if not ADMIN_PASSWORD:
return render_template(
"admin_login.html", error="ADMIN_PASSWORD ist nicht gesetzt (siehe .env)."
), 500
if secrets.compare_digest(request.form.get("password", ""), ADMIN_PASSWORD):
session["admin"] = True
return redirect(url_for("admin_dashboard"))
return render_template("admin_login.html", error="Falsches Passwort"), 401
@app.route("/admin/logout", methods=["POST"])
def admin_logout():
session.clear()
return redirect(url_for("admin_dashboard"))
@app.route("/admin/<page_key>/header", methods=["POST"])
@login_required
def edit_header(page_key):
data = load_data()
page = get_page_or_404(data, page_key)
page["title"] = request.form.get("title", page.get("title", "")).strip()
page["subtitle"] = request.form.get("subtitle", page.get("subtitle", "")).strip()
page["subtitle2"] = request.form.get("subtitle2", page.get("subtitle2", "")).strip()
page["footer_label"] = request.form.get("footer_label", page.get("footer_label", "")).strip()
page["footer_href"] = request.form.get("footer_href", page.get("footer_href", "")).strip()
save_data(data)
return redirect(url_for("admin_dashboard"))
@app.route("/admin/page/add", methods=["POST"])
@login_required
def add_page():
data = load_data()
title = request.form.get("title", "").strip()
slug = request.form.get("slug", "").strip().lower()
slug = re.sub(r"[^a-z0-9-]+", "-", slug).strip("-")
if slug and title and slug not in data:
data[slug] = {
"title": title,
"subtitle": "",
"subtitle2": "",
"footer_label": "",
"footer_href": "",
"sections": [],
}
save_data(data)
return redirect(url_for("admin_dashboard"))
@app.route("/admin/page/<page_key>/delete", methods=["POST"])
@login_required
def delete_page(page_key):
if page_key == "index":
abort(400)
data = load_data()
data.pop(page_key, None)
save_data(data)
return redirect(url_for("admin_dashboard"))
@app.route("/admin/<page_key>/section/add", methods=["POST"])
@login_required
def add_section(page_key):
data = load_data()
page = get_page_or_404(data, page_key)
name = request.form.get("name", "").strip()
if name:
page["sections"].append({"id": secrets.token_hex(4), "name": name, "links": []})
save_data(data)
return redirect(url_for("admin_dashboard"))
@app.route("/admin/<page_key>/section/<section_id>/rename", methods=["POST"])
@login_required
def rename_section(page_key, section_id):
data = load_data()
page = get_page_or_404(data, page_key)
section = get_section_or_404(page, section_id)
name = request.form.get("name", "").strip()
if name:
section["name"] = name
save_data(data)
return redirect(url_for("admin_dashboard"))
@app.route("/admin/<page_key>/section/<section_id>/delete", methods=["POST"])
@login_required
def delete_section(page_key, section_id):
data = load_data()
page = get_page_or_404(data, page_key)
page["sections"] = [s for s in page["sections"] if s["id"] != section_id]
save_data(data)
return redirect(url_for("admin_dashboard"))
@app.route("/admin/<page_key>/section/<section_id>/link/add", methods=["POST"])
@login_required
def add_link(page_key, section_id):
data = load_data()
page = get_page_or_404(data, page_key)
section = get_section_or_404(page, section_id)
title = request.form.get("title", "").strip()
url = request.form.get("url", "").strip()
if title and url:
section["links"].append(
{
"id": secrets.token_hex(4),
"title": title,
"note": request.form.get("note", "").strip(),
"url": url,
"color": request.form.get("color", "blue"),
"half": bool(request.form.get("half")),
}
)
save_data(data)
return redirect(url_for("admin_dashboard"))
@app.route("/admin/<page_key>/link/<link_id>/edit", methods=["POST"])
@login_required
def edit_link(page_key, link_id):
data = load_data()
page = get_page_or_404(data, page_key)
for section in page["sections"]:
for link in section["links"]:
if link["id"] == link_id:
link["title"] = request.form.get("title", link["title"]).strip()
link["note"] = request.form.get("note", link["note"]).strip()
link["url"] = request.form.get("url", link["url"]).strip()
link["color"] = request.form.get("color", link["color"])
link["half"] = bool(request.form.get("half"))
save_data(data)
return redirect(url_for("admin_dashboard"))
@app.route("/admin/<page_key>/link/<link_id>/delete", methods=["POST"])
@login_required
def delete_link(page_key, link_id):
data = load_data()
page = get_page_or_404(data, page_key)
for section in page["sections"]:
section["links"] = [l for l in section["links"] if l["id"] != link_id]
save_data(data)
return redirect(url_for("admin_dashboard"))
@app.route("/admin/<page_key>/link/<link_id>/move", methods=["POST"])
@login_required
def move_link(page_key, link_id):
data = load_data()
page = get_page_or_404(data, page_key)
direction = request.form.get("direction")
for section in page["sections"]:
ids = [l["id"] for l in section["links"]]
if link_id in ids:
idx = ids.index(link_id)
new_idx = idx - 1 if direction == "up" else idx + 1
if 0 <= new_idx < len(section["links"]):
section["links"][idx], section["links"][new_idx] = (
section["links"][new_idx],
section["links"][idx],
)
save_data(data)
return redirect(url_for("admin_dashboard"))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)
+184
View File
@@ -0,0 +1,184 @@
{
"index": {
"title": "Honey Services for Cinnamon",
"subtitle": "Welcome!",
"subtitle2": "Here are all my important links for you!",
"footer_label": "Cinnamon Version",
"footer_href": "honey.html",
"sections": [
{
"id": "s_important",
"name": "Important Stuff",
"links": [
{
"id": "l_crochet",
"title": "CrochetMeMaybe🧶",
"note": "Crochet Website Counting",
"url": "https://crochetmemaybe.asianxpolish.duckdns.org",
"color": "blue",
"half": false
},
{
"id": "l_pixelpaper1",
"title": "PixelPaper📖",
"note": "EBook and Manga Server",
"url": "https://pixelpaper.asianxpolish.duckdns.org",
"color": "blue",
"half": false
},
{
"id": "l_ticket1",
"title": "Ticket System 🎆",
"note": "Tickets an Honey",
"url": "https://hot.asianxpolish.duckdns.org",
"color": "blue",
"half": false
},
{
"id": "l_learn1",
"title": "Learning Tracker✒️",
"note": "Tracker für das lernen und rewards!",
"url": "https://learn.asianxpolish.duckdns.org",
"color": "blue",
"half": false
},
{
"id": "l_live",
"title": "Livestream!👾",
"note": "Dein Persönlicher Livestream",
"url": "https://live.asianxpolish.duckdns.org",
"color": "blue",
"half": false
}
]
},
{
"id": "s_working",
"name": "Working Stuff",
"links": [
{
"id": "l_honeycloud",
"title": "Honeycloud🍯",
"note": "The HoneyCloud Service",
"url": "https://honeyycloud.duckdns.org",
"color": "green",
"half": false
},
{
"id": "l_pocket1",
"title": "Pocket-ID📓",
"note": "Dein User in all den Systemen (Passkey)",
"url": "https://pocket.asianxpolish.duckdns.org",
"color": "green",
"half": false
}
]
},
{
"id": "s_tools1",
"name": "Tools",
"links": [
{
"id": "l_pdf1",
"title": "PDF Tool📝",
"note": "PDF Editieren und Zusammenfügen",
"url": "https://pdf.asianxpolish.duckdns.org",
"color": "orange",
"half": false
}
]
}
]
},
"honey": {
"title": "My Links",
"subtitle": "",
"subtitle2": "Some Stuff",
"footer_label": "Honey Version",
"footer_href": "index.html",
"sections": [
{
"id": "s_pangolin",
"name": "Public with Pangolin",
"links": [
{
"id": "l_pixelpaper2",
"title": "PixelPaper📖",
"note": "EBook and Manga Server",
"url": "https://pixelpaper.asianxpolish.duckdns.org",
"color": "orange",
"half": true
},
{
"id": "l_catto",
"title": "Catto🐈‍⬛",
"note": "",
"url": "https://catto.asianxpolish.duckdns.org",
"color": "orange",
"half": true
}
]
},
{
"id": "s_tools2",
"name": "Tools",
"links": [
{
"id": "l_pdf2",
"title": "PDF Tool📝",
"note": "PDF Editieren und Zusammenfügen",
"url": "https://pdf.asianxpolish.duckdns.org",
"color": "pink",
"half": false
},
{
"id": "l_pocket2",
"title": "Pocket-ID",
"note": "OIDC Passkey",
"url": "https://pocket.asianxpolish.duckdns.org",
"color": "green",
"half": false
}
]
},
{
"id": "s_cinnamon",
"name": "For Cinnamon",
"links": [
{
"id": "l_crochet2",
"title": "CrochetMeMaybe🧶",
"note": "Crochet Website Counting",
"url": "https://crochetmemaybe.asianxpolish.duckdns.org",
"color": "blue",
"half": false
},
{
"id": "l_ticket2",
"title": "Ticket System 🎆",
"note": "",
"url": "https://hot.asianxpolish.duckdns.org/admin.html",
"color": "blue",
"half": false
},
{
"id": "l_learn2",
"title": "Learning Tracker✒️",
"note": "",
"url": "https://learn.asianxpolish.duckdns.org",
"color": "blue",
"half": false
},
{
"id": "32be2487",
"title": "Admin Page",
"note": "",
"url": "https://asianxpolish.duckdns.org/admin",
"color": "blue",
"half": false
}
]
}
]
}
}
+13
View File
@@ -0,0 +1,13 @@
services:
honey-links:
build: .
ports:
- "8080:80"
environment:
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
- SECRET_KEY=${SECRET_KEY}
# Set to "false" only for local http-only testing without a TLS proxy in front.
- COOKIE_SECURE=${COOKIE_SECURE:-true}
volumes:
- ./data:/app/data
restart: unless-stopped
+12 -7
View File
@@ -1,16 +1,21 @@
# Use the official Python image # Use the official Python image
FROM python:3.12-slim FROM python:3.14-slim
# Set the working directory in the container # Set the working directory in the container
WORKDIR /app WORKDIR /app
# Copy the HTML files into the container # Install dependencies
COPY index.html /app COPY requirements.txt .
COPY honey.html /app RUN pip install --no-cache-dir -r requirements.txt
COPY styles.css /app
# Copy the application into the container
COPY app.py .
COPY templates/ templates/
COPY static/ static/
COPY data/ data/
# Expose port 80 # Expose port 80
EXPOSE 80 EXPOSE 80
# Start the Python HTTP server on port 80 # Start the app via a production WSGI server
CMD ["python3", "-m", "http.server", "80"]^ CMD ["waitress-serve", "--host=0.0.0.0", "--port=80", "app:app"]
-78
View File
@@ -1,78 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Honey | Links</title>
<link rel="icon" type="image/x-icon" href="/images/favicon/favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="background-glow"></div>
<!-- Fireflies container -->
<div class="fireflies">
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
</div>
<!-- Content on top -->
<img src="https://c.tenor.com/rfib5QqA0ncAAAAd/tenor.gif" alt="Profile Picture" class="profile-pic" />
<h1 class="fade-in">My Links</h1>
<p class="bio2 fade-in">Some Stuff</p>
<h2 class="section-title">Public with Pangolin</h2>
<a href="https://pixelpaper.asianxpolish.duckdns.org" class="link animated-link orange-glow" target="_blank">
<div class="link-title">PixelPaper📖</div>
<div class="link-note">EBook and Manga Server</div>
</a>
<a href="https://catto.asianxpolish.duckdns.org" class="link animated-link orange-glow" target="_blank">
<div class="link-title">Catto🐈‍⬛</div>
</a>
<h2 class="section-title">Tools</h2>
<a href="https://pdf.asianxpolish.duckdns.org" class="link animated-link pink-glow" target="_blank">
<div class="link-title">PDF Tool📝</div>
<div class="link-note">PDF Editieren und Zusammenfügen</div>
</a>
<a href="https://pocket.asianxpolish.duckdns.org" class="link animated-link green-glow" target="_blank">
<div class="link-title">Pocket-ID</div>
<div class="link-note">OIDC Passkey</div>
</a>
<h2 class="section-title">For Cinnamon</h2>
<a href="https://crochetmemaybe.asianxpolish.duckdns.org" class="link animated-link blue-glow" target="_blank">
<div class="link-title">CrochetMeMaybe🧶</div>
<div class="link-note">Crochet Website Counting</div>
</a>
<a href="https://hot.asianxpolish.duckdns.org/admin.html" class="link animated-link blue-glow" target="_blank">
<div class="link-title">Ticket System 🎆</div>
</a>
<a href="https://learn.asianxpolish.duckdns.org" class="link animated-link blue-glow" target="_blank">
<div class="link-title">Learning Tracker✒️</div>
</a>
<footer>
<a href="index.html"target="_self" title="Switch to Cinnamon Version" class="link-footer">
<div>Honey Version</div>
</a>
© 2025 Marc Luv
</footer>
</body>
</html>
-85
View File
@@ -1,85 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Honey | Links</title>
<link rel="icon" type="image/x-icon" href="/images/favicon/favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="background-glow"></div>
<!-- Fireflies container -->
<div class="fireflies">
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
</div>
<!-- Content on top -->
<img src="https://c.tenor.com/rfib5QqA0ncAAAAd/tenor.gif" alt="Profile Picture" class="profile-pic" />
<h1 class="fade-in">Honey Services for Cinnamon</h1>
<p class="bio fade-in">Welcome!</p>
<p class="bio2 fade-in">Here are all my important links for you!</p>
<h2 class="section-title">Important Stuff</h2>
<a href="https://crochetmemaybe.asianxpolish.duckdns.org" class="link animated-link blue-glow" target="_blank">
<div class="link-title">CrochetMeMaybe🧶</div>
<div class="link-note">Crochet Website Counting</div>
</a>
<a href="https://pixelpaper.asianxpolish.duckdns.org" class="link animated-link blue-glow" target="_blank">
<div class="link-title">PixelPaper📖</div>
<div class="link-note">EBook and Manga Server</div>
</a>
<a href="https://hot.asianxpolish.duckdns.org" class="link animated-link blue-glow" target="_blank">
<div class="link-title">Ticket System 🎆</div>
<div class="link-note">Tickets an Honey</div>
</a>
<a href="https://learn.asianxpolish.duckdns.org" class="link animated-link blue-glow" target="_blank">
<div class="link-title">Learning Tracker✒️</div>
<div class="link-note">Tracker für das lernen und rewards!</div>
</a>
<a href="https://live.asianxpolish.duckdns.org" class="link animated-link blue-glow" target="_blank">
<div class="link-title">Livestream!👾</div>
<div class="link-note">Dein Persönlicher Livestream</div>
</a>
<h2 class="section-title">Working Stuff</h2>
<a href="https://honeyycloud.duckdns.org" class="link animated-link green-glow" target="_blank">
<div class="link-title">Honeycloud🍯</div>
<div class="link-note">The HoneyCloud Service</div>
</a>
<a href="https://pocket.asianxpolish.duckdns.org" class="link animated-link green-glow" target="_blank">
<div class="link-title">Pocket-ID📓</div>
<div class="link-note">Dein User in all den Systemen (Passkey)</div>
</a>
<h2 class="section-title">Tools</h2>
<a href="https://pdf.asianxpolish.duckdns.org" class="link animated-link orange-glow" target="_blank">
<div class="link-title">PDF Tool📝</div>
<div class="link-note">PDF Editieren und Zusammenfügen</div>
</a>
<footer>
<a href="honey.html"target="_self" title="Switch to Honey Version" class="link-footer">
<div>Cinnamon Version</div>
</a>
© 2025 Marc Luv
</footer>
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
Flask>=3.0,<4
waitress>=3.0,<4
+20 -3
View File
@@ -144,11 +144,18 @@ animation: pulseGlow 3s ease-in-out infinite;
} }
.link { .links-grid {
display: block; display: flex;
flex-wrap: wrap;
gap: 10px;
width: 100%; width: 100%;
max-width: 320px; max-width: 320px;
margin: 6px 0; margin-bottom: 14px;
}
.link {
display: block;
flex: 0 0 100%;
padding: 14px 24px; padding: 14px 24px;
background-color: rgba(255, 255, 255, 0.04); background-color: rgba(255, 255, 255, 0.04);
color: #f3f4f6; color: #f3f4f6;
@@ -160,6 +167,12 @@ font-weight: 600;
text-align: left; text-align: left;
} }
/* Opt-in: two links next to each other instead of full width */
.link.half {
flex: 0 0 calc(50% - 5px);
padding: 14px 14px;
}
.link:hover { .link:hover {
background-color: rgba(255, 255, 255, 0.07); /* softer highlight */ background-color: rgba(255, 255, 255, 0.07); /* softer highlight */
color: #e0e7ff; /* gentle light blue tint */ color: #e0e7ff; /* gentle light blue tint */
@@ -307,6 +320,10 @@ will-change: opacity, transform, filter;
font-size: 14px; font-size: 14px;
padding: 12px 20px; padding: 12px 20px;
} }
.link.half {
flex-basis: 100%;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
+209
View File
@@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Admin – Honey Links</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<style>
body {
display: block;
padding: 24px;
max-width: 900px;
margin: 0 auto;
overflow-x: visible;
}
h1 { text-align: left; }
h2.section-title { animation: none; }
.top-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.admin-page { margin-bottom: 40px; border: 1px solid #374151; border-radius: 10px; padding: 20px; }
.admin-page h2 { color: #a5f3fc; margin-bottom: 16px; }
.admin-section { border: 1px solid #374151; border-radius: 8px; padding: 12px; margin-bottom: 16px; }
.section-head { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.admin-link-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; padding: 8px 0; border-bottom: 1px solid #1f2937; }
.admin-link-row:last-of-type { border-bottom: none; }
.admin-link-row form.edit-form {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
flex: 1 1 100%;
}
.admin-link-row .link-actions {
display: flex;
gap: 6px;
align-items: center;
}
form.inline { display: inline-flex; gap: 6px; align-items: center; }
input[type=text], input[type=url], input[type=password] {
padding: 6px 8px;
border-radius: 4px;
border: 1px solid #4b5563;
background: #0f172a;
color: #f3f4f6;
}
select {
padding: 6px 8px;
border-radius: 4px;
background: #0f172a;
color: #f3f4f6;
border: 1px solid #4b5563;
}
button {
padding: 6px 12px;
border-radius: 4px;
border: 1px solid #4b5563;
background: rgba(255, 255, 255, 0.06);
color: #f3f4f6;
cursor: pointer;
white-space: nowrap;
}
button:hover { background: rgba(255, 255, 255, 0.12); }
.btn-danger { border-color: #b91c1c; color: #fca5a5; }
.new-form { margin-top: 12px; padding-top: 12px; border-top: 1px dashed #374151; }
label.checkbox { display: flex; align-items: center; gap: 4px; font-size: 13px; color: #9ca3af; white-space: nowrap; }
.field-title { flex: 1 1 130px; min-width: 100px; }
.field-note { flex: 1 1 150px; min-width: 100px; }
.field-url { flex: 2 1 180px; min-width: 140px; }
.quick-nav { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 24px; }
.quick-nav a {
padding: 6px 12px;
border-radius: 999px;
border: 1px solid #4b5563;
background: rgba(255, 255, 255, 0.04);
color: #a5f3fc;
text-decoration: none;
font-size: 13px;
}
.quick-nav a:hover { background: rgba(255, 255, 255, 0.1); }
.view-link { font-size: 13px; color: #21e4d3; text-decoration: none; margin-left: 10px; white-space: nowrap; }
.view-link:hover { color: #24ffed; }
</style>
</head>
<body>
<div class="top-bar">
<h1>Admin</h1>
<form method="post" action="{{ url_for('admin_logout') }}"><button type="submit">Logout</button></form>
</div>
<div class="quick-nav">
{% for page_key in data.keys() %}
<a href="#page-{{ page_key }}">↓ {{ page_key }}</a>
{% endfor %}
</div>
<div class="admin-page">
<h2>Neue Seite anlegen</h2>
<form method="post" action="{{ url_for('add_page') }}" style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
<input type="text" name="title" placeholder="Titel der Seite" required>
<input type="text" name="slug" placeholder="url-slug (z.B. gaming)" pattern="[a-zA-Z0-9-]+" required>
<button type="submit">+ Seite anlegen</button>
</form>
<p style="color:#9ca3af; font-size:13px; margin-top:8px;">Erreichbar danach unter <code>/&lt;slug&gt;.html</code>, z.B. <code>/gaming.html</code>.</p>
</div>
{% for page_key, page in data.items() %}
<div class="admin-page" id="page-{{ page_key }}">
<div class="section-head">
<h2 style="margin-bottom:0;">
Seite: {{ page_key }} — erreichbar unter {% if page_key == 'index' %}<code>/</code>{% else %}<code>/{{ page_key }}.html</code>{% endif %}
<a class="view-link" href="{{ '/' if page_key == 'index' else '/' + page_key + '.html' }}" target="_blank">🔗 ansehen</a>
</h2>
{% if page_key != 'index' %}
<form method="post" action="{{ url_for('delete_page', page_key=page_key) }}" onsubmit="return confirm('Seite \'{{ page_key }}\' komplett inkl. aller Sections/Links löschen?');">
<button type="submit" class="btn-danger">Seite löschen</button>
</form>
{% endif %}
</div>
<form method="post" action="{{ url_for('edit_header', page_key=page_key) }}" style="margin-bottom: 20px; display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;">
<div>
<label class="checkbox">Titel</label>
<input type="text" name="title" value="{{ page.title }}">
</div>
<div>
<label class="checkbox">Untertitel 1</label>
<input type="text" name="subtitle" value="{{ page.subtitle or '' }}">
</div>
<div>
<label class="checkbox">Untertitel 2</label>
<input type="text" name="subtitle2" value="{{ page.subtitle2 or '' }}">
</div>
<div>
<label class="checkbox">Footer-Link-Ziel</label>
<input type="text" name="footer_href" value="{{ page.footer_href or '' }}" placeholder="z.B. honey.html">
</div>
<div>
<label class="checkbox">Footer-Link-Text</label>
<input type="text" name="footer_label" value="{{ page.footer_label or '' }}" placeholder="z.B. Honey Version">
</div>
<button type="submit">Header speichern</button>
</form>
{% for section in page.sections %}
<div class="admin-section">
<div class="section-head">
<form method="post" action="{{ url_for('rename_section', page_key=page_key, section_id=section.id) }}" class="inline">
<input type="text" name="name" value="{{ section.name }}">
<button type="submit">Umbenennen</button>
</form>
<form method="post" action="{{ url_for('delete_section', page_key=page_key, section_id=section.id) }}" onsubmit="return confirm('Section \'{{ section.name }}\' inkl. aller Links wirklich löschen?');">
<button type="submit" class="btn-danger">Section löschen</button>
</form>
</div>
{% for link in section.links %}
<div class="admin-link-row">
<form method="post" action="{{ url_for('edit_link', page_key=page_key, link_id=link.id) }}" class="edit-form">
<input class="field-title" type="text" name="title" value="{{ link.title }}">
<input class="field-note" type="text" name="note" value="{{ link.note or '' }}" placeholder="Notiz">
<input class="field-url" type="url" name="url" value="{{ link.url }}">
<select name="color">
{% for c in colors %}
<option value="{{ c }}" {% if link.color == c %}selected{% endif %}>{{ c }}</option>
{% endfor %}
</select>
<label class="checkbox"><input type="checkbox" name="half" {% if link.half %}checked{% endif %}> nebeneinander</label>
<div class="link-actions">
<button type="submit">Speichern</button>
</div>
</form>
<div class="link-actions">
<form method="post" action="{{ url_for('move_link', page_key=page_key, link_id=link.id) }}" class="inline">
<input type="hidden" name="direction" value="up">
<button type="submit" title="nach oben">↑</button>
</form>
<form method="post" action="{{ url_for('move_link', page_key=page_key, link_id=link.id) }}" class="inline">
<input type="hidden" name="direction" value="down">
<button type="submit" title="nach unten">↓</button>
</form>
<form method="post" action="{{ url_for('delete_link', page_key=page_key, link_id=link.id) }}" class="inline" onsubmit="return confirm('Link \'{{ link.title }}\' wirklich löschen?');">
<button type="submit" class="btn-danger">Löschen</button>
</form>
</div>
</div>
{% endfor %}
<form method="post" action="{{ url_for('add_link', page_key=page_key, section_id=section.id) }}" class="new-form" style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
<input class="field-title" type="text" name="title" placeholder="Titel" required>
<input class="field-note" type="text" name="note" placeholder="Notiz (optional)">
<input class="field-url" type="url" name="url" placeholder="https://..." required>
<select name="color">
{% for c in colors %}
<option value="{{ c }}">{{ c }}</option>
{% endfor %}
</select>
<label class="checkbox"><input type="checkbox" name="half"> nebeneinander (halbe Breite)</label>
<button type="submit">+ Link hinzufügen</button>
</form>
</div>
{% endfor %}
<form method="post" action="{{ url_for('add_section', page_key=page_key) }}" style="margin-top: 12px; display: flex; gap: 8px;">
<input type="text" name="name" placeholder="Neue Section" required>
<button type="submit">+ Section hinzufügen</button>
</form>
</div>
{% endfor %}
</body>
</html>
+52
View File
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Admin Login</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<style>
.admin-box {
max-width: 320px;
margin: 80px auto;
padding: 24px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid #4b5563;
border-radius: 10px;
}
.admin-box label { display: block; margin-bottom: 6px; color: #9ca3af; font-size: 13px; }
.admin-box input {
width: 100%;
padding: 10px;
margin-bottom: 16px;
border-radius: 6px;
border: 1px solid #4b5563;
background: #0f172a;
color: #f3f4f6;
}
.admin-box button {
width: 100%;
padding: 10px;
border-radius: 6px;
border: none;
background: #21e4d3;
color: #0f172a;
font-weight: 600;
cursor: pointer;
}
.admin-box button:hover { background: #24ffed; }
.admin-error { color: #fca5a5; margin-bottom: 12px; font-size: 14px; }
</style>
</head>
<body>
<div class="admin-box">
<h1 style="font-size: 20px; margin-bottom: 16px; text-align: left;">Admin Login</h1>
{% if error %}<div class="admin-error">{{ error }}</div>{% endif %}
<form method="post" action="{{ url_for('admin_login') }}">
<label for="password">Passwort</label>
<input type="password" id="password" name="password" autofocus required>
<button type="submit">Einloggen</button>
</form>
</div>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Honey | Links</title>
<link rel="icon" type="image/x-icon" href="/images/favicon/favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<div class="background-glow"></div>
<!-- Fireflies container -->
<div class="fireflies">
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
<div class="firefly"></div>
</div>
<!-- Content on top -->
<img src="https://c.tenor.com/rfib5QqA0ncAAAAd/tenor.gif" alt="Profile Picture" class="profile-pic" />
<h1 class="fade-in">{{ page.title }}</h1>
{% if page.subtitle %}<p class="bio fade-in">{{ page.subtitle }}</p>{% endif %}
{% if page.subtitle2 %}<p class="bio2 fade-in">{{ page.subtitle2 }}</p>{% endif %}
{% for section in page.sections %}
<h2 class="section-title">{{ section.name }}</h2>
<div class="links-grid">
{% for link in section.links %}
<a href="{{ link.url }}" class="link animated-link {{ link.color }}-glow{% if link.half %} half{% endif %}" target="_blank" style="--i: {{ loop.index0 }}">
<div class="link-title">{{ link.title }}</div>
{% if link.note %}<div class="link-note">{{ link.note }}</div>{% endif %}
</a>
{% endfor %}
</div>
{% endfor %}
<footer>
<a href="{{ page.footer_href }}" target="_self" title="Switch Version" class="link-footer">
<div>{{ page.footer_label }}</div>
</a>
© 2025 Marc Luv
</footer>
</body>
</html>