From d9cc24f4a8cc2dd7014e6b46a035abc49f7d1d1d Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Thu, 23 Jul 2026 00:40:57 +0200 Subject: [PATCH] =?UTF-8?q?Redesign=20Webinterface:=20Voxel-Game-=C3=84sth?= =?UTF-8?q?etik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Selbst gehostete Fonts (Inter Variable, JetBrains Mono) — kein CDN - Voxel-Würfel-Logo (SVG), Grid-Hintergrund mit grünem Glow, Gradient-Hero - Devlogs als Timeline mit Voxel-Markern, Datums-Chips und Relativzeit - Commits als Zeilen-Liste mit SHA-, Repo- und Branch-Chips statt Tabelle - Discord-Login-Button mit Logo, User-Chip mit Avatar im Header - Mobile-Layout (Karten, umbrechende Commit-Zeilen) - Im Browser verifiziert: Desktop + Mobile, Devlogs + Commits (mit Admin-Session) Co-Authored-By: Claude Opus 4.8 --- frontend/package-lock.json | 20 ++ frontend/package.json | 2 + frontend/src/App.jsx | 52 ++++- frontend/src/main.jsx | 2 + frontend/src/pages/Commits.jsx | 62 +++--- frontend/src/pages/Devlogs.jsx | 55 +++-- frontend/src/style.css | 357 ++++++++++++++++++++++++--------- 7 files changed, 395 insertions(+), 155 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2598d31..43f671d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "ecobot-frontend", "version": "0.1.0", "dependencies": { + "@fontsource-variable/inter": "^5.3.0", + "@fontsource/jetbrains-mono": "^5.3.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.1.0" @@ -741,6 +743,24 @@ "node": ">=18" } }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7b69420..df3d1bb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,8 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource-variable/inter": "^5.3.0", + "@fontsource/jetbrains-mono": "^5.3.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.1.0" diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d5ed5e9..23b7d0d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,6 +4,28 @@ import { apiGet } from './api.js'; import Devlogs from './pages/Devlogs.jsx'; import Commits from './pages/Commits.jsx'; +// Isometrischer Voxel-Würfel als Logo +function VoxelLogo() { + return ( + + ); +} + +function DiscordMark() { + return ( + + ); +} + export default function App() { // Login-Status einmal beim Laden holen const [me, setMe] = useState({ user: null, admin: false, loading: true }); @@ -16,26 +38,35 @@ export default function App() { return (
+ ); diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index a363ed3..45f745f 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -2,6 +2,8 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import App from './App.jsx'; +import '@fontsource-variable/inter'; +import '@fontsource/jetbrains-mono/400.css'; import './style.css'; ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/frontend/src/pages/Commits.jsx b/frontend/src/pages/Commits.jsx index f11a3a6..d791f00 100644 --- a/frontend/src/pages/Commits.jsx +++ b/frontend/src/pages/Commits.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { apiGet } from '../api.js'; const dateFmt = new Intl.DateTimeFormat('de-DE', { - day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', + day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', }); export default function Commits({ me }) { @@ -14,6 +14,7 @@ export default function Commits({ me }) { if (!me.admin) return; setError(null); apiGet(`/api/commits?page=${page}`).then(setData).catch((e) => setError(e.status)); + window.scrollTo(0, 0); }, [page, me.admin]); // Zugriffs-Zustände: nicht eingeloggt / eingeloggt aber kein Admin @@ -21,12 +22,12 @@ export default function Commits({ me }) { if (!me.user) { return (
-

Der Commit-Feed ist privat.

- Login mit Discord +

🔒 Der Commit-Feed ist privat.

+ Login mit Discord
); } - if (!me.admin) return

Kein Zugriff — der Commit-Feed ist privat.

; + if (!me.admin) return

🔒 Kein Zugriff — der Commit-Feed ist privat.

; if (error) return

Commits konnten nicht geladen werden ({error}).

; if (!data) return

Lade …

