Skip to content

Commit 90aa778

Browse files
feat(web): add user onboarding flow (#53)
Co-authored-by: Chenxin Yan <[email protected]>
1 parent 70e93ad commit 90aa778

File tree

23 files changed

+1791
-264
lines changed

23 files changed

+1791
-264
lines changed

apps/web/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
"packageManager": "[email protected]",
1313
"dependencies": {
1414
"@albert-plus/server": "workspace:*",
15-
"@radix-ui/react-checkbox": "^1.3.3",
16-
"@radix-ui/react-select": "^2.2.6",
1715
"@clerk/nextjs": "^6.34.5",
1816
"@radix-ui/react-avatar": "^1.1.11",
17+
"@radix-ui/react-checkbox": "^1.3.3",
1918
"@radix-ui/react-context-menu": "^2.2.16",
2019
"@radix-ui/react-dialog": "^1.1.15",
2120
"@radix-ui/react-dropdown-menu": "^2.1.16",
2221
"@radix-ui/react-hover-card": "^1.1.15",
2322
"@radix-ui/react-label": "^2.1.8",
23+
"@radix-ui/react-select": "^2.2.6",
2424
"@radix-ui/react-separator": "^1.1.8",
2525
"@radix-ui/react-slot": "^1.2.4",
2626
"@radix-ui/react-tooltip": "^1.2.8",

apps/web/src/app/dashboard/layout.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import { api } from "@albert-plus/server/convex/_generated/api";
12
import { auth, currentUser } from "@clerk/nextjs/server";
23
import { cookies } from "next/headers";
4+
import { redirect } from "next/navigation";
35
import { AppSidebar } from "@/app/dashboard/components/sidebar/app-sidebar";
46
import { AppConfigProvider } from "@/components/AppConfigProvider";
57
import { SettingsDialog } from "@/components/SettingsDialog";
68
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
9+
import { fetchProtectedQuery } from "@/lib/convex";
710

811
export default async function Layout({
912
children,
@@ -15,13 +18,18 @@ export default async function Layout({
1518
const cookieStore = await cookies();
1619
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true";
1720
const { isAuthenticated, redirectToSignIn } = await auth();
21+
const student = await fetchProtectedQuery(api.students.getCurrentStudent);
1822

1923
if (!isAuthenticated) {
2024
redirectToSignIn();
2125
}
2226

2327
const user = await currentUser();
2428

29+
if (student == null) {
30+
redirect("/onboarding");
31+
}
32+
2533
return (
2634
<AppConfigProvider>
2735
<SidebarProvider defaultOpen={defaultOpen}>
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"use client";
2+
3+
import { Command as CommandPrimitive } from "cmdk";
4+
import { SearchIcon } from "lucide-react";
5+
import * as React from "react";
6+
import {
7+
Dialog,
8+
DialogContent,
9+
DialogDescription,
10+
DialogHeader,
11+
DialogTitle,
12+
} from "@/components/ui/dialog";
13+
import { cn } from "@/lib/utils";
14+
15+
const Command = React.forwardRef<
16+
React.ElementRef<typeof CommandPrimitive>,
17+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
18+
>(function Command({ className, ...props }, ref) {
19+
return (
20+
<CommandPrimitive
21+
ref={ref}
22+
data-slot="command"
23+
className={cn(
24+
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
25+
className,
26+
)}
27+
{...props}
28+
/>
29+
);
30+
});
31+
32+
function CommandDialog({
33+
title = "Command Palette",
34+
description = "Search for a command to run...",
35+
children,
36+
className,
37+
showCloseButton = true,
38+
...props
39+
}: React.ComponentProps<typeof Dialog> & {
40+
title?: string;
41+
description?: string;
42+
className?: string;
43+
showCloseButton?: boolean;
44+
}) {
45+
return (
46+
<Dialog {...props}>
47+
<DialogHeader className="sr-only">
48+
<DialogTitle>{title}</DialogTitle>
49+
<DialogDescription>{description}</DialogDescription>
50+
</DialogHeader>
51+
<DialogContent
52+
className={cn("overflow-hidden p-0", className)}
53+
showCloseButton={showCloseButton}
54+
>
55+
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
56+
{children}
57+
</Command>
58+
</DialogContent>
59+
</Dialog>
60+
);
61+
}
62+
63+
function CommandInput({
64+
className,
65+
...props
66+
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
67+
return (
68+
<div
69+
data-slot="command-input-wrapper"
70+
className="flex h-9 items-center gap-2 border-b px-3"
71+
>
72+
<SearchIcon className="size-4 shrink-0 opacity-50" />
73+
<CommandPrimitive.Input
74+
data-slot="command-input"
75+
className={cn(
76+
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
77+
className,
78+
)}
79+
{...props}
80+
/>
81+
</div>
82+
);
83+
}
84+
85+
const CommandList = React.forwardRef<
86+
React.ElementRef<typeof CommandPrimitive.List>,
87+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
88+
>(function CommandList({ className, ...props }, ref) {
89+
return (
90+
<CommandPrimitive.List
91+
ref={ref}
92+
data-slot="command-list"
93+
className={cn(
94+
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
95+
className,
96+
)}
97+
{...props}
98+
/>
99+
);
100+
});
101+
102+
function CommandEmpty({
103+
...props
104+
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
105+
return (
106+
<CommandPrimitive.Empty
107+
data-slot="command-empty"
108+
className="py-6 text-center text-sm"
109+
{...props}
110+
/>
111+
);
112+
}
113+
114+
function CommandGroup({
115+
className,
116+
...props
117+
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
118+
return (
119+
<CommandPrimitive.Group
120+
data-slot="command-group"
121+
className={cn(
122+
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
123+
className,
124+
)}
125+
{...props}
126+
/>
127+
);
128+
}
129+
130+
function CommandSeparator({
131+
className,
132+
...props
133+
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
134+
return (
135+
<CommandPrimitive.Separator
136+
data-slot="command-separator"
137+
className={cn("bg-border -mx-1 h-px", className)}
138+
{...props}
139+
/>
140+
);
141+
}
142+
143+
function CommandItem({
144+
className,
145+
...props
146+
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
147+
return (
148+
<CommandPrimitive.Item
149+
data-slot="command-item"
150+
className={cn(
151+
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
152+
className,
153+
)}
154+
{...props}
155+
/>
156+
);
157+
}
158+
159+
function CommandShortcut({
160+
className,
161+
...props
162+
}: React.ComponentProps<"span">) {
163+
return (
164+
<span
165+
data-slot="command-shortcut"
166+
className={cn(
167+
"text-muted-foreground ml-auto text-xs tracking-widest",
168+
className,
169+
)}
170+
{...props}
171+
/>
172+
);
173+
}
174+
175+
export {
176+
Command,
177+
CommandDialog,
178+
CommandInput,
179+
CommandList,
180+
CommandEmpty,
181+
CommandGroup,
182+
CommandItem,
183+
CommandShortcut,
184+
CommandSeparator,
185+
};

0 commit comments

Comments
 (0)