feat(ui): dashboard github stats, sidebar UX, polish

- Add live GitHub stats and commits to dashboard with
  sticky two-column layout and GitHub card link
- Auto-expand sidebar when navigating to files/projects
  while collapsed
- Add GitHub link to landing page
- Restyle storage indicator with sidebar tokens
- Fix nav-user readability with sidebar-foreground tokens
- Update favicon to colored compass logo
This commit is contained in:
Nicholai Vogel 2026-01-24 13:32:50 -07:00
parent 8ea4125505
commit a09024aff7
7 changed files with 245 additions and 90 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 B

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -1,13 +1,79 @@
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { IconExternalLink } from "@tabler/icons-react" import {
IconBrandGithub,
IconExternalLink,
IconGitCommit,
IconGitFork,
IconStar,
IconAlertCircle,
IconEye,
} from "@tabler/icons-react"
const GITHUB_URL = const REPO = "High-Performance-Structures/compass"
"https://github.com/High-Performance-Structures/compass" const GITHUB_URL = `https://github.com/${REPO}`
type RepoStats = {
stargazers_count: number
forks_count: number
open_issues_count: number
subscribers_count: number
}
type Commit = {
sha: string
commit: {
message: string
author: { name: string; date: string }
}
html_url: string
}
async function getRepoData() {
try {
const [repoRes, commitsRes] = await Promise.all([
fetch(`https://api.github.com/repos/${REPO}`, {
next: { revalidate: 300 },
headers: { Accept: "application/vnd.github+json" },
}),
fetch(`https://api.github.com/repos/${REPO}/commits?per_page=8`, {
next: { revalidate: 300 },
headers: { Accept: "application/vnd.github+json" },
}),
])
if (!repoRes.ok || !commitsRes.ok) return null
const repo: RepoStats = await repoRes.json()
const commits: Commit[] = await commitsRes.json()
return { repo, commits }
} catch {
return null
}
}
function timeAgo(date: string) {
const seconds = Math.floor(
(Date.now() - new Date(date).getTime()) / 1000
)
if (seconds < 60) return "just now"
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
if (days < 30) return `${days}d ago`
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}
export default async function Page() {
const data = await getRepoData()
export default function Page() {
return ( return (
<div className="flex flex-1 items-start justify-center p-6 md:p-12"> <div className="flex flex-1 items-start justify-center p-6 md:p-12">
<div className="w-full max-w-3xl py-8"> <div className="w-full max-w-6xl py-8">
<div className="mb-10 text-center"> <div className="mb-10 text-center">
<span <span
className="mx-auto mb-3 block size-12 bg-foreground" className="mx-auto mb-3 block size-12 bg-foreground"
@ -29,6 +95,7 @@ export default function Page() {
</p> </p>
</div> </div>
<div className="grid gap-10 lg:grid-cols-2">
<div className="space-y-8 text-sm leading-relaxed"> <div className="space-y-8 text-sm leading-relaxed">
<section> <section>
<h2 className="mb-3 text-base font-semibold flex items-center gap-2"> <h2 className="mb-3 text-base font-semibold flex items-center gap-2">
@ -90,21 +157,104 @@ export default function Page() {
<li>Advanced scheduling (resource leveling, baseline comparison)</li> <li>Advanced scheduling (resource leveling, baseline comparison)</li>
</ul> </ul>
</section> </section>
</div> </div>
<div className="mt-10 flex justify-center"> {data && (
<Button variant="outline" asChild> <div className="lg:sticky lg:top-6 lg:self-start space-y-6">
<a <a
href={GITHUB_URL} href={GITHUB_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:bg-muted/50 border rounded-lg px-4 py-3 flex items-center gap-3 transition-colors"
> >
<IconExternalLink className="mr-2 size-4" /> <IconBrandGithub className="size-5 shrink-0" />
View on GitHub <div className="min-w-0">
<p className="text-sm font-medium">View on GitHub</p>
<p className="text-muted-foreground text-xs truncate">{REPO}</p>
</div>
<IconExternalLink className="text-muted-foreground size-3.5 shrink-0 ml-auto" />
</a> </a>
</Button> <div className="grid grid-cols-2 gap-3">
<StatCard
icon={<IconStar className="size-4" />}
label="Stars"
value={data.repo.stargazers_count}
/>
<StatCard
icon={<IconGitFork className="size-4" />}
label="Forks"
value={data.repo.forks_count}
/>
<StatCard
icon={<IconAlertCircle className="size-4" />}
label="Issues"
value={data.repo.open_issues_count}
/>
<StatCard
icon={<IconEye className="size-4" />}
label="Watchers"
value={data.repo.subscribers_count}
/>
</div>
<div>
<h2 className="text-muted-foreground mb-3 text-xs font-medium uppercase tracking-wider">
Recent Commits
</h2>
<div className="border rounded-lg divide-y">
{data.commits.map((commit) => (
<a
key={commit.sha}
href={commit.html_url}
target="_blank"
rel="noopener noreferrer"
className="hover:bg-muted/50 flex items-start gap-3 px-4 py-3 transition-colors"
>
<IconGitCommit className="text-muted-foreground mt-0.5 size-4 shrink-0" />
<div className="min-w-0 flex-1">
<p className="truncate text-sm">
{commit.commit.message.split("\n")[0]}
</p>
<p className="text-muted-foreground mt-0.5 text-xs">
{commit.commit.author.name}
<span className="mx-1.5">·</span>
{timeAgo(commit.commit.author.date)}
</p>
</div>
<code className="text-muted-foreground shrink-0 font-mono text-xs">
{commit.sha.slice(0, 7)}
</code>
</a>
))}
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
) )
} }
function StatCard({
icon,
label,
value,
}: {
icon: React.ReactNode
label: string
value: number
}) {
return (
<div className="border rounded-lg px-4 py-3">
<div className="text-muted-foreground mb-1 flex items-center gap-1.5 text-xs">
{icon}
{label}
</div>
<p className="text-2xl font-semibold tabular-nums">
{value.toLocaleString()}
</p>
</div>
)
}

View File

@ -78,7 +78,7 @@ function SidebarNav({
projects: { id: string; name: string }[] projects: { id: string; name: string }[]
}) { }) {
const pathname = usePathname() const pathname = usePathname()
const { state } = useSidebar() const { state, setOpen } = useSidebar()
const { open: openSearch } = useCommandMenu() const { open: openSearch } = useCommandMenu()
const { open: openSettings } = useSettings() const { open: openSettings } = useSettings()
const isExpanded = state === "expanded" const isExpanded = state === "expanded"
@ -86,6 +86,13 @@ function SidebarNav({
const isProjectMode = /^\/dashboard\/projects\/[^/]+/.test( const isProjectMode = /^\/dashboard\/projects\/[^/]+/.test(
pathname ?? "" pathname ?? ""
) )
React.useEffect(() => {
if ((isFilesMode || isProjectMode) && !isExpanded) {
setOpen(true)
}
}, [isFilesMode, isProjectMode, isExpanded, setOpen])
const showContext = isExpanded && (isFilesMode || isProjectMode) const showContext = isExpanded && (isFilesMode || isProjectMode)
const mode = showContext && isFilesMode const mode = showContext && isFilesMode

View File

@ -1,6 +1,5 @@
"use client" "use client"
import { Progress } from "@/components/ui/progress"
import { formatFileSize } from "@/lib/file-utils" import { formatFileSize } from "@/lib/file-utils"
import type { StorageUsage } from "@/lib/files-data" import type { StorageUsage } from "@/lib/files-data"
@ -8,14 +7,15 @@ export function StorageIndicator({ usage }: { usage: StorageUsage }) {
const percent = Math.round((usage.used / usage.total) * 100) const percent = Math.round((usage.used / usage.total) * 100)
return ( return (
<div className="px-3 py-2"> <div className="space-y-2">
<div className="flex items-center justify-between text-xs text-muted-foreground mb-1.5"> <div className="bg-sidebar-foreground/20 h-1.5 w-full overflow-hidden rounded-full">
<span>Storage</span> <div
<span>{percent}% used</span> className="bg-sidebar-primary h-full rounded-full transition-all"
style={{ width: `${percent}%` }}
/>
</div> </div>
<Progress value={percent} className="h-1.5" /> <p className="text-sidebar-foreground/70 text-xs">
<p className="text-xs text-muted-foreground mt-1.5"> {formatFileSize(usage.used)} of {formatFileSize(usage.total)} used
{formatFileSize(usage.used)} of {formatFileSize(usage.total)}
</p> </p>
</div> </div>
) )

View File

@ -19,7 +19,6 @@ import {
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
SidebarSeparator,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -84,8 +83,7 @@ export function NavFiles() {
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
<div className="mt-auto"> <div className="mt-auto px-3 pb-3">
<SidebarSeparator />
<StorageIndicator usage={mockStorageUsage} /> <StorageIndicator usage={mockStorageUsage} />
</div> </div>
</> </>

View File

@ -57,12 +57,12 @@ export function NavUser({
<AvatarFallback className="rounded-lg">MV</AvatarFallback> <AvatarFallback className="rounded-lg">MV</AvatarFallback>
</Avatar> </Avatar>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span> <span className="text-sidebar-foreground truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs"> <span className="text-sidebar-foreground/70 truncate text-xs">
{user.email} {user.email}
</span> </span>
</div> </div>
<IconDotsVertical className="ml-auto size-4" /> <IconDotsVertical className="text-sidebar-foreground/70 ml-auto size-4" />
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent