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 @@
Cancel
-
+
+
+
+
+
{/if}