Add PunktFri Flask landing page
- Landing page with hero, problemet, målgruppe, idé, disclaimer sections - Signup form → SQLite + file log (navn, email, domæner, egne_ns, kommentar) - /tak confirmation page - /admin with HTTP Basic Auth - /health endpoint - Mobile-first minimal CSS, Danish throughout - Honest 'under undersøgelse' framing — no premature organisational claims
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.venv/
|
||||||
|
*.db
|
||||||
|
*.log
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
45
README.md
45
README.md
@@ -0,0 +1,45 @@
|
|||||||
|
# PunktFri.dk
|
||||||
|
|
||||||
|
Flask landing page for PunktFri — a Danish non-profit registrar initiative for self-hosters.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit `http://localhost:5000`
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `PORT` | `5000` | Port to listen on |
|
||||||
|
| `DATABASE` | `punktfri.db` | SQLite database path |
|
||||||
|
| `LOG_FILE` | `signups.log` | Signup log file path |
|
||||||
|
| `ADMIN_USER` | `admin` | Admin username |
|
||||||
|
| `ADMIN_PASS` | `punktfri2024` | Admin password |
|
||||||
|
|
||||||
|
Change `ADMIN_USER` and `ADMIN_PASS` before deploying publicly.
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
- `/` — Landing page + signup form
|
||||||
|
- `/tak` — Confirmation page after signup
|
||||||
|
- `/admin` — Signup table (HTTP Basic Auth)
|
||||||
|
- `/health` — Health check (returns `{"status": "ok"}`)
|
||||||
|
|
||||||
|
## Data
|
||||||
|
|
||||||
|
Signups are stored in two places:
|
||||||
|
- **SQLite** (`punktfri.db`) — queryable, used by `/admin`
|
||||||
|
- **Log file** (`signups.log`) — append-only plaintext log
|
||||||
|
|
||||||
|
Both are created automatically on first run.
|
||||||
|
|
||||||
|
## Admin
|
||||||
|
|
||||||
|
Visit `http://localhost:5000/admin` and enter the credentials above.
|
||||||
|
|||||||
137
app.py
Normal file
137
app.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, g, Response
|
||||||
|
|
||||||
|
DATABASE = os.environ.get("DATABASE", "punktfri.db")
|
||||||
|
LOG_FILE = os.environ.get("LOG_FILE", "signups.log")
|
||||||
|
ADMIN_USER = os.environ.get("ADMIN_USER", "admin")
|
||||||
|
ADMIN_PASS = os.environ.get("ADMIN_PASS", "punktfri2024")
|
||||||
|
PORT = int(os.environ.get("PORT", 5000))
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
signup_logger = logging.getLogger("signups")
|
||||||
|
signup_logger.setLevel(logging.INFO)
|
||||||
|
_fh = logging.FileHandler(LOG_FILE)
|
||||||
|
_fh.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
|
||||||
|
signup_logger.addHandler(_fh)
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = getattr(g, "_database", None)
|
||||||
|
if db is None:
|
||||||
|
db = g._database = sqlite3.connect(DATABASE)
|
||||||
|
db.row_factory = sqlite3.Row
|
||||||
|
return db
|
||||||
|
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close_db(exception):
|
||||||
|
db = getattr(g, "_database", None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
with app.app_context():
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS signups (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
navn TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
domaener INTEGER NOT NULL,
|
||||||
|
egne_ns TEXT NOT NULL,
|
||||||
|
kommentar TEXT,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
auth = request.authorization
|
||||||
|
if not auth or auth.username != ADMIN_USER or auth.password != ADMIN_PASS:
|
||||||
|
return Response(
|
||||||
|
"Login krævet.",
|
||||||
|
401,
|
||||||
|
{"WWW-Authenticate": 'Basic realm="PunktFri Admin"'},
|
||||||
|
)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/", methods=["GET", "POST"])
|
||||||
|
def index():
|
||||||
|
error = None
|
||||||
|
if request.method == "POST":
|
||||||
|
navn = request.form.get("navn", "").strip()
|
||||||
|
email = request.form.get("email", "").strip().lower()
|
||||||
|
domaener_raw = request.form.get("domaener", "").strip()
|
||||||
|
egne_ns = request.form.get("egne_ns", "").strip()
|
||||||
|
kommentar = request.form.get("kommentar", "").strip()
|
||||||
|
|
||||||
|
if not navn or not email or not domaener_raw or egne_ns not in ("ja", "nej"):
|
||||||
|
error = "Udfyld venligst alle påkrævede felter."
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
domaener = int(domaener_raw)
|
||||||
|
if domaener < 1:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
error = "Angiv et gyldigt antal domæner (mindst 1)."
|
||||||
|
else:
|
||||||
|
db = get_db()
|
||||||
|
existing = db.execute(
|
||||||
|
"SELECT id FROM signups WHERE email = ?", (email,)
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
error = "Denne e-mail er allerede tilmeldt — tak for din interesse!"
|
||||||
|
else:
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO signups (navn, email, domaener, egne_ns, kommentar) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
(navn, email, domaener, egne_ns, kommentar or None),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
signup_logger.info(
|
||||||
|
f"SIGNUP navn={navn!r} email={email!r} domaener={domaener} "
|
||||||
|
f"egne_ns={egne_ns} kommentar={kommentar!r}"
|
||||||
|
)
|
||||||
|
return redirect(url_for("tak"))
|
||||||
|
|
||||||
|
return render_template("index.html", error=error)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/tak")
|
||||||
|
def tak():
|
||||||
|
return render_template("tak.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin")
|
||||||
|
@require_admin
|
||||||
|
def admin():
|
||||||
|
db = get_db()
|
||||||
|
signups = db.execute(
|
||||||
|
"SELECT * FROM signups ORDER BY timestamp DESC"
|
||||||
|
).fetchall()
|
||||||
|
count = db.execute("SELECT COUNT(*) FROM signups").fetchone()[0]
|
||||||
|
return render_template("admin.html", signups=signups, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/health")
|
||||||
|
def health():
|
||||||
|
return {"status": "ok"}, 200
|
||||||
|
|
||||||
|
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=PORT, debug=False)
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Flask==3.1.0
|
||||||
|
Werkzeug==3.1.3
|
||||||
563
static/style.css
Normal file
563
static/style.css
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
|
color: #1a1a1a;
|
||||||
|
line-height: 1.65;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Nav ─────────────────────────────── */
|
||||||
|
header {
|
||||||
|
background: #0f172a;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav {
|
||||||
|
max-width: 860px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo span {
|
||||||
|
color: #818cf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Hero ────────────────────────────── */
|
||||||
|
.hero {
|
||||||
|
background: #0f172a;
|
||||||
|
color: #fff;
|
||||||
|
padding: 5rem 1.5rem 4.5rem;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: clamp(2.2rem, 6vw, 3.8rem);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .tagline {
|
||||||
|
font-size: clamp(1rem, 2.5vw, 1.3rem);
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .intro {
|
||||||
|
max-width: 580px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sections ────────────────────────── */
|
||||||
|
.section {
|
||||||
|
padding: 3.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-inner {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:nth-child(odd) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:nth-child(even) {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
font-size: 1.55rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.975rem;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Problem list ────────────────────── */
|
||||||
|
.problem-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 1.25rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-list li {
|
||||||
|
padding: 0.7rem 0 0.7rem 1.6rem;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-list li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-list li::before {
|
||||||
|
content: "→";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #dc2626;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-list li strong {
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Target grid ─────────────────────── */
|
||||||
|
.target-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.25rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 580px) {
|
||||||
|
.target-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box {
|
||||||
|
padding: 1.25rem 1.25rem 1rem;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box.not-for {
|
||||||
|
background: #fff5f5;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box.is-for {
|
||||||
|
background: #f0fdf4;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box h3 {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box.not-for h3 {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box.is-for h3 {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box ul li {
|
||||||
|
padding: 0.3rem 0 0.3rem 1.3rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #374151;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box.not-for ul li::before {
|
||||||
|
content: "✕";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-box.is-for ul li::before {
|
||||||
|
content: "✓";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #16a34a;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Idea list ───────────────────────── */
|
||||||
|
.idea-items {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.idea-items li {
|
||||||
|
padding: 0.75rem 0 0.75rem 1.6rem;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.idea-items li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.idea-items li::before {
|
||||||
|
content: "·";
|
||||||
|
position: absolute;
|
||||||
|
left: 0.45rem;
|
||||||
|
top: 0.45rem;
|
||||||
|
color: #6366f1;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fcd34d;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Not-list ────────────────────────── */
|
||||||
|
.not-list {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.not-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-list li {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.7rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #475569;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-list li::before {
|
||||||
|
content: "✕";
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Signup form ─────────────────────── */
|
||||||
|
.form-section {
|
||||||
|
background: #0f172a !important;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h2 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section .lead {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section .disclaimer {
|
||||||
|
color: #64748b !important;
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .hint {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="email"],
|
||||||
|
.form-group input[type="number"],
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.65rem 0.875rem;
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #f1f5f9;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #6366f1;
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group textarea {
|
||||||
|
min-height: 90px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group input[type="radio"] {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
accent-color: #6366f1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
background: #7f1d1d;
|
||||||
|
border: 1px solid #ef4444;
|
||||||
|
color: #fecaca;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
background: #6366f1;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 0.875rem 2rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
transition: background 0.15s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover {
|
||||||
|
background: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Footer ──────────────────────────── */
|
||||||
|
footer {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner a {
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-inner a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-small {
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tak page ────────────────────────── */
|
||||||
|
.tak-section {
|
||||||
|
min-height: 65vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tak-card {
|
||||||
|
max-width: 520px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tak-icon {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tak-card h1 {
|
||||||
|
font-size: 1.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tak-card p {
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Admin ───────────────────────────── */
|
||||||
|
.admin-section {
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-meta {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signups-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signups-table th {
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signups-table td {
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signups-table tr:hover td {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-ja {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-nej {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
47
templates/admin.html
Normal file
47
templates/admin.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Admin — PunktFri{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-section">
|
||||||
|
<h1>Tilmeldinger</h1>
|
||||||
|
<p class="admin-meta">{{ count }} tilmelding{{ 'er' if count != 1 else '' }} i alt</p>
|
||||||
|
|
||||||
|
{% if signups %}
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table class="signups-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Navn</th>
|
||||||
|
<th>E-mail</th>
|
||||||
|
<th>Domæner</th>
|
||||||
|
<th>Egne NS</th>
|
||||||
|
<th>Kommentar</th>
|
||||||
|
<th>Tidspunkt</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for s in signups %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ s.id }}</td>
|
||||||
|
<td>{{ s.navn }}</td>
|
||||||
|
<td>{{ s.email }}</td>
|
||||||
|
<td>{{ s.domaener }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge-{{ s.egne_ns }}">{{ s.egne_ns | upper }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ s.kommentar or '—' }}</td>
|
||||||
|
<td style="white-space:nowrap; color:#6b7280;">{{ s.timestamp }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>Ingen tilmeldinger endnu.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
29
templates/base.html
Normal file
29
templates/base.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="da">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="PunktFri — non-profit registratorinitiativ for selvhostere med egne navneservere.">
|
||||||
|
<title>{% block title %}PunktFri{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<a href="/" class="logo">Punkt<span>Fri</span></a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="footer-inner">
|
||||||
|
<p><strong>PunktFri er ikke live endnu.</strong> Dette er en interessetilkendegivelse — ingen forpligtelse, ingen faktura, intet abonnement.</p>
|
||||||
|
<p>Kontakt: <a href="mailto:info@punktfri.dk">info@punktfri.dk</a></p>
|
||||||
|
<p class="footer-small">PunktFri er en idé under undersøgelse. Ingen forening er stiftet, ingen akkreditering er søgt.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
161
templates/index.html
Normal file
161
templates/index.html
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}PunktFri — Din domæneaftale. Ingen forhandler.{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<!-- 1. Hero -->
|
||||||
|
<section class="hero">
|
||||||
|
<h1>PunktFri</h1>
|
||||||
|
<p class="tagline">"Din domæneaftale. Ingen forhandler. Ingen bullshit."</p>
|
||||||
|
<p class="intro">
|
||||||
|
Punktum dk tvinger fra 2026 alle .dk-domæneejere til at bruge en kommerciel forhandler.
|
||||||
|
Det rammer dig, der kører egne navneservere og aldrig har haft brug for hjælp.
|
||||||
|
Vi undersøger om vi kan gøre noget ved det — på vores egne præmisser.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. Problemet -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="section-inner">
|
||||||
|
<h2>Problemet</h2>
|
||||||
|
<p>
|
||||||
|
Punktum dk ændrer reglerne. Fra 2026 kan du ikke längre administrere dit .dk-domæne direkte
|
||||||
|
— du skal have en akkrediteret forhandler som mellemled. Uanset om du har brug for dem eller ej.
|
||||||
|
</p>
|
||||||
|
<ul class="problem-list">
|
||||||
|
<li><strong>1. juli 2026:</strong> Nye .dk-domæner kræver en forhandler. Ingen direkte registrering mere.</li>
|
||||||
|
<li><strong>1. juli 2028:</strong> Alle eksisterende .dk-domæner skal overføres til en forhandler.</li>
|
||||||
|
<li><strong>Selvhostere rammes direkte:</strong> Kører du egne navneservere og har altid klaret dig selv hos Punktum dk, skal du nu have et administrativt mellemled — mod din vilje.</li>
|
||||||
|
<li><strong>Prisstigningen er reel:</strong> Kommercielle forhandlere opkræver typisk 90–150 kr/år per domæne. Du betalte ~50 kr direkte. Det er dobbelt til tredobbelt pris for et lag du ikke har bedt om.</li>
|
||||||
|
<li><strong>Ingen teknisk gevinst:</strong> Du mister ikke kontrol over dine navneservere — men du tvinges alligevel til at betale en forhandler for at "hjælpe" dig.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. Hvem er PunktFri for? -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="section-inner">
|
||||||
|
<h2>Er det noget for dig?</h2>
|
||||||
|
<p>PunktFri er tænkt til en meget specifik gruppe. Vær ærlig med dig selv.</p>
|
||||||
|
<div class="target-grid">
|
||||||
|
<div class="target-box not-for">
|
||||||
|
<h3>Ikke for dig hvis</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Du vil have webhosting</li>
|
||||||
|
<li>Du vil have DNS-hosting</li>
|
||||||
|
<li>Du har brug for support på "min hjemmeside virker ikke"</li>
|
||||||
|
<li>Du ikke ved hvad en navneserver er</li>
|
||||||
|
<li>Du leder efter den billigste registrar</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="target-box is-for">
|
||||||
|
<h3>For dig hvis</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Du kører egne navneservere (BIND, PowerDNS, Knot osv.)</li>
|
||||||
|
<li>Du har administreret direkte hos Punktum dk i årevis</li>
|
||||||
|
<li>Du vil beholde fuld kontrol over din infrastruktur</li>
|
||||||
|
<li>Du bare har brug for et tyndt administrativt lag mod Punktum dk</li>
|
||||||
|
<li>Du er ligeglad med support, panels og features</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. Idéen bag PunktFri -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="section-inner">
|
||||||
|
<h2>Idéen bag PunktFri <span class="status-badge">Under undersøgelse</span></h2>
|
||||||
|
<p>
|
||||||
|
PunktFri er på nuværende tidspunkt en idé — ikke en organisation.
|
||||||
|
Vi undersøger om det er muligt og realistisk at etablere følgende:
|
||||||
|
</p>
|
||||||
|
<ul class="idea-items">
|
||||||
|
<li>En non-profit forening der ansøger om akkreditering som registrator hos Punktum dk</li>
|
||||||
|
<li>Kostprismodel — ingen markup, ingen upsell, ingen abonnementsservice du ikke har bedt om</li>
|
||||||
|
<li>Demokratisk struktur — medlemmerne bestemmer, ikke investorer eller kommercielle interesser</li>
|
||||||
|
<li>Du beholder dine egne navneservere fuldstændigt — vi rører ikke din infrastruktur</li>
|
||||||
|
<li>Minimal administration: vi håndterer kun det formelle forhold til Punktum dk på dine vegne</li>
|
||||||
|
</ul>
|
||||||
|
<p style="margin-top: 1.25rem; font-size: 0.875rem; color: #6b7280;">
|
||||||
|
Intet er besluttet endnu. Denne side er til for at afdække om der er nok interesse til at
|
||||||
|
det kan bære sig — juridisk, økonomisk og praktisk.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. Hvad PunktFri IKKE er -->
|
||||||
|
<section class="section">
|
||||||
|
<div class="section-inner">
|
||||||
|
<h2>Hvad PunktFri ikke er</h2>
|
||||||
|
<p>For at undgå enhver tvivl:</p>
|
||||||
|
<ul class="not-list">
|
||||||
|
<li>En hostingudbyder</li>
|
||||||
|
<li>En DNS-udbyder</li>
|
||||||
|
<li>En supportlinje</li>
|
||||||
|
<li>En forretning</li>
|
||||||
|
<li>Et panel med features</li>
|
||||||
|
<li>Noget du betaler for nu</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 6. Signup -->
|
||||||
|
<section class="section form-section">
|
||||||
|
<div class="section-inner">
|
||||||
|
<h2>Er du interesseret?</h2>
|
||||||
|
<p class="lead">Tilmeld dig som interesseret — det forpligter dig til ingenting.</p>
|
||||||
|
<p class="disclaimer">
|
||||||
|
Vi bruger dine oplysninger til at vurdere om der er tilstrækkelig interesse til at gå videre.
|
||||||
|
Ingen spam, ingen salg, ingen automatiske abonnementer. Kun en enkelt opdatering når vi ved mere.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="error-msg">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form class="signup-form" method="post" action="/">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="navn">Navn <span style="color:#ef4444">*</span></label>
|
||||||
|
<input type="text" id="navn" name="navn" required autocomplete="name" placeholder="Dit navn">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">E-mail <span style="color:#ef4444">*</span></label>
|
||||||
|
<input type="email" id="email" name="email" required autocomplete="email" placeholder="du@eksempel.dk">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="domaener">
|
||||||
|
Antal .dk-domæner du administrerer <span style="color:#ef4444">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="number" id="domaener" name="domaener" required min="1" placeholder="fx 3">
|
||||||
|
<span class="hint">Inkludér alle domæner du selv holder styr på</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Kører du egne navneservere? <span style="color:#ef4444">*</span></label>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="egne_ns" value="ja" required>
|
||||||
|
Ja
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="egne_ns" value="nej">
|
||||||
|
Nej
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="kommentar">Kommentarer eller spørgsmål <span style="color:#475569; font-weight:400">(valgfrit)</span></label>
|
||||||
|
<textarea id="kommentar" name="kommentar" placeholder="Har du tanker om modellen, bekymringer, eller noget vi bør tænke over?"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="submit-btn">Ja, jeg er interesseret</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
21
templates/tak.html
Normal file
21
templates/tak.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Tak — PunktFri{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="tak-section">
|
||||||
|
<div class="tak-card">
|
||||||
|
<div class="tak-icon">✓</div>
|
||||||
|
<h1>Tak for din tilmelding</h1>
|
||||||
|
<p>Vi har noteret din interesse. Ingen forpligtelse, ingen faktura, intet abonnement.</p>
|
||||||
|
<p>
|
||||||
|
Vi vender tilbage med en enkelt opdatering, når vi ved om der er tilstrækkelig
|
||||||
|
interesse til at gå videre med idéen.
|
||||||
|
</p>
|
||||||
|
<p style="font-size: 0.8rem; color: #9ca3af;">
|
||||||
|
Spørgsmål? Skriv til <a href="mailto:info@punktfri.dk" style="color: #6366f1;">info@punktfri.dk</a>
|
||||||
|
</p>
|
||||||
|
<a href="/" class="back-link">← Tilbage til forsiden</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user