Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Group Availability Tab #97

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
70b9d19
feat: ✨ add meeting creation submission functionality
seancfong Apr 30, 2024
da456a0
Merge remote-tracking branch 'origin/50-connect-meeting-creation-page…
MinhxNguyen7 May 13, 2024
2f09718
feat(GroupAvailability): ✨ load group availabilities from server
MinhxNguyen7 May 13, 2024
0ae875b
chore: 🔧 update drizzle-kit
MinhxNguyen7 May 13, 2024
d2cc381
feat(Creation): ✨ meeting generation testing functionality
MinhxNguyen7 May 13, 2024
a1cc45d
chore: 🔧 more descriptive function names
MinhxNguyen7 May 13, 2024
32ce0d1
Revert "feat(Creation): ✨ meeting generation testing functionality"
MinhxNguyen7 May 20, 2024
464e709
Merge remote-tracking branch 'origin/main' into group-availability
MinhxNguyen7 May 20, 2024
1cc77b6
fix(availability): 🐛 missing import
MinhxNguyen7 May 20, 2024
8c26969
fix(schema): 🐛 remove extraneous member type column
MinhxNguyen7 May 20, 2024
ea22664
fix(create): 🐛 throw error on fail
MinhxNguyen7 May 20, 2024
0788e10
chore(migrate): 🔧 print stage
MinhxNguyen7 May 20, 2024
edaaced
chore(packages): 🔧 update drizzle-kit
MinhxNguyen7 May 20, 2024
4e8f2b4
fix: 🐛 query from members instead of users
MinhxNguyen7 May 20, 2024
c27a120
fix: 🐛 convert group availability to boolean representation
seancfong May 21, 2024
93f23d7
refactor: ♻️ clean up order of availability parsing
seancfong May 21, 2024
55fb1ba
Merge branch 'main' of https://github.com/icssc/ZotMeet into group-av…
seancfong May 31, 2024
d2b501a
fix: 🐛 update parsing of group availability blocks
seancfong May 31, 2024
f6e3e19
refactor: ♻️ remove duplicate store code
seancfong May 31, 2024
9472a72
feat: ✨ add local update to load data after save form action is called
seancfong May 31, 2024
40b6357
fix: 🐛 separate reactive logic for personal and group availability
seancfong Jun 3, 2024
de5bec4
Merge branch 'main' into group-availability
seancfong Jun 7, 2024
9a5b1d3
chore: 🔧 remove duplicate test meeting creation file
seancfong Jun 7, 2024
add4ede
fix: 🐛 resolve typos and types
seancfong Jun 7, 2024
ca7d74d
refactor: ♻️ remove ts-expect-error for availability page load
seancfong Jun 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,034 changes: 973 additions & 61 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

13 changes: 0 additions & 13 deletions src/lib/components/availability/PersonalAvailability.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@
import {
availabilityDates,
availabilityTimeBlocks,
guestSession,
isEditingAvailability,
isStateUnsaved,
} from "$lib/stores/availabilityStores";
import type { AvailabilityBlockType, SelectionStateType } from "$lib/types/availability";
import { ZotDate } from "$lib/utils/ZotDate";
import { getGeneralAvailability } from "$lib/utils/availability";
import { cn } from "$lib/utils/utils";

export let columns: number;
Expand Down Expand Up @@ -154,17 +152,6 @@
}

onMount(async () => {
$guestSession.meetingId = data.meetingId ?? "";

const generalAvailability = await getGeneralAvailability(data, $guestSession);
const defaultMeetingDates = data.defaultDates.map((item) => new ZotDate(item.date, false, []));
ZotDate.initializeAvailabilities(defaultMeetingDates);

$availabilityDates =
generalAvailability && generalAvailability.length > 0
? generalAvailability
: defaultMeetingDates;

lastPage = Math.floor(($availabilityDates.length - 1) / itemsPerPage);
});
</script>
Expand Down
10 changes: 6 additions & 4 deletions src/lib/db/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ if (!MIGRATION_DB_URL) {
"MIGRATION_DB_URL not found. Please ensure you have the MIGRATION_DB_URL variable defined inside of your environment configuration.",
);
}
const migrationClient = postgres(
`${MIGRATION_DB_URL}${process.env["STAGE"] === "prod" ? "" : "?search_path=dev"}`,
{ max: 1, ssl: "prefer" },
);

