diff --git a/package.json b/package.json index f6d21243..0f633d71 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "cz-conventional-changelog": "3.3.0", "daisyui": "^4.6.2", "devmoji": "2.3.0", - "drizzle-kit": "0.20.14", + "dotenv": "^16.4.5", + "drizzle-kit": "0.20.17", "eslint": "8.54.0", "eslint-config-prettier": "9.0.0", "eslint-plugin-import": "2.29.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c637ddab..324c99e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,8 +110,8 @@ devDependencies: specifier: 2.3.0 version: 2.3.0 drizzle-kit: - specifier: 0.20.14 - version: 0.20.14 + specifier: 0.20.17 + version: 0.20.17 eslint: specifier: 8.54.0 version: 8.54.0 @@ -1975,12 +1975,6 @@ packages: dev: true optional: true - /@drizzle-team/studio@0.0.39: - resolution: {integrity: sha512-c5Hkm7MmQC2n5qAsKShjQrHoqlfGslB8+qWzsGGZ+2dHMRTNG60UuzalF0h0rvBax5uzPXuGkYLGaQ+TUX3yMw==} - dependencies: - superjson: 2.2.1 - dev: true - /@emnapi/core@0.45.0: resolution: {integrity: sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==} requiresBuild: true @@ -2751,6 +2745,21 @@ packages: tslib: 2.6.2 dev: true + /@hono/node-server@1.11.0: + resolution: {integrity: sha512-TLIJq9TMtD1NEG1mVoqNUn1Ita0qSaB5XboZErjFBcO/GJYXwWY4dVdTi9G0lbxtu0x+hJXDItcLaFHb7rlFTw==} + engines: {node: '>=18.14.1'} + dev: true + + /@hono/zod-validator@0.2.1(hono@4.2.6)(zod@3.22.4): + resolution: {integrity: sha512-HFoxln7Q6JsE64qz2WBS28SD33UB2alp3aRKmcWnNLDzEL1BLsWfbdX6e1HIiUprHYTIXf5y7ax8eYidKUwyaA==} + peerDependencies: + hono: '>=3.9.0' + zod: ^3.19.1 + dependencies: + hono: 4.2.6 + zod: 3.22.4 + dev: true + /@httptoolkit/websocket-stream@6.0.1: resolution: {integrity: sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ==} dependencies: @@ -5762,12 +5771,13 @@ packages: wordwrap: 1.0.0 dev: true - /drizzle-kit@0.20.14: - resolution: {integrity: sha512-0fHv3YIEaUcSVPSGyaaBfOi9bmpajjhbJNdPsRMIUvYdLVxBu9eGjH8mRc3Qk7HVmEidFc/lhG1YyJhoXrn5yA==} + /drizzle-kit@0.20.17: + resolution: {integrity: sha512-mLVDS4nXmO09wFVlzGrdshWnAL+U9eQGC5zRs6hTN6Q9arwQGWU2XnZ17I8BM8Quau8CQRx3Ms6VPgRWJFVp7Q==} hasBin: true dependencies: - '@drizzle-team/studio': 0.0.39 '@esbuild-kit/esm-loader': 2.6.5 + '@hono/node-server': 1.11.0 + '@hono/zod-validator': 0.2.1(hono@4.2.6)(zod@3.22.4) camelcase: 7.0.1 chalk: 5.3.0 commander: 9.5.0 @@ -5776,9 +5786,11 @@ packages: esbuild-register: 3.5.0(esbuild@0.19.12) glob: 8.1.0 hanji: 0.0.5 + hono: 4.2.6 json-diff: 0.9.0 minimatch: 7.4.6 semver: 7.6.0 + superjson: 2.2.1 zod: 3.22.4 transitivePeerDependencies: - supports-color @@ -7119,6 +7131,11 @@ packages: parse-passwd: 1.0.0 dev: true + /hono@4.2.6: + resolution: {integrity: sha512-AtbHZJYWsm+uFHLz0C6xltX7hjOV44a55gSEGBfoQOJ00KSxEUOoiIkmd+NXfapNX0j2GCKhqMmYeegBdHRwcQ==} + engines: {node: '>=16.0.0'} + dev: true + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true diff --git a/src/lib/components/availability/AvailabilityBlock.svelte b/src/lib/components/availability/AvailabilityBlock.svelte index 646361f1..49160a96 100644 --- a/src/lib/components/availability/AvailabilityBlock.svelte +++ b/src/lib/components/availability/AvailabilityBlock.svelte @@ -31,6 +31,7 @@ $: { updateBlockColor(selectionState); + isAvailable; } diff --git a/src/lib/components/availability/LoginModal.svelte b/src/lib/components/availability/LoginModal.svelte index 57757222..c3a60ce6 100644 --- a/src/lib/components/availability/LoginModal.svelte +++ b/src/lib/components/availability/LoginModal.svelte @@ -1,19 +1,25 @@ @@ -142,19 +180,18 @@

