diff --git a/frontend/mobile/.eslintrc.js b/frontend/mobile/.eslintrc.js index f333f697..b9bc4de5 100644 --- a/frontend/mobile/.eslintrc.js +++ b/frontend/mobile/.eslintrc.js @@ -5,7 +5,7 @@ module.exports = { rules: { 'prettier/prettier': 'error', 'react/react-in-jsx-scope': 'off', - 'react-native/no-inline-styles': 'off', + 'react-native/no-inline-styles': 'off' }, env: { 'jest/globals': true diff --git a/frontend/mobile/app.json b/frontend/mobile/app.json index eb1f6e1d..39f87529 100644 --- a/frontend/mobile/app.json +++ b/frontend/mobile/app.json @@ -18,7 +18,8 @@ "usesNonExemptEncryption": false }, "supportsTablet": true, - "bundleIdentifier": "com.generatesac.studentactivitycalendar" + "bundleIdentifier": "com.generatesac.studentactivitycalendar", + "backgroundColor": "#ffffff" }, "android": { "adaptiveIcon": { diff --git a/frontend/mobile/app/(app)/(tabs)/_layout.tsx b/frontend/mobile/app/(app)/(tabs)/_layout.tsx index b0447b61..feb2cfbc 100644 --- a/frontend/mobile/app/(app)/(tabs)/_layout.tsx +++ b/frontend/mobile/app/(app)/(tabs)/_layout.tsx @@ -6,10 +6,10 @@ import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; import { faCalendarDays } from '@fortawesome/free-solid-svg-icons/faCalendarDays'; import { faHouse } from '@fortawesome/free-solid-svg-icons/faHouse'; import { faUser } from '@fortawesome/free-solid-svg-icons/faUser'; -import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; import { Text } from '@/app/(design-system)'; -import { Box, Colors } from '@/app/(design-system)'; +import { Box } from '@/app/(design-system)'; +import { Icon } from '@/app/(design-system)/components/Icon/Icon'; interface TabBarLabelProps { focused: boolean; @@ -29,11 +29,7 @@ interface TabBarIconProps { const TabBarIcon: React.FC = ({ focused, icon }) => ( - + ); @@ -41,13 +37,16 @@ const Layout = () => { return ( { return ( - - Calendar - + + + ); }; diff --git a/frontend/mobile/app/(app)/(tabs)/index.tsx b/frontend/mobile/app/(app)/(tabs)/index.tsx index b5962707..bcafdeb0 100644 --- a/frontend/mobile/app/(app)/(tabs)/index.tsx +++ b/frontend/mobile/app/(app)/(tabs)/index.tsx @@ -1,20 +1,100 @@ -import { StyleSheet, View } from 'react-native'; +import { SafeAreaView, ScrollView, StyleSheet, Text } from 'react-native'; -import { Text } from '@/app/(design-system)'; -import { RecruitmentInfo } from '@/app/(design-system)/components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo'; +import { Tag } from '@generatesac/lib'; + +import { EventCard } from '@/app/(design-system)/components/EventCard/EventCard'; +import { GlobalLayout } from '@/app/(design-system)/components/GlobalLayout/GlobalLayout'; + +const tags: Tag[] = [ + { + id: '1', + created_at: new Date(), + updated_at: new Date(), + name: 'Software', + category_id: '1' + }, + { + id: '2', + created_at: new Date(), + updated_at: new Date(), + name: 'Free Food', + category_id: '2' + }, + { + id: '3', + created_at: new Date(), + updated_at: new Date(), + name: 'Panel Discussion', + category_id: '3' + }, + { + id: '4', + created_at: new Date(), + updated_at: new Date(), + name: 'Seminar', + category_id: '4' + }, + { + id: '5', + created_at: new Date(), + updated_at: new Date(), + name: 'Hackathon', + category_id: '5' + } +]; + +const date = new Date(); +date.setHours(date.getHours() + 1); const HomePage = () => { return ( - - Hello - - + + + Home + + + + + + + + ); }; diff --git a/frontend/mobile/app/(app)/(tabs)/profile.tsx b/frontend/mobile/app/(app)/(tabs)/profile.tsx index 7d6ca118..041086c0 100644 --- a/frontend/mobile/app/(app)/(tabs)/profile.tsx +++ b/frontend/mobile/app/(app)/(tabs)/profile.tsx @@ -1,10 +1,14 @@ import { StyleSheet, Text, View } from 'react-native'; +import { GlobalLayout } from '@/app/(design-system)/components/GlobalLayout/GlobalLayout'; + const ProfilePage = () => { return ( - - Profile - + + + Profile + + ); }; diff --git a/frontend/mobile/app/(app)/_layout.tsx b/frontend/mobile/app/(app)/_layout.tsx index b8aa8a74..ae953a96 100644 --- a/frontend/mobile/app/(app)/_layout.tsx +++ b/frontend/mobile/app/(app)/_layout.tsx @@ -5,7 +5,12 @@ import { Stack } from 'expo-router'; const Layout = () => { return ( - + ); }; diff --git a/frontend/mobile/app/(design-system)/components/Button/Button.tsx b/frontend/mobile/app/(design-system)/components/Button/Button.tsx index 08db025e..b331b734 100644 --- a/frontend/mobile/app/(design-system)/components/Button/Button.tsx +++ b/frontend/mobile/app/(design-system)/components/Button/Button.tsx @@ -20,6 +20,7 @@ interface BaseButtonProps { disabled?: boolean; children?: React.ReactNode; color?: SACColors; + outline?: boolean; } interface StandardButtonProps { @@ -49,15 +50,16 @@ const StandardButton: React.FC< disabled = false, color = defaultColor, size = 'full', + outline = false, ...rest }) => { + const localStyles = computeButtonStyles({ disabled, color, size, outline }); + return ( {children} @@ -77,18 +79,18 @@ const IconButton: React.FC< iconPosition = 'left', justify = 'center', size = 'full', + outline = false, ...rest }) => { const buttonIcon = ; + const localStyles = computeButtonStyles({ disabled, color, size, outline }); return ( {iconPosition === 'left' && buttonIcon} @@ -108,34 +110,56 @@ export const Button: React.FC = (props) => { return null; }; +const computeButtonStyles = (props: Partial) => { + let buttonStyles = { + ...styles.base, + ...buttonSizeStyles[props.size ?? 'full'], + backgroundColor: props.disabled ? 'gray' : props.color + }; + if (props.outline) { + buttonStyles = { ...buttonStyles, ...styles.buttonOutline }; + } + if (props.variant === 'iconButton') { + buttonStyles = { ...buttonStyles, ...styles.iconButton }; + } + + return buttonStyles; +}; + const styles = createStyles({ - standardButton: { + base: { alignItems: 'center', justifyContent: 'center', paddingHorizontal: 'l', paddingVertical: 'm', - borderRadius: 'base' + borderRadius: 'md' }, iconButton: { - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 'l', - paddingVertical: 'm', - borderRadius: 'base', flexDirection: 'row', gap: 's' + }, + buttonOutline: { + borderColor: 'darkGray', + borderWidth: 0.3 } }); const buttonSizeStyles = createStyles({ - small: { + xs: { + paddingHorizontal: 's', + paddingVertical: 'xxs' + }, + sm: { minWidth: 80, paddingVertical: 'xs', paddingHorizontal: 'm' }, - medium: { + md: { minWidth: 115 }, + lg: { + minWidth: 150 + }, full: { minWidth: '100%' } diff --git a/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaDateSection.tsx b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaDateSection.tsx new file mode 100644 index 00000000..29edae96 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaDateSection.tsx @@ -0,0 +1,53 @@ +import { forwardRef } from 'react'; + +import { formatTime } from '@/utils/time'; + +import { Box } from '../../Box/Box'; +import { Text } from '../../Text/Text'; +import { EventSection } from '../mockData'; +import AgendaTimeSection from './AgendaTimeSection'; + +type AgendaSectionProps = { + section: EventSection; +}; + +function formatSectionHeader(date: string) { + let addedZero = false; + const { date: dayOfMonth, dayOfWeek } = formatTime( + new Date(date + 'T00:00:00') + ); + if (dayOfMonth.length < 2) { + addedZero = true; + } + + return `${dayOfWeek} ${addedZero ? '0' : ''}${dayOfMonth}`; +} + +export const AgendaDateSection = forwardRef(function AgendaDateSection( + { section }: AgendaSectionProps, + ref +) { + const keys = Object.keys(section.data).sort(); + + return ( + + + {formatSectionHeader(section.date)} + + {keys.map((key, index) => { + return ( + + ); + })} + {keys.length === 0 && ( + + No Events Today + + )} + + ); +}); diff --git a/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaList.tsx b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaList.tsx new file mode 100644 index 00000000..b3b35305 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaList.tsx @@ -0,0 +1,103 @@ +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { ListRenderItem, ViewToken } from 'react-native'; +import { CalendarContext } from 'react-native-calendars-sac'; +import { UpdateSources } from 'react-native-calendars-sac/src/expandableCalendar/commons'; +import { FlatList } from 'react-native-gesture-handler'; + +import { Box } from '../../Box/Box'; +import { EventCardCalendarSkeleton } from '../../EventCard/Skeletons/EventCardCalendarSkeleton'; +import { EventSection } from '../mockData'; +import { AgendaDateSection } from './AgendaDateSection'; + +// Add back my old code: +export type AgendaListProps = { + sections: EventSection[]; + fetchData: () => void; + isLoading: boolean; + autoScroll: boolean; + setAutoScroll: React.Dispatch>; +}; + +export default function AgendaList({ + sections, + fetchData, + isLoading, + autoScroll, + setAutoScroll +}: AgendaListProps) { + const { date, setDate } = useContext(CalendarContext); + const eventList = useRef | null>(null); + const topSection = useRef(null); + const [scrolling, setScrolling] = useState(false); + + const scrollToDate = useCallback( + (dateSections: EventSection[]) => { + const index = dateSections.findIndex( + (section) => section.date === date + ); + if (index !== -1) { + setScrolling(true); + eventList.current?.scrollToIndex({ + index, + animated: true, + viewOffset: 1, + viewPosition: 0 + }); + setTimeout(() => { + setScrolling(false); + setAutoScroll(false); + }, 500); + topSection.current = dateSections[index].date; + } + }, + [date, setAutoScroll] + ); + + const onViewableItemsChanged = ({ + viewableItems + }: { + viewableItems: ViewToken[]; + }) => { + const topItem = viewableItems?.[0]?.item.date; + if (topItem && topItem !== date && !scrolling) { + topSection.current = topItem; + setDate?.(viewableItems[0].item.date, UpdateSources.LIST_DRAG); + } + }; + + useEffect(() => { + if (topSection.current === date || !autoScroll) return; + scrollToDate(sections); + }, [date, autoScroll, scrollToDate, sections]); + + const renderItem: ListRenderItem = ({ item, index }) => { + return ; + }; + + const loadingComponent = () => { + if (isLoading) { + return ( + + + + + + ); + } else { + return null; + } + }; + + return ( + + ); +} diff --git a/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaTimeSection.tsx b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaTimeSection.tsx new file mode 100644 index 00000000..45910f7d --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaTimeSection.tsx @@ -0,0 +1,96 @@ +import { Box } from '../../Box/Box'; +import { Button } from '../../Button/Button'; +import { EventCard } from '../../EventCard/EventCard'; +import { Text } from '../../Text/Text'; +import { EventSectionData } from '../mockData'; + +export type AgendaTimeSectionProps = { + time: number; + data: EventSectionData; +}; + +function convertNumToTime(num: number) { + let time = num; + let meridiem = num === 23 ? 'AM' : 'PM'; + let minutes = num % 100; + + if (num !== 0 && (num < 1000 || num > 2300)) { + throw new Error('Invalid time'); + } + + time = time / 100; + + if (num < 12) { + time += 12; + meridiem = 'AM'; + } + if (num > 12) { + time -= 12; + } + + return { + hour: Math.floor(time), + minute: minutes === 0 ? undefined : minutes.toString(), + meridiem + }; +} + +function TimeHeader({ time }: { time: number }) { + const { hour, minute, meridiem } = convertNumToTime(time); + return ( + + + {hour} + {minute ? `:${minute}` : ''} {meridiem} + + + + ); +} + +export default function AgendaTimeSection({ + time, + data +}: AgendaTimeSectionProps) { + return ( + + + + + + {data.events.map((event, index) => { + return ( + + + + ); + })} + {data.overflow && ( + + )} + + + ); +} diff --git a/frontend/mobile/app/(design-system)/components/Calendar/Calendar.tsx b/frontend/mobile/app/(design-system)/components/Calendar/Calendar.tsx new file mode 100644 index 00000000..758f68f5 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/Calendar.tsx @@ -0,0 +1,106 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { + CalendarProvider, + ExpandableCalendar +} from 'react-native-calendars-sac'; +import { Theme } from 'react-native-calendars-sac/src/types'; + +import { getMarkedDates } from '@/utils/mock'; + +import { createStyles } from '../../theme'; +import { Box } from '../Box/Box'; +import { GlobalLayout } from '../GlobalLayout/GlobalLayout'; +import AgendaList from './Agenda/AgendaList'; +import { EventSection, fetchEvents } from './mockData'; + +const FETCH_RANGE = 1000 * 60 * 60 * 24 * 13; +const NEXT_DAY = 1000 * 60 * 60 * 24; + +const calendarTheme: Theme = { + selectedDayBackgroundColor: 'black', + todayTextColor: 'black', + arrowColor: 'black', + textMonthFontWeight: 'bold' +}; + +export default function Calendar() { + const [isLoading, setIsLoading] = useState(true); + const [events, setEvents] = useState([]); + const [autoScroll, setAutoScroll] = useState(false); + const [minDate, _] = useState(new Date().getTime()); + const [maxDate, setMaxDate] = useState(new Date().getTime()); + + const fetchData = useCallback( + async (direction = 'up') => { + setIsLoading(true); + setTimeout(() => { + const time = + direction === 'up' ? minDate - FETCH_RANGE : maxDate; + fetchEvents(time, FETCH_RANGE).then((data) => { + setEvents((oldData) => [...oldData, ...data]); + setIsLoading(false); + setMaxDate((oldDate) => oldDate + FETCH_RANGE + NEXT_DAY); + }); + }, 1000); + }, + [minDate, maxDate] + ); + + const fetchNextPage = async () => { + if (isLoading) return; + fetchData('down'); + }; + + useEffect(() => { + fetchData('down'); + }, [fetchData]); + + return ( + + + + setAutoScroll(true)} + closeOnDayPress={false} + disableWeekScroll + /> + + + + + + + ); +} + +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' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/Calendar/mockData.ts b/frontend/mobile/app/(design-system)/components/Calendar/mockData.ts new file mode 100644 index 00000000..5352ac8d --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/mockData.ts @@ -0,0 +1,342 @@ +/** + * TODO: + * - Change the background color the app to white + */ +import { Tag } from '@generatesac/lib'; + +const tags: Tag[] = [ + { + id: '1', + created_at: new Date(), + updated_at: new Date(), + name: 'Software', + category_id: '1' + }, + { + id: '2', + created_at: new Date(), + updated_at: new Date(), + name: 'Free Food', + category_id: '2' + }, + { + id: '3', + created_at: new Date(), + updated_at: new Date(), + name: 'Panel Discussion', + category_id: '3' + }, + { + id: '4', + created_at: new Date(), + updated_at: new Date(), + name: 'Seminar', + category_id: '4' + }, + { + id: '5', + created_at: new Date(), + updated_at: new Date(), + name: 'Hackathon', + category_id: '5' + } +]; + +export type EventSection = { + date: string; + data: EventSectionTimes; +}; + +export type EventSectionTimes = { + [key: string]: EventSectionData; +}; + +export type EventSectionData = { + overflow: boolean; + events: EventPreview[]; +}; + +export type EventPreview = { + title: string; + startTime: string; + endTime: string; + host: string; + tags: Tag[]; +}; + +async function generateEvents(startTime: number, dateSpan: number) { + let time = startTime; + const increment = 1000 * 60 * 60 * 24; + const events: EventSection[] = []; + + while (time <= startTime + dateSpan) { + const date = new Date(time).toISOString().split('T')[0]; + events.push({ + date, + data: { + 2000: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: date, + endTime: new Date( + new Date(date).getTime() + 1000 * 60 * 60 + ).toISOString(), + host: 'Generate', + tags: tags + } + ] + }, + 2015: { + overflow: false, + events: [ + { + title: 'Gym Workout', + startTime: date, + endTime: new Date( + new Date(date).getTime() + 1000 * 60 * 60 + ).toISOString(), + host: 'Generate', + tags: tags + } + ] + } + } + }); + time += increment; + } + + return events; +} + +// function generateEvents(endTime: number) { +// let startTime = endTime; +// const events: EventSection[] = []; +// const randomNum = Math.floor(Math.random() * 10) + 1 + +// for (let i = 0; i < randomNum; i++) { +// startTime += 1000 * 60 * 60 * 24; +// const dateStart = new Date(startTime); + +// events.push({ +// date: dateStart.toISOString().split('T')[0], +// data: { +// 2000: [{ +// title: 'Gym Workout', +// startTime: dateStart.toISOString(), +// endTime: dateStart.toISOString(), +// host: 'Generate', +// tags: tags, +// }] +// } +// }) +// } + +// return { events, startTime }; +// } + +export async function fetchEvents(startTime: number, dateSpan: number) { + const events = await generateEvents(startTime, dateSpan); + return events; +} + +export const mockEvents: EventSection[] = [ + { + date: '2024-06-03', + data: {} + }, + { + date: '2024-06-05', + data: { + 2000: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2015: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2100: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + } + } + }, + { + date: '2024-06-07', + data: { + 1900: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 1800: { + overflow: false, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2100: { + overflow: false, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + } + } + }, + { + date: '2024-06-08', + data: { + 2000: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2015: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2100: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + } + } + } +]; diff --git a/frontend/mobile/app/(app)/(components)/ClubIcon.tsx b/frontend/mobile/app/(design-system)/components/ClubIcon/ClubIcon.tsx similarity index 100% rename from frontend/mobile/app/(app)/(components)/ClubIcon.tsx rename to frontend/mobile/app/(design-system)/components/ClubIcon/ClubIcon.tsx diff --git a/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx b/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx index 7418f84d..6a8293ba 100644 --- a/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx +++ b/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx @@ -21,7 +21,7 @@ export const RecruitmentItem = ({ return ( - + {firstLetterUppercase(title)} @@ -37,7 +37,7 @@ const styles = createStyles({ recruitmentItem: { alignItems: 'center', borderWidth: 1, - borderRadius: 'base', + borderRadius: 'sm', borderColor: 'gray', width: '33%' }, diff --git a/frontend/mobile/app/(design-system)/components/EventCard/EventCard.tsx b/frontend/mobile/app/(design-system)/components/EventCard/EventCard.tsx new file mode 100644 index 00000000..1c23e67f --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/EventCard.tsx @@ -0,0 +1,85 @@ +import { Tag } from '@generatesac/lib'; + +import { EventCardBig } from './Variants/EventCardBig'; +import { EventCardCalendar } from './Variants/EventCardCalendar'; +import { EventCardClub } from './Variants/EventCardClub'; +import { EventCardSmall } from './Variants/EventCardSmall'; + +interface EventCardProps { + event: string; + club: string; + startTime: Date; + endTime: Date; + tags?: Tag[]; + image: string; + logo: string; + variant: 'big' | 'small' | 'club' | 'calendar'; +} + +export const EventCard = ({ + event, + club, + startTime, + endTime, + image, + logo, + tags, + variant +}: EventCardProps) => { + switch (variant) { + case 'big': + return ( + + ); + case 'small': + return ( + + ); + case 'club': + return ( + + ); + case 'calendar': + return ( + + ); + default: + return ( + + ); + } +}; diff --git a/frontend/mobile/app/(design-system)/components/EventCard/EventCardTags/EventCardTags.tsx b/frontend/mobile/app/(design-system)/components/EventCard/EventCardTags/EventCardTags.tsx new file mode 100644 index 00000000..0d00b50d --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/EventCardTags/EventCardTags.tsx @@ -0,0 +1,41 @@ +import { Tag } from '@generatesac/lib'; + +import { Box, Tag as TagComponent, Text } from '@/app/(design-system)'; + +interface EventTagProps { + tags: Tag[]; +} + +export const EventTags = ({ tags }: EventTagProps) => { + const renderTags = () => { + let currentLength = 0; + return tags.filter((tag) => { + currentLength += tag.name.length; + return currentLength <= 25; + }); + }; + + const renderPlusTag = (remainingTagCount: number) => { + return ( + remainingTagCount > 0 && ( + + {`+${remainingTagCount}`} + + ) + ); + }; + + const renderedTags = renderTags(); + const remainingTagCount = tags.length - renderedTags.length; + + return ( + + {renderedTags.map((tag, index) => ( + + {tag.name} + + ))} + {renderPlusTag(remainingTagCount)} + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Skeletons/EventCardCalendarSkeleton.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Skeletons/EventCardCalendarSkeleton.tsx new file mode 100644 index 00000000..00bf1663 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Skeletons/EventCardCalendarSkeleton.tsx @@ -0,0 +1,102 @@ +import React from 'react'; + +import { Skeleton } from '@rneui/base'; + +import { Box, createStyles } from '@/app/(design-system)'; + +export const EventCardCalendarSkeleton = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +const styles = createStyles({ + calendarImage: { + flex: 1, + borderRadius: 'sm' + }, + isHappeningContainer: { + zIndex: 1, + position: 'absolute', + marginRight: 'xl', + top: '-15%', + right: '-5%', + flexDirection: 'row', + justifyContent: 'flex-end' + }, + isHappeningContent: { + paddingHorizontal: 's', + paddingVertical: 'xxs', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 'sm', + backgroundColor: 'darkRed', + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.3, + shadowRadius: 2 + }, + cardContainer: { + borderRadius: 'md', + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + backgroundColor: 'white' + }, + cardSubContainer: { + width: '100%', + borderRadius: 'md', + padding: 'm', + backgroundColor: 'white', + flexDirection: 'row', + justifyContent: 'space-between', + gap: 's' + }, + cardContentContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 'xxs', + flexWrap: 'wrap' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx new file mode 100644 index 00000000..e8bfac5d --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx @@ -0,0 +1,67 @@ +// EventCardBig.tsx +import React from 'react'; +import { TouchableOpacity } from 'react-native-gesture-handler'; + +import { Image } from '@rneui/base'; + +import { Box, Text } from '@/app/(design-system)'; +import { calculateDuration, createOptions, eventTime } from '@/utils/time'; + +interface EventCardBigProps { + event: string; + club: string; + startTime: Date; + endTime: Date; + image: string; +} + +export const EventCardBig: React.FC = ({ + event, + club, + startTime, + endTime, + image +}) => { + return ( + + + + + + {event} + + {calculateDuration(startTime, endTime)} + + {club} + + + {eventTime( + startTime, + endTime, + createOptions('dayOfWeek', 'monthAndDate') + )} + + + {eventTime( + startTime, + endTime, + createOptions('start') + )} + + + + + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx new file mode 100644 index 00000000..c6892545 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; + +import { Tag } from '@generatesac/lib'; +import { Avatar, Image } from '@rneui/base'; + +import { Box, Text, createStyles } from '@/app/(design-system)'; +import { createOptions, eventTime, happeningNow } from '@/utils/time'; + +import { EventTags } from '../EventCardTags/EventCardTags'; +import { sharedStyles } from '../shared/styles'; + +interface EventCardCalendarProps { + event: string; + club: string; + startTime: Date; + endTime: Date; + image: string; + logo: string; + tags?: Tag[]; +} + +export const EventCardCalendar: React.FC = ({ + event, + club, + startTime, + endTime, + image, + logo, + tags +}) => { + const isHappening = happeningNow(startTime, endTime); + + return ( + + + {isHappening && ( + + + + NOW + + + + )} + + + + + {event} + + + {`${eventTime( + startTime, + endTime, + createOptions('start', 'end') + )} •`} + + + + + {club} + + + + {tags && } + + + + + + ); +}; + +const styles = createStyles({ + calendarImage: { + flex: 1, + borderRadius: 'sm' + }, + isHappeningContainer: { + zIndex: 1, + position: 'absolute', + marginRight: 'xl', + top: '-15%', + right: '-5%', + flexDirection: 'row', + justifyContent: 'flex-end' + }, + isHappeningContent: { + paddingHorizontal: 's', + paddingVertical: 'xxs', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 'sm', + backgroundColor: 'darkRed', + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.3, + shadowRadius: 2 + }, + cardContainer: { + borderRadius: 'md', + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.2, + shadowRadius: 8, + backgroundColor: 'white' + }, + cardSubContainer: { + width: '100%', + borderRadius: 'md', + padding: 'm', + backgroundColor: 'white', + flexDirection: 'row', + justifyContent: 'space-between', + gap: 's' + }, + cardContentContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 'xxs', + flexWrap: 'wrap' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardClub.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardClub.tsx new file mode 100644 index 00000000..15bee8d9 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardClub.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; + +import { Tag } from '@generatesac/lib'; +import { Avatar, Image } from '@rneui/base'; + +import { Box, Text, createStyles } from '@/app/(design-system)'; +import { createOptions, eventTime } from '@/utils/time'; + +import { EventTags } from '../EventCardTags/EventCardTags'; +import { sharedStyles } from '../shared/styles'; + +interface EventCardClubProps { + event: string; + club: string; + startTime: Date; + endTime: Date; + image: string; + logo: string; + tags?: Tag[]; +} + +export const EventCardClub: React.FC = ({ + event, + club, + startTime, + endTime, + image, + logo, + tags +}) => { + return ( + + + + + + + + {eventTime( + startTime, + endTime, + createOptions( + 'monthAndDate', + 'year', + 'start', + 'end' + ) + )} + + {event} + + + Hosted by {club} + + + + {tags && } + + + + + + ); +}; + +const styles = createStyles({ + cardContainer: { + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + backgroundColor: 'white', + borderRadius: 'md' + }, + cardSubContainer: { + width: '100%', + borderRadius: 'md', + padding: 'm', + backgroundColor: 'white', + flexDirection: 'row', + justifyContent: 'space-between', + gap: 's' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx new file mode 100644 index 00000000..3b0eabc2 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Dimensions } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; + +import { Image } from '@rneui/base'; + +import { Box, Text } from '@/app/(design-system)'; +import { createOptions, eventTime } from '@/utils/time'; + +interface EventCardSmallProps { + event: string; + club: string; + startTime: Date; + endTime: Date; + image: string; +} + +export const EventCardSmall: React.FC = ({ + event, + club, + startTime, + endTime, + image +}) => { + const screenWidth = Dimensions.get('window').width; + const boxWidth = screenWidth * 0.4; + + return ( + + + + + {event} + + {eventTime( + startTime, + endTime, + createOptions('dayOfWeek', 'monthAndDate') + )} + + + {club} + + + + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/EventCard/index.ts b/frontend/mobile/app/(design-system)/components/EventCard/index.ts new file mode 100644 index 00000000..9688efc6 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/index.ts @@ -0,0 +1 @@ +export { EventCard } from './EventCard'; diff --git a/frontend/mobile/app/(design-system)/components/EventCard/shared/styles.ts b/frontend/mobile/app/(design-system)/components/EventCard/shared/styles.ts new file mode 100644 index 00000000..4d61ece0 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/shared/styles.ts @@ -0,0 +1,9 @@ +import { createStyles } from '@/app/(design-system)/theme'; + +export const sharedStyles = createStyles({ + clubInfoContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 'xxs' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/GlobalLayout/GlobalLayout.tsx b/frontend/mobile/app/(design-system)/components/GlobalLayout/GlobalLayout.tsx new file mode 100644 index 00000000..62af6fe1 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/GlobalLayout/GlobalLayout.tsx @@ -0,0 +1,17 @@ +import { View } from 'react-native'; + +import { createBox } from '@shopify/restyle'; + +type GlobalLayoutProps = { + children: React.ReactNode; +}; + +const ViewBase = createBox(View); + +export const GlobalLayout = ({ children }: GlobalLayoutProps) => { + return ( + + {children} + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/Icon/Icon.tsx b/frontend/mobile/app/(design-system)/components/Icon/Icon.tsx index 49adb8d4..05dcff14 100644 --- a/frontend/mobile/app/(design-system)/components/Icon/Icon.tsx +++ b/frontend/mobile/app/(design-system)/components/Icon/Icon.tsx @@ -13,7 +13,7 @@ type IconProps = { export const Icon: React.FC = ({ icon, - size = 'medium', + size = 'md', color = defaultColor }) => { return ( @@ -26,12 +26,18 @@ export const Icon: React.FC = ({ }; const styles = createStyles({ - small: { + xs: { + fontSize: 12 + }, + sm: { fontSize: 16 }, - medium: { + md: { fontSize: 24 }, + lg: { + fontSize: 32 + }, full: { fontSize: 24 } diff --git a/frontend/mobile/app/(design-system)/components/Tag/Tag.tsx b/frontend/mobile/app/(design-system)/components/Tag/Tag.tsx index 38fcb11f..3bb4d61d 100644 --- a/frontend/mobile/app/(design-system)/components/Tag/Tag.tsx +++ b/frontend/mobile/app/(design-system)/components/Tag/Tag.tsx @@ -9,29 +9,30 @@ type TagProps = { children: React.ReactNode; onPress?: () => void; color?: SACColors; - state?: 'selected' | 'unselected' | 'remove'; + variant?: 'selected' | 'unselected' | 'remove' | 'eventCard'; }; export const Tag: React.FC = ({ children, color = defaultColor, - state = 'selected', + variant = 'selected', onPress }) => { - if (state === 'selected' || state === 'unselected') { + if (variant === 'selected' || variant === 'unselected') { return ( ); } - if (state === 'remove') { + if (variant === 'remove') { return ( ); } + if (variant === 'eventCard') { + return ( + + ); + } return null; }; diff --git a/frontend/mobile/app/(design-system)/components/Text/TextVariants.ts b/frontend/mobile/app/(design-system)/components/Text/TextVariants.ts index 2e8fcfde..073fa88e 100644 --- a/frontend/mobile/app/(design-system)/components/Text/TextVariants.ts +++ b/frontend/mobile/app/(design-system)/components/Text/TextVariants.ts @@ -12,6 +12,13 @@ const Texts = { fontStyle: 'normal', fontWeight: '500' }, + 'subheader-2': { + fontFamily: 'DMSans-Medium', + fontSize: 20, + fontStyle: 'normal', + fontWeight: '500', + lineHeight: 'normal' + }, 'body-1': { fontFamily: 'DMSans-Regular', fontSize: 16, @@ -29,6 +36,12 @@ const Texts = { fontSize: 10, fontStyle: 'normal', fontWeight: '200' + }, + 'caption-3': { + fontFamily: 'DMSans-Regular', + fontSize: 12, + fontStyle: 'normal', + fontWeight: '400' } } as const; diff --git a/frontend/mobile/app/(design-system)/shared/border.ts b/frontend/mobile/app/(design-system)/shared/border.ts index 03b6f6b8..801c9b07 100644 --- a/frontend/mobile/app/(design-system)/shared/border.ts +++ b/frontend/mobile/app/(design-system)/shared/border.ts @@ -1,4 +1,9 @@ -export const Border = { - base: 8, +import { ComponentSizes } from './types'; + +export const Border: Record = { + xs: 4, + sm: 8, + md: 12, + lg: 24, full: 999 }; diff --git a/frontend/mobile/app/(design-system)/shared/colors.ts b/frontend/mobile/app/(design-system)/shared/colors.ts index a3d041db..11d0f321 100644 --- a/frontend/mobile/app/(design-system)/shared/colors.ts +++ b/frontend/mobile/app/(design-system)/shared/colors.ts @@ -11,52 +11,15 @@ export const Colors = { yellow: '#FFB626', white: '#FFFFFF', black: '#000000', - gray: '#C3C9D0' + gray: '#C3C9D0', + darkGray: '#7A7A7A', + transparent: '' }; export type SACColors = keyof typeof Colors; export const defaultColor: keyof typeof Colors = 'black'; -export const BackgroundColorVariants = { - darkBlue: { - backgroundColor: 'darkBlue' - }, - darkRed: { - backgroundColor: 'darkRed' - }, - green: { - backgroundColor: 'green' - }, - blue: { - backgroundColor: 'blue' - }, - aqua: { - backgroundColor: 'aqua' - }, - purple: { - backgroundColor: 'purple' - }, - red: { - backgroundColor: 'red' - }, - orange: { - backgroundColor: 'orange' - }, - yellow: { - backgroundColor: 'yellow' - }, - white: { - backgroundColor: 'white' - }, - black: { - backgroundColor: 'black' - }, - disabled: { - backgroundColor: 'disabled' - } -} as const; - export const textColorVariants = { darkBlue: 'white', darkRed: 'white', @@ -69,5 +32,7 @@ export const textColorVariants = { yellow: 'white', white: 'black', black: 'white', - gray: 'white' + gray: 'white', + darkGray: 'white', + transparent: 'black' } as const; diff --git a/frontend/mobile/app/(design-system)/shared/types.ts b/frontend/mobile/app/(design-system)/shared/types.ts index f665b58f..316aef09 100644 --- a/frontend/mobile/app/(design-system)/shared/types.ts +++ b/frontend/mobile/app/(design-system)/shared/types.ts @@ -1,3 +1,3 @@ -type ComponentSizes = 'small' | 'medium' | 'full'; +type ComponentSizes = 'xs' | 'sm' | 'md' | 'lg' | 'full'; export { ComponentSizes }; diff --git a/frontend/mobile/package.json b/frontend/mobile/package.json index 81bedca8..1de3a511 100644 --- a/frontend/mobile/package.json +++ b/frontend/mobile/package.json @@ -29,6 +29,7 @@ "@rneui/base": "^4.0.0-rc.8", "@rneui/themed": "^4.0.0-rc.8", "@shopify/restyle": "^2.4.4", + "@stream-io/flat-list-mvcp": "^0.10.3", "@svgr/core": "^8.1.0", "expo": "~51.0.2", "expo-dev-client": "~4.0.14", @@ -45,6 +46,8 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.74.1", + "react-native-bidirectional-infinite-scroll": "^0.3.3", + "react-native-calendars-sac": "^1.22.25", "react-native-gesture-handler": "^2.16.2", "react-native-reanimated": "^3.11.0", "react-native-safe-area-context": "4.10.1", diff --git a/frontend/mobile/types/time.ts b/frontend/mobile/types/time.ts new file mode 100644 index 00000000..48fec2e8 --- /dev/null +++ b/frontend/mobile/types/time.ts @@ -0,0 +1,10 @@ +export type FormattedTime = { + dayOfWeek: string; + date: string; + month: string; + year: string; + formattedTime: string; + ampm: string; + hour: string; + minute: string; +}; diff --git a/frontend/mobile/utils/mock.ts b/frontend/mobile/utils/mock.ts new file mode 100644 index 00000000..508535f7 --- /dev/null +++ b/frontend/mobile/utils/mock.ts @@ -0,0 +1,361 @@ +export type MarkedDates = { + [key: string]: { marked?: boolean }; +}; + +export type EventInformation = { + hour: string; + duration: string; + title: string; + location: string; +}; + +export const dates = [ + '2024-04-01', + '2024-04-02', + '2024-04-03', + '2024-04-04', + '2024-04-05', + '2024-04-06', + '2024-04-07', + '2024-04-08', + '2024-04-09', + '2024-04-10', + '2024-04-11', + '2024-04-12', + '2024-04-13', + '2024-04-14', + '2024-04-15', + '2024-04-16', + '2024-04-17', + '2024-04-18', + '2024-04-19', + '2024-04-20', + '2024-04-21', + '2024-04-22', + '2024-04-23', + '2024-04-24', + '2024-04-25', + '2024-04-26', + '2024-04-27', + '2024-04-28', + '2024-04-29', + '2024-04-30' +]; + +export const agendaItems = [ + { + title: dates[0], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Club GM', + location: 'West Village G 102' + } + ] + }, + { + title: dates[1], + data: [ + { + hour: '4pm', + duration: '1h', + title: 'Intramural game', + location: 'Marino Center' + }, + { + hour: '5pm', + duration: '1h', + title: 'Generate Team Meeting', + location: 'Sherman Center' + }, + { + hour: '5pm', + duration: '1h', + title: 'Other commitment', + location: 'Online' + } + ] + }, + { + title: dates[2], + data: [ + { + hour: '1pm', + duration: '1h', + title: 'Dance Crew Rehearsal', + location: 'Curry Ballroom' + }, + { + hour: '2pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '3pm', + duration: '1h', + title: 'Club GM', + location: 'West Village G 102' + } + ] + }, + { + title: dates[3], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Ashtanga Yoga', + location: 'Marino Center' + } + ] + }, + { + title: dates[5], + data: [ + { + hour: '11pm', + duration: '1h', + title: 'Generate Team Meeting', + location: 'Big Sherm' + }, + { + hour: '12pm', + duration: '1h', + title: 'Running Group', + location: 'Marino Center' + } + ] + }, + { + title: dates[6], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + } + ] + }, + { + title: dates[7], + data: [] + }, + { + title: dates[8], + data: [ + { + hour: '9pm', + duration: '1h', + title: 'Cheese Club GM', + location: 'Richards 325' + }, + { + hour: '10pm', + duration: '1h', + title: 'Club GM', + location: 'Ryder Hall 289' + }, + { + hour: '11pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '12pm', + duration: '1h', + title: 'Running Group', + location: 'Marino Center' + } + ] + }, + { + title: dates[9], + data: [ + { + hour: '1pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '2pm', + duration: '1h', + title: 'Club GM', + location: 'West Village G 102' + }, + { + hour: '3pm', + duration: '1h', + title: 'Marino Yoga', + location: 'Marino Center' + } + ] + }, + { + title: dates[10], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[11], + data: [ + { + hour: '1pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '2pm', + duration: '1h', + title: 'Club GM', + location: 'Ryder Hall 289' + }, + { + hour: '3pm', + duration: '1h', + title: 'Marino Yoga', + location: 'Marino Center' + } + ] + }, + { + title: dates[12], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[15], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[16], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[18], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + }, + { + hour: '1pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '2pm', + duration: '1h', + title: 'Club GM', + location: 'West Village G 102' + }, + { + hour: '3pm', + duration: '1h', + title: 'Marino Yoga', + location: 'Marino Center' + } + ] + }, + { + title: dates[19], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[20], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[22], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[24], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[25], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + } +]; + +export function getMarkedDates() { + const marked: MarkedDates = {}; + + agendaItems.forEach((item) => { + // NOTE: only mark dates with data + if (item.data.length) { + marked[item.title] = { marked: true }; + } + }); + return marked; +} diff --git a/frontend/mobile/utils/time.ts b/frontend/mobile/utils/time.ts new file mode 100644 index 00000000..acd7b700 --- /dev/null +++ b/frontend/mobile/utils/time.ts @@ -0,0 +1,115 @@ +import { FormattedTime } from '@/types/time'; + +import { Options } from './timeOptions'; + +// calculate duration of event +export const calculateDuration = (startTime: Date, endTime: Date) => { + const difference = endTime.getTime() - startTime.getTime(); + const hours = Math.floor(difference / (1000 * 60 * 60)); + const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); + return `${hours}h ${minutes}m`; +}; + +// format time from Date type +export const formatTime = (time: Date): FormattedTime => { + const daysOfWeek = [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday' + ]; + const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + const dayOfWeek = daysOfWeek[time.getDay()]; + const date = time.getDate(); + const month = months[time.getMonth()]; + const year = time.getFullYear(); + + let hours = time.getHours(); + const minutes = time.getMinutes().toString().padStart(2, '0'); + const ampm = hours >= 12 ? 'PM' : 'AM'; + + const formattedHours = hours % 12 || 12; + const formattedTime = + minutes === '00' ? `${formattedHours}` : `${formattedHours}:${minutes}`; + + return { + dayOfWeek, + date: date.toString(), + month, + year: year.toString(), + formattedTime, + ampm, + hour: formattedHours.toString(), + minute: minutes + }; +}; + +// return true if an event is happening now +export const happeningNow = (start: Date, end: Date) => { + const currentTime = new Date(); + return currentTime >= start && currentTime <= end; +}; + +// select what to display on event time +export const createOptions = ( + ...keys: T[] +): Partial> => { + return keys.reduce( + (acc, key) => { + acc[key] = true; + return acc; + }, + {} as Partial> + ); +}; + +// select options to display on event time: day of the week, month and date, year, startTime, endTime +// e.g. eventTime(startTime, endTime, createOptions('dayOfWeek', 'monthAndDate')) +export const eventTime = ( + startTime: Date, + endTime: Date, + options: Partial> = {} +) => { + const { dayOfWeek, monthAndDate, year, start, end } = options; + + const startTimeFormat = formatTime(startTime); + const endTimeFormat = formatTime(endTime); + + const dayOfWeekString = dayOfWeek ? startTimeFormat.dayOfWeek : ''; + const monthAndDateString = monthAndDate + ? `${startTimeFormat.month} ${startTimeFormat.date}` + : ''; + const yearString = year ? startTimeFormat.year : ''; + + let timeString = ''; + if (start && end) { + timeString = `${startTimeFormat.formattedTime}-${endTimeFormat.formattedTime} ${endTimeFormat.ampm}`; + } else if (start) { + timeString = `${startTimeFormat.formattedTime} ${startTimeFormat.ampm}`; + } + + const parts = [ + dayOfWeekString + (dayOfWeek ? ',' : ''), + monthAndDateString + (timeString && monthAndDate ? ',' : ''), + yearString + (yearString && timeString ? ' •' : ''), + timeString + ].filter(Boolean); + + return parts.join(' '); +}; diff --git a/frontend/mobile/utils/timeOptions.ts b/frontend/mobile/utils/timeOptions.ts new file mode 100644 index 00000000..5c7f5aa2 --- /dev/null +++ b/frontend/mobile/utils/timeOptions.ts @@ -0,0 +1,7 @@ +export type Options = { + dayOfWeek?: true; + monthAndDate?: true; + year?: true; + start?: true; + end?: true; +}; diff --git a/frontend/mobile/yarn.lock b/frontend/mobile/yarn.lock index 291f197e..b4b7f51f 100644 --- a/frontend/mobile/yarn.lock +++ b/frontend/mobile/yarn.lock @@ -996,16 +996,23 @@ mv "~2" safe-json-stringify "~1" +<<<<<<< HEAD +"@expo/cli@0.18.14": + version "0.18.14" + resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.18.14.tgz#bc53230855c97af54f9a9f49c6478c392c4dcee4" + integrity sha512-ke2IVs3MfIMe2NjXY11Ky9+r0+eav6NUa+cy61wpZNPe5QkwIJ56SdJ4wIMMKFPFhgLsy1vfaqhOcZo96U4wCg== +======= "@expo/cli@0.18.13": version "0.18.13" resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.18.13.tgz#b3a6aa1d4cfa78720ba86f73ded7c2c93f4805a9" integrity sha512-ZO1fpDK8z6mLeQGuFP6e3cZyCHV55ohZY7/tEyhpft3bwysS680eyFg5SFe+tWNFesnziFrbtI8JaUyhyjqovA== +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 dependencies: "@babel/runtime" "^7.20.0" "@expo/code-signing-certificates" "0.0.5" - "@expo/config" "~9.0.0" - "@expo/config-plugins" "~8.0.0" - "@expo/devcert" "^1.1.2" + "@expo/config" "~9.0.0-beta.0" + "@expo/config-plugins" "~8.0.0-beta.0" + "@expo/devcert" "^1.0.0" "@expo/env" "~0.3.0" "@expo/image-utils" "^0.5.0" "@expo/json-file" "^8.3.0" @@ -1013,7 +1020,7 @@ "@expo/osascript" "^2.0.31" "@expo/package-manager" "^1.5.0" "@expo/plist" "^0.1.0" - "@expo/prebuild-config" "7.0.4" + "@expo/prebuild-config" "7.0.3" "@expo/rudder-sdk-node" "1.1.1" "@expo/spawn-async" "^1.7.2" "@expo/xcpretty" "^4.3.0" @@ -1112,7 +1119,28 @@ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-51.0.0.tgz#f5df238cd1237d7e4d9cc8217cdef3383c2a00cf" integrity sha512-acn03/u8mQvBhdTQtA7CNhevMltUhbSrpI01FYBJwpVntufkU++ncQujWKlgY/OwIajcfygk1AY4xcNZ5ImkRA== +<<<<<<< HEAD +"@expo/config@9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@expo/config/-/config-9.0.1.tgz#e7b79de5af29d5ab2a98a62c3cda31f03bd75827" + integrity sha512-0tjaXBstTbXmD4z+UMFBkh2SZFwilizSQhW6DlaTMnPG5ezuw93zSFEWAuEC3YzkpVtNQTmYzxAYjxwh6seOGg== + dependencies: + "@babel/code-frame" "~7.10.4" + "@expo/config-plugins" "~8.0.0-beta.0" + "@expo/config-types" "^51.0.0-unreleased" + "@expo/json-file" "^8.3.0" + getenv "^1.0.0" + glob "7.1.6" + require-from-string "^2.0.2" + resolve-from "^5.0.0" + semver "^7.6.0" + slugify "^1.3.4" + sucrase "3.34.0" + +"@expo/config@~9.0.0", "@expo/config@~9.0.0-beta.0": +======= "@expo/config@9.0.2", "@expo/config@~9.0.0", "@expo/config@~9.0.0-beta.0": +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 version "9.0.2" resolved "https://registry.yarnpkg.com/@expo/config/-/config-9.0.2.tgz#112b93436dbca8aa3da73a46329e5b58fdd435d2" integrity sha512-BKQ4/qBf3OLT8hHp5kjObk2vxwoRQ1yYQBbG/OM9Jdz32yYtrU8opTbKRAxfZEWH5i3ZHdLrPdC1rO0I6WxtTw== @@ -1129,7 +1157,11 @@ slugify "^1.3.4" sucrase "3.34.0" +<<<<<<< HEAD +"@expo/devcert@^1.0.0": +======= "@expo/devcert@^1.1.2": +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 version "1.1.2" resolved "https://registry.yarnpkg.com/@expo/devcert/-/devcert-1.1.2.tgz#a4923b8ea5b34fde31d6e006a40d0f594096a0ed" integrity sha512-FyWghLu7rUaZEZSTLt/XNRukm0c9GFfwP0iFaswoDWpV6alvVg+zRAfCLdIVQEz1SVcQ3zo1hMZFDrnKGvkCuQ== @@ -1184,7 +1216,35 @@ json5 "^2.2.2" write-file-atomic "^2.3.0" +<<<<<<< HEAD +"@expo/metro-config@0.18.3": + version "0.18.3" + resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.18.3.tgz#fa198b9bf806df44fd7684f1df9af2535c107aa8" + integrity sha512-E4iW+VT/xHPPv+t68dViOsW7egtGIr+sRElcym0iGpC4goLz9WBux/xGzWgxvgvvHEWa21uSZQPM0jWla0OZXg== + dependencies: + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.5" + "@babel/parser" "^7.20.0" + "@babel/types" "^7.20.0" + "@expo/config" "~9.0.0-beta.0" + "@expo/env" "~0.3.0" + "@expo/json-file" "~8.3.0" + "@expo/spawn-async" "^1.7.2" + chalk "^4.1.0" + debug "^4.3.2" + find-yarn-workspace-root "~2.0.0" + fs-extra "^9.1.0" + getenv "^1.0.0" + glob "^7.2.3" + jsc-safe-url "^0.2.4" + lightningcss "~1.19.0" + postcss "~8.4.32" + resolve-from "^5.0.0" + +"@expo/metro-config@~0.18.0": +======= "@expo/metro-config@0.18.4", "@expo/metro-config@~0.18.0": +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 version "0.18.4" resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.18.4.tgz#bc298e21637a3007f3c31c238525d3bef17e823b" integrity sha512-vh9WDf/SzE+NYCn6gqbzLKiXtENFlFZdAqyj9nI38RvQ4jw6TJIQ8+ExcdLDT3MOG36Ytg44XX9Zb3OWF6LVxw== @@ -1265,23 +1325,6 @@ semver "^7.6.0" xml2js "0.6.0" -"@expo/prebuild-config@7.0.4": - version "7.0.4" - resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-7.0.4.tgz#cf2d001792d69e652ad4cec9830c8bd4905f0e7a" - integrity sha512-E2n3QbwgV8Qa0CBw7BHrWBDWD7l8yw+N/yjvXpSPFFtoZLMSKyegdkJFACh2u+UIRKUSZm8zQwHeZR0rqAxV9g== - dependencies: - "@expo/config" "~9.0.0" - "@expo/config-plugins" "~8.0.0" - "@expo/config-types" "^51.0.0-unreleased" - "@expo/image-utils" "^0.5.0" - "@expo/json-file" "^8.3.0" - "@react-native/normalize-colors" "~0.74.83" - debug "^4.3.1" - fs-extra "^9.0.0" - resolve-from "^5.0.0" - semver "^7.6.0" - xml2js "0.6.0" - "@expo/rudder-sdk-node@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz#6aa575f346833eb6290282118766d4919c808c6a" @@ -1753,9 +1796,15 @@ "@jridgewell/sourcemap-codec" "^1.4.14" "@mantine/core@^7.7.1": +<<<<<<< HEAD + version "7.10.1" + resolved "https://registry.yarnpkg.com/@mantine/core/-/core-7.10.1.tgz#c90a2aed1e47ece38f745db11c1cff56fd5719b9" + integrity sha512-l9ypojKN3PjwO1CSLIsqxi7mA25+7w+xc71Q+JuCCREI0tuGwkZsKbIOpuTATIJOjPh8ycLiW7QxX1LYsRTq6w== +======= version "7.10.0" resolved "https://registry.yarnpkg.com/@mantine/core/-/core-7.10.0.tgz#bfaafc92cf2346e5a6cbb49289f577ce3f7c05f7" integrity sha512-hNqhdn/+4x8+FDWzR5fu1eMgnG1Mw4fZHw4WjIYjKrSv0NeKHY263RiesZz8RwcUQ8r7LlD95/2tUOMnKVTV5Q== +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 dependencies: "@floating-ui/react" "^0.26.9" clsx "^2.1.1" @@ -1765,9 +1814,15 @@ type-fest "^4.12.0" "@mantine/hooks@^7.7.1": +<<<<<<< HEAD + version "7.10.1" + resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.10.1.tgz#68d12570f0dad127555904973ec78ae40065ae31" + integrity sha512-0EH9WBWUdtQLGU3Ak+csQ77EtUxI6pPNfwZdRJQWcaA3f8SFOLo9h9CGxiikFExerhvuCeUlaTf3s+TB9Op/rw== +======= version "7.10.0" resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.10.0.tgz#10a259e204a8af29df6aeeb24090c1e2c6debca0" integrity sha512-fnalwYS2WQEFS4wmhmAetDZ/VdJPLNeUXPX9t+S21o3p/dRTX1xhU2mS7yWaQUKM0hPD1TcujqXGlP2M2g/A9A== +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 "@mantine/utils@^6.0.21": version "6.0.21" @@ -2385,6 +2440,11 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@stream-io/flat-list-mvcp@^0.10.3": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@stream-io/flat-list-mvcp/-/flat-list-mvcp-0.10.3.tgz#774e56aba7f3da5a8f2ae5ad3065f05e5a27e7f2" + integrity sha512-2ZK8piYlEfKIPZrH8BpZz9uj8HZcUvMCV0X7qSLSAc/vhLOANBfR0SSn0OaWPbqb2mFGAd4FxmLSPp1zKEYuaw== + "@svgr/babel-plugin-add-jsx-attribute@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" @@ -2771,9 +2831,15 @@ "@types/node" "*" "@types/node@*": +<<<<<<< HEAD + version "20.12.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.13.tgz#90ed3b8a4e52dd3c5dc5a42dde5b85b74ad8ed88" + integrity sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA== +======= version "20.12.12" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.12.tgz#7cbecdf902085cec634fdb362172dfe12b8f2050" integrity sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw== +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 dependencies: undici-types "~5.26.4" @@ -3667,9 +3733,15 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001587: +<<<<<<< HEAD + version "1.0.30001625" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz#ead1b155ea691d6a87938754d3cb119c24465b03" + integrity sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w== +======= version "1.0.30001624" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001624.tgz#0ec4c8fa7a46e5b785477c70b38a56d0b10058eb" integrity sha512-0dWnQG87UevOCPYaOR49CBcLBwoZLpws+k6W37nLjWUhumP1Isusj0p2u+3KhjNloRWK9OKMgjBBzPujQHw4nA== +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 ccount@^2.0.0: version "2.0.1" @@ -4469,9 +4541,15 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.4.668: +<<<<<<< HEAD + version "1.4.787" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.787.tgz#3eedd0a3b8be2b9e96e21675d399786ad90b99ed" + integrity sha512-d0EFmtLPjctczO3LogReyM2pbBiiZbnsKnGF+cdZhsYzHm/A0GV7W94kqzLD8SN4O3f3iHlgLUChqghgyznvCQ== +======= version "1.4.783" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz#933887165b8b6025a81663d2d97cf4b85cde27b2" integrity sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ== +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 emittery@^0.13.1: version "0.13.1" @@ -4966,27 +5044,28 @@ expo-asset@~10.0.6: md5-file "^3.2.3" expo-constants@~16.0.0: - version "16.0.1" - resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-16.0.1.tgz#1285e29c85513c6e88e118289e2baab72596d3f7" - integrity sha512-s6aTHtglp926EsugWtxN7KnpSsE9FCEjb7CgEjQQ78Gpu4btj4wB+IXot2tlqNwqv+x7xFe5veoPGfJDGF/kVg== + version "16.0.2" + resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-16.0.2.tgz#eb5a1bddb7308fd8cadac8fc44decaf4784cac5e" + integrity sha512-9tNY3OVO0jfiMzl7ngb6IOyR5VFzNoN5OOazUWoeGfmMqVB5kltTemRvKraK9JRbBKIw+SOYLEmF0sEqgFZ6OQ== dependencies: - "@expo/config" "~9.0.0-beta.0" + "@expo/config" "~9.0.0" + "@expo/env" "~0.3.0" expo-dev-client@~4.0.14: - version "4.0.14" - resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-4.0.14.tgz#73d2f8b6f173d01f07af3e01cf8d5acdc6e05c01" - integrity sha512-s5/FZZdgvoxBGA35QgNet61Dc1jh+8u375uaYkH9pUvfKFXURd9PDDAWvtAnOo+QYg9WwgiHPo7dKeCdN6pOPA== + version "4.0.15" + resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-4.0.15.tgz#abb8be48f750490caafa4c5c0d22605c630fedf2" + integrity sha512-Ffwz66DW3xdldlSUwPPXJCWoL4teA8uV374sEJpKwyBhJrFuL+KpMWMe4/Dz/F1oHzjflD8GHBu9xqoqNdiJzw== dependencies: - expo-dev-launcher "4.0.15" + expo-dev-launcher "4.0.16" expo-dev-menu "5.0.14" expo-dev-menu-interface "1.8.3" expo-manifests "~0.14.0" expo-updates-interface "~0.16.2" -expo-dev-launcher@4.0.15: - version "4.0.15" - resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-4.0.15.tgz#cd36f10b7e534e5caa176a5718381ccfa73b0b8c" - integrity sha512-avl4NTwFwalZjojFAXvINPgxAlcAxfdwy9PSsAq5KAkl9Vv+Vr8O2gI3nfrPwtqAA0iOIES/EKN0YFCiQuuvvg== +expo-dev-launcher@4.0.16: + version "4.0.16" + resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-4.0.16.tgz#2220f59bf3bb3741636ac20a80ab8c754e2d29cb" + integrity sha512-mNt71awnJDL+GkvpBp9CzRR3q2Wm0GPo68noRvd389qDFBMA8QA8uyY8JVqekpr7RUwn1eg3cmfox5oSwL5rmA== dependencies: ajv "8.11.0" expo-dev-menu "5.0.14" @@ -5012,10 +5091,10 @@ expo-file-system@~17.0.1: resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-17.0.1.tgz#b9f8af8c1c06ec71d96fd7a0d2567fa9e1c88f15" integrity sha512-dYpnZJqTGj6HCYJyXAgpFkQWsiCH3HY1ek2cFZVHFoEc5tLz9gmdEgTF6nFHurvmvfmXqxi7a5CXyVm0aFYJBw== -expo-font@~12.0.5: - version "12.0.5" - resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-12.0.5.tgz#3451c2bd3f98859b127a6484d3474a292889b93f" - integrity sha512-h/VkN4jlHYDJ6T6pPgOYTVoDEfBY0CTKQe4pxnPDGQiE6H+DFdDgk+qWVABGpRMH0+zXoHB+AEi3OoQjXIynFA== +expo-font@~12.0.5, expo-font@~12.0.6: + version "12.0.6" + resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-12.0.6.tgz#efd4aa226f8cd3ca04fc1af5699b193b9d62e304" + integrity sha512-eognUxmZi2urCdERA5KuZpXUJO9JomOG/5ZKw9fGUhDi86SQ/6UWw+nMGbtshjWdJ0Vt0zHAdaIYx8aHq2iRzA== dependencies: fontfaceobserver "^2.1.0" @@ -5056,10 +5135,17 @@ expo-modules-autolinking@1.11.1: find-up "^5.0.0" fs-extra "^9.1.0" +<<<<<<< HEAD +expo-modules-core@1.12.12: + version "1.12.12" + resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.12.12.tgz#c4c7d70b7a80dcbb60998ca98624f65743d018ac" + integrity sha512-ls2Ido4Aduo4f9/LPQx66Hp3X2qddVOY5S7kP3/lp/G4ieqfPcUMueGYSeaz76fg/TYXRh2XLv/HGvu8zV6bbQ== +======= expo-modules-core@1.12.11: version "1.12.11" resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.12.11.tgz#71d7efb2f6a2a4d3b96defad52fc799b9804f829" integrity sha512-CF5G6hZo/6uIUz6tj4dNRlvE5L4lakYukXPqz5ZHQ+6fLk1NQVZbRdpHjMkxO/QSBQcKUzG/ngeytpoJus7poQ== +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 dependencies: invariant "^2.2.4" @@ -5114,23 +5200,29 @@ expo-web-browser@~13.0.3: integrity sha512-HXb7y82ApVJtqk8tManyudtTrCtx8xcUnVzmJECeHCB0SsWSQ+penVLZxJkcyATWoJOsFMnfVSVdrTcpKKGszQ== expo@~51.0.2: +<<<<<<< HEAD + version "51.0.9" + resolved "https://registry.yarnpkg.com/expo/-/expo-51.0.9.tgz#a47de1b3d3afe1bf51f37bf69e487d0317686722" + integrity sha512-USckGusi4389X/mFy8X8tEA59vgsc5joOILQrTsDPdDJawkUidDAVqY4NIjJxOY7zEq1SfPv/LglJ4cOdtvfsg== +======= version "51.0.8" resolved "https://registry.yarnpkg.com/expo/-/expo-51.0.8.tgz#a7981e86ee20eac4b847c7c8cc5799d9c6b1508d" integrity sha512-bdTOiMb1f3PChtuqEZ9czUm2gMTmS0r1+H+Pkm2O3PsuLnOgxfIBzL6S37+J4cUocLBaENrmx9SOGKpzhBqXpg== +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 dependencies: "@babel/runtime" "^7.20.0" - "@expo/cli" "0.18.13" - "@expo/config" "9.0.2" + "@expo/cli" "0.18.14" + "@expo/config" "9.0.1" "@expo/config-plugins" "8.0.4" - "@expo/metro-config" "0.18.4" + "@expo/metro-config" "0.18.3" "@expo/vector-icons" "^14.0.0" babel-preset-expo "~11.0.6" expo-asset "~10.0.6" expo-file-system "~17.0.1" - expo-font "~12.0.5" + expo-font "~12.0.6" expo-keep-awake "~13.0.2" expo-modules-autolinking "1.11.1" - expo-modules-core "1.12.11" + expo-modules-core "1.12.12" fbemitter "^3.0.0" whatwg-url-without-unicode "8.0.0-3" @@ -5317,9 +5409,9 @@ flow-enums-runtime@^0.0.6: integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw== flow-parser@0.*: - version "0.236.0" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.236.0.tgz#8e8e6c59ff7e8d196c0ed215b3919320a1c6e332" - integrity sha512-0OEk9Gr+Yj7wjDW2KgaNYUypKau71jAfFyeLQF5iVtxqc6uJHag/MT7pmaEApf4qM7u86DkBcd4ualddYMfbLw== + version "0.237.1" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.237.1.tgz#ca0192ccc2403227f7947251c41adaebbcb8efba" + integrity sha512-PUeG8GQLmrv49vEcFcag7mriJvVs7Yyegnv1DGskvcokhP8UyqWsLV0KoTQ1iAW3ePVUIGUc3MFfBaXwz9MmIg== font-awesome@^4.7.0: version "4.7.0" @@ -5915,7 +6007,7 @@ hermes-profile-transformer@^0.0.6: dependencies: source-map "^0.7.3" -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7273,7 +7365,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.debounce@^4.0.8: +lodash.debounce@4.0.8, lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== @@ -7587,7 +7679,7 @@ mdurl@^2.0.0: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== -memoize-one@^5.0.0: +memoize-one@^5.0.0, memoize-one@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== @@ -8186,6 +8278,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment@^2.29.4: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + mri@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -8862,7 +8959,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@15.8.1, prop-types@^15.5.10, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -9168,6 +9265,26 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-native-bidirectional-infinite-scroll@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/react-native-bidirectional-infinite-scroll/-/react-native-bidirectional-infinite-scroll-0.3.3.tgz#31e83e30514be2eaaa889b97d01149c8a08576ec" + integrity sha512-zxYJDjrxTkGqg83WH3fSdufg79XZ7xDDn9HdHlKo9avAcz92Rf28/ivDeUM2aOUmmboqJK8BqtVByT6cF/taYg== + +react-native-calendars-sac@^1.22.25: + version "1.22.25" + resolved "https://registry.yarnpkg.com/react-native-calendars-sac/-/react-native-calendars-sac-1.22.25.tgz#a0d124ceefc4a1e90232cbeb41de4f298401531a" + integrity sha512-4v0y+akbFIAQ+CDddiEuGLgml8JeS0lZJWra5MVF7RcG4Ps4ix1BO4yFlKJRZjQolD9JFI32Vqmr/yJA2gmScg== + dependencies: + hoist-non-react-statics "^3.3.1" + lodash "^4.17.15" + memoize-one "^5.2.1" + prop-types "^15.5.10" + react-native-swipe-gestures "^1.0.5" + recyclerlistview "^4.0.0" + xdate "^0.8.0" + optionalDependencies: + moment "^2.29.4" + react-native-gesture-handler@^2.16.2: version "2.16.2" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz#032bd2a07334292d7f6cff1dc9d1ec928f72e26d" @@ -9245,6 +9362,11 @@ react-native-svg@^15.3.0: css-select "^5.1.0" css-tree "^1.1.3" +react-native-swipe-gestures@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz#a172cb0f3e7478ccd681fd36b8bfbcdd098bde7c" + integrity sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw== + react-native-vector-icons@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-10.1.0.tgz#c98a225213700177d23492e32d1dc920b9bae8aa" @@ -9435,6 +9557,15 @@ recast@^0.21.0: source-map "~0.6.1" tslib "^2.0.1" +recyclerlistview@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.0.tgz#a140149aaa470c9787a1426452651934240d69ef" + integrity sha512-uuBCi0c+ggqHKwrzPX4Z/mJOzsBbjZEAwGGmlwpD/sD7raXixdAbdJ6BTcAmuWG50Cg4ru9p12M94Njwhr/27A== + dependencies: + lodash.debounce "4.0.8" + prop-types "15.8.1" + ts-object-utils "0.0.5" + redux-thunk@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" @@ -10219,16 +10350,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10314,7 +10436,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10328,13 +10450,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -10680,6 +10795,11 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== +ts-object-utils@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077" + integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA== + tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -10698,9 +10818,9 @@ tsutils@^3.21.0: tslib "^1.8.1" turbo-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.0.1.tgz#4daf74bc678ec1285b67ce42fe7a2852acdc3235" - integrity sha512-sm0ZtcX9YWh28p5X8t5McxC2uthrt9p+g0bGE0KTVFhnhNWefpSVCr+67zRNDUOfo4bpXwiOp7otO+dyQ7/y/A== + version "2.1.0" + resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.1.0.tgz#5d610fd7b2f577412eb6439218fb56879cc0033d" + integrity sha512-muUMPf3BT7N6PeJJn91Wf+8oXEkwktNJZ9kBjIm0QzhFYPGJ7xqgQMblRQcscysb2v7VhY9AB+bOQbHaIUXyGA== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -10831,9 +10951,15 @@ undici-types@~5.26.4: integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== undici@^6.10.1: +<<<<<<< HEAD + version "6.18.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.18.2.tgz#f662a5dc33cf654fc412a9912e5a07b138d75c97" + integrity sha512-o/MQLTwRm9IVhOqhZ0NQ9oXax1ygPjw6Vs+Vq/4QRjbOAC3B1GCHy7TYxxbExKlb7bzDRzt9vBWU6BDz0RFfYg== +======= version "6.18.1" resolved "https://registry.yarnpkg.com/undici/-/undici-6.18.1.tgz#8390af4c4bed00fc32cb5f77f1c5e03e3271b8f2" integrity sha512-/0BWqR8rJNRysS5lqVmfc7eeOErcOP4tZpATVjJOojjHZ71gSYVAtFhEmadcIjwMIUehh5NFyKGsXCnXIajtbA== +>>>>>>> 9ff3dae73bb4f6f9dbf26bc7970dd1b36955da59 unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" @@ -11354,7 +11480,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11372,15 +11498,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -11437,6 +11554,11 @@ xcode@^3.0.1: simple-plist "^1.1.0" uuid "^7.0.3" +xdate@^0.8.0: + version "0.8.2" + resolved "https://registry.yarnpkg.com/xdate/-/xdate-0.8.2.tgz#d7b033c00485d02695baf0044f4eacda3fc961a3" + integrity sha512-sNBlLfOC8S3V0vLDEUianQOXcTsc9j4lfeKU/klHe0RjHAYn0CXsSttumTot8dzalboV8gZbH38B+WcCIBjhFQ== + xml-name-validator@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"