const isProd = process.env["STAGE"] === "prod";
console.log(`Running migrations in ${isProd ? "production" : "development"} mode.`);

const migrationConnectionString = `${MIGRATION_DB_URL}${isProd ? "" : "?search_path=dev"}`;
const migrationClient = postgres(migrationConnectionString, { max: 1, ssl: "prefer" });
const db = drizzle(migrationClient);

await migrate(db, {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
export const attendanceValues = ["accepted", "maybe", "declined"] as const;
export type AttendanceValue = (typeof attendanceValues)[number];

export const attendanceEnum = pgEnum("attendance", ["accepted", "maybe", "declined"]);
export const attendanceEnum = pgEnum("attendance", attendanceValues);

// Members encompasses anyone who uses ZotMeet, regardless of guest or user status.
export const members = pgTable("members", {
Expand Down
46 changes: 3 additions & 43 deletions src/lib/stores/availabilityStores.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readable, writable } from "svelte/store";
import { writable } from "svelte/store";

import type { GuestSession, MemberAvailability } from "./../types/availability";

Expand All @@ -25,41 +25,9 @@ endTime.subscribe((value) => {
latestTime = getTimeFromHourMinuteString(value ?? "17:30");
});

const sampleMembers: MemberAvailability[] = [
{
name: "Sean Fong",
availableBlocks: [[1], [2], [3, 4, 5], [], [], [], []],
},
{
name: "Joe Biden",
availableBlocks: [[], [1, 2], [4, 5, 6, 22, 23, 24, 25, 26, 27, 28], [], [], [], []],
},
{
name: "Chuck Norris",
availableBlocks: [
[4, 5, 6, 7, 8, 9, 10, 11, 20, 21, 22, 23, 24],
[3, 4, 5, 6, 7],
[4, 5, 6],
[],
[],
[],
[],
],
},
{
name: "Dwayne the Rock",
availableBlocks: [[], [1, 2, 3, 4, 5], [4, 5, 6, 25, 26, 27, 28], [], [], [], []],
},
{
name: "Kevin Hart",
availableBlocks: [[], [1, 2], [26, 27, 28, 29, 30, 31], [], [], [], []],
},
];

export const generateSampleDates = (
startTime: number = earliestTime,
endTime: number = latestTime,
groupMembers: MemberAvailability[] = sampleMembers,
): ZotDate[] => {
// Placeholder date array from Calendar component
const selectedCalendarDates: ZotDate[] = [
Expand All @@ -74,12 +42,6 @@ export const generateSampleDates = (

ZotDate.initializeAvailabilities(selectedCalendarDates, startTime, endTime, BLOCK_LENGTH);

groupMembers.forEach(({ availableBlocks }, memberIndex) => {
availableBlocks.forEach((availableBlocks, dateIndex) => {
selectedCalendarDates[dateIndex].setGroupMemberAvailability(memberIndex, availableBlocks);
});
});

return selectedCalendarDates;
};

Expand All @@ -95,11 +57,9 @@ export const generateTimeBlocks = (startTime: number, endTime: number): number[]
};

const defaultTimeBlocks = generateTimeBlocks(earliestTime, latestTime);
export const availabilityDates = writable<ZotDate[]>(
generateSampleDates(earliestTime, latestTime, sampleMembers),
);
export const availabilityDates = writable<ZotDate[]>(generateSampleDates(earliestTime, latestTime));
export const availabilityTimeBlocks = writable<number[]>(defaultTimeBlocks);
export const groupAvailabilities = readable<MemberAvailability[]>(sampleMembers);
export const groupAvailabilities = writable<MemberAvailability[]>([]);

export const isEditingAvailability = writable<boolean>(false);
export const isStateUnsaved = writable<boolean>(false);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/types/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type SelectionStateType = {

export interface MemberAvailability {
name: string;
availableBlocks: number[][];
availableBlocks: boolean[][];
}

export interface LoginModalProps {
Expand Down
15 changes: 12 additions & 3 deletions src/lib/utils/ZotDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ export class ZotDate {
*
* e.g. A group member array: `['Sean', 'Collan', 'Joe']`
*
* `setGroupMemberAvailability(1, [3, 4, 5])` will update availability time blocks 3 - 5
* `setGroupMemberAvailability(1, [false, false, true, true, true])` will update availability time blocks 3 - 5
* to indicate Collan is available.
* - if Sean was already available on block 3, block 3 will be changed from `[0]` to `[0, 1]`.
* - if nobody was already available on block 4, block 4 will be changed from `null` to `[1]`.
Expand All @@ -366,8 +366,10 @@ export class ZotDate {
* @param memberIndex the index of a member in an array
* @param availableBlocks an array of availability blocks to set that member's availability
*/
setGroupMemberAvailability(memberIndex: number, availableBlocks: number[]): void {
availableBlocks.forEach((blockIndex) => {
setGroupMemberAvailability(memberIndex: number, availableBlocks: boolean[]): void {
availableBlocks.forEach((isAvailable, blockIndex) => {
if (!isAvailable) return;

if (!this.groupAvailability[blockIndex]) {
this.groupAvailability[blockIndex] = [memberIndex];
} else {
Expand All @@ -376,6 +378,13 @@ export class ZotDate {
});
}

/**
* Resets the group availability array
*/
resetGroupAvailability(): void {
this.groupAvailability = [];
}

/**
* Gets the group availability block based on the block index
* @param index index of the availability block
Expand Down
15 changes: 14 additions & 1 deletion src/lib/utils/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { PageData } from "../../routes/availability/[slug]/$types";
import { ZotDate } from "./ZotDate";

import type { AvailabilityMeetingDateJoinSchema } from "$lib/db/schema";
import type { GuestSession } from "$lib/types/availability";
import type { GuestSession, MemberAvailability } from "$lib/types/availability";

export async function getGuestAvailability(guestSession: GuestSession) {
const response = await fetch("/api/availability", {
Expand Down Expand Up @@ -55,3 +55,16 @@ export const getGeneralAvailability = async (data: PageData, guestSession: Guest

return null;
};

export function availabilityDatesToBlocks(
memberAvailabilities: Record<string, { day: Date; availability_string: string }[]>,
): MemberAvailability[] {
return Object.entries(memberAvailabilities).map(([name, availabilities]) => {
return {
name,
availableBlocks: availabilities.map((availability) => {
return availability.availability_string.split("").map((char) => char === "1");
}),
};
});
}
57 changes: 52 additions & 5 deletions src/routes/availability/[slug]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,29 @@ import {
type AvailabilityInsertSchema,
type AvailabilityMeetingDateJoinSchema,
type MeetingDateSelectSchema,
users,
meetings,
members,
} from "$lib/db/schema";
import type { ZotDate } from "$lib/utils/ZotDate";

export const load: PageServerLoad = (async ({ locals, params }) => {
const user = locals.user;
const meeting_id: string = params?.slug ?? "";

// TODO: If no slug is in the URL (i.e. no meeting ID), we should redirect to an error page

return {
form: await superValidate(_loginSchema),
availability: user ? await getAvailability(user, params?.slug) : null,
meetingId: params?.slug as string | undefined,
meetingData: await getExistingMeeting(params?.slug),
defaultDates: (await _getMeetingDates(params?.slug)) ?? [],
availability: user ? await getUserSpecificAvailability(user, meeting_id) : null,
groupAvailabilities: await getMeetingMemberAvailabilities(meeting_id),
meetingId: meeting_id as string | undefined,
meetingData: await getExistingMeeting(meeting_id),
defaultDates: (await _getMeetingDates(meeting_id)) ?? [],
};
}) satisfies PageServerLoad;

const getAvailability = async (
const getUserSpecificAvailability = async (
user: User,
meetingId: string | undefined,
): Promise<AvailabilityMeetingDateJoinSchema[]> => {
Expand All @@ -47,6 +52,48 @@ const getAvailability = async (
return availability.sort((a, b) => (a.meeting_dates.date > b.meeting_dates.date ? 1 : -1));
};

/**
* Get all availabilities of members for a meeting
*
* @param meetingId
* @returns a record of the member name to their availabilities, each sorted by date
*/
async function getMeetingMemberAvailabilities(meetingId: string) {
const raw_availabilities = await db
.select({
username: users.displayName,
availability_string: availabilities.availability_string,
day: meetingDates.date,
})
.from(availabilities)
.innerJoin(meetingDates, eq(availabilities.meeting_day, meetingDates.id))
.innerJoin(meetings, eq(meetingDates.meeting_id, meetings.id))
.innerJoin(members, eq(availabilities.member_id, members.id))
.innerJoin(users, eq(members.id, users.id))
.where(eq(meetings.id, meetingId));

// Group availabilities by user
const userAvailabilities = raw_availabilities.reduce(
(acc, { username, availability_string, day }) => {
if (!acc[username]) {
acc[username] = [];
}

acc[username].push({ day, availability_string });

return acc;
},
{} as Record<string, { day: Date; availability_string: string }[]>,
);

// Sort availabilities by date
for (const username in userAvailabilities) {
userAvailabilities[username].sort((a, b) => (a.day < b.day ? -1 : 1));
}

return userAvailabilities;
}

export const actions: Actions = {
save: save,
};
Expand Down
55 changes: 48 additions & 7 deletions src/routes/availability/[slug]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
generateSampleDates,
generateTimeBlocks,
getTimeFromHourMinuteString,
groupAvailabilities,
guestSession,
isEditingAvailability,
isStateUnsaved,
} from "$lib/stores/availabilityStores";
import { endTime, startTime } from "$lib/stores/meetingSetupStores";
import type { HourMinuteString } from "$lib/types/chrono";
import { getGeneralAvailability } from "$lib/utils/availability";
import { ZotDate } from "$lib/utils/ZotDate";
import { availabilityDatesToBlocks, getGeneralAvailability } from "$lib/utils/availability";
import { cn } from "$lib/utils/utils";
import CancelCircleOutline from "~icons/mdi/cancel-circle-outline";
import CheckboxMarkerdCircleOutlineIcon from "~icons/mdi/checkbox-marked-circle-outline";
Expand Down Expand Up @@ -51,22 +53,61 @@
$isStateUnsaved = false;
};

let innerWidth = 0;
$: mobileView = innerWidth < 768;
const updatePersonalAvailability = async () => {
if (data.meetingId) {
$guestSession.meetingId = data.meetingId;
}

const generalAvailability = await getGeneralAvailability(data, $guestSession);

const defaultMeetingDates = data.defaultDates.map((item) => new ZotDate(item.date, false, []));
ZotDate.initializeAvailabilities(defaultMeetingDates);

$availabilityDates =
generalAvailability && generalAvailability.length > 0
? generalAvailability
: defaultMeetingDates;
};

const updateGroupAvailability = () => {
const groupAvailabilitiesBlocks = availabilityDatesToBlocks(data.groupAvailabilities);

$availabilityDates.forEach((date) => date.resetGroupAvailability());

groupAvailabilitiesBlocks.forEach(({ availableBlocks }, memberIndex) => {
availableBlocks.forEach((blocks, dateIndex) => {
$availabilityDates[dateIndex].setGroupMemberAvailability(memberIndex, blocks);
});
});

$groupAvailabilities = groupAvailabilitiesBlocks;
};

let innerWidth = 0;
let form: HTMLFormElement;

onMount(async () => {
$startTime = data.meetingData.from_time as HourMinuteString;
$endTime = data.meetingData.to_time as HourMinuteString;
});
$: mobileView = innerWidth < 768;

$: {
if (data) {
updateGroupAvailability();
}
}

$: availabilityTimeBlocks.set(
generateTimeBlocks(
getTimeFromHourMinuteString($startTime),
getTimeFromHourMinuteString($endTime),
),
);

onMount(async () => {
$startTime = data.meetingData.from_time as HourMinuteString;
$endTime = data.meetingData.to_time as HourMinuteString;

await updatePersonalAvailability();
updateGroupAvailability();
});
</script>

<svelte:window bind:innerWidth />
Expand Down
Loading