diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..038975b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/certbot diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..ecbf2df --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY server.py . + +CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..e5ffee2 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,2 @@ +fastapi>=0.68.0 +uvicorn>=0.15.0 diff --git a/api/server.py b/api/server.py new file mode 100644 index 0000000..edca3fe --- /dev/null +++ b/api/server.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +import socket +import asyncio + +app = FastAPI() + +# Configure CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +async def check_port(host: str, port: int, timeout: float = 2.0) -> bool: + try: + # Create async socket connection + reader, writer = await asyncio.wait_for( + asyncio.open_connection(host, port), + timeout=timeout + ) + writer.close() + await writer.wait_closed() + return True + except (socket.gaierror, ConnectionRefusedError, asyncio.TimeoutError): + return False + except Exception: + return False + +@app.get("/check-port") +async def port_check(host: str, port: int): + is_alive = await check_port(host, port) + return {"status": "online" if is_alive else "offline"} + +@app.get("/health") +async def health_check(): + return {"status": "ok"} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..9cfff8c --- /dev/null +++ b/compose.yml @@ -0,0 +1,41 @@ +services: + nginx: + image: nginx:alpine + container_name: nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d + - ./html:/usr/share/nginx/html + - ./certbot/www:/var/www/certbot + - ./certbot/conf:/etc/letsencrypt + networks: + - web-network + depends_on: + - certbot + - port-checker + + certbot: + image: certbot/certbot + container_name: certbot + volumes: + - ./certbot/www:/var/www/certbot + - ./certbot/conf:/etc/letsencrypt + networks: + - web-network + command: certonly --webroot --webroot-path=/var/www/certbot --email p.keier@beyerstedt-it.de --agree-tos --no-eff-email --force-renewal -d garde-studios.de -d www.garde-studios.de + + port-checker: + container_name: port-checker + build: ./api + ports: + - "8000:8000" + networks: + - web-network + restart: unless-stopped + +networks: + web-network: + driver: bridge diff --git a/html/css/catppuccin.css b/html/css/catppuccin.css new file mode 100644 index 0000000..bb68a4c --- /dev/null +++ b/html/css/catppuccin.css @@ -0,0 +1,145 @@ +[data-theme="latte"] { + --theme-rosewater: #dc8a78; + --theme-flamingo: #dd7878; + --theme-pink: #ea76cb; + --theme-mauve: #8839ef; + --theme-red: #d20f39; + --theme-maroon: #e64553; + --theme-peach: #fe640b; + --theme-yellow: #df8e1d; + --theme-green: #40a02b; + --theme-teal: #179299; + --theme-sky: #04a5e5; + --theme-sapphire: #209fb5; + --theme-blue: #1e66f5; + --theme-lavender: #7287fd; + --theme-text: #4c4f69; + --theme-subtext1: #5c5f77; + --theme-subtext0: #6c6f85; + --theme-overlay2: #7c7f93; + --theme-overlay1: #8c8fa1; + --theme-overlay0: #9ca0b0; + --theme-surface2: #acb0be; + --theme-surface1: #bcc0cc; + --theme-surface0: #ccd0da; + --theme-base: #eff1f5; + --theme-mantle: #e6e9ef; + --theme-crust: #dce0e8; +} + +[data-theme="frappe"] { + --theme-rosewater: #f2d5cf; + --theme-flamingo: #eebebe; + --theme-pink: #f4b8e4; + --theme-mauve: #ca9ee6; + --theme-red: #e78284; + --theme-maroon: #ea999c; + --theme-peach: #ef9f76; + --theme-yellow: #e5c890; + --theme-green: #a6d189; + --theme-teal: #81c8be; + --theme-sky: #99d1db; + --theme-sapphire: #85c1dc; + --theme-blue: #8caaee; + --theme-lavender: #babbf1; + --theme-text: #c6d0f5; + --theme-subtext1: #b5bfe2; + --theme-subtext0: #a5adce; + --theme-overlay2: #949cbb; + --theme-overlay1: #838ba7; + --theme-overlay0: #737994; + --theme-surface2: #626880; + --theme-surface1: #51576d; + --theme-surface0: #414559; + --theme-base: #303446; + --theme-mantle: #292c3c; + --theme-crust: #232634; +} + +[data-theme="macchiato"] { + --theme-rosewater: #f4dbd6; + --theme-flamingo: #f0c6c6; + --theme-pink: #f5bde6; + --theme-mauve: #c6a0f6; + --theme-red: #ed8796; + --theme-maroon: #ee99a0; + --theme-peach: #f5a97f; + --theme-yellow: #eed49f; + --theme-green: #a6da95; + --theme-teal: #8bd5ca; + --theme-sky: #91d7e3; + --theme-sapphire: #7dc4e4; + --theme-blue: #8aadf4; + --theme-lavender: #b7bdf8; + --theme-text: #cad3f5; + --theme-subtext1: #b8c0e0; + --theme-subtext0: #a5adcb; + --theme-overlay2: #939ab7; + --theme-overlay1: #8087a2; + --theme-overlay0: #6e738d; + --theme-surface2: #5b6078; + --theme-surface1: #494d64; + --theme-surface0: #363a4f; + --theme-base: #24273a; + --theme-mantle: #1e2030; + --theme-crust: #181926; +} + +[data-theme="mocha"] { + --theme-rosewater: #f5e0dc; + --theme-flamingo: #f2cdcd; + --theme-pink: #f5c2e7; + --theme-mauve: #cba6f7; + --theme-red: #f38ba8; + --theme-maroon: #eba0ac; + --theme-peach: #fab387; + --theme-yellow: #f9e2af; + --theme-green: #a6e3a1; + --theme-teal: #94e2d5; + --theme-sky: #89dceb; + --theme-sapphire: #74c7ec; + --theme-blue: #89b4fa; + --theme-lavender: #b4befe; + --theme-text: #cdd6f4; + --theme-subtext1: #bac2de; + --theme-subtext0: #a6adc8; + --theme-overlay2: #9399b2; + --theme-overlay1: #7f849c; + --theme-overlay0: #6c7086; + --theme-surface2: #585b70; + --theme-surface1: #45475a; + --theme-surface0: #313244; + --theme-base: #1e1e2e; + --theme-mantle: #181825; + --theme-crust: #11111b; +} + +/* Color indicators for each theme */ +[data-theme-color="latte"] { background: var(--theme-red); } +[data-theme-color="frappe"] { background: var(--theme-yellow); } +[data-theme-color="macchiato"] { background: var(--theme-green); } +[data-theme-color="mocha"] { background: var(--theme-blue); } + +/* Theme Switcher Styles */ +.theme-switcher { + display: flex; + justify-content: center; + gap: 8px; +} + +.theme-option { + width: 24px; + height: 24px; + border-radius: 50%; + cursor: pointer; + border: 2px solid transparent; + transition: transform 0.2s; +} + +.theme-option:hover { + transform: scale(1.3); +} + +.theme-option .active { + border-color: var(--theme-text); +} diff --git a/html/css/style.css b/html/css/style.css new file mode 100644 index 0000000..d9e65fd --- /dev/null +++ b/html/css/style.css @@ -0,0 +1,224 @@ +@charset "UTF-8"; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Helvetica Neue', Arial, sans-serif; + line-height: 1.6; + padding-top: 80px; /* Offset for fixed navbar */ + background-color: var(--theme-base); + color: var(--theme-text); +} + +a { + color: var(--theme-red); + text-decoration: none; +} + +a:hover { + color: var(--theme-peach); +} + +/* Navbar Styles */ +.navbar { + position: fixed; + top: 0; + left: 0; + width: 100%; + background-color: var(--theme-base); + box-shadow: 0 2px 10px var(--theme-blue); + z-index: 1000; +} + +.container { + width: 90%; + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 0; +} + +.logo { + font-size: 1.5rem; + font-weight: 700; + color: var(--theme-text); + text-decoration: none; +} + +.logo:hover { + color: var(--theme-text); +} + +.nav-links ul { + display: flex; + list-style: none; +} + +.nav-links li { + margin-left: 2rem; +} + +.nav-links a { + color: var(--theme-sky); + text-decoration: none; + font-weight: 500; + transition: color 0.3s; +} + +.nav-links a:hover { + color: var(--theme-lavender); +} + +/* Mobile Menu */ +.hamburger { + display: none; + background: none; + border: none; + cursor: pointer; + padding: 0.5rem; +} + +.hamburger span { + display: block; + width: 25px; + height: 2px; + background: var(--theme-blue); + margin: 5px 0; + transition: all 0.3s ease; +} + +/* Hero Section */ +.hero { + height: 80vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + padding: 2rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .nav-links { + position: fixed; + top: 80px; + left: -100%; + width: 100%; + height: calc(100vh - 80px); + background-color: var(--theme-base); + transition: left 0.3s ease; + } + + .nav-links.active { + left: 0; + } + + .nav-links ul { + flex-direction: column; + padding: 2rem; + } + + .nav-links li { + margin: 1rem 0; + } + + .hamburger { + display: block; + } + + .hamburger.active span:nth-child(1) { + transform: rotate(45deg) translate(5px, 5px); + } + + .hamburger.active span:nth-child(2) { + opacity: 0; + } + + .hamburger.active span:nth-child(3) { + transform: rotate(-45deg) translate(5px, -5px); + } +} + +/* Section Styles */ +.section { + padding: 5rem 0; +} + +.container { + width: 90%; + max-width: 1200px; + margin: 0 auto; +} + +h2 { + font-size: 2.5rem; + margin-bottom: 2rem; + text-align: center; +} + +/* Section Styling */ +#games .container { + display: flex; + flex-direction: column; + gap: 2rem; +} + +/* Server Card Styling */ +.server-card { + width: 100%; + padding: 1.5rem; + background: var(--theme-base); + border-radius: 8px; + box-shadow: 0 2px 10px var(--theme-blue); +} + +.server-card h3 { + color: var(--theme-text); + margin-bottom: 1rem; + font-size: 1.3rem; +} + +/* Server Info Styling (keep your existing styles) */ +.server-info { + font-family: 'Courier New', monospace; + line-height: 1.8; +} + +.server-info p { + margin: 0.2rem 0; +} + +.arrow { + margin: 0.3rem; +} + +.highlight { + font-weight: bold; +} + +.card-description { + margin: 12px 0; + color: var(--theme-subtext0); + font-size: 0.9rem; + line-height: 1.4; +} + +/* Status indicators (keep your existing styles) */ +.status.online { + color: var(--theme-green); +} + +.status.offline { + color: var(--theme-red); +} + +.status.unknown { + color: var(--theme-yellow); +} diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..a081efb --- /dev/null +++ b/html/index.html @@ -0,0 +1,151 @@ + + + + + + Garde Studios + + + + + + + + +
+ + +
+
+

