Skip to content

Commit 75bf8d3

Browse files
authored
Merge pull request #660 from trycompai/lewis/comp-onboarding-calendar
[dev] [carhartlewis] lewis/comp-onboarding-calendar
2 parents d05bc9c + 797b89d commit 75bf8d3

File tree

15 files changed

+34106
-219
lines changed

15 files changed

+34106
-219
lines changed

apps/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"@aws-sdk/client-sts": "^3.808.0",
2424
"@aws-sdk/s3-request-presigner": "^3.806.0",
2525
"@browserbasehq/sdk": "^2.5.0",
26+
"@calcom/atoms": "^1.0.102-framer",
27+
"@calcom/embed-react": "^1.5.3",
2628
"@comp/data": "workspace:*",
2729
"@comp/db": "workspace:*",
2830
"@comp/notifications": "workspace:*",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"use server";
2+
3+
import { z } from "zod";
4+
import { authActionClient } from "@/actions/safe-action";
5+
import { db } from "@comp/db";
6+
import ky from "ky";
7+
8+
export const bookCallAction = authActionClient
9+
.schema(
10+
z.object({
11+
frameworks: z.array(z.string()),
12+
role: z.string(),
13+
employeeCount: z.string(),
14+
lookingFor: z.string(),
15+
timeline: z.string(),
16+
}),
17+
)
18+
.metadata({
19+
name: "book-call",
20+
})
21+
.action(
22+
async ({
23+
parsedInput: {
24+
frameworks,
25+
role,
26+
employeeCount,
27+
lookingFor,
28+
timeline,
29+
},
30+
ctx: { session },
31+
}) => {
32+
try {
33+
const { activeOrganizationId } = session;
34+
35+
if (!activeOrganizationId) {
36+
return {
37+
success: false,
38+
error: "Not authorized",
39+
};
40+
}
41+
42+
const bookingDetails = {
43+
frameworks,
44+
role,
45+
employeeCount,
46+
lookingFor,
47+
timeline,
48+
};
49+
50+
await db.onboarding.upsert({
51+
where: {
52+
organizationId: activeOrganizationId,
53+
},
54+
create: {
55+
callBooked: true,
56+
organizationId: activeOrganizationId,
57+
companyBookingDetails: bookingDetails,
58+
},
59+
update: {
60+
callBooked: true,
61+
companyBookingDetails: bookingDetails,
62+
},
63+
});
64+
65+
return {
66+
success: true,
67+
};
68+
} catch (error) {
69+
console.error("Error in bookCallAction:", error);
70+
71+
return {
72+
success: false,
73+
error: {
74+
message:
75+
error instanceof Error
76+
? error.message
77+
: "An unexpected error occurred.",
78+
},
79+
};
80+
}
81+
},
82+
);

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/book-call/components/FindOutMore.tsx

