refactor(mana-games): migrate web app from Astro to SvelteKit

Replace Astro SSG with SvelteKit 2 + Svelte 5 to align with monorepo
standard stack. Adds shared packages (auth, theme, PWA, i18n, local-store),
Tailwind 4, PillNavigation, and local-first data layer for game stats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-29 14:44:02 +02:00
parent 89e6a202df
commit 29f2c999b5
119 changed files with 1741 additions and 14629 deletions

View file

@ -7,32 +7,55 @@ AI-powered browser games platform mit 22+ Spielen und KI-Spielgenerierung.
```
games/mana-games/
├── apps/
│ ├── web/ # Astro PWA (@mana-games/web)
│ ├── web/ # SvelteKit Web-App (@mana-games/web)
│ │ ├── src/
│ │ │ ├── pages/ # Astro-Seiten
│ │ │ ├── layouts/ # Layout-Komponenten
│ │ │ ├── components/
│ │ │ ├── data/ # Spielekatalog (games.ts)
│ │ │ └── services/ # Stats, etc.
│ │ └── public/
│ │ ├── games/ # 22 HTML-Spiele
│ │ ├── screenshots/
│ │ └── icons/ # PWA Icons
│ │ │ ├── routes/ # SvelteKit-Routen
│ │ │ │ ├── (app)/ # App-Routen mit PillNavigation
│ │ │ │ │ ├── play/[slug] # Spiel im iframe
│ │ │ │ │ ├── create/ # AI Game Generator
│ │ │ │ │ ├── community/ # Community-Spiele
│ │ │ │ │ ├── stats/ # Spieler-Statistiken
│ │ │ │ │ └── play-generated/ # Generierte Spiele
│ │ │ │ └── (auth)/ # Login/Register
│ │ │ └── lib/
│ │ │ ├── components/ # Svelte 5 Komponenten
│ │ │ ├── data/ # Local-first Store, Game-Katalog
│ │ │ ├── stores/ # Theme, Auth, Navigation
│ │ │ ├── services/ # Game-Kommunikation (postMessage)
│ │ │ └── i18n/ # DE + EN Übersetzungen
│ │ └── static/
│ │ ├── games/ # 22 HTML-Spiele
│ │ └── screenshots/ # Game-Thumbnails
│ ├── web-astro/ # Alte Astro-App (Referenz, zum Löschen)
│ └── backend/ # NestJS API (@mana-games/backend)
│ └── src/
│ ├── game-generator/ # AI-Spielgenerierung (OpenRouter)
│ ├── game-generator/ # AI-Spielgenerierung (Gemini, Claude, GPT-4)
│ ├── game-submission/ # Community-Einreichungen (GitHub API)
│ └── health/
└── package.json # Root (mana-games)
```
## Tech Stack
| Aspekt | Technologie |
|--------|-------------|
| Frontend | SvelteKit 2 + Svelte 5 (Runes) |
| Styling | Tailwind CSS 4 + @manacore/shared-tailwind |
| Auth | @manacore/shared-auth (SSO) |
| PWA | @vite-pwa/sveltekit + @manacore/shared-pwa |
| State | @manacore/local-store (Dexie.js + sync) |
| i18n | svelte-i18n (DE + EN) |
| UI | @manacore/shared-ui (PillNav, AuthGate, etc.) |
| Theming | @manacore/shared-theme (multi-theme) |
| Backend | NestJS (AI-Generierung, Community) |
## Entwicklung
```bash
# Alles starten (Web + Backend)
pnpm mana-games:dev
# Nur Web (Astro)
# Nur Web (SvelteKit)
pnpm dev:mana-games:web
# Nur Backend (NestJS)
@ -43,9 +66,23 @@ pnpm dev:mana-games:app
```
**Ports:**
- Web: http://localhost:4321
- Web: http://localhost:5210
- Backend: http://localhost:3011
## Local-First Daten
Stats und generierte Spiele werden in IndexedDB gespeichert (Dexie.js) mit optionalem Sync:
**Collections:**
- `gameStats` — Highscores, Spielzeit, Spiele pro Game
- `generatedGames` — Mit KI erstellte Spiele (HTML, Prompt, Modell)
- `favorites` — Favorisierte Spiele
**Dateien:**
- `src/lib/data/local-store.ts` — Dexie-Store Definition
- `src/lib/data/queries.ts` — Reactive Queries (useLiveQuery)
- `src/lib/data/games.ts` — Statischer Spielekatalog (21 Spiele)
## API Endpoints
| Endpoint | Method | Beschreibung |
@ -59,10 +96,10 @@ pnpm dev:mana-games:app
```json
{
"description": "Ein Snake-Spiel im Neon-Stil",
"mode": "create", // oder "iterate"
"mode": "create",
"model": "gemini-2.0-flash",
"originalPrompt": "...", // nur bei iterate
"currentCode": "..." // nur bei iterate
"originalPrompt": "...",
"currentCode": "..."
}
```
@ -80,99 +117,43 @@ pnpm dev:mana-games:app
## Environment Variables
Die Variablen werden zentral in `.env.development` verwaltet:
```bash
MANA_GAMES_BACKEND_PORT=3011
# Google Gemini API
MANA_GAMES_GOOGLE_GENAI_API_KEY=your_key
# Anthropic Claude API
MANA_GAMES_ANTHROPIC_API_KEY=your_key
# Azure OpenAI API
MANA_GAMES_AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com
MANA_GAMES_AZURE_OPENAI_API_KEY=your_key
MANA_GAMES_AZURE_OPENAI_DEPLOYMENT=gpt-4o
# GitHub (für Community-Einreichungen)
MANA_GAMES_GITHUB_TOKEN=your_token
MANA_GAMES_GITHUB_OWNER=tillschneider
MANA_GAMES_GITHUB_REPO=mana-games
```
Nach Änderungen: `pnpm setup:env`
## Spiel hinzufügen
1. HTML-Datei erstellen in `apps/web/public/games/spiel_name.html`
2. Screenshot in `apps/web/public/screenshots/spiel-name.jpg`
3. Registrieren in `apps/web/src/data/games.ts`:
```typescript
{
id: '23',
title: 'Spiel Titel',
description: 'Beschreibung',
slug: 'spiel-name',
htmlFile: '/games/spiel_name.html',
thumbnail: '/screenshots/spiel-name.jpg',
tags: ['Arcade', 'Action'],
difficulty: 'Mittel',
complexity: 'Einfach',
controls: 'Pfeiltasten zum Steuern'
}
```
1. HTML-Datei in `apps/web/static/games/spiel_name.html`
2. Screenshot in `apps/web/static/screenshots/spiel-name.jpg`
3. Registrieren in `apps/web/src/lib/data/games.ts`
## Spiel-postMessage Integration
```javascript
// Beim Laden
window.parent.postMessage({
type: 'GAME_LOADED',
gameId: 'spiel-slug'
}, '*');
window.parent.postMessage({ type: 'GAME_LOADED', gameId: 'spiel-slug' }, '*');
// Bei Score-Update
window.parent.postMessage({
type: 'GAME_EVENT',
gameId: 'spiel-slug',
event: 'SCORE_UPDATE',
data: { score: 123 }
type: 'GAME_EVENT', gameId: 'spiel-slug',
event: 'SCORE_UPDATE', data: { score: 123 }
}, '*');
// Bei Game Over
window.parent.postMessage({
type: 'GAME_EVENT',
gameId: 'spiel-slug',
event: 'GAME_OVER',
data: { score: 123 }
type: 'GAME_EVENT', gameId: 'spiel-slug',
event: 'GAME_OVER', data: { score: 123 }
}, '*');
```
## Design
**Farbschema:**
- Primary Background: `#0a0a0a`
- Secondary Background: `#1a1a1a`
- Accent: `#00ff88`
- Text: `#ffffff`
- Border: `#2a2a2a`
## PWA
- Manifest: `apps/web/public/manifest.json`
- Service Worker: `apps/web/public/sw.js`
- Icons in `apps/web/public/icons/` (72x72 bis 512x512)
## Spielekatalog
**22 Spiele** in folgenden Genres:
- Arcade
- Puzzle
- Tower Defense
- Idle/Incremental
- Jump 'n' Run
- Action
- Strategie
**21 Spiele** in folgenden Genres: Arcade, Puzzle, Tower Defense, Idle/Incremental, Jump 'n' Run, Action, Strategie

View file

@ -8,9 +8,9 @@ async function bootstrap() {
// CORS configuration
app.enableCors({
origin: [
'http://localhost:4321', // Astro dev
'http://localhost:5210', // SvelteKit dev
'http://localhost:4321', // Legacy Astro dev
'http://localhost:3000', // Alternative dev
/\.netlify\.app$/, // Legacy Netlify
],
methods: ['GET', 'POST', 'OPTIONS'],
credentials: false,

View file

@ -1,11 +0,0 @@
# OpenRouter API Key
# Get your API key from https://openrouter.ai/keys
OPENROUTER_API_KEY=your_api_key_here
# GitHub API Token (for community submissions)
# Create a personal access token with 'repo' scope at https://github.com/settings/tokens
GITHUB_TOKEN=your_github_token_here
# GitHub Repository Settings (optional - defaults to current repo)
GITHUB_OWNER=your_github_username
GITHUB_REPO=mana-games

View file

@ -1,5 +0,0 @@
// @ts-check
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({});

View file

@ -1,23 +0,0 @@
// @ts-check
import {
baseConfig,
typescriptConfig,
svelteConfig,
prettierConfig,
} from '@manacore/eslint-config';
export default [
{
ignores: [
'dist/**',
'.svelte-kit/**',
'.astro/**',
'node_modules/**',
'**/stats-integration-template.js',
],
},
...baseConfig,
...typescriptConfig,
...svelteConfig,
...prettierConfig,
];

View file

@ -1,19 +1,58 @@
{
"name": "@mana-games/web",
"version": "1.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "rm -rf dist && astro build",
"preview": "astro preview",
"astro": "astro",
"lint": "eslint ."
},
"dependencies": {
"astro": "^5.10.1"
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
"format": "prettier --write .",
"type-check": "svelte-kit sync && svelte-check --threshold error"
},
"devDependencies": {
"sharp": "^0.34.2"
}
"@manacore/shared-pwa": "workspace:*",
"@manacore/shared-vite-config": "workspace:*",
"@sveltejs/adapter-node": "^5.0.0",
"@sveltejs/kit": "^2.47.1",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.1.7",
"@types/node": "^20.0.0",
"@vite-pwa/sveltekit": "^1.1.0",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^5.41.0",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.7",
"tslib": "^2.4.1",
"typescript": "^5.9.3",
"vite": "^6.0.0"
},
"dependencies": {
"@manacore/local-store": "workspace:*",
"@manacore/shared-app-onboarding": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-stores": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-error-tracking": "workspace:*",
"@manacore/feedback": "workspace:*",
"@manacore/shared-i18n": "workspace:*",
"@manacore/help": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/subscriptions": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"@manacore/shared-utils": "workspace:*",
"svelte-i18n": "^4.0.1"
},
"type": "module"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -1,10 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="#0a0a0a"/>
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#00ff88;stop-opacity:1" />
<stop offset="100%" style="stop-color:#00cc6a;stop-opacity:1" />
</linearGradient>
</defs>
<text x="256" y="280" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif" font-size="200" font-weight="900" text-anchor="middle" fill="url(#gradient)">MG</text>
</svg>

Before

Width:  |  Height:  |  Size: 606 B

View file

@ -1,89 +0,0 @@
{
"name": "Mana Games - Spiele ohne Grenzen",
"short_name": "Mana Games",
"description": "Eine Sammlung kostenloser, werbefreier Web-Spiele zum Spielen, Bauen und Lernen",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#1a1a1a",
"background_color": "#0a0a0a",
"categories": ["games", "education", "entertainment"],
"lang": "de",
"dir": "ltr",
"icons": [
{
"src": "/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/screenshots/desktop-home.png",
"sizes": "1280x720",
"type": "image/png",
"label": "Mana Games Startseite"
},
{
"src": "/screenshots/mobile-home.png",
"sizes": "750x1334",
"type": "image/png",
"label": "Mobile Ansicht"
}
],
"shortcuts": [
{
"name": "Snake Game",
"url": "/games/snake",
"description": "Klassisches Snake-Spiel spielen"
},
{
"name": "Meine Statistiken",
"url": "/stats",
"description": "Spielstatistiken anzeigen"
}
]
}

View file

@ -1,184 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Offline - Mana Games</title>
<style>
:root {
--color-bg: #0a0a0a;
--color-bg-secondary: #1a1a1a;
--color-text: #ffffff;
--color-text-secondary: #b0b0b0;
--color-accent: #00ff88;
--color-accent-secondary: #00cc6a;
--color-border: #2a2a2a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: var(--color-bg);
color: var(--color-text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.offline-container {
text-align: center;
max-width: 500px;
}
.offline-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
opacity: 0.5;
}
h1 {
font-size: 2.5rem;
font-weight: 900;
margin-bottom: 1rem;
letter-spacing: -0.05em;
}
.accent {
color: var(--color-accent);
}
p {
color: var(--color-text-secondary);
line-height: 1.6;
margin-bottom: 2rem;
}
.offline-games {
margin-top: 3rem;
}
.offline-games h2 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--color-accent);
}
.games-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.game-item {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
padding: 1rem;
border-radius: 0.5rem;
text-align: left;
}
.game-item h3 {
color: var(--color-text);
font-size: 1.1rem;
margin-bottom: 0.25rem;
}
.game-item p {
font-size: 0.9rem;
margin-bottom: 0;
}
.reload-button {
background-color: var(--color-accent);
color: #000;
border: none;
padding: 0.75rem 2rem;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-block;
}
.reload-button:hover {
background-color: var(--color-accent-secondary);
transform: translateY(-2px);
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.offline-icon {
font-size: 3rem;
}
}
</style>
</head>
<body>
<div class="offline-container">
<div class="offline-icon">📡</div>
<h1>Du bist <span class="accent">offline</span></h1>
<p>
Keine Internetverbindung gefunden. Aber keine Sorge!
Einige Spiele, die du bereits gespielt hast, sind möglicherweise
noch im Cache verfügbar.
</p>
<button class="reload-button" onclick="window.location.reload()">
Erneut versuchen
</button>
<div class="offline-games" id="offline-games" style="display: none;">
<h2>Verfügbare Offline-Spiele</h2>
<div class="games-list" id="games-list"></div>
</div>
</div>
<script>
// Prüfe gecachte Spiele
if ('caches' in window) {
caches.open('mana-games-v1').then(cache => {
cache.keys().then(requests => {
const gameUrls = requests
.map(request => request.url)
.filter(url => url.includes('/games/') && url.endsWith('.html'));
if (gameUrls.length > 0) {
document.getElementById('offline-games').style.display = 'block';
const gamesList = document.getElementById('games-list');
gameUrls.forEach(url => {
const gameName = url.split('/').pop().replace('.html', '').replace(/_/g, ' ');
const gameItem = document.createElement('div');
gameItem.className = 'game-item';
gameItem.innerHTML = `
<h3>${gameName}</h3>
<p>Dieses Spiel ist offline verfügbar</p>
`;
gameItem.style.cursor = 'pointer';
gameItem.onclick = () => window.location.href = url;
gamesList.appendChild(gameItem);
});
}
});
});
}
// Automatisch neu laden, wenn wieder online
window.addEventListener('online', () => {
window.location.reload();
});
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,161 +0,0 @@
const CACHE_NAME = 'mana-games-v1';
const OFFLINE_URL = '/offline.html';
// Assets, die immer gecacht werden sollen
const STATIC_CACHE_URLS = ['/', '/offline.html', '/favicon.svg', '/manifest.json'];
// Cache-Strategien für verschiedene Ressourcen
const CACHE_STRATEGIES = {
// Netzwerk zuerst, dann Cache (für HTML)
networkFirst: [/\/$/, /\.html$/, /\.astro$/],
// Cache zuerst, dann Netzwerk (für Assets)
cacheFirst: [
/\.css$/,
/\.js$/,
/\.woff2?$/,
/\.ttf$/,
/\.otf$/,
/\.svg$/,
/\.png$/,
/\.jpg$/,
/\.jpeg$/,
/\.webp$/,
/\.ico$/,
],
// Nur Netzwerk (für API-Calls)
networkOnly: [/\/api\//, /\.json$/],
};
// Service Worker Installation
self.addEventListener('install', (event) => {
event.waitUntil(
caches
.open(CACHE_NAME)
.then((cache) => {
console.log('Service Worker: Caching static assets');
return cache.addAll(STATIC_CACHE_URLS);
})
.then(() => self.skipWaiting())
);
});
// Service Worker Aktivierung
self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames
.filter((cacheName) => cacheName !== CACHE_NAME)
.map((cacheName) => caches.delete(cacheName))
);
})
.then(() => self.clients.claim())
);
});
// Fetch-Event Handler
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Ignoriere Chrome Extension Requests
if (url.protocol === 'chrome-extension:') {
return;
}
// Bestimme die Cache-Strategie
const strategy = getStrategy(url.pathname);
if (strategy === 'networkFirst') {
event.respondWith(networkFirst(request));
} else if (strategy === 'cacheFirst') {
event.respondWith(cacheFirst(request));
} else if (strategy === 'networkOnly') {
event.respondWith(networkOnly(request));
} else {
// Standard: Network First
event.respondWith(networkFirst(request));
}
});
// Cache-Strategien Implementierung
async function networkFirst(request) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Wenn es eine Navigation ist und wir offline sind, zeige die Offline-Seite
if (request.mode === 'navigate') {
const offlineResponse = await caches.match(OFFLINE_URL);
if (offlineResponse) {
return offlineResponse;
}
}
throw error;
}
}
async function cacheFirst(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('Fetch failed:', error);
throw error;
}
}
async function networkOnly(request) {
return fetch(request);
}
// Hilfsfunktion zur Bestimmung der Cache-Strategie
function getStrategy(pathname) {
for (const [strategy, patterns] of Object.entries(CACHE_STRATEGIES)) {
if (patterns.some((pattern) => pattern.test(pathname))) {
return strategy;
}
}
return 'networkFirst';
}
// Message Handler für Cache-Updates
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CACHE_GAME') {
const gameUrl = event.data.url;
caches
.open(CACHE_NAME)
.then((cache) => cache.add(gameUrl))
.then(() => {
event.ports[0].postMessage({ cached: true });
})
.catch((error) => {
event.ports[0].postMessage({ cached: false, error: error.message });
});
}
});

View file

@ -0,0 +1,10 @@
@import "tailwindcss";
@import "@manacore/shared-tailwind/themes.css";
/* Scan shared packages for Tailwind classes */
@source "../../../../../../packages/shared-ui/src";
@source "../../../../../../packages/shared-auth-ui/src";
@source "../../../../../../packages/shared-branding/src";
@source "../../../../../../packages/shared-theme-ui/src";
@source "../../../../../../packages/shared-theme-ui/src/components";
@source "../../../../../../packages/shared-theme-ui/src/pages";

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -1,188 +0,0 @@
---
export interface Props {
variant?: 'primary' | 'secondary' | 'accent' | 'ghost' | 'danger';
size?: 'small' | 'medium' | 'large' | 'icon';
href?: string;
onclick?: string;
id?: string;
class?: string;
title?: string;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
}
const {
variant = 'secondary',
size = 'medium',
href,
onclick,
id,
class: className = '',
title,
disabled = false,
type = 'button',
} = Astro.props;
const isLink = Boolean(href);
const Component = isLink ? 'a' : 'button';
const classes = ['btn', `btn-${variant}`, `btn-${size}`, className].filter(Boolean).join(' ');
const props = {
class: classes,
...(id && { id }),
...(title && { title }),
...(isLink ? { href } : { type, disabled }),
...(onclick && { onclick }),
};
---
<Component {...props}>
<slot />
</Component>
<style>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-family: inherit;
font-weight: 500;
text-decoration: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
position: relative;
overflow: hidden;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Sizes */
.btn-small {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.btn-medium {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.btn-large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.btn-icon {
width: 40px;
height: 40px;
padding: 0;
font-size: 1.2rem;
}
/* Variants */
.btn-primary {
background-color: var(--color-accent);
color: #000;
border: 1px solid var(--color-accent);
}
.btn-primary:hover:not(:disabled) {
background-color: var(--color-accent-secondary);
border-color: var(--color-accent-secondary);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 255, 136, 0.3);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 255, 136, 0.2);
}
.btn-secondary {
background-color: var(--color-bg-secondary);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.btn-secondary:hover:not(:disabled) {
background-color: #252525;
border-color: #404040;
}
.btn-accent {
background-color: rgba(0, 255, 136, 0.1);
color: var(--color-accent);
border: 1px solid var(--color-accent);
}
.btn-accent:hover:not(:disabled) {
background-color: var(--color-accent);
color: #000;
}
.btn-ghost {
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.btn-ghost:hover:not(:disabled) {
background-color: rgba(255, 255, 255, 0.05);
color: var(--color-text);
border-color: rgba(255, 255, 255, 0.2);
}
.btn-danger {
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.btn-danger:hover:not(:disabled) {
background-color: #ff4444;
color: #fff;
border-color: #ff4444;
}
/* Special hover effects */
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
transform: translate(-50%, -50%);
transition:
width 0.6s,
height 0.6s;
}
.btn:hover::before {
width: 300px;
height: 300px;
}
/* Mobile optimizations */
@media (max-width: 768px) {
.btn-icon {
width: 36px;
height: 36px;
font-size: 1.1rem;
}
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.8125rem;
}
}
</style>

View file

@ -1,193 +0,0 @@
---
// Footer component with compact site navigation
---
<footer class="site-footer">
<div class="footer-container">
<div class="footer-content">
<!-- Brand -->
<div class="footer-brand">
<a href="/" class="footer-logo">
<span class="logo-text">MANA</span>
<span class="logo-accent">GAMES</span>
</a>
<p class="footer-tagline">Spiele ohne Grenzen</p>
</div>
<!-- Navigation Links -->
<div class="footer-nav">
<div class="footer-section">
<h4>Spielen</h4>
<ul>
<li><a href="/">Alle Spiele</a></li>
<li><a href="/create">KI Generator</a></li>
<li><a href="/stats">Meine Stats</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Über Uns</h4>
<ul>
<li><a href="/about">Vision</a></li>
<li><a href="/mitmachen">Mitmachen</a></li>
<li>
<a href="https://github.com/anthropics/mana-games" target="_blank" rel="noopener"
>GitHub</a
>
</li>
</ul>
</div>
<div class="footer-section">
<h4>Rechtliches</h4>
<ul>
<li><a href="/impressum">Impressum</a></li>
<li><a href="/datenschutz">Datenschutz</a></li>
<li><a href="/agb">AGB</a></li>
<li><a href="/jugendschutz">Jugendschutz</a></li>
<li><a href="/copyright">Copyright</a></li>
</ul>
</div>
</div>
</div>
<!-- Bottom Bar -->
<div class="footer-bottom">
<p>&copy; 2024 Mana Games. Alle Rechte vorbehalten.</p>
</div>
</div>
</footer>
<style>
.site-footer {
background: var(--color-bg-secondary);
border-top: 1px solid var(--color-border);
margin-top: 4rem;
padding: 3rem 0 1.5rem;
}
.footer-container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 2rem;
}
.footer-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 4rem;
margin-bottom: 3rem;
}
/* Brand Section */
.footer-brand {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.footer-logo {
text-decoration: none;
font-size: 1.5rem;
font-weight: 900;
letter-spacing: -0.05em;
display: inline-block;
transition: opacity 0.2s ease;
}
.footer-logo:hover {
opacity: 0.8;
}
.logo-text {
color: var(--color-text);
}
.logo-accent {
color: var(--color-accent);
margin-left: 0.25rem;
}
.footer-tagline {
color: var(--color-text-secondary);
font-size: 0.9rem;
margin: 0;
}
/* Navigation */
.footer-nav {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.footer-section h4 {
color: var(--color-text);
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0 0 1rem 0;
}
.footer-section ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.footer-section a {
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.875rem;
transition: color 0.2s ease;
display: inline-block;
}
.footer-section a:hover {
color: var(--color-accent);
}
/* Bottom Bar */
.footer-bottom {
padding-top: 2rem;
border-top: 1px solid var(--color-border);
text-align: center;
}
.footer-bottom p {
color: var(--color-text-muted);
font-size: 0.75rem;
margin: 0;
}
/* Responsive */
@media (max-width: 768px) {
.site-footer {
margin-top: 3rem;
padding: 2rem 0 1rem;
}
.footer-content {
grid-template-columns: 1fr;
gap: 2rem;
}
.footer-nav {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.footer-bottom {
text-align: center;
}
}
/* Full width pages adjustment */
body.full-width .site-footer {
margin-top: 0;
}
</style>

View file

@ -1,309 +0,0 @@
---
import GameStats from './GameStats.astro';
import Button from './Button.astro';
export interface Props {
title: string;
description: string;
slug: string;
thumbnail?: string;
tags?: string[];
complexity?: 'Minimal' | 'Einfach' | 'Mittel' | 'Komplex';
codeStats?: {
total: number;
code: number;
comments: number;
};
}
const { title, description, slug, thumbnail, tags = [], complexity, codeStats } = Astro.props;
---
<article class="game-card">
<a href={`/games/${slug}`} class="card-link">
<div class="card-image">
{
thumbnail ? (
<img src={thumbnail} alt={title} />
) : (
<div class="placeholder">
<span>{title.charAt(0)}</span>
</div>
)
}
</div>
<div class="card-content">
<h3 class="card-title">{title}</h3>
<p class="card-description">{description}</p>
<div class="card-meta">
{
complexity && (
<span class={`complexity complexity-${complexity.toLowerCase()}`}>{complexity}</span>
)
}
{
tags.length > 0 && (
<div class="card-tags">
{tags.map((tag) => (
<span class="tag">{tag}</span>
))}
</div>
)
}
</div>
{
codeStats && (
<div class="code-info">
<span class="code-lines">
{codeStats.total} Zeilen
<span class="code-detail">
({codeStats.code} Code / {codeStats.comments} Kommentare)
</span>
</span>
</div>
)
}
<GameStats gameId={slug} />
</div>
<div class="hover-buttons">
<Button
href={`/games/${slug}/playground`}
variant="secondary"
size="small"
class="code-btn"
onclick="event.stopPropagation(); event.preventDefault(); window.location.href=this.href;"
>
Code
</Button>
<Button variant="primary" size="small" class="play-btn"> Spielen </Button>
</div>
</a>
</article>
<style>
/* Basis Card Styling */
.game-card {
position: relative;
width: 100%;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 1rem;
overflow: hidden;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.game-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.15);
}
/* Link Container */
.card-link {
display: block;
text-decoration: none;
color: inherit;
}
/* Bild/Placeholder Section */
.card-image {
position: relative;
width: 100%;
aspect-ratio: 4/3;
background: #0a0a0a;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a1a, #0a0a0a);
color: var(--color-accent);
font-size: 4rem;
font-weight: 900;
text-shadow: 0 0 30px rgba(0, 255, 136, 0.5);
}
/* Content Section */
.card-content {
padding: 1.5rem;
}
.card-title {
margin: 0 0 0.75rem 0;
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text);
}
.card-description {
margin: 0 0 1rem 0;
font-size: 0.95rem;
line-height: 1.5;
color: var(--color-text-secondary);
}
/* Tags */
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Hover Buttons */
.hover-buttons {
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.game-card:hover .hover-buttons {
opacity: 1;
pointer-events: all;
}
.hover-buttons :global(.play-btn) {
position: absolute;
top: 1rem;
right: 1rem;
border-radius: 20px;
}
.hover-buttons :global(.code-btn) {
position: absolute;
top: 1rem;
left: 1rem;
border-radius: 20px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
}
/* Responsive */
@media (max-width: 768px) {
.card-image {
height: 150px;
}
.card-content {
padding: 1.25rem;
}
.card-title {
font-size: 1.25rem;
}
.card-description {
font-size: 0.9rem;
}
}
/* Code Info */
.code-info {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.code-lines {
font-size: 0.8rem;
color: var(--color-accent);
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.code-lines::before {
content: '< >';
font-family: monospace;
opacity: 0.7;
}
.code-detail {
font-size: 0.75rem;
color: var(--color-text-secondary);
font-weight: normal;
margin-left: 0.25rem;
}
/* Card Meta Section */
.card-meta {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* Complexity Badge */
.complexity {
display: inline-block;
padding: 0.25rem 0.75rem;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
border-radius: 12px;
align-self: flex-start;
}
.complexity-minimal {
background: #4ade80;
color: #000;
}
.complexity-einfach {
background: #60a5fa;
color: #000;
}
.complexity-mittel {
background: #fbbf24;
color: #000;
}
.complexity-komplex {
background: #f87171;
color: #000;
}
</style>
<script>
// Fallback für fehlende Bilder
const images = document.querySelectorAll('.card-image img');
images.forEach((img) => {
img.addEventListener('error', function () {
const placeholder = document.createElement('div');
placeholder.className = 'placeholder';
placeholder.innerHTML = `<span>${this.alt.charAt(0)}</span>`;
this.parentElement.replaceChild(placeholder, this);
});
});
</script>

View file

@ -1,146 +0,0 @@
---
import { statsService } from '../services/statsService';
export interface Props {
gameId: string;
showDetails?: boolean;
}
const { gameId, showDetails = false } = Astro.props;
const stats = statsService.getStats(gameId);
---
{
stats && (
<div class="game-stats">
<div class="stats-row">
{stats.highScore > 0 && (
<div class="stat-item highscore">
<span class="stat-icon">🏆</span>
<span class="stat-value">{stats.highScore.toLocaleString('de-DE')}</span>
</div>
)}
{stats.gamesPlayed > 0 && (
<div class="stat-item games-played">
<span class="stat-icon">🎮</span>
<span class="stat-value">{stats.gamesPlayed}x</span>
</div>
)}
{stats.totalPlayTime > 0 && (
<div class="stat-item play-time">
<span class="stat-icon">⏱️</span>
<span class="stat-value">{statsService.formatPlayTime(stats.totalPlayTime)}</span>
</div>
)}
</div>
{showDetails && stats.lastPlayed && (
<div class="last-played">
Zuletzt gespielt: {statsService.getRelativeTime(stats.lastPlayed)}
</div>
)}
{showDetails && stats.achievements && stats.achievements.length > 0 && (
<div class="achievements">
<h4>Achievements</h4>
<div class="achievement-list">
{stats.achievements.map((achievement) => (
<div class="achievement" title={achievement.description}>
<span class="achievement-icon">🏅</span>
<span class="achievement-name">{achievement.name}</span>
</div>
))}
</div>
</div>
)}
</div>
)
}
<style>
.game-stats {
margin-top: 0.5rem;
font-size: 0.85rem;
}
.stats-row {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.stat-item {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-icon {
font-size: 0.9rem;
}
.stat-value {
font-weight: 600;
color: var(--color-text);
}
.highscore .stat-value {
color: #fbbf24;
}
.games-played .stat-value {
color: #60a5fa;
}
.play-time .stat-value {
color: #4ade80;
}
.last-played {
margin-top: 0.5rem;
color: var(--color-text-secondary);
font-size: 0.75rem;
}
.achievements {
margin-top: 1rem;
}
.achievements h4 {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: var(--color-text);
}
.achievement-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.achievement {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: rgba(255, 215, 0, 0.1);
border: 1px solid rgba(255, 215, 0, 0.3);
border-radius: 8px;
font-size: 0.75rem;
}
.achievement-icon {
font-size: 0.8rem;
}
.achievement-name {
color: #fbbf24;
font-weight: 500;
}
</style>

View file

@ -1,352 +0,0 @@
---
import GameCard from './GameCard.astro';
export interface Props {
title: string;
games: any[];
id?: string;
}
const { title, games, id = 'scroller' } = Astro.props;
---
<section class="scroller-section">
<div class="scroller-header">
<h2>{title}</h2>
<div class="scroller-controls">
<button class="scroll-btn scroll-left" data-scroller={id} aria-label="Nach links scrollen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M15 18L9 12L15 6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
</button>
<button class="scroll-btn scroll-right" data-scroller={id} aria-label="Nach rechts scrollen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M9 18L15 12L9 6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
</button>
</div>
</div>
<div class="scroller-container">
<div class="scroller-gradient-left"></div>
<div class="scroller-gradient-right"></div>
<div class="scroller-track" id={id}>
<div class="scroller-content">
{
games.map((game) => (
<div class="scroller-item">
<GameCard
title={game.title}
description={game.description}
slug={game.slug}
thumbnail={game.thumbnail}
tags={game.tags}
complexity={game.complexity}
codeStats={game.codeStats}
/>
</div>
))
}
</div>
</div>
</div>
</section>
<style>
.scroller-section {
position: relative;
margin-bottom: 3rem;
width: 100%;
}
.scroller-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding: 0 max(1.5rem, calc((100vw - 1400px) / 2));
}
.scroller-header h2 {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
margin: 0;
}
.scroller-controls {
display: flex;
gap: 0.5rem;
}
.scroll-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--color-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
opacity: 0.6;
}
.scroll-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.15);
opacity: 1;
transform: scale(1.05);
}
.scroll-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.scroller-container {
position: relative;
width: 100%;
overflow: hidden;
}
.scroller-gradient-left,
.scroller-gradient-right {
position: absolute;
top: 0;
bottom: 0;
width: 100px;
z-index: 2;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.scroller-gradient-left {
left: 0;
background: linear-gradient(90deg, var(--color-bg) 0%, transparent 100%);
}
.scroller-gradient-right {
right: 0;
background: linear-gradient(270deg, var(--color-bg) 0%, transparent 100%);
}
.scroller-container.has-scroll-left .scroller-gradient-left,
.scroller-container.has-scroll-right .scroller-gradient-right {
opacity: 1;
}
.scroller-track {
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
padding: 0.5rem 0 1.5rem;
}
.scroller-track::-webkit-scrollbar {
display: none;
}
.scroller-content {
display: flex;
gap: 1.5rem;
padding: 0 max(1.5rem, calc((100vw - 1400px) / 2));
min-width: min-content;
}
.scroller-item {
flex: 0 0 320px;
max-width: 320px;
opacity: 0;
transform: translateY(20px);
animation: scrollerItemFadeIn 0.4s ease forwards;
}
.scroller-item:nth-child(1) {
animation-delay: 0s;
}
.scroller-item:nth-child(2) {
animation-delay: 0.05s;
}
.scroller-item:nth-child(3) {
animation-delay: 0.1s;
}
.scroller-item:nth-child(4) {
animation-delay: 0.15s;
}
.scroller-item:nth-child(5) {
animation-delay: 0.2s;
}
.scroller-item:nth-child(6) {
animation-delay: 0.25s;
}
.scroller-item:nth-child(n + 7) {
animation-delay: 0.3s;
}
@keyframes scrollerItemFadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
@media (hover: hover) {
.scroller-item {
transition: transform 0.2s ease;
}
.scroller-item:hover {
transform: scale(1.02);
}
}
@media (max-width: 768px) {
.scroller-header {
padding: 0 1rem;
}
.scroller-content {
padding: 0 1rem;
gap: 1rem;
}
.scroller-item {
flex: 0 0 280px;
max-width: 280px;
}
.scroll-btn {
width: 36px;
height: 36px;
}
.scroller-gradient-left,
.scroller-gradient-right {
width: 50px;
}
}
@media (max-width: 480px) {
.scroller-item {
flex: 0 0 240px;
max-width: 240px;
}
.scroller-controls {
display: none;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const scrollers = document.querySelectorAll('.scroller-track');
scrollers.forEach((scroller) => {
const scrollerId = scroller.id;
const container = scroller.closest('.scroller-container');
const leftBtn = document.querySelector(
`.scroll-left[data-scroller="${scrollerId}"]`
) as HTMLButtonElement;
const rightBtn = document.querySelector(
`.scroll-right[data-scroller="${scrollerId}"]`
) as HTMLButtonElement;
if (!container || !leftBtn || !rightBtn) return;
const updateButtons = () => {
const scrollLeft = scroller.scrollLeft;
const scrollWidth = scroller.scrollWidth;
const clientWidth = scroller.clientWidth;
leftBtn.disabled = scrollLeft <= 0;
rightBtn.disabled = scrollLeft >= scrollWidth - clientWidth - 1;
if (scrollLeft > 0) {
container.classList.add('has-scroll-left');
} else {
container.classList.remove('has-scroll-left');
}
if (scrollLeft < scrollWidth - clientWidth - 1) {
container.classList.add('has-scroll-right');
} else {
container.classList.remove('has-scroll-right');
}
};
const scrollAmount = () => {
const item = scroller.querySelector('.scroller-item') as HTMLElement;
if (!item) return 320;
return item.offsetWidth + 24;
};
leftBtn.addEventListener('click', () => {
scroller.scrollBy({ left: -scrollAmount(), behavior: 'smooth' });
});
rightBtn.addEventListener('click', () => {
scroller.scrollBy({ left: scrollAmount(), behavior: 'smooth' });
});
scroller.addEventListener('scroll', updateButtons);
window.addEventListener('resize', updateButtons);
setTimeout(updateButtons, 100);
let touchStartX = 0;
let touchEndX = 0;
let isSwiping = false;
scroller.addEventListener(
'touchstart',
(e) => {
touchStartX = e.touches[0].clientX;
isSwiping = true;
},
{ passive: true }
);
scroller.addEventListener(
'touchmove',
(e) => {
if (!isSwiping) return;
touchEndX = e.touches[0].clientX;
},
{ passive: true }
);
scroller.addEventListener('touchend', () => {
if (!isSwiping) return;
isSwiping = false;
const swipeDistance = touchEndX - touchStartX;
const threshold = 50;
if (Math.abs(swipeDistance) > threshold) {
if (swipeDistance > 0) {
scroller.scrollBy({ left: -scrollAmount(), behavior: 'smooth' });
} else {
scroller.scrollBy({ left: scrollAmount(), behavior: 'smooth' });
}
}
});
});
});
</script>

View file

@ -1,183 +0,0 @@
---
// Keine Props benötigt
---
<div id="install-prompt" class="install-prompt hidden">
<div class="prompt-content">
<div class="prompt-icon">📱</div>
<div class="prompt-text">
<h3>App installieren</h3>
<p>Installiere Mana Games für schnelleren Zugriff!</p>
</div>
<div class="prompt-actions">
<button id="install-button" class="install-btn">Installieren</button>
<button id="dismiss-button" class="dismiss-btn">Später</button>
</div>
</div>
</div>
<style>
.install-prompt {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
z-index: 1000;
max-width: 400px;
width: calc(100% - 2rem);
transition: all 0.3s ease;
}
.install-prompt.hidden {
display: none;
}
.prompt-content {
display: flex;
align-items: center;
gap: 1rem;
}
.prompt-icon {
font-size: 2.5rem;
flex-shrink: 0;
}
.prompt-text h3 {
margin: 0 0 0.25rem 0;
font-size: 1.1rem;
color: var(--color-text);
}
.prompt-text p {
margin: 0;
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.prompt-actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.install-btn,
.dismiss-btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.install-btn {
background-color: var(--color-accent);
color: #000;
}
.install-btn:hover {
background-color: var(--color-accent-secondary);
transform: translateY(-1px);
}
.dismiss-btn {
background-color: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
}
.dismiss-btn:hover {
background-color: rgba(255, 255, 255, 0.05);
}
@media (max-width: 768px) {
.install-prompt {
bottom: 1rem;
padding: 1rem;
}
.prompt-content {
flex-wrap: wrap;
}
.prompt-icon {
font-size: 2rem;
}
.prompt-actions {
width: 100%;
margin-top: 1rem;
}
.install-btn,
.dismiss-btn {
flex: 1;
}
}
</style>
<script>
let deferredPrompt: any;
const installPrompt = document.getElementById('install-prompt');
const installButton = document.getElementById('install-button');
const dismissButton = document.getElementById('dismiss-button');
// Prüfe ob App bereits installiert ist
if (window.matchMedia('(display-mode: standalone)').matches) {
// App ist bereits installiert
} else {
// Zeige Prompt nach 30 Sekunden oder 3 Seitenaufrufen
const promptShown = localStorage.getItem('install-prompt-shown');
const pageViews = parseInt(localStorage.getItem('page-views') || '0') + 1;
localStorage.setItem('page-views', pageViews.toString());
if (!promptShown && pageViews >= 3) {
setTimeout(() => {
if (installPrompt && deferredPrompt) {
installPrompt.classList.remove('hidden');
}
}, 30000);
}
}
// Installationsprompt abfangen
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
});
// Install Button Handler
installButton?.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('PWA wurde installiert');
}
deferredPrompt = null;
installPrompt?.classList.add('hidden');
localStorage.setItem('install-prompt-shown', 'true');
});
// Dismiss Button Handler
dismissButton?.addEventListener('click', () => {
installPrompt?.classList.add('hidden');
localStorage.setItem('install-prompt-shown', 'true');
});
// App wurde installiert
window.addEventListener('appinstalled', () => {
console.log('PWA wurde erfolgreich installiert');
installPrompt?.classList.add('hidden');
});
</script>

