From ab2b9e2bffa5a903bc16662de386f49cb3f433ea Mon Sep 17 00:00:00 2001 From: Mai Nguyen <123816878+in-mai-space@users.noreply.github.com> Date: Tue, 18 Jun 2024 00:09:12 +0700 Subject: [PATCH] feat: event & club preview components (#1034) Co-authored-by: Alder Whiteford --- backend/entities/clubs/events/transactions.go | 2 +- frontend/lib/package.json | 2 +- frontend/lib/src/types/index.ts | 1 + frontend/mobile/package.json | 2 +- .../mobile/src/app/(app)/(tabs)/_layout.tsx | 94 ++++---- .../mobile/src/app/(app)/(tabs)/index.tsx | 42 +--- frontend/mobile/src/app/(app)/_layout.tsx | 11 + frontend/mobile/src/app/(app)/club/[id].tsx | 220 ++++++++++++++++++ .../mobile/src/app/(app)/club/_layout.tsx | 13 ++ .../app/(app)/club/components/skeleton.tsx | 46 ++++ frontend/mobile/src/app/(app)/event/[id].tsx | 162 +++++-------- .../src/app/(app)/event/components/about.tsx | 36 +-- .../(app)/event/components/description.tsx | 5 +- .../app/(app)/event/components/location.tsx | 27 +-- .../app/(app)/event/components/overview.tsx | 40 ++-- .../app/(app)/event/components/register.tsx | 2 - .../event/components/upcoming-events.tsx | 5 +- .../components/AboutSection/AboutSection.tsx | 34 +++ .../AnimatedImageHeader.tsx | 61 +++++ .../components/Calendar/Day.tsx | 4 +- .../components/ClubIcon/ClubIcon.tsx | 3 +- .../components/ClubPage/ClubPage.tsx | 172 -------------- .../components/ClubPage/ClubPageHeader.tsx | 23 -- .../RecruitmentInfo/ClubRecruitmentInfo.tsx | 10 +- .../components/EventCard/EventCardList.tsx | 12 +- .../EventCard/Variants/EventCardBig.tsx | 4 +- .../EventCard/Variants/EventCardCalendar.tsx | 17 +- .../EventCard/Variants/EventCardSmall.tsx | 2 +- .../components/PageError/PageError.tsx} | 4 +- .../components/Preview/Club/ClubPreview.tsx | 125 ++++++++++ .../Preview/Club/ClubPreviewSkeleton.tsx | 38 +++ .../components/Preview/Event/EventPreview.tsx | 125 ++++++++++ .../Preview/Event/EventPreviewSkeleton.tsx | 38 +++ .../components/Preview/PreviewError.tsx | 14 ++ .../(design-system)/components/Tag/Tags.tsx | 27 +++ .../mobile/src/app/(design-system)/index.ts | 3 + frontend/mobile/src/app/_layout.tsx | 25 +- frontend/mobile/src/hooks/useClub.ts | 75 ++++++ frontend/mobile/src/hooks/useEvent.ts | 70 ++++++ frontend/mobile/src/hooks/usePreview.ts | 56 +++++ frontend/mobile/src/store/slices/clubSlice.ts | 68 ++++++ .../mobile/src/store/slices/eventSlice.ts | 69 ++++++ frontend/mobile/src/store/store.ts | 6 +- frontend/mobile/yarn.lock | 8 +- 44 files changed, 1307 insertions(+), 496 deletions(-) create mode 100644 frontend/mobile/src/app/(app)/club/[id].tsx create mode 100644 frontend/mobile/src/app/(app)/club/_layout.tsx create mode 100644 frontend/mobile/src/app/(app)/club/components/skeleton.tsx create mode 100644 frontend/mobile/src/app/(design-system)/components/AboutSection/AboutSection.tsx create mode 100644 frontend/mobile/src/app/(design-system)/components/AnimatedImageHeader/AnimatedImageHeader.tsx delete mode 100644 frontend/mobile/src/app/(design-system)/components/ClubPage/ClubPage.tsx delete mode 100644 frontend/mobile/src/app/(design-system)/components/ClubPage/ClubPageHeader.tsx rename frontend/mobile/src/app/{(app)/event/components/error.tsx => (design-system)/components/PageError/PageError.tsx} (90%) create mode 100644 frontend/mobile/src/app/(design-system)/components/Preview/Club/ClubPreview.tsx create mode 100644 frontend/mobile/src/app/(design-system)/components/Preview/Club/ClubPreviewSkeleton.tsx create mode 100644 frontend/mobile/src/app/(design-system)/components/Preview/Event/EventPreview.tsx create mode 100644 frontend/mobile/src/app/(design-system)/components/Preview/Event/EventPreviewSkeleton.tsx create mode 100644 frontend/mobile/src/app/(design-system)/components/Preview/PreviewError.tsx create mode 100644 frontend/mobile/src/app/(design-system)/components/Tag/Tags.tsx create mode 100644 frontend/mobile/src/hooks/useClub.ts create mode 100644 frontend/mobile/src/hooks/useEvent.ts create mode 100644 frontend/mobile/src/hooks/usePreview.ts create mode 100644 frontend/mobile/src/store/slices/clubSlice.ts create mode 100644 frontend/mobile/src/store/slices/eventSlice.ts diff --git a/backend/entities/clubs/events/transactions.go b/backend/entities/clubs/events/transactions.go index 9d4ddf98a..dcdd159f3 100644 --- a/backend/entities/clubs/events/transactions.go +++ b/backend/entities/clubs/events/transactions.go @@ -14,7 +14,7 @@ func GetClubEvents(db *gorm.DB, clubID uuid.UUID, pageInfo fiberpaginate.PageInf db = cache.SetUseCache(db, true) var events []models.Event - if err := db.Where("club_id = ?", clubID).Scopes(utilities.IntoScope(pageInfo, db)).Find(&events).Error; err != nil { + if err := db.Where("host = ?", clubID).Scopes(utilities.IntoScope(pageInfo, db)).Find(&events).Error; err != nil { return nil, err } diff --git a/frontend/lib/package.json b/frontend/lib/package.json index c08e89d8b..fd4707354 100644 --- a/frontend/lib/package.json +++ b/frontend/lib/package.json @@ -1,6 +1,6 @@ { "name": "@generatesac/lib", - "version": "0.0.170", + "version": "0.0.171", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/frontend/lib/src/types/index.ts b/frontend/lib/src/types/index.ts index 693dbacf2..6352c6a66 100644 --- a/frontend/lib/src/types/index.ts +++ b/frontend/lib/src/types/index.ts @@ -10,3 +10,4 @@ export * from "./event"; export * from "./file"; export * from "./pointOfContact"; export * from "./verification"; +export * from "./recruitment"; diff --git a/frontend/mobile/package.json b/frontend/mobile/package.json index 743b29a97..d2ee424e5 100644 --- a/frontend/mobile/package.json +++ b/frontend/mobile/package.json @@ -25,7 +25,7 @@ "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-native-fontawesome": "^0.3.2", - "@generatesac/lib": "0.0.170", + "@generatesac/lib": "0.0.171", "@gorhom/bottom-sheet": "^4.6.3", "@hookform/resolvers": "^3.4.2", "@react-native-async-storage/async-storage": "^1.23.1", diff --git a/frontend/mobile/src/app/(app)/(tabs)/_layout.tsx b/frontend/mobile/src/app/(app)/(tabs)/_layout.tsx index e7996b90b..7733fc27d 100644 --- a/frontend/mobile/src/app/(app)/(tabs)/_layout.tsx +++ b/frontend/mobile/src/app/(app)/(tabs)/_layout.tsx @@ -35,54 +35,56 @@ const TabBarIcon: React.FC = ({ focused, icon }) => ( const Layout = () => { return ( - - - TabBarLabel({ focused, title: 'Home' }), - tabBarIcon: ({ focused }) => - TabBarIcon({ focused, icon: faHouse }) + <> + - - TabBarLabel({ focused, title: 'Calendar' }), - tabBarIcon: ({ focused }) => - TabBarIcon({ focused, icon: faCalendarDays }) + sceneContainerStyle={{ + backgroundColor: 'white' }} - /> - - TabBarLabel({ focused, title: 'Profile' }), - tabBarIcon: ({ focused }) => - TabBarIcon({ focused, icon: faUser }) - }} - /> - + > + + TabBarLabel({ focused, title: 'Home' }), + tabBarIcon: ({ focused }) => + TabBarIcon({ focused, icon: faHouse }) + }} + /> + + TabBarLabel({ focused, title: 'Calendar' }), + tabBarIcon: ({ focused }) => + TabBarIcon({ focused, icon: faCalendarDays }) + }} + /> + + TabBarLabel({ focused, title: 'Profile' }), + tabBarIcon: ({ focused }) => + TabBarIcon({ focused, icon: faUser }) + }} + /> + + ); }; diff --git a/frontend/mobile/src/app/(app)/(tabs)/index.tsx b/frontend/mobile/src/app/(app)/(tabs)/index.tsx index dfc535ff1..7c612d70e 100644 --- a/frontend/mobile/src/app/(app)/(tabs)/index.tsx +++ b/frontend/mobile/src/app/(app)/(tabs)/index.tsx @@ -1,47 +1,7 @@ import React from 'react'; -import { Pressable, StyleSheet } from 'react-native'; - -import { router } from 'expo-router'; - -import { Box, Text } from '@/src/app/(design-system)'; -import { EventCard } from '@/src/app/(design-system)/components/EventCard'; const HomePage = () => { - const item = { - name: 'Your Event Name', - host: 'Your Club Name', - start_time: new Date(), - end_time: new Date() - }; - - return ( - - Home - router.push(`/event/1`)}> - - - - ); + return <>; }; -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center' - }, - contentContainer: { - flex: 1, - alignItems: 'center' - } -}); - export default HomePage; diff --git a/frontend/mobile/src/app/(app)/_layout.tsx b/frontend/mobile/src/app/(app)/_layout.tsx index c981213db..c136c2c31 100644 --- a/frontend/mobile/src/app/(app)/_layout.tsx +++ b/frontend/mobile/src/app/(app)/_layout.tsx @@ -17,6 +17,17 @@ const Layout = () => { } }} /> + { + const { id } = useLocalSearchParams<{ id: string }>(); + const { width } = Dimensions.get('window'); + const IMG_HEIGHT = width; + + const scrollRef = useAnimatedRef(); + const scrollOffset = useScrollViewOffset(scrollRef); + + const bottomSheet = useRef(null); + + const club = useAppSelector((state) => state.club); + const { setRetriggerFetch, apiLoading, apiError } = useClub(id as string); + + const headerAnimatedStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateY: interpolate( + scrollOffset.value, + [-IMG_HEIGHT, 0, IMG_HEIGHT], + [0, 0, -IMG_HEIGHT * 0.75] + ) + } + ] + }; + }); + + return ( + + ( + + + + ) + }} + /> + + {apiLoading ? ( + + ) : apiError ? ( + + ) : ( + <> + + + + + + + + + + {club.name} + + + bottomSheet.current?.snapToIndex(0) + } + type="club" + /> + {club.recruitment?.is_recruiting && ( + + )} + + + Recruiting + + + + + + Upcoming Events + + {club.events.length > 0 ? ( + <> + + + + ) : ( + <> + )} + + + + + )} + + + + ); +}; + +export default ClubPage; diff --git a/frontend/mobile/src/app/(app)/club/_layout.tsx b/frontend/mobile/src/app/(app)/club/_layout.tsx new file mode 100644 index 000000000..026d149b4 --- /dev/null +++ b/frontend/mobile/src/app/(app)/club/_layout.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { Stack } from 'expo-router'; + +const Layout = () => { + return ( + + + + ); +}; + +export default Layout; diff --git a/frontend/mobile/src/app/(app)/club/components/skeleton.tsx b/frontend/mobile/src/app/(app)/club/components/skeleton.tsx new file mode 100644 index 000000000..0e1cf3c9f --- /dev/null +++ b/frontend/mobile/src/app/(app)/club/components/skeleton.tsx @@ -0,0 +1,46 @@ +import { Skeleton } from '@rneui/base'; + +import { Box, Colors } from '@/src/app/(design-system)'; + +const ClubPageSkeleton = () => { + return ( + + + + + + + + + + + + + ); +}; + +export default ClubPageSkeleton; diff --git a/frontend/mobile/src/app/(app)/event/[id].tsx b/frontend/mobile/src/app/(app)/event/[id].tsx index 64cc02bf9..e867f04f6 100644 --- a/frontend/mobile/src/app/(app)/event/[id].tsx +++ b/frontend/mobile/src/app/(app)/event/[id].tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useRef } from 'react'; import { Dimensions } from 'react-native'; import Animated, { interpolate, @@ -9,18 +9,20 @@ import Animated, { import { Stack, useLocalSearchParams } from 'expo-router'; -import { EventType, clubApi } from '@generatesac/lib'; -import { eventApi } from '@generatesac/lib'; +import { EventType } from '@generatesac/lib'; import BottomSheet from '@gorhom/bottom-sheet'; import { Arrow, Box, KebabMenu } from '@/src/app/(design-system)'; import { SACColors } from '@/src/app/(design-system)'; import { Button } from '@/src/app/(design-system)/components/Button/Button'; import { description, events, tags } from '@/src/consts/event-page'; +import useEvent from '@/src/hooks/useEvent'; +import { useAppSelector } from '@/src/store/store'; +import AnimatedImageHeader from '../../(design-system)/components/AnimatedImageHeader/AnimatedImageHeader'; +import PageError from '../../(design-system)/components/PageError/PageError'; import { AboutEvent } from './components/about'; import { Description } from './components/description'; -import EventPageError from './components/error'; import { Location } from './components/location'; import { Overview } from './components/overview'; import { RegisterBottomSheet } from './components/register'; @@ -33,7 +35,7 @@ const MockEvent = { clubId: 'generate', // uuid logo: 'data:image/png;base64,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', eventName: 'Generate Spring Showcase 2024', - color: 'aqua' as SACColors, + color: 'darkRed' as SACColors, club: 'Generate', startTime: new Date('2024-06-01T10:00:00'), endTime: new Date('2024-06-01T12:00:00'), @@ -56,39 +58,12 @@ const EventPage = () => { const scrollRef = useAnimatedRef(); const scrollOffset = useScrollViewOffset(scrollRef); - const [retriggerFetch, setRetriggerFetch] = useState(false); - - const [ - getEvent, - { isLoading: eventLoading, error: eventError, data: event } - ] = eventApi.useLazyEventQuery(); - const [getClub, { isLoading: clubLoading, error: clubError, data: club }] = - clubApi.useLazyClubQuery(); - const [ - getEventTags, - { isLoading: tagsLoading, error: tagsError, data: tags } - ] = eventApi.useLazyEventTagsQuery(); + const event = useAppSelector((state) => state.event); + const { name: clubName, logo: clubLogo } = useAppSelector( + (state) => state.club + ); - const imageAnimatedStyle = useAnimatedStyle(() => { - return { - transform: [ - { - translateY: interpolate( - scrollOffset.value, - [-IMG_HEIGHT, 0, IMG_HEIGHT], - [-IMG_HEIGHT / 2, 0, IMG_HEIGHT * 0.75] - ) - }, - { - scale: interpolate( - scrollOffset.value, - [-IMG_HEIGHT, 0, IMG_HEIGHT], - [2, 1, 1] - ) - } - ] - }; - }); + const { setRetriggerFetch, apiLoading, apiError } = useEvent(id as string); const headerAnimatedStyle = useAnimatedStyle(() => { return { @@ -104,22 +79,6 @@ const EventPage = () => { }; }); - useEffect(() => { - // Fetch events - getEvent(id as string).then(({ data: eventData }) => { - if (eventData) { - // Fetch club - getClub(eventData.host as string); - // Fetch tags: - getEventTags(eventData.id); - } - }); - }, [retriggerFetch, id, getClub, getEvent, getEventTags]); - - const apiLoading = eventLoading || clubLoading || tagsLoading; - const apiError = eventError || clubError || tagsError; - const allData = event && tags && club; - return ( { {apiLoading ? ( ) : apiError ? ( - + ) : ( - allData && ( - <> - - - - - - - - - - + + + + + - bottomSheet.current?.snapToIndex(0) - } + club={clubName} + startTime={event.start_time} + endTime={event.end_time} + location={event.location || 'ISEC'} + type={event.event_type} /> - + register.current?.snapToIndex(0) } - /> + > + Register + - {/* */} + + bottomSheet.current?.snapToIndex(0) + } + /> + - - ) + {/* */} + + )} @@ -233,6 +178,7 @@ const EventPage = () => { ); diff --git a/frontend/mobile/src/app/(app)/event/components/about.tsx b/frontend/mobile/src/app/(app)/event/components/about.tsx index 438eb6aad..442237dd6 100644 --- a/frontend/mobile/src/app/(app)/event/components/about.tsx +++ b/frontend/mobile/src/app/(app)/event/components/about.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Linking, TouchableOpacity } from 'react-native'; +import { Linking } from 'react-native'; import { faVideo } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; @@ -12,9 +12,11 @@ import { Text, textColorVariants } from '@/src/app/(design-system)'; -import { Tag as TagComponent } from '@/src/app/(design-system)'; +import { AboutSection } from '@/src/app/(design-system)/components/AboutSection/AboutSection'; import { Button } from '@/src/app/(design-system)/components/Button/Button'; +import { PageTags } from '../../../(design-system)/components/Tag/Tags'; + interface AboutEventProps { tags: Tag[]; description: string; @@ -36,31 +38,15 @@ export const AboutEvent = ({ } }; - const renderTag = (item: Tag) => ( - - {item.name} - - ); - return ( - - About Event - - - - - {description} - - - Read More... - - - - - {tags.length >= 5 - ? tags.slice(0, 5).map((item) => renderTag(item)) - : tags.map((item) => renderTag(item))} + + + {zoomLink && ( - - - {club.name} - - {tags.map((tag) => ( - - {tag.name} - - ))} - - - About Us - {club.description} - - - - - - Recruiting - - - - Upcoming Events - - - - - Leadership - - - - - ); -}; diff --git a/frontend/mobile/src/app/(design-system)/components/ClubPage/ClubPageHeader.tsx b/frontend/mobile/src/app/(design-system)/components/ClubPage/ClubPageHeader.tsx deleted file mode 100644 index ba81006b2..000000000 --- a/frontend/mobile/src/app/(design-system)/components/ClubPage/ClubPageHeader.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import Animated from 'react-native-reanimated'; - -import { Box } from '../Box/Box'; - -interface AnimatedImageBoxProps { - uri: string; - animatedStyle: any; -} - -export const AnimatedImageBox: React.FC = ({ - uri, - animatedStyle -}) => { - return ( - - - - ); -}; diff --git a/frontend/mobile/src/app/(design-system)/components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo.tsx b/frontend/mobile/src/app/(design-system)/components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo.tsx index 32d191b15..bfe2313f7 100644 --- a/frontend/mobile/src/app/(design-system)/components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo.tsx +++ b/frontend/mobile/src/app/(design-system)/components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo.tsx @@ -15,11 +15,9 @@ interface RecruitmentInfoProps { recruitmentCycle: RecruitmentCycle; recruitingType: RecruitmentType; isRecruiting?: boolean; - recruitmentText: string; } export const RecruitmentInfo = ({ - recruitmentText, color, recruitmentCycle, recruitingType, @@ -35,12 +33,8 @@ export const RecruitmentInfo = ({ /> = ({ events }) => { +export const EventCardList: React.FC = ({ + events, + club +}) => { const renderEventCard = ({ item }: { item: Event }) => { return ( router.push(`/event/${item.id}`)}> diff --git a/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx b/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx index c5bedf986..1edd303d7 100644 --- a/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx +++ b/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx @@ -7,7 +7,7 @@ import { router } from 'expo-router'; import { Image } from '@rneui/base'; import { Box, Text } from '@/src/app/(design-system)'; -import { calculateDuration, createOptions, eventTime } from '@/src/utils/time'; +import { createOptions, eventTime } from '@/src/utils/time'; interface EventCardBigProps { event: string; @@ -46,8 +46,6 @@ export const EventCardBig: React.FC = ({ width="100%" > {event} - - {calculateDuration(startTime, endTime)} {club} diff --git a/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx b/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx index 0a6d769be..32047c78b 100644 --- a/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx +++ b/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx @@ -1,12 +1,15 @@ import React from 'react'; import { TouchableOpacity } from 'react-native'; -import { router } from 'expo-router'; - import { Tag } from '@generatesac/lib'; import { Avatar, Image } from '@rneui/base'; import { Box, Text, createStyles } from '@/src/app/(design-system)'; +import { + setEventId, + setEventShouldPreview +} from '@/src/store/slices/eventSlice'; +import { useAppDispatch } from '@/src/store/store'; import { createOptions, eventTime, happeningNow } from '@/src/utils/time'; import { EventTags } from '../EventCardTags/EventCardTags'; @@ -35,11 +38,15 @@ export const EventCardCalendar: React.FC = ({ }) => { const isHappening = happeningNow(startTime, endTime); const isPast = new Date(endTime).getTime() < new Date().getTime(); + const dispatch = useAppDispatch(); return ( router.navigate(`/event/${eventId}`)} + onPress={() => { + dispatch(setEventId(eventId)); + dispatch(setEventShouldPreview(true)); + }} > {isHappening && ( @@ -63,8 +70,8 @@ export const EventCardCalendar: React.FC = ({ /> - {event.length >= 28 - ? `${event.slice(0, 28).trim()}...` + {event.length >= 26 + ? `${event.slice(0, 26).trim()}...` : event} diff --git a/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx b/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx index 392cef2fb..740e71105 100644 --- a/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx +++ b/frontend/mobile/src/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx @@ -27,7 +27,7 @@ export const EventCardSmall: React.FC = ({ image }) => { const screenWidth = Dimensions.get('window').width; - const boxWidth = screenWidth * 0.4; + const boxWidth = screenWidth * 0.41; return ( diff --git a/frontend/mobile/src/app/(app)/event/components/error.tsx b/frontend/mobile/src/app/(design-system)/components/PageError/PageError.tsx similarity index 90% rename from frontend/mobile/src/app/(app)/event/components/error.tsx rename to frontend/mobile/src/app/(design-system)/components/PageError/PageError.tsx index 9fb570dc1..5fcb1ef28 100644 --- a/frontend/mobile/src/app/(app)/event/components/error.tsx +++ b/frontend/mobile/src/app/(design-system)/components/PageError/PageError.tsx @@ -8,7 +8,7 @@ type EventPageErrorProps = { refetch: React.Dispatch>; }; -const EventPageError = ({ refetch }: EventPageErrorProps) => { +const PageError = ({ refetch }: EventPageErrorProps) => { return ( @@ -30,4 +30,4 @@ const EventPageError = ({ refetch }: EventPageErrorProps) => { ); }; -export default EventPageError; +export default PageError; diff --git a/frontend/mobile/src/app/(design-system)/components/Preview/Club/ClubPreview.tsx b/frontend/mobile/src/app/(design-system)/components/Preview/Club/ClubPreview.tsx new file mode 100644 index 000000000..b9cfc6a5e --- /dev/null +++ b/frontend/mobile/src/app/(design-system)/components/Preview/Club/ClubPreview.tsx @@ -0,0 +1,125 @@ +import React, { forwardRef, useCallback, useState } from 'react'; + +import { router } from 'expo-router'; + +import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'; +import { SerializedError } from '@reduxjs/toolkit'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { Avatar } from '@rneui/base'; + +import { setClubShouldPreview } from '@/src/store/slices/clubSlice'; +import { useAppDispatch, useAppSelector } from '@/src/store/store'; + +import { Box } from '../../Box/Box'; +import { Button } from '../../Button/Button'; +import { Text } from '../../Text/Text'; +import PreviewError from '../PreviewError'; +import ClubPreviewSkeleton from './ClubPreviewSkeleton'; + +interface ClubPreviewProps { + clubId: string; + isLoading: boolean; + error: FetchBaseQueryError | SerializedError | undefined; +} + +type Ref = BottomSheet; +const LOGO_HEIGHT = 77; + +export const ClubPreview = forwardRef( + ({ clubId, isLoading, error }, ref) => { + const club = useAppSelector((state) => state.club); + const dispatch = useAppDispatch(); + + const renderBackdrop = useCallback( + (props: any) => ( + + ), + [] + ); + + const ClubLogo = () => { + return () => ( + + + + ); + }; + + const NoLogo = () => { + return () => <>; + }; + + const [logo, setLogo] = useState(NoLogo); + + return ( + { + toIndex === 0 && !error + ? setLogo(ClubLogo) + : setLogo(NoLogo); + }} + backgroundStyle={{ backgroundColor: 'white' }} + backdropComponent={renderBackdrop} + onClose={() => dispatch(setClubShouldPreview(false))} + > + + + {isLoading || club.id === '' ? ( + + ) : error ? ( + + ) : ( + <> + + {club.name} + + + Who are we? + {`${club.preview}...`} + + + + )} + + + ); + } +); diff --git a/frontend/mobile/src/app/(design-system)/components/Preview/Club/ClubPreviewSkeleton.tsx b/frontend/mobile/src/app/(design-system)/components/Preview/Club/ClubPreviewSkeleton.tsx new file mode 100644 index 000000000..d406be081 --- /dev/null +++ b/frontend/mobile/src/app/(design-system)/components/Preview/Club/ClubPreviewSkeleton.tsx @@ -0,0 +1,38 @@ +import { Skeleton } from '@rneui/base'; + +import { Colors } from '../../../shared/colors'; +import { Box } from '../../Box/Box'; + +const ClubPreviewSkeleton = () => { + return ( + + + + + + + + + + + + + + + ); +}; + +export default ClubPreviewSkeleton; diff --git a/frontend/mobile/src/app/(design-system)/components/Preview/Event/EventPreview.tsx b/frontend/mobile/src/app/(design-system)/components/Preview/Event/EventPreview.tsx new file mode 100644 index 000000000..648efba7a --- /dev/null +++ b/frontend/mobile/src/app/(design-system)/components/Preview/Event/EventPreview.tsx @@ -0,0 +1,125 @@ +import React, { forwardRef, useCallback } from 'react'; +import { TouchableOpacity } from 'react-native-gesture-handler'; + +import { router } from 'expo-router'; + +import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'; +import { SerializedError } from '@reduxjs/toolkit'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { Avatar } from '@rneui/base'; + +import { setEventShouldPreview } from '@/src/store/slices/eventSlice'; +import { useAppDispatch, useAppSelector } from '@/src/store/store'; + +import { Box } from '../../Box/Box'; +import { Button } from '../../Button/Button'; +import { Text } from '../../Text/Text'; +import PreviewError from '../PreviewError'; +import EventPreviewSkeleton from './EventPreviewSkeleton'; + +interface EventPreviewProps { + eventId: string; + isLoading: boolean; + error: FetchBaseQueryError | SerializedError | undefined; +} + +type Ref = BottomSheet; + +export const EventPreview = forwardRef( + ({ eventId, isLoading, error }, ref) => { + const event = useAppSelector((state) => state.event); + const { name: clubName, logo: clubLogo } = useAppSelector( + (state) => state.club + ); + + const dispatch = useAppDispatch(); + + const renderBackdrop = useCallback( + (props: any) => ( + + ), + [] + ); + + return ( + dispatch(setEventShouldPreview(false))} + > + + {isLoading || event.id === '' ? ( + + ) : error ? ( + + ) : ( + <> + + {`${event.name.slice(0, 32)}${event.name.length > 32 ? '...' : ''}`} + + + { + dispatch(setEventShouldPreview(false)); + router.push(`/club/${event.host}`); + }} + > + + + Hosted by {clubName} + + + + + + + About Event + {`${event.description.slice(0, 220).trim()}${event.description.length > 220 ? '...' : ''}`} + + + + )} + + + ); + } +); diff --git a/frontend/mobile/src/app/(design-system)/components/Preview/Event/EventPreviewSkeleton.tsx b/frontend/mobile/src/app/(design-system)/components/Preview/Event/EventPreviewSkeleton.tsx new file mode 100644 index 000000000..e9a365191 --- /dev/null +++ b/frontend/mobile/src/app/(design-system)/components/Preview/Event/EventPreviewSkeleton.tsx @@ -0,0 +1,38 @@ +import { Skeleton } from '@rneui/base'; + +import { Colors } from '../../../shared/colors'; +import { Box } from '../../Box/Box'; + +const EventPreviewSkeleton = () => { + return ( + + + + + + + + + + + + + + + ); +}; + +export default EventPreviewSkeleton; diff --git a/frontend/mobile/src/app/(design-system)/components/Preview/PreviewError.tsx b/frontend/mobile/src/app/(design-system)/components/Preview/PreviewError.tsx new file mode 100644 index 000000000..635442ec0 --- /dev/null +++ b/frontend/mobile/src/app/(design-system)/components/Preview/PreviewError.tsx @@ -0,0 +1,14 @@ +import { Box } from '../Box/Box'; +import { Text } from '../Text/Text'; + +const PreviewError = () => { + return ( + + + Something went wrong, please try again later. + + + ); +}; + +export default PreviewError; diff --git a/frontend/mobile/src/app/(design-system)/components/Tag/Tags.tsx b/frontend/mobile/src/app/(design-system)/components/Tag/Tags.tsx new file mode 100644 index 000000000..67e21f0ab --- /dev/null +++ b/frontend/mobile/src/app/(design-system)/components/Tag/Tags.tsx @@ -0,0 +1,27 @@ +import { Tag } from '@generatesac/lib'; + +import { SACColors } from '../../shared/colors'; +import { Box } from '../Box/Box'; +import { Text } from '../Text/Text'; +import { Tag as TagComponent } from './Tag'; + +interface PageTags { + tags: Tag[]; + color: SACColors; +} + +export const PageTags: React.FC = ({ tags, color }) => { + const renderTag = (item: Tag) => ( + + {item.name} + + ); + + return ( + + {tags.length >= 5 + ? tags.slice(0, 5).map((item) => renderTag(item)) + : tags.map((item) => renderTag(item))} + + ); +}; diff --git a/frontend/mobile/src/app/(design-system)/index.ts b/frontend/mobile/src/app/(design-system)/index.ts index c45d74daa..ed1baac5d 100644 --- a/frontend/mobile/src/app/(design-system)/index.ts +++ b/frontend/mobile/src/app/(design-system)/index.ts @@ -12,3 +12,6 @@ export * from './components/Dropdown/SelectOne'; export * from './components/Dropdown/Multiselect'; export * from './components/PointOfContactCard/PointOfContactCard'; export * from './components/Tag/Tag'; +export * from './components/Preview/Event/EventPreview'; +export * from './components/Preview/Club/ClubPreview'; +export * from './components/Tag/Tags'; diff --git a/frontend/mobile/src/app/_layout.tsx b/frontend/mobile/src/app/_layout.tsx index abcba3931..f35b22cc3 100644 --- a/frontend/mobile/src/app/_layout.tsx +++ b/frontend/mobile/src/app/_layout.tsx @@ -10,9 +10,10 @@ import { StatusBar } from 'expo-status-bar'; import FontAwesome from '@expo/vector-icons/FontAwesome'; import { ThemeProvider } from '@shopify/restyle'; +import usePreview from '../hooks/usePreview'; import StoreProvider from '../store/StoreProvider'; import { useAppSelector } from '../store/store'; -import { theme } from './(design-system)'; +import { ClubPreview, EventPreview, theme } from './(design-system)'; export { ErrorBoundary } from 'expo-router'; @@ -20,6 +21,16 @@ SplashScreen.preventAutoHideAsync(); const InitalLayout = () => { const { accessToken } = useAppSelector((state) => state.user); + const { + eventPreviewRef, + eventId, + eventApiError, + eventApiLoading, + clubPreviewRef, + clubId, + clubApiError, + clubApiLoading + } = usePreview(); useEffect(() => { if (!accessToken) { @@ -36,6 +47,18 @@ const InitalLayout = () => { + + ); }; diff --git a/frontend/mobile/src/hooks/useClub.ts b/frontend/mobile/src/hooks/useClub.ts new file mode 100644 index 000000000..3f1726069 --- /dev/null +++ b/frontend/mobile/src/hooks/useClub.ts @@ -0,0 +1,75 @@ +import { useEffect, useState } from 'react'; + +import { clubApi } from '@generatesac/lib'; + +import { setClub, setClubEvents, setClubTags } from '../store/slices/clubSlice'; +import { useAppDispatch, useAppSelector } from '../store/store'; + +const useClub = (id: string) => { + const [retriggerFetch, setRetriggerFetch] = useState(false); + + const [getClub, { isLoading: clubLoading, error: clubError }] = + clubApi.useLazyClubQuery(); + const [getClubTags, { isLoading: tagsLoading, error: tagsError }] = + clubApi.useLazyClubTagsQuery(); + const [getEvents, { isLoading: eventsLoading, error: eventsError }] = + clubApi.useLazyClubEventsQuery(); + + const dispatch = useAppDispatch(); + const { shouldPreview, id: clubId } = useAppSelector((state) => state.club); + + const apiLoading = clubLoading || tagsLoading || eventsLoading; + const apiError = clubError || tagsError || eventsError; + + useEffect(() => { + if ((id !== '' && (id !== clubId || shouldPreview)) || apiError) { + getClub(id).then(({ data: clubData }) => { + if (clubData) { + dispatch(setClub(clubData)); + } + }); + + getClubTags(id).then(({ data: tagData }) => { + if (tagData) { + dispatch(setClubTags(tagData)); + } + }); + + getEvents({ id }).then(({ data: eventData }) => { + if (eventData) { + const sortedEvents = [...eventData]; + sortedEvents.sort((a, b) => { + return ( + new Date(a.start_time).getTime() - + new Date(b.start_time).getTime() + ); + }); + const events = sortedEvents.filter( + (event) => + new Date(event.end_time).getTime() > + new Date().getTime() + ); + dispatch(setClubEvents(events)); + } + }); + } + }, [ + retriggerFetch, + id, + getClub, + getClubTags, + getEvents, + dispatch, + clubId, + apiError, + shouldPreview + ]); + + return { + setRetriggerFetch, + apiLoading, + apiError + }; +}; + +export default useClub; diff --git a/frontend/mobile/src/hooks/useEvent.ts b/frontend/mobile/src/hooks/useEvent.ts new file mode 100644 index 000000000..6cb2778a2 --- /dev/null +++ b/frontend/mobile/src/hooks/useEvent.ts @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react'; + +import { clubApi, eventApi } from '@generatesac/lib'; + +import { setClub } from '../store/slices/clubSlice'; +import { setEvent, setEventTags } from '../store/slices/eventSlice'; +import { useAppDispatch, useAppSelector } from '../store/store'; + +const useEvent = (id: string) => { + const [retriggerFetch, setRetriggerFetch] = useState(false); + + const [getEvent, { isLoading: eventLoading, error: eventError }] = + eventApi.useLazyEventQuery(); + const [getClub, { isLoading: clubLoading, error: clubError }] = + clubApi.useLazyClubQuery(); + const [getEventTags, { isLoading: tagsLoading, error: tagsError }] = + eventApi.useLazyEventTagsQuery(); + + const dispatch = useAppDispatch(); + const { shouldPreview, id: eventId } = useAppSelector( + (state) => state.event + ); + + const apiLoading = eventLoading || clubLoading || tagsLoading; + const apiError = eventError || clubError || tagsError; + + useEffect(() => { + if ((id !== '' && (id !== eventId || shouldPreview)) || apiError) { + console.log('fetching data!'); + // Fetch events + getEvent(id).then(({ data: eventData }) => { + dispatch(setEvent(eventData)); + if (eventData) { + // Fetch club + getClub(eventData.host as string).then( + ({ data: clubData }) => { + if (clubData) { + dispatch(setClub(clubData)); + } + } + ); + } + }); + + getEventTags(id).then(({ data: tagData }) => { + if (tagData) { + dispatch(setEventTags(tagData)); + } + }); + } + }, [ + retriggerFetch, + id, + getClub, + getEvent, + getEventTags, + dispatch, + eventId, + shouldPreview, + apiError + ]); + + return { + setRetriggerFetch, + apiLoading, + apiError + }; +}; + +export default useEvent; diff --git a/frontend/mobile/src/hooks/usePreview.ts b/frontend/mobile/src/hooks/usePreview.ts new file mode 100644 index 000000000..8cba55d54 --- /dev/null +++ b/frontend/mobile/src/hooks/usePreview.ts @@ -0,0 +1,56 @@ +import { useEffect, useRef } from 'react'; + +import BottomSheet from '@gorhom/bottom-sheet'; + +import { setClubShouldPreview } from '../store/slices/clubSlice'; +import { setEventShouldPreview } from '../store/slices/eventSlice'; +import { useAppDispatch, useAppSelector } from '../store/store'; +import useClub from './useClub'; +import useEvent from './useEvent'; + +const usePreview = () => { + const dispatch = useAppDispatch(); + const eventPreviewRef = useRef(null); + const clubPreviewRef = useRef(null); + const { shouldPreview: eventShouldPreview, id: eventId } = useAppSelector( + (state) => state.event + ); + const { shouldPreview: clubShouldPreview, id: clubId } = useAppSelector( + (state) => state.club + ); + const { apiLoading: eventApiLoading, apiError: eventApiError } = + useEvent(eventId); + const { apiLoading: clubApiLoading, apiError: clubApiError } = + useClub(clubId); + + useEffect(() => { + dispatch(setEventShouldPreview(false)); + dispatch(setClubShouldPreview(false)); + }, [dispatch]); + + useEffect(() => { + if (eventShouldPreview) { + eventPreviewRef.current?.snapToIndex(0); + } else { + eventPreviewRef.current?.close(); + } + if (clubShouldPreview) { + clubPreviewRef.current?.snapToIndex(0); + } else { + clubPreviewRef.current?.close(); + } + }, [eventShouldPreview, clubShouldPreview]); + + return { + eventPreviewRef, + clubPreviewRef, + eventId, + clubId, + eventApiLoading, + eventApiError, + clubApiLoading, + clubApiError + }; +}; + +export default usePreview; diff --git a/frontend/mobile/src/store/slices/clubSlice.ts b/frontend/mobile/src/store/slices/clubSlice.ts new file mode 100644 index 000000000..3f44c9d12 --- /dev/null +++ b/frontend/mobile/src/store/slices/clubSlice.ts @@ -0,0 +1,68 @@ +import { Club, Event, Tag } from '@generatesac/lib'; +import { createSlice } from '@reduxjs/toolkit'; + +type ClubState = { + shouldPreview?: boolean; + tags: Tag[]; + events: Event[]; +}; + +const initialState: Club & ClubState = { + id: '', + created_at: '', + updated_at: '', + name: '', + description: '', + preview: '', + num_members: 0, + logo: '', + weekly_time_committment: 0, + one_word_to_describe_us: '', + tags: [], + events: [] +}; + +export const clubSlice = createSlice({ + name: 'club', + initialState, + reducers: { + setClubId: (state, action) => { + state.id = action.payload; + }, + setClub: (state, action) => { + state.id = action.payload.id; + state.created_at = action.payload.created_at; + state.updated_at = action.payload.updated_at; + state.name = action.payload.name; + state.description = action.payload.description; + state.preview = action.payload.preview; + state.num_members = action.payload.num_members; + state.logo = action.payload.logo; + state.weekly_time_committment = + action.payload.weekly_time_committment; + state.one_word_to_describe_us = + action.payload.one_word_to_describe_us; + state.recruitment = action.payload.recruitment; + }, + setClubTags: (state, action) => { + state.tags = action.payload; + }, + setClubEvents: (state, action) => { + state.events = action.payload; + }, + resetClub: () => initialState, + setClubShouldPreview: (state, action) => { + state.shouldPreview = action.payload; + } + } +}); + +export const { + setClubId, + setClub, + setClubTags, + setClubEvents, + resetClub, + setClubShouldPreview +} = clubSlice.actions; +export default clubSlice.reducer; diff --git a/frontend/mobile/src/store/slices/eventSlice.ts b/frontend/mobile/src/store/slices/eventSlice.ts new file mode 100644 index 000000000..cf43da36a --- /dev/null +++ b/frontend/mobile/src/store/slices/eventSlice.ts @@ -0,0 +1,69 @@ +import { Event, Tag } from '@generatesac/lib'; +import { createSlice } from '@reduxjs/toolkit'; + +type EventClubState = { + shouldPreview?: boolean; + tags: Tag[]; +}; + +const initialState: Event & EventClubState = { + id: '', + created_at: '', + updated_at: '', + name: '', + description: '', + start_time: '', + end_time: '', + location: '', + event_type: 'hybrid', + is_archived: false, + is_draft: false, + is_recurring: false, + is_public: false, + preview: '', + host: '', + tags: [] +}; + +export const eventSlice = createSlice({ + name: 'event', + initialState, + reducers: { + setEventId: (state, action) => { + state.id = action.payload; + }, + setEvent: (state, action) => { + state.id = action.payload.id; + state.created_at = action.payload.created_at; + state.updated_at = action.payload.updated_at; + state.name = action.payload.name; + state.description = action.payload.description; + state.start_time = action.payload.start_time; + state.end_time = action.payload.end_time; + state.location = action.payload.location; + state.event_type = action.payload.event_type; + state.is_archived = action.payload.is_archived; + state.is_draft = action.payload.is_draft; + state.is_recurring = action.payload.is_recurring; + state.is_public = action.payload.is_public; + state.preview = action.payload.preview; + state.host = action.payload.host; + }, + setEventTags: (state, action) => { + state.tags = action.payload; + }, + resetEvent: () => initialState, + setEventShouldPreview: (state, action) => { + state.shouldPreview = action.payload; + } + } +}); + +export const { + setEvent, + resetEvent, + setEventShouldPreview, + setEventTags, + setEventId +} = eventSlice.actions; +export default eventSlice.reducer; diff --git a/frontend/mobile/src/store/store.ts b/frontend/mobile/src/store/store.ts index c505d2ff9..21e502edc 100644 --- a/frontend/mobile/src/store/store.ts +++ b/frontend/mobile/src/store/store.ts @@ -16,10 +16,14 @@ import persistReducer from 'redux-persist/es/persistReducer'; import autoMergeLevel2 from 'redux-persist/es/stateReconciler/autoMergeLevel2'; import { thunk } from 'redux-thunk'; +import clubReducer from '@/src/store/slices/clubSlice'; +import eventReducer from '@/src/store/slices/eventSlice'; import userReducer from '@/src/store/slices/userSlice'; const rootReducer = combineReducers({ user: userReducer, + event: eventReducer, + club: clubReducer, [baseApi.reducerPath]: baseApi.reducer }); @@ -30,7 +34,7 @@ const persistanceConfig = { storage: AsyncStorage, stateReconciler: autoMergeLevel2, blacklist: [baseApi.reducerPath], - whitelist: ['user'] + whitelist: ['user', 'event', 'club'] }; const persistedReducer = persistReducer( diff --git a/frontend/mobile/yarn.lock b/frontend/mobile/yarn.lock index 26a578108..07999f6a7 100644 --- a/frontend/mobile/yarn.lock +++ b/frontend/mobile/yarn.lock @@ -1402,10 +1402,10 @@ humps "^2.0.1" prop-types "^15.7.2" -"@generatesac/lib@0.0.170": - version "0.0.170" - resolved "https://registry.yarnpkg.com/@generatesac/lib/-/lib-0.0.170.tgz#2401ac4216d43dc555f142633aba16d1141ca356" - integrity sha512-oPkqZTudQd7tGEQUpAXQrhD/cngIUV595q069d7SMQZ3zZleQqGXirvk1TOjdoEzNug1gN21yS238stq6CL4Nw== +"@generatesac/lib@0.0.171": + version "0.0.171" + resolved "https://registry.yarnpkg.com/@generatesac/lib/-/lib-0.0.171.tgz#e5ea2c800dc319e4938616e0033de6d1b2fb3ee9" + integrity sha512-/9RN79Oa5b8+bJsWaMD+8A+fyyaVdzc1KVt+9QVnHog8OaQivx6fAFu+lV2X/j25+KbKsEd/rCSWX5u6DfMe3Q== dependencies: "@reduxjs/toolkit" "^2.2.3" react "^18.2.0"