Skip to content

Commit f7e4fe3

Browse files
authored
chore: MVP launch! (#103)
1 parent 9de9986 commit f7e4fe3

File tree

8 files changed

+212
-48
lines changed

8 files changed

+212
-48
lines changed

apps/web/src/app/dashboard/components/sidebar/nav-items.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,25 @@ export function NavItems({
2121
<SidebarGroup {...props}>
2222
<SidebarGroupContent>
2323
<SidebarMenu>
24-
{items.map((item) => (
25-
<SidebarMenuItem key={item.title}>
26-
<SidebarMenuButton asChild>
27-
<a href={item.url}>
28-
<item.icon />
29-
<span>{item.title}</span>
30-
</a>
31-
</SidebarMenuButton>
32-
</SidebarMenuItem>
33-
))}
24+
{items.map((item) => {
25+
const isExternal = item.url.startsWith("http");
26+
return (
27+
<SidebarMenuItem key={item.title}>
28+
<SidebarMenuButton asChild>
29+
<a
30+
href={item.url}
31+
{...(isExternal && {
32+
target: "_blank",
33+
rel: "noopener noreferrer",
34+
})}
35+
>
36+
<item.icon />
37+
<span>{item.title}</span>
38+
</a>
39+
</SidebarMenuButton>
40+
</SidebarMenuItem>
41+
);
42+
})}
3443
</SidebarMenu>
3544
</SidebarGroupContent>
3645
</SidebarGroup>
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import { redirect } from "next/navigation";
2+
13
const HomePage = () => {
2-
return;
4+
// TODO: homepage is not ready yet, hide if from MVP for now
5+
redirect("/dashboard/register");
36
};
47

58
export default HomePage;

apps/web/src/app/onboarding/component/onboarding-form.tsx

Lines changed: 133 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { api } from "@albert-plus/server/convex/_generated/api";
44
import type { Doc, Id } from "@albert-plus/server/convex/_generated/dataModel";
5+
import { useClerk, useUser } from "@clerk/nextjs";
56
import { useForm } from "@tanstack/react-form";
67
import {
78
useConvexAuth,
@@ -10,6 +11,7 @@ import {
1011
useQuery,
1112
} from "convex/react";
1213
import type { FunctionArgs } from "convex/server";
14+
import { LogOutIcon } from "lucide-react";
1315
import { useRouter } from "next/navigation";
1416
import React, { Activity } from "react";
1517
import { toast } from "sonner";
@@ -25,13 +27,16 @@ import {
2527
CardHeader,
2628
CardTitle,
2729
} from "@/components/ui/card";
30+
import { Checkbox } from "@/components/ui/checkbox";
2831
import {
2932
FieldContent,
33+
FieldDescription,
3034
FieldError,
3135
FieldGroup,
3236
FieldLabel,
3337
Field as UIField,
3438
} from "@/components/ui/field";
39+
import { Label } from "@/components/ui/label";
3540
import MultipleSelector from "@/components/ui/multiselect";
3641
import {
3742
Select,
@@ -72,6 +77,8 @@ const onboardingFormSchema = z
7277
expectedGraduationDate: dateSchema,
7378
// User courses
7479
userCourses: z.array(z.object()).optional(),
80+
// Final presentation invite
81+
attendPresentation: z.boolean().optional(),
7582
})
7683
.refine(
7784
(data) => {
@@ -94,13 +101,16 @@ const onboardingFormSchema = z
94101

95102
export function OnboardingForm() {
96103
const router = useRouter();
104+
const { user } = useUser();
105+
const { signOut } = useClerk();
97106
const { isAuthenticated } = useConvexAuth();
98107
const [isFileLoaded, setIsFileLoaded] = React.useState(false);
99108
const [currentStep, setCurrentStep] = React.useState<1 | 2>(1);
100109

101110
// actions
102111
const upsertStudent = useMutation(api.students.upsertCurrentStudent);
103112
const importUserCourses = useMutation(api.userCourses.importUserCourses);
113+
const createInvite = useMutation(api.studentInvites.createInvite);
104114

105115
// schools
106116
const schools = useQuery(
@@ -183,6 +193,8 @@ export function OnboardingForm() {
183193
userCourses: undefined as
184194
| FunctionArgs<typeof api.userCourses.importUserCourses>["courses"]
185195
| undefined,
196+
// final presentation
197+
attendPresentation: false,
186198
},
187199
validators: {
188200
onSubmit: ({ value }) => {
@@ -215,6 +227,14 @@ export function OnboardingForm() {
215227
if (value.userCourses) {
216228
await importUserCourses({ courses: value.userCourses });
217229
}
230+
231+
if (value.attendPresentation && user) {
232+
await createInvite({
233+
name: user.fullName || "Unknown",
234+
email:
235+
user.primaryEmailAddress?.emailAddress || "[email protected]",
236+
});
237+
}
218238
} catch (error) {
219239
console.error("Error completing onboarding:", error);
220240
toast.error("Could not complete onboarding. Please try again.");
@@ -255,37 +275,51 @@ export function OnboardingForm() {
255275
<Activity mode={currentStep === 1 ? "visible" : "hidden"}>
256276
<Card>
257277
<CardHeader>
258-
<CardTitle className="text-2xl flex items-center gap-2">
259-
Degree Progress Report
260-
<Tooltip>
261-
<TooltipTrigger asChild>
262-
<Badge
263-
variant="outline"
264-
className="cursor-help size-5 rounded-full p-0 text-xs hover:bg-muted"
265-
>
266-
?
267-
</Badge>
268-
</TooltipTrigger>
269-
<TooltipContent className="max-w-xs">
270-
<p>
271-
We do not store your degree progress report. Need help
272-
finding it?{" "}
273-
<a
274-
href="https://www.nyu.edu/students/student-information-and-resources/registration-records-and-graduation/registration/tracking-degree-progress.html"
275-
target="_blank"
276-
rel="noopener noreferrer"
277-
className="underline"
278-
>
279-
View NYU's guide
280-
</a>
281-
</p>
282-
</TooltipContent>
283-
</Tooltip>
284-
</CardTitle>
285-
<CardDescription>
286-
Upload your degree progress report (PDF) so we can help you track
287-
your academic progress and suggest courses.
288-
</CardDescription>
278+
<div className="flex items-start justify-between gap-4">
279+
<div className="flex-1">
280+
<CardTitle className="text-2xl flex items-center gap-2">
281+
Degree Progress Report
282+
<Tooltip>
283+
<TooltipTrigger asChild>
284+
<Badge
285+
variant="outline"
286+
className="cursor-help size-5 rounded-full p-0 text-xs hover:bg-muted"
287+
>
288+
?
289+
</Badge>
290+
</TooltipTrigger>
291+
<TooltipContent className="max-w-xs">
292+
<p>
293+
We do not store your degree progress report. Need help
294+
finding it?{" "}
295+
<a
296+
href="https://www.nyu.edu/students/student-information-and-resources/registration-records-and-graduation/registration/tracking-degree-progress.html"
297+
target="_blank"
298+
rel="noopener noreferrer"
299+
className="underline"
300+
>
301+
View NYU's guide
302+
</a>
303+
</p>
304+
</TooltipContent>
305+
</Tooltip>
306+
</CardTitle>
307+
<CardDescription>
308+
Upload your degree progress report (PDF) so we can help you
309+
track your academic progress and suggest courses.
310+
</CardDescription>
311+
</div>
312+
<Button
313+
type="button"
314+
variant="ghost"
315+
size="sm"
316+
onClick={() => signOut({ redirectUrl: "/" })}
317+
className="gap-2 text-muted-foreground hover:text-foreground"
318+
>
319+
<LogOutIcon className="size-4" />
320+
Sign out
321+
</Button>
322+
</div>
289323
</CardHeader>
290324
<CardContent>
291325
<DegreeProgreeUpload
@@ -337,11 +371,25 @@ export function OnboardingForm() {
337371
<Activity mode={currentStep === 2 ? "visible" : "hidden"}>
338372
<Card>
339373
<CardHeader>
340-
<CardTitle className="text-2xl">Academic Information</CardTitle>
341-
<CardDescription>
342-
Tell us about your academic background so we can personalize your
343-
experience.
344-
</CardDescription>
374+
<div className="flex items-start justify-between gap-4">
375+
<div className="flex-1">
376+
<CardTitle className="text-2xl">Academic Information</CardTitle>
377+
<CardDescription>
378+
Tell us about your academic background so we can personalize
379+
your experience.
380+
</CardDescription>
381+
</div>
382+
<Button
383+
type="button"
384+
variant="ghost"
385+
size="sm"
386+
onClick={() => signOut({ redirectUrl: "/" })}
387+
className="gap-2 text-muted-foreground hover:text-foreground"
388+
>
389+
<LogOutIcon className="size-4" />
390+
Sign out
391+
</Button>
392+
</div>
345393
</CardHeader>
346394
<CardContent>
347395
<FieldGroup>
@@ -545,6 +593,55 @@ export function OnboardingForm() {
545593
{(field) => <FieldError errors={field.state.meta.errors} />}
546594
</form.Field>
547595
</FieldGroup>
596+
597+
<div className="rounded-lg border border-border/40 bg-muted/5 p-5">
598+
<div className="space-y-4">
599+
<div className="space-y-1">
600+
<h3 className="text-base font-semibold">
601+
Tech@NYU Final Presentation RSVP
602+
</h3>
603+
<p className="text-sm text-muted-foreground">
604+
The Tech@NYU Dev Team will showcase the Albert Plus
605+
project during our final presentation between December 7
606+
and December 12, 2025. Let us know if you'd like to join
607+
so we can send the exact date, time, and location details.
608+
</p>
609+
</div>
610+
{/* Final presentation invite */}
611+
<form.Field name="attendPresentation">
612+
{(field) => (
613+
<div className="space-y-2">
614+
<UIField
615+
orientation="horizontal"
616+
className="items-start gap-3"
617+
>
618+
<Checkbox
619+
id={field.name}
620+
checked={Boolean(field.state.value)}
621+
onCheckedChange={(checked) =>
622+
field.handleChange(checked === true)
623+
}
624+
aria-describedby={`${field.name}-description`}
625+
/>
626+
<FieldContent>
627+
<Label
628+
htmlFor={field.name}
629+
className="text-sm font-medium leading-snug"
630+
>
631+
I plan to attend the final presentation
632+
</Label>
633+
<FieldDescription id={`${field.name}-description`}>
634+
We'll email you an RSVP as soon as the schedule is
635+
finalized.
636+
</FieldDescription>
637+
</FieldContent>
638+
</UIField>
639+
<FieldError errors={field.state.meta.errors} />
640+
</div>
641+
)}
642+
</form.Field>
643+
</div>
644+
</div>
548645
</FieldGroup>
549646
</CardContent>
550647
<CardFooter>

apps/web/src/lib/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ const config = {
1010
],
1111
navBottom: [
1212
{ title: "Settings", url: "#settings", icon: Settings },
13-
{ title: "Feedback", url: "/feedback", icon: Send },
13+
{
14+
title: "Feedback",
15+
url: "https://techatnyu.featurebase.app/",
16+
icon: Send,
17+
},
1418
],
1519
},
1620
};

packages/server/convex/_generated/api.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import type * as schemas_courseOfferings from "../schemas/courseOfferings.js";
2121
import type * as schemas_courses from "../schemas/courses.js";
2222
import type * as schemas_programs from "../schemas/programs.js";
2323
import type * as schemas_schools from "../schemas/schools.js";
24+
import type * as schemas_studentInvites from "../schemas/studentInvites.js";
2425
import type * as schemas_students from "../schemas/students.js";
2526
import type * as schools from "../schools.js";
2627
import type * as scraper from "../scraper.js";
2728
import type * as seed from "../seed.js";
29+
import type * as studentInvites from "../studentInvites.js";
2830
import type * as students from "../students.js";
2931
import type * as userCourseOfferings from "../userCourseOfferings.js";
3032
import type * as userCourses from "../userCourses.js";
@@ -49,10 +51,12 @@ declare const fullApi: ApiFromModules<{
4951
"schemas/courses": typeof schemas_courses;
5052
"schemas/programs": typeof schemas_programs;
5153
"schemas/schools": typeof schemas_schools;
54+
"schemas/studentInvites": typeof schemas_studentInvites;
5255
"schemas/students": typeof schemas_students;
5356
schools: typeof schools;
5457
scraper: typeof scraper;
5558
seed: typeof seed;
59+
studentInvites: typeof studentInvites;
5660
students: typeof students;
5761
userCourseOfferings: typeof userCourseOfferings;
5862
userCourses: typeof userCourses;

packages/server/convex/schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { courses, prerequisites, userCourses } from "./schemas/courses";
88
import { programs, requirements } from "./schemas/programs";
99
import { schools } from "./schemas/schools";
10+
import { studentInvites } from "./schemas/studentInvites";
1011
import { students } from "./schemas/students";
1112

1213
export default defineSchema({
@@ -49,4 +50,5 @@ export default defineSchema({
4950
]),
5051
students: defineTable(students).index("by_user_id", ["userId"]),
5152
schools: defineTable(schools).index("by_name_level", ["name", "level"]),
53+
studentInvites: defineTable(studentInvites).index("by_user_id", ["userId"]),
5254
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { v } from "convex/values";
2+
3+
const studentInvites = {
4+
userId: v.string(),
5+
name: v.string(),
6+
email: v.string(),
7+
};
8+
9+
export { studentInvites };
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ConvexError } from "convex/values";
2+
import { omit } from "convex-helpers";
3+
import { protectedMutation, protectedQuery } from "./helpers/auth";
4+
import { studentInvites } from "./schemas/studentInvites";
5+
6+
export const getCurrentUserInvite = protectedQuery({
7+
args: {},
8+
handler: async (ctx) => {
9+
const invite = await ctx.db
10+
.query("studentInvites")
11+
.withIndex("by_user_id", (q) => q.eq("userId", ctx.user.subject))
12+
.unique();
13+
14+
return invite;
15+
},
16+
});
17+
18+
export const createInvite = protectedMutation({
19+
args: omit(studentInvites, ["userId"]),
20+
handler: async (ctx, args) => {
21+
// Check if invite already exists
22+
const existing = await ctx.db
23+
.query("studentInvites")
24+
.withIndex("by_user_id", (q) => q.eq("userId", ctx.user.subject))
25+
.unique();
26+
27+
if (existing) {
28+
throw new ConvexError("Invite already exists for this user");
29+
}
30+
31+
return await ctx.db.insert("studentInvites", {
32+
...args,
33+
userId: ctx.user.subject,
34+
});
35+
},
36+
});

0 commit comments

Comments
 (0)