diff --git a/app.json b/app.json index ab63ad26..ede0ea96 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "TAMU SHPE", "slug": "TAMU-SHPE", - "version": "1.0.16", + "version": "1.0.17", "owner": "tamu-shpe", "orientation": "portrait", "icon": "./assets/icon.png", diff --git a/assets/calendar-days-solid_black.svg b/assets/calendar-days-solid_black.svg new file mode 100644 index 00000000..8d38d2cc --- /dev/null +++ b/assets/calendar-days-solid_black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/calendar-days-solid_white.svg b/assets/calendar-days-solid_white.svg new file mode 100644 index 00000000..f8d5d5f4 --- /dev/null +++ b/assets/calendar-days-solid_white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/clock-solid_black.svg b/assets/clock-solid_black.svg new file mode 100644 index 00000000..66f32461 --- /dev/null +++ b/assets/clock-solid_black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/clock-solid_white.svg b/assets/clock-solid_white.svg new file mode 100644 index 00000000..40b063d1 --- /dev/null +++ b/assets/clock-solid_white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/location-dot-solid_black.svg b/assets/location-dot-solid_black.svg new file mode 100644 index 00000000..acd033c1 --- /dev/null +++ b/assets/location-dot-solid_black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/location-dot-solid_white.svg b/assets/location-dot-solid_white.svg new file mode 100644 index 00000000..89977cad --- /dev/null +++ b/assets/location-dot-solid_white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package.json b/package.json index 661ed312..f10f3e1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shpe-app", - "version": "1.0.16", + "version": "1.0.17", "scripts": { "start": "npx expo start --dev-client", "test": "jest --coverage=true --verbose --bail --config ./jest.config.ts", diff --git a/src/api/firebaseUtils.ts b/src/api/firebaseUtils.ts index 18648bcf..21942aff 100644 --- a/src/api/firebaseUtils.ts +++ b/src/api/firebaseUtils.ts @@ -528,6 +528,29 @@ export const getUpcomingEvents = async () => { return events; }; +export const getWeekPastEvents = async (): Promise => { + const currentTime = new Date(); + const twoWeeksAgo = new Date(currentTime); + twoWeeksAgo.setDate(currentTime.getDate() - 8); + + const eventsRef = collection(db, "events"); + const q = query( + eventsRef, + where("endTime", "<", currentTime), + where("endTime", ">", twoWeeksAgo), + orderBy("endTime", "desc") + ); + + const querySnapshot = await getDocs(q); + const events: SHPEEvent[] = []; + + querySnapshot.forEach(doc => { + events.push({ id: doc.id, ...doc.data() } as SHPEEvent); + }); + + return events; +}; + export const getPastEvents = async (numLimit: number, startAfterDoc: any, setEndOfData?: (endOfData: boolean) => void) => { const currentTime = new Date(); const eventsRef = collection(db, "events"); diff --git a/src/components/MOTMCard.tsx b/src/components/MOTMCard.tsx index c9735e15..24314dcc 100644 --- a/src/components/MOTMCard.tsx +++ b/src/components/MOTMCard.tsx @@ -68,6 +68,9 @@ const MOTMCard: React.FC = ({ navigation }) => { }, [currentUser]) ); + if (!MOTM) { + return null; + } return ( diff --git a/src/helpers/timeUtils.ts b/src/helpers/timeUtils.ts index b97e163b..db54f1ca 100644 --- a/src/helpers/timeUtils.ts +++ b/src/helpers/timeUtils.ts @@ -65,26 +65,6 @@ export const formatTime = (date: Date): string => { return `${hour % 12 == 0 ? 12 : hour % 12}:${minute.toString().padStart(2, '0')}${hour > 11 ? "pm" : "am"}`; } -export const formatEventTime = (startDate: Date, endDate: Date): string => { - const startHour = startDate.getHours(); - const endHour = endDate.getHours(); - const startMinute = startDate.getMinutes(); - const endMinute = endDate.getMinutes(); - - const startAmPm = startHour >= 12 ? "pm" : "am"; - const endAmPm = endHour >= 12 ? "pm" : "am"; - - const formattedStartTime = `${startHour % 12 === 0 ? 12 : startHour % 12}:${startMinute.toString().padStart(2, '0')}`; - const formattedEndTime = `${endHour % 12 === 0 ? 12 : endHour % 12}:${endMinute.toString().padStart(2, '0')}${endAmPm}`; - - if (startAmPm === endAmPm) { - return `${formattedStartTime} - ${formattedEndTime}`; - } - - return `${formattedStartTime}${startAmPm} - ${formattedEndTime}`; -} - - /** * Constructs a readable string that represents the date and time of day of a given `Date` object and it's timezone offset. * @param date @@ -94,7 +74,26 @@ export const formatDateTime = (date: Date): string => { return `${formatDate(date)}, ${formatTime(date)} GMT${date.getTimezoneOffset() < 0 ? "+" : "-"}${(date.getTimezoneOffset() / 60).toString().padStart(2, '0')}` } -export const formatEventDate = (startTime: Date, endTime: Date) => { +export const formatEventDate = (startTime: Date, endTime: Date): string => { + // Formats the date as "Monday, September 16" + const formatFullDate = (date: Date): string => { + const options: Intl.DateTimeFormatOptions = { weekday: 'long', month: 'long', day: 'numeric' }; + return date.toLocaleDateString('en-US', options); + }; + + // Formats the date as "September 16" + const formatMonthDay = (date: Date): string => { + const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric' }; + return date.toLocaleDateString('en-US', options); + }; + + // Formats the date as "September 16, 2024" + const formatMonthDayYear = (date: Date): string => { + const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric', year: 'numeric' }; + return date.toLocaleDateString('en-US', options); + }; + + // Check if start and end times are on the same day, month, or year const isSameDay = startTime.getDate() === endTime.getDate() && startTime.getMonth() === endTime.getMonth() && startTime.getFullYear() === endTime.getFullYear(); @@ -103,22 +102,82 @@ export const formatEventDate = (startTime: Date, endTime: Date) => { startTime.getFullYear() === endTime.getFullYear(); const isSameYear = startTime.getFullYear() === endTime.getFullYear(); - const formatMonthDayOnly = (date: Date): string => { - const day = date.getDate(); - const month = monthNames[date.getMonth()]; - return `${month} ${day}`; + if (isSameDay) { + return formatFullDate(startTime); // "Monday, September 16" + } else if (isSameMonth) { + return `${formatMonthDay(startTime)} - ${endTime.getDate()}`; // "September 16 - 17" + } else if (isSameYear) { + return `${formatMonthDay(startTime)} - ${formatMonthDay(endTime)}`; // "September 16 - October 18" + } else { + return `${formatMonthDayYear(startTime)} - ${formatMonthDayYear(endTime)}`; // "December 17, 2024 - December 18, 2025" } +}; +export const formatEventDateTime = (startTime: Date, endTime: Date): string => { + // Formats the date as "Monday, September 16" + const formatFullDate = (date: Date): string => { + const options: Intl.DateTimeFormatOptions = { weekday: 'long', month: 'long', day: 'numeric' }; + return date.toLocaleDateString('en-US', options); + }; + + // Formats the date as "September 16" + const formatMonthDay = (date: Date): string => { + const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric' }; + return date.toLocaleDateString('en-US', options); + }; + + // Formats the date as "September 16, 2024" + const formatMonthDayYear = (date: Date): string => { + const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric', year: 'numeric' }; + return date.toLocaleDateString('en-US', options); + }; + + // Formats the time as "1:00pm" + const formatTime = (date: Date): string => { + const hours = date.getHours(); + const minutes = date.getMinutes(); + const amPm = hours >= 12 ? 'pm' : 'am'; + const formattedHour = hours % 12 === 0 ? 12 : hours % 12; + const formattedMinute = minutes.toString().padStart(2, '0'); + return `${formattedHour}:${formattedMinute}${amPm}`; + }; + + const isSameDay = startTime.getDate() === endTime.getDate() && + startTime.getMonth() === endTime.getMonth() && + startTime.getFullYear() === endTime.getFullYear(); + + const isSameMonth = startTime.getMonth() === endTime.getMonth() && + startTime.getFullYear() === endTime.getFullYear(); + + const isSameYear = startTime.getFullYear() === endTime.getFullYear(); if (isSameDay) { - return `${formatDate(startTime)}`; + return `${formatFullDate(startTime)}, ${formatTime(startTime)} - ${formatTime(endTime)}`; // "Monday, September 16, 1:00pm - 2:00pm" } else if (isSameMonth) { - return `${formatMonthDayOnly(startTime)} - ${endTime.getDate()}`; + return `${formatMonthDay(startTime)}, ${formatTime(startTime)} - ${formatMonthDay(endTime)}, ${formatTime(endTime)}`; // "September 16, 1:00pm - September 17, 2:00pm" } else if (isSameYear) { - return `${formatMonthDayOnly(startTime)} - ${formatDate(endTime)}`; + return `${formatMonthDay(startTime)}, ${formatTime(startTime)} - ${formatMonthDay(endTime)}, ${formatTime(endTime)}`; // "September 16, 1:00pm - October 18, 2:00pm" } else { - return `${formatDateWithYear(startTime)} - ${formatDateWithYear(endTime)}`; + return `${formatMonthDayYear(startTime)}, ${formatTime(startTime)} - ${formatMonthDayYear(endTime)}, ${formatTime(endTime)}`; // "December 17, 2024, 1:00pm - December 18, 2025, 2:00pm" } -}; \ No newline at end of file +}; + +export const formatEventTime = (startDate: Date, endDate: Date): string => { + // Helper function to format time + const formatTime = (date: Date): string => { + const hours = date.getHours(); + const minutes = date.getMinutes(); + const amPm = hours >= 12 ? 'pm' : 'am'; + const formattedHour = hours % 12 === 0 ? 12 : hours % 12; + const formattedMinute = minutes.toString().padStart(2, '0'); + return `${formattedHour}:${formattedMinute}${amPm}`; + }; + + const startFormatted = formatTime(startDate); + const endFormatted = formatTime(endDate); + + // Format time range output + return `${startFormatted} - ${endFormatted}`; +}; diff --git a/src/screens/committees/CommitteeInfo.tsx b/src/screens/committees/CommitteeInfo.tsx index 6f3cec89..136b4d46 100644 --- a/src/screens/committees/CommitteeInfo.tsx +++ b/src/screens/committees/CommitteeInfo.tsx @@ -348,12 +348,6 @@ const CommitteeInfo: React.FC = ({ route, navigat Upcoming Events - navigation.getParent()?.navigate('EventsTab', { screen: 'EventsScreen', params: { filter: EventType.COMMITTEE_MEETING, committee: firebaseDocName } })} - > - View all - { const route = useRoute(); @@ -142,6 +152,15 @@ const EventInfo = ({ navigation }: EventProps) => { const eventButtonState = getEventButtonState(event, userEventLog); + const isSameDay = (startDate: Date, endDate: Date): boolean => { + return startDate.getDate() === endDate.getDate() && + startDate.getMonth() === endDate.getMonth() && + startDate.getFullYear() === endDate.getFullYear(); + }; + const sameDay = startTime && endTime && isSameDay(startTime.toDate(), endTime.toDate()); + + + return ( @@ -309,41 +328,41 @@ const EventInfo = ({ navigation }: EventProps) => { {name} {eventType} - {committee && (" • " + reverseFormattedFirebaseName(committee))} • {calculateMaxPossiblePoints(event)} points - - - Hosted By {creatorData?.name} + {committee && (" • " + reverseFormattedFirebaseName(committee))} • {calculateMaxPossiblePoints(event)} points • {creatorData?.name} {/* Date, Time and Location */} - - - - Date - {(startTime && endTime) ? formatEventDate(startTime.toDate(), endTime.toDate()) : ""} + + + + {darkMode ? : } + + {(startTime && endTime) ? + (sameDay ? + formatEventDate(startTime.toDate(), endTime.toDate()) + : formatEventDateTime(startTime.toDate(), endTime.toDate()) + ) + : "" + } + + - - Time - {startTime && endTime && formatEventTime(startTime.toDate(), endTime.toDate())} + + {sameDay && ( + + + {darkMode ? : } + + + {startTime && endTime && formatEventTime(startTime.toDate(), endTime.toDate())} + - + )} {locationName && ( - Location {geolocation ? ( { handleLinkPress(`https://www.google.com/maps/dir/?api=1&destination=${geolocation.latitude},${geolocation.longitude}`); } }} + className='flex-row items-center' > - + + {darkMode ? : } + + {locationName} ) : ( - + {locationName} )} @@ -372,8 +395,8 @@ const EventInfo = ({ navigation }: EventProps) => { {/* Description */} {(description && description.trim() != "") && ( - - About Event + + Description {description} )} diff --git a/src/screens/events/Events.tsx b/src/screens/events/Events.tsx index bdc5cca3..9694286e 100644 --- a/src/screens/events/Events.tsx +++ b/src/screens/events/Events.tsx @@ -1,14 +1,12 @@ import { View, Text, TouchableOpacity, ActivityIndicator, ScrollView, Image, useColorScheme } from 'react-native' -import React, { useCallback, useContext, useEffect, useRef, useState } from 'react' +import React, { useCallback, useContext, useEffect, useState } from 'react' import { SafeAreaView } from 'react-native-safe-area-context'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { RouteProp, useFocusEffect, useRoute } from '@react-navigation/core'; import { Octicons, FontAwesome6 } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { StatusBar } from 'expo-status-bar'; -import { auth } from '../../config/firebaseConfig'; -import { getUpcomingEvents, getPastEvents, getCommittees, fetchAndStoreUser } from '../../api/firebaseUtils'; +import { getUpcomingEvents, fetchAndStoreUser, getWeekPastEvents } from '../../api/firebaseUtils'; import { UserContext } from '../../context/UserContext'; import { Images } from '../../../assets'; import { formatTime } from '../../helpers/timeUtils'; @@ -16,43 +14,46 @@ import { truncateStringWithEllipsis } from '../../helpers/stringUtils'; import { EventsStackParams } from '../../types/navigation'; import { EventType, ExtendedEventType, SHPEEvent } from '../../types/events'; import EventCard from './EventCard'; -import { Committee } from '../../types/committees'; import DismissibleModal from '../../components/DismissibleModal'; +interface EventGroups { + today: SHPEEvent[]; + upcoming: SHPEEvent[]; + past: SHPEEvent[]; +} + + const Events = ({ navigation }: EventsProps) => { const route = useRoute(); const userContext = useContext(UserContext); const { userInfo, setUserInfo } = userContext!; - const fixDarkMode = userInfo?.private?.privateInfo?.settings?.darkMode; const useSystemDefault = userInfo?.private?.privateInfo?.settings?.useSystemDefault; const colorScheme = useColorScheme(); const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; - const filterScrollViewRef = useRef(null); - const committeeScrollViewRef = useRef(null); - const [isLoading, setIsLoading] = useState(true); - const [todayEvents, setTodayEvents] = useState([]); - const [upcomingEvents, setUpcomingEvents] = useState([]); - const [pastEvents, setPastEvents] = useState([]); - const [selectedFilter, setSelectedFilter] = useState(route.params?.filter || null); - const [committees, setCommittees] = useState([]); - const [selectedCommittee, setSelectedCommittee] = useState(route.params?.committee || null); + const [mainEvents, setMainEvents] = useState({ today: [], upcoming: [], past: [] }); + const [intramuralEvents, setIntramuralEvents] = useState({ today: [], upcoming: [], past: [] }); + const [committeeEvents, setCommitteeEvents] = useState({ today: [], upcoming: [], past: [] }); const [infoVisible, setInfoVisible] = useState(false); + const [filter, setFilter] = useState<"main" | "intramural" | "committee">("main"); const hasPrivileges = (userInfo?.publicInfo?.roles?.admin?.valueOf() || userInfo?.publicInfo?.roles?.officer?.valueOf() || userInfo?.publicInfo?.roles?.developer?.valueOf() || userInfo?.publicInfo?.roles?.lead?.valueOf() || userInfo?.publicInfo?.roles?.representative?.valueOf()); + const selectedEvents = filter === "main" ? mainEvents : filter === "intramural" ? intramuralEvents : committeeEvents; + const fetchEvents = async () => { try { setIsLoading(true); const upcomingEventsData = await getUpcomingEvents(); - const pastEventsData = await getPastEvents(3, null); + const allPastEvents = await getWeekPastEvents(); const currentTime = new Date(); const today = new Date(currentTime.getFullYear(), currentTime.getMonth(), currentTime.getDate()); + const todayEvents = upcomingEventsData.filter(event => { const startTime = event.startTime ? event.startTime.toDate() : new Date(0); return startTime >= today && startTime < new Date(today.getTime() + 24 * 60 * 60 * 1000); @@ -62,9 +63,63 @@ const Events = ({ navigation }: EventsProps) => { return startTime >= new Date(today.getTime() + 24 * 60 * 60 * 1000); }); - setTodayEvents(todayEvents); - setUpcomingEvents(upcomingEvents); - setPastEvents(pastEventsData.events); + const mainEventsFiltered = upcomingEventsData.filter( + (event: SHPEEvent) => + (hasPrivileges || !event.hiddenEvent) && + event.eventType !== EventType.COMMITTEE_MEETING && + event.eventType !== EventType.INTRAMURAL_EVENT + ); + + const intramuralEventsFiltered = upcomingEventsData.filter( + (event: SHPEEvent) => + (hasPrivileges || !event.hiddenEvent) && + event.eventType === EventType.INTRAMURAL_EVENT + ); + + const committeeEventsFiltered = upcomingEventsData.filter( + (event: SHPEEvent) => + (hasPrivileges || !event.hiddenEvent) && + event.eventType === EventType.COMMITTEE_MEETING + ); + + const pastMainEvents = allPastEvents.filter( + (event: SHPEEvent) => + (hasPrivileges || !event.hiddenEvent) && + event.eventType !== EventType.COMMITTEE_MEETING && + event.eventType !== EventType.INTRAMURAL_EVENT + ); + + const pastIntramuralEvents = allPastEvents.filter( + (event: SHPEEvent) => + (hasPrivileges || !event.hiddenEvent) && + event.eventType === EventType.INTRAMURAL_EVENT + ); + + const pastCommitteeEvents = allPastEvents.filter( + (event: SHPEEvent) => + (hasPrivileges || !event.hiddenEvent) && + event.eventType === EventType.COMMITTEE_MEETING + ); + + + + setMainEvents({ + today: todayEvents.filter(event => mainEventsFiltered.includes(event)), + upcoming: upcomingEvents.filter(event => mainEventsFiltered.includes(event)), + past: pastMainEvents, + }); + + setIntramuralEvents({ + today: todayEvents.filter(event => intramuralEventsFiltered.includes(event)), + upcoming: upcomingEvents.filter(event => intramuralEventsFiltered.includes(event)), + past: pastIntramuralEvents, + }); + + setCommitteeEvents({ + today: todayEvents.filter(event => committeeEventsFiltered.includes(event)), + upcoming: upcomingEvents.filter(event => committeeEventsFiltered.includes(event)), + past: pastCommitteeEvents, + }); setIsLoading(false); } catch (error) { @@ -73,11 +128,6 @@ const Events = ({ navigation }: EventsProps) => { } }; - const fetchCommittees = async () => { - const committeeData = await getCommittees(); - setCommittees(committeeData); - }; - useEffect(() => { const fetchUserData = async () => { @@ -89,30 +139,8 @@ const Events = ({ navigation }: EventsProps) => { fetchEvents(); fetchUserData(); - fetchCommittees(); }, []) - useEffect(() => { - if (route.params?.filter !== undefined) { - setSelectedFilter(route.params.filter); - - const filterIndex = ["myEvents", "clubWide", ...Object.values(EventType)].indexOf(route.params.filter); - if (filterIndex !== -1 && filterScrollViewRef.current) { - const scrollPosition = filterIndex * 115; - filterScrollViewRef.current.scrollTo({ x: scrollPosition, animated: true }); - } - } - - if (route.params?.committee !== undefined) { - setSelectedCommittee(route.params.committee); - - const committeeIndex = committees.findIndex(committee => committee.firebaseDocName === route.params.committee); - if (committeeIndex !== -1 && committeeScrollViewRef.current) { - const scrollPosition = committeeIndex * 100; - committeeScrollViewRef.current.scrollTo({ x: scrollPosition, animated: true }); - } - } - }, [route.params, committees]); useFocusEffect( useCallback(() => { @@ -122,75 +150,6 @@ const Events = ({ navigation }: EventsProps) => { }, [hasPrivileges]) ); - const handleFilterSelect = (filter?: ExtendedEventType, committee?: string) => { - // Deselect committee when the same committee is selected - if (committee) { - if (selectedCommittee === committee) { - setSelectedCommittee(null); - if (!selectedFilter) { - setSelectedFilter(null); - } - } else { - setSelectedCommittee(committee); - if (selectedFilter !== EventType.COMMITTEE_MEETING) { - setSelectedFilter(EventType.COMMITTEE_MEETING); - } - } - return; - } - - // Deselect "Committee Meetings" when no committee is selected - if (filter === EventType.COMMITTEE_MEETING && selectedFilter === EventType.COMMITTEE_MEETING) { - setSelectedFilter(null); - setSelectedCommittee(null); - return; - } - - // Handle other filters - if (selectedFilter === filter) { - setSelectedFilter(null); - setSelectedCommittee(null); - } else { - setSelectedFilter(filter!); - setSelectedCommittee(null); - } - }; - - const filteredEvents = (events: SHPEEvent[]): SHPEEvent[] => { - // If no filter is selected, filter out hidden events and committee meetings - if (!selectedFilter) { - return events.filter(event => - (event.eventType !== EventType.COMMITTEE_MEETING || event.general) && - !event.hiddenEvent - ); - } - - if (selectedFilter === 'myEvents') { - return events.filter(event => - (userInfo?.publicInfo?.committees?.includes(event.committee || '') || - userInfo?.publicInfo?.interests?.includes(event.eventType || '')) && - !event.hiddenEvent - ); - } - - if (selectedFilter === 'clubWide') { - return events.filter(event => event.general && !event.hiddenEvent); - } - // Show hidden events for "Custom Event" filter - if (selectedFilter === 'Custom Event') { - return events.filter(event => event.eventType === selectedFilter); - } - - if (selectedFilter === EventType.COMMITTEE_MEETING) { - return events.filter(event => - event.eventType === EventType.COMMITTEE_MEETING && - (selectedCommittee ? event.committee === selectedCommittee : true) && - !event.hiddenEvent - ); - } - - return events.filter(event => event.eventType === selectedFilter && !event.hiddenEvent); - }; return ( @@ -205,99 +164,57 @@ const Events = ({ navigation }: EventsProps) => { {/* Filters */} - - - handleFilterSelect("myEvents")} + { + setFilter("main") + }} + > + - My Events - - - handleFilterSelect("clubWide")} + Main + + + + { + setFilter("intramural") + }} + > + - Club Wide - - - {Object.values(EventType).map((type) => ( - handleFilterSelect(type)} - > - {type} - - ))} - - - - {/* Additional Committee Filter */} - {selectedFilter === EventType.COMMITTEE_MEETING && ( - - - {committees.map(committee => ( - handleFilterSelect(undefined, committee.firebaseDocName)} - > - {committee.name} - - ))} - - - )} + Intramural + + + { + setFilter("committee") + }} + > + + Committee + + + {isLoading && @@ -307,100 +224,110 @@ const Events = ({ navigation }: EventsProps) => { {/* Event Listings */} {!isLoading && ( - - {filteredEvents(todayEvents).length === 0 && filteredEvents(upcomingEvents).length === 0 && filteredEvents(pastEvents).length === 0 ? ( - + + {selectedEvents.today.length === 0 && + selectedEvents.upcoming.length === 0 && + selectedEvents.past.length === 0 ? ( + No Events ) : ( {/* Today's Events */} - {filteredEvents(todayEvents).length !== 0 && ( - + {selectedEvents.today.length !== 0 && ( + Today's Events - {filteredEvents(todayEvents)?.map((event: SHPEEvent, index) => { - return ( - 0 && "mt-8"}`} - style={{ - shadowColor: "#000", - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 3.84, - - elevation: 5, - }} - onPress={() => { navigation.navigate("EventInfo", { event: event }) }} + {selectedEvents.today.map((event: SHPEEvent, index) => ( + 0 && "mt-8"}`} + style={{ + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }} + onPress={() => { + navigation.navigate("EventInfo", { event: event }); + }} + > + + - - + + {truncateStringWithEllipsis(event.name, 20)} + + {event.locationName ? ( + + {truncateStringWithEllipsis(event.locationName, 24)} + + ) : null} + + {formatTime(event.startTime?.toDate()!)} + + + + {hasPrivileges && ( + { + navigation.navigate("QRCode", { event: event }); + }} + className="absolute right-0 top-0 p-2 m-2 rounded-full" + style={{ backgroundColor: "rgba(0,0,0,0.7)" }} > - - {truncateStringWithEllipsis(event.name, 20)} - {event.locationName ? ( - {truncateStringWithEllipsis(event.locationName, 24)} - ) : null} - {formatTime(event.startTime?.toDate()!)} - - - {hasPrivileges && ( - { navigation.navigate("QRCode", { event: event }) }} - className='absolute right-0 top-0 p-2 m-2 rounded-full' - style={{ backgroundColor: 'rgba(0,0,0,0.7)' }} - > - - - )} - - ); - })} + + + )} + + ))} )} {/* Upcoming Events */} - {filteredEvents(upcomingEvents).length !== 0 && ( - - Upcoming Events - {filteredEvents(upcomingEvents)?.map((event: SHPEEvent, index) => { - return ( - 0 && "mt-8"}`}> - - - ); - })} + {selectedEvents.upcoming.length !== 0 && ( + + + Upcoming Events + + {selectedEvents.upcoming.map((event: SHPEEvent, index) => ( + 0 && "mt-8"}`}> + + + ))} )} {/* Past Events */} - {filteredEvents(pastEvents).length !== 0 && ( - - Past Events - {filteredEvents(pastEvents)?.map((event: SHPEEvent, index) => { - return ( - 0 && "mt-8"}`}> - - - ); - })} + {selectedEvents.past.length !== 0 && ( + + + Past Events + + {selectedEvents.past.map((event: SHPEEvent, index) => ( + 0 && "mt-8"}`}> + + + ))} )} - {!selectedFilter && ( - navigation.navigate("PastEvents")}> - View more - - )} + navigation.navigate("PastEvents")}> + View all past events + )} @@ -464,7 +391,7 @@ const Events = ({ navigation }: EventsProps) => { Event Location Check - The location check only happens during scans; we do not track you continuously. You are free to leave the area, but if a sign-out scan is required, you must be at the location to sign out. + The location check only happens during scans; we do not track you continuously. If a sign-out scan is required, you must be at the location to sign out. diff --git a/src/screens/events/FinalizeEvent.tsx b/src/screens/events/FinalizeEvent.tsx index 5e9d678d..36d6de74 100644 --- a/src/screens/events/FinalizeEvent.tsx +++ b/src/screens/events/FinalizeEvent.tsx @@ -6,14 +6,21 @@ import { Octicons } from '@expo/vector-icons'; import { UserContext } from '../../context/UserContext'; import { RouteProp, useRoute } from '@react-navigation/core'; import { Images } from '../../../assets'; -import { MillisecondTimes, formatEventDate, formatEventTime } from '../../helpers/timeUtils'; +import { MillisecondTimes, formatEventDate, formatEventDateTime, formatEventTime } from '../../helpers/timeUtils'; import { StatusBar } from 'expo-status-bar'; import { handleLinkPress } from '../../helpers/links'; -import { SHPEEvent } from '../../types/events'; +import { EventType, SHPEEvent } from '../../types/events'; import { LinearGradient } from 'expo-linear-gradient'; import { reverseFormattedFirebaseName } from '../../types/committees'; import InteractButton from '../../components/InteractButton'; import { createEvent } from '../../api/firebaseUtils'; +import CalendarIconBlack from '../../../assets/calendar-days-solid_black.svg' +import CalendarIconWhite from '../../../assets/calendar-days-solid_white.svg' +import ClockIconBlack from '../../../assets/clock-solid_black.svg' +import ClockIconWhite from '../../../assets/clock-solid_white.svg' +import LocationDotIconBlack from '../../../assets/location-dot-solid_black.svg' +import LocationDotIconWhite from '../../../assets/location-dot-solid_white.svg' + const FinalizeEvent = ({ navigation }: EventProps) => { const route = useRoute(); @@ -30,6 +37,13 @@ const FinalizeEvent = ({ navigation }: EventProps) => { const colorScheme = useColorScheme(); const darkMode = useSystemDefault ? colorScheme === 'dark' : fixDarkMode; + const isSameDay = (startDate: Date, endDate: Date): boolean => { + return startDate.getDate() === endDate.getDate() && + startDate.getMonth() === endDate.getMonth() && + startDate.getFullYear() === endDate.getFullYear(); + }; + const sameDay = startTime && endTime && isSameDay(startTime.toDate(), endTime.toDate()); + return ( @@ -79,51 +93,57 @@ const FinalizeEvent = ({ navigation }: EventProps) => { {/* General Details */} {nationalConventionEligible && ( - - This event is eligible for national convention requirements* + This event is eligible for national convention requirements* + + )} + + {(eventType === EventType.STUDY_HOURS) && ( + + Feel free to leave the area. Just be sure to scan in and out at the event location to fully earn your points! )} + {name} {eventType} - {committee && (" • " + reverseFormattedFirebaseName(committee))} • {calculateMaxPossiblePoints(event)} points - - - Hosted By {userInfo?.publicInfo?.name} + {committee && (" • " + reverseFormattedFirebaseName(committee))} • {calculateMaxPossiblePoints(event)} points • {userInfo?.publicInfo?.name} - {/* Date, Time and Location */} - - - - Date - {(startTime && endTime) ? formatEventDate(startTime.toDate(), endTime.toDate()) : ""} - - - Time - {startTime && endTime && formatEventTime(startTime.toDate(), endTime.toDate())} + {/* Date, Time and Location */} + + + + {darkMode ? : } + + {(startTime && endTime) ? + (sameDay ? + formatEventDate(startTime.toDate(), endTime.toDate()) + : formatEventDateTime(startTime.toDate(), endTime.toDate()) + ) + : "" + } + + {sameDay && ( + + + {darkMode ? : } + + + {startTime && endTime && formatEventTime(startTime.toDate(), endTime.toDate())} + + + )} + {locationName && ( - Location {geolocation ? ( { handleLinkPress(`https://www.google.com/maps/dir/?api=1&destination=${geolocation.latitude},${geolocation.longitude}`); } }} + className='flex-row items-center' > - + + {darkMode ? : } + + {locationName} ) : ( - + {locationName} )} )} + + {/* Description */} {(description && description.trim() != "") && ( - - About Event + + Description {description} )} @@ -173,9 +199,6 @@ const FinalizeEvent = ({ navigation }: EventProps) => { }} /> - - - ) } diff --git a/src/screens/events/QRCodeScanningScreen.tsx b/src/screens/events/QRCodeScanningScreen.tsx index ff153ecc..1427a659 100644 --- a/src/screens/events/QRCodeScanningScreen.tsx +++ b/src/screens/events/QRCodeScanningScreen.tsx @@ -1,15 +1,31 @@ -import { View, Text, Button, StyleSheet, TouchableHighlight, TouchableOpacity, Alert, Animated, Easing } from 'react-native'; +import { View, Text, TouchableOpacity, Animated, Easing, Dimensions, PixelRatio } from 'react-native'; import React, { useEffect, useRef, useState } from 'react'; -import { CameraView, Camera } from 'expo-camera'; +import { CameraView, Camera, BarcodeBounds } from 'expo-camera'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { MainStackParams } from '../../types/navigation'; import { Octicons } from '@expo/vector-icons'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { MainStackParams } from '../../types/navigation'; + +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +type BarCodeScannedResult = { + type: string; + data: string; + bounds?: BarcodeBounds +}; const QRCodeScanningScreen = ({ navigation }: NativeStackScreenProps) => { const [hasCameraPermissions, setHasCameraPermissions] = useState(null); - const [scanned, setScanned] = useState(false); + const [boxColor, setBoxColor] = useState('#FFFFFF'); + const [validScanned, setValidScanned] = useState(false); + const pulseAnim = useRef(new Animated.Value(1)).current; + const boxTop = useRef(new Animated.Value((screenHeight / 2) - 240)).current; + const boxLeft = useRef(new Animated.Value((screenWidth / 2) - 120)).current; + const boxWidth = useRef(new Animated.Value(240)).current; + const boxHeight = useRef(new Animated.Value(240)).current; + const lastScale = useRef(1); + useEffect(() => { const getBarCodeScannerPermissions = async () => { @@ -27,13 +43,13 @@ const QRCodeScanningScreen = ({ navigation }: NativeStackScreenProps pulse()); }; @@ -41,27 +57,54 @@ const QRCodeScanningScreen = ({ navigation }: NativeStackScreenProps { - console.log('Data Received', `Bar code with type ${type} and data ${data} has been scanned!`); - setScanned(true); - const dataRegex: RegExp = /^tamu-shpe:\/\/event\?id=[a-zA-z0-9]+&mode=(sign-in|sign-out)$/i; - if (!dataRegex.test(data)) { - Alert.alert("Invalid QR Code", "Either this QR Code is invalid or was misscanned. Please try again.", [ - { - text: 'ok', - onPress: () => { - setScanned(false); - } - } - ]); + const handleBarCodeScanned = ({ bounds, type, data }: BarCodeScannedResult) => { + if (validScanned) { + return; } - else { - const linkVariables = data.split('?')[1].split('&'); - const id = linkVariables[0].split('=')[1]; - const mode = linkVariables[1].split('=')[1]; - if (id && mode === 'sign-in' || mode === 'sign-out') { - navigation.navigate("EventVerificationScreen", { id, mode }); + + const dataRegex = /^tamu-shpe:\/\/event\?id=[a-zA-Z0-9]+&mode=(sign-in|sign-out)$/i; + if (dataRegex.test(data)) { + setValidScanned(true); + console.log('Data Received', `Bar code with type ${type} and data ${data} has been scanned!`); + if (bounds) { + setBoxColor('#FD652F'); + + Animated.parallel([ + Animated.timing(boxTop, { + toValue: bounds.origin.y, + duration: 90, + easing: Easing.inOut(Easing.ease), + useNativeDriver: false, + }), + Animated.timing(boxLeft, { + toValue: bounds.origin.x, + duration: 90, + easing: Easing.inOut(Easing.ease), + useNativeDriver: false, + }), + Animated.timing(boxWidth, { + toValue: bounds.size.width, + duration: 90, + easing: Easing.inOut(Easing.ease), + useNativeDriver: false, + }), + Animated.timing(boxHeight, { + toValue: bounds.size.height, + duration: 90, + easing: Easing.inOut(Easing.ease), + useNativeDriver: false, + }), + ]).start(); } + + setTimeout(() => { + const linkVariables = data.split('?')[1].split('&'); + const id = linkVariables[0].split('=')[1]; + const mode = linkVariables[1].split('=')[1]; + if (id && (mode === 'sign-in' || mode === 'sign-out')) { + navigation.navigate('EventVerificationScreen', { id, mode }); + } + }, 500); } }; @@ -74,34 +117,53 @@ const QRCodeScanningScreen = ({ navigation }: NativeStackScreenProps - {/* Header */} Scanner - navigation.goBack()} > + navigation.goBack()}> - - {/* Pulsing Effect */} - - - - - - - - - - + {/* Pulsing Effect with Animated Transition */} + + + + + + + + @@ -112,4 +174,4 @@ const QRCodeScanningScreen = ({ navigation }: NativeStackScreenProps) => - navigation.getParent()?.navigate('EventsTab', { screen: 'EventsScreen', params: { filter: 'myEvents' } })} - > - View all -