refactor: convert to bun workspaces monorepo

- Move web app into apps/web/
- Create packages/shared/ with shared types
- Create plugins/decky-vault/ scaffold
- Root package.json manages workspaces only
This commit is contained in:
2026-06-28 05:20:28 +08:00
parent c4bede20d4
commit cd72b7a948
345 changed files with 488 additions and 126 deletions
+118
View File
@@ -0,0 +1,118 @@
import { notFound } from "next/navigation"
import { db } from "@/lib/db/index"
import { user, performanceEntries, games, gameVersions, hardware } from "@/lib/db/schema"
import { eq, sql, and, desc } from "drizzle-orm"
import { ProfilePageClient } from "./profile-page-client"
// This page needs live data — skip static generation at build time
export const dynamic = "force-dynamic"
export const metadata = {
title: "Profile",
}
export default async function ProfilePage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const [profile] = await db
.select({
id: user.id,
name: user.name,
image: user.image,
role: user.role,
createdAt: user.createdAt,
emailVerified: user.emailVerified,
})
.from(user)
.where(eq(user.id, id))
.limit(1)
if (!profile) {
notFound()
}
// Count contributions
const [{ count: contributions }] = await db
.select({ count: sql<number>`count(*)::int` })
.from(performanceEntries)
.where(and(
eq(performanceEntries.userId, id),
eq(performanceEntries.isRemoved, false)
))
// Count verified entries
const [{ count: verifiedEntries }] = await db
.select({ count: sql<number>`count(*)::int` })
.from(performanceEntries)
.where(and(
eq(performanceEntries.userId, id),
eq(performanceEntries.isRemoved, false),
sql`${performanceEntries.verifiedAt} IS NOT NULL`
))
// Calculate reputation (upvotes - downvotes)
const reputationResult = await db
.select({
totalUpvotes: sql<number>`coalesce(sum(${performanceEntries.upvotes}), 0)::int`,
totalDownvotes: sql<number>`coalesce(sum(${performanceEntries.downvotes}), 0)::int`,
})
.from(performanceEntries)
.where(and(
eq(performanceEntries.userId, id),
eq(performanceEntries.isRemoved, false),
))
// Fetch recent contributions (last 5)
const recentContributions = await db
.select({
id: performanceEntries.id,
fpsAvg: performanceEntries.fpsAvg,
fpsLow: performanceEntries.fpsLow,
fpsHigh: performanceEntries.fpsHigh,
hardwareSlug: performanceEntries.hardwareSlug,
hardwareName: hardware.name,
upscalerType: performanceEntries.upscalerType,
upscalerVersion: performanceEntries.upscalerVersion,
frameGenMethod: performanceEntries.frameGenMethod,
verifiedAt: performanceEntries.verifiedAt,
createdAt: performanceEntries.createdAt,
gameTitle: games.title,
gameId: games.id,
gameHeaderImage: games.headerImage,
})
.from(performanceEntries)
.innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id))
.innerJoin(games, eq(gameVersions.gameId, games.id))
.innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug))
.where(and(
eq(performanceEntries.userId, id),
eq(performanceEntries.isRemoved, false)
))
.orderBy(desc(performanceEntries.createdAt))
.limit(5)
return (
<ProfilePageClient
profile={{
...profile,
createdAt: profile.createdAt.toISOString(),
contributions,
verifiedEntries,
reputation: Math.max(0,
(reputationResult[0]?.totalUpvotes ?? 0) -
(reputationResult[0]?.totalDownvotes ?? 0)
),
verified: !!profile.emailVerified,
}}
recentContributions={recentContributions.map((e) => ({
...e,
createdAt: e.createdAt.toISOString(),
verifiedAt: e.verifiedAt?.toISOString() ?? null,
}))}
/>
)
}
@@ -0,0 +1,60 @@
"use client"
import { ProfileHeader } from "@/components/profile/profile-header"
import { StatsRow } from "@/components/profile/stats-row"
import { ContributionList } from "@/components/profile/contribution-list"
import type { ContributionEntry } from "@/types/api"
interface ProfilePageClientProps {
profile: {
id: string
name: string
image: string | null
role: string | null
createdAt: string
contributions: number
verifiedEntries: number
reputation: number
verified: boolean
}
recentContributions: ContributionEntry[]
}
export function ProfilePageClient({ profile, recentContributions }: ProfilePageClientProps) {
return (
<div className="w-full flex flex-col gap-8 pb-16">
<div className="px-4 md:px-[10svw]">
<div className="max-w-7xl mx-auto">
<ProfileHeader
name={profile.name}
role={profile.role}
verified={profile.verified}
createdAt={profile.createdAt}
image={profile.image}
/>
</div>
</div>
<div className="px-4 md:px-[10svw]">
<div className="max-w-7xl mx-auto">
<StatsRow
contributions={profile.contributions}
verifiedEntries={profile.verifiedEntries}
reputation={profile.reputation}
/>
</div>
</div>
<div className="px-4 md:px-[10svw]">
<div className="max-w-7xl mx-auto">
<h2 className="text-lg font-semibold mb-4">Recent Contributions</h2>
<ContributionList
entries={recentContributions}
showViewAll
totalCount={profile.contributions}
/>
</div>
</div>
</div>
)
}