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 { 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,82 +95,166 @@ export default function Page() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-8 text-sm leading-relaxed">
|
<div className="grid gap-10 lg:grid-cols-2">
|
||||||
<section>
|
<div className="space-y-8 text-sm leading-relaxed">
|
||||||
<h2 className="mb-3 text-base font-semibold flex items-center gap-2">
|
<section>
|
||||||
<span className="inline-block size-2 rounded-full bg-green-500" />
|
<h2 className="mb-3 text-base font-semibold flex items-center gap-2">
|
||||||
Working
|
<span className="inline-block size-2 rounded-full bg-green-500" />
|
||||||
</h2>
|
Working
|
||||||
<ul className="space-y-1.5 pl-4">
|
</h2>
|
||||||
<li>Projects — create and manage projects with D1 database</li>
|
<ul className="space-y-1.5 pl-4">
|
||||||
<li>Schedule — Gantt chart with phases, tasks, dependencies, and critical path</li>
|
<li>Projects — create and manage projects with D1 database</li>
|
||||||
<li>File browser — drive-style UI with folder navigation</li>
|
<li>Schedule — Gantt chart with phases, tasks, dependencies, and critical path</li>
|
||||||
<li>Settings — app preferences with theme and notifications</li>
|
<li>File browser — drive-style UI with folder navigation</li>
|
||||||
<li>Sidebar navigation with contextual project/file views</li>
|
<li>Settings — app preferences with theme and notifications</li>
|
||||||
<li>Command palette search (Cmd+K)</li>
|
<li>Sidebar navigation with contextual project/file views</li>
|
||||||
</ul>
|
<li>Command palette search (Cmd+K)</li>
|
||||||
</section>
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<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">
|
||||||
<span className="inline-block size-2 rounded-full bg-yellow-500" />
|
<span className="inline-block size-2 rounded-full bg-yellow-500" />
|
||||||
In Progress
|
In Progress
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="space-y-1.5 pl-4">
|
<ul className="space-y-1.5 pl-4">
|
||||||
<li>Project auto-provisioning (code generation, CSI folder structure)</li>
|
<li>Project auto-provisioning (code generation, CSI folder structure)</li>
|
||||||
<li>Budget tracking (CSI divisions, estimated vs actual, change orders)</li>
|
<li>Budget tracking (CSI divisions, estimated vs actual, change orders)</li>
|
||||||
<li>Document management (S3/R2 storage, metadata, versioning)</li>
|
<li>Document management (S3/R2 storage, metadata, versioning)</li>
|
||||||
<li>Communication logging (manual entries, timeline view)</li>
|
<li>Communication logging (manual entries, timeline view)</li>
|
||||||
<li>Dashboard — three-column layout (past due, due today, action items)</li>
|
<li>Dashboard — three-column layout (past due, due today, action items)</li>
|
||||||
<li>User authentication and roles (WorkOS)</li>
|
<li>User authentication and roles (WorkOS)</li>
|
||||||
<li>Email notifications (Resend)</li>
|
<li>Email notifications (Resend)</li>
|
||||||
<li>Basic reports (budget variance, overdue tasks, monthly actuals)</li>
|
<li>Basic reports (budget variance, overdue tasks, monthly actuals)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<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">
|
||||||
<span className="inline-block size-2 rounded-full bg-muted-foreground/50" />
|
<span className="inline-block size-2 rounded-full bg-muted-foreground/50" />
|
||||||
Planned
|
Planned
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="space-y-1.5 pl-4 text-muted-foreground">
|
<ul className="space-y-1.5 pl-4 text-muted-foreground">
|
||||||
<li>Client portal with read-only views</li>
|
<li>Client portal with read-only views</li>
|
||||||
<li>BuilderTrend import wizard (CSV-based)</li>
|
<li>BuilderTrend import wizard (CSV-based)</li>
|
||||||
<li>Daily logs</li>
|
<li>Daily logs</li>
|
||||||
<li>Time tracking</li>
|
<li>Time tracking</li>
|
||||||
<li>Report builder (custom fields and filters)</li>
|
<li>Report builder (custom fields and filters)</li>
|
||||||
<li>Bid package management</li>
|
<li>Bid package management</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<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">
|
||||||
<span className="inline-block size-2 rounded-full bg-muted-foreground/30" />
|
<span className="inline-block size-2 rounded-full bg-muted-foreground/30" />
|
||||||
Future
|
Future
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="space-y-1.5 pl-4 text-muted-foreground">
|
<ul className="space-y-1.5 pl-4 text-muted-foreground">
|
||||||
<li>Netsuite/QuickBooks API sync</li>
|
<li>Netsuite/QuickBooks API sync</li>
|
||||||
<li>Payment integration</li>
|
<li>Payment integration</li>
|
||||||
<li>RFI/Submittal tracking</li>
|
<li>RFI/Submittal tracking</li>
|
||||||
<li>Native mobile apps (iOS/Android)</li>
|
<li>Native mobile apps (iOS/Android)</li>
|
||||||
<li>Advanced scheduling (resource leveling, baseline comparison)</li>
|
<li>Advanced scheduling (resource leveling, baseline comparison)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-10 flex justify-center">
|
</div>
|
||||||
<Button variant="outline" asChild>
|
|
||||||
<a
|
{data && (
|
||||||
href={GITHUB_URL}
|
<div className="lg:sticky lg:top-6 lg:self-start space-y-6">
|
||||||
target="_blank"
|
<a
|
||||||
rel="noopener noreferrer"
|
href={GITHUB_URL}
|
||||||
>
|
target="_blank"
|
||||||
<IconExternalLink className="mr-2 size-4" />
|
rel="noopener noreferrer"
|
||||||
View on GitHub
|
className="hover:bg-muted/50 border rounded-lg px-4 py-3 flex items-center gap-3 transition-colors"
|
||||||
</a>
|
>
|
||||||
</Button>
|
<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>
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user