View file

@ -1,537 +0,0 @@
---
export interface Props {
maxGames?: number;
}
const { maxGames = 8 } = Astro.props;
---
<div class="my-games-section">
<div class="section-header">
<h2>Meine generierten Spiele</h2>
<div class="section-actions">
<button id="viewAllMyGames" class="action-btn"> Alle anzeigen </button>
<button id="clearMyGames" class="action-btn danger hidden"> Alle löschen </button>
</div>
</div>
<div id="myGamesContainer" class="my-games-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Lade deine Spiele...</p>
</div>
</div>
<div id="emptyState" class="empty-state hidden">
<div class="empty-content">
<p class="empty-icon">🎮</p>
<p class="empty-text">Du hast noch keine Spiele erstellt</p>
<a href="/create" class="create-btn"> Erstelle dein erstes Spiel </a>
</div>
</div>
</div>
<script>
interface SavedGame {
id: string;
title: string;
description: string;
prompt: string;
html: string;
createdAt: Date;
updatedAt: Date;
thumbnail?: string;
stats?: {
linesOfCode: number;
hasAnimation: boolean;
hasSound: boolean;
};
}
class MyGamesManager {
private dbName = 'ManaGamesDB';
private storeName = 'generatedGames';
private db: IDBDatabase | null = null;
private container: HTMLElement;
private emptyState: HTMLElement;
private viewAllBtn: HTMLElement;
private clearBtn: HTMLElement;
private maxGames: number;
constructor(maxGames: number = 8) {
this.container = document.getElementById('myGamesContainer')!;
this.emptyState = document.getElementById('emptyState')!;
this.viewAllBtn = document.getElementById('viewAllMyGames')!;
this.clearBtn = document.getElementById('clearMyGames')!;
this.maxGames = maxGames;
this.init();
}
async init() {
await this.openDB();
await this.loadGames();
// Event listeners
this.viewAllBtn.addEventListener('click', () => {
window.location.href = '/my-games';
});
this.clearBtn.addEventListener('click', async () => {
if (confirm('Bist du sicher, dass du alle deine generierten Spiele löschen möchtest?')) {
await this.clearAllGames();
}
});
}
async openDB(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
store.createIndex('createdAt', 'createdAt', { unique: false });
store.createIndex('title', 'title', { unique: false });
}
};
});
}
async loadGames() {
try {
const games = await this.getAllGames();
if (games.length === 0) {
this.showEmptyState();
return;
}
// Sort by creation date (newest first)
games.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
// Show only first maxGames
const displayGames = games.slice(0, this.maxGames);
this.renderGames(displayGames, games.length);
// Show/hide buttons
if (games.length > this.maxGames) {
this.viewAllBtn.classList.remove('hidden');
}
this.clearBtn.classList.remove('hidden');
} catch (error) {
console.error('Error loading games:', error);
this.showError();
}
}
async getAllGames(): Promise<SavedGame[]> {
if (!this.db) await this.openDB();
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result || []);
request.onerror = () => reject(request.error);
});
}
async deleteGame(id: string): Promise<void> {
if (!this.db) await this.openDB();
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async clearAllGames() {
if (!this.db) await this.openDB();
const transaction = this.db!.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
await store.clear();
this.showEmptyState();
this.clearBtn.classList.add('hidden');
this.viewAllBtn.classList.add('hidden');
}
renderGames(games: SavedGame[], totalCount: number) {
const gamesHTML = games.map((game) => this.createGameCard(game)).join('');
this.container.innerHTML = `
<div class="games-grid">
${gamesHTML}
</div>
${
totalCount > this.maxGames
? `
<p class="more-games-text">
+${totalCount - this.maxGames} weitere Spiele in deiner Bibliothek
</p>
`
: ''
}
`;
// Add event listeners to game cards
this.container.querySelectorAll('.delete-game-btn').forEach((btn) => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const gameId = (e.target as HTMLElement)
.closest('.delete-game-btn')
?.getAttribute('data-game-id');
if (gameId) {
await this.deleteGame(gameId);
await this.loadGames();
}
});
});
this.container.querySelectorAll('.my-game-card').forEach((card) => {
card.addEventListener('click', (e) => {
if (!(e.target as HTMLElement).closest('.delete-game-btn')) {
const gameId = card.getAttribute('data-game-id');
if (gameId) {
window.location.href = `/play-generated?id=${gameId}`;
}
}
});
});
}
createGameCard(game: SavedGame): string {
const date = new Date(game.createdAt).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
return `
<div class="my-game-card" data-game-id="${game.id}">
<div class="game-thumbnail">
${
game.thumbnail
? `<img src="${game.thumbnail}" alt="${game.title}" />`
: `<div class="placeholder-thumbnail">🎮</div>`
}
<button class="delete-game-btn" data-game-id="${game.id}" title="Spiel löschen">
<span>×</span>
</button>
</div>
<div class="game-info">
<h3>${game.title}</h3>
<p class="game-date">${date}</p>
${
game.stats
? `
<div class="game-stats">
<span>${game.stats.linesOfCode} Zeilen</span>
${game.stats.hasAnimation ? '<span>🎬</span>' : ''}
${game.stats.hasSound ? '<span>🔊</span>' : ''}
</div>
`
: ''
}
</div>
</div>
`;
}
showEmptyState() {
this.container.classList.add('hidden');
this.emptyState.classList.remove('hidden');
}
showError() {
this.container.innerHTML = `
<div class="error-state">
<p>Fehler beim Laden der Spiele</p>
</div>
`;
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
const maxGames = parseInt(
document.querySelector('.my-games-section')?.getAttribute('data-max-games') || '8'
);
new MyGamesManager(maxGames);
});
</script>
<style>
.my-games-section {
margin: 3rem 0;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.section-header h2 {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
margin: 0;
}
.section-actions {
display: flex;
gap: 1rem;
}
.action-btn {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 0.5rem 1rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.action-btn:hover {
background: var(--color-bg);
border-color: var(--color-accent);
}
.action-btn.danger {
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.action-btn.danger:hover {
background: rgba(239, 68, 68, 0.1);
border-color: #ef4444;
}
.my-games-container {
min-height: 200px;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--color-text-secondary);
}
.spinner {
width: 2rem;
height: 2rem;
border: 2px solid var(--color-border);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.games-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
}
.my-game-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.my-game-card:hover {
transform: translateY(-2px);
border-color: var(--color-accent);
box-shadow: 0 4px 12px rgba(0, 255, 136, 0.1);
}
.game-thumbnail {
position: relative;
aspect-ratio: 4/3;
background: var(--color-bg);
overflow: hidden;
}
.game-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.placeholder-thumbnail {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--color-text-muted);
}
.delete-game-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: rgba(0, 0, 0, 0.8);
color: white;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: all 0.2s ease;
font-size: 1.5rem;
line-height: 1;
}
.my-game-card:hover .delete-game-btn {
opacity: 1;
}
.delete-game-btn:hover {
background: rgba(239, 68, 68, 0.9);
transform: scale(1.1);
}
.game-info {
padding: 1rem;
}
.game-info h3 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.game-date {
color: var(--color-text-secondary);
font-size: 0.875rem;
margin: 0 0 0.5rem 0;
}
.game-stats {
display: flex;
gap: 0.75rem;
font-size: 0.75rem;
color: var(--color-text-muted);
}
.game-stats span {
display: flex;
align-items: center;
gap: 0.25rem;
}
.more-games-text {
text-align: center;
color: var(--color-text-secondary);
margin-top: 2rem;
font-size: 0.9rem;
}
.empty-state {
padding: 4rem 2rem;
text-align: center;
}
.empty-content {
max-width: 400px;
margin: 0 auto;
}
.empty-icon {
font-size: 4rem;
margin: 0 0 1rem 0;
opacity: 0.3;
}
.empty-text {
color: var(--color-text-secondary);
font-size: 1.1rem;
margin: 0 0 2rem 0;
}
.create-btn {
display: inline-block;
background: var(--color-accent);
color: var(--color-bg);
padding: 0.75rem 2rem;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
transition: all 0.2s ease;
}
.create-btn:hover {
background: var(--color-accent-hover);
transform: translateY(-1px);
}
.error-state {
text-align: center;
padding: 3rem;
color: #ef4444;
}
.hidden {
display: none !important;
}
@media (max-width: 768px) {
.games-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.section-actions {
width: 100%;
justify-content: flex-start;
}
}
</style>

View file

