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 (
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
-
-
-
- | SHA |
- Message |
- Repo |
- Branch |
- Zeit |
-
-
-
- {data.items.map((c) => (
-
-
- {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; }