├── .eslintrc.json
├── .gitignore
├── README.md
├── app
├── (bookingPage)
│ └── [username]
│ │ └── [eventName]
│ │ └── page.tsx
├── actions.ts
├── api
│ ├── auth
│ │ ├── [...nextauth]
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── oauth
│ │ └── exchange
│ │ │ └── route.ts
│ └── uploadthing
│ │ ├── core.ts
│ │ └── route.ts
├── components
│ ├── SubmitButton.tsx
│ ├── TimeSlots.tsx
│ ├── dashboard
│ │ ├── CopyLinkMenuItem.tsx
│ │ ├── DasboardLinks.tsx
│ │ ├── EditEventTypeForm.tsx
│ │ ├── EmptyState.tsx
│ │ ├── EventTypeSwitcher.tsx
│ │ ├── ThemeProvider.tsx
│ │ ├── ThemeToggle.tsx
│ │ └── settingsForm.tsx
│ ├── demo
│ │ ├── Calendar.tsx
│ │ ├── CalendarButton.tsx
│ │ ├── CalendarCell.tsx
│ │ ├── CalendarGrid.tsx
│ │ ├── CalendarHeader.tsx
│ │ └── RenderCalendar.tsx
│ └── landingPage
│ │ ├── AuthModal.tsx
│ │ ├── Cta.tsx
│ │ ├── Features.tsx
│ │ ├── Hero.tsx
│ │ ├── Logos.tsx
│ │ ├── Navbar.tsx
│ │ ├── Testimonial.tsx
│ │ └── oldhomepage.tsx
├── dashboard
│ ├── availability
│ │ └── page.tsx
│ ├── event
│ │ └── [eventTypeId]
│ │ │ ├── delete
│ │ │ └── page.tsx
│ │ │ └── page.tsx
│ ├── layout.tsx
│ ├── meetings
│ │ └── page.tsx
│ ├── new
│ │ └── page.tsx
│ ├── page.tsx
│ └── settings
│ │ └── page.tsx
├── favicon.ico
├── fonts
│ ├── GeistMonoVF.woff
│ └── GeistVF.woff
├── globals.css
├── layout.tsx
├── lib
│ ├── auth.ts
│ ├── db.ts
│ ├── hooks.ts
│ ├── nylas.ts
│ ├── times.ts
│ ├── uploadthing.ts
│ └── zodSchemas.ts
├── meeting
│ └── [username]
│ │ └── [meetingName]
│ │ └── page.tsx
├── onboarding
│ ├── grant-id
│ │ └── page.tsx
│ └── page.tsx
├── page.tsx
├── success
│ └── page.tsx
└── test
│ └── page.tsx
├── components.json
├── components
└── ui
│ ├── ButtonGroup.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── calendar.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── sonner.tsx
│ ├── switch.tsx
│ ├── textarea.tsx
│ └── tooltip.tsx
├── lib
├── prisma.ts
└── utils.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prisma
└── schema.prisma
├── public
├── better.png
├── dashboard-new.png
├── face.jpeg
├── github.svg
├── google.svg
├── gradient.svg
├── hero.png
├── logo.png
├── meet.png
├── nextjs-logo.svg
├── nylas-logo.png
├── supabase.svg
├── teams.png
├── typescript-logo.png
├── vercel.svg
└── work-is-almost-over-happy.gif
├── tailwind.config.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "next/typescript"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/app/(bookingPage)/[username]/[eventName]/page.tsx:
--------------------------------------------------------------------------------
1 | import { createMeetingAction } from "@/app/actions";
2 | import { RenderCalendar } from "@/app/components/demo/RenderCalendar";
3 | import { SubmitButton } from "@/app/components/SubmitButton";
4 | import { TimeSlots } from "@/app/components/TimeSlots";
5 | import { Card, CardContent } from "@/components/ui/card";
6 | import { Input } from "@/components/ui/input";
7 | import { Label } from "@/components/ui/label";
8 | import { Separator } from "@/components/ui/separator";
9 | import prisma from "@/lib/prisma";
10 | import { format } from "date-fns";
11 | import { BookMarked, CalendarX2, Clock } from "lucide-react";
12 | import Image from "next/image";
13 | import { notFound } from "next/navigation";
14 | import React from "react";
15 |
16 | async function getData(username: string, eventName: string) {
17 | const eventType = await prisma.eventType.findFirst({
18 | where: {
19 | url: eventName,
20 | user: {
21 | username: username,
22 | },
23 | active: true,
24 | },
25 | select: {
26 | id: true,
27 | description: true,
28 | title: true,
29 | duration: true,
30 | videoCallSoftware: true,
31 |
32 | user: {
33 | select: {
34 | image: true,
35 | name: true,
36 | Availability: {
37 | select: {
38 | day: true,
39 | isActive: true,
40 | },
41 | },
42 | },
43 | },
44 | },
45 | });
46 |
47 | if (!eventType) {
48 | return notFound();
49 | }
50 |
51 | return eventType;
52 | }
53 |
54 | const BookingPage = async ({
55 | params,
56 | searchParams,
57 | }: {
58 | params: { username: string; eventName: string };
59 | searchParams: { date?: string; time?: string };
60 | }) => {
61 | const selectedDate = searchParams.date
62 | ? new Date(searchParams.date)
63 | : new Date();
64 | const eventType = await getData(params.username, params.eventName);
65 |
66 | const formattedDate = new Intl.DateTimeFormat("en-US", {
67 | weekday: "long",
68 | day: "numeric",
69 | month: "long",
70 | }).format(selectedDate);
71 |
72 | const showForm = !!searchParams.date && !!searchParams.time;
73 |
74 | return (
75 |
76 | {showForm ? (
77 |
78 |
79 |
80 |
87 |
88 | {eventType.user.name}
89 |
90 |
{eventType.title}
91 |
92 | {eventType.description}
93 |
94 |
95 |
96 |
97 |
98 |
99 | {formattedDate}
100 |
101 |
102 |
103 |
104 |
105 | {eventType.duration} Mins
106 |
107 |
108 |
109 |
110 |
111 | {eventType.videoCallSoftware}
112 |
113 |
114 |
115 |
116 |
120 |
121 |
146 |
147 |
148 | ) : (
149 |
150 |
151 |
152 |
159 |
160 | {eventType.user.name}
161 |
162 |
{eventType.title}
163 |
164 | {eventType.description}
165 |
166 |
167 |
168 |
169 |
170 | {formattedDate}
171 |
172 |
173 |
174 |
175 |
176 | {eventType.duration} Mins
177 |
178 |
179 |
180 |
181 |
182 | Google Meet
183 |
184 |
185 |
186 |
187 |
188 |
192 |
193 |
194 |
195 |
196 |
197 |
201 |
202 |
207 |
208 |
209 | )}
210 |
211 | );
212 | };
213 |
214 | export default BookingPage;
215 |
--------------------------------------------------------------------------------
/app/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { parseWithZod } from "@conform-to/zod";
4 | import prisma from "./lib/db";
5 | import { requireUser } from "./lib/hooks";
6 | import {
7 | aboutSettingsSchema,
8 | eventTypeSchema,
9 | EventTypeServerSchema,
10 | onboardingSchema,
11 | } from "./lib/zodSchemas";
12 | import { redirect } from "next/navigation";
13 |
14 | import { revalidatePath } from "next/cache";
15 | import { nylas } from "./lib/nylas";
16 |
17 | export async function onboardingAction(prevState: any, formData: FormData) {
18 | const session = await requireUser();
19 |
20 | const submission = await parseWithZod(formData, {
21 | schema: onboardingSchema({
22 | async isUsernameUnique() {
23 | const exisitngSubDirectory = await prisma.user.findUnique({
24 | where: {
25 | username: formData.get("username") as string,
26 | },
27 | });
28 | return !exisitngSubDirectory;
29 | },
30 | }),
31 |
32 | async: true,
33 | });
34 |
35 | if (submission.status !== "success") {
36 | return submission.reply();
37 | }
38 |
39 | const OnboardingData = await prisma.user.update({
40 | where: {
41 | id: session.user?.id,
42 | },
43 | data: {
44 | username: submission.value.username,
45 | name: submission.value.fullName,
46 | Availability: {
47 | createMany: {
48 | data: [
49 | {
50 | day: "Monday",
51 | fromTime: "08:00",
52 | tillTime: "18:00",
53 | },
54 | {
55 | day: "Tuesday",
56 | fromTime: "08:00",
57 | tillTime: "18:00",
58 | },
59 | {
60 | day: "Wednesday",
61 | fromTime: "08:00",
62 | tillTime: "18:00",
63 | },
64 | {
65 | day: "Thursday",
66 | fromTime: "08:00",
67 | tillTime: "18:00",
68 | },
69 | {
70 | day: "Friday",
71 | fromTime: "08:00",
72 | tillTime: "18:00",
73 | },
74 | {
75 | day: "Saturday",
76 | fromTime: "08:00",
77 | tillTime: "18:00",
78 | },
79 | {
80 | day: "Sunday",
81 | fromTime: "08:00",
82 | tillTime: "18:00",
83 | },
84 | ],
85 | },
86 | },
87 | },
88 | });
89 |
90 | return redirect("/onboarding/grant-id");
91 | }
92 |
93 | export async function SettingsAction(prevState: any, formData: FormData) {
94 | const session = await requireUser();
95 |
96 | const submission = parseWithZod(formData, {
97 | schema: aboutSettingsSchema,
98 | });
99 |
100 | if (submission.status !== "success") {
101 | return submission.reply();
102 | }
103 |
104 | const user = await prisma.user.update({
105 | where: {
106 | id: session.user?.id as string,
107 | },
108 | data: {
109 | name: submission.value.fullName,
110 | image: submission.value.profileImage,
111 | },
112 | });
113 |
114 | return redirect("/dashboard");
115 | }
116 |
117 | export async function CreateEventTypeAction(
118 | prevState: any,
119 | formData: FormData
120 | ) {
121 | const session = await requireUser();
122 |
123 | const submission = await parseWithZod(formData, {
124 | schema: EventTypeServerSchema({
125 | async isUrlUnique() {
126 | const data = await prisma.eventType.findFirst({
127 | where: {
128 | userId: session.user?.id,
129 | url: formData.get("url") as string,
130 | },
131 | });
132 | return !data;
133 | },
134 | }),
135 |
136 | async: true,
137 | });
138 | if (submission.status !== "success") {
139 | return submission.reply();
140 | }
141 |
142 | const data = await prisma.eventType.create({
143 | data: {
144 | title: submission.value.title,
145 | duration: submission.value.duration,
146 | url: submission.value.url,
147 | description: submission.value.description,
148 | userId: session.user?.id as string,
149 | videoCallSoftware: submission.value.videoCallSoftware,
150 | },
151 | });
152 |
153 | return redirect("/dashboard");
154 | }
155 |
156 | export async function EditEventTypeAction(prevState: any, formData: FormData) {
157 | const session = await requireUser();
158 |
159 | const submission = await parseWithZod(formData, {
160 | schema: EventTypeServerSchema({
161 | async isUrlUnique() {
162 | const data = await prisma.eventType.findFirst({
163 | where: {
164 | userId: session.user?.id,
165 | url: formData.get("url") as string,
166 | },
167 | });
168 | return !data;
169 | },
170 | }),
171 |
172 | async: true,
173 | });
174 |
175 | if (submission.status !== "success") {
176 | return submission.reply();
177 | }
178 |
179 | const data = await prisma.eventType.update({
180 | where: {
181 | id: formData.get("id") as string,
182 | userId: session.user?.id as string,
183 | },
184 | data: {
185 | title: submission.value.title,
186 | duration: submission.value.duration,
187 | url: submission.value.url,
188 | description: submission.value.description,
189 | videoCallSoftware: submission.value.videoCallSoftware,
190 | },
191 | });
192 |
193 | return redirect("/dashboard");
194 | }
195 |
196 | export async function DeleteEventTypeAction(formData: FormData) {
197 | const session = await requireUser();
198 |
199 | const data = await prisma.eventType.delete({
200 | where: {
201 | id: formData.get("id") as string,
202 | userId: session.user?.id as string,
203 | },
204 | });
205 |
206 | return redirect("/dashboard");
207 | }
208 |
209 | export async function updateEventTypeStatusAction(
210 | prevState: any,
211 | {
212 | eventTypeId,
213 | isChecked,
214 | }: {
215 | eventTypeId: string;
216 | isChecked: boolean;
217 | }
218 | ) {
219 | try {
220 | const session = await requireUser();
221 |
222 | const data = await prisma.eventType.update({
223 | where: {
224 | id: eventTypeId,
225 | userId: session.user?.id as string,
226 | },
227 | data: {
228 | active: isChecked,
229 | },
230 | });
231 |
232 | revalidatePath(`/dashboard`);
233 | return {
234 | status: "success",
235 | message: "EventType Status updated successfully",
236 | };
237 | } catch (error) {
238 | return {
239 | status: "error",
240 | message: "Something went wrong",
241 | };
242 | }
243 | }
244 |
245 | export async function updateAvailabilityAction(formData: FormData) {
246 | const session = await requireUser();
247 |
248 | const rawData = Object.fromEntries(formData.entries());
249 | const availabilityData = Object.keys(rawData)
250 | .filter((key) => key.startsWith("id-"))
251 | .map((key) => {
252 | const id = key.replace("id-", "");
253 | return {
254 | id,
255 | isActive: rawData[`isActive-${id}`] === "on",
256 | fromTime: rawData[`fromTime-${id}`] as string,
257 | tillTime: rawData[`tillTime-${id}`] as string,
258 | };
259 | });
260 |
261 | try {
262 | await prisma.$transaction(
263 | availabilityData.map((item) =>
264 | prisma.availability.update({
265 | where: { id: item.id },
266 | data: {
267 | isActive: item.isActive,
268 | fromTime: item.fromTime,
269 | tillTime: item.tillTime,
270 | },
271 | })
272 | )
273 | );
274 |
275 | revalidatePath("/dashboard/availability");
276 | return { status: "success", message: "Availability updated successfully" };
277 | } catch (error) {
278 | console.error("Error updating availability:", error);
279 | return { status: "error", message: "Failed to update availability" };
280 | }
281 | }
282 |
283 | export async function createMeetingAction(formData: FormData) {
284 | const getUserData = await prisma.user.findUnique({
285 | where: {
286 | username: formData.get("username") as string,
287 | },
288 | select: {
289 | grantEmail: true,
290 | grantId: true,
291 | },
292 | });
293 |
294 | if (!getUserData) {
295 | throw new Error("User not found");
296 | }
297 |
298 | const eventTypeData = await prisma.eventType.findUnique({
299 | where: {
300 | id: formData.get("eventTypeId") as string,
301 | },
302 | select: {
303 | title: true,
304 | description: true,
305 | },
306 | });
307 |
308 | const formTime = formData.get("fromTime") as string;
309 | const meetingLength = Number(formData.get("meetingLength"));
310 | const eventDate = formData.get("eventDate") as string;
311 |
312 | const startDateTime = new Date(`${eventDate}T${formTime}:00`);
313 |
314 | // Calculate the end time by adding the meeting length (in minutes) to the start time
315 | const endDateTime = new Date(startDateTime.getTime() + meetingLength * 60000);
316 |
317 | await nylas.events.create({
318 | identifier: getUserData?.grantId as string,
319 | requestBody: {
320 | title: eventTypeData?.title,
321 | description: eventTypeData?.description,
322 | when: {
323 | startTime: Math.floor(startDateTime.getTime() / 1000),
324 | endTime: Math.floor(endDateTime.getTime() / 1000),
325 | },
326 | conferencing: {
327 | autocreate: {},
328 | provider: "Google Meet",
329 | },
330 | participants: [
331 | {
332 | name: formData.get("name") as string,
333 | email: formData.get("email") as string,
334 | status: "yes",
335 | },
336 | ],
337 | },
338 | queryParams: {
339 | calendarId: getUserData?.grantEmail as string,
340 | notifyParticipants: true,
341 | },
342 | });
343 |
344 | return redirect(`/success`);
345 | }
346 |
347 | export async function cancelMeetingAction(formData: FormData) {
348 | const session = await requireUser();
349 |
350 | const userData = await prisma.user.findUnique({
351 | where: {
352 | id: session.user?.id as string,
353 | },
354 | select: {
355 | grantEmail: true,
356 | grantId: true,
357 | },
358 | });
359 |
360 | if (!userData) {
361 | throw new Error("User not found");
362 | }
363 |
364 | const data = await nylas.events.destroy({
365 | eventId: formData.get("eventId") as string,
366 | identifier: userData?.grantId as string,
367 | queryParams: {
368 | calendarId: userData?.grantEmail as string,
369 | },
370 | });
371 |
372 | revalidatePath("/dashboard/meetings");
373 | }
374 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from "@/app/lib/auth";
2 |
3 | export const { GET, POST } = handlers;
4 |
--------------------------------------------------------------------------------
/app/api/auth/route.ts:
--------------------------------------------------------------------------------
1 | import { nylas, nylasConfig } from "@/app/lib/nylas";
2 | import { redirect } from "next/navigation";
3 |
4 | export async function GET() {
5 | const authUrl = nylas.auth.urlForOAuth2({
6 | clientId: nylasConfig.clientId as string,
7 | redirectUri: nylasConfig.callbackUri,
8 | });
9 | return redirect(authUrl);
10 | }
11 |
--------------------------------------------------------------------------------
/app/api/oauth/exchange/route.ts:
--------------------------------------------------------------------------------
1 | import prisma from "@/app/lib/db";
2 | import { requireUser } from "@/app/lib/hooks";
3 | import { nylas, nylasConfig } from "@/app/lib/nylas";
4 |
5 | import { redirect } from "next/navigation";
6 | import { NextRequest } from "next/server";
7 |
8 | export async function GET(req: NextRequest) {
9 | console.log("Received callback from Nylas");
10 | const session = await requireUser();
11 | const url = new URL(req.url as string);
12 | const code = url.searchParams.get("code");
13 |
14 | if (!code) {
15 | return Response.json("No authorization code returned from Nylas", {
16 | status: 400,
17 | });
18 | }
19 | const codeExchangePayload = {
20 | clientSecret: nylasConfig.apiKey,
21 | clientId: nylasConfig.clientId as string,
22 | redirectUri: nylasConfig.callbackUri,
23 | code,
24 | };
25 |
26 | try {
27 | const response = await nylas.auth.exchangeCodeForToken(codeExchangePayload);
28 | const { grantId, email } = response;
29 |
30 | await prisma.user.update({
31 | where: {
32 | id: session.user?.id as string,
33 | },
34 | data: {
35 | grantId: grantId,
36 | grantEmail: email,
37 | },
38 | });
39 |
40 | console.log({ grantId });
41 | } catch (error) {
42 | console.error("Error exchanging code for token:", error);
43 | }
44 |
45 | redirect("/dashboard");
46 | }
47 |
--------------------------------------------------------------------------------
/app/api/uploadthing/core.ts:
--------------------------------------------------------------------------------
1 | import { createUploadthing, type FileRouter } from "uploadthing/next";
2 | import { UploadThingError } from "uploadthing/server";
3 |
4 | const f = createUploadthing();
5 |
6 | const auth = (req: Request) => ({ id: "fakeId" }); // Fake auth function
7 |
8 | // FileRouter for your app, can contain multiple FileRoutes
9 | export const ourFileRouter = {
10 | // Define as many FileRoutes as you like, each with a unique routeSlug
11 | imageUploader: f({ image: { maxFileSize: "4MB" } })
12 | // Set permissions and file types for this FileRoute
13 | .middleware(async ({ req }) => {
14 | // This code runs on your server before upload
15 | const user = await auth(req);
16 |
17 | // If you throw, the user will not be able to upload
18 | if (!user) throw new UploadThingError("Unauthorized");
19 |
20 | // Whatever is returned here is accessible in onUploadComplete as `metadata`
21 | return { userId: user.id };
22 | })
23 | .onUploadComplete(async ({ metadata, file }) => {
24 | // This code RUNS ON YOUR SERVER after upload
25 | console.log("Upload complete for userId:", metadata.userId);
26 |
27 | console.log("file url", file.url);
28 |
29 | // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
30 | return { uploadedBy: metadata.userId };
31 | }),
32 | } satisfies FileRouter;
33 |
34 | export type OurFileRouter = typeof ourFileRouter;
35 |
--------------------------------------------------------------------------------
/app/api/uploadthing/route.ts:
--------------------------------------------------------------------------------
1 | import { createRouteHandler } from "uploadthing/next";
2 |
3 | import { ourFileRouter } from "./core";
4 |
5 | // Export routes for Next App Router
6 | export const { GET, POST } = createRouteHandler({
7 | router: ourFileRouter,
8 |
9 | // Apply an (optional) custom config:
10 | // config: { ... },
11 | });
12 |
--------------------------------------------------------------------------------
/app/components/SubmitButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { cn } from "@/lib/utils";
5 | import { Loader2 } from "lucide-react";
6 | import Image from "next/image";
7 | import { useFormStatus } from "react-dom";
8 | import GithubLogo from "@/public/github.svg";
9 | import GoogleLogo from "@/public/google.svg";
10 | interface iAppProps {
11 | text: string;
12 | variant?:
13 | | "default"
14 | | "destructive"
15 | | "outline"
16 | | "secondary"
17 | | "ghost"
18 | | "link"
19 | | null
20 | | undefined;
21 |
22 | className?: string;
23 | }
24 |
25 | export function SubmitButton({ text, variant, className }: iAppProps) {
26 | const { pending } = useFormStatus();
27 |
28 | return (
29 | <>
30 | {pending ? (
31 |
32 | Please wait
33 |
34 | ) : (
35 |
40 | {text}
41 |
42 | )}
43 | >
44 | );
45 | }
46 |
47 | export function GitHubAuthButton() {
48 | const { pending } = useFormStatus();
49 | return (
50 | <>
51 | {pending ? (
52 |
53 | Please wait
54 |
55 | ) : (
56 |
57 |
62 | Sign in with GitHub
63 |
64 | )}
65 | >
66 | );
67 | }
68 |
69 | export function GoogleAuthButton() {
70 | const { pending } = useFormStatus();
71 | return (
72 | <>
73 | {pending ? (
74 |
75 | Please wait
76 |
77 | ) : (
78 |
79 |
80 | Sign in with Google
81 |
82 | )}
83 | >
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/app/components/TimeSlots.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | addMinutes,
3 | format,
4 | fromUnixTime,
5 | isAfter,
6 | isBefore,
7 | parse,
8 | } from "date-fns";
9 | import prisma from "../lib/db";
10 | import { Prisma } from "@prisma/client";
11 | import { nylas } from "../lib/nylas";
12 | import Link from "next/link";
13 | import { Button } from "@/components/ui/button";
14 | import { NylasResponse, GetFreeBusyResponse } from "nylas";
15 |
16 | interface iappProps {
17 | selectedDate: Date;
18 | userName: string;
19 | meetingDuration: number;
20 | }
21 |
22 | async function getAvailability(selectedDate: Date, userName: string) {
23 | const currentDay = format(selectedDate, "EEEE");
24 |
25 | const startOfDay = new Date(selectedDate);
26 | startOfDay.setHours(0, 0, 0, 0);
27 | const endOfDay = new Date(selectedDate);
28 | endOfDay.setHours(23, 59, 59, 999);
29 | const data = await prisma.availability.findFirst({
30 | where: {
31 | day: currentDay as Prisma.EnumDayFilter,
32 | User: {
33 | username: userName,
34 | },
35 | },
36 | select: {
37 | fromTime: true,
38 | tillTime: true,
39 | id: true,
40 | User: {
41 | select: {
42 | grantEmail: true,
43 | grantId: true,
44 | },
45 | },
46 | },
47 | });
48 |
49 | const nylasCalendarData = await nylas.calendars.getFreeBusy({
50 | identifier: data?.User.grantId as string,
51 | requestBody: {
52 | startTime: Math.floor(startOfDay.getTime() / 1000),
53 | endTime: Math.floor(endOfDay.getTime() / 1000),
54 | emails: [data?.User.grantEmail as string],
55 | },
56 | });
57 |
58 | return { data, nylasCalendarData };
59 | }
60 |
61 | function calculateAvailableTimeSlots(
62 | dbAvailability: {
63 | fromTime: string | undefined;
64 | tillTime: string | undefined;
65 | },
66 | nylasData: NylasResponse,
67 | date: string,
68 | duration: number
69 | ) {
70 | const now = new Date(); // Get the current time
71 |
72 | // Convert DB availability to Date objects
73 | const availableFrom = parse(
74 | `${date} ${dbAvailability.fromTime}`,
75 | "yyyy-MM-dd HH:mm",
76 | new Date()
77 | );
78 | const availableTill = parse(
79 | `${date} ${dbAvailability.tillTime}`,
80 | "yyyy-MM-dd HH:mm",
81 | new Date()
82 | );
83 |
84 | // Extract busy slots from Nylas data
85 | const busySlots = nylasData.data[0].timeSlots.map((slot: any) => ({
86 | start: fromUnixTime(slot.startTime),
87 | end: fromUnixTime(slot.endTime),
88 | }));
89 |
90 | // Generate all possible 30-minute slots within the available time
91 | const allSlots = [];
92 | let currentSlot = availableFrom;
93 | while (isBefore(currentSlot, availableTill)) {
94 | allSlots.push(currentSlot);
95 | currentSlot = addMinutes(currentSlot, duration);
96 | }
97 |
98 | // Filter out busy slots and slots before the current time
99 | const freeSlots = allSlots.filter((slot) => {
100 | const slotEnd = addMinutes(slot, duration);
101 | return (
102 | isAfter(slot, now) && // Ensure the slot is after the current time
103 | !busySlots.some(
104 | (busy: { start: any; end: any }) =>
105 | (!isBefore(slot, busy.start) && isBefore(slot, busy.end)) ||
106 | (isAfter(slotEnd, busy.start) && !isAfter(slotEnd, busy.end)) ||
107 | (isBefore(slot, busy.start) && isAfter(slotEnd, busy.end))
108 | )
109 | );
110 | });
111 |
112 | // Format the free slots
113 | return freeSlots.map((slot) => format(slot, "HH:mm"));
114 | }
115 |
116 | export async function TimeSlots({
117 | selectedDate,
118 | userName,
119 | meetingDuration,
120 | }: iappProps) {
121 | const { data, nylasCalendarData } = await getAvailability(
122 | selectedDate,
123 | userName
124 | );
125 |
126 | const dbAvailability = { fromTime: data?.fromTime, tillTime: data?.tillTime };
127 |
128 | const formattedDate = format(selectedDate, "yyyy-MM-dd");
129 |
130 | const availableSlots = calculateAvailableTimeSlots(
131 | dbAvailability,
132 | nylasCalendarData,
133 | formattedDate,
134 | meetingDuration
135 | );
136 |
137 | return (
138 |
139 |
140 | {format(selectedDate, "EEE")}.{" "}
141 |
142 | {format(selectedDate, "MMM. d")}
143 |
144 |
145 |
146 |
147 | {availableSlots.length > 0 ? (
148 | availableSlots.map((slot, index) => (
149 |
153 |
154 | {slot}
155 |
156 |
157 | ))
158 | ) : (
159 |
No available time slots for this date.
160 | )}
161 |
162 |
163 | );
164 | }
165 |
--------------------------------------------------------------------------------
/app/components/dashboard/CopyLinkMenuItem.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
4 | import { Link2 } from "lucide-react";
5 | import { toast } from "sonner";
6 |
7 | interface CopyLinkMenuItemProps {
8 | meetingUrl: string;
9 | }
10 |
11 | export function CopyLinkMenuItem({ meetingUrl }: CopyLinkMenuItemProps) {
12 | const handleCopy = async () => {
13 | try {
14 | await navigator.clipboard.writeText(meetingUrl);
15 | toast.success("URL copied to clipboard");
16 | } catch (err) {
17 | console.error("Could not copy text: ", err);
18 | toast.error("Failed to copy URL");
19 | }
20 | };
21 |
22 | return (
23 |
24 |
25 | Copy
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/app/components/dashboard/DasboardLinks.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { CalendarCheck, HomeIcon, Settings, Users2 } from "lucide-react";
5 | import Link from "next/link";
6 | import { usePathname } from "next/navigation";
7 | import React from "react";
8 |
9 | export const dashboardLinks = [
10 | {
11 | id: 0,
12 | name: "Event Types",
13 | href: "/dashboard",
14 | icon: HomeIcon,
15 | },
16 | {
17 | id: 1,
18 | name: "Meetings",
19 | href: "/dashboard/meetings",
20 | icon: Users2,
21 | },
22 | {
23 | id: 2,
24 | name: "Availablity",
25 | href: "/dashboard/availability",
26 | icon: CalendarCheck,
27 | },
28 | {
29 | id: 3,
30 | name: "Settings",
31 | href: "/dashboard/settings",
32 | icon: Settings,
33 | },
34 | ];
35 |
36 | export function DasboardLinks() {
37 | const pathname = usePathname();
38 | return (
39 | <>
40 | {dashboardLinks.map((link) => (
41 |
51 |
52 | {link.name}
53 |
54 | ))}
55 | >
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/app/components/dashboard/EditEventTypeForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import { Input } from "@/components/ui/input";
13 | import { Label } from "@/components/ui/label";
14 | import { Textarea } from "@/components/ui/textarea";
15 | import Link from "next/link";
16 | import { SubmitButton } from "../SubmitButton";
17 | import { useFormState } from "react-dom";
18 | import { useForm } from "@conform-to/react";
19 | import { parseWithZod } from "@conform-to/zod";
20 | import { eventTypeSchema } from "@/app/lib/zodSchemas";
21 | import { EditEventTypeAction } from "@/app/actions";
22 | import {
23 | Select,
24 | SelectContent,
25 | SelectGroup,
26 | SelectItem,
27 | SelectLabel,
28 | SelectTrigger,
29 | SelectValue,
30 | } from "@/components/ui/select";
31 | import { ButtonGroup } from "@/components/ui/ButtonGroup";
32 | import { useState } from "react";
33 |
34 | interface iAppProps {
35 | id: string;
36 | title: string;
37 | url: string;
38 | description: string;
39 | duration: number;
40 | callProvider: string;
41 | }
42 |
43 | type Platform = "Zoom Meeting" | "Google Meet" | "Microsoft Teams";
44 |
45 | export function EditEventTypeForm({
46 | description,
47 | duration,
48 | title,
49 | url,
50 | callProvider,
51 | id,
52 | }: iAppProps) {
53 | const [lastResult, action] = useFormState(EditEventTypeAction, undefined);
54 | const [form, fields] = useForm({
55 | // Sync the result of last submission
56 | lastResult,
57 |
58 | // Reuse the validation logic on the client
59 | onValidate({ formData }) {
60 | return parseWithZod(formData, { schema: eventTypeSchema });
61 | },
62 |
63 | // Validate the form on blur event triggered
64 | shouldValidate: "onBlur",
65 | shouldRevalidate: "onInput",
66 | });
67 | const [activePlatform, setActivePlatform] = useState(
68 | callProvider as Platform
69 | );
70 |
71 | const togglePlatform = (platform: Platform) => {
72 | setActivePlatform(platform);
73 | };
74 | return (
75 |
76 |
77 |
78 | Add new appointment type
79 |
80 | Create a new appointment type that allows people to book times.
81 |
82 |
83 |
203 |
204 |
205 | );
206 | }
207 |
--------------------------------------------------------------------------------
/app/components/dashboard/EmptyState.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Ban, FileIcon, PlusCircle } from "lucide-react";
3 | import Link from "next/link";
4 |
5 | interface iAppProps {
6 | title: string;
7 | description: string;
8 | buttonText: string;
9 | href: string;
10 | }
11 |
12 | export function EmptyState({
13 | buttonText,
14 | description,
15 | href,
16 | title,
17 | }: iAppProps) {
18 | return (
19 |
20 |
21 |
22 |
23 |
{title}
24 |
25 | {description}
26 |
27 |
28 |
29 |
30 | {buttonText}
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/app/components/dashboard/EventTypeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { updateEventTypeStatusAction } from "@/app/actions";
4 | import { Switch } from "@/components/ui/switch";
5 | import React, { useEffect, useTransition } from "react";
6 | import { useFormState } from "react-dom";
7 | import { toast } from "sonner";
8 |
9 | export function MenuActiveSwitcher({
10 | initialChecked,
11 | eventTypeId,
12 | }: {
13 | eventTypeId: string;
14 | initialChecked: boolean;
15 | }) {
16 | const [isPending, startTransition] = useTransition();
17 | const [state, action] = useFormState(updateEventTypeStatusAction, undefined);
18 |
19 | useEffect(() => {
20 | if (state?.status === "success") {
21 | toast.success(state.message);
22 | } else if (state?.status === "error") {
23 | toast.error(state.message);
24 | }
25 | }, [state]);
26 |
27 | return (
28 | {
32 | startTransition(() => {
33 | action({
34 | isChecked: isChecked,
35 | eventTypeId,
36 | });
37 | });
38 | }}
39 | />
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/app/components/dashboard/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import { type ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children} ;
9 | }
10 |
--------------------------------------------------------------------------------
/app/components/dashboard/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Moon, Sun } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 |
15 | export function ThemeToggle() {
16 | const { setTheme } = useTheme();
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | Toggle theme
25 |
26 |
27 |
28 | setTheme("light")}>
29 | Light
30 |
31 | setTheme("dark")}>
32 | Dark
33 |
34 | setTheme("system")}>
35 | System
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/components/dashboard/settingsForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { SettingsAction } from "@/app/actions";
5 | import { aboutSettingsSchema } from "@/app/lib/zodSchemas";
6 | import {
7 | Card,
8 | CardContent,
9 | CardDescription,
10 | CardFooter,
11 | CardHeader,
12 | CardTitle,
13 | } from "@/components/ui/card";
14 | import { Input } from "@/components/ui/input";
15 | import { Label } from "@/components/ui/label";
16 | import { Textarea } from "@/components/ui/textarea";
17 | import { useForm } from "@conform-to/react";
18 | import { parseWithZod } from "@conform-to/zod";
19 | import { useFormState } from "react-dom";
20 | import { SubmitButton } from "../SubmitButton";
21 | import { UploadDropzone } from "@/app/lib/uploadthing";
22 | import Image from "next/image";
23 | import { X } from "lucide-react";
24 | import { Button } from "@/components/ui/button";
25 | import { toast } from "sonner";
26 |
27 | interface iAppProps {
28 | fullName: string;
29 | email: string;
30 |
31 | profileImage: string;
32 | }
33 |
34 | export function SettingsForm({ fullName, email, profileImage }: iAppProps) {
35 | const [lastResult, action] = useFormState(SettingsAction, undefined);
36 | const [currentProfileImage, setCurrentProfileImage] = useState(profileImage);
37 |
38 | const [form, fields] = useForm({
39 | // Sync the result of last submission
40 | lastResult,
41 |
42 | // Reuse the validation logic on the client
43 | onValidate({ formData }) {
44 | return parseWithZod(formData, { schema: aboutSettingsSchema });
45 | },
46 |
47 | // Validate the form on blur event triggered
48 | shouldValidate: "onBlur",
49 | shouldRevalidate: "onInput",
50 | });
51 |
52 | const handleDeleteImage = () => {
53 | setCurrentProfileImage("");
54 | };
55 |
56 | return (
57 |
58 |
59 | Settings
60 | Manage your account settings.
61 |
62 |
63 |
64 |
65 |
Full Name
66 |
72 |
{fields.fullName.errors}
73 |
74 |
75 | Email
76 |
77 |
78 |
79 |
80 |
86 |
Profile Image
87 | {currentProfileImage ? (
88 |
89 |
96 |
103 |
104 |
105 |
106 | ) : (
107 |
{
113 | setCurrentProfileImage(res[0].url);
114 | toast.success("Profile image uploaded");
115 | }}
116 | onUploadError={(error) => {
117 | toast.error(error.message);
118 | }}
119 | />
120 | )}
121 | {fields.profileImage.errors}
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | );
130 | }
131 |
--------------------------------------------------------------------------------
/app/components/demo/Calendar.tsx:
--------------------------------------------------------------------------------
1 | import { createCalendar } from "@internationalized/date";
2 | import { CalendarProps, DateValue, useCalendar, useLocale } from "react-aria";
3 | import { useCalendarState } from "react-stately";
4 | import { CalendarHeader } from "./CalendarHeader";
5 | import { CalendarGrid } from "./CalendarGrid";
6 |
7 | export function Calendar(
8 | props: CalendarProps & {
9 | isDateUnavailable?: (date: DateValue) => boolean;
10 | }
11 | ) {
12 | const { locale } = useLocale();
13 | const state = useCalendarState({
14 | ...props,
15 | visibleDuration: { months: 1 },
16 | locale,
17 | createCalendar,
18 | });
19 |
20 | const { calendarProps, prevButtonProps, nextButtonProps } = useCalendar(
21 | props,
22 | state
23 | );
24 | return (
25 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/components/demo/CalendarButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { type AriaButtonProps, useButton } from "@react-aria/button";
3 | import { useFocusRing } from "@react-aria/focus";
4 | import { mergeProps } from "@react-aria/utils";
5 | import type { CalendarState } from "@react-stately/calendar";
6 | import { useRef } from "react";
7 |
8 | export function CalendarButton(
9 | props: AriaButtonProps<"button"> & {
10 | state?: CalendarState;
11 | side?: "left" | "right";
12 | }
13 | ) {
14 | const ref = useRef(null);
15 | const { buttonProps } = useButton(props, ref);
16 | const { focusProps, isFocusVisible } = useFocusRing();
17 | return (
18 |
25 | {props.children}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/app/components/demo/CalendarCell.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import {
3 | CalendarDate,
4 | getLocalTimeZone,
5 | isSameMonth,
6 | isToday,
7 | } from "@internationalized/date";
8 | import { useRef } from "react";
9 | import { mergeProps, useCalendarCell, useFocusRing } from "react-aria";
10 | import { CalendarState } from "react-stately";
11 |
12 | export function CalendarCell({
13 | state,
14 | date,
15 | currentMonth,
16 | isUnavailable,
17 | }: {
18 | state: CalendarState;
19 | date: CalendarDate;
20 | currentMonth: CalendarDate;
21 | isUnavailable?: boolean;
22 | }) {
23 | const ref = useRef(null);
24 | const { cellProps, buttonProps, isSelected, isDisabled, formattedDate } =
25 | useCalendarCell({ date }, state, ref);
26 |
27 | // Override isDisabled if the date is unavailable
28 | const finalIsDisabled = isDisabled || isUnavailable;
29 |
30 | const { focusProps, isFocusVisible } = useFocusRing();
31 |
32 | const isOutsideMonth = !isSameMonth(currentMonth, date);
33 |
34 | const isDateToday = isToday(date, getLocalTimeZone());
35 |
36 | return (
37 |
41 |
47 |
58 | {formattedDate}
59 | {isDateToday && (
60 |
66 | )}
67 |
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/app/components/demo/CalendarGrid.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DateDuration,
3 | endOfMonth,
4 | getWeeksInMonth,
5 | } from "@internationalized/date";
6 | import { DateValue, useCalendarGrid, useLocale } from "react-aria";
7 | import { CalendarState } from "react-stately";
8 | import { CalendarCell } from "./CalendarCell";
9 |
10 | export function CalendarGrid({
11 | state,
12 | offset = {},
13 | isDateUnavailable,
14 | }: {
15 | state: CalendarState;
16 | offset?: DateDuration;
17 | isDateUnavailable?: (date: DateValue) => boolean;
18 | }) {
19 | const startDate = state.visibleRange.start.add(offset);
20 | const endDate = endOfMonth(startDate);
21 | const { locale } = useLocale();
22 | const { gridProps, headerProps, weekDays } = useCalendarGrid(
23 | {
24 | startDate,
25 | endDate,
26 | weekdayStyle: "short",
27 | },
28 | state
29 | );
30 |
31 | // Get the number of weeks in the month so we can render the proper number of rows.
32 | const weeksInMonth = getWeeksInMonth(startDate, locale);
33 | return (
34 |
35 |
36 |
37 | {weekDays.map((day, index) => (
38 | {day}
39 | ))}
40 |
41 |
42 |
43 | {Array.from({ length: weeksInMonth }, (_, weekIndex) => (
44 |
45 | {state
46 | .getDatesInWeek(weekIndex, startDate)
47 | .map((date, i) =>
48 | date ? (
49 |
56 | ) : (
57 |
58 | )
59 | )}
60 |
61 | ))}
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/app/components/demo/CalendarHeader.tsx:
--------------------------------------------------------------------------------
1 | import type { AriaButtonProps } from "@react-aria/button";
2 | import { useDateFormatter } from "@react-aria/i18n";
3 | import { VisuallyHidden } from "@react-aria/visually-hidden";
4 | import type { CalendarState } from "@react-stately/calendar";
5 | import type { DOMAttributes, FocusableElement } from "@react-types/shared";
6 | import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
7 | import { CalendarButton } from "./CalendarButton";
8 |
9 | export function CalendarHeader({
10 | state,
11 | calendarProps,
12 | prevButtonProps,
13 | nextButtonProps,
14 | }: {
15 | state: CalendarState;
16 | calendarProps: DOMAttributes;
17 | prevButtonProps: AriaButtonProps<"button">;
18 | nextButtonProps: AriaButtonProps<"button">;
19 | }) {
20 | const monthDateFormatter = useDateFormatter({
21 | month: "short",
22 | year: "numeric",
23 | timeZone: state.timeZone,
24 | });
25 |
26 | const [monthName, _, year] = monthDateFormatter
27 | .formatToParts(state.visibleRange.start.toDate(state.timeZone))
28 | .map((part) => part.value);
29 |
30 | return (
31 |
32 |
33 | {calendarProps["aria-label"]}
34 |
35 |
36 |
37 | {monthName}{" "}
38 |
39 | {year}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/app/components/demo/RenderCalendar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter, useSearchParams } from "next/navigation";
4 | import { Calendar } from "./Calendar";
5 | import { useState, useEffect } from "react";
6 | import {
7 | CalendarDate,
8 | DateValue,
9 | getLocalTimeZone,
10 | today,
11 | parseDate,
12 | } from "@internationalized/date";
13 |
14 | interface iAppProps {
15 | daysofWeek: { day: string; isActive: boolean }[];
16 | }
17 |
18 | export function RenderCalendar({ daysofWeek }: iAppProps) {
19 | const router = useRouter();
20 | const searchParams = useSearchParams();
21 |
22 | const [date, setDate] = useState(() => {
23 | const dateParam = searchParams.get("date");
24 | return dateParam ? parseDate(dateParam) : today(getLocalTimeZone());
25 | });
26 |
27 | useEffect(() => {
28 | const dateParam = searchParams.get("date");
29 | if (dateParam) {
30 | setDate(parseDate(dateParam));
31 | }
32 | }, [searchParams]);
33 |
34 | const handleChangeDate = (date: DateValue) => {
35 | console.log(date);
36 | setDate(date as CalendarDate);
37 | const url = new URL(window.location.href);
38 |
39 | url.searchParams.set("date", date.toString());
40 |
41 | router.push(url.toString());
42 | };
43 |
44 | const isDateUnavailable = (date: DateValue) => {
45 | const dayOfWeek = date.toDate(getLocalTimeZone()).getDay();
46 | // Adjust the index to match the daysofWeek array
47 | const adjustedIndex = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
48 | return !daysofWeek[adjustedIndex].isActive;
49 | };
50 |
51 | return (
52 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/app/components/landingPage/AuthModal.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogHeader,
6 | DialogTrigger,
7 | } from "@/components/ui/dialog";
8 |
9 | import Logo from "@/public/logo.png";
10 | import Image from "next/image";
11 | import GooleLogo from "@/public/google.svg";
12 | import GitHubLogo from "@/public/github.svg";
13 |
14 | import { signIn } from "@/app/lib/auth";
15 | import { GitHubAuthButton, GoogleAuthButton } from "../SubmitButton";
16 |
17 | export function AuthModal() {
18 | return (
19 |
20 |
21 | Try for Free
22 |
23 |
24 |
25 |
26 |
27 | CalMarshal
28 |
29 |
30 |
31 |
{
34 | "use server";
35 | await signIn("google");
36 | }}
37 | >
38 |
39 |
40 |
41 | {
44 | "use server";
45 | await signIn("github");
46 | }}
47 | >
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/app/components/landingPage/Cta.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 |
3 | export function CTA() {
4 | return (
5 |
6 |
7 |
8 | Start using CalMarshal Now!
9 |
10 |
11 | CalMarshal makes it easy for your clients to scheduale a meeting with
12 | you clients.
13 |
14 |
15 | Get Started Today
16 |
17 | {/* gradient svg */}
18 |
23 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/components/landingPage/Features.tsx:
--------------------------------------------------------------------------------
1 | import { CloudRain } from "lucide-react";
2 |
3 | const features = [
4 | {
5 | name: "Sign up for free",
6 | description:
7 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna.",
8 | icon: CloudRain,
9 | },
10 | {
11 | name: "Balzing fast",
12 | description:
13 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna.",
14 | icon: CloudRain,
15 | },
16 | {
17 | name: "Super secure with Nylas",
18 | description:
19 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna.",
20 | icon: CloudRain,
21 | },
22 | {
23 | name: "Easy to use",
24 | description:
25 | "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna.",
26 | icon: CloudRain,
27 | },
28 | ];
29 |
30 | export function Features() {
31 | return (
32 |
33 |
34 |
Schedule faster
35 |
36 | Schedule meetings in minutes
37 |
38 |
39 | With CalMarshal you can schedule meetings in minutes. We make it easy
40 | for you to schedule meetings in minutes. The meetings are very fast
41 | and easy to schedule.
42 |
43 |
44 |
45 |
46 |
47 | {features.map((feature) => (
48 |
49 |
50 |
51 |
52 |
53 | {feature.name}
54 |
55 |
56 | {feature.description}
57 |
58 |
59 | ))}
60 |
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/app/components/landingPage/Hero.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import HeroImage from "@/public/better.png";
4 | import { AuthModal } from "./AuthModal";
5 |
6 | export function Hero() {
7 | return (
8 |
9 |
10 |
11 |
12 | Introducing CalMarshal 1.0
13 |
14 |
15 |
16 | Scheduling made{" "}
17 | super easy!
18 |
19 |
20 |
21 | Scheduling a meeting can be a pain. But we at CalMarshal make it
22 | easy for your clients to schedule meetings with you.
23 |
24 |
27 |
28 |
29 |
30 |
39 |
40 |
41 |
45 |
49 |
53 |
57 |
58 |
59 |
60 |
69 |
70 |
76 |
80 |
81 |
82 |
83 |
84 |
90 |
91 |
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/app/components/landingPage/Logos.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import NylasLogo from "@/public/nylas-logo.png";
3 | import NextjsLogo from "@/public/nextjs-logo.svg";
4 | import vercelLogo from "@/public/vercel.svg";
5 |
6 | export function Logos() {
7 | return (
8 |
9 |
10 | Trusted by the best companies in the world
11 |
12 |
13 |
18 |
23 |
28 |
33 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/app/components/landingPage/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import Logo from "@/public/logo.png";
4 |
5 | import { ThemeToggle } from "../dashboard/ThemeToggle";
6 | import { AuthModal } from "./AuthModal";
7 | export function Navbar() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | CalMarshal
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/app/components/landingPage/Testimonial.tsx:
--------------------------------------------------------------------------------
1 | import { SVGProps } from "react";
2 |
3 | export function Testimonial() {
4 | return (
5 | <>
6 |
7 | {/* Blockquote */}
8 |
9 |
10 |
11 |
12 |
13 |
22 |
26 |
27 |
28 | Wow CalMarshal is the best platform i have ever used. Definitely
29 | Recommend it to you guys, try it out!
30 |
31 |
32 |
33 |
39 |
40 | {/* End Blockquote */}
41 |
42 | >
43 | );
44 | }
45 |
46 | const YouTube = (props: SVGProps) => (
47 |
54 |
60 |
61 |
66 |
71 |
72 |
73 |
74 |
81 |
88 |
95 |
102 |
109 |
116 |
123 |
124 |
125 |
126 |
127 | );
128 |
--------------------------------------------------------------------------------
/app/dashboard/availability/page.tsx:
--------------------------------------------------------------------------------
1 | import { SubmitButton } from "@/app/components/SubmitButton";
2 | import prisma from "@/app/lib/db";
3 | import { times } from "@/app/lib/times";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import {
13 | Select,
14 | SelectContent,
15 | SelectGroup,
16 | SelectItem,
17 | SelectTrigger,
18 | SelectValue,
19 | } from "@/components/ui/select";
20 | import { Switch } from "@/components/ui/switch";
21 | import { notFound } from "next/navigation";
22 | import React from "react";
23 | import { requireUser } from "@/app/lib/hooks";
24 | import { updateAvailabilityAction } from "@/app/actions";
25 |
26 | async function getData(userId: string) {
27 | const data = await prisma.availability.findMany({
28 | where: {
29 | userId: userId,
30 | },
31 | });
32 |
33 | if (!data) {
34 | return notFound();
35 | }
36 |
37 | return data;
38 | }
39 |
40 | const AvailabilityPage = async () => {
41 | const session = await requireUser();
42 | const data = await getData(session.user?.id as string);
43 |
44 | return (
45 |
46 |
47 | Availability
48 |
49 | In this section you can manage your availability.
50 |
51 |
52 |
53 |
54 | {data.map((item) => (
55 |
59 |
60 |
61 |
65 |
{item.day}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | {times.map((time) => (
74 |
75 | {time.time}
76 |
77 | ))}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | {times.map((time) => (
88 |
89 | {time.time}
90 |
91 | ))}
92 |
93 |
94 |
95 |
96 | ))}
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | export default AvailabilityPage;
107 |
--------------------------------------------------------------------------------
/app/dashboard/event/[eventTypeId]/delete/page.tsx:
--------------------------------------------------------------------------------
1 | import { DeleteEventTypeAction } from "@/app/actions";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Card,
5 | CardDescription,
6 | CardFooter,
7 | CardHeader,
8 | CardTitle,
9 | } from "@/components/ui/card";
10 | import Link from "next/link";
11 | import React from "react";
12 |
13 | const DeleteEventType = ({ params }: { params: { eventTypeId: string } }) => {
14 | return (
15 |
16 |
17 |
18 | Delete Event Type
19 |
20 | Are you sure you want to delete this event type?
21 |
22 |
23 |
24 |
25 | Cancel
26 |
27 |
28 |
29 | Delete
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default DeleteEventType;
38 |
--------------------------------------------------------------------------------
/app/dashboard/event/[eventTypeId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { EditEventTypeForm } from "@/app/components/dashboard/EditEventTypeForm";
2 | import prisma from "@/app/lib/db";
3 | import { notFound } from "next/navigation";
4 | import React from "react";
5 |
6 | async function getData(eventTypeId: string) {
7 | const data = await prisma.eventType.findUnique({
8 | where: {
9 | id: eventTypeId,
10 | },
11 | select: {
12 | title: true,
13 | description: true,
14 | duration: true,
15 | url: true,
16 | id: true,
17 | videoCallSoftware: true,
18 | },
19 | });
20 |
21 | if (!data) {
22 | return notFound();
23 | }
24 |
25 | return data;
26 | }
27 | const EditEventTypePage = async ({
28 | params,
29 | }: {
30 | params: { eventTypeId: string };
31 | }) => {
32 | const data = await getData(params.eventTypeId);
33 |
34 | return (
35 |
44 | );
45 | };
46 |
47 | export default EditEventTypePage;
48 |
--------------------------------------------------------------------------------
/app/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Menu } from "lucide-react";
3 |
4 | import { Button } from "@/components/ui/button";
5 |
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuLabel,
11 | DropdownMenuSeparator,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
15 | import { ReactNode } from "react";
16 | import { requireUser } from "../lib/hooks";
17 | import prisma from "../lib/db";
18 | import { redirect } from "next/navigation";
19 | import Logo from "@/public/logo.png";
20 | import Image from "next/image";
21 | import { DasboardLinks } from "../components/dashboard/DasboardLinks";
22 | import { ThemeToggle } from "../components/dashboard/ThemeToggle";
23 | import { Toaster } from "@/components/ui/sonner";
24 | import { auth, signOut } from "../lib/auth";
25 |
26 | async function getData(id: string) {
27 | const data = await prisma.user.findUnique({
28 | where: {
29 | id: id,
30 | },
31 | select: {
32 | username: true,
33 | grantId: true,
34 | },
35 | });
36 |
37 | if (!data?.username) {
38 | return redirect("/onboarding");
39 | }
40 |
41 | if (!data.grantId) {
42 | return redirect("/onboarding/grant-id");
43 | }
44 |
45 | return data;
46 | }
47 |
48 | export default async function Dashboard({ children }: { children: ReactNode }) {
49 | const session = await auth();
50 |
51 | if (!session?.user) {
52 | return redirect("/");
53 | }
54 |
55 | const data = await getData(session.user.id as string);
56 |
57 | return (
58 | <>
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | CalMarshal
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
137 |
138 | {children}
139 |
140 |
141 |
142 |
143 | >
144 | );
145 | }
146 |
--------------------------------------------------------------------------------
/app/dashboard/meetings/page.tsx:
--------------------------------------------------------------------------------
1 | import { cancelMeetingAction } from "@/app/actions";
2 | import { EmptyState } from "@/app/components/dashboard/EmptyState";
3 | import { SubmitButton } from "@/app/components/SubmitButton";
4 | import { auth } from "@/app/lib/auth";
5 | import { nylas } from "@/app/lib/nylas";
6 |
7 | import {
8 | Card,
9 | CardContent,
10 | CardDescription,
11 | CardHeader,
12 | CardTitle,
13 | } from "@/components/ui/card";
14 | import { Separator } from "@/components/ui/separator";
15 | import prisma from "@/lib/prisma";
16 | import { format, fromUnixTime } from "date-fns";
17 | import { Icon, Video } from "lucide-react";
18 |
19 | import React from "react";
20 |
21 | async function getData(userId: string) {
22 | const userData = await prisma.user.findUnique({
23 | where: {
24 | id: userId,
25 | },
26 | select: {
27 | grantId: true,
28 | grantEmail: true,
29 | },
30 | });
31 |
32 | if (!userData) {
33 | throw new Error("User not found");
34 | }
35 | const data = await nylas.events.list({
36 | identifier: userData?.grantId as string,
37 | queryParams: {
38 | calendarId: userData?.grantEmail as string,
39 | },
40 | });
41 |
42 | return data;
43 | }
44 |
45 | const MeetingsPage = async () => {
46 | const session = await auth();
47 | const data = await getData(session?.user?.id as string);
48 |
49 | return (
50 | <>
51 | {data.data.length < 1 ? (
52 |
58 | ) : (
59 |
60 |
61 | Bookings
62 |
63 | See upcoming and past events booked through your event type links.
64 |
65 |
66 |
67 | {data.data.map((item) => (
68 |
69 |
70 |
71 |
72 |
73 | {format(fromUnixTime(item.when.startTime), "EEE, dd MMM")}
74 |
75 |
76 | {format(fromUnixTime(item.when.startTime), "hh:mm a")} -{" "}
77 | {format(fromUnixTime(item.when.endTime), "hh:mm a")}
78 |
79 |
89 |
90 |
91 |
{item.title}
92 |
93 | You and {item.participants[0].name}
94 |
95 |
96 |
101 |
102 |
103 |
104 | ))}
105 |
106 |
107 | )}
108 | >
109 | );
110 | };
111 |
112 | export default MeetingsPage;
113 |
114 | {
115 | /*
116 |
117 |
118 |
119 |
120 | {format(fromUnixTime(item.when.startTime), "EEE, dd MMM")}
121 |
122 |
123 | {format(fromUnixTime(item.when.startTime), "hh:mm a")} -{" "}
124 | {format(fromUnixTime(item.when.endTime), "hh:mm a")}
125 |
126 |
132 |
133 |
134 |
{item.title}
135 |
You and {item.participants[0].name}
136 |
137 |
142 |
143 |
144 | */
145 | }
146 |
--------------------------------------------------------------------------------
/app/dashboard/new/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { CreateEventTypeAction } from "@/app/actions";
4 | import { SubmitButton } from "@/app/components/SubmitButton";
5 | import { eventTypeSchema } from "@/app/lib/zodSchemas";
6 | import { Button } from "@/components/ui/button";
7 | import { ButtonGroup } from "@/components/ui/ButtonGroup";
8 | import {
9 | Card,
10 | CardContent,
11 | CardDescription,
12 | CardFooter,
13 | CardHeader,
14 | CardTitle,
15 | } from "@/components/ui/card";
16 | import { Input } from "@/components/ui/input";
17 | import { Label } from "@/components/ui/label";
18 | import {
19 | Select,
20 | SelectContent,
21 | SelectGroup,
22 | SelectItem,
23 | SelectLabel,
24 | SelectTrigger,
25 | SelectValue,
26 | } from "@/components/ui/select";
27 | import { Textarea } from "@/components/ui/textarea";
28 | import { useForm } from "@conform-to/react";
29 | import { parseWithZod } from "@conform-to/zod";
30 | import Link from "next/link";
31 | import React, { useState } from "react";
32 | import { useFormState } from "react-dom";
33 |
34 | type Platform = "Zoom Meeting" | "Google Meet" | "Microsoft Teams";
35 |
36 | const CreateNewEvent = () => {
37 | const [lastResult, action] = useFormState(CreateEventTypeAction, undefined);
38 | const [form, fields] = useForm({
39 | // Sync the result of last submission
40 | lastResult,
41 |
42 | // Reuse the validation logic on the client
43 | onValidate({ formData }) {
44 | return parseWithZod(formData, { schema: eventTypeSchema });
45 | },
46 |
47 | // Validate the form on blur event triggered
48 | shouldValidate: "onBlur",
49 | shouldRevalidate: "onInput",
50 | });
51 | const [activePlatform, setActivePlatform] = useState("Google Meet");
52 |
53 | const togglePlatform = (platform: Platform) => {
54 | setActivePlatform(platform);
55 | };
56 | return (
57 |
58 |
59 |
60 | Add new appointment type
61 |
62 | Create a new appointment type that allows people to book times.
63 |
64 |
65 |
66 |
67 |
68 |
Title
69 |
75 |
{fields.title.errors}
76 |
77 |
78 |
79 |
URL Slug
80 |
81 |
82 | CalMarshal.com/
83 |
84 |
92 |
93 |
94 |
{fields.url.errors}
95 |
96 |
97 |
98 |
Description
99 |
105 |
106 | {fields.description.errors}
107 |
108 |
109 |
110 |
111 |
Duration
112 |
117 |
118 |
119 |
120 |
121 |
122 | Duration
123 | 15 Mins
124 | 30 Min
125 | 45 Mins
126 | 1 Hour
127 |
128 |
129 |
130 |
131 |
{fields.duration.errors}
132 |
133 |
134 |
135 |
140 | Video Call Provider
141 |
142 | togglePlatform("Zoom Meeting")}
144 | type="button"
145 | className="w-full"
146 | variant={
147 | activePlatform === "Zoom Meeting" ? "secondary" : "outline"
148 | }
149 | >
150 | Zoom
151 |
152 | togglePlatform("Google Meet")}
154 | type="button"
155 | className="w-full"
156 | variant={
157 | activePlatform === "Google Meet" ? "secondary" : "outline"
158 | }
159 | >
160 | Google Meet
161 |
162 | togglePlatform("Microsoft Teams")}
171 | >
172 | Microsoft Teams
173 |
174 |
175 |
176 |
177 |
178 |
179 | Cancel
180 |
181 |
182 |
183 |
184 |
185 |
186 | );
187 | };
188 |
189 | export default CreateNewEvent;
190 |
--------------------------------------------------------------------------------
/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import Link from "next/link";
3 | import { notFound } from "next/navigation";
4 | import React from "react";
5 | import prisma from "../lib/db";
6 | import { requireUser } from "../lib/hooks";
7 | import { ExternalLink, Pen, Settings, Trash, Users2 } from "lucide-react";
8 |
9 | import { EmptyState } from "../components/dashboard/EmptyState";
10 |
11 | import {
12 | DropdownMenu,
13 | DropdownMenuContent,
14 | DropdownMenuGroup,
15 | DropdownMenuItem,
16 | DropdownMenuLabel,
17 | DropdownMenuSeparator,
18 | DropdownMenuTrigger,
19 | } from "@/components/ui/dropdown-menu";
20 | import { MenuActiveSwitcher } from "../components/dashboard/EventTypeSwitcher";
21 | import { CopyLinkMenuItem } from "../components/dashboard/CopyLinkMenuItem";
22 |
23 | async function getData(id: string) {
24 | const data = await prisma.user.findUnique({
25 | where: {
26 | id: id,
27 | },
28 |
29 | select: {
30 | EventType: {
31 | select: {
32 | id: true,
33 | active: true,
34 | title: true,
35 | url: true,
36 | duration: true,
37 | },
38 | orderBy: {
39 | createdAt: "desc",
40 | },
41 | },
42 | username: true,
43 | },
44 | });
45 |
46 | if (!data) {
47 | return notFound();
48 | }
49 |
50 | return data;
51 | }
52 |
53 | const DashbaordPage = async () => {
54 | const session = await requireUser();
55 | const data = await getData(session.user?.id as string);
56 |
57 | return (
58 | <>
59 |
60 |
61 |
Event Types
62 |
63 | Create and manage your event types.
64 |
65 |
66 |
67 | Create New Event
68 |
69 |
70 | {data.EventType.length === 0 ? (
71 |
77 | ) : (
78 |
79 | {data.EventType.map((item) => (
80 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | Event
93 |
94 |
95 |
96 |
97 |
98 | Preview
99 |
100 |
101 |
104 |
105 |
106 |
107 | Edit
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | Delete
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | {item.duration} Minutes Meeting
132 |
133 |
134 |
135 | {item.title}
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
148 |
149 |
150 | Edit Event
151 |
152 |
153 |
154 | ))}
155 |
156 | )}
157 | >
158 | );
159 | };
160 |
161 | export default DashbaordPage;
162 |
--------------------------------------------------------------------------------
/app/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { SettingsForm } from "@/app/components/dashboard/settingsForm";
2 |
3 | import prisma from "@/app/lib/db";
4 | import { requireUser } from "@/app/lib/hooks";
5 |
6 | import { notFound } from "next/navigation";
7 | import React from "react";
8 |
9 | async function getData(id: string) {
10 | const data = await prisma.user.findUnique({
11 | where: {
12 | id: id,
13 | },
14 | select: {
15 | name: true,
16 | email: true,
17 | image: true,
18 | },
19 | });
20 |
21 | if (!data) {
22 | return notFound();
23 | }
24 |
25 | return data;
26 | }
27 |
28 | const SettingsPage = async () => {
29 | const session = await requireUser();
30 | const data = await getData(session.user?.id as string);
31 | return (
32 |
37 | );
38 | };
39 |
40 | export default SettingsPage;
41 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/app/favicon.ico
--------------------------------------------------------------------------------
/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 222.2 84% 4.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 222.2 84% 4.9%;
13 | --primary: 221.2 83.2% 53.3%;
14 | --primary-foreground: 210 40% 98%;
15 | --secondary: 210 40% 96.1%;
16 | --secondary-foreground: 222.2 47.4% 11.2%;
17 | --muted: 210 40% 96.1%;
18 | --muted-foreground: 215.4 16.3% 46.9%;
19 | --accent: 210 40% 96.1%;
20 | --accent-foreground: 222.2 47.4% 11.2%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 214.3 31.8% 91.4%;
24 | --input: 214.3 31.8% 91.4%;
25 | --ring: 221.2 83.2% 53.3%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 222.2 84% 4.9%;
36 | --foreground: 210 40% 98%;
37 | --card: 222.2 84% 4.9%;
38 | --card-foreground: 210 40% 98%;
39 | --popover: 222.2 84% 4.9%;
40 | --popover-foreground: 210 40% 98%;
41 | --primary: 217.2 91.2% 59.8%;
42 | --primary-foreground: 222.2 47.4% 11.2%;
43 | --secondary: 217.2 32.6% 17.5%;
44 | --secondary-foreground: 210 40% 98%;
45 | --muted: 217.2 32.6% 17.5%;
46 | --muted-foreground: 215 20.2% 65.1%;
47 | --accent: 217.2 32.6% 17.5%;
48 | --accent-foreground: 210 40% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 210 40% 98%;
51 | --border: 217.2 32.6% 17.5%;
52 | --input: 217.2 32.6% 17.5%;
53 | --ring: 224.3 76.3% 48%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import localFont from "next/font/local";
3 | import "./globals.css";
4 | import { ThemeProvider } from "./components/dashboard/ThemeProvider";
5 | import { ourFileRouter } from "@/app/api/uploadthing/core";
6 | import { NextSSRPlugin } from "@uploadthing/react/next-ssr-plugin";
7 | import { extractRouterConfig } from "uploadthing/server";
8 |
9 | const geistSans = localFont({
10 | src: "./fonts/GeistVF.woff",
11 | variable: "--font-geist-sans",
12 | weight: "100 900",
13 | });
14 | const geistMono = localFont({
15 | src: "./fonts/GeistMonoVF.woff",
16 | variable: "--font-geist-mono",
17 | weight: "100 900",
18 | });
19 |
20 | export const metadata: Metadata = {
21 | title: "Create Next App",
22 | description: "Generated by create next app",
23 | };
24 |
25 | export default function RootLayout({
26 | children,
27 | }: Readonly<{
28 | children: React.ReactNode;
29 | }>) {
30 | return (
31 |
32 |
35 |
41 |
42 | {children}
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/app/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import GitHub from "next-auth/providers/github";
3 | import prisma from "./db";
4 | import { PrismaAdapter } from "@auth/prisma-adapter";
5 |
6 | export const { handlers, signIn, signOut, auth } = NextAuth({
7 | adapter: PrismaAdapter(prisma),
8 |
9 | providers: [GitHub],
10 | });
11 |
--------------------------------------------------------------------------------
/app/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient();
5 | };
6 |
7 | declare const globalThis: {
8 | prismaGlobal: ReturnType;
9 | } & typeof global;
10 |
11 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
12 |
13 | export default prisma;
14 |
15 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
16 |
--------------------------------------------------------------------------------
/app/lib/hooks.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 | import { auth } from "./auth";
3 |
4 | export async function requireUser() {
5 | const session = await auth();
6 |
7 | if (!session?.user?.id) {
8 | return redirect("/");
9 | }
10 |
11 | return session;
12 | }
13 |
--------------------------------------------------------------------------------
/app/lib/nylas.ts:
--------------------------------------------------------------------------------
1 | import Nylas from "nylas";
2 |
3 | export const nylasConfig = {
4 | clientId: process.env.NYLAS_CLIENT_ID,
5 | callbackUri: process.env.NEXT_PUBLIC_URL + "/api/oauth/exchange",
6 | apiKey: process.env.NYLAS_API_SECRET_KEY,
7 | apiUri: process.env.NYLAS_API_URL,
8 | };
9 |
10 | const AuthConfig = {
11 | clientId: process.env.NYLAS_CLIENT_ID as string,
12 | redirectUri: "http://localhost:3000/oauth/exchange",
13 | };
14 | export const nylas = new Nylas({
15 | apiKey: process.env.NYLAS_API_SECRET_KEY!,
16 | apiUri: process.env.NYLAS_API_URL,
17 | });
18 |
--------------------------------------------------------------------------------
/app/lib/times.ts:
--------------------------------------------------------------------------------
1 | export const times = [
2 | {
3 | id: 0,
4 | time: "00:00",
5 | },
6 | {
7 | id: 1,
8 | time: "00:30",
9 | },
10 | {
11 | id: 2,
12 | time: "01:00",
13 | },
14 | {
15 | id: 3,
16 | time: "01:30",
17 | },
18 | {
19 | id: 4,
20 | time: "02:00",
21 | },
22 | {
23 | id: 5,
24 | time: "02:30",
25 | },
26 | {
27 | id: 6,
28 | time: "03:00",
29 | },
30 | {
31 | id: 7,
32 | time: "03:30",
33 | },
34 | {
35 | id: 8,
36 | time: "04:00",
37 | },
38 | {
39 | id: 9,
40 | time: "04:30",
41 | },
42 | {
43 | id: 10,
44 | time: "05:00",
45 | },
46 | {
47 | id: 11,
48 | time: "05:30",
49 | },
50 | {
51 | id: 12,
52 | time: "06:00",
53 | },
54 | {
55 | id: 13,
56 | time: "06:30",
57 | },
58 | {
59 | id: 14,
60 | time: "07:00",
61 | },
62 | {
63 | id: 15,
64 | time: "07:30",
65 | },
66 | {
67 | id: 16,
68 | time: "08:00",
69 | },
70 | {
71 | id: 17,
72 | time: "08:30",
73 | },
74 | {
75 | id: 18,
76 | time: "09:00",
77 | },
78 | {
79 | id: 19,
80 | time: "09:30",
81 | },
82 | {
83 | id: 20,
84 | time: "10:00",
85 | },
86 | {
87 | id: 21,
88 | time: "10:30",
89 | },
90 | {
91 | id: 22,
92 | time: "11:00",
93 | },
94 | {
95 | id: 23,
96 | time: "11:30",
97 | },
98 | {
99 | id: 24,
100 | time: "12:00",
101 | },
102 | {
103 | id: 25,
104 | time: "12:30",
105 | },
106 | {
107 | id: 26,
108 | time: "13:00",
109 | },
110 | {
111 | id: 27,
112 | time: "13:30",
113 | },
114 | {
115 | id: 28,
116 | time: "14:00",
117 | },
118 | {
119 | id: 29,
120 | time: "14:30",
121 | },
122 | {
123 | id: 30,
124 | time: "15:00",
125 | },
126 | {
127 | id: 31,
128 | time: "15:30",
129 | },
130 | {
131 | id: 32,
132 | time: "16:00",
133 | },
134 | {
135 | id: 33,
136 | time: "16:30",
137 | },
138 | {
139 | id: 34,
140 | time: "17:00",
141 | },
142 | {
143 | id: 35,
144 | time: "17:30",
145 | },
146 | {
147 | id: 36,
148 | time: "18:00",
149 | },
150 | {
151 | id: 37,
152 | time: "18:30",
153 | },
154 | {
155 | id: 38,
156 | time: "19:00",
157 | },
158 | {
159 | id: 39,
160 | time: "19:30",
161 | },
162 | {
163 | id: 40,
164 | time: "20:00",
165 | },
166 | {
167 | id: 41,
168 | time: "20:30",
169 | },
170 | {
171 | id: 42,
172 | time: "21:00",
173 | },
174 | {
175 | id: 43,
176 | time: "21:30",
177 | },
178 | {
179 | id: 44,
180 | time: "22:00",
181 | },
182 | {
183 | id: 45,
184 | time: "22:30",
185 | },
186 | {
187 | id: 46,
188 | time: "23:00",
189 | },
190 | {
191 | id: 47,
192 | time: "23:30",
193 | },
194 | ];
195 |
--------------------------------------------------------------------------------
/app/lib/uploadthing.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generateUploadButton,
3 | generateUploadDropzone,
4 | } from "@uploadthing/react";
5 |
6 | import type { OurFileRouter } from "@/app/api/uploadthing/core";
7 |
8 | export const UploadButton = generateUploadButton();
9 | export const UploadDropzone = generateUploadDropzone();
10 |
--------------------------------------------------------------------------------
/app/lib/zodSchemas.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { conformZodMessage } from "@conform-to/zod";
3 |
4 | export function onboardingSchema(options?: {
5 | isUsernameUnique: () => Promise;
6 | }) {
7 | return z.object({
8 | username: z
9 | .string()
10 | .min(3)
11 | .max(150)
12 | .regex(/^[a-zA-Z0-9-]+$/, {
13 | message: "Username must contain only letters, numbers, and hyphens",
14 | })
15 | // Pipe the schema so it runs only if the email is valid
16 | .pipe(
17 | // Note: The callback cannot be async here
18 | // As we run zod validation synchronously on the client
19 | z.string().superRefine((_, ctx) => {
20 | // This makes Conform to fallback to server validation
21 | // by indicating that the validation is not defined
22 | if (typeof options?.isUsernameUnique !== "function") {
23 | ctx.addIssue({
24 | code: "custom",
25 | message: conformZodMessage.VALIDATION_UNDEFINED,
26 | fatal: true,
27 | });
28 | return;
29 | }
30 |
31 | // If it reaches here, then it must be validating on the server
32 | // Return the result as a promise so Zod knows it's async instead
33 | return options.isUsernameUnique().then((isUnique) => {
34 | if (!isUnique) {
35 | ctx.addIssue({
36 | code: "custom",
37 | message: "Username is already used",
38 | });
39 | }
40 | });
41 | })
42 | ),
43 | fullName: z.string().min(3).max(150),
44 | });
45 | }
46 |
47 | export const onboardingSchemaLocale = z.object({
48 | username: z
49 | .string()
50 | .min(3)
51 | .max(150)
52 | .regex(/^[a-zA-Z0-9-]+$/, {
53 | message: "Username must contain only letters, numbers, and hyphens",
54 | }),
55 | fullName: z.string().min(3).max(150),
56 | });
57 |
58 | export const aboutSettingsSchema = z.object({
59 | fullName: z.string().min(3).max(150),
60 |
61 | profileImage: z.string(),
62 | });
63 |
64 | export const eventTypeSchema = z.object({
65 | title: z.string().min(3).max(150),
66 | duration: z.number().min(1).max(100),
67 | url: z.string().min(3).max(150),
68 | description: z.string().min(3).max(300),
69 | videoCallSoftware: z.string(),
70 | });
71 |
72 | export function EventTypeServerSchema(options?: {
73 | isUrlUnique: () => Promise;
74 | }) {
75 | return z.object({
76 | url: z
77 | .string()
78 | .min(3)
79 | .max(150)
80 | .pipe(
81 | // Note: The callback cannot be async here
82 | // As we run zod validation synchronously on the client
83 | z.string().superRefine((_, ctx) => {
84 | // This makes Conform to fallback to server validation
85 | // by indicating that the validation is not defined
86 | if (typeof options?.isUrlUnique !== "function") {
87 | ctx.addIssue({
88 | code: "custom",
89 | message: conformZodMessage.VALIDATION_UNDEFINED,
90 | fatal: true,
91 | });
92 | return;
93 | }
94 |
95 | // If it reaches here, then it must be validating on the server
96 | // Return the result as a promise so Zod knows it's async instead
97 | return options.isUrlUnique().then((isUnique) => {
98 | if (!isUnique) {
99 | ctx.addIssue({
100 | code: "custom",
101 | message: "Url is already used",
102 | });
103 | }
104 | });
105 | })
106 | ),
107 | title: z.string().min(3).max(150),
108 | duration: z.number().min(1).max(100),
109 | description: z.string().min(3).max(300),
110 | videoCallSoftware: z.string(),
111 | });
112 | }
113 |
--------------------------------------------------------------------------------
/app/meeting/[username]/[meetingName]/page.tsx:
--------------------------------------------------------------------------------
1 | import prisma from "@/app/lib/db";
2 | import { nylas } from "@/app/lib/nylas";
3 | import { notFound } from "next/navigation";
4 | import React from "react";
5 | import { addDays } from "date-fns";
6 | import { Card, CardContent } from "@/components/ui/card";
7 | import Image from "next/image";
8 | import { BookMarked, CalendarX2, Clock } from "lucide-react";
9 | import { Separator } from "@/components/ui/separator";
10 | import { RenderCalendar } from "@/app/components/demo/RenderCalendar";
11 | import { TimeSlots } from "@/app/components/TimeSlots";
12 | import { Label } from "@/components/ui/label";
13 | import { Input } from "@/components/ui/input";
14 | import { SubmitButton } from "@/app/components/SubmitButton";
15 | import { createMeetingAction } from "@/app/actions";
16 |
17 | const targetDate = new Date(2024, 8, 19); // Note: month is 0-indexed, so 8 is September
18 | const nextDay = addDays(targetDate, 1);
19 |
20 | async function getData(userName: string, meetingName: string) {
21 | const data = await prisma.user.findUnique({
22 | where: {
23 | username: userName,
24 | },
25 | select: {
26 | grantEmail: true,
27 | name: true,
28 | grantId: true,
29 | image: true,
30 | Availability: true,
31 | EventType: {
32 | where: {
33 | user: {
34 | username: userName,
35 | },
36 | url: meetingName,
37 | },
38 | },
39 | },
40 | });
41 |
42 | /* const nylasCalendarData = await nylas.calendars.getFreeBusy({
43 | identifier: data?.grantId as string,
44 | requestBody: {
45 | startTime: Math.floor(targetDate.getTime() / 1000),
46 | endTime: Math.floor(nextDay.getTime() / 1000),
47 | emails: [data?.grantEmail as string],
48 | },
49 | }); */
50 |
51 | if (!data) {
52 | return notFound();
53 | }
54 |
55 | return { data };
56 | }
57 |
58 | const MeetingPagee = async ({
59 | params,
60 | searchParams,
61 | }: {
62 | params: { username: string; meetingName: string };
63 | searchParams: { date?: string; time?: string };
64 | }) => {
65 | const { data } = await getData(params.username, params.meetingName);
66 | const selectedDate = searchParams.date
67 | ? new Date(searchParams.date)
68 | : new Date();
69 |
70 | const showForm = !!searchParams.date && !!searchParams.time;
71 |
72 | return (
73 |
74 | {showForm ? (
75 |
76 |
77 |
78 |
85 |
86 | {data.name}
87 |
88 |
Design Workshop
89 |
90 | A longer chat to run through design.
91 |
92 |
93 |
94 |
95 |
96 |
97 | Friday, 24th June
98 |
99 |
100 |
101 |
102 |
103 | 30 Mins
104 |
105 |
106 |
107 |
108 |
109 | Google Meet
110 |
111 |
112 |
113 |
114 |
118 |
119 |
123 |
124 |
129 |
130 | Your Name
131 |
132 |
133 |
134 |
135 | Your Email
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | ) : (
144 |
145 |
146 |
147 |
154 |
155 | {data.name}
156 |
157 |
158 | {data.EventType[0].title}
159 |
160 |
161 | {data.EventType[0].description}
162 |
163 |
164 |
165 |
166 |
167 |
168 | Friday, 24th June
169 |
170 |
171 |
172 |
173 |
174 | {data.EventType[0].duration} Mins
175 |
176 |
177 |
178 |
179 |
180 | Google Meet
181 |
182 |
183 |
184 |
185 |
186 |
190 |
191 |
192 |
193 |
194 |
195 |
199 |
200 |
201 |
202 |
203 | )}
204 |
205 | );
206 | };
207 |
208 | export default MeetingPagee;
209 |
--------------------------------------------------------------------------------
/app/onboarding/grant-id/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardDescription,
5 | CardHeader,
6 | CardTitle,
7 | } from "@/components/ui/card";
8 | import React from "react";
9 | import AlmostFinished from "@/public/work-is-almost-over-happy.gif";
10 | import Image from "next/image";
11 | import { Button } from "@/components/ui/button";
12 | import { CalendarCheck2 } from "lucide-react";
13 | import Link from "next/link";
14 |
15 | const GrantIdRoute = () => {
16 | return (
17 |
18 |
19 |
20 | You Are Almost Done!
21 |
22 | We have to now connect your calendar to your account.
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 | Connect Calender to Account
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default GrantIdRoute;
44 |
--------------------------------------------------------------------------------
/app/onboarding/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Card,
5 | CardContent,
6 | CardDescription,
7 | CardFooter,
8 | CardHeader,
9 | CardTitle,
10 | } from "@/components/ui/card";
11 | import { Input } from "@/components/ui/input";
12 | import { Label } from "@/components/ui/label";
13 | import React from "react";
14 | import { SubmitButton } from "../components/SubmitButton";
15 | import { useFormState } from "react-dom";
16 | import { parseWithZod } from "@conform-to/zod";
17 | import { onboardingSchemaLocale } from "../lib/zodSchemas";
18 | import { useForm } from "@conform-to/react";
19 | import { onboardingAction } from "../actions";
20 |
21 | const OnboardingPage = () => {
22 | const [lastResult, action] = useFormState(onboardingAction, undefined);
23 | const [form, fields] = useForm({
24 | // Sync the result of last submission
25 | lastResult,
26 |
27 | // Reuse the validation logic on the client
28 | onValidate({ formData }) {
29 | return parseWithZod(formData, { schema: onboardingSchemaLocale });
30 | },
31 |
32 | // Validate the form on blur event triggered
33 | shouldValidate: "onBlur",
34 | shouldRevalidate: "onInput",
35 | });
36 |
37 | return (
38 |
39 |
40 |
41 | Welcome to CalMarshal
42 |
43 | We need the following information to set up your profile
44 |
45 |
46 |
47 |
48 |
49 |
50 |
Full Name
51 |
57 |
{fields.fullName.errors}
58 |
59 |
60 |
Username
61 |
62 |
63 |
64 | CalMarshal.com/
65 |
66 |
74 |
75 |
{fields.username.errors}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default OnboardingPage;
88 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 | import { Navbar } from "./components/landingPage/Navbar";
3 | import { Hero } from "./components/landingPage/Hero";
4 | import { Logos } from "./components/landingPage/Logos";
5 | import { Features } from "./components/landingPage/Features";
6 | import { Testimonial } from "./components/landingPage/Testimonial";
7 | import { auth } from "./lib/auth";
8 | import { CTA } from "./components/landingPage/Cta";
9 |
10 | export default async function Home() {
11 | const session = await auth();
12 |
13 | if (session?.user) {
14 | return redirect("/dashboard");
15 | }
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/app/success/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Card, CardContent, CardFooter } from "@/components/ui/card";
5 | import { Separator } from "@/components/ui/separator";
6 | import { Check } from "lucide-react";
7 | import Link from "next/link";
8 |
9 | export default function SuccessPage() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | This event is scheduled
19 |
20 |
21 | We emailed you and the other attendees a calendar invitation with
22 | all the details.
23 |
24 |
25 | {/*
26 |
27 |
28 |
29 |
What
30 |
31 |
32 |
Design Workshop
33 |
34 |
35 |
36 |
When
37 |
38 |
41 |
42 |
Where
43 |
44 |
47 |
*/}
48 |
49 |
50 |
51 | Close this Page
52 |
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/app/test/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { NylasDatePicker } from "@nylas/react";
4 | import { NylasLocaleSwitch } from "@nylas/react";
5 |
6 | const TesTpage = () => {
7 | // Define the selectable dates (e.g., the next 30 days)
8 | const generateSelectableDates = () => {
9 | const today = new Date();
10 | const dates = [];
11 | for (let i = 0; i < 30; i++) {
12 | const date = new Date();
13 | date.setDate(today.getDate() + i);
14 | dates.push(date);
15 | }
16 | return dates;
17 | };
18 |
19 | return (
20 |
21 |
22 |
28 |
29 | );
30 | };
31 |
32 | export default TesTpage;
33 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/components/ui/ButtonGroup.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Children, ReactElement, cloneElement } from "react";
4 |
5 | import { ButtonProps } from "@/components/ui/button";
6 | import { cn } from "@/lib/utils";
7 |
8 | interface ButtonGroupProps {
9 | className?: string;
10 | children: ReactElement[];
11 | }
12 |
13 | export const ButtonGroup = ({
14 | className,
15 |
16 | children,
17 | }: ButtonGroupProps) => {
18 | const totalButtons = Children.count(children);
19 |
20 | return (
21 |
22 | {Children.map(children, (child, index) => {
23 | const isFirst = index === 0;
24 | const isLast = index === totalButtons - 1;
25 |
26 | return cloneElement(child, {
27 | className: cn(
28 | {
29 | "rounded-l-none": !isFirst,
30 | "rounded-r-none": !isLast,
31 | "border-l-0": !isFirst,
32 | },
33 | child.props.className
34 | ),
35 | });
36 | })}
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
43 | : "[&:has([aria-selected])]:rounded-md"
44 | ),
45 | day: cn(
46 | buttonVariants({ variant: "ghost" }),
47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
48 | ),
49 | day_range_start: "day-range-start",
50 | day_range_end: "day-range-end",
51 | day_selected:
52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
53 | day_today: "bg-accent text-accent-foreground",
54 | day_outside:
55 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
56 | day_disabled: "text-muted-foreground opacity-50",
57 | day_range_middle:
58 | "aria-selected:bg-accent aria-selected:text-accent-foreground",
59 | day_hidden: "invisible",
60 | ...classNames,
61 | }}
62 | components={{
63 | IconLeft: ({ ...props }) => ,
64 | IconRight: ({ ...props }) => ,
65 | }}
66 | {...props}
67 | />
68 | )
69 | }
70 | Calendar.displayName = "Calendar"
71 |
72 | export { Calendar }
73 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | CaretSortIcon,
6 | CheckIcon,
7 | ChevronDownIcon,
8 | ChevronUpIcon,
9 | } from "@radix-ui/react-icons"
10 | import * as SelectPrimitive from "@radix-ui/react-select"
11 |
12 | import { cn } from "@/lib/utils"
13 |
14 | const Select = SelectPrimitive.Root
15 |
16 | const SelectGroup = SelectPrimitive.Group
17 |
18 | const SelectValue = SelectPrimitive.Value
19 |
20 | const SelectTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 | span]:line-clamp-1",
28 | className
29 | )}
30 | {...props}
31 | >
32 | {children}
33 |
34 |
35 |
36 |
37 | ))
38 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
39 |
40 | const SelectScrollUpButton = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 |
53 |
54 | ))
55 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
56 |
57 | const SelectScrollDownButton = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 |
70 |
71 | ))
72 | SelectScrollDownButton.displayName =
73 | SelectPrimitive.ScrollDownButton.displayName
74 |
75 | const SelectContent = React.forwardRef<
76 | React.ElementRef,
77 | React.ComponentPropsWithoutRef
78 | >(({ className, children, position = "popper", ...props }, ref) => (
79 |
80 |
91 |
92 |
99 | {children}
100 |
101 |
102 |
103 |
104 | ))
105 | SelectContent.displayName = SelectPrimitive.Content.displayName
106 |
107 | const SelectLabel = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ))
117 | SelectLabel.displayName = SelectPrimitive.Label.displayName
118 |
119 | const SelectItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | SelectItem.displayName = SelectPrimitive.Item.displayName
140 |
141 | const SelectSeparator = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef
144 | >(({ className, ...props }, ref) => (
145 |
150 | ))
151 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
152 |
153 | export {
154 | Select,
155 | SelectGroup,
156 | SelectValue,
157 | SelectTrigger,
158 | SelectContent,
159 | SelectLabel,
160 | SelectItem,
161 | SelectSeparator,
162 | SelectScrollUpButton,
163 | SelectScrollDownButton,
164 | }
165 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient();
5 | };
6 |
7 | type PrismaClientSingleton = ReturnType;
8 |
9 | const globalForPrisma = globalThis as unknown as {
10 | prisma: PrismaClientSingleton | undefined;
11 | };
12 |
13 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
14 |
15 | export default prisma;
16 |
17 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
18 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | { hostname: "avatar.vercel.sh", port: "", protocol: "https" },
6 | { hostname: "utfs.io", port: "", protocol: "https" },
7 | {
8 | hostname: "avatars.githubusercontent.com",
9 | port: "",
10 | protocol: "https",
11 | },
12 | ],
13 | },
14 | };
15 |
16 | export default nextConfig;
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "calcom-nylas",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@auth/prisma-adapter": "^2.5.0",
13 | "@conform-to/react": "^1.1.5",
14 | "@conform-to/zod": "^1.1.5",
15 | "@internationalized/date": "^3.5.5",
16 | "@nylas/react": "^1.1.3",
17 | "@prisma/client": "^5.19.1",
18 | "@radix-ui/react-dialog": "^1.1.1",
19 | "@radix-ui/react-dropdown-menu": "^2.1.1",
20 | "@radix-ui/react-icons": "^1.3.0",
21 | "@radix-ui/react-label": "^2.1.0",
22 | "@radix-ui/react-select": "^2.1.1",
23 | "@radix-ui/react-separator": "^1.1.0",
24 | "@radix-ui/react-slot": "^1.1.0",
25 | "@radix-ui/react-switch": "^1.1.0",
26 | "@radix-ui/react-tooltip": "^1.1.2",
27 | "@uploadthing/react": "^7.0.1",
28 | "class-variance-authority": "^0.7.0",
29 | "clsx": "^2.1.1",
30 | "date-fns": "^3.6.0",
31 | "iron-session": "^8.0.3",
32 | "lucide-react": "^0.440.0",
33 | "next": "14.2.10",
34 | "next-auth": "^5.0.0-beta.21",
35 | "next-themes": "^0.3.0",
36 | "npm": "^10.8.3",
37 | "nylas": "^7.5.2",
38 | "react": "^18",
39 | "react-aria": "^3.34.3",
40 | "react-aria-components": "^1.3.3",
41 | "react-day-picker": "^8.10.1",
42 | "react-dom": "^18",
43 | "react-stately": "^3.32.2",
44 | "sonner": "^1.5.0",
45 | "tailwind-merge": "^2.5.2",
46 | "tailwindcss-animate": "^1.0.7",
47 | "uploadthing": "^7.0.1",
48 | "zod": "^3.23.8"
49 | },
50 | "devDependencies": {
51 | "@types/node": "^20",
52 | "@types/react": "^18",
53 | "@types/react-dom": "^18",
54 | "eslint": "^8",
55 | "eslint-config-next": "14.2.10",
56 | "postcss": "^8",
57 | "prisma": "^5.19.1",
58 | "tailwindcss": "^3.4.1",
59 | "typescript": "^5"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | datasource db {
12 | provider = "postgresql"
13 | url = env("DATABASE_URL")
14 | directUrl = env("DIRECT_URL")
15 | }
16 |
17 | model User {
18 | id String @id @default(uuid())
19 | name String?
20 | email String @unique
21 | emailVerified DateTime?
22 | image String? @default("https://avatars.githubusercontent.com/u/10367109?v=4")
23 | accounts Account[]
24 | sessions Session[]
25 | grantId String?
26 | grantEmail String?
27 | username String? @unique
28 | createdAt DateTime @default(now())
29 | updatedAt DateTime @updatedAt
30 | Availability Availability[]
31 | EventType EventType[]
32 | }
33 |
34 | model Account {
35 | userId String
36 | type String
37 | provider String
38 | providerAccountId String
39 | refresh_token String?
40 | access_token String?
41 | expires_at Int?
42 | token_type String?
43 | scope String?
44 | id_token String?
45 | session_state String?
46 |
47 | createdAt DateTime @default(now())
48 | updatedAt DateTime @updatedAt
49 |
50 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
51 |
52 | @@id([provider, providerAccountId])
53 | }
54 |
55 | model Session {
56 | sessionToken String @unique
57 | userId String
58 | expires DateTime
59 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
60 |
61 | createdAt DateTime @default(now())
62 | updatedAt DateTime @updatedAt
63 | }
64 |
65 | model VerificationToken {
66 | identifier String
67 | token String
68 | expires DateTime
69 |
70 | @@id([identifier, token])
71 | }
72 |
73 | model Availability {
74 | id String @id @default(uuid())
75 |
76 | day Day
77 | fromTime String // e.g., '08:00'
78 | tillTime String // e.g., '18:00'
79 | isActive Boolean @default(true) // Toggle availability for the day
80 |
81 | User User @relation(fields: [userId], references: [id], onDelete: Cascade)
82 | userId String
83 |
84 | createdAt DateTime @default(now())
85 | updatedAt DateTime @updatedAt
86 | }
87 |
88 | enum Day {
89 | Monday
90 | Tuesday
91 | Wednesday
92 | Thursday
93 | Friday
94 | Saturday
95 | Sunday
96 | }
97 |
98 | model EventType {
99 | id String @id @default(uuid())
100 | title String
101 | duration Int
102 | url String
103 | description String
104 | active Boolean @default(true)
105 | videoCallSoftware String @default("Google Meet")
106 |
107 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
108 | userId String
109 |
110 | createdAt DateTime @default(now())
111 | }
112 |
--------------------------------------------------------------------------------
/public/better.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/better.png
--------------------------------------------------------------------------------
/public/dashboard-new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/dashboard-new.png
--------------------------------------------------------------------------------
/public/face.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/face.jpeg
--------------------------------------------------------------------------------
/public/github.svg:
--------------------------------------------------------------------------------
1 |
9 |
11 |
--------------------------------------------------------------------------------
/public/google.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/gradient.svg:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
16 |
20 |
24 |
28 |
29 |
30 |
31 |
40 |
41 |
47 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/public/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/hero.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/logo.png
--------------------------------------------------------------------------------
/public/meet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/meet.png
--------------------------------------------------------------------------------
/public/nextjs-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/public/nylas-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/nylas-logo.png
--------------------------------------------------------------------------------
/public/supabase.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/public/teams.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/teams.png
--------------------------------------------------------------------------------
/public/typescript-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/typescript-logo.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/work-is-almost-over-happy.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/calcom-nylas/ccaa57ac0f1cee73691fe5d0f08b4b3115dcf5f7/public/work-is-almost-over-happy.gif
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import { withUt } from "uploadthing/tw";
3 |
4 | const config: Config = {
5 | darkMode: ["class"],
6 | content: [
7 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
9 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
10 | ],
11 | theme: {
12 | extend: {
13 | colors: {
14 | background: "hsl(var(--background))",
15 | foreground: "hsl(var(--foreground))",
16 | card: {
17 | DEFAULT: "hsl(var(--card))",
18 | foreground: "hsl(var(--card-foreground))",
19 | },
20 | popover: {
21 | DEFAULT: "hsl(var(--popover))",
22 | foreground: "hsl(var(--popover-foreground))",
23 | },
24 | primary: {
25 | DEFAULT: "hsl(var(--primary))",
26 | foreground: "hsl(var(--primary-foreground))",
27 | },
28 | secondary: {
29 | DEFAULT: "hsl(var(--secondary))",
30 | foreground: "hsl(var(--secondary-foreground))",
31 | },
32 | muted: {
33 | DEFAULT: "hsl(var(--muted))",
34 | foreground: "hsl(var(--muted-foreground))",
35 | },
36 | accent: {
37 | DEFAULT: "hsl(var(--accent))",
38 | foreground: "hsl(var(--accent-foreground))",
39 | },
40 | destructive: {
41 | DEFAULT: "hsl(var(--destructive))",
42 | foreground: "hsl(var(--destructive-foreground))",
43 | },
44 | border: "hsl(var(--border))",
45 | input: "hsl(var(--input))",
46 | ring: "hsl(var(--ring))",
47 | chart: {
48 | "1": "hsl(var(--chart-1))",
49 | "2": "hsl(var(--chart-2))",
50 | "3": "hsl(var(--chart-3))",
51 | "4": "hsl(var(--chart-4))",
52 | "5": "hsl(var(--chart-5))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | },
61 | },
62 | plugins: [require("tailwindcss-animate")],
63 | };
64 | export default withUt(config);
65 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------