Games @ Garde Studios

+
+

OpenRA - Red Alert

+
+

Die Alliierten vertrauen auf fortschrittliche Technologie und schnelle Aufklärung, während die Sowjets mit roher Gewalt und überwältigenden Zahlen dominieren – in diesem zeitlosen Konflikt entscheiden Taktik und Ressourcenkontrolle über den Sieg.

+
+
+ +

>> Link: openra.net

+

>> Domain/IP: garde-studios.de

+

>> Port: 1234

+ +

>> Password: garde-studios

+
+

>> Status: Unknown

+
+
+
+ +
+

OpenRA - Command & Conquer: Tiberian Dawn

+
+

Die GDI setzen auf hochwertige Technologie und schwere Panzerung, während die Bruderschaft von Nod mit Guerilla-Taktik und Tiberium-Waffen kämpft – doch beide Fraktionen müssen sich gegen die tödliche Verseuchung des Tiberiums behaupten.

+
+
+

>> Link: openra.net

+

>> Domain/IP: garde-studios.de

+

>> Port: 1235

+ +

>> Password: garde-studios

+
+

>> Status: Unknown

+
+
+
+ +
+

OpenRA - Dune 2000

+
+

Die Atreides setzen auf Elite-Truppen und Präzision, die Harkonnen überrollen Feinde mit brutaler Feuerkraft, und die Ordos kämpfen mit hinterhältigen Söldnertaktiken – doch alle müssen um das Melange kämpfen, die Quelle der Macht auf Arrakis.

+
+
+

>> Link: openra.net

+

>> Domain/IP: garde-studios.de

+

>> Port: 1236

+ +

>> Password: garde-studios

+
+

>> Status: Unknown

+
+
+
+ +
+

Veloren

+
+

Die Abenteurer erkunden eine pixelige Fantasy-Welt mit komplexen Kämpfen und tiefen Handwerkssystemen, während dunkele Kreaturen und tödliche Höhlen auf unvorsichtige Helden lauern – doch der größte Feind ist oft die eigene Gier nach legendärer Beute.

+
+
+ +

>> Link: veloren.net

+

>> Domain/IP: garde-studios.de

+

>> Port: 14004

+
+

>> Status: Unknown

+
+
+
+ +
+

Factorio (No Space Age DLC)

+
+

Die Ingenieure optimieren Fabriken mit Präzision und Logistik, während die Biters mit schierer Zahl und evolutionärer Anpassung drohen – doch das wahre Chaos entsteht, wenn die Produktionsketten ins Stolpern geraten.

+
+
+

>> Link: factorio.com

+

>> Domain/IP: garde-studios.de

+

>> Port: 34197

+

>> Password: garde-studios

+
+

>> Status: Unknown

+
+
+
+ +
+

Project Zomboid

+
+

Die Überlebenden horten Vorräte und planen Fluchtwege, während die Horden durch Geräusche und Blutgeruch angelockt werden – doch der gefährlichste Feind bleibt die eigene Unvorsichtigkeit.

+
+
+

>> Link: projectzomboid.com

+

>> Domain/IP: garde-studios.de

