Compare commits
9 Commits
b6abfcfc67
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ba1d36b71a | |||
| c27a054497 | |||
| a0095a993a | |||
| db6b16d261 | |||
|
|
b4245f2c29 | ||
| 8db70323a1 | |||
| 03da907c73 | |||
| fb60f8582d | |||
| f59275deea |
26
Dockerfile
26
Dockerfile
@@ -1,21 +1,21 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
# Arbeitsverzeichnis
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Git installieren
|
# Systemabhängigkeiten (optional, falls gebraucht)
|
||||||
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Repository klonen
|
# Requirements installieren
|
||||||
ARG REPO_URL
|
COPY requirements.txt .
|
||||||
RUN git clone ${REPO_URL} /app
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Abhängigkeiten installieren (falls requirements.txt existiert)
|
# Code kopieren
|
||||||
RUN pip install --no-cache-dir -r requirements.txt || true
|
COPY . .
|
||||||
|
|
||||||
# Entrypoint-Skript für git pull beim Start
|
ENV FLASK_APP=app.py
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
ENV FLASK_ENV=development
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE 5000
|
CMD ["flask", "run", "--host=0.0.0.0"]
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
|
|||||||
32
app.py
32
app.py
@@ -1,12 +1,36 @@
|
|||||||
from flask import Flask, render_template, request
|
from flask import Flask, render_template, request, url_for, redirect, session
|
||||||
|
from sqlalchemy import create_engine, Column, Integer, String
|
||||||
|
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.secret_key = "HURENSOHN"
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def hello_world(): # put application's code here
|
def index():
|
||||||
return render_template("index.html")
|
return render_template('base.html',
|
||||||
|
site_name='Mein Spiel Wiki',
|
||||||
|
current_year=2025,
|
||||||
|
popular_items=[],
|
||||||
|
categories=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
return render_template("login.html")
|
||||||
|
|
||||||
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
return render_template("register.html")
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.pop()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/search', methods=['POST'])
|
||||||
|
def search():
|
||||||
|
return ""
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ".:/app"
|
- ".:/app"
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5001:5000"
|
||||||
environment:
|
environment:
|
||||||
- FLASK_APP=app.py
|
- FLASK_APP=app.py
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=development
|
||||||
@@ -28,7 +28,5 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- ./postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
flask
|
||||||
|
psycopg2-binary
|
||||||
|
sqlalchemy
|
||||||
30
src/Database/Database.py
Normal file
30
src/Database/Database.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self, connection_string: dict):
|
||||||
|
"""
|
||||||
|
connection_string ist ein Dictionary, z. B.:
|
||||||
|
{
|
||||||
|
"driver": "postgresql+psycopg2",
|
||||||
|
"username": "postgres",
|
||||||
|
"password": "mein_passwort",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"database": "myapp"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
self.connection_string = connection_string
|
||||||
|
self.engine = self._create_engine()
|
||||||
|
self.SessionLocal = sessionmaker(bind=self.engine)
|
||||||
|
|
||||||
|
def _create_engine(self):
|
||||||
|
conn = self.connection_string
|
||||||
|
url = f"{conn['driver']}://{conn['username']}:{conn['password']}@{conn['host']}:{conn['port']}/{conn['database']}"
|
||||||
|
return create_engine(url, echo=True)
|
||||||
|
|
||||||
|
def get_engine(self):
|
||||||
|
return self.engine
|
||||||
|
|
||||||
|
def get_session(self):
|
||||||
|
return self.SessionLocal()
|
||||||
15
src/Database/User.py
Normal file
15
src/Database/User.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, func
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
username = Column(String(50), unique=True, nullable=False)
|
||||||
|
password = Column(String(255), nullable=False)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<User(id={self.id}, username='{self.username}')>"
|
||||||
BIN
static/HURENSOHN.ico
Normal file
BIN
static/HURENSOHN.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
@@ -0,0 +1,88 @@
|
|||||||
|
/* Bootstrap Dark Mode aktivieren */
|
||||||
|
:root, [data-bs-theme=dark] {
|
||||||
|
--bs-body-bg: #0f1724; /* Hintergrund */
|
||||||
|
--bs-body-color: #e6eef8; /* Textfarbe */
|
||||||
|
--bs-border-color: rgba(255,255,255,0.1);
|
||||||
|
--bs-link-color: #ffcc33;
|
||||||
|
--bs-link-hover-color: #ffdb66;
|
||||||
|
|
||||||
|
/* Deine Custom-Werte */
|
||||||
|
--accent: #ffcc33;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container */
|
||||||
|
.wrap {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 28px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Topbar */
|
||||||
|
header.topbar {
|
||||||
|
@extend .d-flex, .align-items-center, .justify-content-between, .rounded-3, .p-3;
|
||||||
|
background: var(--bs-dark-bg-subtle, rgba(255,255,255,0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(135deg, var(--accent), #ff9a3c);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #081022;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 320px;
|
||||||
|
gap: 18px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.card, aside.sidebar {
|
||||||
|
background: var(--bs-dark-bg-subtle, rgba(255,255,255,0.05));
|
||||||
|
padding: 18px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero */
|
||||||
|
.hero {
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helpers */
|
||||||
|
a.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #081022;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 880px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,137 @@
|
|||||||
<html lang="de">
|
<!doctype html>
|
||||||
|
<html lang="de" data-bs-theme=dark>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>{% block title %}Spiel-Fandom{% endblock %} — {{ site_name or 'My Game Wiki' }}</title>
|
||||||
|
<meta name="description"
|
||||||
|
content="{% block description %}Fandom-Seite für {{ site_name or 'ein Spiel' }}{% endblock %}">
|
||||||
|
|
||||||
<head>
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<meta charstet="utf-8">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;900&display=swap" rel="stylesheet">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
|
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||||
|
<style>
|
||||||
|
|
||||||
<title> {% block title %} {% endblock %} </title>
|
</style>
|
||||||
|
|
||||||
</head>
|
{% block extra_head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<header class="topbar d-flex justify-content-between p-3">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="logo">GF</div>
|
||||||
|
<div>
|
||||||
|
<h1>{{ site_name or 'Game Fandom' }}</h1>
|
||||||
|
<div style="font-size:0.85rem;color:var(--muted)">{% block tagline %}Community-geführtes Wiki &
|
||||||
|
Guides{% endblock %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<body>
|
<div class="search m-2 d-flex align-items-end" role="search">
|
||||||
|
<div>
|
||||||
|
{% block login_logout %}
|
||||||
|
|
||||||
<h1> Herzlich Willkommen auf der Website der BI23b! </h1>
|
{% endblock %}
|
||||||
<p> Zurücklehnen und genießen... </p>
|
</div>
|
||||||
|
|
||||||
</body>
|
<form action="{{ url_for('search') }}" method="get">
|
||||||
|
<input name="q" placeholder="Suche Artikel, Charaktere, Items..." aria-label="Suche"
|
||||||
|
value="{{ request.args.get('q','') }}" class="form-control">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
</html>
|
<nav style="margin-top:12px;color:var(--muted)">
|
||||||
|
<a href="{{ url_for('index') }}">Startseite</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<main class="card border-0">
|
||||||
|
{% block hero %}
|
||||||
|
<div class="hero"
|
||||||
|
style="background-image: url('{{ url_for('static', filename='images/hero.jpg') }}');"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h2 class="title">{% block page_title %}Willkommen{% endblock %}</h2>
|
||||||
|
<div class="meta">{% block meta %}Letzte Aktualisierung: —{% endblock %}</div>
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Hier kommt der Seiteninhalt.</p>
|
||||||
|
{% endblock %}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<section style="margin-top:18px">
|
||||||
|
<h3>Beliebte Artikel</h3>
|
||||||
|
<div class="card-row">
|
||||||
|
{% for item in popular_items|default([]) %}
|
||||||
|
<div class="mini-card">
|
||||||
|
<a href="{{ url_for('article', slug=item.slug) }}"><strong>{{ item.title }}</strong></a>
|
||||||
|
<div style="color:var(--muted);font-size:0.9rem">{{ item.short_desc }}</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="mini-card">Keine Daten.</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div style="margin-bottom:12px">
|
||||||
|
<h4>Neueste Diskussionen</h4>
|
||||||
|
<ul style="padding-left:14px;color:var(--muted);margin-top:8px">
|
||||||
|
{% for d in discussions|default([]) %}
|
||||||
|
<li><a href="{{ url_for('discussion', id=d.id) }}">{{ d.title }}</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li>Keine Diskussionen.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:12px">
|
||||||
|
<h4>Kategorien</h4>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px">
|
||||||
|
{% for c in categories|default([]) %}
|
||||||
|
<a href="{{ url_for('category', slug=c.slug) }}" class="button">{{ c.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<div style="color:var(--muted)">Keine Kategorien.</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4>Über</h4>
|
||||||
|
<p style="color:var(--muted);font-size:0.95rem">{% block about_snippet %}Fandom-Community. Leitfäden,
|
||||||
|
Lore, Builds und mehr.{% endblock %}</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div>© {{ current_year or 2025 }} {{ site_name or 'Game Fandom' }} — Erstellt mit ❤️</div>
|
||||||
|
<div style="margin-top:6px;color:var(--muted);font-size:0.85rem">Powered by Flask • <a href="#">Datenschutz</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// kleines Script: z.B. optionales keyboard shortcut für Suche (/)
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === "/" && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
|
||||||
|
const q = document.querySelector('.search input');
|
||||||
|
if (q) {
|
||||||
|
e.preventDefault();
|
||||||
|
q.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Startseite{% endblock %}
|
||||||
{% block title %}
|
{% block page_title %}Willkommen im {{ site_name or 'Game Fandom' }}{% endblock %}
|
||||||
HURENSOHN
|
{% block content %}
|
||||||
|
<p>Dies ist die Startseite deines Wikis. Wähle eine Kategorie oder suche nach Artikeln.</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
18
templates/login.html
Normal file
18
templates/login.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Login{% endblock %}
|
||||||
|
{% block page_title %}Anmelden{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" action="{{ url_for('login') }}" class="auth-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="exampleInputEmail1" class="form-label">Email address</label>
|
||||||
|
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
|
||||||
|
<div id="emailHelp" class="form-text">We'll never share your email with anyone else.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="exampleInputPassword1" class="form-label">Password</label>
|
||||||
|
<input type="password" class="form-control" id="exampleInputPassword1">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
<p>Noch keinen Account? <a href="{{ url_for('register') }}">Jetzt registrieren</a>.</p>
|
||||||
|
{% endblock %}
|
||||||
25
templates/register.html
Normal file
25
templates/register.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Registrieren{% endblock %}
|
||||||
|
{% block page_title %}Registrieren{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" action="{{ url_for('register') }}" class="auth-form">
|
||||||
|
<label for="username">Benutzername</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
|
||||||
|
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
<input type="email" id="email" name="email" required>
|
||||||
|
|
||||||
|
|
||||||
|
<label for="password">Passwort</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
|
||||||
|
|
||||||
|
<label for="confirm">Passwort bestätigen</label>
|
||||||
|
<input type="password" id="confirm" name="confirm" required>
|
||||||
|
|
||||||
|
|
||||||
|
<button type="submit">Registrieren</button>
|
||||||
|
</form>
|
||||||
|
<p>Schon ein Konto? <a href="{{ url_for('login') }}">Zum Login</a>.</p>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user