; @@ -36,40 +37,27 @@ export default function Commits({ me }) { return ( <> -

Commits

-

{data.total} archivierte Commits

+
+

Commits

+

{data.total} archivierte Commits über alle Repos

+
-
- - - - - - - - - - - - {data.items.map((c) => ( - - - - - - - - ))} - -
SHAMessageRepoBranchZeit
- {c.url ? ( - - {c.sha.slice(0, 7)} - - ) : ( - {c.sha.slice(0, 7)} - )} - {c.message.split('\n')[0]}{c.repo}{c.branch}{dateFmt.format(new Date(c.committed_at))}
+
+ {data.items.map((c) => ( +
+ {c.url ? ( + + {c.sha.slice(0, 7)} + + ) : ( + {c.sha.slice(0, 7)} + )} + {c.message.split('\n')[0]} + {c.repo.split('/').pop()} + {c.branch} + {dateFmt.format(new Date(c.committed_at))} +
+ ))}
{pages > 1 && ( diff --git a/frontend/src/pages/Devlogs.jsx b/frontend/src/pages/Devlogs.jsx index 3af92d1..40330eb 100644 --- a/frontend/src/pages/Devlogs.jsx +++ b/frontend/src/pages/Devlogs.jsx @@ -6,6 +6,16 @@ const dateFmt = new Intl.DateTimeFormat('de-DE', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric', }); +/** Relative Angabe wie „vor 3 Tagen" für die Karten-Ecke */ +function relative(iso) { + const days = Math.floor((Date.now() - new Date(iso).getTime()) / 86400000); + if (days <= 0) return 'heute'; + if (days === 1) return 'gestern'; + if (days < 30) return `vor ${days} Tagen`; + const months = Math.floor(days / 30); + return months === 1 ? 'vor 1 Monat' : `vor ${months} Monaten`; +} + export default function Devlogs() { const [data, setData] = useState(null); const [page, setPage] = useState(1); @@ -14,27 +24,42 @@ export default function Devlogs() { useEffect(() => { setError(false); apiGet(`/api/devlogs?page=${page}`).then(setData).catch(() => setError(true)); + window.scrollTo(0, 0); }, [page]); - if (error) return

Devlogs konnten nicht geladen werden.

; - if (!data) return

Lade …

; - if (data.total === 0) return

Noch keine Devlogs archiviert.

; - - const pages = Math.ceil(data.total / data.pageSize); + const pages = data ? Math.ceil(data.total / data.pageSize) : 0; return ( <> -

Devlogs

-

{data.total} Einträge aus der Entwicklung von EcoGame

+
+

Devlog

+

+ Die Entwicklung von EcoGame — einem Voxel-Sandbox-Survival + in Unreal Engine 5. Frisch aus dem Editor, Tag für Tag. +

+
- {data.items.map((d) => ( -
-
{dateFmt.format(new Date(d.posted_at))}
-
- -
-
- ))} + {error &&

Devlogs konnten nicht geladen werden.

} + {!error && !data &&

Lade …

} + {data?.total === 0 &&

Noch keine Devlogs archiviert.

} + + {data?.total > 0 && ( +
+ {data.items.map((d) => ( +
+
+ + {dateFmt.format(new Date(d.posted_at))} + + {relative(d.posted_at)} +
+
+ +
+
+ ))} +
+ )} {pages > 1 && (
diff --git a/frontend/src/style.css b/frontend/src/style.css index 9d6d5f8..1d252c7 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,161 +1,330 @@ -/* EcoGame Webinterface — dunkles Theme, EcoGame-Grün als Akzent */ +/* EcoGame Webinterface — Voxel-Game-Ästhetik, dunkles Theme */ :root { - --bg: #0f1412; - --bg-card: #171e1a; - --bg-header: #131916; - --border: #263129; - --text: #e4ece6; - --text-dim: #93a297; - --accent: #2e8b57; - --accent-bright: #3fae6f; + --bg: #0b0f0d; + --bg-raised: #111713; + --bg-card: rgba(22, 30, 25, 0.75); + --border: rgba(255, 255, 255, 0.07); + --border-strong: rgba(255, 255, 255, 0.12); + --text: #edf3ee; + --text-dim: #8fa396; + --text-faint: #5c6e63; + --green-100: #4ade80; + --green-200: #22c55e; + --green-300: #15803d; + --discord: #5865f2; + --discord-hover: #6a76f5; + --font: 'Inter Variable', system-ui, sans-serif; + --mono: 'JetBrains Mono', ui-monospace, monospace; } * { box-sizing: border-box; } +html { color-scheme: dark; } body { margin: 0; background: var(--bg); color: var(--text); - font-family: system-ui, 'Segoe UI', sans-serif; - line-height: 1.6; + font-family: var(--font); + line-height: 1.65; + font-size: 15.5px; + -webkit-font-smoothing: antialiased; } -.app { min-height: 100vh; display: flex; flex-direction: column; } +.app { min-height: 100vh; display: flex; flex-direction: column; position: relative; } -/* Header */ +/* Sanfter grüner Glow oben + feines Grid als Spiel-Referenz */ +.bg-glow { + position: absolute; + inset: 0; + pointer-events: none; + background: + radial-gradient(600px 320px at 50% -80px, rgba(34, 197, 94, 0.14), transparent 70%), + linear-gradient(rgba(255, 255, 255, 0.015) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.015) 1px, transparent 1px); + background-size: auto, 32px 32px, 32px 32px; + mask-image: linear-gradient(to bottom, black 0%, black 40%, transparent 100%); + -webkit-mask-image: linear-gradient(to bottom, black 0%, black 40%, transparent 100%); + z-index: 0; +} + +/* ---------- Header ---------- */ .header { - background: var(--bg-header); - border-bottom: 1px solid var(--border); position: sticky; top: 0; + z-index: 10; + background: rgba(11, 15, 13, 0.85); + border-bottom: 1px solid var(--border); } .header-inner { - max-width: 880px; + max-width: 820px; margin: 0 auto; - padding: 0.7rem 1rem; + padding: 0.75rem 1.25rem; display: flex; align-items: center; - gap: 1.5rem; + gap: 2rem; } .brand { - font-weight: 700; - font-size: 1.15rem; - color: var(--text); - text-decoration: none; display: flex; align-items: center; - gap: 0.5rem; + gap: 0.6rem; + text-decoration: none; + color: var(--text); } -.brand-dot { - width: 10px; height: 10px; - border-radius: 50%; - background: var(--accent-bright); - box-shadow: 0 0 8px var(--accent-bright); -} -.nav { display: flex; gap: 1rem; flex: 1; } +.logo { width: 26px; height: 26px; } +.brand-name { font-weight: 800; font-size: 1.1rem; letter-spacing: -0.02em; } +.brand-accent { color: var(--green-100); } + +.nav { display: flex; gap: 0.4rem; flex: 1; } .nav a { color: var(--text-dim); text-decoration: none; - padding: 0.25rem 0; - border-bottom: 2px solid transparent; + font-weight: 500; + font-size: 0.92rem; + padding: 0.35rem 0.85rem; + border-radius: 99px; + transition: color 0.15s, background 0.15s; } .nav a:hover { color: var(--text); } -.nav a.active { color: var(--text); border-bottom-color: var(--accent); } +.nav a.active { color: var(--green-100); background: rgba(34, 197, 94, 0.1); } -.auth { display: flex; align-items: center; gap: 0.6rem; } -.avatar { width: 28px; height: 28px; border-radius: 50%; } -.username { color: var(--text-dim); font-size: 0.9rem; } +/* ---------- Auth ---------- */ +.auth { display: flex; align-items: center; } +.user-chip { + display: flex; + align-items: center; + gap: 0.55rem; + background: var(--bg-raised); + border: 1px solid var(--border); + border-radius: 99px; + padding: 0.25rem 0.7rem 0.25rem 0.3rem; +} +.avatar { width: 26px; height: 26px; border-radius: 50%; } +.username { color: var(--text-dim); font-size: 0.85rem; font-weight: 500; } +.logout { + color: var(--text-faint); + text-decoration: none; + font-size: 0.8rem; + padding: 0 0.15rem; +} +.logout:hover { color: var(--text); } -/* Buttons */ +/* ---------- Buttons ---------- */ .btn { - display: inline-block; - background: var(--accent); - color: #fff; + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: var(--green-200); + color: #04120a; border: none; - border-radius: 6px; - padding: 0.45rem 0.9rem; + border-radius: 8px; + padding: 0.5rem 1rem; + font-family: var(--font); font-size: 0.9rem; + font-weight: 600; text-decoration: none; cursor: pointer; + transition: background 0.15s, transform 0.1s; } -.btn:hover:not(:disabled) { background: var(--accent-bright); } -.btn:disabled { opacity: 0.4; cursor: default; } -.btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text-dim); } -.btn-ghost:hover { color: var(--text); background: transparent; } +.btn:hover:not(:disabled) { background: var(--green-100); transform: translateY(-1px); } +.btn:disabled { opacity: 0.35; cursor: default; } + +.btn-discord { background: var(--discord); color: #fff; } +.btn-discord:hover:not(:disabled) { background: var(--discord-hover); } +.discord-mark { width: 18px; height: 18px; } + +/* ---------- Layout ---------- */ +.main { + flex: 1; + max-width: 820px; + width: 100%; + margin: 0 auto; + padding: 2.5rem 1.25rem 4rem; + position: relative; + z-index: 1; +} + +.hero { margin-bottom: 2.25rem; } +.hero h1 { + margin: 0 0 0.4rem; + font-size: 2.3rem; + font-weight: 800; + letter-spacing: -0.03em; + background: linear-gradient(100deg, var(--text) 30%, var(--green-100)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.hero p { margin: 0; color: var(--text-dim); max-width: 34rem; } +.hero strong { color: var(--text); } -/* Inhalt */ -.main { flex: 1; max-width: 880px; width: 100%; margin: 0 auto; padding: 1.5rem 1rem 3rem; } -h1 { margin: 0.5rem 0 0.2rem; font-size: 1.6rem; } -.subtitle { color: var(--text-dim); margin: 0 0 1.5rem; font-size: 0.95rem; } .notice { color: var(--text-dim); padding: 2rem 0; } -.notice.center { text-align: center; } -.notice .btn { margin-top: 0.5rem; } +.notice.center { text-align: center; padding: 4rem 0; } +.notice.center p { margin: 0 0 1rem; font-size: 1.05rem; } + +/* ---------- Devlog-Timeline ---------- */ +.timeline { + display: flex; + flex-direction: column; + gap: 1.4rem; + position: relative; + padding-left: 1.4rem; +} +.timeline::before { + content: ''; + position: absolute; + left: 4px; + top: 8px; + bottom: 8px; + width: 2px; + background: linear-gradient(to bottom, var(--green-300), rgba(21, 128, 61, 0.08)); + border-radius: 2px; +} -/* Devlog-Karten */ .card { + position: relative; background: var(--bg-card); border: 1px solid var(--border); - border-left: 3px solid var(--accent); - border-radius: 8px; - padding: 1rem 1.25rem; - margin-bottom: 1.25rem; + border-radius: 14px; + padding: 1.15rem 1.4rem 1.05rem; + transition: border-color 0.2s, transform 0.15s; +} +.card:hover { border-color: var(--border-strong); transform: translateY(-2px); } +.card::before { + /* Voxel-Marker auf der Timeline */ + content: ''; + position: absolute; + left: -1.55rem; + top: 1.45rem; + width: 9px; + height: 9px; + background: var(--green-200); + border-radius: 2px; + transform: rotate(45deg); + box-shadow: 0 0 10px rgba(34, 197, 94, 0.5); +} + +.card-meta { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.5rem; } .card-date { - color: var(--accent-bright); + color: var(--green-100); font-size: 0.85rem; - font-weight: 600; + font-weight: 650; text-transform: capitalize; - margin-bottom: 0.3rem; + letter-spacing: 0.01em; } -.card-body p { margin: 0.5rem 0; } -.card-body a { color: var(--accent-bright); } +.card-rel { color: var(--text-faint); font-size: 0.78rem; white-space: nowrap; } + +.card-body { color: #cfdad2; } +.card-body p { margin: 0.55rem 0; } +.card-body p:first-child { margin-top: 0; } +.card-body p:last-child { margin-bottom: 0; } +.card-body strong { color: var(--text); } +.card-body em { color: var(--text-dim); } +.card-body a { color: var(--green-100); text-decoration: none; border-bottom: 1px solid rgba(74, 222, 128, 0.3); } +.card-body a:hover { border-bottom-color: var(--green-100); } .card-body code { - background: #0d1210; + font-family: var(--mono); + font-size: 0.82em; + background: rgba(0, 0, 0, 0.35); border: 1px solid var(--border); - border-radius: 4px; - padding: 0.05rem 0.35rem; - font-size: 0.85em; + border-radius: 5px; + padding: 0.08rem 0.4rem; + color: #a7e8bd; } -/* Commit-Tabelle */ -.table-wrap { overflow-x: auto; } -.commits { width: 100%; border-collapse: collapse; font-size: 0.9rem; } -.commits th { - text-align: left; - color: var(--text-dim); - font-weight: 600; - padding: 0.5rem 0.75rem; +/* ---------- Commit-Liste ---------- */ +.commit-list { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 14px; + overflow: hidden; +} +.commit-row { + display: flex; + align-items: center; + gap: 0.9rem; + padding: 0.65rem 1.1rem; border-bottom: 1px solid var(--border); + font-size: 0.9rem; +} +.commit-row:last-child { border-bottom: none; } +.commit-row:hover { background: rgba(255, 255, 255, 0.02); } + +.sha { + font-family: var(--mono); + font-size: 0.8rem; + color: var(--green-100); + text-decoration: none; + background: rgba(34, 197, 94, 0.08); + border: 1px solid rgba(34, 197, 94, 0.2); + border-radius: 6px; + padding: 0.1rem 0.45rem; white-space: nowrap; } -.commits td { - padding: 0.5rem 0.75rem; - border-bottom: 1px solid var(--border); - vertical-align: top; -} -.commits .msg { min-width: 240px; } -.commits .time { white-space: nowrap; color: var(--text-dim); } -.commits a { color: var(--accent-bright); text-decoration: none; } -.commits a:hover { text-decoration: underline; } -.commits code { font-size: 0.85em; } +a.sha:hover { border-color: var(--green-200); } -/* Pagination */ +.commit-msg { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #cfdad2; +} + +.chip { + font-size: 0.72rem; + font-weight: 550; + padding: 0.12rem 0.55rem; + border-radius: 99px; + white-space: nowrap; +} +.chip-repo { background: rgba(255, 255, 255, 0.05); color: var(--text-dim); border: 1px solid var(--border); } +.chip-branch { background: rgba(88, 101, 242, 0.12); color: #a5b0ff; border: 1px solid rgba(88, 101, 242, 0.25); } + +.commit-time { + font-family: var(--mono); + font-size: 0.75rem; + color: var(--text-faint); + white-space: nowrap; +} + +/* ---------- Pagination ---------- */ .pager { display: flex; align-items: center; justify-content: center; - gap: 1rem; - margin-top: 1.5rem; - color: var(--text-dim); - font-size: 0.9rem; + gap: 1.25rem; + margin-top: 2rem; + color: var(--text-faint); + font-size: 0.88rem; } -/* Footer */ +/* ---------- Footer ---------- */ .footer { border-top: 1px solid var(--border); - color: var(--text-dim); - text-align: center; - padding: 1rem; + color: var(--text-faint); + padding: 1.25rem; font-size: 0.85rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.6rem; +} +.footer .logo { width: 18px; height: 18px; opacity: 0.7; } +.footer a { color: var(--text-dim); text-decoration: none; } +.footer a:hover { color: var(--green-100); } + +/* ---------- Mobile ---------- */ +@media (max-width: 640px) { + .header-inner { gap: 1rem; } + .username { display: none; } + .hero h1 { font-size: 1.8rem; } + .timeline { padding-left: 1.1rem; } + .commit-row { flex-wrap: wrap; row-gap: 0.3rem; } + .commit-msg { flex-basis: 100%; order: -1; white-space: normal; } } -.footer a { color: var(--accent-bright); text-decoration: none; }