diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 734b59e..a94a0ec 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1589,7 +1589,7 @@ const SEARCH_TOOL_INDEX = [ {type:'tool',label:'Whiteboard',icon:'πŸ–Š',sub:'Werkzeuge',id:'whiteboard',keywords:['whiteboard','zeichnen','canvas','board','draw','skizze','malen','zeichen']}, {type:'tool',label:'Dev-Tools',icon:'πŸ”§',sub:'Werkzeuge',id:'devtools',keywords:['entwickler','developer','tools','werkzeuge']}, {type:'tool',label:'Paywall-Killer',icon:'πŸ”“',sub:'Werkzeuge',id:'paywallkiller',keywords:['paywall','archiv','archive','artikel','zeitung','bild','waz','heise','spiegel','zeit','bypass','umgehen','lesen','gesperrt','bezahlschranke']}, - {type:'tool',label:'Gebietseroberung',icon:'πŸ—ΊοΈ',sub:'Freizeit',id:'gebietseroberung',keywords:['spiel','game','gebietseroberung','multiplayer','gebiet','strategie','raster','einladen','duell']}, + {type:'tool',label:'Hex Wars',icon:'⬑',sub:'Freizeit',id:'gebietseroberung',keywords:['hex wars','spiel','game','multiplayer','gebiet','strategie','raster','einladen','duell','gebietseroberung']}, {type:'tool',label:'Media',icon:'🎬',sub:'Freizeit',id:'media',keywords:['kino','film','movie','streaming','demnΓ€chst','favoriten','cinema']}, {type:'tool',label:'Kino – Aktuell',icon:'🎬',sub:'Media',id:'media',keywords:['kino','kinocharts','charts','laufend','now playing']}, {type:'tool',label:'Kino – DemnΓ€chst',icon:'πŸ—“',sub:'Media',id:'media',keywords:['demnΓ€chst','neustart','upcoming','vorschau','kino']}, diff --git a/frontend/src/toolRegistry.js b/frontend/src/toolRegistry.js index 5f5fe0d..e97fb26 100644 --- a/frontend/src/toolRegistry.js +++ b/frontend/src/toolRegistry.js @@ -66,8 +66,8 @@ export const TOOLS = [ { id: 'gebietseroberung', Icon: GeoIcon, - label: 'Gebietseroberung', - navLabel: 'Gebiete', + label: 'Hex Wars', + navLabel: 'Hex Wars', group: 'Freizeit', component: Gebietseroberung, }, diff --git a/frontend/src/tools/gebietseroberung.jsx b/frontend/src/tools/gebietseroberung.jsx index b114968..0fdb6d7 100644 --- a/frontend/src/tools/gebietseroberung.jsx +++ b/frontend/src/tools/gebietseroberung.jsx @@ -2,51 +2,58 @@ import { useState, useEffect, useCallback } from 'react'; import { api, S } from '../lib.js'; const GRID = 20; -const OWNER_COLOR = '#4ecdc4'; -const OPP_COLOR = '#ff6b9d'; -const NEUTRAL_COLOR = 'rgba(255,255,255,0.06)'; -const HOVER_COLOR = 'rgba(255,255,255,0.18)'; +const MY_COLOR = '#4ecdc4'; +const OPP_COLOR = '#ff6b9d'; + +// User-ID aus JWT lesen (kein API-Call nΓΆtig) +function getMyId() { + try { + const token = localStorage.getItem('sk_token'); + if (!token) return null; + return JSON.parse(atob(token.split('.')[1])).id || null; + } catch { return null; } +} // ── SpielerklΓ€rung ──────────────────────────────────────────────────────────── function HelpModal({ onClose }) { return (
-
+
- πŸ—ΊοΈ Wie funktioniert's? + ⬑ Wie funktioniert's? - +

- Dein Ziel: Am Ende mehr Felder als dein Gegner besitzen. + Dein Ziel: Am Ende mehr Felder als dein Gegner besitzen.

So geht's:
- Jeder Spieler startet in einer Ecke des 20Γ—20 Rasters mit einem Startfeld. - Abwechselnd wΓ€hlt jeder eine neutrale (graue) Zelle, - die direkt an sein eigenes Gebiet grenzt β€” oben, unten, links oder rechts. - Diese Zelle gehΓΆrt dann ihm. + Jeder Spieler startet in einer Ecke des 20Γ—20 Rasters. Abwechselnd wΓ€hlst du eine + neutrale (graue) Zelle, + die direkt an dein Gebiet grenzt β€” oben, unten, links oder rechts.

Taktik:
- Breite dich schnell aus β€” aber pass auf: Wenn du deinen Gegner - einmauerst, kann er nicht mehr wachsen. - Blockieren ist genauso wichtig wie Expandieren! + Breite dich aus β€” aber mauere deinen Gegner + ein, bevor er dich einsperrt! + Blockieren ist genauso wichtig wie Expandieren.

Ende:
- Wenn keine neutralen Felder mehr erreichbar sind, gewinnt wer - mehr Felder besitzt. Du bekommst eine Pushover-Benachrichtigung, - wenn du dran bist. + Wenn niemand mehr expandieren kann, gewinnt wer mehr Felder hat. + Du kriegst eine Pushover-Benachrichtigung wenn du dran bist.

-
- πŸ’‘ Klicke auf ein leuchtendes Feld um deinen Zug zu machen. Nur Felder neben deinem Gebiet sind wΓ€hlbar. +
+ πŸ’‘ Leuchtende Felder sind anklickbar β€” nur Felder neben deinem Gebiet.
@@ -69,7 +76,7 @@ function NewGameModal({ onClose, onCreated, toast }) { setLoading(true); try { const res = await api('/tools/gebietseroberung', { body: { opponent_id: Number(oppId) } }); - toast('Spiel erstellt! Dein Gegner wurde benachrichtigt πŸ—ΊοΈ'); + toast('Spiel erstellt! Dein Gegner wurde benachrichtigt ⬑'); onCreated(res.id); } catch(e) { toast(e.message || 'Fehler beim Erstellen', 'error'); @@ -89,11 +96,7 @@ function NewGameModal({ onClose, onCreated, toast }) {
- setOppId(e.target.value)} style={{ ...S.inp, marginBottom:16 }}> {users.map(u => )} @@ -110,115 +113,102 @@ function NewGameModal({ onClose, onCreated, toast }) { // ── Spielfeld ───────────────────────────────────────────────────────────────── function GameBoard({ game, myId, onMove, toast }) { - const [hover, setHover] = useState(null); + const [hover, setHover] = useState(null); const [moving, setMoving] = useState(false); - const grid = JSON.parse(game.grid); + const grid = JSON.parse(game.grid); const isMyTurn = game.current_turn === myId; const opponent = game.owner_id === myId ? game.opponent_id : game.owner_id; - const myColor = game.owner_id === myId ? OWNER_COLOR : OPP_COLOR; - const oppColor = game.owner_id === myId ? OPP_COLOR : OWNER_COLOR; + const myColor = game.owner_id === myId ? MY_COLOR : OPP_COLOR; + const oppColor = game.owner_id === myId ? OPP_COLOR : MY_COLOR; + const oppName = game.owner_id === myId ? game.opp_name : game.owner_name; - const isAdjacent = (r, c) => { - const dirs = [[-1,0],[1,0],[0,-1],[0,1]]; - return dirs.some(([dr,dc]) => { - const nr = r+dr, nc = c+dc; - return nr>=0 && nr=0 && nc [[-1,0],[1,0],[0,-1],[0,1]].some(([dr,dc]) => { + const nr = r+dr, nc = c+dc; + return nr>=0 && nr=0 && nc isMyTurn && game.status === 'active' && grid[r][c] === 0 && isAdjacent(r, c); + const canClick = (r, c) => + isMyTurn && game.status === 'active' && grid[r][c] === 0 && isAdj(r, c); const handleClick = async (r, c) => { if (!canClick(r, c) || moving) return; setMoving(true); - try { - await onMove(r, c); - } finally { setMoving(false); } + try { await onMove(r, c); } finally { setMoving(false); } }; const myCount = grid.flat().filter(v => v === myId).length; const oppCount = grid.flat().filter(v => v === opponent).length; const neutral = grid.flat().filter(v => v === 0).length; - // Gewinner-Banner let banner = null; if (game.status === 'finished') { - if (!game.winner_id) { - banner = { text: 'Unentschieden! 🀝', color: '#ffe66d' }; - } else if (game.winner_id === myId) { - banner = { text: 'Du hast gewonnen! πŸŽ‰', color: '#4ecdc4' }; - } else { - banner = { text: 'Du hast verloren.', color: '#ff6b9d' }; - } + if (!game.winner_id) banner = { text:'Unentschieden! 🀝', color:'#ffe66d' }; + else if (game.winner_id===myId) banner = { text:'Du hast gewonnen! πŸŽ‰', color:MY_COLOR }; + else banner = { text:'Du hast verloren.', color:OPP_COLOR }; } - // Grid-Zellengrâße responsiv - const isMob = window.innerWidth < 600; - const cellSize = isMob ? Math.floor((window.innerWidth - 48) / GRID) : 24; + // Zellgrâße: passt sich ans Viewport an, nie horizontales Scrollen + const vw = Math.min(window.innerWidth, 600); + const cellSize = Math.floor((vw - 48) / GRID); // 48px = padding + gap return (
- {/* Status-Leiste */} -
-
-
DU
-
{myCount}
+ {/* Scoreboard */} +
+
+
DU
+
{myCount}
-
- {neutral} neutral +
+ {neutral}
frei
-
-
- {game.owner_id === myId ? game.opp_name : game.owner_name} -
-
{oppCount}
+
+
{oppName}
+
{oppCount}
- {/* Gewinner-Banner */} + {/* Banner */} {banner && (
- {banner.text} -
+ }}>{banner.text}
)} {/* Zug-Hinweis */} {game.status === 'active' && (
- {isMyTurn ? (moving ? 'Zug wird gespeichert…' : 'β–Ά Dein Zug β€” klicke ein angrenzendes Feld') : '⏳ Warte auf Gegner…'} + {isMyTurn + ? (moving ? 'Zug wird gespeichert…' : 'β–Ά Dein Zug') + : `⏳ ${oppName} ist dran…`}
)} - {/* Grid */} + {/* Grid β€” immer passend ohne Scrollen */}
{grid.map((row, r) => row.map((cell, c) => { - const key = `${r}-${c}`; - const clickable = canClick(r, c); - const isHovered = hover === key && clickable; - let bg = NEUTRAL_COLOR; - if (cell === myId) bg = `${myColor}55`; + const key = `${r}-${c}`; + const clic = canClick(r, c); + const hov = hover === key && clic; + let bg = 'rgba(255,255,255,0.05)'; + if (cell === myId) bg = `${myColor}55`; if (cell === opponent) bg = `${oppColor}55`; - if (isHovered) bg = HOVER_COLOR; - if (clickable && !isHovered) bg = `${myColor}25`; - + if (clic && !hov) bg = `${myColor}22`; + if (hov) bg = `${myColor}66`; return (
setHover(key)} onMouseLeave={() => setHover(null)} style={{ - width: cellSize, height: cellSize, - background: bg, - borderRadius: 2, - cursor: clickable ? 'pointer' : 'default', - transition: 'background 0.1s', + width:cellSize, height:cellSize, + background:bg, borderRadius:1, + cursor: clic ? 'pointer' : 'default', + transition:'background 0.08s', + border: clic ? `1px solid ${myColor}55` : '1px solid transparent', boxSizing:'border-box', - border: clickable ? `1px solid ${myColor}66` : '1px solid transparent', }} /> ); @@ -249,30 +238,25 @@ function GameList({ games, myId, onSelect, onNew }) { const GameRow = ({ g }) => { const isMyTurn = g.status === 'active' && g.current_turn === myId; - const opp = g.owner_id === myId ? g.opp_name : g.owner_name; - const grid = JSON.parse(g.grid); + const opp = g.owner_id === myId ? g.opp_name : g.owner_name; + const grid = JSON.parse(g.grid); const myCount = grid.flat().filter(v => v === myId).length; const oppCount = grid.flat().filter(v => v !== myId && v !== 0).length; let result = null; if (g.status === 'finished') { - if (!g.winner_id) result = { label:'Unentschieden', color:'#ffe66d' }; - else if (g.winner_id === myId) result = { label:'Gewonnen πŸŽ‰', color:'#4ecdc4' }; - else result = { label:'Verloren', color:'#ff6b9d' }; + if (!g.winner_id) result = { label:'Unentschieden', color:'#ffe66d' }; + else if (g.winner_id === myId) result = { label:'Gewonnen πŸŽ‰', color:MY_COLOR }; + else result = { label:'Verloren', color:OPP_COLOR }; } return ( - - {active.length > 0 && ( - <> -
LAUFENDE SPIELE ({active.length})
- {active.map(g => )} - - )} - {finished.length > 0 && ( - <> -
BEENDETE SPIELE
- {finished.map(g => )} - - )} + {active.length > 0 && <> +
LAUFEND ({active.length})
+ {active.map(g => )} + } + {finished.length > 0 && <> +
BEENDET
+ {finished.map(g => )} + } {games.length === 0 && (
- Noch keine Spiele. Starte eines! + Noch keine Spiele β€” starte eines!
)}
@@ -316,27 +296,23 @@ function GameList({ games, myId, onSelect, onNew }) { } // ── Hauptkomponente ─────────────────────────────────────────────────────────── -export default function Gebietseroberung({ toast }) { - const [games, setGames] = useState([]); - const [activeId, setActiveId] = useState(null); - const [game, setGame] = useState(null); - const [myId, setMyId] = useState(null); - const [showNew, setShowNew] = useState(false); - const [showHelp, setShowHelp] = useState(false); - const [loading, setLoading] = useState(true); +export default function HexWars({ toast }) { + const [games, setGames] = useState([]); + const [activeId, setActiveId] = useState(null); + const [game, setGame] = useState(null); + const [showNew, setShowNew] = useState(false); + const [showHelp, setShowHelp] = useState(false); + const [loading, setLoading] = useState(true); - // Eigene User-ID aus JWT ermitteln - useEffect(() => { - api('/auth/me').then(u => setMyId(u.id)).catch(() => {}); - }, []); + // User-ID direkt aus JWT β€” kein API-Call + const myId = getMyId(); const loadGames = useCallback(async () => { try { const gs = await api('/tools/gebietseroberung'); setGames(gs); - } catch(e) { - toast?.('Fehler beim Laden', 'error'); - } finally { setLoading(false); } + } catch { toast?.('Fehler beim Laden', 'error'); } + finally { setLoading(false); } }, []); useEffect(() => { loadGames(); }, [loadGames]); @@ -347,42 +323,32 @@ export default function Gebietseroberung({ toast }) { api(`/tools/gebietseroberung/${activeId}`).then(setGame).catch(() => {}); }, [activeId]); - // Polling wenn aktives Spiel lΓ€uft und Gegner dran ist + // Polling wenn Gegner dran ist useEffect(() => { if (!activeId || !game || !myId) return; if (game.status !== 'active' || game.current_turn === myId) return; const iv = setInterval(async () => { const g = await api(`/tools/gebietseroberung/${activeId}`).catch(() => null); - if (g) { - setGame(g); - if (g.current_turn === myId) { - toast?.('Du bist dran! πŸ—ΊοΈ'); - } - } + if (!g) return; + setGame(g); + if (g.current_turn === myId) toast?.('Du bist dran! ⬑'); }, 10000); return () => clearInterval(iv); }, [activeId, game, myId]); const handleMove = async (row, col) => { - try { - const updated = await api(`/tools/gebietseroberung/${activeId}/move`, { body: { row, col } }); - setGame(updated); - // Spielliste auch aktualisieren - setGames(prev => prev.map(g => g.id === updated.id ? { ...g, ...updated } : g)); - } catch(e) { - toast?.(e.message || 'Fehler beim Zug', 'error'); - } + const updated = await api(`/tools/gebietseroberung/${activeId}/move`, { body: { row, col } }); + setGame(updated); + setGames(prev => prev.map(g => g.id === updated.id ? { ...g, ...updated } : g)); }; const handleResign = async () => { if (!window.confirm('Wirklich aufgeben?')) return; - try { - await api(`/tools/gebietseroberung/${activeId}/resign`, { body: {} }); - const updated = await api(`/tools/gebietseroberung/${activeId}`); - setGame(updated); - setGames(prev => prev.map(g => g.id === updated.id ? { ...g, ...updated } : g)); - toast('Du hast aufgegeben.'); - } catch(e) { toast?.(e.message, 'error'); } + await api(`/tools/gebietseroberung/${activeId}/resign`, { body: {} }); + const updated = await api(`/tools/gebietseroberung/${activeId}`); + setGame(updated); + setGames(prev => prev.map(g => g.id === updated.id ? { ...g, ...updated } : g)); + toast('Du hast aufgegeben.'); }; if (loading) return ( @@ -392,38 +358,28 @@ export default function Gebietseroberung({ toast }) { ); return ( -
+
{/* Header */} -
+
{activeId && ( )} -

- πŸ—ΊοΈ GEBIETSEROBERUNG -

+ ⬑ HEX WARS {activeId && game?.status === 'active' && ( )}
- {/* Inhalt */} - {!activeId ? ( - setActiveId(id)} - onNew={() => setShowNew(true)} - /> - ) : ( - game && myId + {!activeId + ? setShowNew(true)} /> + : game && myId ? :
Lade Spiel…
- )} + } - {/* Modals */} {showNew && ( setShowNew(false)} diff --git a/frontend/src/tools/linkliste.jsx b/frontend/src/tools/linkliste.jsx index 7ddefee..bfd862c 100644 --- a/frontend/src/tools/linkliste.jsx +++ b/frontend/src/tools/linkliste.jsx @@ -16,7 +16,7 @@ const TOOL_ENTRIES = [ { url:'tool://linkliste', label:'πŸ”— Linkliste', group:'Werkzeuge' }, { url:'tool://codeschnipsel', label:' Code-Schnipsel', group:'Werkzeuge' }, { url:'tool://media', label:'🎬 Media / Kino', group:'Freizeit' }, - { url:'tool://gebietseroberung', label:'πŸ—ΊοΈ Gebietseroberung', group:'Freizeit' }, + { url:'tool://gebietseroberung', label:'⬑ Hex Wars', group:'Freizeit' }, { url:'tool://devtools', label:'πŸ”§ Dev-Tools (alle)', group:'Dev-Tools' }, { url:'tool://devtools?sub=cron', label:'⏱ Crontab', group:'Dev-Tools' }, { url:'tool://devtools?sub=elektro', label:'⚑ Elektro', group:'Dev-Tools' },