Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"dependencies": {
"@cloudflare/vite-plugin": "^1.26.0",
"@quickhub/icons": "workspace:*",
"@quickhub/ui": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "latest",
Expand All @@ -32,6 +33,7 @@
"better-auth": "^1.6.0",
"drizzle-orm": "^0.45.2",
"lucide-react": "^0.545.0",
"next-themes": "^0.4.6",
"octokit": "^5.0.5",
"react": "^19.2.0",
"react-dom": "^19.2.0",
Expand Down
19 changes: 19 additions & 0 deletions apps/dashboard/src/components/layouts/dashboard-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getRouteApi, Outlet } from "@tanstack/react-router";
import { DashboardTopbar } from "./dashboard-topbar";

const routeApi = getRouteApi("/_protected");

export function DashboardLayout() {
const { user } = routeApi.useRouteContext();

return (
<div className="flex h-dvh flex-col bg-muted">
<DashboardTopbar user={user} />
<div className="flex flex-1 flex-col overflow-hidden p-2 pt-0">
<div className="flex-1 overflow-hidden rounded-xl border bg-card shadow-[0_1px_4px_0_rgba(0,0,0,0.03)]">
<Outlet />
</div>
</div>
</div>
);
}
156 changes: 156 additions & 0 deletions apps/dashboard/src/components/layouts/dashboard-topbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {
GitPullRequestIcon,
HomeIcon,
IssuesIcon,
MoonIcon,
MoreHorizontalIcon,
ReviewsIcon,
SunIcon,
SystemIcon,
} from "@quickhub/icons";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@quickhub/ui/components/avatar";
import { Button } from "@quickhub/ui/components/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@quickhub/ui/components/dropdown-menu";
import { Link } from "@tanstack/react-router";
import { useTheme } from "next-themes";
import { signOut } from "#/lib/auth.client";

interface DashboardTopbarProps {
user: {
name?: string | null;
email: string;
image?: string | null;
};
}

const navItems = [
{ to: "/", label: "Overview", icon: HomeIcon },
{ to: "/pull-requests", label: "Pull Requests", icon: GitPullRequestIcon },
{ to: "/issues", label: "Issues", icon: IssuesIcon },
{ to: "/reviews", label: "Reviews", icon: ReviewsIcon },
] as const;

const themeOptions = [
{ value: "light", icon: SunIcon, label: "Light" },
{ value: "dark", icon: MoonIcon, label: "Dark" },
{ value: "system", icon: SystemIcon, label: "System" },
] as const;