@ -1,713 +0,0 @@
---
import Button from '../components/Button.astro';
import InstallPrompt from '../components/InstallPrompt.astro';
import Footer from '../components/Footer.astro';
export interface Props {
title: string;
description?: string;
isGamePage?: boolean;
gameTitle?: string;
gameSlug?: string;
isPlayground?: boolean;
fullWidth?: boolean;
hideFooter?: boolean;
}
const { title, description = "Mana Games - Eine Sammlung von Web-basierten Spielen", isGamePage = false, gameTitle, gameSlug, isPlayground = false, fullWidth = false, hideFooter = false } = Astro.props;
---
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="description" content={description} />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title} | Mana Games</title>
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Theme Color -->
<meta name="theme-color" content="#1a1a1a" />
<!-- iOS Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Mana Games" />
<!-- iOS Icons -->
<link rel="apple-touch-icon" href="/icons/icon-180x180.png" />
<link rel="apple-touch-icon" sizes="120x120" href="/icons/icon-120x120.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/icons/icon-167x167.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180x180.png" />
<!-- iOS Splash Screens -->
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-640x1136.png" />
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-750x1334.png" />
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-828x1792.png" />
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" href="/splash/splash-1125x2436.png" />
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" href="/splash/splash-1242x2688.png" />
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-1536x2048.png" />
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-1668x2224.png" />
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-2048x2732.png" />
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#1a1a1a" />
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
<!-- Open Graph -->
<meta property="og:title" content={title + " | Mana Games"} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:image" content="/icons/icon-512x512.png" />
</head>
<body class={fullWidth ? 'full-width' : ''}>
<nav>
<div class="nav-container">
{isGamePage ? (
<div class="breadcrumb">
<a href="/" class="breadcrumb-logo">
<span class="logo-text">MANA</span>
<span class="logo-accent">GAMES</span>
</a>
<span class="breadcrumb-separator"></span>
<span class="breadcrumb-game">{gameTitle}</span>
</div>
) : (
<a href="/" class="logo">
<span class="logo-text">MANA</span>
<span class="logo-accent">GAMES</span>
</a>
)}
<div class="nav-links">
{isGamePage ? (
<div class="game-controls">
<Button
href="/"
variant="ghost"
size="icon"
title="Zurück zur Spieleübersicht"
class="back-btn"
>
<span class="icon">←</span>
</Button>
<div class="separator"></div>
<Button
id="menuBtn"
variant="ghost"
size="icon"
title="Menü öffnen"
>
<span class="icon">☰</span>
</Button>
<Button
id="refreshBtn"
variant="ghost"
size="icon"
title="Spiel neu laden"
>
<span class="icon">↻</span>
</Button>
<Button
id="fullscreenBtn"
variant="ghost"
size="icon"
title="Vollbild"
>
<span class="icon">⛶</span>
</Button>
<div class="separator"></div>
{isPlayground ? (
<Button
href={`/games/${gameSlug}`}
variant="accent"
size="icon"
title="Zum Spiel"
>
<span class="icon">🎮</span>
</Button>
) : (
<Button
href={`/games/${gameSlug}/playground`}
variant="accent"
size="icon"
title="Code bearbeiten"
>
<span class="icon">🔧</span>
</Button>
)}
</div>
) : (
<div class="nav-menu">
<Button href="/" variant="accent">Spiele</Button>
<Button href="/create" variant="accent">KI Generator</Button>
<Button href="/community" variant="accent">Community</Button>
<Button href="/submit" variant="ghost">Einreichen</Button>
<Button href="/stats" variant="ghost">Stats</Button>
<!-- More Dropdown -->
<div class="dropdown">
<button class="dropdown-toggle" id="moreDropdown" title="Mehr Optionen">
<span class="icon">⋮</span>
</button>
<div class="dropdown-menu" id="moreDropdownMenu">
<button class="dropdown-item" id="debugToggleDropdown">
<span class="icon">🐛</span>
<span>Debug Borders</span>
</button>
<div class="dropdown-divider"></div>
<a href="/datenschutz" class="dropdown-item">
<span class="icon">🔒</span>
<span>Datenschutz</span>
</a>
<a href="/impressum" class="dropdown-item">
<span class="icon">📋</span>
<span>Impressum</span>
</a>
<div class="dropdown-divider"></div>
<a href="/agb" class="dropdown-item">
<span class="icon">📜</span>
<span>AGB</span>
</a>
<a href="/jugendschutz" class="dropdown-item">
<span class="icon">🛡️</span>
<span>Jugendschutz</span>
</a>
<a href="/copyright" class="dropdown-item">
<span class="icon">©️</span>
<span>Copyright</span>
</a>
</div>
</div>
</div>
)}
</div>
</div>
</nav>
<main>
<slot />
</main>
{!hideFooter && <Footer />}
<InstallPrompt />
<script>
// Service Worker Registration
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registriert:', registration);
// Update gefunden
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// Neuer Service Worker verfügbar
if (confirm('Neue Version verfügbar! Jetzt aktualisieren?')) {
newWorker.postMessage({ type: 'SKIP_WAITING' });
window.location.reload();
}
}
});
});
} catch (error) {
console.error('Service Worker Registrierung fehlgeschlagen:', error);
}
});
}
// iOS PWA Detection
if (window.navigator.standalone === true) {
document.documentElement.classList.add('ios-pwa');
}
// Debug Borders Toggle
const debugToggleDropdown = document.getElementById('debugToggleDropdown');
const debugState = localStorage.getItem('debugBorders') === 'true';
// Apply initial state
if (debugState) {
document.body.classList.add('debug-borders');
}
// Function to toggle debug borders
function toggleDebugBorders() {
const isEnabled = document.body.classList.toggle('debug-borders');
localStorage.setItem('debugBorders', isEnabled.toString());
}
// Add click handler for dropdown button
debugToggleDropdown?.addEventListener('click', () => {
toggleDebugBorders();
// Close dropdown after clicking
document.getElementById('moreDropdownMenu')?.classList.remove('show');
});
// Dropdown Menu Toggle
const moreDropdown = document.getElementById('moreDropdown');
const moreDropdownMenu = document.getElementById('moreDropdownMenu');
moreDropdown?.addEventListener('click', (e) => {
e.stopPropagation();
moreDropdownMenu?.classList.toggle('show');
});
// Close dropdown when clicking outside
document.addEventListener('click', () => {
moreDropdownMenu?.classList.remove('show');
});
// Prevent dropdown from closing when clicking inside
moreDropdownMenu?.addEventListener('click', (e) => {
e.stopPropagation();
});
// Install Prompt für Android
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// Zeige Install-Button wenn gewünscht
const installButton = document.getElementById('install-button');
if (installButton) {
installButton.style.display = 'block';
installButton.addEventListener('click', async () => {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User ${outcome} the install prompt`);
deferredPrompt = null;
});
}
});
</script>
</body>
</html>
<style is:global>
:root {
--color-bg: #0a0a0a;
--color-bg-secondary: #1a1a1a;
--color-text: #ffffff;
--color-text-secondary: #b0b0b0;
--color-accent: #00ff88;
--color-accent-secondary: #00cc6a;
--color-border: #2a2a2a;
--max-width: 1200px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
}
body.no-scroll {
overflow: hidden;
height: 100vh;
}
main {
flex: 1;
width: 100%;
max-width: var(--max-width);
margin: 0 auto;
padding: 2rem;
}
body.full-width main {
max-width: none;
padding: 0;
}
/* Override container widths for full-width pages */
body.full-width .hero,
body.full-width .games-section,
body.full-width .stats-section,
body.full-width section {
max-width: none !important;
width: 100% !important;
}
body.full-width .games-grid {
max-width: none !important;
padding: 0 2rem;
}
body.full-width .section-content,
body.full-width .stats-container {
max-width: none !important;
}
body.no-scroll main {
padding: 0;
height: 100%;
overflow: hidden;
}
nav {
background-color: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border);
position: sticky;
top: 0;
z-index: 100;
}
/* Nav Left Container */
.nav-left {
display: flex;
align-items: center;
gap: 1rem;
}
/* Debug Toggle Button */
.debug-toggle {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
font-size: 1rem;
padding: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4px;
opacity: 0.7;
display: flex;
align-items: center;
justify-content: center;
}
/* Dropdown Styles */
.dropdown {
position: relative;
}
.dropdown-toggle {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text);
padding: 0.5rem 0.75rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 1.2rem;
display: flex;
align-items: center;
justify-content: center;
}
.dropdown-toggle:hover {
background: rgba(255, 255, 255, 0.05);
border-color: var(--color-accent);
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 0.5rem;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 8px;
min-width: 200px;
padding: 0.5rem;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s ease;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.dropdown-menu.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
background: transparent;
border: none;
color: var(--color-text);
text-decoration: none;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4px;
font-family: inherit;
}
.dropdown-item:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--color-accent);
}
.dropdown-item .icon {
font-size: 1.1rem;
width: 1.5rem;
text-align: center;
}
.dropdown-divider {
height: 1px;
background: var(--color-border);
margin: 0.5rem 0;
}
.debug-toggle:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.05);
border-color: var(--color-border);
}
.debug-toggle.clicked {
animation: pulse 0.3s ease;
}
body.debug-borders .debug-toggle {
opacity: 1;
color: var(--color-accent);
border-color: var(--color-accent);
}
@keyframes pulse {
0% { transform: translateY(-50%) scale(1); }
50% { transform: translateY(-50%) scale(1.2); }
100% { transform: translateY(-50%) scale(1); }
}
.nav-container {
max-width: var(--max-width);
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
text-decoration: none;
font-size: 1.5rem;
font-weight: 900;
letter-spacing: -0.05em;
}
.logo-text {
color: var(--color-text);
}
.logo-accent {
color: var(--color-accent);
margin-left: 0.25rem;
}
.nav-links {
display: flex;
gap: 2rem;
}
.nav-menu {
display: flex;
gap: 1rem;
align-items: center;
}
.game-controls {
display: flex;
gap: 0.75rem;
align-items: center;
}
.separator {
width: 1px;
height: 24px;
background-color: var(--color-border);
margin: 0 0.5rem;
}
/* Special styling for back button */
.back-btn:hover {
background-color: var(--color-text) !important;
color: var(--color-bg) !important;
border-color: var(--color-text) !important;
}
/* Breadcrumb Navigation */
.breadcrumb {
display: flex;
align-items: center;
gap: 0.75rem;
}
.breadcrumb-logo {
text-decoration: none;
font-size: 1.5rem;
font-weight: 900;
letter-spacing: -0.05em;
opacity: 0.5;
transition: opacity 0.2s ease;
}
.breadcrumb-logo:hover {
opacity: 0.8;
}
.breadcrumb-logo .logo-text {
color: var(--color-text);
}
.breadcrumb-logo .logo-accent {
color: var(--color-accent);
margin-left: 0.25rem;
}
.breadcrumb-separator {
color: var(--color-text-secondary);
opacity: 0.3;
font-size: 1.5rem;
margin: 0 0.25rem;
}
.breadcrumb-game {
color: var(--color-text);
font-size: 1.5rem;
font-weight: 900;
letter-spacing: -0.05em;
}
h1, h2, h3, h4, h5, h6 {
color: var(--color-text);
margin-bottom: 1rem;
}
p {
margin-bottom: 1rem;
}
@media (max-width: 768px) {
main {
padding: 1rem;
}
.nav-container {
padding: 1rem;
}
.nav-links {
gap: 1rem;
}
.logo {
font-size: 1.25rem;
}
.breadcrumb-logo {
font-size: 1.25rem;
}
.breadcrumb-game {
font-size: 1.25rem;
}
.breadcrumb-separator {
font-size: 1.25rem;
}
.debug-toggle {
font-size: 0.9rem;
padding: 0.4rem;
}
.dropdown-toggle {
padding: 0.4rem 0.6rem;
font-size: 1rem;
}
.dropdown-menu {
min-width: 180px;
}
}
/* Debug Borders Styles */
body.debug-borders * {
outline: 1px solid rgba(255, 0, 0, 0.25);
}
body.debug-borders div {
outline-color: rgba(0, 255, 0, 0.3);
}
body.debug-borders button,
body.debug-borders a {
outline-color: rgba(0, 255, 255, 0.4);
}
body.debug-borders section,
body.debug-borders article,
body.debug-borders main,
body.debug-borders nav,
body.debug-borders header,
body.debug-borders footer {
outline-color: rgba(255, 255, 0, 0.4);
outline-width: 2px;
}
body.debug-borders form,
body.debug-borders input,
body.debug-borders textarea,
body.debug-borders select {
outline-color: rgba(255, 0, 255, 0.4);
}
body.debug-borders img,
body.debug-borders video,
body.debug-borders iframe,
body.debug-borders canvas {
outline-color: rgba(255, 128, 0, 0.5);
outline-width: 2px;
}
body.debug-borders .container,
body.debug-borders .wrapper,
body.debug-borders .panel,
body.debug-borders .split-container,
body.debug-borders .left-panel,
body.debug-borders .right-panel {
outline-color: rgba(128, 128, 255, 0.5);
outline-width: 2px;
outline-style: dashed;
}
/* Hover effect for debug mode */
body.debug-borders *:hover {
outline-width: 2px;
outline-style: solid;
}
/* Exclude debug button from debug borders */
body.debug-borders .debug-toggle {
outline: none !important;
}
</style>

View file

@ -0,0 +1,54 @@
<script lang="ts">
import type { Game } from '$lib/data/games';
let { game, href }: { game: Game; href: string } = $props();
const difficultyColors: Record<string, string> = {
Einfach: 'bg-green-500/20 text-green-400',
Mittel: 'bg-yellow-500/20 text-yellow-400',
Schwer: 'bg-red-500/20 text-red-400',
};
</script>
<a
{href}
class="group block rounded-xl border border-border bg-card p-0 overflow-hidden transition-all hover:border-primary/50 hover:shadow-lg hover:shadow-primary/5 hover:-translate-y-0.5"
>
{#if game.thumbnail}
<div class="aspect-video w-full overflow-hidden bg-muted">
<img
src={game.thumbnail}
alt={game.title}
class="h-full w-full object-cover transition-transform group-hover:scale-105"
loading="lazy"
/>
</div>
{:else}
<div class="aspect-video w-full bg-muted flex items-center justify-center">
<span class="text-4xl opacity-40">🎮</span>
</div>
{/if}
<div class="p-4">
<div class="flex items-start justify-between gap-2 mb-2">
<h3 class="font-semibold text-foreground group-hover:text-primary transition-colors">
{game.title}
</h3>
<span class="shrink-0 text-xs px-2 py-0.5 rounded-full {difficultyColors[game.difficulty]}">
{game.difficulty}
</span>
</div>
<p class="text-sm text-muted-foreground line-clamp-2 mb-3">
{game.description}
</p>
<div class="flex flex-wrap gap-1">
{#each game.tags.slice(0, 3) as tag}
<span class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
{tag}
</span>
{/each}
</div>
</div>
</a>

View file

@ -0,0 +1,72 @@
<script lang="ts">
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div class="app-loading-skeleton" role="status" aria-label="App wird geladen...">
<div class="header-skeleton">
<SkeletonBox width="120px" height="32px" borderRadius="8px" />
<div class="header-nav">
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
</div>
<SkeletonBox width="36px" height="36px" borderRadius="50%" />
</div>
<div class="content-skeleton">
<div class="games-placeholder">
{#each Array(6) as _}
<SkeletonBox width="100%" height="200px" borderRadius="12px" />
{/each}
</div>
</div>
</div>
<style>
.app-loading-skeleton {
min-height: 100vh;
background: hsl(var(--background));
}
.header-skeleton {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
border-bottom: 1px solid hsl(var(--border));
}
.header-nav {
display: flex;
gap: 0.5rem;
}
.content-skeleton {
max-width: 80rem;
margin: 0 auto;
padding: 2rem;
}
.games-placeholder {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
@media (max-width: 768px) {
.header-nav {
display: none;
}
.header-skeleton {
padding: 1rem;
}
.content-skeleton {
padding: 1rem;
}
.games-placeholder {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -0,0 +1 @@
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';

View file

@ -14,7 +14,6 @@ export interface Game {
code: number;
comments: number;
};
// Community game fields
community?: boolean;
author?: string;
submittedAt?: string;
@ -33,11 +32,7 @@ export const games: Game[] = [
difficulty: 'Einfach',
complexity: 'Komplex',
controls: 'Pfeiltasten oder WASD zum Steuern',
codeStats: {
total: 604,
code: 338,
comments: 192,
},
codeStats: { total: 604, code: 338, comments: 192 },
},
{
id: '2',
@ -51,11 +46,7 @@ export const games: Game[] = [
difficulty: 'Mittel',
complexity: 'Mittel',
controls: 'A/D oder Pfeiltasten zum Bewegen, Leertaste zum Schießen',
codeStats: {
total: 436,
code: 348,
comments: 32,
},
codeStats: { total: 436, code: 348, comments: 32 },
},
{
id: '3',
@ -69,11 +60,7 @@ export const games: Game[] = [
difficulty: 'Schwer',
complexity: 'Mittel',
controls: 'Klicke für Gravitationspunkte, Leertaste für Partikel',
codeStats: {
total: 426,
code: 348,
comments: 21,
},
codeStats: { total: 426, code: 348, comments: 21 },
},
{
id: '4',
@ -87,11 +74,7 @@ export const games: Game[] = [
difficulty: 'Einfach',
complexity: 'Einfach',
controls: 'Mausbewegung zum Steuern des Paddles',
codeStats: {
total: 437,
code: 289,
comments: 87,
},
codeStats: { total: 437, code: 289, comments: 87 },
},
{
id: '5',
@ -105,11 +88,7 @@ export const games: Game[] = [
difficulty: 'Mittel',
complexity: 'Komplex',
controls: 'WASD oder Pfeiltasten zum Bewegen',
codeStats: {
total: 832,
code: 644,
comments: 69,
},
codeStats: { total: 832, code: 644, comments: 69 },
},
{
id: '6',
@ -123,11 +102,7 @@ export const games: Game[] = [
difficulty: 'Mittel',
complexity: 'Komplex',
controls: 'A, S, D, F Tasten im Rhythmus drücken',
codeStats: {
total: 741,
code: 584,
comments: 56,
},
codeStats: { total: 741, code: 584, comments: 56 },
},
{
id: '7',
@ -140,11 +115,7 @@ export const games: Game[] = [
difficulty: 'Einfach',
complexity: 'Minimal',
controls: 'Klicke auf das rote Quadrat',
codeStats: {
total: 111,
code: 88,
comments: 23,
},
codeStats: { total: 111, code: 88, comments: 23 },
},
{
id: '8',
@ -158,11 +129,7 @@ export const games: Game[] = [
difficulty: 'Einfach',
complexity: 'Minimal',
controls: 'Klicke die Farben in der richtigen Reihenfolge',
codeStats: {
total: 86,
code: 86,
comments: 0,
},
codeStats: { total: 86, code: 86, comments: 0 },
},
{
id: '9',
@ -176,11 +143,7 @@ export const games: Game[] = [
difficulty: 'Einfach',
complexity: 'Minimal',
controls: 'Klicke wenn der Bildschirm grün wird',
codeStats: {
total: 78,
code: 78,
comments: 0,
},
codeStats: { total: 78, code: 78, comments: 0 },
},
{
id: '10',
@ -194,35 +157,27 @@ export const games: Game[] = [
difficulty: 'Mittel',
complexity: 'Mittel',
controls: 'WASD oder Pfeiltasten zum Fliegen, Leertaste für Boost',
codeStats: {
total: 485,
code: 428,
comments: 57,
},
codeStats: { total: 485, code: 428, comments: 57 },
},
{
id: '11',
title: 'Fish Catcher',
description:
'Fange Fische mit deinem Boot! Verschiedene Fischarten bringen unterschiedliche Punkte. Sammle Power-ups für größere Netze und Boni.',
'Fange Fische mit deinem Boot! Verschiedene Fischarten bringen unterschiedliche Punkte.',
slug: 'fish-catcher',
htmlFile: '/games/fish_catcher.html',
thumbnail: '/screenshots/fish-catcher.jpg',
tags: ['Arcade', 'Familie', 'Entspannend'],
difficulty: 'Einfach',
complexity: 'Einfach',
controls: 'A/D oder Pfeiltasten zum Bewegen, Maus für sanfte Steuerung',
codeStats: {
total: 362,
code: 321,
comments: 41,
},
controls: 'A/D oder Pfeiltasten zum Bewegen',
codeStats: { total: 362, code: 321, comments: 41 },
},
{
id: '12',
title: 'Balloon Pop',
description:
'Platze bunte Ballons bevor sie entkommen! Verschiedene Ballonarten, Power-ups und Combo-System für maximalen Spaß.',
'Platze bunte Ballons bevor sie entkommen! Verschiedene Ballonarten, Power-ups und Combo-System.',
slug: 'balloon-pop',
htmlFile: '/games/balloon_pop.html',
thumbnail: '/screenshots/balloon-pop.jpg',
@ -230,11 +185,7 @@ export const games: Game[] = [
difficulty: 'Einfach',
complexity: 'Einfach',
controls: 'Maus zum Klicken auf Ballons',
codeStats: {
total: 398,
code: 351,
comments: 47,
},
codeStats: { total: 398, code: 351, comments: 47 },
},
{
id: '13',
@ -243,34 +194,24 @@ export const games: Game[] = [
'Entschlüssele durcheinandergewürfelte Wörter! Mit 5 Kategorien, Combo-System und steigender Schwierigkeit.',
slug: 'word-scramble',
htmlFile: '/games/word_scramble.html',
thumbnail: '/screenshots/word-scramble.jpg',
tags: ['Puzzle', 'Wortspiel', 'Bildung'],
difficulty: 'Mittel',
complexity: 'Mittel',
controls: 'Tastatur zum Eingeben, Maus zum Klicken auf Buchstaben',
codeStats: {
total: 850,
code: 720,
comments: 130,
},
codeStats: { total: 850, code: 720, comments: 130 },
},
{
id: '14',
title: 'Memory Card Match',
description:
'Das klassische Memory-Spiel! Finde alle Kartenpaare mit Emojis. Drei Schwierigkeitsstufen für jeden Spieler.',
'Das klassische Memory-Spiel! Finde alle Kartenpaare mit Emojis. Drei Schwierigkeitsstufen.',
slug: 'memory-card-match',
htmlFile: '/games/memory_card_match.html',
thumbnail: '/screenshots/memory-card-match.jpg',
tags: ['Gedächtnis', 'Kartenspiel', 'Familie'],
difficulty: 'Einfach',
complexity: 'Einfach',
controls: 'Maus zum Aufdecken der Karten',
codeStats: {
total: 415,
code: 350,
comments: 0,
},
codeStats: { total: 415, code: 350, comments: 0 },
},
{
id: '15',
@ -279,106 +220,76 @@ export const games: Game[] = [
'Drift durch die Kurven und stelle Bestzeiten auf! Mit realistischer Drift-Physik und Nitro-Boost.',
slug: 'turbo-racer',
htmlFile: '/games/turbo_racer.html',
thumbnail: '/screenshots/turbo-racer.jpg',
tags: ['Rennen', 'Action', 'Arcade'],
difficulty: 'Mittel',
complexity: 'Mittel',
controls: 'WASD oder Pfeiltasten zum Fahren, Leertaste für Boost',
codeStats: {
total: 680,
code: 620,
comments: 60,
},
codeStats: { total: 680, code: 620, comments: 60 },
},
{
id: '16',
title: 'Card Stack Rush',
description:
'Sortiere Karten blitzschnell auf die richtigen Stapel! Mit wechselnden Regeln, Combo-System und Zeitdruck.',
'Sortiere Karten blitzschnell auf die richtigen Stapel! Mit wechselnden Regeln und Combo-System.',
slug: 'card-stack-rush',
htmlFile: '/games/card_stack_rush.html',
thumbnail: '/screenshots/card-stack-rush.jpg',
tags: ['Kartenspiel', 'Geschwindigkeit', 'Arcade'],
difficulty: 'Mittel',
complexity: 'Einfach',
controls: 'Drag & Drop oder Klicken zum Platzieren',
codeStats: {
total: 520,
code: 480,
comments: 0,
},
codeStats: { total: 520, code: 480, comments: 0 },
},
{
id: '17',
title: 'Flappy Mana',
description:
'Fliege durch Röhren und sammle Punkte! Ein Flappy Bird Klon mit Partikeleffekten und Highscore-System.',
'Fliege durch Röhren und sammle Punkte! Ein Flappy Bird Klon mit Partikeleffekten.',
slug: 'flappy-mana',
htmlFile: '/games/flappy_mana.html',
thumbnail: '/screenshots/flappy-mana.jpg',
tags: ['Arcade', 'Geschicklichkeit', 'Endless'],
difficulty: 'Mittel',
complexity: 'Einfach',
controls: 'Klick oder Leertaste zum Fliegen',
codeStats: {
total: 450,
code: 430,
comments: 20,
},
codeStats: { total: 450, code: 430, comments: 20 },
},
{
id: '18',
title: 'Mana Runner',
description:
'Laufe und springe durch magische Welten! Sammle Mana-Kristalle, weiche Hindernissen aus und schalte den Doppelsprung frei.',
'Laufe und springe durch magische Welten! Sammle Mana-Kristalle und weiche Hindernissen aus.',
slug: 'mana-runner',
htmlFile: '/games/mana_runner.html',
thumbnail: '/screenshots/mana-runner.jpg',
tags: ['Jump n Run', 'Arcade', 'Endless'],
difficulty: 'Mittel',
complexity: 'Mittel',
controls: 'Leertaste zum Springen, Doppelsprung nach 10 Kristallen',
codeStats: {
total: 600,
code: 580,
comments: 20,
},
codeStats: { total: 600, code: 580, comments: 20 },
},
{
id: '19',
title: 'Mana Defense',
description:
'Verteidige deinen Mana-Kristall! Baue Türme, plane deine Strategie und überlebe 20 Wellen von Gegnern.',
'Verteidige deinen Mana-Kristall! Baue Türme, plane deine Strategie und überlebe 20 Wellen.',
slug: 'mana-defense',
htmlFile: '/games/mana_defense.html',
thumbnail: '/screenshots/mana-defense.jpg',
tags: ['Tower Defense', 'Strategie', 'Aufbau'],
difficulty: 'Schwer',
complexity: 'Komplex',
controls: 'Maus zum Platzieren, 1-3 für Turmauswahl, S zum Verkaufen',
codeStats: {
total: 900,
code: 850,
comments: 50,
},
codeStats: { total: 900, code: 850, comments: 50 },
},
{
id: '20',
title: 'Mana Factory',
description:
'Baue die größte Mana-Produktionsanlage! Ein Idle-Game mit Upgrades, Prestige-System und exponentiellem Wachstum.',
'Baue die größte Mana-Produktionsanlage! Ein Idle-Game mit Upgrades und Prestige-System.',
slug: 'mana-factory',
htmlFile: '/games/mana_factory.html',
thumbnail: '/screenshots/mana-factory.jpg',
tags: ['Idle', 'Incremental', 'Aufbau'],
difficulty: 'Einfach',
complexity: 'Mittel',
controls: 'Maus zum Klicken und Kaufen',
codeStats: {
total: 800,
code: 750,
comments: 50,
},
codeStats: { total: 800, code: 750, comments: 50 },
},
{
id: '21',
@ -387,15 +298,28 @@ export const games: Game[] = [
'Klassisches Tetris-Gameplay! Stapele fallende Blöcke, vervollständige Reihen und erreiche den höchsten Score.',
slug: 'puzzle-blocks',
htmlFile: '/games/puzzle_blocks.html',
thumbnail: '/screenshots/puzzle-blocks.jpg',
tags: ['Puzzle', 'Klassiker', 'Arcade'],
difficulty: 'Mittel',
complexity: 'Einfach',
controls: '← → zum Bewegen, ↑ zum Drehen, ↓ schneller fallen, Space für Harddrop',
codeStats: {
total: 450,
code: 420,
comments: 30,
},
codeStats: { total: 450, code: 420, comments: 30 },
},
];
export function getGameBySlug(slug: string): Game | undefined {
return games.find((g) => g.slug === slug);
}
export function getGamesByTag(tag: string): Game[] {
return games.filter((g) => g.tags.includes(tag));
}
export function getAllTags(): string[] {
const tagSet = new Set<string>();
for (const game of games) {
for (const tag of game.tags) {
tagSet.add(tag);
}
}
return [...tagSet].sort();
}

View file

@ -0,0 +1 @@
// No guest seed data needed — games are static HTML files, stats build up from play

View file

@ -0,0 +1,55 @@
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
// ─── Types ──────────────────────────────────────────────────
export interface LocalGameStats extends BaseRecord {
gameId: string;
highScore: number;
lastScore: number;
gamesPlayed: number;
totalPlayTime: number;
lastPlayed: string;
}
export interface LocalGeneratedGame extends BaseRecord {
title: string;
description: string;
htmlCode: string;
prompt: string;
model: string;
iterationCount: number;
}
export interface LocalFavorite extends BaseRecord {
gameId: string;
}
// ─── Store ──────────────────────────────────────────────────
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
export const gamesStore = createLocalStore({
appId: 'mana-games',
collections: [
{
name: 'gameStats',
indexes: ['gameId', 'highScore'],
},
{
name: 'generatedGames',
indexes: ['title'],
},
{
name: 'favorites',
indexes: ['gameId'],
},
],
sync: {
serverUrl: SYNC_SERVER_URL,
},
});
// Typed collection accessors
export const gameStatsCollection = gamesStore.collection<LocalGameStats>('gameStats');
export const generatedGameCollection = gamesStore.collection<LocalGeneratedGame>('generatedGames');
export const favoriteCollection = gamesStore.collection<LocalFavorite>('favorites');

View file

@ -0,0 +1,24 @@
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
import {
gameStatsCollection,
generatedGameCollection,
favoriteCollection,
type LocalGameStats,
type LocalGeneratedGame,
type LocalFavorite,
} from './local-store';
export function useAllGameStats() {
return useLiveQueryWithDefault(async () => gameStatsCollection.getAll(), [] as LocalGameStats[]);
}
export function useAllGeneratedGames() {
return useLiveQueryWithDefault(async () => {
const games = await generatedGameCollection.getAll();
return games.reverse();
}, [] as LocalGeneratedGame[]);
}
export function useAllFavorites() {
return useLiveQueryWithDefault(async () => favoriteCollection.getAll(), [] as LocalFavorite[]);
}

View file

@ -0,0 +1,38 @@
import { browser } from '$app/environment';
import { init, register, locale, waitLocale } from 'svelte-i18n';
export const supportedLocales = ['de', 'en'] as const;
export type SupportedLocale = (typeof supportedLocales)[number];
const defaultLocale = 'de';
register('de', () => import('./locales/de.json'));
register('en', () => import('./locales/en.json'));
function getInitialLocale(): SupportedLocale {
if (browser) {
const stored = localStorage.getItem('mana_games_locale');
if (stored && supportedLocales.includes(stored as SupportedLocale)) {
return stored as SupportedLocale;
}
const browserLang = navigator.language.split('-')[0];
if (supportedLocales.includes(browserLang as SupportedLocale)) {
return browserLang as SupportedLocale;
}
}
return defaultLocale;
}
init({
fallbackLocale: defaultLocale,
initialLocale: getInitialLocale(),
});
export function setLocale(newLocale: SupportedLocale) {
locale.set(newLocale);
if (browser) {
localStorage.setItem('mana_games_locale', newLocale);
}
}
export { waitLocale };

View file

@ -0,0 +1,66 @@
{
"app": {
"name": "Mana Games",
"loading": "Wird geladen..."
},
"nav": {
"home": "Spiele",
"create": "Erstellen",
"community": "Community",
"stats": "Statistiken",
"settings": "Einstellungen"
},
"home": {
"title": "Browser-Spiele",
"subtitle": "22+ Spiele direkt im Browser spielen",
"search": "Spiel suchen...",
"noResults": "Keine Spiele gefunden",
"allGames": "Alle Spiele",
"favorites": "Favoriten",
"recentlyPlayed": "Zuletzt gespielt"
},
"game": {
"play": "Spielen",
"difficulty": "Schwierigkeit",
"controls": "Steuerung",
"tags": "Tags",
"stats": "Statistiken",
"highScore": "Highscore",
"gamesPlayed": "Spiele gespielt",
"totalPlayTime": "Gesamtspielzeit",
"lastPlayed": "Zuletzt gespielt",
"back": "Zurück",
"fullscreen": "Vollbild",
"editor": "Code ansehen"
},
"create": {
"title": "Spiel erstellen",
"subtitle": "Beschreibe dein Spiel und lass es von KI generieren",
"prompt": "Was für ein Spiel soll erstellt werden?",
"promptPlaceholder": "Ein Neon-Snake-Spiel mit Partikeleffekten...",
"generate": "Generieren",
"generating": "Generiere Spiel...",
"model": "KI-Modell",
"iterate": "Verbessern",
"save": "Speichern",
"preview": "Vorschau"
},
"stats": {
"title": "Deine Statistiken",
"totalGames": "Gespielte Spiele",
"totalTime": "Gesamtspielzeit",
"favoriteGame": "Lieblingsspiel",
"noStats": "Noch keine Statistiken. Spiele ein paar Spiele!"
},
"difficulty": {
"Einfach": "Einfach",
"Mittel": "Mittel",
"Schwer": "Schwer"
},
"time": {
"justNow": "Gerade eben",
"minutesAgo": "Vor {minutes} Minuten",
"hoursAgo": "Vor {hours} Stunden",
"daysAgo": "Vor {days} Tagen"
}
}

View file

@ -0,0 +1,66 @@
{
"app": {
"name": "Mana Games",
"loading": "Loading..."
},
"nav": {
"home": "Games",
"create": "Create",
"community": "Community",
"stats": "Statistics",
"settings": "Settings"
},
"home": {
"title": "Browser Games",
"subtitle": "22+ games to play right in your browser",
"search": "Search games...",
"noResults": "No games found",
"allGames": "All Games",
"favorites": "Favorites",
"recentlyPlayed": "Recently Played"
},
"game": {
"play": "Play",
"difficulty": "Difficulty",
"controls": "Controls",
"tags": "Tags",
"stats": "Statistics",
"highScore": "High Score",
"gamesPlayed": "Games Played",
"totalPlayTime": "Total Play Time",
"lastPlayed": "Last Played",
"back": "Back",
"fullscreen": "Fullscreen",
"editor": "View Code"
},
"create": {
"title": "Create Game",
"subtitle": "Describe your game and let AI generate it",
"prompt": "What kind of game should be created?",
"promptPlaceholder": "A neon snake game with particle effects...",
"generate": "Generate",
"generating": "Generating game...",
"model": "AI Model",
"iterate": "Improve",
"save": "Save",
"preview": "Preview"
},
"stats": {
"title": "Your Statistics",
"totalGames": "Games Played",
"totalTime": "Total Play Time",
"favoriteGame": "Favorite Game",
"noStats": "No statistics yet. Play some games!"
},
"difficulty": {
"Einfach": "Easy",
"Mittel": "Medium",
"Schwer": "Hard"
},
"time": {
"justNow": "Just now",
"minutesAgo": "{minutes} minutes ago",
"hoursAgo": "{hours} hours ago",
"daysAgo": "{days} days ago"
}
}

View file

@ -0,0 +1,114 @@
import { gameStatsCollection, type LocalGameStats } from '$lib/data/local-store';
export interface GameMessage {
type: 'GAME_EVENT' | 'GAME_LOADED' | 'GAME_ENDED';
gameId: string;
event?: string;
data?: Record<string, unknown>;
}
export function initGameCommunication(gameSlug: string) {
let gameStartTime: number | null = null;
async function getOrCreateStats(gameId: string): Promise<LocalGameStats | null> {
const all = await gameStatsCollection.getAll();
return all.find((s) => s.gameId === gameId) || null;
}
async function updateGameStats(gameId: string, update: Partial<LocalGameStats>) {
const existing = await getOrCreateStats(gameId);
if (existing) {
await gameStatsCollection.update(existing.id, {
...update,
lastPlayed: new Date().toISOString(),
});
} else {
await gameStatsCollection.insert({
gameId,
highScore: 0,
lastScore: 0,
gamesPlayed: 0,
totalPlayTime: 0,
lastPlayed: new Date().toISOString(),
...update,
});
}
}
function handleMessage(event: MessageEvent) {
if (event.origin !== window.location.origin) return;
const message = event.data as GameMessage;
if (!message.type || message.gameId !== gameSlug) return;
switch (message.type) {
case 'GAME_LOADED':
gameStartTime = Date.now();
getOrCreateStats(gameSlug).then((stats) => {
updateGameStats(gameSlug, {
gamesPlayed: (stats?.gamesPlayed || 0) + 1,
});
});
break;
case 'GAME_EVENT':
handleGameEvent(gameSlug, message.event!, message.data);
break;
case 'GAME_ENDED':
if (gameStartTime) {
const playTime = Math.floor((Date.now() - gameStartTime) / 1000);
getOrCreateStats(gameSlug).then((stats) => {
updateGameStats(gameSlug, {
totalPlayTime: (stats?.totalPlayTime || 0) + playTime,
});
});
gameStartTime = null;
}
break;
}
}
function handleBeforeUnload() {
if (gameStartTime) {
const playTime = Math.floor((Date.now() - gameStartTime) / 1000);
getOrCreateStats(gameSlug).then((stats) => {
updateGameStats(gameSlug, {
totalPlayTime: (stats?.totalPlayTime || 0) + playTime,
});
});
}
}
window.addEventListener('message', handleMessage);
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('message', handleMessage);
window.removeEventListener('beforeunload', handleBeforeUnload);
};
async function handleGameEvent(
gameId: string,
event: string,
data: Record<string, unknown> | undefined
) {
if (!data) return;
switch (event) {
case 'SCORE_UPDATE':
case 'GAME_OVER': {
const score = data.score as number;
if (score) {
const stats = await getOrCreateStats(gameId);
await updateGameStats(gameId, {
lastScore: score,
highScore: Math.max(score, stats?.highScore || 0),
});
}
break;
}
}
}
}

View file

@ -0,0 +1,5 @@
import { createManaAuthStore } from '@manacore/shared-auth-stores';
export const authStore = createManaAuthStore({
devBackendPort: 3011,
});

View file

@ -0,0 +1,5 @@
import { createSimpleNavigationStores } from '@manacore/shared-stores';
export const { isNavCollapsed } = createSimpleNavigationStores({
storageKey: 'mana-games',
});

View file

@ -0,0 +1,6 @@
import { createThemeStore } from '@manacore/shared-theme';
export const theme = createThemeStore({
appId: 'mana-games',
defaultVariant: 'lume',
});

View file

@ -0,0 +1,18 @@
import { browser } from '$app/environment';
import { createUserSettingsStore } from '@manacore/shared-theme';
import { authStore } from './auth.svelte';
function getAuthUrl(): string {
if (browser && typeof window !== 'undefined') {
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
if (injectedUrl) return injectedUrl;
}
return import.meta.env.DEV ? 'http://localhost:3001' : '';
}
export const userSettings = createUserSettingsStore({
appId: 'mana-games',
authUrl: getAuthUrl,
getAccessToken: () => authStore.getAccessToken(),
});

View file

@ -1,684 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Button from '../components/Button.astro';
import { games } from '../data/games';
// Statistiken berechnen
const totalGames = games.length;
const totalLines = games.reduce((sum, game) => sum + (game.codeStats?.total || 0), 0);
const genres = [...new Set(games.flatMap((game) => game.tags))].length;
const complexityBreakdown = {
Minimal: games.filter((g) => g.complexity === 'Minimal').length,
Einfach: games.filter((g) => g.complexity === 'Einfach').length,
Mittel: games.filter((g) => g.complexity === 'Mittel').length,
Komplex: games.filter((g) => g.complexity === 'Komplex').length,
};
---
<Layout title="Über uns">
<div class="about-hero">
<div class="hero-background">
<div class="floating-element element-1"></div>
<div class="floating-element element-2"></div>
<div class="floating-element element-3"></div>
</div>
<div class="hero-content">
<h1 class="hero-title">
<span class="title-line">Mehr als nur</span>
<span class="title-highlight">Spiele</span>
</h1>
<p class="hero-subtitle">Eine Plattform für Kreativität, Lernen und Spaß</p>
</div>
</div>
<div class="about-container">
<!-- Mission Section -->
<section class="mission-section">
<div class="section-header">
<span class="section-number">01</span>
<h2>Unsere Mission</h2>
</div>
<div class="mission-grid">
<div class="mission-card">
<div class="card-icon">🎮</div>
<h3>Spielen ohne Grenzen</h3>
<p>
Keine Downloads, keine Installationen. Einfach spielen - direkt im Browser, auf jedem
Gerät.
</p>
</div>
<div class="mission-card">
<div class="card-icon">🎨</div>
<h3>Kreativität fördern</h3>
<p>
Jedes Spiel ist ein Kunstwerk aus Code. Von minimalistisch bis komplex - wir zeigen die
Vielfalt der Spieleentwicklung.
</p>
</div>
<div class="mission-card">
<div class="card-icon">📚</div>
<h3>Lernen durch Code</h3>
<p>
Unsere Spiele sind vollständig dokumentiert und dienen als Lernressource für angehende
Entwickler.
</p>
</div>
</div>
</section>
<!-- Stats Section -->
<section class="stats-section">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">{totalGames}</div>
<div class="stat-label">Spiele</div>
<div class="stat-detail">und es werden mehr</div>
</div>
<div class="stat-card">
<div class="stat-number">{totalLines.toLocaleString('de-DE')}</div>
<div class="stat-label">Zeilen Code</div>
<div class="stat-detail">handgeschrieben</div>
</div>
<div class="stat-card">
<div class="stat-number">{genres}</div>
<div class="stat-label">Genres</div>
<div class="stat-detail">für jeden Geschmack</div>
</div>
<div class="stat-card">
<div class="stat-number">100%</div>
<div class="stat-label">Open Source</div>
<div class="stat-detail">lerne vom Code</div>
</div>
</div>
</section>
<!-- Games Showcase -->
<section class="showcase-section">
<div class="section-header">
<span class="section-number">02</span>
<h2>Unser Spielekatalog</h2>
</div>
<div class="showcase-content">
<div class="complexity-chart">
<h3>Komplexität unserer Spiele</h3>
<div class="chart-bars">
{
Object.entries(complexityBreakdown).map(([level, count]) => (
<div class="chart-bar">
<div class="bar-fill" style={`height: ${(count / totalGames) * 100}%`}>
<span class="bar-count">{count}</span>
</div>
<span class="bar-label">{level}</span>
</div>
))
}
</div>
</div>
<div class="featured-games">
<h3>Beliebte Kategorien</h3>
<div class="category-tags">
<span class="tag">🕹️ Arcade</span>
<span class="tag">🧩 Puzzle</span>
<span class="tag">🚀 Action</span>
<span class="tag">🎵 Rhythmus</span>
<span class="tag">🏃 Jump'n'Run</span>
<span class="tag">🗼 Tower Defense</span>
</div>
<p class="showcase-text">
Von klassischen Arcade-Spielen wie Snake bis zu innovativen Physik-Puzzles wie Gravity
Painter - unsere Sammlung wächst stetig. Jedes Spiel ist mit Liebe zum Detail entwickelt
und optimiert für flüssige Performance auf allen Geräten.
</p>
</div>
</div>
</section>
<!-- Technology Section -->
<section class="tech-section">
<div class="section-header">
<span class="section-number">03</span>
<h2>Moderne Technologie</h2>
</div>
<div class="tech-grid">
<div class="tech-card">
<div class="tech-icon">
<span>HTML5</span>
</div>
<h4>Canvas API</h4>
<p>Flüssige 60 FPS Grafiken direkt im Browser</p>
</div>
<div class="tech-card">
<div class="tech-icon">
<span>JS</span>
</div>
<h4>Vanilla JavaScript</h4>
<p>Keine Dependencies, pure Performance</p>
</div>
<div class="tech-card">
<div class="tech-icon">
<span>PWA</span>
</div>
<h4>Progressive Web App</h4>
<p>Installierbar, offline spielbar</p>
</div>
<div class="tech-card">
<div class="tech-icon">
<span>📱</span>
</div>
<h4>Responsive Design</h4>
<p>Perfekt auf jedem Bildschirm</p>
</div>
</div>
</section>
<!-- Philosophy Section -->
<section class="philosophy-section">
<div class="philosophy-content">
<h2>Unsere Philosophie</h2>
<blockquote>
"Spiele sollten mehr sein als nur Unterhaltung. Sie sind interaktive Kunst, technische
Meisterwerke und Lernwerkzeuge in einem."
</blockquote>
<div class="philosophy-points">
<div class="point">
<span class="point-icon">✨</span>
<div>
<strong>Qualität vor Quantität</strong>
<p>Jedes Spiel wird sorgfältig entwickelt und getestet</p>
</div>
</div>
<div class="point">
<span class="point-icon">🌍</span>
<div>
<strong>Zugänglichkeit für alle</strong>
<p>Kostenlos, werbefrei und ohne versteckte Kosten</p>
</div>
</div>
<div class="point">
<span class="point-icon">💡</span>
<div>
<strong>Innovation fördern</strong>
<p>Neue Spielkonzepte und kreative Mechaniken</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="cta-section">
<div class="cta-content">
<h2>Werde Teil der Community</h2>
<p>
Hast du Ideen für neue Spiele? Möchtest du zur Plattform beitragen? Oder einfach nur
Feedback geben? Wir freuen uns von dir zu hören!
</p>
<div class="cta-buttons">
<Button href="/" variant="primary" size="large"> Spiele entdecken </Button>
<Button href="/stats" variant="accent" size="large"> Deine Statistiken </Button>
</div>
</div>
</section>
</div>
</Layout>
<style>
/* Hero Section */
.about-hero {
position: relative;
min-height: 50vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-bottom: 4rem;
}
.hero-background {
position: absolute;
inset: 0;
opacity: 0.1;
}
.floating-element {
position: absolute;
border: 2px solid var(--color-accent);
border-radius: 50%;
animation: float 20s ease-in-out infinite;
}
.element-1 {
width: 300px;
height: 300px;
top: -100px;
left: -100px;
animation-delay: 0s;
}
.element-2 {
width: 200px;
height: 200px;
bottom: -50px;
right: -50px;
animation-delay: 5s;
}
.element-3 {
width: 150px;
height: 150px;
top: 50%;
left: 80%;
animation-delay: 10s;
}
@keyframes float {
0%,
100% {
transform: translate(0, 0) rotate(0deg);
}
33% {
transform: translate(30px, -30px) rotate(120deg);
}
66% {
transform: translate(-20px, 20px) rotate(240deg);
}
}
.hero-content {
position: relative;
z-index: 1;
text-align: center;
}
.hero-title {
font-size: clamp(3rem, 8vw, 5rem);
font-weight: 900;
line-height: 1;
margin-bottom: 1rem;
}
.title-line {
display: block;
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s ease forwards;
}
.title-highlight {
display: block;
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s ease 0.2s forwards;
}
.hero-subtitle {
font-size: 1.5rem;
color: var(--color-text-secondary);
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s ease 0.4s forwards;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
/* Container */
.about-container {
max-width: 1200px;
margin: 0 auto;
}
/* Sections */
section {
margin-bottom: 6rem;
opacity: 0;
transform: translateY(30px);
animation: fadeInUp 0.8s ease forwards;
}
.section-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 3rem;
}
.section-number {
font-size: 3rem;
font-weight: 900;
color: var(--color-accent);
opacity: 0.3;
}
h2 {
font-size: 2.5rem;
font-weight: 900;
}
/* Mission Section */
.mission-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
.mission-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
transition: all 0.3s ease;
}
.mission-card:hover {
transform: translateY(-5px);
border-color: var(--color-accent);
box-shadow: 0 10px 30px rgba(0, 255, 136, 0.1);
}
.card-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.mission-card h3 {
color: var(--color-text);
margin-bottom: 0.5rem;
}
.mission-card p {
color: var(--color-text-secondary);
line-height: 1.6;
}
/* Stats Section */
.stats-section {
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
border-radius: 2rem;
padding: 3rem;
margin: 4rem 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
}
.stat-card {
text-align: center;
}
.stat-number {
font-size: 3rem;
font-weight: 900;
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
font-size: 1.2rem;
color: var(--color-text);
margin: 0.5rem 0;
}
.stat-detail {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
/* Showcase Section */
.showcase-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 3rem;
align-items: start;
}
.complexity-chart {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
}
.complexity-chart h3 {
margin-bottom: 1.5rem;
color: var(--color-text);
}
.chart-bars {
display: flex;
justify-content: space-around;
align-items: flex-end;
height: 200px;
gap: 1rem;
}
.chart-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.bar-fill {
width: 100%;
background: linear-gradient(to top, var(--color-accent), var(--color-accent-secondary));
border-radius: 0.5rem 0.5rem 0 0;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 0.5rem;
transition: height 0.5s ease;
}
.bar-count {
color: #000;
font-weight: 700;
}
.bar-label {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.featured-games h3 {
margin-bottom: 1rem;
color: var(--color-text);
}
.category-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tag {
background: rgba(0, 255, 136, 0.1);
border: 1px solid var(--color-accent);
color: var(--color-accent);
padding: 0.5rem 1rem;
border-radius: 2rem;
font-size: 0.9rem;
}
.showcase-text {
color: var(--color-text-secondary);
line-height: 1.8;
}
/* Tech Section */
.tech-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.tech-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
}
.tech-card:hover {
transform: translateY(-5px);
border-color: var(--color-accent);
}
.tech-icon {
width: 80px;
height: 80px;
margin: 0 auto 1rem;
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-secondary));
border-radius: 1rem;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
font-size: 1.5rem;
color: #000;
}
.tech-card h4 {
color: var(--color-text);
margin-bottom: 0.5rem;
}
.tech-card p {
color: var(--color-text-secondary);
font-size: 0.9rem;
}
/* Philosophy Section */
.philosophy-section {
background: var(--color-bg-secondary);
border-radius: 2rem;
padding: 4rem;
margin: 4rem 0;
}
.philosophy-content h2 {
text-align: center;
margin-bottom: 2rem;
}
blockquote {
font-size: 1.5rem;
font-style: italic;
text-align: center;
color: var(--color-text-secondary);
margin: 2rem 0 3rem;
padding: 0 2rem;
}
.philosophy-points {
display: flex;
flex-direction: column;
gap: 2rem;
}
.point {
display: flex;
gap: 1.5rem;
align-items: start;
}
.point-icon {
font-size: 2rem;
flex-shrink: 0;
}
.point strong {
display: block;
color: var(--color-text);
margin-bottom: 0.25rem;
}
.point p {
color: var(--color-text-secondary);
}
/* CTA Section */
.cta-section {
text-align: center;
padding: 4rem;
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), transparent);
border-radius: 2rem;
}
.cta-content h2 {
margin-bottom: 1rem;
}
.cta-content p {
font-size: 1.2rem;
color: var(--color-text-secondary);
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.cta-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
/* Responsive */
@media (max-width: 768px) {
.hero-title {
font-size: 3rem;
}
.hero-subtitle {
font-size: 1.2rem;
}
section {
margin-bottom: 4rem;
}
.section-header {
flex-direction: column;
text-align: center;
}
h2 {
font-size: 2rem;
}
.showcase-content {
grid-template-columns: 1fr;
}
.philosophy-section {
padding: 2rem;
}
blockquote {
font-size: 1.2rem;
}
.cta-section {
padding: 2rem;
}
}
</style>

View file

@ -1,472 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Button from '../components/Button.astro';
---
<Layout title="Nutzungsbedingungen">
<div class="agb-container">
<header class="agb-header">
<h1>Allgemeine Geschäftsbedingungen</h1>
<p class="subtitle">Nutzungsbedingungen für Mana Games</p>
<p class="last-updated">Stand: Januar 2024</p>
</header>
<nav class="toc">
<h2>Inhaltsverzeichnis</h2>
<ol>
<li><a href="#geltungsbereich">Geltungsbereich</a></li>
<li><a href="#nutzung">Nutzung der Plattform</a></li>
<li><a href="#registrierung">Registrierung und Nutzerkonto</a></li>
<li><a href="#inhalte">Nutzergenierte Inhalte</a></li>
<li><a href="#verhaltensregeln">Verhaltensregeln</a></li>
<li><a href="#geistigeseigentum">Geistiges Eigentum</a></li>
<li><a href="#haftung">Haftungsausschluss</a></li>
<li><a href="#datenschutz">Datenschutz</a></li>
<li><a href="#aenderungen">Änderungen der AGB</a></li>
<li><a href="#schlussbestimmungen">Schlussbestimmungen</a></li>
</ol>
</nav>
<section id="geltungsbereich" class="content-section">
<h2>§ 1 Geltungsbereich</h2>
<p>
(1) Diese Allgemeinen Geschäftsbedingungen (nachfolgend "AGB") gelten für die Nutzung der
Website mana-games.netlify.app (nachfolgend "Plattform") und alle darauf angebotenen Dienste
und Spiele.
</p>
<p>
(2) Mit der Nutzung der Plattform akzeptieren Sie diese AGB. Wenn Sie mit diesen Bedingungen
nicht einverstanden sind, nutzen Sie bitte unsere Dienste nicht.
</p>
<p>
(3) Die Plattform richtet sich an Nutzer aller Altersgruppen. Für minderjährige Nutzer
gelten zusätzlich unsere <a href="/jugendschutz">Jugendschutzbestimmungen</a>.
</p>
</section>
<section id="nutzung" class="content-section">
<h2>§ 2 Nutzung der Plattform</h2>
<p>
(1) Die Nutzung der Plattform und der darauf angebotenen Spiele ist grundsätzlich kostenlos
und ohne Registrierung möglich.
</p>
<p>
(2) Wir gewähren Ihnen ein nicht-exklusives, nicht übertragbares, widerrufliches Recht zur
persönlichen Nutzung der Plattform und der Spiele.
</p>
<div class="highlight-box">
<h4>Erlaubte Nutzung umfasst:</h4>
<ul>
<li>Spielen aller verfügbaren Spiele</li>
<li>Erstellen eigener Spiele mit dem KI-Generator</li>
<li>Speichern von Spielständen im lokalen Browser-Speicher</li>
<li>Teilen von Links zu Spielen</li>
</ul>
</div>
<div class="warning-box">
<h4>Nicht erlaubt ist:</h4>
<ul>
<li>Kommerzielle Nutzung ohne ausdrückliche Genehmigung</li>
<li>Automatisierte Zugriffe (Bots, Scraping)</li>
<li>Umgehung von Sicherheitsmaßnahmen</li>
<li>Verbreitung von Schadsoftware</li>
</ul>
</div>
</section>
<section id="registrierung" class="content-section">
<h2>§ 3 Registrierung und Nutzerkonto</h2>
<p>
(1) Aktuell ist keine Registrierung für die Nutzung der Plattform erforderlich. Alle Daten
werden lokal in Ihrem Browser gespeichert.
</p>
<p>
(2) Sollten wir in Zukunft Nutzerkonten einführen, werden wir Sie rechtzeitig informieren
und separate Bedingungen dafür bereitstellen.
</p>
</section>
<section id="inhalte" class="content-section">
<h2>§ 4 Nutzergenierte Inhalte</h2>
<p>
(1) Mit unserem KI-Generator können Sie eigene Spiele erstellen. Diese werden ausschließlich
lokal in Ihrem Browser gespeichert.
</p>
<p>
(2) Sie sind für die von Ihnen erstellten Inhalte selbst verantwortlich und stellen sicher,
dass diese:
</p>
<ul class="content-list">
<li>Keine Rechte Dritter verletzen</li>
<li>Keine illegalen Inhalte enthalten</li>
<li>Nicht diskriminierend, beleidigend oder anstößig sind</li>
<li>Keine Gewaltverherrlichung oder extremistische Inhalte beinhalten</li>
<li>Jugendschutzbestimmungen einhalten</li>
</ul>
<p>
(3) Wir behalten uns vor, bei Kenntnis von rechtswidrigen Inhalten entsprechende Maßnahmen
zu ergreifen.
</p>
</section>
<section id="verhaltensregeln" class="content-section">
<h2>§ 5 Verhaltensregeln</h2>
<div class="rules-grid">
<div class="rule-card positive">
<h4>✅ Erwünschtes Verhalten</h4>
<ul>
<li>Respektvoller Umgang mit anderen Nutzern</li>
<li>Konstruktives Feedback</li>
<li>Melden von Bugs und Problemen</li>
<li>Teilen von kreativen Ideen</li>
</ul>
</div>
<div class="rule-card negative">
<h4>❌ Unerwünschtes Verhalten</h4>
<ul>
<li>Spam oder Werbung</li>
<li>Hacking-Versuche</li>
<li>Verbreitung falscher Informationen</li>
<li>Belästigung anderer Nutzer</li>
</ul>
</div>
</div>
</section>
<section id="geistigeseigentum" class="content-section">
<h2>§ 6 Geistiges Eigentum</h2>
<p>
(1) Alle Rechte an der Plattform, dem Design, den offiziellen Spielen und dem Quellcode
liegen bei uns bzw. unseren Lizenzgebern.
</p>
<p>
(2) Die Plattform ist Open Source. Details zur Lizenzierung finden Sie auf unserer
<a href="/copyright">Copyright-Seite</a>.
</p>
<p>
(3) An den von Ihnen mit dem KI-Generator erstellten Spielen räumen Sie uns ein einfaches,
nicht-exklusives Nutzungsrecht ein, sofern Sie diese öffentlich teilen.
</p>
</section>
<section id="haftung" class="content-section">
<h2>§ 7 Haftungsausschluss</h2>
<div class="info-box">
<p>
(1) Die Nutzung der Plattform erfolgt auf eigene Gefahr. Wir übernehmen keine Gewähr für
die ständige Verfügbarkeit, Fehlerfreiheit oder Vollständigkeit der angebotenen Dienste.
</p>
</div>
<p>
(2) Wir haften nur für Schäden, die durch vorsätzliches oder grob fahrlässiges Verhalten
unsererseits entstehen. Die Haftung für leichte Fahrlässigkeit ist ausgeschlossen, soweit
keine wesentlichen Vertragspflichten verletzt werden.
</p>
<p>
(3) Für Datenverluste, insbesondere von lokal gespeicherten Spielständen oder selbst
erstellten Spielen, übernehmen wir keine Haftung. Wir empfehlen regelmäßige Backups
wichtiger Daten.
</p>
<p>(4) Die Haftung für mittelbare und Folgeschäden ist ausgeschlossen.</p>
</section>
<section id="datenschutz" class="content-section">
<h2>§ 8 Datenschutz</h2>
<p>
Der Schutz Ihrer Daten ist uns wichtig. Einzelheiten zur Datenverarbeitung finden Sie in
unserer <a href="/datenschutz">Datenschutzerklärung</a>.
</p>
</section>
<section id="aenderungen" class="content-section">
<h2>§ 9 Änderungen der AGB</h2>
<p>
(1) Wir behalten uns vor, diese AGB jederzeit zu ändern. Änderungen werden auf der Plattform
bekannt gegeben.
</p>
<p>
(2) Die weitere Nutzung der Plattform nach Bekanntgabe von Änderungen gilt als Zustimmung zu
den geänderten Bedingungen.
</p>
</section>
<section id="schlussbestimmungen" class="content-section">
<h2>§ 10 Schlussbestimmungen</h2>
<p>
(1) Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.
</p>
<p>
(2) Sollten einzelne Bestimmungen dieser AGB unwirksam sein oder werden, berührt dies die
Wirksamkeit der übrigen Bestimmungen nicht.
</p>
<p>
(3) Gerichtsstand für alle Streitigkeiten aus diesem Vertragsverhältnis ist, soweit
gesetzlich zulässig, [Ihr Ort].
</p>
</section>
<div class="footer-actions">
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
<Button href="/datenschutz" variant="ghost">Datenschutz</Button>
<Button href="/impressum" variant="ghost">Impressum</Button>
</div>
</div>
</Layout>
<style>
.agb-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem 0;
}
.agb-header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--color-border);
}
.agb-header h1 {
font-size: 3rem;
font-weight: 900;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--color-text), var(--color-text-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-size: 1.2rem;
color: var(--color-text-secondary);
margin-bottom: 0.5rem;
}
.last-updated {
color: var(--color-text-secondary);
font-size: 0.9rem;
opacity: 0.8;
}
.toc {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
margin-bottom: 3rem;
}
.toc h2 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--color-text);
}
.toc ol {
padding-left: 1.5rem;
}
.toc li {
color: var(--color-text-secondary);
margin-bottom: 0.5rem;
}
.toc a {
color: var(--color-text-secondary);
text-decoration: none;
transition: color 0.2s ease;
}
.toc a:hover {
color: var(--color-accent);
}
.content-section {
margin-bottom: 3rem;
scroll-margin-top: 2rem;
}
.content-section h2 {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--color-text);
}
.content-section h4 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--color-text);
}
.content-section p {
color: var(--color-text-secondary);
line-height: 1.8;
margin-bottom: 1rem;
}
.highlight-box,
.warning-box,
.info-box {
border-radius: 0.5rem;
padding: 1.5rem;
margin: 1.5rem 0;
}
.highlight-box {
background: rgba(0, 255, 136, 0.1);
border: 1px solid rgba(0, 255, 136, 0.3);
}
.warning-box {
background: rgba(255, 59, 48, 0.1);
border: 1px solid rgba(255, 59, 48, 0.3);
}
.info-box {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
}
.highlight-box ul,
.warning-box ul,
.content-list {
list-style: none;
padding: 0;
margin: 0.5rem 0;
}
.highlight-box li,
.warning-box li,
.content-list li {
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
color: var(--color-text-secondary);
}
.highlight-box li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--color-accent);
}
.warning-box li::before {
content: '✗';
position: absolute;
left: 0;
color: #ff3b30;
}
.content-list li::before {
content: '•';
position: absolute;
left: 0;
color: var(--color-accent);
}
.rules-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin: 1.5rem 0;
}
.rule-card {
border-radius: 0.5rem;
padding: 1.5rem;
}
.rule-card.positive {
background: rgba(0, 255, 136, 0.05);
border: 1px solid rgba(0, 255, 136, 0.2);
}
.rule-card.negative {
background: rgba(255, 59, 48, 0.05);
border: 1px solid rgba(255, 59, 48, 0.2);
}
.rule-card h4 {
margin-bottom: 1rem;
}
.rule-card ul {
list-style: none;
padding: 0;
}
.rule-card li {
padding: 0.5rem 0;
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.footer-actions {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.agb-header h1 {
font-size: 2rem;
}
.content-section h2 {
font-size: 1.5rem;
}
.toc {
padding: 1.5rem;
}
.rules-grid {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -1,356 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import GameCard from '../components/GameCard.astro';
import { games } from '../data/games';
// Filter community games
const communityGames = games.filter((game) => 'community' in game && game.community === true);
// Try to load additional community games from JSON file
let additionalCommunityGames = [];
// TODO: When community-games.json exists, load it here
// For now, we'll just use an empty array until the first game is submitted
// Combine all community games
const allCommunityGames = [...communityGames, ...additionalCommunityGames];
---
<Layout title="Community Spiele - MANA Games" description="Von der Community erstellte Spiele">
<main>
<div class="page-header">
<h1>Community Spiele</h1>
<p>Entdecke Spiele, die von unserer talentierten Community erstellt wurden!</p>
</div>
<div class="community-info">
<div class="info-card">
<h3>🎮 Werde Teil der Community!</h3>
<p>Hast du ein eigenes Spiel erstellt? Reiche es ein und teile es mit anderen Spielern!</p>
<a href="/submit" class="submit-button">
<span class="icon">📤</span>
Spiel einreichen
</a>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">{allCommunityGames.length}</div>
<div class="stat-label">Community Spiele</div>
</div>
<div class="stat-card">
<div class="stat-number">🏆</div>
<div class="stat-label">Top bewertete Spiele</div>
</div>
<div class="stat-card">
<div class="stat-number">👥</div>
<div class="stat-label">Aktive Entwickler</div>
</div>
</div>
</div>
{
allCommunityGames.length > 0 ? (
<div class="games-section">
<h2>Eingereichte Spiele</h2>
<div class="games-grid">
{allCommunityGames.map((game) => (
<div class="community-game-card">
<GameCard game={game} />
{game.author && (
<div class="author-info">
<span class="author-label">Von:</span>
<span class="author-name">{game.author}</span>
</div>
)}
{game.submittedAt && (
<div class="submission-date">
Eingereicht am {new Date(game.submittedAt).toLocaleDateString('de-DE')}
</div>
)}
</div>
))}
</div>
</div>
) : (
<div class="empty-state">
<div class="empty-icon">🎯</div>
<h2>Noch keine Community-Spiele</h2>
<p>Sei der Erste, der ein Spiel einreicht!</p>
<a href="/submit" class="submit-button">
<span class="icon">📤</span>
Erstes Spiel einreichen
</a>
</div>
)
}
<div class="pending-section">
<h2>🔄 In Prüfung</h2>
<p>Diese Spiele werden gerade von unserem Team geprüft:</p>
<div class="pending-list" id="pendingList">
<div class="loading">Lade ausstehende Einreichungen...</div>
</div>
</div>
</main>
</Layout>
<style>
.community-info {
display: grid;
gap: 2rem;
margin-bottom: 3rem;
}
.info-card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 2rem;
text-align: center;
}
.info-card h3 {
color: #00ff88;
margin-bottom: 1rem;
font-size: 1.3rem;
}
.info-card p {
margin-bottom: 1.5rem;
color: #cccccc;
}
.submit-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: #00ff88;
color: #0a0a0a;
padding: 0.75rem 1.5rem;
border-radius: 4px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
}
.submit-button:hover {
background: #00cc6a;
transform: translateY(-2px);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.stat-card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 1.5rem;
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #00ff88;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.9rem;
color: #888;
}
.games-section {
margin-bottom: 3rem;
}
.games-section h2 {
color: #00ff88;
margin-bottom: 1.5rem;
}
.games-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.community-game-card {
position: relative;
}
.author-info {
margin-top: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(0, 255, 136, 0.1);
border: 1px solid rgba(0, 255, 136, 0.3);
border-radius: 4px;
font-size: 0.9rem;
}
.author-label {
color: #888;
margin-right: 0.5rem;
}
.author-name {
color: #00ff88;
font-weight: 500;
}
.submission-date {
font-size: 0.8rem;
color: #666;
margin-top: 0.5rem;
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
margin: 2rem 0;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-state h2 {
color: #ffffff;
margin-bottom: 1rem;
}
.empty-state p {
color: #888;
margin-bottom: 2rem;
}
.pending-section {
margin-top: 3rem;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 2rem;
}
.pending-section h2 {
color: #00ff88;
margin-bottom: 1rem;
}
.pending-section p {
color: #888;
margin-bottom: 1.5rem;
}
.pending-list {
display: grid;
gap: 1rem;
}
.pending-item {
background: #0a0a0a;
border: 1px solid #2a2a2a;
border-radius: 4px;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.pending-info h4 {
color: #ffffff;
margin-bottom: 0.25rem;
}
.pending-info p {
font-size: 0.9rem;
color: #888;
margin: 0;
}
.pr-link {
color: #00ff88;
text-decoration: none;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
.pr-link:hover {
text-decoration: underline;
}
.loading {
text-align: center;
color: #888;
padding: 2rem;
}
@media (max-width: 768px) {
.games-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>
<script>
// Fetch pending PRs from GitHub
async function fetchPendingGames() {
const pendingList = document.getElementById('pendingList');
try {
// For now, we'll show a placeholder since we need GitHub API access
// In production, this would fetch actual PRs
pendingList.innerHTML = `
<div class="pending-item">
<div class="pending-info">
<p>Ausstehende Einreichungen werden hier angezeigt, sobald das GitHub-Repository konfiguriert ist.</p>
</div>
</div>
`;
// Example of what it would look like with real data:
/*
const response = await fetch('/.netlify/functions/get-pending-games');
const pendingGames = await response.json();
if (pendingGames.length === 0) {
pendingList.innerHTML = '<p style="color: #888; text-align: center;">Keine ausstehenden Einreichungen</p>';
} else {
pendingList.innerHTML = pendingGames.map(game => `
<div class="pending-item">
<div class="pending-info">
<h4>${game.title}</h4>
<p>Von ${game.author} • ${new Date(game.submittedAt).toLocaleDateString('de-DE')}</p>
</div>
<a href="${game.prUrl}" target="_blank" class="pr-link">
PR #${game.prNumber} →
</a>
</div>
`).join('');
}
*/
} catch (error) {
console.error('Error fetching pending games:', error);
pendingList.innerHTML =
'<p style="color: #ff4444;">Fehler beim Laden der ausstehenden Spiele</p>';
}
}
// Load pending games on page load
fetchPendingGames();
</script>

View file

@ -1,719 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Button from '../components/Button.astro';
---
<Layout title="Copyright & Lizenzen">
<div class="copyright-container">
<header class="copyright-header">
<div class="header-icon">©️</div>
<h1>Copyright & Lizenzen</h1>
<p class="subtitle">Open Source mit Herz</p>
</header>
<section class="intro-section">
<div class="open-source-banner">
<div class="banner-icon">🌟</div>
<div class="banner-content">
<h2>100% Open Source</h2>
<p>
Mana Games ist ein Open-Source-Projekt. Der gesamte Quellcode ist öffentlich verfügbar
und kann frei verwendet, modifiziert und weitergegeben werden.
</p>
<a
href="https://github.com/yourusername/mana-games"
target="_blank"
rel="noopener noreferrer"
class="github-button"
>
<span class="icon">📦</span>
Zum GitHub Repository
</a>
</div>
</div>
</section>
<section class="content-section">
<h2>Projekt-Lizenz</h2>
<div class="license-card main-license">
<div class="license-header">
<h3>MIT License</h3>
<span class="license-badge">Hauptlizenz</span>
</div>
<div class="license-content">
<p>Copyright (c) 2024 [Ihr Name]</p>
<p>
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following
conditions:
</p>
<p>
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
</p>
<p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
</p>
</div>
<div class="license-explanation">
<h4>Was bedeutet das für Sie?</h4>
<ul>
<li>✅ Sie können den Code frei verwenden</li>
<li>✅ Sie können den Code modifizieren</li>
<li>✅ Sie können den Code in kommerziellen Projekten nutzen</li>
<li>✅ Sie können den Code weitergeben</li>
<li>⚠️ Keine Garantie oder Haftung</li>
<li>📋 Copyright-Hinweis muss erhalten bleiben</li>
</ul>
</div>
</div>
</section>
<section class="content-section">
<h2>Spiele-Lizenzen</h2>
<div class="games-licenses">
<div class="license-info">
<h3>Offizielle Spiele</h3>
<p>
Alle offiziellen Spiele auf unserer Plattform unterliegen ebenfalls der MIT-Lizenz. Sie
können den Code jedes Spiels frei verwenden, studieren und modifizieren.
</p>
</div>
<div class="license-info">
<h3>Nutzer-generierte Spiele</h3>
<p>
Spiele, die mit unserem KI-Generator erstellt werden, gehören dem jeweiligen Ersteller.
Die Ersteller können selbst entscheiden, unter welcher Lizenz sie ihre Spiele
veröffentlichen möchten.
</p>
</div>
</div>
</section>
<section class="content-section">
<h2>Verwendete Technologien & Bibliotheken</h2>
<div class="tech-credits">
<div class="credit-category">
<h3>Framework & Build-Tools</h3>
<div class="credit-list">
<div class="credit-item">
<div class="credit-name">
<strong>Astro</strong>
<span class="version">v4.x</span>
</div>
<div class="credit-info">
<span class="license">MIT License</span>
<a href="https://astro.build" target="_blank" rel="noopener noreferrer"
>astro.build</a
>
</div>
</div>
<div class="credit-item">
<div class="credit-name">
<strong>TypeScript</strong>
<span class="version">v5.x</span>
</div>
<div class="credit-info">
<span class="license">Apache-2.0 License</span>
<a href="https://www.typescriptlang.org" target="_blank" rel="noopener noreferrer"
>typescriptlang.org</a
>
</div>
</div>
</div>
</div>
<div class="credit-category">
<h3>Deployment & Hosting</h3>
<div class="credit-list">
<div class="credit-item">
<div class="credit-name">
<strong>Netlify</strong>
</div>
<div class="credit-info">
<span class="license">Hosting Service</span>
<a href="https://www.netlify.com" target="_blank" rel="noopener noreferrer"
>netlify.com</a
>
</div>
</div>
</div>
</div>
<div class="credit-category">
<h3>KI-Integration</h3>
<div class="credit-list">
<div class="credit-item">
<div class="credit-name">
<strong>OpenRouter API</strong>
</div>
<div class="credit-info">
<span class="license">API Service</span>
<a href="https://openrouter.ai" target="_blank" rel="noopener noreferrer"
>openrouter.ai</a
>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="content-section">
<h2>Assets & Medien</h2>
<div class="assets-info">
<div class="asset-category">
<h3>Icons & Emojis</h3>
<p>
Wir verwenden System-Emojis, die je nach Betriebssystem unterschiedlich dargestellt
werden können. Diese sind gemeinfrei oder unterliegen den jeweiligen Systemlizenzen.
</p>
</div>
<div class="asset-category">
<h3>Schriftarten</h3>
<p>
Wir nutzen System-Schriftarten für optimale Performance und Lesbarkeit. Keine externen
Schriftarten werden geladen.
</p>
</div>
<div class="asset-category">
<h3>Grafiken</h3>
<p>
Alle Spielgrafiken werden programmatisch mit Canvas API erstellt. Es werden keine
externen Bilddateien in den Spielen verwendet.
</p>
</div>
</div>
</section>
<section class="content-section">
<h2>Beitragen zum Projekt</h2>
<div class="contribute-section">
<h3>Wie Sie beitragen können</h3>
<div class="contribute-steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h4>Fork erstellen</h4>
<p>Erstellen Sie einen Fork des Repositories auf GitHub</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h4>Änderungen vornehmen</h4>
<p>Entwickeln Sie Ihre Features oder Bugfixes</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h4>Pull Request</h4>
<p>Reichen Sie einen Pull Request mit Ihren Änderungen ein</p>
</div>
</div>
</div>
<div class="contribute-note">
<p>
<strong>Wichtig:</strong> Mit dem Einreichen eines Pull Requests stimmen Sie zu, dass Ihre
Beiträge unter der MIT-Lizenz veröffentlicht werden.
</p>
</div>
</div>
</section>
<section class="content-section">
<h2>Namensnennung & Credits</h2>
<div class="credits-section">
<h3>Hauptentwickler</h3>
<div class="developer-card">
<p>[Ihr Name]</p>
<p class="role">Projektinitiator & Hauptentwickler</p>
</div>
<h3>Contributors</h3>
<p>
Eine vollständige Liste aller Contributors finden Sie auf unserer
<a
href="https://github.com/yourusername/mana-games/graphs/contributors"
target="_blank"
rel="noopener noreferrer"
>
GitHub Contributors-Seite
</a>
</p>
<h3>Besonderer Dank</h3>
<ul class="thanks-list">
<li>An die Open-Source-Community für ihre großartigen Tools</li>
<li>An alle Spieler, die uns Feedback geben</li>
<li>An alle Contributors, die das Projekt verbessern</li>
</ul>
</div>
</section>
<section class="content-section">
<h2>Rechtliche Hinweise</h2>
<div class="legal-notes">
<div class="note-card">
<h3>Markenrechte</h3>
<p>
"Mana Games" ist eine eingetragene Marke. Die Verwendung des Namens bedarf unserer
ausdrücklichen Genehmigung.
</p>
</div>
<div class="note-card">
<h3>Haftungsausschluss</h3>
<p>
Die Software wird "wie besehen" ohne jegliche Garantie bereitgestellt. Details finden
Sie in der MIT-Lizenz.
</p>
</div>
<div class="note-card">
<h3>Externe Links</h3>
<p>Wir übernehmen keine Verantwortung für die Inhalte verlinkter externer Websites.</p>
</div>
</div>
</section>
<div class="footer-actions">
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
<Button href="/impressum" variant="ghost">Impressum</Button>
<Button href="/datenschutz" variant="ghost">Datenschutz</Button>
</div>
</div>
</Layout>
<style>
.copyright-container {
max-width: 900px;
margin: 0 auto;
padding: 2rem 0;
}
.copyright-header {
text-align: center;
margin-bottom: 3rem;
}
.header-icon {
font-size: 4rem;
margin-bottom: 1rem;
display: inline-block;
}
.copyright-header h1 {
font-size: 3rem;
font-weight: 900;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--color-text), var(--color-accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-size: 1.2rem;
color: var(--color-text-secondary);
}
.intro-section {
margin-bottom: 3rem;
}
.open-source-banner {
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), rgba(0, 255, 136, 0.05));
border: 2px solid var(--color-accent);
border-radius: 1rem;
padding: 2rem;
display: flex;
gap: 2rem;
align-items: center;
}
.banner-icon {
font-size: 4rem;
flex-shrink: 0;
}
.banner-content h2 {
margin-bottom: 0.5rem;
color: var(--color-text);
}
.banner-content p {
color: var(--color-text-secondary);
margin-bottom: 1.5rem;
}
.github-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--color-accent);
color: #000;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
text-decoration: none;
font-weight: 700;
transition: all 0.3s ease;
}
.github-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 255, 136, 0.3);
}
.content-section {
margin-bottom: 4rem;
}
.content-section h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 2rem;
color: var(--color-text);
}
.content-section h3 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--color-text);
}
.content-section h4 {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--color-text);
}
.license-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
}
.main-license {
border: 2px solid var(--color-accent);
}
.license-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.license-badge {
background: var(--color-accent);
color: #000;
padding: 0.25rem 1rem;
border-radius: 2rem;
font-size: 0.85rem;
font-weight: 600;
}
.license-content {
background: var(--color-bg);
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
font-family: monospace;
font-size: 0.9rem;
color: var(--color-text-secondary);
overflow-x: auto;
}
.license-content p {
margin-bottom: 1rem;
}
.license-explanation {
background: rgba(0, 255, 136, 0.05);
border: 1px solid rgba(0, 255, 136, 0.2);
border-radius: 0.5rem;
padding: 1.5rem;
}
.license-explanation ul {
list-style: none;
padding: 0;
}
.license-explanation li {
padding: 0.5rem 0;
color: var(--color-text-secondary);
}
.games-licenses {
display: grid;
gap: 1.5rem;
}
.license-info {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
}
.tech-credits {
display: grid;
gap: 2rem;
}
.credit-category {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 1.5rem;
}
.credit-list {
display: grid;
gap: 1rem;
margin-top: 1rem;
}
.credit-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: var(--color-bg);
border-radius: 0.5rem;
}
.credit-name {
display: flex;
align-items: center;
gap: 0.5rem;
}
.version {
font-size: 0.85rem;
color: var(--color-text-secondary);
}
.credit-info {
display: flex;
align-items: center;
gap: 1rem;
}
.license {
background: rgba(0, 255, 136, 0.1);
color: var(--color-accent);
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.8rem;
}
.credit-info a {
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.9rem;
}
.credit-info a:hover {
color: var(--color-accent);
}
.assets-info {
display: grid;
gap: 1.5rem;
}
.asset-category {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
}
.asset-category p {
color: var(--color-text-secondary);
}
.contribute-section {
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
border-radius: 1rem;
padding: 2rem;
}
.contribute-steps {
display: grid;
gap: 1.5rem;
margin: 1.5rem 0;
}
.step {
display: flex;
gap: 1.5rem;
align-items: start;
}
.step-number {
width: 40px;
height: 40px;
background: var(--color-accent);
color: #000;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
flex-shrink: 0;
}
.step-content h4 {
margin-bottom: 0.25rem;
}
.step-content p {
color: var(--color-text-secondary);
margin: 0;
}
.contribute-note {
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1.5rem;
}
.credits-section {
background: var(--color-bg-secondary);
border-radius: 1rem;
padding: 2rem;
}
.developer-card {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
}
.developer-card p {
margin: 0;
font-weight: 600;
}
.developer-card .role {
font-weight: 400;
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.thanks-list {
list-style: none;
padding: 0;
margin-top: 1rem;
}
.thanks-list li {
padding: 0.5rem 0;
color: var(--color-text-secondary);
position: relative;
padding-left: 1.5rem;
}
.thanks-list li::before {
content: '❤️';
position: absolute;
left: 0;
}
.legal-notes {
display: grid;
gap: 1.5rem;
}
.note-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
}
.note-card p {
color: var(--color-text-secondary);
margin: 0;
}
.footer-actions {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.copyright-header h1 {
font-size: 2rem;
}
.open-source-banner {
flex-direction: column;
text-align: center;
}
.content-section h2 {
font-size: 1.5rem;
}
.credit-item {
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -1,445 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Button from '../components/Button.astro';
---
<Layout title="Datenschutz">
<div class="datenschutz-container">
<header class="datenschutz-header">
<h1>Datenschutzerklärung</h1>
<p class="last-updated">Stand: Januar 2024</p>
</header>
<section class="content-section">
<h2>1. Datenschutz auf einen Blick</h2>
<div class="info-box">
<h3>Allgemeine Hinweise</h3>
<p>
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren
personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten
sind alle Daten, mit denen Sie persönlich identifiziert werden können.
</p>
</div>
<div class="highlight-box">
<div class="highlight-icon">🛡️</div>
<div class="highlight-content">
<h4>Ihre Daten sind bei uns sicher</h4>
<p>
Wir erheben nur minimal notwendige Daten. Keine Tracker, keine Werbung, keine
versteckten Datensammlungen. Ihre Privatsphäre ist uns wichtig.
</p>
</div>
</div>
</section>
<section class="content-section">
<h2>2. Datenerfassung auf dieser Website</h2>
<h3>Wer ist verantwortlich für die Datenerfassung?</h3>
<p>
Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Die
Kontaktdaten können Sie dem Abschnitt „Hinweis zur verantwortlichen Stelle" in dieser
Datenschutzerklärung entnehmen.
</p>
<h3>Wie erfassen wir Ihre Daten?</h3>
<p>
Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es
sich z.B. um Daten handeln, die Sie in ein Kontaktformular eingeben.
</p>
<p>
Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website durch
unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z.B. Internetbrowser,
Betriebssystem oder Uhrzeit des Seitenaufrufs).
</p>
<h3>Wofür nutzen wir Ihre Daten?</h3>
<p>
Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu
gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.
</p>
</section>
<section class="content-section">
<h2>3. Hosting und Content Delivery Networks (CDN)</h2>
<div class="service-box">
<h3>Netlify</h3>
<p>
Wir hosten unsere Website bei Netlify. Anbieter ist die Netlify, Inc., 2325 3rd Street,
Suite 296, San Francisco, CA 94107, USA.
</p>
<p>
Beim Besuch unserer Website erfasst Netlify verschiedene Logfiles inklusive Ihrer
IP-Adressen. Details entnehmen Sie der Datenschutzerklärung von Netlify:
<a href="https://www.netlify.com/privacy/" target="_blank" rel="noopener noreferrer">
https://www.netlify.com/privacy/
</a>
</p>
<p>
Die Verwendung von Netlify erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Wir haben
ein berechtigtes Interesse an einer möglichst zuverlässigen Darstellung unserer Website.
</p>
</div>
</section>
<section class="content-section">
<h2>4. Allgemeine Hinweise und Pflichtinformationen</h2>
<h3>Datenschutz</h3>
<p>
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir
behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen
Datenschutzvorschriften sowie dieser Datenschutzerklärung.
</p>
<h3>Hinweis zur verantwortlichen Stelle</h3>
<p>Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:</p>
<div class="contact-box">
<p>
[Ihr Name/Firma]<br />
[Ihre Adresse]<br />
[PLZ und Ort]
</p>
<p>E-Mail: [Ihre E-Mail-Adresse]</p>
</div>
<h3>Speicherdauer</h3>
<p>
Soweit innerhalb dieser Datenschutzerklärung keine speziellere Speicherdauer genannt wurde,
verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck für die Datenverarbeitung
entfällt.
</p>
</section>
<section class="content-section">
<h2>5. Datenerfassung auf dieser Website</h2>
<h3>Server-Log-Dateien</h3>
<p>
Der Provider der Seiten erhebt und speichert automatisch Informationen in so genannten
Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:
</p>
<ul class="data-list">
<li>Browsertyp und Browserversion</li>
<li>Verwendetes Betriebssystem</li>
<li>Referrer URL</li>
<li>Hostname des zugreifenden Rechners</li>
<li>Uhrzeit der Serveranfrage</li>
<li>IP-Adresse</li>
</ul>
<p>Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen.</p>
<h3>Progressive Web App (PWA)</h3>
<p>
Diese Website kann als Progressive Web App (PWA) installiert werden. Bei der Installation
werden folgende Daten lokal auf Ihrem Gerät gespeichert:
</p>
<ul class="data-list">
<li>App-Manifest und Icons</li>
<li>Service Worker für Offline-Funktionalität</li>
<li>Spielstände und Einstellungen (im localStorage)</li>
</ul>
<p>
Diese Daten verbleiben ausschließlich auf Ihrem Gerät und werden nicht an uns oder Dritte
übertragen.
</p>
</section>
<section class="content-section">
<h2>6. Analyse-Tools und Werbung</h2>
<div class="highlight-box positive">
<div class="highlight-icon">✅</div>
<div class="highlight-content">
<h4>Keine Analyse-Tools</h4>
<p>
Wir verwenden keine Analyse-Tools wie Google Analytics oder ähnliche Dienste. Ihre
Nutzung unserer Website wird nicht getrackt oder analysiert.
</p>
</div>
</div>
<div class="highlight-box positive">
<div class="highlight-icon">🚫</div>
<div class="highlight-content">
<h4>Keine Werbung</h4>
<p>
Unsere Website ist komplett werbefrei. Wir verwenden keine Werbenetzwerke oder
Display-Werbung jeglicher Art.
</p>
</div>
</div>
</section>
<section class="content-section">
<h2>7. Plugins und Tools</h2>
<h3>Keine externen Plugins</h3>
<p>
Wir verwenden keine Social Media Plugins, keine eingebetteten Videos von Drittanbietern und
keine externen Schriftarten. Alle Ressourcen werden direkt von unserem Server ausgeliefert.
</p>
</section>
<section class="content-section">
<h2>8. Ihre Rechte</h2>
<h3>Sie haben folgende Rechte:</h3>
<div class="rights-grid">
<div class="right-card">
<h4>Auskunftsrecht</h4>
<p>Sie können Auskunft über Ihre gespeicherten personenbezogenen Daten verlangen.</p>
</div>
<div class="right-card">
<h4>Berichtigung</h4>
<p>Sie können die Berichtigung unrichtiger Daten verlangen.</p>
</div>
<div class="right-card">
<h4>Löschung</h4>
<p>Sie können die Löschung Ihrer personenbezogenen Daten verlangen.</p>
</div>
<div class="right-card">
<h4>Einschränkung</h4>
<p>Sie können die Einschränkung der Verarbeitung verlangen.</p>
</div>
<div class="right-card">
<h4>Widerspruch</h4>
<p>Sie können der Verarbeitung Ihrer Daten widersprechen.</p>
</div>
<div class="right-card">
<h4>Datenübertragbarkeit</h4>
<p>Sie haben das Recht auf Datenübertragbarkeit.</p>
</div>
</div>
</section>
<section class="content-section">
<h2>9. Änderungen</h2>
<p>
Wir behalten uns vor, diese Datenschutzerklärung anzupassen, damit sie stets den aktuellen
rechtlichen Anforderungen entspricht oder um Änderungen unserer Leistungen in der
Datenschutzerklärung umzusetzen, z.B. bei der Einführung neuer Services. Für Ihren erneuten
Besuch gilt dann die neue Datenschutzerklärung.
</p>
</section>
<div class="footer-actions">
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
</div>
</div>
</Layout>
<style>
.datenschutz-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem 0;
}
.datenschutz-header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--color-border);
}
.datenschutz-header h1 {
font-size: 3rem;
font-weight: 900;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--color-text), var(--color-text-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.last-updated {
color: var(--color-text-secondary);
font-size: 1.1rem;
}
.content-section {
margin-bottom: 4rem;
}
.content-section h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--color-text);
}
.content-section h3 {
font-size: 1.5rem;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 1rem;
color: var(--color-text);
}
.content-section h4 {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--color-text);
}
.content-section p {
color: var(--color-text-secondary);
line-height: 1.8;
margin-bottom: 1rem;
}
.info-box {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 1.5rem;
margin: 1.5rem 0;
}
.highlight-box {
display: flex;
gap: 1.5rem;
align-items: start;
background: rgba(0, 255, 136, 0.05);
border: 1px solid rgba(0, 255, 136, 0.2);
border-radius: 1rem;
padding: 1.5rem;
margin: 2rem 0;
}
.highlight-box.positive {
background: rgba(0, 255, 136, 0.1);
border-color: var(--color-accent);
}
.highlight-icon {
font-size: 2rem;
flex-shrink: 0;
}
.highlight-content h4 {
margin-bottom: 0.5rem;
}
.highlight-content p {
margin-bottom: 0;
}
.service-box {
background: var(--color-bg-secondary);
border-left: 3px solid var(--color-accent);
padding: 1.5rem;
margin: 1.5rem 0;
}
.service-box h3 {
margin-top: 0;
}
.contact-box {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
margin: 1rem 0;
font-family: monospace;
}
.data-list {
list-style: none;
padding: 0;
margin: 1rem 0;
}
.data-list li {
color: var(--color-text-secondary);
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
}
.data-list li::before {
content: '•';
position: absolute;
left: 0;
color: var(--color-accent);
}
.rights-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin: 1.5rem 0;
}
.right-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.right-card:hover {
border-color: var(--color-accent);
transform: translateY(-2px);
}
.right-card h4 {
color: var(--color-accent);
margin-bottom: 0.5rem;
}
.right-card p {
font-size: 0.9rem;
margin-bottom: 0;
}
.footer-actions {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
text-align: center;
}
a {
color: var(--color-accent);
text-decoration: none;
transition: opacity 0.2s ease;
}
a:hover {
opacity: 0.8;
text-decoration: underline;
}
/* Responsive */
@media (max-width: 768px) {
.datenschutz-header h1 {
font-size: 2rem;
}
.content-section h2 {
font-size: 1.5rem;
}
.content-section h3 {
font-size: 1.25rem;
}
.highlight-box {
flex-direction: column;
gap: 1rem;
}
.rights-grid {
grid-template-columns: 1fr;
}
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -1,427 +0,0 @@
---
import Layout from '../../../layouts/Layout.astro';
import { games } from '../../../data/games';
export function getStaticPaths() {
return games.map((game) => ({
params: { slug: game.slug },
props: { game },
}));
}
const { game } = Astro.props;
---
<Layout
title={`${game.title} - Playground`}
description={`Code bearbeiten für ${game.title}`}
isGamePage={true}
gameTitle={`${game.title} - Playground`}
gameSlug={game.slug}
isPlayground={true}
hideFooter={true}
>
<div class="playground-page">
<div class="playground-container">
<div class="editor-panel">
<div class="editor-header">
<h3>Code Editor</h3>
<div class="editor-actions">
<button id="resetBtn" class="editor-btn">
<span class="icon">↺</span>
Reset
</button>
<button id="runBtn" class="editor-btn primary">
<span class="icon">▶</span>
Ausführen
</button>
</div>
</div>
<div id="editor" class="code-editor"></div>
</div>
<div class="preview-panel">
<div class="preview-header">
<h3>Vorschau</h3>
<button id="fullscreenPreviewBtn" class="editor-btn">
<span class="icon">⛶</span>
</button>
</div>
<div class="preview-frame">
<iframe
id="preview"
title={`${game.title} Preview`}
sandbox="allow-scripts allow-same-origin"></iframe>
</div>
</div>
</div>
</div>
<div id="loadingOverlay" class="loading-overlay">
<div class="spinner"></div>
<p>Code wird geladen...</p>
</div>
</Layout>
<script>
// Add no-scroll class to body
document.body.classList.add('no-scroll');
// Import CodeMirror CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css';
document.head.appendChild(link);
const themeLink = document.createElement('link');
themeLink.rel = 'stylesheet';
themeLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/monokai.min.css';
document.head.appendChild(themeLink);
// Import CodeMirror
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js';
document.head.appendChild(script);
script.onload = () => {
// Load modes
const modes = ['xml', 'javascript', 'css', 'htmlmixed'];
let loadedModes = 0;
modes.forEach((mode) => {
const modeScript = document.createElement('script');
modeScript.src = `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/${mode}/${mode}.min.js`;
document.head.appendChild(modeScript);
modeScript.onload = () => {
loadedModes++;
if (loadedModes === modes.length) {
initializePlayground();
}
};
});
};
async function initializePlayground() {
const gameUrl = window.location.pathname.replace('/playground', '');
const htmlFile =
document.querySelector<HTMLElement>('[data-game-file]')?.dataset.gameFile ||
`/games/${gameUrl.split('/').pop()}_game.html`;
const editor = document.getElementById('editor');
const preview = document.getElementById('preview') as HTMLIFrameElement;
const runBtn = document.getElementById('runBtn');
const resetBtn = document.getElementById('resetBtn');
const fullscreenPreviewBtn = document.getElementById('fullscreenPreviewBtn');
const loadingOverlay = document.getElementById('loadingOverlay');
let originalCode = '';
let cm: any;
try {
// Fetch the game HTML
const response = await fetch(htmlFile);
originalCode = await response.text();
// Initialize CodeMirror
cm = (window as any).CodeMirror(editor, {
value: originalCode,
mode: 'htmlmixed',
theme: 'monokai',
lineNumbers: true,
lineWrapping: true,
indentUnit: 2,
tabSize: 2,
autofocus: true,
viewportMargin: Infinity,
});
// Force CodeMirror to fill the container
setTimeout(() => {
cm.setSize('100%', '100%');
cm.refresh();
}, 100);
// Initial preview
updatePreview(originalCode);
// Hide loading overlay
loadingOverlay?.classList.add('hidden');
} catch (error) {
console.error('Error loading game:', error);
loadingOverlay!.innerHTML = '<p>Fehler beim Laden des Spiels</p>';
}
// Run button
runBtn?.addEventListener('click', () => {
const code = cm.getValue();
updatePreview(code);
showNotification('Code ausgeführt!', 'success');
});
// Reset button
resetBtn?.addEventListener('click', () => {
if (confirm('Möchtest du wirklich alle Änderungen zurücksetzen?')) {
cm.setValue(originalCode);
updatePreview(originalCode);
showNotification('Code zurückgesetzt!', 'info');
}
});
// Fullscreen preview
fullscreenPreviewBtn?.addEventListener('click', () => {
if (preview.requestFullscreen) {
preview.requestFullscreen();
}
});
// Handle window resize
window.addEventListener('resize', () => {
cm.refresh();
});
// Auto-save to localStorage
cm.on('change', () => {
const code = cm.getValue();
localStorage.setItem(`playground_${gameUrl}`, code);
});
// Load from localStorage if available
const savedCode = localStorage.getItem(`playground_${gameUrl}`);
if (savedCode && savedCode !== originalCode) {
if (confirm('Es gibt gespeicherte Änderungen. Möchtest du diese laden?')) {
cm.setValue(savedCode);
updatePreview(savedCode);
}
}
function updatePreview(code: string) {
const blob = new Blob([code], { type: 'text/html' });
const url = URL.createObjectURL(blob);
preview.src = url;
}
}
function showNotification(message: string, type: 'success' | 'info' | 'error' = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.classList.add('show'), 10);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 2000);
}
</script>
<script define:vars={{ gameFile: game.htmlFile }}>
// Pass game file to the script
document.body.dataset.gameFile = gameFile;
</script>
<style>
.playground-page {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: calc(100vh - 60px);
}
.playground-container {
display: grid;
grid-template-columns: 1fr 1fr;
height: 100%;
gap: 1px;
background: var(--color-border);
}
.editor-panel,
.preview-panel {
background: var(--color-bg);
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-header,
.preview-header {
background: var(--color-bg-secondary);
padding: 1rem;
border-bottom: 1px solid var(--color-border);
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-header h3,
.preview-header h3 {
margin: 0;
font-size: 1rem;
color: var(--color-text-secondary);
}
.editor-actions {
display: flex;
gap: 0.5rem;
}
.editor-btn {
background: var(--color-bg);
color: var(--color-text-secondary);
border: 1px solid var(--color-border);
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-size: 0.85rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease;
}
.editor-btn:hover {
background: var(--color-bg-secondary);
border-color: var(--color-accent);
}
.editor-btn.primary {
background: var(--color-accent);
color: #000;
border-color: var(--color-accent);
}
.editor-btn.primary:hover {
background: var(--color-accent-secondary);
}
.editor-btn .icon {
font-size: 1rem;
}
.code-editor {
flex: 1;
overflow: hidden;
position: relative;
min-height: 0;
}
/* CodeMirror overrides */
.code-editor .CodeMirror {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100%;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 14px;
}
.preview-frame {
flex: 1;
position: relative;
background: #000;
}
.preview-frame iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
/* Loading overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--color-bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
transition: opacity 0.3s ease;
}
.loading-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-border);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Notifications */
.notification {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--color-bg-secondary);
padding: 0.75rem 1.5rem;
border-radius: 0.25rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 2000;
transition: transform 0.3s ease;
font-size: 0.9rem;
}
.notification.show {
transform: translateX(-50%) translateY(0);
}
.notification-success {
border: 1px solid var(--color-accent);
color: var(--color-accent);
}
.notification-info {
border: 1px solid #3b82f6;
color: #3b82f6;
}
.notification-error {
border: 1px solid #ef4444;
color: #ef4444;
}
@media (max-width: 768px) {
.playground-container {
grid-template-columns: 1fr;
}
.preview-panel {
display: none;
}
.editor-header h3::after {
content: ' (Vorschau ausgeblendet)';
font-size: 0.8rem;
opacity: 0.6;
}
}
</style>

View file

@ -1,432 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Button from '../components/Button.astro';
---
<Layout title="Impressum">
<div class="impressum-container">
<header class="impressum-header">
<h1>Impressum</h1>
<p class="subtitle">Angaben gemäß § 5 TMG</p>
</header>
<section class="content-section">
<h2>Verantwortlich für den Inhalt</h2>
<div class="contact-card">
<div class="contact-icon">👤</div>
<div class="contact-info">
<p class="name">[Ihr Name]</p>
<p>[Ihre Straße und Hausnummer]</p>
<p>[PLZ und Ort]</p>
<p>Deutschland</p>
</div>
</div>
</section>
<section class="content-section">
<h2>Kontakt</h2>
<div class="contact-grid">
<div class="contact-item">
<span class="icon">📧</span>
<div>
<h4>E-Mail</h4>
<p>[ihre-email@beispiel.de]</p>
</div>
</div>
<div class="contact-item">
<span class="icon">📱</span>
<div>
<h4>Telefon</h4>
<p>[+49 123 456789]</p>
</div>
</div>
</div>
</section>
<section class="content-section">
<h2>Umsatzsteuer-ID</h2>
<p>Umsatzsteuer-Identifikationsnummer gemäß §27 a Umsatzsteuergesetz:</p>
<div class="highlight-box">
<code>DE[IHRE-UST-ID]</code>
</div>
<p class="note">
Falls Sie keine Umsatzsteuer-ID haben, können Sie diesen Abschnitt entfernen.
</p>
</section>
<section class="content-section">
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
<div class="responsible-box">
<p>[Ihr Name]</p>
<p>[Ihre Adresse]</p>
<p>[PLZ und Ort]</p>
</div>
</section>
<section class="content-section">
<h2>EU-Streitschlichtung</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
</p>
<div class="link-box">
<a href="https://ec.europa.eu/consumers/odr/" target="_blank" rel="noopener noreferrer">
https://ec.europa.eu/consumers/odr/
</a>
</div>
<p>Unsere E-Mail-Adresse finden Sie oben im Impressum.</p>
</section>
<section class="content-section">
<h2>Verbraucherstreitbeilegung / Universalschlichtungsstelle</h2>
<p>
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
Verbraucherschlichtungsstelle teilzunehmen.
</p>
</section>
<section class="content-section">
<h2>Haftungsausschluss (Disclaimer)</h2>
<h3>Haftung für Inhalte</h3>
<p>
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach
den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter
jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen
oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
</p>
<p>
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den
allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst
ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden
von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
</p>
<h3>Haftung für Links</h3>
<p>
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen
Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen.
Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der
Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf
mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung
nicht erkennbar.
</p>
<p>
Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete
Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von
Rechtsverletzungen werden wir derartige Links umgehend entfernen.
</p>
<h3>Urheberrecht</h3>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem
deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der
Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung
des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den
privaten, nicht kommerziellen Gebrauch gestattet.
</p>
<p>
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die
Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche
gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden,
bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden
wir derartige Inhalte umgehend entfernen.
</p>
</section>
<section class="content-section">
<h2>Open Source Hinweis</h2>
<div class="opensource-box">
<div class="opensource-icon">💻</div>
<div class="opensource-content">
<h4>Diese Website ist Open Source</h4>
<p>
Der Quellcode dieser Website ist öffentlich verfügbar. Sie finden das Repository auf:
</p>
<a
href="https://github.com/yourusername/mana-games"
target="_blank"
rel="noopener noreferrer"
class="github-link"
>
<span class="icon">📦</span>
GitHub Repository
</a>
</div>
</div>
</section>
<div class="footer-actions">
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
<Button href="/datenschutz" variant="ghost">Datenschutz</Button>
</div>
</div>
</Layout>
<style>
.impressum-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem 0;
}
.impressum-header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--color-border);
}
.impressum-header h1 {
font-size: 3rem;
font-weight: 900;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--color-text), var(--color-text-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
color: var(--color-text-secondary);
font-size: 1.1rem;
}
.content-section {
margin-bottom: 3rem;
}
.content-section h2 {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--color-text);
}
.content-section h3 {
font-size: 1.3rem;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 1rem;
color: var(--color-text);
}
.content-section h4 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--color-text);
}
.content-section p {
color: var(--color-text-secondary);
line-height: 1.8;
margin-bottom: 1rem;
}
.contact-card {
display: flex;
gap: 2rem;
align-items: start;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
margin-bottom: 1rem;
}
.contact-icon {
font-size: 3rem;
opacity: 0.5;
}
.contact-info p {
margin-bottom: 0.5rem;
font-family: monospace;
}
.contact-info .name {
font-weight: 600;
color: var(--color-text);
font-size: 1.2rem;
margin-bottom: 1rem;
}
.contact-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 1rem;
}
.contact-item {
display: flex;
gap: 1rem;
align-items: start;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
}
.contact-item .icon {
font-size: 2rem;
flex-shrink: 0;
}
.contact-item h4 {
margin-bottom: 0.25rem;
}
.contact-item p {
margin-bottom: 0;
font-family: monospace;
}
.highlight-box {
background: rgba(0, 255, 136, 0.1);
border: 1px solid var(--color-accent);
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
text-align: center;
}
.highlight-box code {
color: var(--color-accent);
font-size: 1.2rem;
font-weight: 600;
}
.note {
font-size: 0.9rem;
font-style: italic;
opacity: 0.8;
}
.responsible-box {
background: var(--color-bg-secondary);
border-left: 3px solid var(--color-accent);
padding: 1.5rem;
margin: 1rem 0;
}
.responsible-box p {
margin-bottom: 0.5rem;
font-family: monospace;
}
.link-box {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
text-align: center;
}
.link-box a {
color: var(--color-accent);
text-decoration: none;
word-break: break-all;
}
.link-box a:hover {
text-decoration: underline;
}
.opensource-box {
display: flex;
gap: 2rem;
align-items: center;
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
border: 1px solid rgba(0, 255, 136, 0.2);
border-radius: 1rem;
padding: 2rem;
margin: 2rem 0;
}
.opensource-icon {
font-size: 3rem;
flex-shrink: 0;
}
.opensource-content h4 {
color: var(--color-accent);
margin-bottom: 0.5rem;
}
.opensource-content p {
margin-bottom: 1rem;
}
.github-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
color: var(--color-text);
text-decoration: none;
transition: all 0.3s ease;
}
.github-link:hover {
border-color: var(--color-accent);
color: var(--color-accent);
transform: translateY(-2px);
}
.footer-actions {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
a {
color: var(--color-accent);
text-decoration: none;
transition: opacity 0.2s ease;
}
a:hover {
opacity: 0.8;
text-decoration: underline;
}
/* Responsive */
@media (max-width: 768px) {
.impressum-header h1 {
font-size: 2rem;
}
.content-section h2 {
font-size: 1.5rem;
}
.content-section h3 {
font-size: 1.2rem;
}
.contact-card {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.contact-grid {
grid-template-columns: 1fr;
}
.opensource-box {
flex-direction: column;
text-align: center;
gap: 1rem;
}
}
</style>

View file

@ -1,765 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import GameCard from '../components/GameCard.astro';
import MyGamesSection from '../components/MyGamesSection.astro';
import HorizontalScroller from '../components/HorizontalScroller.astro';
import { games } from '../data/games';
// Filtere offizielle Spiele (ohne community flag)
const officialGames = games.filter((game) => !game.community);
// Kategorisiere Spiele nach Genres für verschiedene Scroller
const arcadeGames = officialGames.filter((game) => game.tags.includes('Arcade'));
const puzzleGames = officialGames.filter((game) => game.tags.includes('Puzzle'));
const actionGames = officialGames.filter(
(game) =>
game.tags.includes('Action') ||
game.tags.includes('Shooter') ||
game.tags.includes('Jump n Run')
);
// Sortiere nach Beliebtheit/Komplexität
const featuredGames = [...officialGames].slice(0, 8);
---
<Layout title="Startseite" fullWidth={true}>
<section class="hero">
<div class="hero-content">
<h1 class="hero-title">
<span class="line line-1">
<span id="changingWord">Spiele</span>
<span class="line line-2">ohne&nbsp;&nbsp;&nbsp;Grenzen</span>
</span>
</h1>
</div>
<div class="hero-visual">
<div class="floating-squares">
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
</div>
</div>
</section>
<section class="stats-section">
<div class="stats-container">
<div class="stat">
<span class="stat-number">{officialGames.length}</span>
<span class="stat-label">Spiele</span>
</div>
<div class="stat-divider"></div>
<div class="stat">
<span class="stat-number">100%</span>
<span class="stat-label">Kostenlos</span>
</div>
<div class="stat-divider"></div>
<div class="stat">
<span class="stat-number">100%</span>
<span class="stat-label">Werbefrei</span>
</div>
<div class="stat-divider"></div>
<a href="/stats" class="stat stat-link">
<span class="stat-number">📊</span>
<span class="stat-label">Meine Stats</span>
</a>
</div>
</section>
<!-- Featured Official Games - Netflix Style -->
<HorizontalScroller title="Offizielle Mana Games" games={featuredGames} id="featured-scroller" />
<!-- My Games Section -->
<MyGamesSection maxGames={4} />
<!-- Genre-basierte Scroller -->
{
arcadeGames.length > 0 && (
<HorizontalScroller title="Arcade Spiele" games={arcadeGames} id="arcade-scroller" />
)
}
{
puzzleGames.length > 0 && (
<HorizontalScroller title="Puzzle & Denkspiele" games={puzzleGames} id="puzzle-scroller" />
)
}
{
actionGames.length > 0 && (
<HorizontalScroller title="Action & Adventure" games={actionGames} id="action-scroller" />
)
}
<section class="games-section">
<div class="section-header">
<h2>Alle Spiele durchsuchen</h2>
<div class="filter-tabs">
<button class="filter-tab active" data-filter="all"> Alle </button>
<button class="filter-tab" data-filter="official"> Offizielle </button>
<button class="filter-tab" data-filter="my-games"> Meine Spiele </button>
<button class="filter-tab" data-filter="community"> Community </button>
</div>
</div>
<div id="officialGames" class="games-grid">
{
officialGames.map((game, index) => (
<div class="game-wrapper" style={`--delay: ${0.4 + index * 0.1}s`}>
<GameCard
title={game.title}
description={game.description}
slug={game.slug}
thumbnail={game.thumbnail}
tags={game.tags}
complexity={game.complexity}
codeStats={game.codeStats}
/>
</div>
))
}
</div>
<div id="myGamesGrid" class="games-grid hidden">
<!-- Will be populated by JavaScript -->
</div>
<div id="communityGamesGrid" class="games-grid hidden">
<!-- Will be populated by JavaScript -->
</div>
<div id="allGamesGrid" class="games-grid hidden">
<!-- Will be populated by JavaScript -->
</div>
</section>
</Layout>
<style>
.hero {
position: relative;
min-height: 30vh;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
overflow: hidden;
}
.hero-content {
position: relative;
z-index: 2;
text-align: center;
}
.hero-title {
font-size: clamp(2.5rem, 7vw, 4.5rem);
font-weight: 900;
line-height: 0.85;
margin-bottom: 1rem;
letter-spacing: -0.05em;
}
.line {
display: inline;
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.4s ease forwards;
}
.line-1 {
animation-delay: 0s;
}
.line-2 {
animation-delay: 0.1s;
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-left: 0.1em;
}
#changingWord {
display: inline-block;
min-width: 3em;
text-align: right;
position: relative;
color: var(--color-text);
}
.word-fade-out {
animation: fadeOut 0.3s ease-out forwards;
}
.word-fade-in {
animation: fadeIn 0.3s ease-in forwards;
}
@keyframes fadeOut {
to {
opacity: 0;
transform: translateY(-10px);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Floating visual elements */
.hero-visual {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.1;
}
.floating-squares {
position: relative;
width: 100%;
height: 100%;
}
.square {
position: absolute;
border: 1px solid rgba(0, 255, 136, 0.2);
border-radius: 1rem;
}
.square:nth-child(1) {
width: 200px;
height: 200px;
top: 10%;
left: 10%;
transform: rotate(15deg);
}
.square:nth-child(2) {
width: 300px;
height: 300px;
top: 50%;
right: 10%;
transform: rotate(-20deg);
}
.square:nth-child(3) {
width: 150px;
height: 150px;
bottom: 10%;
left: 50%;
transform: rotate(45deg);
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
/* Stats Section */
.stats-section {
margin-top: -1rem;
margin-bottom: 2.5rem;
opacity: 0;
transform: translateY(15px);
animation: fadeInUp 0.4s ease 0.3s forwards;
}
.stats-container {
display: flex;
justify-content: center;
align-items: center;
gap: 2.5rem;
flex-wrap: wrap;
}
.stat {
text-align: center;
}
.stat-number {
display: block;
font-size: 1.75rem;
font-weight: 900;
color: var(--color-accent);
line-height: 1;
margin-bottom: 0.25rem;
}
.stat-label {
display: block;
font-size: 0.75rem;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.1em;
opacity: 0.7;
}
.stat-divider {
width: 1px;
height: 30px;
background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.1), transparent);
}
.stat-link {
text-decoration: none;
transition: all 0.2s ease;
cursor: pointer;
}
.stat-link:hover {
transform: translateY(-2px);
}
.stat-link .stat-number {
font-size: 2rem;
}
.stat-link:hover .stat-number {
transform: scale(1.1);
transition: transform 0.2s ease;
}
/* Games Section */
.games-section {
position: relative;
margin-top: 4rem;
margin-bottom: 6rem;
padding-top: 3rem;
border-top: 1px solid var(--color-border);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3rem;
}
.section-header h2 {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
margin: 0;
}
.filter-tabs {
display: flex;
gap: 0.5rem;
background: var(--color-surface);
padding: 0.25rem;
border-radius: 10px;
border: 1px solid var(--color-border);
}
.filter-tab {
background: transparent;
color: var(--color-text-secondary);
border: none;
border-radius: 8px;
padding: 0.5rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.filter-tab:hover {
color: var(--color-text);
}
.filter-tab.active {
background: var(--color-bg);
color: var(--color-accent);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.hidden {
display: none !important;
}
.games-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
align-items: stretch;
}
.game-wrapper {
opacity: 0;
transform: translateY(15px);
animation: fadeInUp 0.3s ease var(--delay) forwards;
}
/* Minimal grid line decoration */
.games-section::before {
content: '';
position: absolute;
top: -3rem;
left: 0;
width: 100px;
height: 2px;
background: linear-gradient(90deg, var(--color-accent), transparent);
}
/* Generated game cards styling */
.my-game-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
height: 100%;
display: flex;
flex-direction: column;
}
.my-game-card:hover {
transform: translateY(-2px);
border-color: var(--color-accent);
box-shadow: 0 4px 12px rgba(0, 255, 136, 0.1);
}
.my-game-card .card-thumbnail {
aspect-ratio: 16/9;
background: var(--color-bg);
overflow: hidden;
}
.my-game-card .card-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.my-game-card .placeholder-thumbnail {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--color-text-muted);
}
.my-game-card .card-content {
padding: 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
}
.my-game-card .card-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--color-text);
margin: 0 0 0.5rem 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.my-game-card .card-description {
color: var(--color-text-secondary);
font-size: 0.9rem;
margin: 0 0 1rem 0;
flex: 1;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.my-game-card .card-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85rem;
}
.my-game-card .complexity-badge {
background: var(--color-accent);
color: var(--color-bg);
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-weight: 600;
font-size: 0.75rem;
}
.my-game-card .creation-date {
color: var(--color-text-muted);
}
@media (max-width: 768px) {
.hero {
min-height: 25vh;
margin-bottom: 1rem;
}
.hero-title {
font-size: clamp(2rem, 9vw, 3rem);
}
.stats-container {
gap: 2rem;
}
.stats-section {
margin-top: -0.5rem;
margin-bottom: 2rem;
}
.stat-number {
font-size: 1.5rem;
}
.stat-label {
font-size: 0.7rem;
}
.stat-divider {
height: 25px;
}
.games-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.square {
display: none;
}
}
</style>
<script>
// Wörter-Animation für den Hero-Text
const words = ['Spiele', 'Baue', 'Lerne'];
let currentWordIndex = 0;
const changingWord = document.getElementById('changingWord');
function changeWord() {
// Fade out
changingWord.classList.add('word-fade-out');
setTimeout(() => {
// Wechsle zum nächsten Wort
currentWordIndex = (currentWordIndex + 1) % words.length;
changingWord.textContent = words[currentWordIndex];
// Fade in
changingWord.classList.remove('word-fade-out');
changingWord.classList.add('word-fade-in');
setTimeout(() => {
changingWord.classList.remove('word-fade-in');
}, 300);
}, 300);
}
// Starte Animation nach 2 Sekunden, dann alle 3 Sekunden
setTimeout(() => {
changeWord();
setInterval(changeWord, 3000);
}, 2000);
// Filter functionality
const filterTabs = document.querySelectorAll('.filter-tab');
const officialGames = document.getElementById('officialGames');
const myGamesGrid = document.getElementById('myGamesGrid');
const communityGamesGrid = document.getElementById('communityGamesGrid');
const allGamesGrid = document.getElementById('allGamesGrid');
// GameStorage class (simplified version for reading)
class GameStorage {
private dbName = 'ManaGamesDB';
private storeName = 'generatedGames';
private db: IDBDatabase | null = null;
async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
store.createIndex('createdAt', 'createdAt', { unique: false });
store.createIndex('title', 'title', { unique: false });
}
};
});
}
async getAllGames(): Promise<any[]> {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result || []);
request.onerror = () => reject(request.error);
});
}
}
const gameStorage = new GameStorage();
// Load and display my games
async function loadMyGames() {
try {
const myGames = await gameStorage.getAllGames();
if (myGames.length === 0) {
myGamesGrid.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem;">
<p style="color: var(--color-text-secondary); font-size: 1.1rem;">
Du hast noch keine eigenen Spiele erstellt
</p>
<a href="/create" style="display: inline-block; margin-top: 1rem; background: var(--color-accent); color: var(--color-bg); padding: 0.75rem 2rem; border-radius: 8px; text-decoration: none; font-weight: 600;">
Erstelle dein erstes Spiel
</a>
</div>
`;
return;
}
// Sort by creation date (newest first)
myGames.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
// Create game cards
const gameCards = myGames
.map((game, index) => {
const date = new Date(game.createdAt).toLocaleDateString('de-DE');
return `
<div class="game-wrapper" style="--delay: ${0.4 + index * 0.1}s">
<div class="game-card my-game-card" onclick="window.location.href='/play-generated?id=${game.id}'">
<div class="card-thumbnail">
${
game.thumbnail
? `<img src="${game.thumbnail}" alt="${game.title}" />`
: `<div class="placeholder-thumbnail">🎮</div>`
}
</div>
<div class="card-content">
<h3 class="card-title">${game.title}</h3>
<p class="card-description">${game.description || game.prompt}</p>
<div class="card-meta">
<span class="complexity-badge">Generiert</span>
<span class="creation-date">${date}</span>
</div>
</div>
</div>
</div>
`;
})
.join('');
myGamesGrid.innerHTML = gameCards;
} catch (error) {
console.error('Error loading my games:', error);
myGamesGrid.innerHTML = '<p style="color: #ef4444;">Fehler beim Laden der Spiele</p>';
}
}
// Load community games
async function loadCommunityGames() {
// For now, show a placeholder - will be populated when community games are added
communityGamesGrid.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem;">
<p style="color: var(--color-text-secondary); font-size: 1.1rem;">
Noch keine Community-Spiele verfügbar
</p>
<a href="/submit" style="display: inline-block; margin-top: 1rem; background: var(--color-accent); color: var(--color-bg); padding: 0.75rem 2rem; border-radius: 8px; text-decoration: none; font-weight: 600;">
Reiche dein Spiel ein
</a>
</div>
`;
}
// Merge official and my games for "All" view
async function loadAllGames() {
const officialGamesHTML = officialGames.innerHTML;
const myGames = await gameStorage.getAllGames();
// Clone official games
allGamesGrid.innerHTML = officialGamesHTML;
// Add my games if any
if (myGames.length > 0) {
const myGamesHTML = myGamesGrid.innerHTML;
allGamesGrid.innerHTML = officialGamesHTML + myGamesHTML;
}
}
// Filter tab click handlers
filterTabs.forEach((tab) => {
tab.addEventListener('click', async () => {
// Update active tab
filterTabs.forEach((t) => t.classList.remove('active'));
tab.classList.add('active');
const filter = tab.getAttribute('data-filter');
// Show/hide appropriate grids
officialGames.classList.add('hidden');
myGamesGrid.classList.add('hidden');
communityGamesGrid.classList.add('hidden');
allGamesGrid.classList.add('hidden');
switch (filter) {
case 'official':
officialGames.classList.remove('hidden');
break;
case 'my-games':
if (myGamesGrid.innerHTML === '') {
await loadMyGames();
}
myGamesGrid.classList.remove('hidden');
break;
case 'community':
if (communityGamesGrid.innerHTML === '') {
await loadCommunityGames();
}
communityGamesGrid.classList.remove('hidden');
break;
case 'all':
default:
if (allGamesGrid.innerHTML === '') {
await loadAllGames();
}
allGamesGrid.classList.remove('hidden');
break;
}
});
});
// Initialize with "All" view
document.addEventListener('DOMContentLoaded', async () => {
// Trigger click on "All" tab to load combined view
const allTab = document.querySelector('[data-filter="all"]');
if (allTab) {
(allTab as HTMLElement).click();
}
});
</script>

View file

@ -1,601 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Button from '../components/Button.astro';
---
<Layout title="Jugendschutz">
<div class="jugendschutz-container">
<header class="jugendschutz-header">
<div class="header-icon">🛡️</div>
<h1>Jugendschutz bei Mana Games</h1>
<p class="subtitle">Sicher spielen für alle Altersgruppen</p>
</header>
<section class="intro-section">
<div class="intro-card">
<p>
Bei Mana Games liegt uns die Sicherheit und das Wohlbefinden junger Spieler besonders am
Herzen. Unsere Plattform ist so gestaltet, dass Kinder und Jugendliche sicher und
altersgerecht spielen können.
</p>
</div>
</section>
<section class="content-section">
<h2>Unsere Jugendschutz-Prinzipien</h2>
<div class="principles-grid">
<div class="principle-card">
<div class="card-icon">🎮</div>
<h3>Altersgerechte Inhalte</h3>
<p>
Alle unsere Spiele sind familienfreundlich und enthalten keine Gewalt, explizite Inhalte
oder verstörende Elemente.
</p>
</div>
<div class="principle-card">
<div class="card-icon">🚫</div>
<h3>Keine Werbung</h3>
<p>
Unsere Plattform ist komplett werbefrei. Kinder werden nicht mit kommerziellen Inhalten
oder In-App-Käufen konfrontiert.
</p>
</div>
<div class="principle-card">
<div class="card-icon">🔒</div>
<h3>Datenschutz</h3>
<p>
Wir sammeln keine persönlichen Daten von Kindern. Alle Spielstände werden nur lokal im
Browser gespeichert.
</p>
</div>
<div class="principle-card">
<div class="card-icon">💬</div>
<h3>Kein Chat</h3>
<p>
Es gibt keine Chat-Funktionen oder soziale Features, die eine Kontaktaufnahme zwischen
Nutzern ermöglichen.
</p>
</div>
</div>
</section>
<section class="content-section">
<h2>Altersempfehlungen</h2>
<div class="age-recommendations">
<div class="age-group">
<div class="age-badge" style="--badge-color: #4ade80;">0-6 Jahre</div>
<h4>Vorschulalter</h4>
<p>Einfache Spiele mit großen Buttons und klaren visuellen Elementen:</p>
<ul>
<li>Memory-Spiele</li>
<li>Einfache Puzzle</li>
<li>Farb- und Formspiele</li>
</ul>
</div>
<div class="age-group">
<div class="age-badge" style="--badge-color: #22d3ee;">6-12 Jahre</div>
<h4>Grundschulalter</h4>
<p>Spiele die Geschicklichkeit und logisches Denken fördern:</p>
<ul>
<li>Jump'n'Run Spiele</li>
<li>Einfache Strategiespiele</li>
<li>Lernspiele</li>
</ul>
</div>
<div class="age-group">
<div class="age-badge" style="--badge-color: #a78bfa;">12+ Jahre</div>
<h4>Jugendliche</h4>
<p>Komplexere Spiele mit anspruchsvollen Herausforderungen:</p>
<ul>
<li>Tower Defense</li>
<li>Komplexe Puzzle</li>
<li>Strategiespiele</li>
</ul>
</div>
</div>
</section>
<section class="content-section">
<h2>Hinweise für Eltern</h2>
<div class="parent-tips">
<div class="tip-card">
<h3>🕐 Spielzeiten begrenzen</h3>
<p>
Auch wenn unsere Spiele pädagogisch wertvoll sind, empfehlen wir altersgerechte
Bildschirmzeiten einzuhalten.
</p>
<div class="time-recommendations">
<div class="time-item">
<strong>3-6 Jahre:</strong> max. 30 Minuten täglich
</div>
<div class="time-item">
<strong>6-9 Jahre:</strong> max. 1 Stunde täglich
</div>
<div class="time-item">
<strong>10+ Jahre:</strong> max. 2 Stunden täglich
</div>
</div>
</div>
<div class="tip-card">
<h3>👨‍👩‍👧 Gemeinsam spielen</h3>
<p>
Nutzen Sie die Gelegenheit, mit Ihren Kindern gemeinsam zu spielen. Das fördert nicht
nur die Bindung, sondern ermöglicht auch Gespräche über das Spielerlebnis.
</p>
</div>
<div class="tip-card">
<h3>🎯 Altersgerechte Auswahl</h3>
<p>Achten Sie auf die Komplexitätsstufen unserer Spiele:</p>
<ul class="complexity-list">
<li><span class="badge minimal">Minimal</span> - Für die Kleinsten</li>
<li><span class="badge einfach">Einfach</span> - Ab Grundschulalter</li>
<li><span class="badge mittel">Mittel</span> - Für erfahrene Spieler</li>
<li><span class="badge komplex">Komplex</span> - Herausfordernd</li>
</ul>
</div>
</div>
</section>
<section class="content-section">
<h2>KI-Generator und Jugendschutz</h2>
<div class="ai-safety">
<div class="safety-info">
<h3>Sichere KI-Nutzung</h3>
<p>Unser KI-Spielegenerator verfügt über eingebaute Sicherheitsmechanismen:</p>
<ul>
<li>Filterung ungeeigneter Begriffe und Themen</li>
<li>Automatische Prüfung generierter Inhalte</li>
<li>Keine Generierung von gewalttätigen oder ungeeigneten Spielen</li>
</ul>
</div>
<div class="warning-box">
<h4>⚠️ Empfehlung</h4>
<p>
Wir empfehlen, dass Kinder unter 12 Jahren den KI-Generator nur unter Aufsicht von
Erwachsenen nutzen.
</p>
</div>
</div>
</section>
<section class="content-section">
<h2>Technische Schutzmaßnahmen</h2>
<div class="tech-measures">
<div class="measure">
<div class="measure-icon">🌐</div>
<div class="measure-content">
<h4>Keine externen Links</h4>
<p>Unsere Spiele enthalten keine Links zu externen Websites.</p>
</div>
</div>
<div class="measure">
<div class="measure-icon">📵</div>
<div class="measure-content">
<h4>Offline spielbar</h4>
<p>Nach dem ersten Laden können alle Spiele offline gespielt werden.</p>
</div>
</div>
<div class="measure">
<div class="measure-icon">🔐</div>
<div class="measure-content">
<h4>Lokale Datenspeicherung</h4>
<p>Alle Daten bleiben auf dem Gerät des Nutzers.</p>
</div>
</div>
</div>
</section>
<section class="content-section">
<h2>Kontakt und Meldungen</h2>
<div class="contact-info">
<p>
Haben Sie Bedenken bezüglich eines Spiels oder möchten Sie uns auf problematische Inhalte
hinweisen? Wir nehmen jeden Hinweis ernst.
</p>
<div class="contact-card">
<h3>Jugendschutzbeauftragter</h3>
<p>
E-Mail: jugendschutz@[ihre-domain].de<br />
Wir antworten innerhalb von 24 Stunden auf alle Anfragen.
</p>
</div>
</div>
</section>
<section class="content-section">
<h2>Weitere Ressourcen</h2>
<div class="resources">
<h3>Hilfreiche Links für Eltern:</h3>
<ul class="resource-list">
<li>
<a href="https://www.klicksafe.de" target="_blank" rel="noopener noreferrer">
klicksafe.de - EU-Initiative für mehr Sicherheit im Netz
</a>
</li>
<li>
<a href="https://www.schau-hin.info" target="_blank" rel="noopener noreferrer">
SCHAU HIN! - Medienratgeber für Familien
</a>
</li>
<li>
<a href="https://www.jugendschutz.net" target="_blank" rel="noopener noreferrer">
jugendschutz.net - Kompetenzzentrum für Jugendschutz im Internet
</a>
</li>
</ul>
</div>
</section>
<div class="footer-actions">
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
<Button href="/agb" variant="ghost">Nutzungsbedingungen</Button>
</div>
</div>
</Layout>
<style>
.jugendschutz-container {
max-width: 900px;
margin: 0 auto;
padding: 2rem 0;
}
.jugendschutz-header {
text-align: center;
margin-bottom: 3rem;
}
.header-icon {
font-size: 4rem;
margin-bottom: 1rem;
display: inline-block;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.jugendschutz-header h1 {
font-size: 3rem;
font-weight: 900;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--color-text), var(--color-accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-size: 1.2rem;
color: var(--color-text-secondary);
}
.intro-section {
margin-bottom: 3rem;
}
.intro-card {
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), transparent);
border: 1px solid rgba(0, 255, 136, 0.2);
border-radius: 1rem;
padding: 2rem;
text-align: center;
}
.intro-card p {
font-size: 1.1rem;
line-height: 1.8;
color: var(--color-text);
margin: 0;
}
.content-section {
margin-bottom: 4rem;
}
.content-section h2 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 2rem;
color: var(--color-text);
}
.content-section h3 {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--color-text);
}
.content-section h4 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--color-text);
}
.content-section p {
color: var(--color-text-secondary);
line-height: 1.8;
margin-bottom: 1rem;
}
.principles-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.principle-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
}
.principle-card:hover {
transform: translateY(-5px);
border-color: var(--color-accent);
box-shadow: 0 10px 30px rgba(0, 255, 136, 0.1);
}
.card-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.age-recommendations {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
}
.age-group {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
}
.age-badge {
display: inline-block;
background: var(--badge-color, var(--color-accent));
color: #000;
padding: 0.5rem 1.5rem;
border-radius: 2rem;
font-weight: 700;
margin-bottom: 1rem;
}
.age-group ul {
list-style: none;
padding: 0;
}
.age-group li {
padding: 0.5rem 0;
color: var(--color-text-secondary);
position: relative;
padding-left: 1.5rem;
}
.age-group li::before {
content: '•';
position: absolute;
left: 0;
color: var(--color-accent);
}
.parent-tips {
display: grid;
gap: 2rem;
}
.tip-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
}
.time-recommendations {
background: var(--color-bg);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.time-item {
padding: 0.5rem 0;
color: var(--color-text-secondary);
}
.complexity-list {
list-style: none;
padding: 0;
margin-top: 1rem;
}
.complexity-list li {
padding: 0.5rem 0;
display: flex;
align-items: center;
gap: 1rem;
}
.badge {
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.85rem;
font-weight: 600;
}
.badge.minimal {
background: #4ade80;
color: #000;
}
.badge.einfach {
background: #22d3ee;
color: #000;
}
.badge.mittel {
background: #fbbf24;
color: #000;
}
.badge.komplex {
background: #f87171;
color: #000;
}
.ai-safety {
display: grid;
gap: 2rem;
}
.safety-info {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
}
.safety-info ul {
list-style: none;
padding: 0;
margin-top: 1rem;
}
.safety-info li {
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
color: var(--color-text-secondary);
}
.safety-info li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--color-accent);
}
.warning-box {
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
border-radius: 0.5rem;
padding: 1.5rem;
}
.tech-measures {
display: grid;
gap: 1.5rem;
}
.measure {
display: flex;
gap: 1.5rem;
align-items: start;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
}
.measure-icon {
font-size: 2rem;
flex-shrink: 0;
}
.contact-info {
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
border-radius: 1rem;
padding: 2rem;
}
.contact-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
margin-top: 1rem;
}
.resource-list {
list-style: none;
padding: 0;
margin-top: 1rem;
}
.resource-list li {
padding: 0.75rem 0;
}
.resource-list a {
color: var(--color-accent);
text-decoration: none;
transition: opacity 0.2s ease;
}
.resource-list a:hover {
opacity: 0.8;
text-decoration: underline;
}
.footer-actions {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.jugendschutz-header h1 {
font-size: 2rem;
}
.content-section h2 {
font-size: 1.5rem;
}
.principles-grid {
grid-template-columns: 1fr;
}
.age-recommendations {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -1,644 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
import Button from '../components/Button.astro';
---
<Layout title="Mitmachen">
<div class="mitmachen-hero">
<div class="hero-background">
<div class="floating-element element-1"></div>
<div class="floating-element element-2"></div>
<div class="floating-element element-3"></div>
</div>
<div class="hero-content">
<h1 class="hero-title">
<span class="title-line">Werde Teil der</span>
<span class="title-highlight">Community</span>
</h1>
<p class="hero-subtitle">Gemeinsam erschaffen wir die Zukunft des Web-Gaming</p>
</div>
</div>
<div class="mitmachen-container">
<!-- Intro Section -->
<section class="intro-section">
<div class="intro-content">
<p class="intro-text">
Mana Games ist mehr als nur eine Spielesammlung es ist eine wachsende Community von
Entwicklern, Kreativen und Gaming-Enthusiasten. Deine Ideen und Beiträge können Teil
dieser Vision werden.
</p>
</div>
</section>
<!-- Ways to Contribute -->
<section class="contribute-section">
<div class="section-header">
<span class="section-number">01</span>
<h2>Wie du beitragen kannst</h2>
</div>
<div class="contribute-cards">
<div class="contribute-row row-left">
<div class="contribute-visual">
<div class="icon-box">💡</div>
</div>
<div class="contribute-content">
<h3>Spielideen einreichen</h3>
<p>
Du hast eine geniale Spielidee? Teile sie mit uns! Wir sind immer auf der Suche nach
innovativen Konzepten, die Spaß machen und gleichzeitig technisch interessant sind.
</p>
<ul class="feature-list">
<li>Neue Gameplay-Mechaniken</li>
<li>Kreative Themes und Settings</li>
<li>Innovative Steuerungskonzepte</li>
</ul>
</div>
</div>
<div class="contribute-row row-right">
<div class="contribute-content">
<h3>Code & Entwicklung</h3>
<p>
Als Open-Source-Projekt freuen wir uns über Code-Beiträge jeder Art. Ob Bug-Fixes,
Performance-Optimierungen oder neue Features jeder Beitrag zählt.
</p>
<ul class="feature-list">
<li>JavaScript/HTML5 Canvas Expertise</li>
<li>Performance-Optimierungen</li>
<li>Bug-Fixes und Verbesserungen</li>
</ul>
</div>
<div class="contribute-visual">
<div class="icon-box">🚀</div>
</div>
</div>
<div class="contribute-row row-left">
<div class="contribute-visual">
<div class="icon-box">🎨</div>
</div>
<div class="contribute-content">
<h3>Design & Grafik</h3>
<p>
Hilf uns dabei, Mana Games noch schöner zu machen! Von Spiel-Assets über
UI-Verbesserungen bis hin zu komplett neuen visuellen Konzepten.
</p>
<ul class="feature-list">
<li>Pixel Art & Sprites</li>
<li>UI/UX Verbesserungen</li>
<li>Visuelle Effekte & Animationen</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Benefits Section -->
<section class="benefits-section">
<div class="section-header">
<span class="section-number">02</span>
<h2>Was dich erwartet</h2>
</div>
<div class="benefits-grid">
<div class="benefit-card">
<div class="benefit-icon">🏆</div>
<h4>Anerkennung</h4>
<p>Dein Name in den Credits und der Contributors-Liste</p>
</div>
<div class="benefit-card">
<div class="benefit-icon">📚</div>
<h4>Lernerfahrung</h4>
<p>Arbeite mit modernen Web-Technologien und lerne von der Community</p>
</div>
<div class="benefit-card">
<div class="benefit-icon">🌍</div>
<h4>Reichweite</h4>
<p>Deine Arbeit wird von Spielern weltweit gesehen und gespielt</p>
</div>
<div class="benefit-card">
<div class="benefit-icon">🤝</div>
<h4>Netzwerk</h4>
<p>Verbinde dich mit gleichgesinnten Entwicklern und Kreativen</p>
</div>
</div>
</section>
<!-- Guidelines Section -->
<section class="guidelines-section">
<div class="section-header">
<span class="section-number">03</span>
<h2>Unsere Richtlinien</h2>
</div>
<div class="guidelines-content">
<div class="guideline">
<span class="guideline-icon">✅</span>
<div>
<strong>Qualität über Quantität</strong>
<p>Wir legen Wert auf durchdachte, gut implementierte Beiträge</p>
</div>
</div>
<div class="guideline">
<span class="guideline-icon">🎯</span>
<div>
<strong>Performance im Fokus</strong>
<p>Spiele müssen flüssig auf allen Geräten laufen</p>
</div>
</div>
<div class="guideline">
<span class="guideline-icon">🌟</span>
<div>
<strong>Kreativität fördern</strong>
<p>Neue Ideen und innovative Ansätze sind immer willkommen</p>
</div>
</div>
<div class="guideline">
<span class="guideline-icon">👥</span>
<div>
<strong>Respektvolle Community</strong>
<p>Ein freundlicher und konstruktiver Umgang miteinander</p>
</div>
</div>
</div>
</section>
<!-- Tech Stack -->
<section class="tech-section">
<div class="section-header">
<span class="section-number">04</span>
<h2>Unser Tech-Stack</h2>
</div>
<div class="tech-info">
<p class="tech-intro">
Arbeite mit modernen Web-Technologien und erweitere deine Fähigkeiten:
</p>
<div class="tech-grid">
<div class="tech-item">
<code>HTML5 Canvas</code>
<span>Grafik-Engine</span>
</div>
<div class="tech-item">
<code>JavaScript ES6+</code>
<span>Programmierung</span>
</div>
<div class="tech-item">
<code>Astro</code>
<span>Framework</span>
</div>
<div class="tech-item">
<code>PWA</code>
<span>App-Technologie</span>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="cta-section">
<div class="cta-content">
<h2>Bereit durchzustarten?</h2>
<p>
Egal ob du Entwickler, Designer oder einfach voller Ideen bist wir freuen uns auf deinen
Beitrag zur Mana Games Community!
</p>
<div class="cta-buttons">
<Button href="https://github.com/yourusername/mana-games" variant="primary" size="large">
GitHub Repository
</Button>
<Button href="/contact" variant="accent" size="large"> Kontakt aufnehmen </Button>
</div>
</div>
</section>
</div>
</Layout>
<style>
/* Hero Section */
.mitmachen-hero {
position: relative;
min-height: 50vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
margin-bottom: 4rem;
}
.hero-background {
position: absolute;
inset: 0;
opacity: 0.1;
}
.floating-element {
position: absolute;
border: 2px solid var(--color-accent);
border-radius: 50%;
animation: float 20s ease-in-out infinite;
}
.element-1 {
width: 300px;
height: 300px;
top: -100px;
left: -100px;
animation-delay: 0s;
}
.element-2 {
width: 200px;
height: 200px;
bottom: -50px;
right: -50px;
animation-delay: 5s;
}
.element-3 {
width: 150px;
height: 150px;
top: 50%;
left: 80%;
animation-delay: 10s;
}
@keyframes float {
0%,
100% {
transform: translate(0, 0) rotate(0deg);
}
33% {
transform: translate(30px, -30px) rotate(120deg);
}
66% {
transform: translate(-20px, 20px) rotate(240deg);
}
}
.hero-content {
position: relative;
z-index: 1;
text-align: center;
}
.hero-title {
font-size: clamp(3rem, 8vw, 5rem);
font-weight: 900;
line-height: 1;
margin-bottom: 1rem;
}
.title-line {
display: block;
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s ease forwards;
}
.title-highlight {
display: block;
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s ease 0.2s forwards;
}
.hero-subtitle {
font-size: 1.5rem;
color: var(--color-text-secondary);
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.6s ease 0.4s forwards;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
/* Container */
.mitmachen-container {
max-width: 1200px;
margin: 0 auto;
}
/* Sections */
section {
margin-bottom: 6rem;
opacity: 0;
transform: translateY(30px);
animation: fadeInUp 0.8s ease forwards;
}
.section-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 3rem;
}
.section-number {
font-size: 3rem;
font-weight: 900;
color: var(--color-accent);
opacity: 0.3;
}
h2 {
font-size: 2.5rem;
font-weight: 900;
}
/* Intro Section */
.intro-section {
text-align: center;
margin-bottom: 4rem;
}
.intro-text {
font-size: 1.25rem;
color: var(--color-text-secondary);
line-height: 1.8;
max-width: 800px;
margin: 0 auto;
}
/* Contribute Section - Alternating Layout */
.contribute-cards {
display: flex;
flex-direction: column;
gap: 4rem;
}
.contribute-row {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 3rem;
align-items: center;
}
.row-right {
grid-template-columns: 2fr 1fr;
}
.contribute-visual {
display: flex;
justify-content: center;
align-items: center;
}
.icon-box {
width: 150px;
height: 150px;
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), rgba(0, 255, 136, 0.05));
border: 2px solid var(--color-accent);
border-radius: 2rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
animation: pulse 3s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.contribute-content h3 {
color: var(--color-text);
margin-bottom: 1rem;
font-size: 1.75rem;
}
.contribute-content p {
color: var(--color-text-secondary);
line-height: 1.8;
margin-bottom: 1.5rem;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
color: var(--color-text-secondary);
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
}
.feature-list li::before {
content: '→';
position: absolute;
left: 0;
color: var(--color-accent);
}
/* Benefits Section */
.benefits-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.benefit-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 1rem;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
}
.benefit-card:hover {
transform: translateY(-5px);
border-color: var(--color-accent);
box-shadow: 0 10px 30px rgba(0, 255, 136, 0.1);
}
.benefit-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.benefit-card h4 {
color: var(--color-text);
margin-bottom: 0.5rem;
}
.benefit-card p {
color: var(--color-text-secondary);
font-size: 0.9rem;
}
/* Guidelines Section */
.guidelines-section {
background: var(--color-bg-secondary);
border-radius: 2rem;
padding: 3rem;
margin: 4rem 0;
}
.guidelines-content {
display: grid;
gap: 2rem;
}
.guideline {
display: flex;
gap: 1.5rem;
align-items: start;
}
.guideline-icon {
font-size: 2rem;
flex-shrink: 0;
}
.guideline strong {
display: block;
color: var(--color-text);
margin-bottom: 0.25rem;
}
.guideline p {
color: var(--color-text-secondary);
}
/* Tech Section */
.tech-info {
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
border-radius: 1rem;
padding: 2rem;
}
.tech-intro {
color: var(--color-text-secondary);
margin-bottom: 2rem;
text-align: center;
}
.tech-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.tech-item {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
}
.tech-item:hover {
border-color: var(--color-accent);
transform: translateY(-2px);
}
.tech-item code {
display: block;
color: var(--color-accent);
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.tech-item span {
color: var(--color-text-secondary);
font-size: 0.9rem;
}
/* CTA Section */
.cta-section {
text-align: center;
padding: 4rem;
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), transparent);
border-radius: 2rem;
}
.cta-content h2 {
margin-bottom: 1rem;
}
.cta-content p {
font-size: 1.2rem;
color: var(--color-text-secondary);
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.cta-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
/* Responsive */
@media (max-width: 768px) {
.hero-title {
font-size: 3rem;
}
.hero-subtitle {
font-size: 1.2rem;
}
section {
margin-bottom: 4rem;
}
.section-header {
flex-direction: column;
text-align: center;
}
h2 {
font-size: 2rem;
}
.contribute-row {
grid-template-columns: 1fr;
text-align: center;
}
.row-right {
grid-template-columns: 1fr;
}
.contribute-visual {
order: -1;
}
.icon-box {
width: 120px;
height: 120px;
font-size: 3rem;
}
.guidelines-section {
padding: 2rem;
}
.cta-section {
padding: 2rem;
}
}
</style>

View file

@ -1,397 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
---
<Layout title="Generiertes Spiel" fullWidth={true} hideFooter={true}>
<div class="game-container">
<div class="game-header">
<button id="backBtn" class="back-btn"> ← Zurück </button>
<h1 id="gameTitle">Lade Spiel...</h1>
<div class="game-actions">
<button id="editBtn" class="action-btn"> 📝 Bearbeiten </button>
<button id="fullscreenBtn" class="action-btn"> ⛶ Vollbild </button>
<button id="deleteBtn" class="action-btn danger"> 🗑️ Löschen </button>
</div>
</div>
<div class="game-frame-container">
<div id="loadingState" class="loading-state">
<div class="spinner"></div>
<p>Lade dein Spiel...</p>
</div>
<iframe id="gameFrame" class="game-iframe hidden" sandbox="allow-scripts allow-same-origin"
></iframe>
<div id="errorState" class="error-state hidden">
<p>❌ Spiel konnte nicht geladen werden</p>
<p class="error-detail">Das gesuchte Spiel wurde nicht gefunden.</p>
<a href="/" class="home-link">Zurück zur Startseite</a>
</div>
</div>
<div class="game-info">
<p id="gameDescription" class="game-description"></p>
<p id="gameDate" class="game-date"></p>
</div>
</div>
</Layout>
<script>
// Get game ID from URL parameters
const urlParams = new URLSearchParams(window.location.search);
const gameId = urlParams.get('id');
if (!gameId) {
window.location.href = '/';
}
// Game Storage
class GameStorage {
constructor() {
this.dbName = 'ManaGamesDB';
this.storeName = 'generatedGames';
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
store.createIndex('createdAt', 'createdAt', { unique: false });
store.createIndex('title', 'title', { unique: false });
}
};
});
}
async getGame(id) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(id);
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(request.error);
});
}
async deleteGame(id) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
// Initialize
const gameStorage = new GameStorage();
const loadingState = document.getElementById('loadingState');
const errorState = document.getElementById('errorState');
const gameFrame = document.getElementById('gameFrame');
const gameTitle = document.getElementById('gameTitle');
const gameDescription = document.getElementById('gameDescription');
const gameDate = document.getElementById('gameDate');
const backBtn = document.getElementById('backBtn');
const editBtn = document.getElementById('editBtn');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const deleteBtn = document.getElementById('deleteBtn');
// Load game
async function loadGame() {
try {
const game = await gameStorage.getGame(gameId);
if (!game) {
showError();
return;
}
// Update UI
document.title = `${game.title} - ManaGames`;
gameTitle.textContent = game.title;
gameDescription.textContent = game.description || game.prompt;
const date = new Date(game.createdAt).toLocaleDateString('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
gameDate.textContent = `Erstellt am ${date}`;
// Display game
const blob = new Blob([game.html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
gameFrame.src = url;
gameFrame.addEventListener(
'load',
() => {
URL.revokeObjectURL(url);
hideLoading();
},
{ once: true }
);
} catch (error) {
console.error('Error loading game:', error);
showError();
}
}
function hideLoading() {
loadingState.classList.add('hidden');
gameFrame.classList.remove('hidden');
}
function showError() {
loadingState.classList.add('hidden');
errorState.classList.remove('hidden');
}
// Event handlers
backBtn.addEventListener('click', () => {
window.history.back();
});
editBtn.addEventListener('click', () => {
// Store game ID in sessionStorage for the create page to load
sessionStorage.setItem('editGameId', gameId);
window.location.href = '/create';
});
fullscreenBtn.addEventListener('click', () => {
if (gameFrame.requestFullscreen) {
gameFrame.requestFullscreen();
}
});
deleteBtn.addEventListener('click', async () => {
if (confirm('Bist du sicher, dass du dieses Spiel löschen möchtest?')) {
try {
await gameStorage.deleteGame(gameId);
window.location.href = '/';
} catch (error) {
console.error('Error deleting game:', error);
alert('Fehler beim Löschen des Spiels');
}
}
});
// Load game on page load
loadGame();
</script>
<style>
.game-container {
height: calc(100vh - 60px);
display: flex;
flex-direction: column;
padding: 1rem;
max-width: 1400px;
margin: 0 auto;
}
.game-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
padding: 1rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
}
.back-btn {
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 0.5rem 1rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.back-btn:hover {
border-color: var(--color-accent);
transform: translateX(-2px);
}
#gameTitle {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text);
margin: 0;
}
.game-actions {
display: flex;
gap: 0.5rem;
}
.action-btn {
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 0.5rem 1rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.action-btn:hover {
background: var(--color-surface);
border-color: var(--color-accent);
}
.action-btn.danger {
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.action-btn.danger:hover {
background: rgba(239, 68, 68, 0.1);
border-color: #ef4444;
}
.game-frame-container {
flex: 1;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
overflow: hidden;
position: relative;
}
.game-iframe {
width: 100%;
height: 100%;
border: none;
}
.loading-state,
.error-state {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--color-bg);
}
.spinner {
width: 3rem;
height: 3rem;
border: 3px solid var(--color-border);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-state p,
.error-state p {
color: var(--color-text-secondary);
font-size: 1.1rem;
margin: 0.5rem 0;
}
.error-detail {
font-size: 0.9rem !important;
color: var(--color-text-muted) !important;
}
.home-link {
margin-top: 1rem;
background: var(--color-accent);
color: var(--color-bg);
padding: 0.75rem 2rem;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
transition: all 0.2s ease;
}
.home-link:hover {
background: var(--color-accent-hover);
transform: translateY(-1px);
}
.game-info {
margin-top: 1rem;
padding: 1rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
}
.game-description {
color: var(--color-text);
margin: 0 0 0.5rem 0;
}
.game-date {
color: var(--color-text-secondary);
font-size: 0.875rem;
margin: 0;
}
.hidden {
display: none !important;
}
@media (max-width: 768px) {
.game-container {
padding: 0.5rem;
}
.game-header {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.game-actions {
width: 100%;
justify-content: center;
}
#gameTitle {
font-size: 1.25rem;
}
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -1,754 +0,0 @@
---
import Layout from '../layouts/Layout.astro';
---
<Layout
title="Spiel einreichen - MANA Games"
description="Reiche dein eigenes Spiel für die MANA Games Community ein"
>
<main>
<div class="page-header">
<h1>Community Spiel einreichen</h1>
<p>Teile dein selbst erstelltes Spiel mit der MANA Games Community!</p>
</div>
<div class="submit-container">
<div class="guidelines">
<h2>📋 Richtlinien für Einreichungen</h2>
<ul>
<li>Das Spiel muss in einer einzelnen HTML-Datei funktionieren</li>
<li>Keine externen Abhängigkeiten (CDNs sind erlaubt)</li>
<li>Maximale Dateigröße: 1MB für HTML, 500KB für Screenshot</li>
<li>Das Spiel muss familienfreundlich sein</li>
<li>Kein urheberrechtlich geschütztes Material</li>
<li>Das Spiel sollte die postMessage API für Score-Integration nutzen</li>
</ul>
</div>
<form id="submitForm" class="submit-form">
<div class="form-section">
<h3>Spiel-Informationen</h3>
<div class="form-group">
<label for="gameTitle">Spiel-Titel *</label>
<input
type="text"
id="gameTitle"
name="gameTitle"
required
maxlength="50"
placeholder="z.B. Space Defender"
/>
</div>
<div class="form-group">
<label for="gameDescription">Beschreibung *</label>
<textarea
id="gameDescription"
name="gameDescription"
required
maxlength="200"
rows="3"
placeholder="Kurze Beschreibung deines Spiels..."></textarea>
</div>
<div class="form-group">
<label for="gameControls">Steuerung *</label>
<input
type="text"
id="gameControls"
name="gameControls"
required
placeholder="z.B. Pfeiltasten zum Bewegen, Leertaste zum Schießen"
/>
</div>
<div class="form-row">
<div class="form-group">
<label for="gameDifficulty">Schwierigkeit *</label>
<select id="gameDifficulty" name="gameDifficulty" required>
<option value="">Wähle...</option>
<option value="Einfach">Einfach</option>
<option value="Mittel">Mittel</option>
<option value="Schwer">Schwer</option>
</select>
</div>
<div class="form-group">
<label for="gameComplexity">Komplexität *</label>
<select id="gameComplexity" name="gameComplexity" required>
<option value="">Wähle...</option>
<option value="Minimal">Minimal</option>
<option value="Einfach">Einfach</option>
<option value="Mittel">Mittel</option>
<option value="Komplex">Komplex</option>
</select>
</div>
</div>
<div class="form-group">
<label for="gameTags">Tags (kommagetrennt) *</label>
<input
type="text"
id="gameTags"
name="gameTags"
required
placeholder="z.B. Arcade, Shooter, Retro"
/>
</div>
</div>
<div class="form-section">
<h3>Dateien hochladen</h3>
<div class="form-group">
<label for="htmlFile">HTML-Datei *</label>
<div class="file-upload">
<input type="file" id="htmlFile" name="htmlFile" accept=".html,.htm" required />
<div class="file-info">
<span class="file-name">Keine Datei ausgewählt</span>
<span class="file-size"></span>
</div>
</div>
<div class="file-preview" id="codePreview" style="display: none;">
<h4>Code-Vorschau:</h4>
<pre><code id="codeContent" /></pre>
</div>
</div>
<div class="form-group">
<label for="screenshotFile">Screenshot (JPG/PNG) *</label>
<div class="file-upload">
<input
type="file"
id="screenshotFile"
name="screenshotFile"
accept=".jpg,.jpeg,.png"
required
/>
<div class="file-info">
<span class="file-name">Keine Datei ausgewählt</span>
<span class="file-size"></span>
</div>
</div>
<div class="image-preview" id="imagePreview" style="display: none;">
<img id="previewImage" alt="Screenshot-Vorschau" />
</div>
</div>
</div>
<div class="form-section">
<h3>Deine Informationen</h3>
<div class="form-group">
<label for="authorName">Dein Name/Pseudonym *</label>
<input
type="text"
id="authorName"
name="authorName"
required
placeholder="Dein Name oder Pseudonym (wird öffentlich angezeigt)"
/>
</div>
<div class="form-group">
<label for="authorEmail">E-Mail (für Rückfragen)</label>
<input
type="email"
id="authorEmail"
name="authorEmail"
placeholder="deine.email@beispiel.de (nur für Rückfragen, nicht öffentlich)"
/>
</div>
<div class="form-group">
<label for="authorGithub">GitHub Username</label>
<input
type="text"
id="authorGithub"
name="authorGithub"
placeholder="Dein GitHub Username für Attribution (optional)"
/>
</div>
</div>
<div class="form-section">
<div class="form-group checkbox-group">
<label>
<input type="checkbox" id="acceptTerms" name="acceptTerms" required />
<span
>Ich bestätige, dass dieses Spiel meine eigene Arbeit ist und ich die Rechte
besitze, es zu teilen.</span
>
</label>
</div>
</div>
<div class="form-actions">
<button type="button" id="validateBtn" class="btn-secondary">
<span class="icon">🔍</span>
Validieren
</button>
<button type="submit" id="submitBtn" class="btn-primary" disabled>
<span class="icon">📤</span>
Einreichen
</button>
</div>
</form>
<div id="validationResults" class="validation-results" style="display: none;">
<h3>Validierungsergebnisse</h3>
<div class="validation-content"></div>
</div>
</div>
</main>
</Layout>
<style>
.submit-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.guidelines {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.guidelines h2 {
color: #00ff88;
margin-bottom: 1rem;
font-size: 1.2rem;
}
.guidelines ul {
list-style: none;
padding: 0;
}
.guidelines li {
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
}
.guidelines li::before {
content: '✓';
position: absolute;
left: 0;
color: #00ff88;
}
.submit-form {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 2rem;
}
.form-section {
margin-bottom: 2rem;
}
.form-section:last-child {
margin-bottom: 0;
}
.form-section h3 {
color: #00ff88;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #ffffff;
font-weight: 500;
}
.form-group input[type='text'],
.form-group input[type='email'],
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.75rem;
background: #0a0a0a;
border: 1px solid #2a2a2a;
border-radius: 4px;
color: #ffffff;
font-size: 1rem;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #00ff88;
box-shadow: 0 0 0 2px rgba(0, 255, 136, 0.1);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.file-upload {
position: relative;
background: #0a0a0a;
border: 2px dashed #2a2a2a;
border-radius: 4px;
padding: 1rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.file-upload:hover {
border-color: #00ff88;
background: rgba(0, 255, 136, 0.05);
}
.file-upload input[type='file'] {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
.file-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.file-name {
font-weight: 500;
}
.file-size {
font-size: 0.9rem;
color: #888;
}
.file-preview,
.image-preview {
margin-top: 1rem;
background: #0a0a0a;
border: 1px solid #2a2a2a;
border-radius: 4px;
padding: 1rem;
}
.file-preview h4 {
color: #00ff88;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.file-preview pre {
max-height: 200px;
overflow-y: auto;
background: #000;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
.image-preview img {
max-width: 100%;
max-height: 300px;
display: block;
margin: 0 auto;
border-radius: 4px;
}
.checkbox-group {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.checkbox-group input[type='checkbox'] {
margin-top: 0.25rem;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
.btn-primary,
.btn-secondary {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: #00ff88;
color: #0a0a0a;
}
.btn-primary:hover:not(:disabled) {
background: #00cc6a;
transform: translateY(-2px);
}
.btn-primary:disabled {
background: #2a2a2a;
color: #666;
cursor: not-allowed;
}
.btn-secondary {
background: #2a2a2a;
color: #ffffff;
}
.btn-secondary:hover {
background: #3a3a3a;
}
.validation-results {
margin-top: 2rem;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 1.5rem;
}
.validation-results h3 {
color: #00ff88;
margin-bottom: 1rem;
}
.validation-item {
padding: 0.5rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.validation-item.success {
color: #00ff88;
}
.validation-item.error {
color: #ff4444;
}
.validation-item.warning {
color: #ffaa00;
}
@media (max-width: 768px) {
.submit-container {
padding: 1rem;
}
.form-row {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.btn-primary,
.btn-secondary {
width: 100%;
justify-content: center;
}
}
</style>
<script>
// Form elements
const form = document.getElementById('submitForm') as HTMLFormElement;
const htmlFileInput = document.getElementById('htmlFile') as HTMLInputElement;
const screenshotFileInput = document.getElementById('screenshotFile') as HTMLInputElement;
const validateBtn = document.getElementById('validateBtn') as HTMLButtonElement;
const submitBtn = document.getElementById('submitBtn') as HTMLButtonElement;
const validationResults = document.getElementById('validationResults') as HTMLDivElement;
const validationContent = validationResults.querySelector(
'.validation-content'
) as HTMLDivElement;
// File preview elements
const codePreview = document.getElementById('codePreview') as HTMLDivElement;
const codeContent = document.getElementById('codeContent') as HTMLElement;
const imagePreview = document.getElementById('imagePreview') as HTMLDivElement;
const previewImage = document.getElementById('previewImage') as HTMLImageElement;
// Validation state
let isValidated = false;
let validationPassed = false;
// File handling
htmlFileInput.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
const fileInfo = htmlFileInput.parentElement?.querySelector('.file-info');
const fileName = fileInfo?.querySelector('.file-name');
const fileSize = fileInfo?.querySelector('.file-size');
if (fileName) fileName.textContent = file.name;
if (fileSize) fileSize.textContent = `${(file.size / 1024).toFixed(1)} KB`;
// Show code preview
const text = await file.text();
codeContent.textContent = text.substring(0, 1000) + (text.length > 1000 ? '...' : '');
codePreview.style.display = 'block';
// Reset validation
isValidated = false;
validationPassed = false;
submitBtn.disabled = true;
validationResults.style.display = 'none';
});
screenshotFileInput.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
const fileInfo = screenshotFileInput.parentElement?.querySelector('.file-info');
const fileName = fileInfo?.querySelector('.file-name');
const fileSize = fileInfo?.querySelector('.file-size');
if (fileName) fileName.textContent = file.name;
if (fileSize) fileSize.textContent = `${(file.size / 1024).toFixed(1)} KB`;
// Show image preview
const reader = new FileReader();
reader.onload = (e) => {
previewImage.src = e.target?.result as string;
imagePreview.style.display = 'block';
};
reader.readAsDataURL(file);
// Reset validation
isValidated = false;
validationPassed = false;
submitBtn.disabled = true;
validationResults.style.display = 'none';
});
// Validation
validateBtn.addEventListener('click', async () => {
const htmlFile = htmlFileInput.files?.[0];
const screenshotFile = screenshotFileInput.files?.[0];
if (!htmlFile || !screenshotFile) {
alert('Bitte lade beide Dateien hoch bevor du validierst.');
return;
}
validationContent.innerHTML = '<div class="validation-item">🔄 Validierung läuft...</div>';
validationResults.style.display = 'block';
const results: Array<{ type: 'success' | 'error' | 'warning'; message: string }> = [];
// File size checks
if (htmlFile.size > 1024 * 1024) {
results.push({ type: 'error', message: 'HTML-Datei ist zu groß (max. 1MB)' });
} else {
results.push({ type: 'success', message: 'HTML-Dateigröße OK' });
}
if (screenshotFile.size > 512 * 1024) {
results.push({ type: 'error', message: 'Screenshot ist zu groß (max. 500KB)' });
} else {
results.push({ type: 'success', message: 'Screenshot-Größe OK' });
}
// HTML content checks
const htmlContent = await htmlFile.text();
// Check for required game structure
if (htmlContent.includes('<canvas') || htmlContent.includes('<game')) {
results.push({ type: 'success', message: 'Spiel-Element gefunden' });
} else {
results.push({ type: 'warning', message: 'Kein Canvas-Element gefunden' });
}
// Check for postMessage integration
if (htmlContent.includes('postMessage')) {
results.push({ type: 'success', message: 'postMessage-Integration gefunden' });
} else {
results.push({
type: 'warning',
message: 'Keine postMessage-Integration gefunden (empfohlen für Scores)',
});
}
// Check for external dependencies
const externalScripts = (htmlContent.match(/<script[^>]+src=['"](https?:\/\/[^'"]+)/g) || [])
.length;
if (externalScripts > 0) {
results.push({ type: 'warning', message: `${externalScripts} externe Script(s) gefunden` });
} else {
results.push({ type: 'success', message: 'Keine externen Abhängigkeiten' });
}
// Check for potentially malicious code
const suspiciousPatterns = [
'eval(',
'document.write(',
'innerHTML =',
'.cookie',
'localStorage.clear()',
'XMLHttpRequest',
'fetch(',
];
const foundSuspicious = suspiciousPatterns.filter((pattern) => htmlContent.includes(pattern));
if (foundSuspicious.length > 0) {
results.push({
type: 'warning',
message: `Potenziell unsicherer Code gefunden: ${foundSuspicious.join(', ')}`,
});
} else {
results.push({ type: 'success', message: 'Keine verdächtigen Code-Muster gefunden' });
}
// Display results
validationContent.innerHTML = results
.map((result) => {
const icon = result.type === 'success' ? '✅' : result.type === 'error' ? '❌' : '⚠️';
return `<div class="validation-item ${result.type}">${icon} ${result.message}</div>`;
})
.join('');
// Check if validation passed
const hasErrors = results.some((r) => r.type === 'error');
validationPassed = !hasErrors;
isValidated = true;
submitBtn.disabled = !validationPassed;
if (validationPassed) {
validationContent.innerHTML +=
'<div class="validation-item success" style="margin-top: 1rem; font-weight: bold;">✅ Validierung erfolgreich! Du kannst dein Spiel einreichen.</div>';
} else {
validationContent.innerHTML +=
'<div class="validation-item error" style="margin-top: 1rem; font-weight: bold;">❌ Bitte behebe die Fehler vor dem Einreichen.</div>';
}
});
// Form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!isValidated || !validationPassed) {
alert('Bitte validiere dein Spiel zuerst!');
return;
}
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="icon">⏳</span> Wird eingereicht...';
try {
// Prepare form data
const formData = new FormData(form);
// Add files
const htmlFile = htmlFileInput.files?.[0];
const screenshotFile = screenshotFileInput.files?.[0];
if (!htmlFile || !screenshotFile) {
throw new Error('Dateien fehlen');
}
// Read file contents
const htmlContent = await htmlFile.text();
const screenshotBase64 = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as string);
reader.readAsDataURL(screenshotFile);
});
// Create submission data
const submission = {
title: formData.get('gameTitle'),
description: formData.get('gameDescription'),
controls: formData.get('gameControls'),
difficulty: formData.get('gameDifficulty'),
complexity: formData.get('gameComplexity'),
tags: (formData.get('gameTags') as string).split(',').map((t) => t.trim()),
author: {
name: formData.get('authorName'),
email: formData.get('authorEmail'),
github: formData.get('authorGithub'),
},
files: {
html: {
name: htmlFile.name,
content: htmlContent,
},
screenshot: {
name: screenshotFile.name,
content: screenshotBase64,
},
},
submittedAt: new Date().toISOString(),
};
// Send to backend API
const backendUrl = import.meta.env.PUBLIC_BACKEND_URL || 'http://localhost:3011';
const response = await fetch(`${backendUrl}/api/games/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(submission),
});
if (!response.ok) {
throw new Error('Einreichung fehlgeschlagen');
}
const result = await response.json();
// Show success message
alert(
`Vielen Dank! Dein Spiel "${submission.title}" wurde erfolgreich eingereicht. Du erhältst Updates über den Pull Request: ${result.prUrl}`
);
// Reset form
form.reset();
codePreview.style.display = 'none';
imagePreview.style.display = 'none';
validationResults.style.display = 'none';
isValidated = false;
validationPassed = false;
} catch (error) {
console.error('Submission error:', error);
alert('Fehler beim Einreichen. Bitte versuche es später erneut.');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = '<span class="icon">📤</span> Einreichen';
}
});
</script>