Lines changed: 441 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import PageWithBreadcrumb from "@/components/pages/PageWithBreadcrumb";
2+
import { FindOutMoreForm } from "./components/FindOutMore";
3+
import { getI18n } from "@/locales/server";
4+
import { auth } from "@/utils/auth";
5+
import { db } from "@comp/db";
6+
import type { Metadata } from "next";
7+
import { setStaticParamsLocale } from "next-international/server";
8+
import { headers } from "next/headers";
9+
import { cache } from "react";
10+
import { notFound } from "next/navigation";
11+
import { z } from "zod";
12+
13+
const findOutMoreSchema = z.object({
14+
frameworks: z.array(z.string()),
15+
role: z.string(),
16+
employeeCount: z.string(),
17+
lookingFor: z.string(),
18+
timeline: z.string(),
19+
});
20+
21+
export default async function FindOutMore({
22+
params,
23+
}: {
24+
params: Promise<{ locale: string, orgId: string }>;
25+
}) {
26+
const { locale, orgId } = await params;
27+
setStaticParamsLocale(locale);
28+
29+
const organization = await organizationDetails();
30+
31+
if (!organization) {
32+
return notFound();
33+
}
34+
35+
return (
36+
<PageWithBreadcrumb
37+
breadcrumbs={[
38+
{
39+
label: "Implementation",
40+
current: false,
41+
href: `/${orgId}/implementation`,
42+
},
43+
{ label: "Find out more about Comp AI", current: true },
44+
]}
45+
>
46+
<FindOutMoreForm
47+
organizationName={organization?.name ?? ""}
48+
isBooked={organization?.callBooked ?? false}
49+
companyBookingDetails={organization?.companyBookingDetails as z.infer<typeof findOutMoreSchema> ?? {
50+
frameworks: [],
51+
role: "",
52+
employeeCount: "",
53+
lookingFor: "",
54+
timeline: "",
55+
}}
56+
/>
57+
</PageWithBreadcrumb>
58+
);
59+
}
60+
61+
62+
const organizationDetails = cache(async () => {
63+
const session = await auth.api.getSession({
64+
headers: await headers(),
65+
});
66+
67+
if (!session?.session.activeOrganizationId) {
68+
return null;
69+
}
70+
71+
const organization = await db.organization.findUnique({
72+
where: { id: session?.session.activeOrganizationId },
73+
select: {
74+
name: true,
75+
id: true,
76+
website: true,
77+
},
78+
});
79+
80+
const onboarding = await db.onboarding.findFirst({
81+
where: {
82+
organizationId: session?.session.activeOrganizationId,
83+
},
84+
select: {
85+
callBooked: true,
86+
companyBookingDetails: true,
87+
},
88+
});
89+
90+
return {
91+
name: organization?.name,
92+
callBooked: onboarding?.callBooked,
93+
companyBookingDetails: onboarding?.companyBookingDetails as z.infer<typeof findOutMoreSchema>,
94+
};
95+
});

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/checklist-items.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import type { Onboarding } from "@comp/db/types";
22
import { Icons } from "@comp/ui/icons";
3-
import { Briefcase, ListCheck, NotebookText, Store, Users } from "lucide-react";
3+
import {
4+
Briefcase,
5+
Calendar,
6+
ListCheck,
7+
NotebookText,
8+
Store,
9+
Users,
10+
} from "lucide-react";
411
import type { ChecklistItemProps } from "./types";
512
import { companyDetailsObjectSchema } from "./lib/models/CompanyDetails";
613
import { z } from "zod";
@@ -10,6 +17,17 @@ export function generateChecklistItems(
1017
orgId: string,
1118
): ChecklistItemProps[] {
1219
return [
20+
{
21+
title: "Optional: Get SOC 2 or ISO 27001 in just 4 weeks",
22+
description: "Get SOC 2 compliant in just 4 weeks, implemented by the Comp AI team. Packages starting at $3,000/year - book a call and we'll share more..",
23+
href: `/${orgId}/implementation/calendar`,
24+
buttonLabel: "Get started",
25+
icon: <Calendar className="h-5 w-5" />,
26+
docs: "https://trycomp.ai/docs/call",
27+
completed: onboarding.callBooked,
28+
calendarPath: `/${orgId}/implementation/book-call`,
29+
type: "calendar",
30+
},
1331
{
1432
title: "Fill out company details",
1533
description:

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/components/Checklist.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function Checklist({ items }: { items: ChecklistItemProps[] }) {
77
return (
88
<div className="flex flex-col gap-4">
99
{items.map((item) => (
10-
<ChecklistItem key={`checklist-${item.dbColumn}`} {...item} />
10+
<ChecklistItem key={`checklist-${item.dbColumn}-${item?.type}`} {...item} />
1111
))}
1212
</div>
1313
);

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/components/ChecklistItem.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import {
99
CardTitle,
1010
} from "@comp/ui/card";
1111
import { Separator } from "@comp/ui/separator";
12-
import { ArrowRight, CheckCheck, Circle, Loader2 } from "lucide-react";
12+
import { ArrowRight, Calendar, CheckCheck, Circle, Loader2 } from "lucide-react";
1313
import { useParams, useRouter } from "next/navigation";
1414
import { useState } from "react";
1515
import { toast } from "sonner";
1616
import { updateOnboardingItem } from "../actions/update-onboarding-item";
1717
import type { ChecklistItemProps } from "../types";
18+
import CalendarEmbed from "@/components/calendar-embed";
1819

1920
export function ChecklistItem({
2021
title,
@@ -27,13 +28,15 @@ export function ChecklistItem({
2728
icon,
2829
type,
2930
wizardPath,
31+
calendarPath,
3032
}: ChecklistItemProps) {
3133
const { orgId } = useParams<{ orgId: string }>();
3234
const linkWithOrgReplaced = href
3335
? href.replace(":organizationId", orgId)
3436
: undefined;
3537
const [isUpdating, setIsUpdating] = useState(false);
3638
const [isAnimating, setIsAnimating] = useState(false);
39+
const [showCalendar, setShowCalendar] = useState(false);
3740
const router = useRouter();
3841

3942
const handleMarkAsDone = async () => {
@@ -67,6 +70,12 @@ export function ChecklistItem({
6770
}
6871
};
6972

73+
const handleCalendarRedirect = () => {
74+
if (calendarPath) {
75+
router.push(calendarPath.replace(":organizationId", orgId));
76+
}
77+
};
78+
7079
return (
7180
<Card>
7281
<div>
@@ -86,6 +95,8 @@ export function ChecklistItem({
8695
onClick={() => {
8796
if (type === "wizard") {
8897
handleWizardRedirect();
98+
} else if (type === "calendar") {
99+
handleCalendarRedirect();
89100
} else {
90101
handleMarkAsDone();
91102
}
@@ -94,9 +105,6 @@ export function ChecklistItem({
94105
View again <ArrowRight className="h-4 w-4" />
95106
</Button>
96107
)}
97-
{/* {completed && (
98-
<Badge variant="marketing">Completed</Badge>
99-
)} */}
100108
</CardTitle>
101109
{description && !completed && (
102110
<CardDescription className="text-sm text-muted-foreground flex flex-col gap-4">
@@ -117,6 +125,18 @@ export function ChecklistItem({
117125
{buttonLabel}
118126
<ArrowRight className="ml-1 h-4 w-4" />
119127
</Button>
128+
) : type === "calendar" ? (
129+
<>
130+
<Button
131+
variant={"secondary"}
132+
className="w-full sm:w-fit"
133+
disabled={isUpdating}
134+
onClick={handleCalendarRedirect}
135+
>
136+
{buttonLabel}
137+
<ArrowRight className="ml-1 h-4 w-4" />
138+
</Button>
139+
</>
120140
) : (
121141
<Button
122142
variant={"secondary"}

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ export default async function Page({
1717
}
1818

1919
return (
20-
<div className="space-y-6">
20+
<div className="space-y-6 max-w-[600px] mx-auto">
2121
<OnboardingProgress
2222
completedSteps={checklistData.completedItems}
2323
totalSteps={checklistData.totalItems}
2424
/>
25-
<Checklist items={checklistData.checklistItems} />
25+
<Checklist key={checklistData.checklistItems.length} items={checklistData.checklistItems} />
2626
</div>
2727
);
2828
}

apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/implementation/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const onboardingSteps = [
66
"vendors",
77
"risk",
88
"tasks",
9+
"callBooked",
910
] as const; // Use 'as const' for literal types
1011

1112
export type OnboardingStep = (typeof onboardingSteps)[number];
@@ -19,8 +20,10 @@ export interface ChecklistItemProps {
1920
completed?: boolean;
2021
buttonLabel: string;
2122
icon: React.ReactNode;
22-
type?: "default" | "wizard";
23+
type?: "default" | "wizard" | "calendar";
2324
wizardPath?: string;
25+
calendarPath?: string;
2426
// For wizards, allow specifying a completion boolean directly
2527
wizardCompleted?: boolean;
28+
calendarCompleted?: boolean;
2629
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use client";
2+
3+
import Cal, { getCalApi } from "@calcom/embed-react";
4+
import { useEffect } from "react";
5+
6+
export default function CalendarEmbed() {
7+
useEffect(() => {
8+
(async () => {
9+
const cal = await getCalApi({ namespace: "meet-us" });
10+
cal("ui", { hideEventTypeDetails: false, layout: "month_view" });
11+
})();
12+
}, []);
13+
14+
return (
15+
<Cal
16+
namespace="meet-us"
17+
calLink="team/compai/meet-us"
18+
style={{ width: "100%", height: "100%", overflow: "scroll" }}
19+
config={{ layout: "month_view" }}
20+
/>
21+
);
22+
}
23+

0 commit comments

Comments
 (0)