Dashboard UI overhaul: modern design with system stats, SVG icons, Inter font, theme toggle, loading states, and log modal

This commit is contained in:
Gan, Jimmy
2026-02-19 02:29:25 +08:00
parent ae1b88732d
commit f3db7d3654
13 changed files with 854 additions and 188 deletions
+109 -23
View File
@@ -1,38 +1,124 @@
<script>
import { onMount } from "svelte";
import { get } from "../lib/api.js";
let repos = $state([]);
let commits = $state([]);
let selectedRepo = $state("");
async function load() { const data = await get("/gitea/repos"); repos = data?.data || []; }
let loading = $state(true);
let loadingCommits = $state(false);
async function load() {
loading = true;
const data = await get("/gitea/repos");
repos = Array.isArray(data) ? data : data?.data || [];
loading = false;
}
async function showCommits(owner, repo) {
selectedRepo = `${owner}/${repo}`;
loadingCommits = true;
const data = await get(`/gitea/repos/${owner}/${repo}/commits`);
commits = Array.isArray(data) ? data : [];
loadingCommits = false;
}
load();
function timeAgo(d) {
const s = Math.floor((Date.now() - new Date(d)) / 1000);
if (s < 60) return "just now";
if (s < 3600) return Math.floor(s / 60) + "m ago";
if (s < 86400) return Math.floor(s / 3600) + "h ago";
return Math.floor(s / 86400) + "d ago";
}
const langColors = {
Python: "#3572A5", JavaScript: "#f1e05a", Shell: "#89e051", Go: "#00ADD8",
TypeScript: "#3178c6", Markdown: "#083fa1", CSS: "#563d7c", HTML: "#e34c26",
};
onMount(load);
</script>
<h2 class="text-2xl font-bold mb-6">Gitea Repos</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
{#each repos as r}
<button class="border rounded-lg p-4 text-left hover:bg-gray-50" onclick={() => showCommits(r.owner?.login, r.name)}>
<span class="font-medium">{r.full_name}</span>
<p class="text-xs text-gray-400 mt-1">{r.description || "No description"}</p>
<p class="text-xs text-gray-400">Updated {new Date(r.updated_at).toLocaleDateString()}</p>
</button>
{/each}
</div>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-surface-900 tracking-tight">Repositories</h1>
<p class="text-sm text-surface-400 mt-1">{repos.length} repositories on Gitea</p>
</div>
{#if selectedRepo}
<div class="mt-6">
<h3 class="font-semibold mb-3">Commits: {selectedRepo}</h3>
<div class="space-y-2">
{#each commits.slice(0, 20) as c}
<div class="border-b pb-2">
<p class="text-sm">{c.commit?.message?.split("\n")[0]}</p>
<p class="text-xs text-gray-400">{c.commit?.author?.name} - {new Date(c.commit?.author?.date).toLocaleString()}</p>
</div>
{#if loading}
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
{#each Array(4) as _}
<div class="h-[100px] rounded-xl bg-surface-100 animate-pulse"></div>
{/each}
</div>
</div>
{/if}
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
{#each repos as r}
<button
class="bg-white rounded-xl border border-surface-200 p-4 text-left shadow-sm hover:shadow-md hover:border-primary-200 transition-all duration-200 group {selectedRepo === `${r.owner?.login}/${r.name}` ? 'ring-2 ring-primary-500 border-primary-300' : ''}"
onclick={() => showCommits(r.owner?.login, r.name)}
>
<div class="flex items-start justify-between">
<div class="min-w-0">
<p class="text-sm font-semibold text-surface-800 group-hover:text-primary-600 transition-colors truncate">{r.name}</p>
<p class="text-xs text-surface-400 mt-1 line-clamp-1">{r.description || "No description"}</p>
</div>
{#if r.language}
<span class="flex items-center gap-1 text-[11px] text-surface-500 shrink-0 ml-2">
<span class="w-2 h-2 rounded-full" style="background: {langColors[r.language] || '#888'}"></span>
{r.language}
</span>
{/if}
</div>
<p class="text-[11px] text-surface-400 mt-2.5">Updated {timeAgo(r.updated_at)}</p>
</button>
{/each}
</div>
{/if}
<!-- Commits Panel -->
{#if selectedRepo}
<div class="bg-white rounded-xl border border-surface-200 p-5 shadow-sm">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-sm font-bold text-surface-800">Recent Commits</h3>
<p class="text-xs text-surface-400 mt-0.5">{selectedRepo}</p>
</div>
<button
aria-label="Close commits"
class="text-xs text-surface-400 hover:text-surface-600 transition-colors"
onclick={() => { selectedRepo = ""; commits = []; }}
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
{#if loadingCommits}
<div class="space-y-3">
{#each Array(3) as _}
<div class="h-12 rounded-lg bg-surface-100 animate-pulse"></div>
{/each}
</div>
{:else}
<div class="space-y-0">
{#each commits.slice(0, 15) as c, i}
<div class="flex gap-3 py-2.5 {i < commits.length - 1 ? 'border-b border-surface-100' : ''}">
<div class="flex flex-col items-center pt-1">
<div class="w-2 h-2 rounded-full bg-primary-400 shrink-0"></div>
{#if i < commits.length - 1}
<div class="w-px flex-1 bg-surface-200 mt-1"></div>
{/if}
</div>
<div class="min-w-0 flex-1">
<p class="text-sm text-surface-700 truncate">{c.commit?.message?.split("\n")[0]}</p>
<p class="text-[11px] text-surface-400 mt-0.5">
<span class="font-medium">{c.commit?.author?.name}</span> · {timeAgo(c.commit?.author?.date)}
</p>
</div>
<span class="text-[10px] font-mono text-surface-400 bg-surface-50 px-1.5 py-0.5 rounded h-fit shrink-0">{c.sha?.slice(0, 7)}</span>
</div>
{/each}
</div>
{/if}
</div>
{/if}
</div>