Skip to content

Commit

Permalink
Calendar Page (#978)
Browse files Browse the repository at this point in the history
Co-authored-by: Alder Whiteford <[email protected]>
Co-authored-by: Alder Whiteford <[email protected]>
  • Loading branch information
3 people authored Jun 10, 2024
1 parent 8cdc0a0 commit d30b126
Show file tree
Hide file tree
Showing 29 changed files with 3,112 additions and 5,912 deletions.
2 changes: 1 addition & 1 deletion frontend/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"test": "jest"
},
"dependencies": {
"@generatesac/lib": "^0.0.1",
"@generatesac/lib": "^0.0.11",
"@hookform/resolvers": "^3.4.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
Expand Down
1,636 changes: 12 additions & 1,624 deletions frontend/dashboard/yarn.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@generatesac/lib",
"version": "0.0.1",
"version": "0.0.12",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
20 changes: 10 additions & 10 deletions frontend/lib/src/api/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ export const baseApi = createApi({
baseQuery: fetchBaseQuery({
baseUrl: API_BASE_URL,
credentials: "include",
prepareHeaders: async (headers, { getState }) => {
const token = (getState() as { auth: { token: string } })?.auth.token;
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
return headers;
},
// prepareHeaders: async (headers, { getState }) => {
// const token = (getState() as { auth: { token: string } })?.auth.token;
// if (token) {
// headers.set("Authorization", `Bearer ${token}`);
// }
// return headers;
// },
}),
tagTypes: [
"User",
Expand All @@ -35,11 +35,11 @@ export function handleQueryParams(
baseUrl: string,
queryParams?: Record<string, string | number | boolean>,
): string {
const url = new URL(baseUrl);
let url = `${baseUrl}?`;
if (queryParams) {
Object.entries(queryParams).forEach(([key, value]) => {
url.searchParams.append(key, value.toString());
url += `${key}=${value}&`
});
}
return url.toString();
return url;
}
9 changes: 3 additions & 6 deletions frontend/lib/src/api/eventApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const eventApi = baseApi.injectEndpoints({
url: handleQueryParams(`${EVENT_API_BASE_URL}/`, queryParams),
method: "GET",
}),
providesTags: (result, _, _arg) =>
providesTags: (result) =>
result
? result.map((event) => ({ type: "Event", id: event.id }))
: ["Event"],
Expand Down Expand Up @@ -49,10 +49,7 @@ export const eventApi = baseApi.injectEndpoints({
return eventSchema.parse(response);
},
}),
updateEvent: builder.mutation<
Event,
{ id: string; body: UpdateEventRequestBody }
>({
updateEvent: builder.mutation<Event, { id: string; body: UpdateEventRequestBody }>({
query: ({ id, body }) => ({
url: `${EVENT_API_BASE_URL}/${id}`,
method: "PATCH",
Expand Down Expand Up @@ -88,7 +85,7 @@ export const eventApi = baseApi.injectEndpoints({
url: `${EVENT_API_BASE_URL}/${id}/tags`,
method: "GET",
}),
providesTags: (result, _, _arg) =>
providesTags: (result) =>
result ? result.map((tag) => ({ type: "Tag", id: tag.id })) : ["Tag"],
}),
}),
Expand Down
15 changes: 9 additions & 6 deletions frontend/lib/src/types/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { rootModelSchema } from "./root";
*/

// Enums:
const eventTypeEnum = z.enum(["open", "membersOnly"]);
const eventTypeEnum = z.enum(["hybrid", "in_person", "virtual"]);

// Schemas:
const createEventRequestBodySchema = z.object({
Expand Down Expand Up @@ -35,13 +35,16 @@ const updateEventRequestBodySchema = z.object({
const eventSchemaIntermediate = z.object({
name: z.string().max(255),
preview: z.string().max(255),
content: z.string().max(255),
start_time: z.date(),
end_time: z.date(),
description: z.string().max(255),
start_time: z.string(),
end_time: z.string(),
location: z.string().max(255),
meeting_link: z.string().max(255).optional(),
link: z.string().max(255).optional(),
event_type: eventTypeEnum,
is_recurring: z.boolean(),
is_recurring: z.boolean().optional(),
is_public: z.boolean(),
is_draft: z.boolean(),
is_archived: z.boolean(),
host: z.string().uuid(),
});

Expand Down
File renamed without changes.
6 changes: 4 additions & 2 deletions frontend/lib/src/types/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ export type uuid = string;

export const rootModelSchema = z.object({
id: z.string().uuid(),
created_at: z.date(),
updated_at: z.date(),
created_at: z.string(),
updated_at: z.string(),
});

const paginationQueryParams = z
.object({
page: z.number().int().positive().optional(),
limit: z.number().int().positive().optional(),
start: z.string().optional(),
end: z.string().optional(),
})
.optional();

Expand Down
2 changes: 1 addition & 1 deletion frontend/mobile/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "sac-mobile",
"slug": "student-activity-calendar",
"version": "1.0.0",
"version": "1.0.3",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
Expand Down
189 changes: 185 additions & 4 deletions frontend/mobile/app/(app)/(tabs)/calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,197 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { SafeAreaView } from 'react-native';
import {
CalendarProvider,
ExpandableCalendar
} from 'react-native-calendars-sac';
import { Theme } from 'react-native-calendars-sac/src/types';

import Calendar from '@/app/(design-system)/components/Calendar/Calendar';
import { eventApi } from '@generatesac/lib';

// We pull events in a date range from the backend (1 month)
// Show two weeks before today and two weeks after today
import { Box, Colors, createStyles } from '@/app/(design-system)';
import Calendar, {
DAY_EPOCH_TIME,
FetchNewEventsPropsUnion
} from '@/app/(design-system)/components/Calendar/Calendar';
import { EventSection } from '@/app/(design-system)/components/Calendar/DayTimeSection';
import { parseData } from '@/app/(design-system)/components/Calendar/parser/calendarParser';

const TODAY = new Date();

const BATCH_SIZE = 15;
const BATCH_SIZE_HALF = Math.floor(BATCH_SIZE / 2);

const CalendarPage = () => {
const [loadedEvents, setLoadedEvents] = useState<EventSection[]>([]);
const [minDate, setMinDate] = useState(
TODAY.getTime() - DAY_EPOCH_TIME * BATCH_SIZE_HALF
);
const [maxDate, setMaxDate] = useState(
TODAY.getTime() + DAY_EPOCH_TIME * BATCH_SIZE_HALF
);
const [datePress, setDatePress] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);
const oldEvents = useRef<EventSection[]>([]);
const recentRefresh = useRef<string>();
const [getEvents] = eventApi.useLazyEventsQuery({});

const emptyDatePlaceholder = new Array(BATCH_SIZE_HALF).fill({});

const calendarTheme: Theme = {
selectedDayBackgroundColor: Colors.darkRed,
todayTextColor: 'black',
arrowColor: 'black',
textMonthFontWeight: 'bold'
};

const fetchEvents = useCallback(
async (startTime: number, endTime: number) => {
const start = new Date(startTime).toISOString().split('T')[0];
const end = new Date(endTime).toISOString().split('T')[0];

return await getEvents({ start, end }).then(
({ data, error: fetchError }) => {
if (fetchError) {
setError(true);
} else {
setError(false);
}

return parseData(startTime, endTime, data);
}
);
},
[getEvents]
);

const fetchNewEvents = useCallback(
async (props: FetchNewEventsPropsUnion) => {
const { direction } = props;
const refreshId = Math.random().toString(36).substring(7);
recentRefresh.current = refreshId;

if (direction === 'start') {
// Store the old events:
oldEvents.current = [...loadedEvents];
setLoadedEvents([
...emptyDatePlaceholder,
...oldEvents.current
]);

setIsLoading(true);
setTimeout(async () => {
const newEvents = await fetchEvents(
minDate - DAY_EPOCH_TIME * BATCH_SIZE_HALF,
minDate
);
if (recentRefresh.current !== refreshId) return;
setMinDate(minDate - DAY_EPOCH_TIME * BATCH_SIZE_HALF);
setLoadedEvents([...newEvents, ...oldEvents.current]);
setIsLoading(false);
}, 200);
}
if (direction === 'end') {
// Store the old events:
oldEvents.current = [...loadedEvents];
setLoadedEvents([
...oldEvents.current,
...emptyDatePlaceholder
]);

setIsLoading(true);
setTimeout(async () => {
const newEvents = await fetchEvents(
maxDate,
maxDate + DAY_EPOCH_TIME * (BATCH_SIZE_HALF + 1)
);
if (recentRefresh.current !== refreshId) return;
setMaxDate(
maxDate + DAY_EPOCH_TIME * (BATCH_SIZE_HALF + 1)
);
setLoadedEvents([...oldEvents.current, ...newEvents]);
setIsLoading(false);
}, 200);
}
if (direction === 'base') {
const { date } = props;
const minDateNew =
new Date(date).getTime() - DAY_EPOCH_TIME * BATCH_SIZE_HALF;
const maxDateNew =
new Date(date).getTime() + DAY_EPOCH_TIME * BATCH_SIZE_HALF;
setMinDate(minDateNew);
setMaxDate(maxDateNew);

setIsLoading(true);
setTimeout(async () => {
const newEvents = await fetchEvents(minDateNew, maxDateNew);
if (recentRefresh.current !== refreshId) return;
setLoadedEvents(newEvents);
setIsLoading(false);
}, 200);
} else return;
},
[minDate, maxDate, loadedEvents, emptyDatePlaceholder, fetchEvents]
);

useEffect(() => {
fetchNewEvents({
direction: 'base',
date: TODAY.toISOString().split('T')[0]
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<SafeAreaView style={{ flex: 1 }}>
<Calendar />
<CalendarProvider
date={new Date().toISOString().split('T')[0]}
theme={calendarTheme}
disabledOpacity={0.5}
>
<Box {...styles.expandableCalendarContainer}>
<Box {...styles.expandableCalendarSubContainer}>
<ExpandableCalendar
horizontal
pagingEnabled={true}
hideArrows
theme={calendarTheme}
closeOnDayPress={false}
disableWeekScroll={true}
onDayPress={() => setDatePress(true)}
/>
</Box>
</Box>
<Calendar
loadedEvents={loadedEvents}
loadBatchSize={BATCH_SIZE}
isLoading={isLoading}
refreshThreshhold={0.25}
refetchEvents={fetchNewEvents}
datePress={datePress}
setDatePress={setDatePress}
error={error}
/>
</CalendarProvider>
</SafeAreaView>
);
};

export default CalendarPage;

const styles = createStyles({
expandableCalendarContainer: {
backgroundColor: 'white',
borderBottomStartRadius: 'lg',
borderBottomEndRadius: 'lg',
shadowColor: 'black',
shadowOffset: { width: 0, height: 5 },
shadowOpacity: 0.2
},
expandableCalendarSubContainer: {
borderBottomStartRadius: 'lg',
borderBottomEndRadius: 'lg',
backgroundColor: 'white',
overflow: 'hidden'
}
});
1 change: 1 addition & 0 deletions frontend/mobile/app/(app)/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const HomePage = () => {
variant="small"
event={item.name}
club={item.host}
clubId="1"
startTime={item.start_time}
endTime={item.end_time}
image="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSLF3ord7lnV_5Je-pC2AUgUiesHNPcZlpI7A&s"
Expand Down
Loading

0 comments on commit d30b126

Please sign in to comment.