Added lots of improvements to the webpage.
This commit is contained in:
22
de_som_forsvinner_i_taaken/webpage/.gitignore
vendored
Normal file
22
de_som_forsvinner_i_taaken/webpage/.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Virtual environment
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# SQLite database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
*db-shm
|
||||||
|
*db-wal
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
14
de_som_forsvinner_i_taaken/webpage/Dockerfile
Normal file
14
de_som_forsvinner_i_taaken/webpage/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.13.6-slim
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN python3 -m pip install --upgrade pip
|
||||||
|
RUN python3 -m pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD [ "gunicorn", "--config", "gunicorn-cfg.py", "-k", "sync", "app:app" ]
|
80
de_som_forsvinner_i_taaken/webpage/README.md
Normal file
80
de_som_forsvinner_i_taaken/webpage/README.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Email Capture Flask App
|
||||||
|
|
||||||
|
A minimal Flask service that accepts an email from a form POST and stores it in a SQLite database.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
- `POST /subscribe` — Accepts `email` via HTML form or JSON and stores it.
|
||||||
|
- `GET /stats` — Returns a count of stored subscribers.
|
||||||
|
- `GET /health` — Basic health check.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
1. Create and activate a virtualenv (recommended):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
# or
|
||||||
|
FLASK_DEBUG=1 python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The server listens on `http://localhost:5000`.
|
||||||
|
|
||||||
|
## Configure
|
||||||
|
|
||||||
|
- `DATABASE_PATH` (optional): path to the SQLite database file. Default is `emails.db` in the project root.
|
||||||
|
- `PORT` (optional): server port (default `5000`).
|
||||||
|
|
||||||
|
## Posting from your HTML form
|
||||||
|
|
||||||
|
Your existing form should point to `/subscribe` and include an `email` field:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<form action="http://localhost:5000/subscribe" method="post">
|
||||||
|
<input type="email" name="email" placeholder="you@example.com" required />
|
||||||
|
<button type="submit">Subscribe</button>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
If your web page is served from a different origin (domain/port), you may need to enable CORS on this service (e.g., via a proxy or using `flask-cors`).
|
||||||
|
|
||||||
|
## cURL examples
|
||||||
|
|
||||||
|
- Form POST:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -d "email=test@example.com" http://localhost:5000/subscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
- JSON POST:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"test@example.com"}' \
|
||||||
|
http://localhost:5000/subscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
- Check stats:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5000/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Emails are normalized to lowercase and checked with a simple regex. For strict validation, consider the [`email-validator`](https://pypi.org/project/email-validator/) package.
|
||||||
|
- Duplicate submissions return HTTP 409 with status `duplicate`.
|
||||||
|
- SQLite is suitable for lightweight usage; for higher traffic, consider a server database (PostgreSQL, MySQL) with a proper connection pool.
|
137
de_som_forsvinner_i_taaken/webpage/app.py
Normal file
137
de_som_forsvinner_i_taaken/webpage/app.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
from contextlib import closing
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import Flask, request, jsonify, g
|
||||||
|
|
||||||
|
# Simple (not perfect) email regex. For stricter validation, use `email-validator` package.
|
||||||
|
EMAIL_REGEX = re.compile(r"^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$", re.IGNORECASE)
|
||||||
|
|
||||||
|
DB_PATH = os.getenv("DATABASE_PATH", "emails.db")
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
if "db" not in g:
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
# Basic pragmas for better SQLite behavior
|
||||||
|
cur.execute("PRAGMA foreign_keys = ON;")
|
||||||
|
cur.execute("PRAGMA journal_mode = WAL;")
|
||||||
|
g.db = conn
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = get_db()
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS subscribers (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
ip TEXT,
|
||||||
|
user_agent TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
# Ensure the database/table exists before handling traffic
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close_db(_e=None):
|
||||||
|
db = g.pop("db", None)
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_email(email: str) -> str:
|
||||||
|
return email.strip().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_email(email: str) -> bool:
|
||||||
|
return bool(EMAIL_REGEX.match(email))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/health", methods=["GET"])
|
||||||
|
def health():
|
||||||
|
return jsonify({"status": "ok"}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/stats", methods=["GET"])
|
||||||
|
def stats():
|
||||||
|
db = get_db()
|
||||||
|
with closing(db.cursor()) as cur:
|
||||||
|
cur.execute("SELECT COUNT(*) AS cnt FROM subscribers;")
|
||||||
|
row = cur.fetchone()
|
||||||
|
return jsonify({"subscribers": row["cnt"] if row else 0}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/subscribe", methods=["POST"])
|
||||||
|
def subscribe():
|
||||||
|
# Accept both HTML form (application/x-www-form-urlencoded or multipart/form-data)
|
||||||
|
# and JSON (application/json)
|
||||||
|
email = None
|
||||||
|
if request.is_json:
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
email = data.get("email")
|
||||||
|
else:
|
||||||
|
email = request.form.get("email")
|
||||||
|
|
||||||
|
if not email:
|
||||||
|
return jsonify({"status": "error", "message": "Email is required."}), 400
|
||||||
|
|
||||||
|
email = normalize_email(email)
|
||||||
|
if not is_valid_email(email):
|
||||||
|
return jsonify({"status": "error", "message": "Invalid email format."}), 400
|
||||||
|
|
||||||
|
ip = request.headers.get("X-Forwarded-For", request.remote_addr or "")
|
||||||
|
user_agent = request.headers.get("User-Agent", "")
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
try:
|
||||||
|
with closing(db.cursor()) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO subscribers (email, ip, user_agent) VALUES (?, ?, ?)",
|
||||||
|
(email, ip, user_agent),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
# Unique constraint violation (already subscribed)
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"status": "duplicate",
|
||||||
|
"message": "Email already subscribed.",
|
||||||
|
"email": email,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
409,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Subscribed successfully.",
|
||||||
|
"email": email,
|
||||||
|
"created_at": datetime.utcnow().isoformat() + "Z",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
201,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
port = int(os.getenv("PORT", "5000"))
|
||||||
|
debug = os.getenv("FLASK_DEBUG", "0") == "1"
|
||||||
|
app.run(host="0.0.0.0", port=port, debug=debug)
|
22
de_som_forsvinner_i_taaken/webpage/docker-compose.yml
Normal file
22
de_som_forsvinner_i_taaken/webpage/docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: daarke
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
daarke:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./emails.db:/usr/src/app/emails.db:rw
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.29.1
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./nginx:/etc/nginx/conf.d:ro
|
||||||
|
- ./html:/usr/share/nginx/html:ro
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
9
de_som_forsvinner_i_taaken/webpage/gunicorn-cfg.py
Normal file
9
de_som_forsvinner_i_taaken/webpage/gunicorn-cfg.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
bind = '0.0.0.0:5000'
|
||||||
|
workers = 4
|
||||||
|
worker_class = 'uvicorn.workers.UvicornWorker'
|
||||||
|
accesslog = '-'
|
||||||
|
loglevel = 'debug'
|
||||||
|
capture_output = True
|
||||||
|
enable_stdio_inheritance = True
|
Before Width: | Height: | Size: 746 KiB After Width: | Height: | Size: 746 KiB |
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="no" class="dark">
|
<html lang="no" class="dark">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -9,6 +10,7 @@
|
|||||||
tailwind.config = { darkMode: 'class' };
|
tailwind.config = { darkMode: 'class' };
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100 transition-colors">
|
<body class="bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100 transition-colors">
|
||||||
<header class="bg-white dark:bg-gray-800 shadow">
|
<header class="bg-white dark:bg-gray-800 shadow">
|
||||||
<div class="max-w-5xl mx-auto px-6 py-4 flex justify-between items-center">
|
<div class="max-w-5xl mx-auto px-6 py-4 flex justify-between items-center">
|
||||||
@@ -16,7 +18,8 @@
|
|||||||
<nav class="flex items-center space-x-4 text-gray-700 dark:text-gray-200">
|
<nav class="flex items-center space-x-4 text-gray-700 dark:text-gray-200">
|
||||||
<a href="#boken" class="hover:underline">Boken</a>
|
<a href="#boken" class="hover:underline">Boken</a>
|
||||||
<a href="#om-forfatteren" class="hover:underline">Om forfatteren</a>
|
<a href="#om-forfatteren" class="hover:underline">Om forfatteren</a>
|
||||||
<button id="themeToggle" aria-pressed="true" class="ml-4 inline-flex items-center px-3 py-1 rounded-full border border-gray-400 dark:border-gray-500 bg-transparent text-gray-700 hover:bg-gray-200 dark:text-gray-200 dark:hover:bg-gray-700 transition">
|
<button id="themeToggle" aria-pressed="true"
|
||||||
|
class="ml-4 inline-flex items-center px-3 py-1 rounded-full border border-gray-400 dark:border-gray-500 bg-transparent text-gray-700 hover:bg-gray-200 dark:text-gray-200 dark:hover:bg-gray-700 transition">
|
||||||
<span aria-hidden="true" id="themeIcon">☀︎</span>
|
<span aria-hidden="true" id="themeIcon">☀︎</span>
|
||||||
<span class="sr-only">Veksle lys/mørk modus</span>
|
<span class="sr-only">Veksle lys/mørk modus</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -33,19 +36,21 @@
|
|||||||
<p class="text-lg text-gray-700 dark:text-gray-300 mb-6">
|
<p class="text-lg text-gray-700 dark:text-gray-300 mb-6">
|
||||||
Da et rituelt drap knuser idyllen på den velstående øya Nesøya,
|
Da et rituelt drap knuser idyllen på den velstående øya Nesøya,
|
||||||
blir en pensjonert drapsetterforsker dratt inn i en mørk jakt på sannheten.
|
blir en pensjonert drapsetterforsker dratt inn i en mørk jakt på sannheten.
|
||||||
Mens politiet låser seg fast på et enkelt spor, innser han at den
|
Øyas dypeste hemmeligheter er farligere enn noen kunne forestille seg.
|
||||||
virkelige morderen er en iskald skygge som skjuler seg i alles åsyn,
|
|
||||||
og at øyas dypeste hemmeligheter er farligere enn noen kunne forestille seg.
|
|
||||||
|
|
||||||
<b>Jonas Daarkes debutroman</b> drar deg inn i en verden der grenser
|
<b>Jonas Daarke</b>s debutroman drar deg inn i en verden der grenser
|
||||||
mellom virkelighet og myte viskes ut.
|
mellom virkelighet og myte viskes ut.
|
||||||
</p>
|
</p>
|
||||||
<form class="flex flex-col sm:flex-row gap-4">
|
<form id="subscribe-form" class="flex flex-col sm:flex-row gap-4">
|
||||||
<input type="email" placeholder="Din e-postadresse" class="flex-1 px-4 py-3 rounded-lg border border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" required>
|
<input type="email" name="email" id="email" placeholder="Din e-postadresse" autocomplete="email"
|
||||||
<button type="submit" class="px-6 py-3 rounded-lg border border-gray-400 dark:border-gray-600 bg-transparent text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 transition">
|
class="flex-1 px-4 py-3 rounded-lg border border-gray-400 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
required />
|
||||||
|
<button type="submit"
|
||||||
|
class="px-6 py-3 rounded-lg border border-gray-400 dark:border-gray-600 bg-transparent text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 transition">
|
||||||
Meld meg på
|
Meld meg på
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<p id="message" class="mt-2 text-sm"></p>
|
||||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Få nyheter om lanseringen rett i innboksen din.</p>
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Få nyheter om lanseringen rett i innboksen din.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -55,17 +60,17 @@
|
|||||||
<h2 class="text-2xl font-bold mb-4">Om forfatteren</h2>
|
<h2 class="text-2xl font-bold mb-4">Om forfatteren</h2>
|
||||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
<p class="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||||
For <b>Jonas Daarke</b> er ikke krimhistorier noe man finner på; det er noe man finner.
|
For <b>Jonas Daarke</b> er ikke krimhistorier noe man finner på; det er noe man finner.
|
||||||
Med et skarpt blikk for detaljer og en dyp fascinasjon for hvordan idylliske steder kan skjule mørke hemmeligheter,
|
Med et skarpt blikk for detaljer og en dyp fascinasjon for hvordan idylliske steder kan skjule mørke
|
||||||
|
hemmeligheter,
|
||||||
bruker han sin erfaring til å grave frem fortellingene som ligger begravet rett under overflaten.
|
bruker han sin erfaring til å grave frem fortellingene som ligger begravet rett under overflaten.
|
||||||
Han tror fullt og fast på at de mest rystende mysteriene ikke utspiller seg i mørke bakgater, men bak de lukkede dørene
|
|
||||||
i nabolag akkurat som ditt eget.
|
|
||||||
|
|
||||||
"De som forsvinner i tåken" er hans første utgivelse, men neppe den siste.
|
"De som forsvinner i tåken" er hans første utgivelse, men neppe den siste.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 py-6 text-center text-gray-500 dark:text-gray-400 text-sm">
|
<footer
|
||||||
|
class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 py-6 text-center text-gray-500 dark:text-gray-400 text-sm">
|
||||||
© 2025 Jonas Daarke. Alle rettigheter reservert.
|
© 2025 Jonas Daarke. Alle rettigheter reservert.
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -74,8 +79,8 @@
|
|||||||
const toggle = document.getElementById('themeToggle');
|
const toggle = document.getElementById('themeToggle');
|
||||||
const icon = document.getElementById('themeIcon');
|
const icon = document.getElementById('themeIcon');
|
||||||
|
|
||||||
function applyTheme(mode){
|
function applyTheme(mode) {
|
||||||
if(mode === 'dark') {
|
if (mode === 'dark') {
|
||||||
html.classList.add('dark');
|
html.classList.add('dark');
|
||||||
toggle.setAttribute('aria-pressed', 'true');
|
toggle.setAttribute('aria-pressed', 'true');
|
||||||
icon.textContent = '☀︎';
|
icon.textContent = '☀︎';
|
||||||
@@ -88,9 +93,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(function init(){
|
(function init() {
|
||||||
const stored = localStorage.getItem('theme');
|
const stored = localStorage.getItem('theme');
|
||||||
if(stored === 'light' || stored === 'dark') {
|
if (stored === 'light' || stored === 'dark') {
|
||||||
applyTheme(stored);
|
applyTheme(stored);
|
||||||
} else {
|
} else {
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
@@ -103,5 +108,44 @@
|
|||||||
applyTheme(isDark ? 'light' : 'dark');
|
applyTheme(isDark ? 'light' : 'dark');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById('subscribe-form');
|
||||||
|
const message = document.getElementById('message');
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
message.className = 'mt-2 text-sm';
|
||||||
|
message.textContent = 'Sender...';
|
||||||
|
|
||||||
|
const email = form.elements.email.value.trim();
|
||||||
|
try {
|
||||||
|
const res = await fetch('/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({ email })
|
||||||
|
});
|
||||||
|
|
||||||
|
const isJson = res.headers.get('content-type')?.includes('application/json');
|
||||||
|
const data = isJson ? await res.json() : { message: await res.text() };
|
||||||
|
|
||||||
|
if (res.status === 201) {
|
||||||
|
message.className = 'mt-2 text-sm text-green-600';
|
||||||
|
message.textContent = 'Takk! Du er nå påmeldt.';
|
||||||
|
form.reset();
|
||||||
|
} else if (res.status === 409) {
|
||||||
|
message.className = 'mt-2 text-sm text-yellow-600';
|
||||||
|
message.textContent = 'Denne e-posten er allerede påmeldt.';
|
||||||
|
} else {
|
||||||
|
message.className = 'mt-2 text-sm text-red-600';
|
||||||
|
message.textContent = data.message || 'Noe gikk galt. Prøv igjen.';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
message.className = 'mt-2 text-sm text-red-600';
|
||||||
|
message.textContent = 'Kunne ikke kontakte serveren.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
45
de_som_forsvinner_i_taaken/webpage/nginx/localhost.conf
Normal file
45
de_som_forsvinner_i_taaken/webpage/nginx/localhost.conf
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /subscribe {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
|
proxy_pass http://daarke:5000;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /stats {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
|
proxy_pass http://daarke:5000;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
location /health {
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
|
proxy_pass http://daarke:5000;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
}
|
11
de_som_forsvinner_i_taaken/webpage/requirements.txt
Normal file
11
de_som_forsvinner_i_taaken/webpage/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
blinker==1.9.0
|
||||||
|
click==8.2.1
|
||||||
|
Flask==3.1.1
|
||||||
|
gunicorn==23.0.0
|
||||||
|
h11==0.16.0
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
packaging==25.0
|
||||||
|
uvicorn==0.35.0
|
||||||
|
Werkzeug==3.1.3
|
Reference in New Issue
Block a user