Save as Guest

handleGuestSubmit(data.meetingId)} > - {#if $guestErrors._errors} + {#if formState === "failure"} {/if} @@ -166,17 +203,11 @@ type="text" class="grow appearance-none border-none focus:border-none focus:outline-none focus:ring-0" placeholder="username" - bind:value={$guestForm.username} + name="username" /> - +
diff --git a/src/lib/components/availability/PersonalAvailability.svelte b/src/lib/components/availability/PersonalAvailability.svelte index 076b00cd..515b1759 100644 --- a/src/lib/components/availability/PersonalAvailability.svelte +++ b/src/lib/components/availability/PersonalAvailability.svelte @@ -1,23 +1,25 @@
diff --git a/src/lib/config/zod-schemas.ts b/src/lib/config/zod-schemas.ts index a37f8db9..e0a734b2 100644 --- a/src/lib/config/zod-schemas.ts +++ b/src/lib/config/zod-schemas.ts @@ -43,4 +43,5 @@ export const guestSchema = z.object({ .string({ required_error: "Username is required" }) .min(1, { message: "Username is required" }) .trim(), + meetingId: z.string().min(1, { message: "Username is required" }).trim(), }); diff --git a/src/lib/db/databaseUtils.server.ts b/src/lib/db/databaseUtils.server.ts index a850d8c6..e88f9243 100644 --- a/src/lib/db/databaseUtils.server.ts +++ b/src/lib/db/databaseUtils.server.ts @@ -1,9 +1,17 @@ -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import type { SuperValidated } from "sveltekit-superforms"; import type { ZodObject, ZodString } from "zod"; import { db } from "./drizzle"; -import { users, type UserInsertSchema } from "./schema"; +import { members, users, guests, meetings, meetingDates } from "./schema"; +import type { + UserInsertSchema, + MemberInsertSchema, + MeetingSelectSchema, + GuestInsertSchema, + MeetingInsertSchema, + MeetingDateInsertSchema, +} from "./schema"; import type { AlertMessageType } from "$lib/types/auth"; @@ -29,10 +37,30 @@ export const checkIfEmailExists = async (email: string) => { return queryResult.length > 0; }; +export const checkIfGuestUsernameExists = async ( + username: string, + meeting: MeetingSelectSchema, +) => { + const result = await db + .select() + .from(guests) + .where(and(eq(guests.username, username), eq(guests.meeting_id, meeting.id))); + + return result.length > 0; +}; + +export const insertNewMember = async (member: MemberInsertSchema) => { + return await db.insert(members).values(member); +}; + export const insertNewUser = async (user: UserInsertSchema) => { return await db.insert(users).values(user); }; +export const insertNewGuest = async (guest: GuestInsertSchema) => { + return await db.insert(guests).values(guest); +}; + export const getAllUsers = async () => { const queryResult = await db .select({ @@ -60,3 +88,57 @@ export const getExistingUser = async ( return existingUser; }; + +export const getExistingGuest = async (username: string, meeting: MeetingSelectSchema) => { + const [existingGuest] = await db + .select() + .from(guests) + .where(and(eq(guests.username, username), eq(guests.meeting_id, meeting.id))); + + return existingGuest; +}; + +/** + * To create a meeting, call this function with: + * 1. A title + * 2. A start time; I used: 2024-01-31T16:00:00.000Z + * 3. An end time: I used: 2024-02-06T16:00:00.000Z + * + * NOTE: + * `generateSampleDates()` is called whenever no availability is found + * If you use dates other than the ones above, generateSampleDates() will return dates + * other than the ones your meeting may *actually* be of + * + * @param meeting + */ +export const insertMeeting = async (meeting: MeetingInsertSchema) => { + const [dbMeeting] = await db.insert(meetings).values(meeting).returning(); + await insertMeetingDates(dbMeeting); +}; + +export const getExistingMeeting = async (meetingId: string) => { + const [dbMeeting] = await db.select().from(meetings).where(eq(meetings.id, meetingId)); + + return dbMeeting; +}; + +export const insertMeetingDates = async (meeting: MeetingSelectSchema) => { + const currentDate = meeting.from_time; + currentDate.setDate(currentDate.getDate()); + + for (let i = 0; currentDate <= meeting.to_time; i++) { + const value: MeetingDateInsertSchema = { date: currentDate, meeting_id: meeting.id }; + await db.insert(meetingDates).values(value); + + currentDate.setDate(currentDate.getDate() + 1); + } +}; + +export const getExistingMeetingDates = async (meetingId: string) => { + const dbMeetingDates = await db + .select() + .from(meetingDates) + .where(eq(meetingDates.meeting_id, meetingId)); + + return dbMeetingDates; +}; diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 23d919ff..19976be6 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -35,6 +35,7 @@ export const users = pgTable("users", { }); // Guests are Members who do not have an account and are bound to one specific meeting. + export const guests = pgTable( "guests", { @@ -96,6 +97,7 @@ export const availabilities = pgTable( pk: primaryKey({ columns: [table.member_id, table.meeting_day] }), }), ); + // meeting_day export const oauthAccountsTable = pgTable( "oauth_accounts", @@ -105,7 +107,6 @@ export const oauthAccountsTable = pgTable( .references(() => users.id, { onDelete: "cascade", }), - providerId: text("provider_id").notNull(), providerUserId: text("provider_user_id").notNull(), }, @@ -132,6 +133,7 @@ export const sessions = pgTable( }; }, ); + export const usersInGroup = pgTable( "users_in_group", { @@ -210,16 +212,16 @@ export const meetingsRelations = relations(meetings, ({ one, many }) => ({ fields: [meetings.host_id], references: [members.id], }), - availabilities: many(availabilities), membersInMeeting: many(membersInMeeting), meetingDates: many(meetingDates), })); -export const meetingDateRelations = relations(meetingDates, ({ one }) => ({ +export const meetingDatesRelations = relations(meetingDates, ({ one, many }) => ({ meetings: one(meetings, { fields: [meetingDates.meeting_id], references: [meetings.id], }), + availabilities: many(availabilities), })); export const sessionsRelations = relations(sessions, ({ one }) => ({ @@ -247,4 +249,11 @@ export const availabilitiesRelations = relations(availabilities, ({ one }) => ({ }), })); +export type MemberInsertSchema = typeof members.$inferInsert; export type UserInsertSchema = typeof users.$inferInsert; +export type GuestInsertSchema = typeof guests.$inferInsert; +export type AvailabilityInsertSchema = typeof availabilities.$inferInsert; +export type MeetingInsertSchema = typeof meetings.$inferInsert; +export type MeetingSelectSchema = typeof meetings.$inferSelect; +export type MeetingDateInsertSchema = typeof meetingDates.$inferInsert; +export type MeetingDateSelectSchema = typeof meetingDates.$inferSelect; diff --git a/src/lib/stores/availabilityStores.ts b/src/lib/stores/availabilityStores.ts index 7da0fc7e..c02fbd20 100644 --- a/src/lib/stores/availabilityStores.ts +++ b/src/lib/stores/availabilityStores.ts @@ -1,6 +1,6 @@ import { readable, writable } from "svelte/store"; -import type { MemberAvailability } from "./../types/availability"; +import type { GuestSession, MemberAvailability } from "./../types/availability"; import { TimeConstants } from "$lib/types/chrono"; import { ZotDate } from "$lib/utils/ZotDate"; @@ -50,20 +50,20 @@ const sampleMembers: MemberAvailability[] = [ }, ]; -const generateSampleDates = ( - startTime: number, - endTime: number, - groupMembers: MemberAvailability[], +export const generateSampleDates = ( + startTime: number = earliestTime, + endTime: number = latestTime, + groupMembers: MemberAvailability[] = sampleMembers, ): ZotDate[] => { // Placeholder date array from Calendar component const selectedCalendarDates: ZotDate[] = [ + new ZotDate(new Date(2024, 0, 30)), + new ZotDate(new Date(2024, 0, 31)), new ZotDate(new Date(2024, 1, 1)), new ZotDate(new Date(2024, 1, 2)), new ZotDate(new Date(2024, 1, 3)), new ZotDate(new Date(2024, 1, 4)), new ZotDate(new Date(2024, 1, 5)), - new ZotDate(new Date(2024, 1, 6)), - new ZotDate(new Date(2024, 1, 7)), ]; ZotDate.initializeAvailabilities(selectedCalendarDates, startTime, endTime, BLOCK_LENGTH); @@ -97,3 +97,8 @@ export const groupMembers = readable(sampleMembers); export const isEditingAvailability = writable(false); export const isStateUnsaved = writable(false); + +export const guestSession = writable({ + guestName: "", + meetingId: "", +}); diff --git a/src/lib/types/availability.ts b/src/lib/types/availability.ts index 0da851e8..99c0fa3b 100644 --- a/src/lib/types/availability.ts +++ b/src/lib/types/availability.ts @@ -23,3 +23,8 @@ export interface LoginModalProps { form: SuperValidated>; guestForm: SuperValidated>; } + +export interface GuestSession { + guestName: string; + meetingId: string; +} diff --git a/src/lib/utils/ZotDate.ts b/src/lib/utils/ZotDate.ts index 0f7f8fa6..8e0173db 100644 --- a/src/lib/utils/ZotDate.ts +++ b/src/lib/utils/ZotDate.ts @@ -16,13 +16,13 @@ export class ZotDate { * @param day a Date object representing a calendar day * @param isSelected whether the day is selected from the calendar */ - constructor(day: Date = new Date(), isSelected: boolean = false) { + constructor(day: Date = new Date(), isSelected: boolean = false, availability: boolean[] = []) { this.day = day; this.isSelected = isSelected; this.blockLength = 15; this.earliestTime = 0; this.latestTime = 0; - this.availability = []; + this.availability = availability; this.groupAvailability = []; } @@ -301,8 +301,8 @@ export class ZotDate { */ static initializeAvailabilities( selectedDates: ZotDate[], - earliestTime: number, - latestTime: number, + earliestTime: number = 480, + latestTime: number = 1050, blockLength: number = 15, ): void { const minuteRange = Math.abs(latestTime - earliestTime); diff --git a/src/lib/utils/availability.ts b/src/lib/utils/availability.ts new file mode 100644 index 00000000..a66f5506 --- /dev/null +++ b/src/lib/utils/availability.ts @@ -0,0 +1,57 @@ +import type { PageData } from "../../routes/availability/[slug]/$types"; + +import { ZotDate } from "./ZotDate"; + +import type { AvailabilityInsertSchema } from "$lib/db/schema"; +import type { GuestSession } from "$lib/types/availability"; + +export async function getGuestAvailability(guestSession: GuestSession) { + const response = await fetch("/api/availability", { + method: "POST", + body: JSON.stringify(guestSession), + headers: { + "content-type": "application/json", + }, + }); + + const guestData: AvailabilityInsertSchema[] = await response.json(); + + return guestData?.map( + (availability) => + new ZotDate( + new Date(availability.day), + false, + JSON.parse("[" + availability.availability_string + "]"), + ), + ); +} + +export const getUserAvailability = (data: PageData) => { + if (data.availability) { + return data.availability?.map( + (availability) => + new ZotDate( + new Date(availability.day), + false, + JSON.parse("[" + availability.availability_string + "]"), + ), + ); + } + return null; +}; + +export const getGeneralAvailability = async (data: PageData, guestSession: GuestSession) => { + const userAvailability = getUserAvailability(data); + + if (userAvailability) { + return userAvailability; + } + + const guestAvailability = await getGuestAvailability(guestSession); + + if (guestAvailability) { + return guestAvailability; + } + + return null; +}; diff --git a/src/routes/api/availability/+server.ts b/src/routes/api/availability/+server.ts new file mode 100644 index 00000000..266c76f8 --- /dev/null +++ b/src/routes/api/availability/+server.ts @@ -0,0 +1,35 @@ +import { json } from "@sveltejs/kit"; +import { and, eq } from "drizzle-orm"; + +import type { RequestHandler } from "./$types"; + +import { db } from "$lib/db/drizzle"; +import { availabilities, guests } from "$lib/db/schema"; + +export const POST: RequestHandler = async ({ request }) => { + const data = await request.json(); + + if (data.guestName === "" || data.meetingId === "") { + return json([]); + } + + const [guest] = await db + .select() + .from(guests) + .where(and(eq(guests.username, data.guestName), eq(guests.meeting_id, data.meetingId))); + + if (!guest) { + return json([]); + } + + const availability = await db + .select() + .from(availabilities) + .where(eq(availabilities.member_id, guest.id)); + + if (availability.length == 0) { + return json([]); + } + + return json(availability.sort((a, b) => (a.day < b.day ? -1 : 1))); +}; diff --git a/src/routes/auth/guest/+page.server.ts b/src/routes/auth/guest/+page.server.ts new file mode 100644 index 00000000..702d2f30 --- /dev/null +++ b/src/routes/auth/guest/+page.server.ts @@ -0,0 +1,66 @@ +import { fail } from "@sveltejs/kit"; +import { generateId } from "lucia"; +import { setError, superValidate } from "sveltekit-superforms/client"; + +import { _getMeeting } from "../../availability/[slug]/+page.server"; + +import { guestSchema } from "$lib/config/zod-schemas"; +import { + checkIfGuestUsernameExists, + // insertMeeting, + insertNewGuest, + insertNewMember, +} from "$lib/db/databaseUtils.server"; +import type { AlertMessageType } from "$lib/types/auth"; + +export const _guestSchema = guestSchema.pick({ + username: true, + meetingId: true, +}); + +export const actions = { + default: createGuest, +}; + +async function createGuest({ request }: { request: Request }) { + const form = await superValidate(request, _guestSchema); + + if (!form.valid) { + return fail(400, { form }); + } + + try { + const isGuestUsernameAlreadyRegistered = await checkIfGuestUsernameExists( + form.data.username, + await _getMeeting(form.data.meetingId), + ); + + if (isGuestUsernameAlreadyRegistered === true) { + return setError(form, "username", "Guest username already exists"); + } + + const id = generateId(15); + await insertNewMember({ id: id }); + await insertNewGuest({ + username: form.data.username, + id: id, + meeting_id: form.data.meetingId, + }); + + // await insertMeeting({ + // title: "test", + // from_time: new Date("2024-01-31T16:00:00.000Z"), + // to_time: new Date("2024-02-06T16:00:00.000Z"), + // }); + } catch (error) { + console.error(error); + + return setError( + form, + "username", + `An error occurred while processing your request. Please try again. Error: ${error}`, + ); + } + + return form.data; +} diff --git a/src/routes/auth/login/+page.server.ts b/src/routes/auth/login/+page.server.ts index af5ce3ea..7c20e0de 100644 --- a/src/routes/auth/login/+page.server.ts +++ b/src/routes/auth/login/+page.server.ts @@ -32,8 +32,6 @@ async function login({ request, cookies }: { request: Request; cookies: Cookies return fail(400, { form }); } - console.log(); - const existingUser = await getExistingUser(form); if (!existingUser) { diff --git a/src/routes/auth/register/+page.server.ts b/src/routes/auth/register/+page.server.ts index 9cd0f3d1..9f812b2f 100644 --- a/src/routes/auth/register/+page.server.ts +++ b/src/routes/auth/register/+page.server.ts @@ -6,7 +6,7 @@ import type { Actions, PageServerLoad } from "./$types"; import { userSchema } from "$lib/config/zod-schemas"; import { createAndSetSession } from "$lib/db/authUtils.server"; -import { checkIfEmailExists, insertNewUser } from "$lib/db/databaseUtils.server"; +import { checkIfEmailExists, insertNewMember, insertNewUser } from "$lib/db/databaseUtils.server"; import { lucia } from "$lib/server/lucia"; import type { AlertMessageType } from "$lib/types/auth"; @@ -49,12 +49,17 @@ async function register({ request, cookies }: { request: Request; cookies: Cooki const userId = generateId(15); const hashedPassword = await new Scrypt().hash(form.data.password); + await insertNewMember({ + id: userId, + type: "user", + }); + await insertNewUser({ id: userId, displayName: form.data.displayName, email: form.data.email, password: hashedPassword, - // authMethods: ["email"], + authMethods: ["email"], }); await createAndSetSession(lucia, userId, cookies); diff --git a/src/routes/availability/+page.server.ts b/src/routes/availability/+page.server.ts deleted file mode 100644 index c9705ff0..00000000 --- a/src/routes/availability/+page.server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { superValidate } from "sveltekit-superforms/server"; - -import { _loginSchema } from "../auth/login/+page.server"; - -import type { PageServerLoad } from "./$types"; - -import { guestSchema } from "$lib/config/zod-schemas"; - -const guestLoginSchema = guestSchema.pick({ username: true }); - -export const load = (async () => { - return { - form: await superValidate(_loginSchema), - guestForm: await superValidate(guestLoginSchema), - }; -}) satisfies PageServerLoad; diff --git a/src/routes/availability/[slug]/+page.server.ts b/src/routes/availability/[slug]/+page.server.ts new file mode 100644 index 00000000..4b416bb4 --- /dev/null +++ b/src/routes/availability/[slug]/+page.server.ts @@ -0,0 +1,126 @@ +import type { Actions } from "@sveltejs/kit"; +import { and, eq, sql } from "drizzle-orm"; +import type { User } from "lucia"; +import { superValidate } from "sveltekit-superforms/server"; + +import type { PageServerLoad } from "../$types"; +import { _loginSchema } from "../../auth/login/+page.server"; + +import { guestSchema } from "$lib/config/zod-schemas"; +import { getExistingGuest } from "$lib/db/databaseUtils.server"; +import { db } from "$lib/db/drizzle"; +import { + availabilities, + meetings, + meetingDates, + type AvailabilityInsertSchema, + type MeetingSelectSchema, + type MeetingDateSelectSchema, +} from "$lib/db/schema"; +import type { ZotDate } from "$lib/utils/ZotDate"; + +const guestLoginSchema = guestSchema.pick({ username: true }); + +export const load: PageServerLoad = (async ({ locals, params }) => { + const user = locals.user; + + return { + form: await superValidate(_loginSchema), + guestForm: await superValidate(guestLoginSchema), + availability: user ? await getAvailability(user, params?.slug) : null, + meetingId: params?.slug as string | undefined, + defaultDates: (await getMeetingDates(params?.slug)) ?? [], + }; +}) satisfies PageServerLoad; + +const getAvailability = async (user: User, meetingId: string | undefined) => { + const availability = await db + .select() + .from(availabilities) + .innerJoin(meetingDates, eq(availabilities.meeting_day, meetingDates.id)) + .where( + and(eq(availabilities.member_id, user.id), eq(meetingDates.meeting_id, meetingId ?? "")), + ); + + return availability.map((item) => item.availabilities).sort((a, b) => (a.day < b.day ? -1 : 1)); +}; + +export const actions: Actions = { + save: save, +}; + +async function save({ request, locals }: { request: Request; locals: App.Locals }) { + const user: User | null = locals.user; + + const formData = await request.formData(); + const availabilityDates: ZotDate[] = JSON.parse( + (formData.get("availabilityDates") as string) ?? "[]", + ); + const meetingId = (formData.get("meetingId") as string) ?? ""; + + let dbMeetingDates: MeetingDateSelectSchema[] = []; + + try { + dbMeetingDates = await getMeetingDates(meetingId); + } catch (e) { + console.log("Error getting meeting dates:", e); + } + + if (!dbMeetingDates || dbMeetingDates.length === 0) return; + + try { + const memberId = + user?.id ?? + (await getExistingGuest(formData.get("username") as string, await _getMeeting(meetingId))).id; + + const insertDates: AvailabilityInsertSchema[] = availabilityDates.map((date, index) => ({ + day: new Date(date.day).toISOString(), + member_id: memberId, + meeting_day: dbMeetingDates[index].id as string, // Type-cast since id is guaranteed if a meetingDate exists + availability_string: date.availability.toString(), + })); + + await db.transaction(async (tx) => { + await tx + .insert(availabilities) + .values(insertDates) + .onConflictDoUpdate({ + target: [availabilities.member_id, availabilities.meeting_day], + set: { + availability_string: sql.raw(`excluded.availability_string`), // `excluded` refers to the row currently in conflict + }, + }); + }); + + return { + status: 200, + body: { + message: "Saved successfully", + }, + }; + } catch (error) { + console.log("Error saving availabilities:", error); + return { + status: 500, + body: { + error: "Failed to save", + }, + }; + } +} + +export async function _getMeeting(meetingId: string): Promise { + const [meeting] = await db.select().from(meetings).where(eq(meetings.id, meetingId)); + + return meeting; +} + +async function getMeetingDates(meetingId: string): Promise { + const dbMeeting = await _getMeeting(meetingId); + const dbMeetingDates = await db + .select() + .from(meetingDates) + .where(eq(meetingDates.meeting_id, dbMeeting.id)); + + return dbMeetingDates.sort((a, b) => (a.date < b.date ? -1 : 1)); +} diff --git a/src/routes/availability/+page.svelte b/src/routes/availability/[slug]/+page.svelte similarity index 54% rename from src/routes/availability/+page.svelte rename to src/routes/availability/[slug]/+page.svelte index 6cc35e33..9dcc7bf2 100644 --- a/src/routes/availability/+page.svelte +++ b/src/routes/availability/[slug]/+page.svelte @@ -1,31 +1,44 @@
-

+

Sample Meeting Winter 2024

@@ -54,16 +69,40 @@ - + + + + +
{/if}