Init
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					/certbot
 | 
				
			||||||
							
								
								
									
										10
									
								
								api/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								api/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -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"]
 | 
				
			||||||
							
								
								
									
										2
									
								
								api/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								api/requirements.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					fastapi>=0.68.0
 | 
				
			||||||
 | 
					uvicorn>=0.15.0
 | 
				
			||||||
							
								
								
									
										38
									
								
								api/server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								api/server.py
									
									
									
									
									
										Normal file
									
								
							@@ -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"}
 | 
				
			||||||
							
								
								
									
										41
									
								
								compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								compose.yml
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										145
									
								
								html/css/catppuccin.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								html/css/catppuccin.css
									
									
									
									
									
										Normal file
									
								
							@@ -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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										224
									
								
								html/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								html/css/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										151
									
								
								html/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								html/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
						<meta charset="UTF-8">
 | 
				
			||||||
 | 
						<meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
						<title>Garde Studios</title> 
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="css/catppuccin.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="css/style.css">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
						<header class="navbar">
 | 
				
			||||||
 | 
							<div class="container">
 | 
				
			||||||
 | 
								<a href="/" class="logo">Garde Studios</a>
 | 
				
			||||||
 | 
								<nav class="nav-links">
 | 
				
			||||||
 | 
									<ul>
 | 
				
			||||||
 | 
										<li><a href="#games">Games</a></li>
 | 
				
			||||||
 | 
									<li>
 | 
				
			||||||
 | 
										<div class="theme-switcher">
 | 
				
			||||||
 | 
											<div class="theme-option active" data-theme-color="latte" data-theme="latte"></div>
 | 
				
			||||||
 | 
											<div class="theme-option" data-theme-color="frappe" data-theme="frappe"></div>
 | 
				
			||||||
 | 
											<div class="theme-option" data-theme-color="macchiato" data-theme="macchiato"></div>
 | 
				
			||||||
 | 
											<div class="theme-option" data-theme-color="mocha" data-theme="mocha"></div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
									</ul>
 | 
				
			||||||
 | 
								</nav>
 | 
				
			||||||
 | 
								<button class="hamburger" aria-label="Menu">
 | 
				
			||||||
 | 
									<span></span>
 | 
				
			||||||
 | 
									<span></span>
 | 
				
			||||||
 | 
									<span></span>
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<!-- Previous content remains until <main> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<main>
 | 
				
			||||||
 | 
							<!--<section class="hero">
 | 
				
			||||||
 | 
								<h1>Garde Studios</h1>
 | 
				
			||||||
 | 
								<p>Digital experiences reimagined</p>
 | 
				
			||||||
 | 
								</section>-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<section id="games" class="section">
 | 
				
			||||||
 | 
								<div class="container">
 | 
				
			||||||
 | 
									<h2>Games @ Garde Studios</h2>
 | 
				
			||||||
 | 
									<div class="server-card" data-ip="garde-studios.de" data-port="1234">
 | 
				
			||||||
 | 
										<h3>OpenRA - Red Alert</h3>
 | 
				
			||||||
 | 
										<div class="card-description">
 | 
				
			||||||
 | 
											<p>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.</p>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="server-info">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Link: <span class="highlight"><a href="https://www.openra.net/">openra.net</a></span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Domain/IP: <span class="highlight">garde-studios.de</span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Port: <span class="highlight">1234</span></p>
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Password: <span class="highlight">garde-studios</span></p>
 | 
				
			||||||
 | 
											<div class="server-info">
 | 
				
			||||||
 | 
												<p><span class="arrow">>></span> Status: <span class="status unknown">Unknown</span></p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									<div class="server-card" data-ip="garde-studios.de" data-port="1235">
 | 
				
			||||||
 | 
										<h3>OpenRA - Command & Conquer: Tiberian Dawn</h3>
 | 
				
			||||||
 | 
										<div class="card-description">
 | 
				
			||||||
 | 
											<p>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.</p>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="server-info">
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Link: <span class="highlight"><a href="https://www.openra.net/">openra.net</a></span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Domain/IP: <span class="highlight">garde-studios.de</span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Port: <span class="highlight">1235</span></p>
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Password: <span class="highlight">garde-studios</span></p>
 | 
				
			||||||
 | 
											<div class="server-info">
 | 
				
			||||||
 | 
												<p><span class="arrow">>></span> Status: <span class="status unknown">Unknown</span></p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<div class="server-card" data-ip="garde-studios.de" data-port="1236">
 | 
				
			||||||
 | 
										<h3>OpenRA - Dune 2000</h3>
 | 
				
			||||||
 | 
										<div class="card-description">
 | 
				
			||||||
 | 
											<p>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.</p>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="server-info">
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Link: <span class="highlight"><a href="https://www.openra.net/">openra.net</a></span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Domain/IP: <span class="highlight">garde-studios.de</span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Port: <span class="highlight">1236</span></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Password: <span class="highlight">garde-studios</span></p>
 | 
				
			||||||
 | 
											<div class="server-info">
 | 
				
			||||||
 | 
												<p><span class="arrow">>></span> Status: <span class="status unknown">Unknown</span></p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<div class="server-card" data-ip="garde-studios.de" data-port="14004">
 | 
				
			||||||
 | 
										<h3>Veloren</h3>
 | 
				
			||||||
 | 
										<div class="card-description">
 | 
				
			||||||
 | 
											<p>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.</p>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="server-info">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Link: <span class="highlight"><a href="https://www.veloren.net/">veloren.net</a></span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Domain/IP: <span class="highlight">garde-studios.de</span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Port: <span class="highlight">14004</span></p>
 | 
				
			||||||
 | 
											<div class="server-info">
 | 
				
			||||||
 | 
												<p><span class="arrow">>></span> Status: <span class="status unknown">Unknown</span></p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<div class="server-card" data-ip="garde-studios.de" data-port="34197">
 | 
				
			||||||
 | 
										<h3>Factorio (No Space Age DLC)</h3>
 | 
				
			||||||
 | 
										<div class="card-description">
 | 
				
			||||||
 | 
											<p>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.</p>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="server-info">
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Link: <span class="highlight"><a href="https://factorio.com/">factorio.com</a></span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Domain/IP: <span class="highlight">garde-studios.de</span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Port: <span class="highlight">34197</span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Password: <span class="highlight">garde-studios</span></p>
 | 
				
			||||||
 | 
											<div class="server-info">
 | 
				
			||||||
 | 
												<p><span class="arrow">>></span> Status: <span class="status unknown">Unknown</span></p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<div class="server-card" data-ip="garde-studios.de" data-port="16261">
 | 
				
			||||||
 | 
										<h3>Project Zomboid</h3>
 | 
				
			||||||
 | 
										<div class="card-description">
 | 
				
			||||||
 | 
											<p>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.</p>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="server-info">
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Link: <span class="highlight"><a href="https://projectzomboid.com/">projectzomboid.com</a></span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Domain/IP: <span class="highlight">garde-studios.de</span></p>
 | 
				
			||||||
 | 
											<p><span class="arrow">>></span> Port: <span class="highlight">16261</span></p>
 | 
				
			||||||
 | 
											<div class="server-info">
 | 
				
			||||||
 | 
												<p><span class="arrow">>></span> Status: <span class="status unknown">Unknown</span></p>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</section>
 | 
				
			||||||
 | 
						</main>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<script src="js/main.js"></script>
 | 
				
			||||||
 | 
						<script src="js/theme-switcher.js"></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										74
									
								
								html/js/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								html/js/main.js
									
									
									
									
									
										Normal file
									
								
							@@ -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);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										34
									
								
								html/js/theme-switcher.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								html/js/theme-switcher.js
									
									
									
									
									
										Normal file
									
								
							@@ -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';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						    });
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										58
									
								
								nginx/conf.d/default.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								nginx/conf.d/default.conf
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user