export function DashboardTopbar({ user }: DashboardTopbarProps) {
const { theme, setTheme } = useTheme();
const displayName = user.name ?? user.email;
const initials = displayName
.split(" ")
.map((part) => part[0])
.join("")
.slice(0, 2)
.toUpperCase();

return (
<nav className="flex items-center gap-3 px-3 py-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex size-8 items-center justify-center rounded-full"
>
<Avatar className="size-7">
<AvatarImage src={user.image ?? undefined} alt={displayName} />
<AvatarFallback className="text-xs">{initials}</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel className="flex items-center justify-between">
<div>
<p>{displayName}</p>
<p className="font-normal text-muted-foreground">{user.email}</p>
</div>
<div className="flex items-center gap-0.5 rounded-md border border-border/50 p-0.5">
{themeOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setTheme(opt.value)}
className={`flex size-6 items-center justify-center rounded-sm transition-colors ${
theme === opt.value
? "bg-surface-1 text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
title={opt.label}
>
<opt.icon size={13} strokeWidth={2} />
</button>
))}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut keys={["G", "P"]} />
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut keys={["G", "S"]} />
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() =>
signOut().then(() => {
window.location.href = "/login";
})
}
>
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

<div className="flex items-center gap-0.5">
{navItems.map((item) => (
<Button
key={item.label}
variant="ghost"
size="sm"
asChild
className="text-muted-foreground [&.active]:bg-surface-1 [&.active]:text-foreground"
>
<Link
to={item.to as string}
activeOptions={{ exact: true }}
activeProps={{ className: "active" }}
>
<item.icon size={15} strokeWidth={2} />
<span>{item.label}</span>
</Link>
</Button>
))}
</div>

<div className="ml-auto">
<Button
variant="ghost"
size="icon"
className="size-8 text-muted-foreground hover:bg-surface-1"
>
<MoreHorizontalIcon className="size-5" strokeWidth={2} />
</Button>
</div>
</nav>
);
}
50 changes: 13 additions & 37 deletions apps/dashboard/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as ProtectedRouteImport } from './routes/_protected'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ProtectedDashboardRouteImport } from './routes/_protected/dashboard'
import { Route as ProtectedIndexRouteImport } from './routes/_protected/index'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'

const LoginRoute = LoginRouteImport.update({
Expand All @@ -24,14 +23,9 @@ const ProtectedRoute = ProtectedRouteImport.update({
id: '/_protected',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
const ProtectedIndexRoute = ProtectedIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const ProtectedDashboardRoute = ProtectedDashboardRouteImport.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => ProtectedRoute,
} as any)
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
Expand All @@ -41,41 +35,31 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/': typeof ProtectedIndexRoute
'/login': typeof LoginRoute
'/dashboard': typeof ProtectedDashboardRoute
'/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof LoginRoute
'/dashboard': typeof ProtectedDashboardRoute
'/': typeof ProtectedIndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/_protected': typeof ProtectedRouteWithChildren
'/login': typeof LoginRoute
'/_protected/dashboard': typeof ProtectedDashboardRoute
'/_protected/': typeof ProtectedIndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/login' | '/dashboard' | '/api/auth/$'
fullPaths: '/' | '/login' | '/api/auth/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/login' | '/dashboard' | '/api/auth/$'
id:
| '__root__'
| '/'
| '/_protected'
| '/login'
| '/_protected/dashboard'
| '/api/auth/$'
to: '/login' | '/' | '/api/auth/$'
id: '__root__' | '/_protected' | '/login' | '/_protected/' | '/api/auth/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ProtectedRoute: typeof ProtectedRouteWithChildren
LoginRoute: typeof LoginRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
Expand All @@ -97,18 +81,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProtectedRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
'/_protected/': {
id: '/_protected/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/_protected/dashboard': {
id: '/_protected/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof ProtectedDashboardRouteImport
preLoaderRoute: typeof ProtectedIndexRouteImport
parentRoute: typeof ProtectedRoute
}
'/api/auth/$': {
Expand All @@ -122,19 +99,18 @@ declare module '@tanstack/react-router' {
}

interface ProtectedRouteChildren {
ProtectedDashboardRoute: typeof ProtectedDashboardRoute
ProtectedIndexRoute: typeof ProtectedIndexRoute
}

const ProtectedRouteChildren: ProtectedRouteChildren = {
ProtectedDashboardRoute: ProtectedDashboardRoute,
ProtectedIndexRoute: ProtectedIndexRoute,
}

const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren(
ProtectedRouteChildren,
)

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ProtectedRoute: ProtectedRouteWithChildren,
LoginRoute: LoginRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
Expand Down
7 changes: 6 additions & 1 deletion apps/dashboard/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { Agentation } from "agentation";
import { ThemeProvider } from "next-themes";

import appCss from "../styles.css?url";

Expand Down Expand Up @@ -48,5 +49,9 @@ function RootDocument({ children }: { children: React.ReactNode }) {
}

function RootComponent() {
return <Outlet />;
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<Outlet />
</ThemeProvider>
);
}
5 changes: 3 additions & 2 deletions apps/dashboard/src/routes/_protected.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { createFileRoute, redirect } from "@tanstack/react-router";
import { DashboardLayout } from "#/components/layouts/dashboard-layout";
import { getSession } from "#/lib/auth.functions";

export const Route = createFileRoute("/_protected")({
Expand All @@ -12,5 +13,5 @@ export const Route = createFileRoute("/_protected")({
}
return { user: session.user, session: session.session };
},
component: () => <Outlet />,
component: DashboardLayout,
});
Loading