+

>> Port: 16261

+
+

>> Status: Unknown

+
+
+
+
+
+
+ + + + + diff --git a/html/js/main.js b/html/js/main.js new file mode 100644 index 0000000..1d9a3a4 --- /dev/null +++ b/html/js/main.js @@ -0,0 +1,74 @@ +document.addEventListener('DOMContentLoaded', function() { + // Mobile menu toggle + const hamburger = document.querySelector('.hamburger'); + const navLinks = document.querySelector('.nav-links'); + + hamburger.addEventListener('click', function() { + this.classList.toggle('active'); + navLinks.classList.toggle('active'); + }); + + // Close menu when clicking a link (mobile) + document.querySelectorAll('.nav-links a').forEach(link => { + link.addEventListener('click', () => { + hamburger.classList.remove('active'); + navLinks.classList.remove('active'); + }); + }); + + // Smooth scrolling for anchor links + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + e.preventDefault(); + + const targetId = this.getAttribute('href'); + if (targetId === '#') return; + + const targetElement = document.querySelector(targetId); + if (targetElement) { + window.scrollTo({ + top: targetElement.offsetTop - 80, // Adjusted for fixed header + behavior: 'smooth' + }); + } + }); + }); +}); + +document.addEventListener('DOMContentLoaded', function() { + const serverCards = document.querySelectorAll('.server-card'); + const API_URL = '/api/check-port'; // Change in production + + async function checkServerStatus(card) { + const ip = card.dataset.ip; + const port = card.dataset.port; + const statusElement = card.querySelector('.status'); + + if (!ip || !port || !statusElement) return; + + statusElement.textContent = 'Checking...'; + statusElement.className = 'status unknown'; + + try { + const response = await fetch(`${API_URL}?host=${encodeURIComponent(ip)}&port=${port}`); + const data = await response.json(); + + statusElement.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1); + statusElement.className = `status ${data.status}`; + } catch (e) { + statusElement.textContent = 'Error'; + statusElement.className = 'status unknown'; + } + } + + // Initial check + serverCards.forEach(checkServerStatus); + + // Click handler for manual refresh + serverCards.forEach(card => { + card.addEventListener('click', () => checkServerStatus(card)); + }); + + // Periodic checks every 5 minutes + setInterval(() => serverCards.forEach(checkServerStatus), 5 * 60 * 1000); +}); diff --git a/html/js/theme-switcher.js b/html/js/theme-switcher.js new file mode 100644 index 0000000..98b83cc --- /dev/null +++ b/html/js/theme-switcher.js @@ -0,0 +1,34 @@ +document.addEventListener('DOMContentLoaded', () => { + const themeOptions = document.querySelectorAll('.theme-option'); + const html = document.documentElement; + + // Set initial theme from localStorage or default to latte + const savedTheme = localStorage.getItem('catppuccin-theme') || 'latte'; + html.setAttribute('data-theme', savedTheme); + updateActiveTheme(savedTheme); + + // Theme switcher functionality + themeOptions.forEach(option => { + option.addEventListener('click', () => { + const theme = option.dataset.theme; + html.setAttribute('data-theme', theme); + localStorage.setItem('catppuccin-theme', theme); + updateActiveTheme(theme); + }); + }); + + // Update active theme indicator + function updateActiveTheme(theme) { + themeOptions.forEach(opt => { + const isActive = opt.dataset.theme === theme; + opt.classList.toggle('active', isActive); + + // Update border color immediately + if (isActive) { + opt.style.borderColor = `var(--theme-mantle)`; + } else { + opt.style.borderColor = 'transparent'; + } + }); + } +}); diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000..a7c5575 --- /dev/null +++ b/nginx/conf.d/default.conf @@ -0,0 +1,58 @@ +server { + listen 80; + server_name garde-studios.de www.garde-studios.de; + + charset utf-8; + charset_types text/html text/css application/javascript text/plain text/xml; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + server_name garde-studios.de www.garde-studios.de; + + charset utf-8; + charset_types text/html text/css application/javascript text/plain text/xml; + + ssl_certificate /etc/letsencrypt/live/garde-studios.de/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/garde-studios.de/privkey.pem; + + # SSL configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers EECDH+AESGCM:EDH+AESGCM; + ssl_ecdh_curve secp384r1; + ssl_session_timeout 10m; + ssl_session_cache shared:SSL:10m; + ssl_session_tickets off; + ssl_stapling on; + ssl_stapling_verify on; + + root /usr/share/nginx/html; + index index.html; + + # API reverse proxy + location /api/ { + proxy_pass http://port-checker:8000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support (if needed in future) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Frontend static files + location / { + try_files $uri $uri/ /index.html; + } +}