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:
parent
8ea4125505
commit
a09024aff7
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 |
@ -1,13 +1,79 @@
|
||||
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 =
|
||||
"https://github.com/High-Performance-Structures/compass"
|
||||
const REPO = "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 (
|
||||
<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">
|
||||
<span
|
||||
className="mx-auto mb-3 block size-12 bg-foreground"
|
||||
@ -29,6 +95,7 @@ export default function Page() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-10 lg:grid-cols-2">
|
||||
<div className="space-y-8 text-sm leading-relaxed">
|
||||
<section>
|
||||
<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>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-10 flex justify-center">
|
||||
<Button variant="outline" asChild>
|
||||
{data && (
|
||||
<div className="lg:sticky lg:top-6 lg:self-start space-y-6">
|
||||
<a
|
||||
href={GITHUB_URL}
|
||||
target="_blank"
|
||||
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" />
|
||||
View on GitHub
|
||||
<IconBrandGithub className="size-5 shrink-0" />
|
||||
<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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ function SidebarNav({
|
||||
projects: { id: string; name: string }[]
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
const { state } = useSidebar()
|
||||
const { state, setOpen } = useSidebar()
|
||||
const { open: openSearch } = useCommandMenu()
|
||||
const { open: openSettings } = useSettings()
|
||||
const isExpanded = state === "expanded"
|
||||
@ -86,6 +86,13 @@ function SidebarNav({
|
||||
const isProjectMode = /^\/dashboard\/projects\/[^/]+/.test(
|
||||
pathname ?? ""
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if ((isFilesMode || isProjectMode) && !isExpanded) {
|
||||
setOpen(true)
|
||||
}
|
||||
}, [isFilesMode, isProjectMode, isExpanded, setOpen])
|
||||
|
||||
const showContext = isExpanded && (isFilesMode || isProjectMode)
|
||||
|
||||
const mode = showContext && isFilesMode
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { formatFileSize } from "@/lib/file-utils"
|
||||
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)
|
||||
|
||||
return (
|
||||
<div className="px-3 py-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground mb-1.5">
|
||||
<span>Storage</span>
|
||||
<span>{percent}% used</span>
|
||||
<div className="space-y-2">
|
||||
<div className="bg-sidebar-foreground/20 h-1.5 w-full overflow-hidden rounded-full">
|
||||
<div
|
||||
className="bg-sidebar-primary h-full rounded-full transition-all"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<Progress value={percent} className="h-1.5" />
|
||||
<p className="text-xs text-muted-foreground mt-1.5">
|
||||
{formatFileSize(usage.used)} of {formatFileSize(usage.total)}
|
||||
<p className="text-sidebar-foreground/70 text-xs">
|
||||
{formatFileSize(usage.used)} of {formatFileSize(usage.total)} used
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -19,7 +19,6 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarSeparator,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@ -84,8 +83,7 @@ export function NavFiles() {
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<div className="mt-auto">
|
||||
<SidebarSeparator />
|
||||
<div className="mt-auto px-3 pb-3">
|
||||
<StorageIndicator usage={mockStorageUsage} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -57,12 +57,12 @@ export function NavUser({
|
||||
<AvatarFallback className="rounded-lg">MV</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
<span className="text-sidebar-foreground truncate font-medium">{user.name}</span>
|
||||
<span className="text-sidebar-foreground/70 truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
<IconDotsVertical className="ml-auto size-4" />
|
||||
<IconDotsVertical className="text-sidebar-foreground/70 ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user