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>
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
||||
|
|
@ -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,
|
||||
];
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -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 |
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
|
@ -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 });
|
||||
});
|
||||
}
|
||||
});
|
||||
10
games/mana-games/apps/web/src/app.css
Normal 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";
|
||||
12
games/mana-games/apps/web/src/app.html
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>© 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
54
games/mana-games/apps/web/src/lib/components/GameCard.svelte
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
|
||||
|
|
@ -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();
|
||||
}
|
||||
1
games/mana-games/apps/web/src/lib/data/guest-seed.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
// No guest seed data needed — games are static HTML files, stats build up from play
|
||||
55
games/mana-games/apps/web/src/lib/data/local-store.ts
Normal 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');
|
||||
24
games/mana-games/apps/web/src/lib/data/queries.ts
Normal 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[]);
|
||||
}
|
||||
38
games/mana-games/apps/web/src/lib/i18n/index.ts
Normal 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 };
|
||||
66
games/mana-games/apps/web/src/lib/i18n/locales/de.json
Normal 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"
|
||||
}
|
||||
}
|
||||
66
games/mana-games/apps/web/src/lib/i18n/locales/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
114
games/mana-games/apps/web/src/lib/services/game-communication.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
games/mana-games/apps/web/src/lib/stores/auth.svelte.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createManaAuthStore } from '@manacore/shared-auth-stores';
|
||||
|
||||
export const authStore = createManaAuthStore({
|
||||
devBackendPort: 3011,
|
||||
});
|
||||
5
games/mana-games/apps/web/src/lib/stores/navigation.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createSimpleNavigationStores } from '@manacore/shared-stores';
|
||||
|
||||
export const { isNavCollapsed } = createSimpleNavigationStores({
|
||||
storageKey: 'mana-games',
|
||||
});
|
||||
6
games/mana-games/apps/web/src/lib/stores/theme.svelte.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { createThemeStore } from '@manacore/shared-theme';
|
||||
|
||||
export const theme = createThemeStore({
|
||||
appId: 'mana-games',
|
||||
defaultVariant: 'lume',
|
||||
});
|
||||
|
|
@ -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(),
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
292
games/mana-games/apps/web/src/routes/(app)/+layout.svelte
Normal 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>
|
||||
83
games/mana-games/apps/web/src/routes/(app)/+page.svelte
Normal 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>
|
||||
|
|
@ -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>
|
||||
211
games/mana-games/apps/web/src/routes/(app)/create/+page.svelte
Normal 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} · 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>
|
||||
|
|
@ -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} · {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>
|
||||
|
|
@ -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">
|
||||
← {$_('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}
|
||||
|
|
@ -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>
|
||||
39
games/mana-games/apps/web/src/routes/+layout.svelte
Normal 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}
|
||||
2
games/mana-games/apps/web/src/routes/+layout.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Disable SSR — all data is local-first (IndexedDB + mana-sync)
|
||||
export const ssr = false;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 385 B |