View file

@ -0,0 +1,292 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { PillNavigation, CommandBar, SyncIndicator } from '@manacore/shared-ui';
import type {
PillNavItem,
PillDropdownItem,
CommandBarItem,
QuickAction,
} from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { games, getGameBySlug } from '$lib/data/games';
import {
THEME_DEFINITIONS,
DEFAULT_THEME_VARIANTS,
EXTENDED_THEME_VARIANTS,
} from '@manacore/shared-theme';
import type { ThemeVariant } from '@manacore/shared-theme';
import { filterHiddenNavItems } from '@manacore/shared-theme';
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { getPillAppItems } from '@manacore/shared-branding';
import { setLocale, supportedLocales } from '$lib/i18n';
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import { gamesStore } from '$lib/data/local-store';
import {
tagLocalStore,
tagMutations,
useAllTags as useAllSharedTags,
} from '@manacore/shared-stores';
const allTags = useAllSharedTags();
let showGuestWelcome = $state(false);
function initGuestWelcome() {
if (!authStore.isAuthenticated && shouldShowGuestWelcome('mana-games')) {
showGuestWelcome = true;
}
}
const appItems = getPillAppItems('mana-games');
let { children } = $props();
let commandBarOpen = $state(false);
const commandBarQuickActions: QuickAction[] = [
{ id: 'home', label: 'Alle Spiele', icon: 'gamepad-2', href: '/', shortcut: '1' },
{ id: 'create', label: 'Spiel erstellen', icon: 'sparkles', href: '/create', shortcut: '2' },
{ id: 'community', label: 'Community', icon: 'users', href: '/community', shortcut: '3' },
{ id: 'stats', label: 'Statistiken', icon: 'bar-chart-3', href: '/stats', shortcut: '4' },
{ id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' },
];
async function handleCommandBarSearch(query: string): Promise<CommandBarItem[]> {
if (!query.trim()) return [];
const queryLower = query.toLowerCase();
return games
.filter(
(g) =>
g.title.toLowerCase().includes(queryLower) ||
g.tags.some((t) => t.toLowerCase().includes(queryLower))
)
.slice(0, 10)
.map((g) => ({
id: `game-${g.slug}`,
title: g.title,
subtitle: g.tags.join(', '),
}));
}
function handleCommandBarSelect(item: CommandBarItem) {
const slug = item.id.replace('game-', '');
goto(`/play/${slug}`);
}
let isCollapsed = $state(false);
let isDark = $derived(theme.isDark);
let pinnedThemes = $derived<ThemeVariant[]>(
(userSettings.theme?.pinnedThemes || []).filter((t): t is ThemeVariant =>
EXTENDED_THEME_VARIANTS.includes(t as ThemeVariant)
)
);
let visibleThemes = $derived<ThemeVariant[]>([...DEFAULT_THEME_VARIANTS, ...pinnedThemes]);
let themeVariantItems = $derived<PillDropdownItem[]>([
...visibleThemes.map((variant) => ({
id: variant,
label: THEME_DEFINITIONS[variant]?.label || variant,
icon: THEME_DEFINITIONS[variant]?.icon || '🎨',
onClick: () => theme.setVariant(variant),
active: (theme.variant || 'lume') === variant,
})),
{
id: 'all-themes',
label: 'Alle Themes',
icon: 'palette',
onClick: () => goto('/themes'),
active: false,
},
]);
let currentThemeVariantLabel = $derived(
THEME_DEFINITIONS[theme.variant]?.label || THEME_DEFINITIONS.lume?.label || 'Lume'
);
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
let userEmail = $derived(authStore.isAuthenticated ? authStore.user?.email || 'Menü' : '');
const baseNavItems: PillNavItem[] = [
{ href: '/', label: 'Spiele', icon: 'gamepad-2' },
{ href: '/create', label: 'Erstellen', icon: 'sparkles' },
{ href: '/community', label: 'Community', icon: 'users' },
{ href: '/stats', label: 'Statistiken', icon: 'bar-chart-3' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
];
const navItems = $derived(
filterHiddenNavItems('mana-games', baseNavItems, userSettings.nav?.hiddenNavItems || {})
);
function handleKeydown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault();
commandBarOpen = true;
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
collapsedStore.set(collapsed);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('mana-games-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
theme.toggleMode();
}
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
theme.setMode(mode);
}
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
async function handleAuthReady() {
await Promise.all([gamesStore.initialize(), tagLocalStore.initialize()]);
if (authStore.isAuthenticated) {
const getToken = () => authStore.getValidToken();
gamesStore.startSync(getToken);
tagMutations.startSync(getToken);
}
const savedCollapsed = localStorage.getItem('mana-games-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
collapsedStore.set(true);
}
initGuestWelcome();
if (authStore.isAuthenticated) {
await userSettings.load();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Mana Games"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#00ff88"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
themesHref="/themes"
helpHref="/help"
allAppsHref="/apps"
/>
<main class="main-content bg-background">
<div class="content-wrapper">
{@render children()}
</div>
</main>
<CommandBar
bind:open={commandBarOpen}
onClose={() => (commandBarOpen = false)}
onSearch={handleCommandBarSearch}
onSelect={handleCommandBarSelect}
quickActions={commandBarQuickActions}
placeholder="Spiel suchen..."
emptyText="Keine Ergebnisse"
searchingText="Suche..."
/>
</div>
<GuestWelcomeModal
appId="mana-games"
visible={showGuestWelcome}
onClose={() => (showGuestWelcome = false)}
onLogin={() => goto('/login')}
onRegister={() => goto('/register')}
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
/>
{#if authStore.isAuthenticated}
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
{/if}
<SyncIndicator />
</AuthGate>
<style>
.layout-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
position: relative;
z-index: 0;
padding-bottom: 100px;
}
.content-wrapper {
max-width: 100%;
margin-left: auto;
margin-right: auto;
padding: 1rem;
position: relative;
z-index: 0;
}
@media (min-width: 640px) {
.content-wrapper {
padding: 1.5rem;
}
}
@media (min-width: 1024px) {
.content-wrapper {
padding: 2rem;
}
}
</style>

View file

@ -0,0 +1,83 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { games, getAllTags } from '$lib/data/games';
import GameCard from '$lib/components/GameCard.svelte';
let searchQuery = $state('');
let selectedTag = $state<string | null>(null);
const allTags = getAllTags();
let filteredGames = $derived(() => {
let result = games;
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
result = result.filter(
(g) =>
g.title.toLowerCase().includes(q) ||
g.description.toLowerCase().includes(q) ||
g.tags.some((t) => t.toLowerCase().includes(q))
);
}
if (selectedTag) {
result = result.filter((g) => g.tags.includes(selectedTag!));
}
return result;
});
</script>
<svelte:head>
<title>{$_('app.name')} - {$_('home.title')}</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">{$_('home.title')}</h1>
<p class="text-muted-foreground mt-1">{$_('home.subtitle')}</p>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<input
type="text"
bind:value={searchQuery}
placeholder={$_('home.search')}
class="flex-1 rounded-lg border border-border bg-background px-4 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div class="flex flex-wrap gap-2">
<button
class="text-xs px-3 py-1.5 rounded-full transition-colors {selectedTag === null
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
onclick={() => (selectedTag = null)}
>
{$_('home.allGames')}
</button>
{#each allTags as tag}
<button
class="text-xs px-3 py-1.5 rounded-full transition-colors {selectedTag === tag
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
onclick={() => (selectedTag = selectedTag === tag ? null : tag)}
>
{tag}
</button>
{/each}
</div>
{#if filteredGames().length === 0}
<div class="text-center py-12">
<p class="text-muted-foreground">{$_('home.noResults')}</p>
</div>
{:else}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{#each filteredGames() as game (game.id)}
<GameCard {game} href="/play/{game.slug}" />
{/each}
</div>
{/if}
</div>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
</script>
<svelte:head>
<title>{$_('nav.community')} - Mana Games</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">{$_('nav.community')}</h1>
<p class="text-muted-foreground mt-1">
Von der Community erstellte Spiele. Reiche dein eigenes Spiel ein!
</p>
</div>
<div class="text-center py-12 rounded-xl border border-dashed border-border">
<p class="text-4xl mb-4">🎮</p>
<p class="text-muted-foreground">Noch keine Community-Spiele vorhanden.</p>
<a
href="/create"
class="inline-block mt-4 px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
Erstelle das erste Spiel!
</a>
</div>
</div>

View file

@ -0,0 +1,211 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { generatedGameCollection } from '$lib/data/local-store';
const BACKEND_URL = import.meta.env.DEV
? 'http://localhost:3011'
: import.meta.env.PUBLIC_MANA_GAMES_BACKEND_URL || '';
const models = [
{ id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', provider: 'Google', speed: 'Schnell' },
{ id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', provider: 'Google', speed: 'Schnell' },
{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', provider: 'Google', speed: 'Langsam' },
{ id: 'claude-3.5-haiku', label: 'Claude 3.5 Haiku', provider: 'Anthropic', speed: 'Schnell' },
{ id: 'claude-3.5-sonnet', label: 'Claude Sonnet', provider: 'Anthropic', speed: 'Mittel' },
{ id: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'Azure', speed: 'Schnell' },
{ id: 'gpt-4o', label: 'GPT-4o', provider: 'Azure', speed: 'Mittel' },
];
let prompt = $state('');
let selectedModel = $state('gemini-2.0-flash');
let isGenerating = $state(false);
let generatedHtml = $state('');
let error = $state('');
let iterationCount = $state(0);
let originalPrompt = $state('');
async function generateGame() {
if (!prompt.trim() || isGenerating) return;
isGenerating = true;
error = '';
try {
const body: Record<string, unknown> = {
description: prompt,
model: selectedModel,
mode: iterationCount > 0 ? 'iterate' : 'create',
};
if (iterationCount > 0 && generatedHtml) {
body.originalPrompt = originalPrompt;
body.currentCode = generatedHtml;
body.iterationCount = iterationCount;
}
const response = await fetch(`${BACKEND_URL}/api/games/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Fehler: ${response.status} ${response.statusText}`);
}
const data = await response.json();
if (data.success && data.html) {
generatedHtml = data.html;
if (iterationCount === 0) {
originalPrompt = prompt;
}
iterationCount++;
} else {
error = data.error || 'Unbekannter Fehler bei der Generierung.';
}
} catch (err) {
error = err instanceof Error ? err.message : 'Verbindungsfehler zum Backend.';
} finally {
isGenerating = false;
}
}
async function saveGame() {
if (!generatedHtml || !prompt) return;
await generatedGameCollection.insert({
title: originalPrompt || prompt,
description: prompt,
htmlCode: generatedHtml,
prompt: originalPrompt || prompt,
model: selectedModel,
iterationCount,
});
// Reset
prompt = '';
generatedHtml = '';
iterationCount = 0;
originalPrompt = '';
}
function resetGame() {
generatedHtml = '';
iterationCount = 0;
originalPrompt = '';
prompt = '';
error = '';
}
</script>
<svelte:head>
<title>{$_('create.title')} - Mana Games</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">{$_('create.title')}</h1>
<p class="text-muted-foreground mt-1">{$_('create.subtitle')}</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Input Panel -->
<div class="space-y-4">
<div>
<label for="prompt" class="block text-sm font-medium text-foreground mb-2">
{$_('create.prompt')}
</label>
<textarea
id="prompt"
bind:value={prompt}
placeholder={$_('create.promptPlaceholder')}
rows="4"
class="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"
></textarea>
</div>
<div>
<label for="model" class="block text-sm font-medium text-foreground mb-2">
{$_('create.model')}
</label>
<select
id="model"
bind:value={selectedModel}
class="w-full rounded-lg border border-border bg-background px-4 py-2.5 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
>
{#each models as model}
<option value={model.id}>
{model.label} ({model.provider} - {model.speed})
</option>
{/each}
</select>
</div>
{#if error}
<div class="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
{error}
</div>
{/if}
<div class="flex gap-2">
<button
onclick={generateGame}
disabled={!prompt.trim() || isGenerating}
class="flex-1 px-4 py-2.5 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{#if isGenerating}
<span class="inline-flex items-center gap-2">
<span class="animate-spin"></span>
{$_('create.generating')}
</span>
{:else if iterationCount > 0}
{$_('create.iterate')}
{:else}
{$_('create.generate')}
{/if}
</button>
{#if generatedHtml}
<button
onclick={saveGame}
class="px-4 py-2.5 rounded-lg border border-border bg-card text-foreground hover:bg-muted transition-colors"
>
{$_('create.save')}
</button>
<button
onclick={resetGame}
class="px-4 py-2.5 rounded-lg border border-border bg-card text-muted-foreground hover:bg-muted transition-colors"
>
Neu
</button>
{/if}
</div>
{#if iterationCount > 0}
<p class="text-xs text-muted-foreground">
Iteration {iterationCount} &middot; Beschreibe Änderungen im Prompt-Feld
</p>
{/if}
</div>
<!-- Preview Panel -->
<div class="rounded-xl border border-border bg-black overflow-hidden">
{#if generatedHtml}
<iframe
srcdoc={generatedHtml}
title="Generiertes Spiel"
class="w-full aspect-[16/10] border-0"
sandbox="allow-scripts"
></iframe>
{:else}
<div class="w-full aspect-[16/10] flex items-center justify-center">
<div class="text-center">
<p class="text-4xl mb-3 opacity-40">🎮</p>
<p class="text-muted-foreground text-sm">{$_('create.preview')}</p>
</div>
</div>
{/if}
</div>
</div>
</div>

View file

@ -0,0 +1,84 @@
<script lang="ts">
import { useAllGeneratedGames } from '$lib/data/queries';
import { generatedGameCollection } from '$lib/data/local-store';
const generatedGames = useAllGeneratedGames();
let selectedGameId = $state<string | null>(null);
let selectedGame = $derived(generatedGames.value.find((g) => g.id === selectedGameId));
async function deleteGame(id: string) {
await generatedGameCollection.remove(id);
if (selectedGameId === id) {
selectedGameId = null;
}
}
</script>
<svelte:head>
<title>Generierte Spiele - Mana Games</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Generierte Spiele</h1>
<p class="text-muted-foreground mt-1">Deine mit KI erstellten Spiele</p>
</div>
{#if generatedGames.value.length === 0}
<div class="text-center py-12 rounded-xl border border-dashed border-border">
<p class="text-4xl mb-4"></p>
<p class="text-muted-foreground">Noch keine generierten Spiele.</p>
<a
href="/create"
class="inline-block mt-4 px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
Erstelle dein erstes Spiel!
</a>
</div>
{:else}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="space-y-2 lg:col-span-1">
{#each generatedGames.value as game (game.id)}
<button
onclick={() => (selectedGameId = game.id)}
class="w-full text-left rounded-lg border p-3 transition-colors {selectedGameId ===
game.id
? 'border-primary bg-primary/5'
: 'border-border bg-card hover:bg-muted/50'}"
>
<p class="font-medium text-foreground text-sm truncate">{game.title}</p>
<p class="text-xs text-muted-foreground mt-1">
{game.model} &middot; {game.iterationCount} Iterationen
</p>
</button>
{/each}
</div>
<div class="lg:col-span-2 rounded-xl border border-border bg-black overflow-hidden">
{#if selectedGame}
<div class="flex items-center justify-between px-3 py-2 bg-card border-b border-border">
<span class="text-sm text-foreground truncate">{selectedGame.title}</span>
<button
onclick={() => deleteGame(selectedGame!.id)}
class="text-xs text-red-400 hover:text-red-300 transition-colors"
>
Löschen
</button>
</div>
<iframe
srcdoc={selectedGame.htmlCode}
title={selectedGame.title}
class="w-full aspect-[16/10] border-0"
sandbox="allow-scripts"
></iframe>
{:else}
<div class="w-full aspect-[16/10] flex items-center justify-center">
<p class="text-muted-foreground text-sm">Wähle ein Spiel aus der Liste</p>
</div>
{/if}
</div>
</div>
{/if}
</div>

View file

@ -0,0 +1,132 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount, onDestroy } from 'svelte';
import { _ } from 'svelte-i18n';
import { getGameBySlug } from '$lib/data/games';
import { initGameCommunication } from '$lib/services/game-communication';
import { gameStatsCollection, type LocalGameStats } from '$lib/data/local-store';
const slug = $derived($page.params.slug);
const game = $derived(getGameBySlug(slug));
let stats = $state<LocalGameStats | null>(null);
let isFullscreen = $state(false);
let iframeEl: HTMLIFrameElement;
let cleanup: (() => void) | undefined;
onMount(async () => {
if (!slug) return;
cleanup = initGameCommunication(slug);
const all = await gameStatsCollection.getAll();
stats = all.find((s) => s.gameId === slug) || null;
});
onDestroy(() => {
cleanup?.();
});
function toggleFullscreen() {
if (!iframeEl) return;
if (!document.fullscreenElement) {
iframeEl.requestFullscreen();
isFullscreen = true;
} else {
document.exitFullscreen();
isFullscreen = false;
}
}
function formatPlayTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
</script>
<svelte:head>
<title>{game?.title || 'Spiel'} - Mana Games</title>
</svelte:head>
{#if game}
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<a href="/" class="text-muted-foreground hover:text-foreground transition-colors">
&larr; {$_('game.back')}
</a>
<h1 class="text-xl font-bold text-foreground">{game.title}</h1>
</div>
<div class="flex gap-2">
<button
onclick={toggleFullscreen}
class="px-3 py-1.5 text-sm rounded-lg border border-border bg-card text-foreground hover:bg-muted transition-colors"
>
{$_('game.fullscreen')}
</button>
</div>
</div>
<div class="rounded-xl overflow-hidden border border-border bg-black">
<iframe
bind:this={iframeEl}
src={game.htmlFile}
title={game.title}
class="w-full aspect-[16/10] border-0"
sandbox="allow-scripts allow-same-origin"
></iframe>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="rounded-xl border border-border bg-card p-4 space-y-3">
<h2 class="font-semibold text-foreground">{game.title}</h2>
<p class="text-sm text-muted-foreground">{game.description}</p>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.difficulty')}</span>
<span class="text-foreground">{game.difficulty}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.controls')}</span>
<span class="text-foreground text-right max-w-[60%]">{game.controls}</span>
</div>
</div>
<div class="flex flex-wrap gap-1 pt-2">
{#each game.tags as tag}
<span class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
{tag}
</span>
{/each}
</div>
</div>
{#if stats}
<div class="rounded-xl border border-border bg-card p-4 space-y-3">
<h2 class="font-semibold text-foreground">{$_('game.stats')}</h2>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.highScore')}</span>
<span class="text-foreground font-mono">{stats.highScore.toLocaleString()}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.gamesPlayed')}</span>
<span class="text-foreground">{stats.gamesPlayed}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.totalPlayTime')}</span>
<span class="text-foreground">{formatPlayTime(stats.totalPlayTime)}</span>
</div>
</div>
</div>
{/if}
</div>
</div>
{:else}
<div class="text-center py-12">
<p class="text-muted-foreground">Spiel nicht gefunden.</p>
<a href="/" class="text-primary hover:underline mt-2 inline-block">Zurück zur Übersicht</a>
</div>
{/if}

View file

@ -0,0 +1,95 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { useAllGameStats } from '$lib/data/queries';
import { games } from '$lib/data/games';
const allStats = useAllGameStats();
let totalGamesPlayed = $derived(allStats.value.reduce((sum, s) => sum + s.gamesPlayed, 0));
let totalPlayTime = $derived(allStats.value.reduce((sum, s) => sum + s.totalPlayTime, 0));
let favoriteGame = $derived(() => {
if (allStats.value.length === 0) return null;
const top = allStats.value.reduce((fav, s) => (s.gamesPlayed > fav.gamesPlayed ? s : fav));
return games.find((g) => g.slug === top.gameId || g.id === top.gameId);
});
let sortedStats = $derived([...allStats.value].sort((a, b) => b.highScore - a.highScore));
function formatPlayTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
function getGameTitle(gameId: string): string {
return games.find((g) => g.slug === gameId || g.id === gameId)?.title || gameId;
}
</script>
<svelte:head>
<title>{$_('stats.title')} - Mana Games</title>
</svelte:head>
<div class="space-y-6">
<h1 class="text-2xl font-bold text-foreground">{$_('stats.title')}</h1>
{#if allStats.value.length === 0}
<div class="text-center py-12">
<p class="text-4xl mb-4">📊</p>
<p class="text-muted-foreground">{$_('stats.noStats')}</p>
</div>
{:else}
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="rounded-xl border border-border bg-card p-4 text-center">
<p class="text-3xl font-bold text-primary">{totalGamesPlayed}</p>
<p class="text-sm text-muted-foreground mt-1">{$_('stats.totalGames')}</p>
</div>
<div class="rounded-xl border border-border bg-card p-4 text-center">
<p class="text-3xl font-bold text-primary">{formatPlayTime(totalPlayTime)}</p>
<p class="text-sm text-muted-foreground mt-1">{$_('stats.totalTime')}</p>
</div>
<div class="rounded-xl border border-border bg-card p-4 text-center">
<p class="text-3xl font-bold text-primary">{favoriteGame()?.title || '-'}</p>
<p class="text-sm text-muted-foreground mt-1">{$_('stats.favoriteGame')}</p>
</div>
</div>
<div class="rounded-xl border border-border bg-card overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-border text-left">
<th class="px-4 py-3 text-muted-foreground font-medium">Spiel</th>
<th class="px-4 py-3 text-muted-foreground font-medium text-right"
>{$_('game.highScore')}</th
>
<th class="px-4 py-3 text-muted-foreground font-medium text-right hidden sm:table-cell"
>{$_('game.gamesPlayed')}</th
>
<th class="px-4 py-3 text-muted-foreground font-medium text-right hidden md:table-cell"
>{$_('game.totalPlayTime')}</th
>
</tr>
</thead>
<tbody>
{#each sortedStats as stat (stat.id)}
<tr class="border-b border-border/50 hover:bg-muted/30 transition-colors">
<td class="px-4 py-3 text-foreground">{getGameTitle(stat.gameId)}</td>
<td class="px-4 py-3 text-foreground font-mono text-right"
>{stat.highScore.toLocaleString()}</td
>
<td class="px-4 py-3 text-muted-foreground text-right hidden sm:table-cell"
>{stat.gamesPlayed}</td
>
<td class="px-4 py-3 text-muted-foreground text-right hidden md:table-cell"
>{formatPlayTime(stat.totalPlayTime)}</td
>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>

View file

@ -0,0 +1,39 @@
<script lang="ts">
import '../app.css';
import { onMount } from 'svelte';
import { isLoading as isLocaleLoading } from 'svelte-i18n';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { waitLocale } from '$lib/i18n';
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
let { children } = $props();
let loading = $state(true);
onMount(() => {
const cleanupErrorHandler = setupGlobalErrorHandler();
const init = async () => {
await waitLocale();
theme.initialize();
await authStore.initialize();
loading = false;
};
init();
return cleanupErrorHandler;
});
</script>
<ToastContainer />
{#if $isLocaleLoading || loading}
<AppLoadingSkeleton />
{:else}
<div class="min-h-screen bg-background text-foreground">
{@render children()}
</div>
{/if}

View file

@ -0,0 +1,2 @@
// Disable SSR — all data is local-first (IndexedDB + mana-sync)
export const ssr = false;

View file

@ -1,71 +0,0 @@
import { statsService } from '../services/statsService';
export interface GameMessage {
type: 'GAME_EVENT' | 'GAME_LOADED' | 'GAME_ENDED';
gameId: string;
event?: string;
data?: any;
}
export function initGameCommunication(gameSlug: string) {
let gameStartTime: number | null = null;
window.addEventListener('message', (event) => {
if (event.origin !== window.location.origin) return;
const message = event.data as GameMessage;
if (!message.type || message.gameId !== gameSlug) return;
switch (message.type) {
case 'GAME_LOADED':
gameStartTime = Date.now();
statsService.incrementGamesPlayed(gameSlug);
break;
case 'GAME_EVENT':
handleGameEvent(gameSlug, message.event!, message.data);
break;
case 'GAME_ENDED':
if (gameStartTime) {
const playTime = Math.floor((Date.now() - gameStartTime) / 1000);
statsService.addPlayTime(gameSlug, playTime);
gameStartTime = null;
}
break;
}
});
window.addEventListener('beforeunload', () => {
if (gameStartTime) {
const playTime = Math.floor((Date.now() - gameStartTime) / 1000);
statsService.addPlayTime(gameSlug, playTime);
}
});
}
function handleGameEvent(gameId: string, event: string, data: any) {
switch (event) {
case 'SCORE_UPDATE':
if (data.score) {
statsService.updateStats(gameId, {
lastScore: data.score,
});
}
break;
case 'ACHIEVEMENT_UNLOCKED':
if (data.achievement) {
statsService.unlockAchievement(gameId, data.achievement);
}
break;
case 'GAME_OVER':
if (data.score) {
statsService.updateStats(gameId, {
lastScore: data.score,
});
}
break;
}
}

View file

@ -1,77 +0,0 @@
// Template für Stats-Integration in Spiele
// Dieses Template zeigt, wie man die Stats-Integration in ein Spiel einbaut
// 1. Game ID definieren (muss mit dem Slug in games.ts übereinstimmen)
const GAME_ID = 'dein-spiel-slug';
// 2. Beim Spielstart senden
window.addEventListener('load', () => {
window.parent.postMessage(
{
type: 'GAME_LOADED',
gameId: GAME_ID,
},
'*'
);
});
// 3. Bei Score-Updates senden
function updateScore(newScore) {
score = newScore;
// UI Update...
window.parent.postMessage(
{
type: 'GAME_EVENT',
gameId: GAME_ID,
event: 'SCORE_UPDATE',
data: { score: score },
},
'*'
);
}
// 4. Bei Game Over senden
function gameOver() {
// Game Over Logik...
window.parent.postMessage(
{
type: 'GAME_EVENT',
gameId: GAME_ID,
event: 'GAME_OVER',
data: { score: finalScore },
},
'*'
);
// Achievement Beispiele
if (score >= 100) {
window.parent.postMessage(
{
type: 'GAME_EVENT',
gameId: GAME_ID,
event: 'ACHIEVEMENT_UNLOCKED',
data: {
achievement: {
id: 'first-100',
name: 'Erste 100',
description: '100 Punkte erreicht!',
},
},
},
'*'
);
}
}
// 5. Optional: Bei Spielende/Verlassen
window.addEventListener('beforeunload', () => {
window.parent.postMessage(
{
type: 'GAME_ENDED',
gameId: GAME_ID,
},
'*'
);
});

View file

@ -1,192 +0,0 @@
export interface GameStats {
gameId: string;
highScore: number;
lastScore: number;
gamesPlayed: number;
totalPlayTime: number;
lastPlayed: string;
achievements?: Achievement[];
}
export interface Achievement {
id: string;
name: string;
description: string;
unlockedAt?: string;
}
export interface GlobalStats {
totalGamesPlayed: number;
totalPlayTime: number;
favoriteGame?: string;
lastPlayedGame?: string;
gamesWithStats: number;
}
class StatsService {
private readonly STATS_KEY = 'mana-games-stats';
private getStoredStats(): Record<string, GameStats> {
if (typeof window === 'undefined') return {};
try {
const stored = localStorage.getItem(this.STATS_KEY);
return stored ? JSON.parse(stored) : {};
} catch (error) {
console.error('Error reading stats:', error);
return {};
}
}
private saveStats(stats: Record<string, GameStats>): void {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(this.STATS_KEY, JSON.stringify(stats));
} catch (error) {
console.error('Error saving stats:', error);
}
}
getStats(gameId: string): GameStats | null {
const allStats = this.getStoredStats();
return allStats[gameId] || null;
}
updateStats(gameId: string, update: Partial<GameStats>): void {
const allStats = this.getStoredStats();
const currentStats = allStats[gameId] || {
gameId,
highScore: 0,
lastScore: 0,
gamesPlayed: 0,
totalPlayTime: 0,
lastPlayed: new Date().toISOString(),
achievements: [],
};
allStats[gameId] = {
...currentStats,
...update,
gameId,
lastPlayed: new Date().toISOString(),
};
if (update.lastScore && update.lastScore > currentStats.highScore) {
allStats[gameId].highScore = update.lastScore;
}
this.saveStats(allStats);
}
getAllStats(): Record<string, GameStats> {
return this.getStoredStats();
}
getGlobalStats(): GlobalStats {
const allStats = this.getStoredStats();
const statsArray = Object.values(allStats);
if (statsArray.length === 0) {
return {
totalGamesPlayed: 0,
totalPlayTime: 0,
gamesWithStats: 0,
};
}
const totalGamesPlayed = statsArray.reduce((sum, stat) => sum + stat.gamesPlayed, 0);
const totalPlayTime = statsArray.reduce((sum, stat) => sum + stat.totalPlayTime, 0);
const favoriteGame = statsArray.reduce((fav, stat) =>
!fav || stat.gamesPlayed > fav.gamesPlayed ? stat : fav
).gameId;
const lastPlayedGame = statsArray.reduce((last, stat) =>
!last || new Date(stat.lastPlayed) > new Date(last.lastPlayed) ? stat : last
).gameId;
return {
totalGamesPlayed,
totalPlayTime,
favoriteGame,
lastPlayedGame,
gamesWithStats: statsArray.length,
};
}
incrementGamesPlayed(gameId: string): void {
const stats = this.getStats(gameId) || {
gameId,
highScore: 0,
lastScore: 0,
gamesPlayed: 0,
totalPlayTime: 0,
lastPlayed: new Date().toISOString(),
};
this.updateStats(gameId, {
gamesPlayed: stats.gamesPlayed + 1,
});
}
addPlayTime(gameId: string, seconds: number): void {
const stats = this.getStats(gameId) || {
gameId,
highScore: 0,
lastScore: 0,
gamesPlayed: 0,
totalPlayTime: 0,
lastPlayed: new Date().toISOString(),
};
this.updateStats(gameId, {
totalPlayTime: stats.totalPlayTime + seconds,
});
}
unlockAchievement(gameId: string, achievement: Achievement): void {
const stats = this.getStats(gameId);
if (!stats) return;
const achievements = stats.achievements || [];
const exists = achievements.find((a) => a.id === achievement.id);
if (!exists) {
achievements.push({
...achievement,
unlockedAt: new Date().toISOString(),
});
this.updateStats(gameId, { achievements });
}
}
formatPlayTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
}
getRelativeTime(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Gerade eben';
if (diffMins < 60) return `Vor ${diffMins} Minuten`;
if (diffHours < 24) return `Vor ${diffHours} Stunden`;
if (diffDays < 7) return `Vor ${diffDays} Tagen`;
return date.toLocaleDateString('de-DE');
}
}
export const statsService = new StatsService();

View file

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more