diff --git a/components/timetable/Event.tsx b/components/timetable/Event.tsx index 67eeebb..0a03ca0 100644 --- a/components/timetable/Event.tsx +++ b/components/timetable/Event.tsx @@ -1,4 +1,3 @@ -import { useFavorite } from '@/hooks/useFavorite'; import dayjs from 'dayjs'; import { useTranslation } from 'react-i18next'; import { Image, View } from 'react-native'; @@ -12,6 +11,8 @@ interface EventProps { end: Date; color: string; thumbnail: string; + isFavorite: boolean; + toggleFavorite: () => void; } const getEventTimeString = (start: Date, end: Date) => { @@ -25,9 +26,18 @@ const getEventTimeString = (start: Date, end: Date) => { return `${startTime.format('HH:mm')} - ${endTime.format('HH:mm')}`; }; -const Event = ({ id, title, location, start, end, color, thumbnail }: EventProps) => { +const Event = ({ + id, + title, + location, + start, + end, + color, + thumbnail, + isFavorite, + toggleFavorite, +}: EventProps) => { const timeString = getEventTimeString(start, end); - const { favorite, toggle: toggleFavorite } = useFavorite(id); const { t, i18n } = useTranslation(); dayjs.locale(i18n.language); @@ -105,7 +115,7 @@ const Event = ({ id, title, location, start, end, color, thumbnail }: EventProps process.env.EXPO_PUBLIC_ENVIRONMENT === 'preview') && ( toggleFavorite()} - icon={favorite ? 'heart' : 'heart-outline'} + icon={isFavorite ? 'heart' : 'heart-outline'} style={{ position: 'absolute', top: 0, diff --git a/elements/timetable/EventsBox.tsx b/elements/timetable/EventsBox.tsx index 77d0f8d..5d632ae 100644 --- a/elements/timetable/EventsBox.tsx +++ b/elements/timetable/EventsBox.tsx @@ -4,9 +4,11 @@ import { ScrollView } from 'react-native'; interface EventsBoxProps { events: AssemblyEvent[]; + favorites: number[]; + toggleFavorite: (id: number) => void; } -const EventsBox = ({ events }: EventsBoxProps) => { +const EventsBox = ({ events, favorites, toggleFavorite }: EventsBoxProps) => { return ( { end={event.end} color={event.color} thumbnail={event.thumbnail} + toggleFavorite={() => toggleFavorite(event.id)} + isFavorite={favorites.includes(event.id)} /> ))} diff --git a/elements/timetable/Timetable.tsx b/elements/timetable/Timetable.tsx index 66c7b66..340decd 100644 --- a/elements/timetable/Timetable.tsx +++ b/elements/timetable/Timetable.tsx @@ -1,25 +1,25 @@ import { AssemblyEvent, getEvents } from '@/api/eventService'; import DateSelector from '@/components/timetable/DateSelector'; import EventsBox from '@/elements/timetable/EventsBox'; -import { useEffect, useState } from 'react'; +import { useFavorite } from '@/hooks/useFavorite'; +import { useNavigationPanel } from '@/hooks/useNavigationPanel'; +import { useCallback, useEffect, useState } from 'react'; import { View } from 'react-native'; +import PagerView from 'react-native-pager-view'; import { ActivityIndicator } from 'react-native-paper'; +import Animated from 'react-native-reanimated'; + +const AnimatedPager = Animated.createAnimatedComponent(PagerView); const Timetable = () => { const [events, setEvents] = useState([]); const [eventDayIndex, setEventDayIndex] = useState(0); + const { favorites, toggle: toggleFavorite } = useFavorite(); - const previous = () => { - if (eventDayIndex > 0) { - setEventDayIndex(eventDayIndex - 1); - } - }; - - const next = () => { - if (eventDayIndex < events.length - 1) { - setEventDayIndex(eventDayIndex + 1); - } - }; + const callback = useCallback((position: number) => { + setEventDayIndex(position); + }, []); + const { ref, nextPage, previousPage, onPageSelected } = useNavigationPanel(callback); useEffect(() => { getEvents().then((eventRes) => { @@ -51,28 +51,44 @@ const Timetable = () => { }, []); return ( - + {events.length === 0 ? ( ) : ( - <> + 0} + next={nextPage} + previous={previousPage} /> - - - - + + {events.map((day, index) => ( + + + + ))} + + )} ); diff --git a/hooks/useFavorite.ts b/hooks/useFavorite.ts index c514584..e7be4db 100644 --- a/hooks/useFavorite.ts +++ b/hooks/useFavorite.ts @@ -33,31 +33,26 @@ const saveFavorites = async (favorites: number[]) => { /** * Hook to manage favorite state for a given eventId. * - * @param id - The ID of the event to track favorite state for. - * @returns An object containing the current favorite state and a function to toggle the favorite state. + * @returns An object containing the current favorites and a function to toggle the favorite state by id. */ -export const useFavorite = (id: number) => { - const [favorite, setFavorite] = useState(false); +export const useFavorite = () => { + const [favorites, setFavorites] = useState([]); useEffect(() => { getFavorites().then((favorites) => { - setFavorite(favorites.includes(id)); + setFavorites(favorites); }); - }, [id]); + }, []); - const toggle = async () => { - let favorites = await getFavorites(); - - if (favorite) { - favorites.push(id); + const toggle = async (id: number) => { + if (!favorites.includes(id)) { + setFavorites([...favorites, id]); + await saveFavorites([...favorites, id]); } else { - favorites.filter((f) => f !== id); + setFavorites(favorites.filter((n) => n !== id)); + await saveFavorites(favorites.filter((n) => n !== id)); } - - saveFavorites(favorites); - // Trigger redraw - setFavorite(!favorite); }; - return { favorite, toggle }; + return { favorites, toggle }; }; diff --git a/hooks/useNavigationPanel.ts b/hooks/useNavigationPanel.ts new file mode 100644 index 0000000..240ea6a --- /dev/null +++ b/hooks/useNavigationPanel.ts @@ -0,0 +1,50 @@ +import { useCallback, useMemo, useRef, useState } from 'react'; +import { Animated } from 'react-native'; +import type { + default as PagerView, + PagerViewOnPageSelectedEventData, +} from 'react-native-pager-view'; + +export type UseNavigationPanelProps = ReturnType; + +export function useNavigationPanel(onPageSelectedCallback: (position: number) => void = () => {}) { + const ref = useRef(null); + const [activePage, setActivePage] = useState(0); + const [isAnimated, setIsAnimated] = useState(true); + const onPageSelectedPosition = useRef(new Animated.Value(0)).current; + + const setPage = useCallback( + (page: number) => + isAnimated ? ref.current?.setPage(page) : ref.current?.setPageWithoutAnimation(page), + [isAnimated] + ); + + const nextPage = () => setPage(activePage + 1); + const previousPage = () => setPage(activePage - 1); + + const onPageSelected = useMemo( + () => + Animated.event( + [{ nativeEvent: { position: onPageSelectedPosition } }], + { + listener: ({ nativeEvent: { position } }) => { + setActivePage(position); + onPageSelectedCallback(position); + }, + useNativeDriver: false, + } + ), + [onPageSelectedCallback, onPageSelectedPosition] + ); + + return { + ref, + activePage, + nextPage, + previousPage, + isAnimated, + setIsAnimated, + setPage, + onPageSelected, + }; +} diff --git a/package-lock.json b/package-lock.json index c114a51..38ba39f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "react-i18next": "^14.1.2", "react-native": "0.74.2", "react-native-gesture-handler": "~2.16.1", + "react-native-pager-view": "^6.3.3", "react-native-paper": "^5.12.3", "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "^4.10.1", @@ -15620,6 +15621,15 @@ "react": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-native-pager-view": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.3.3.tgz", + "integrity": "sha512-HViKBlfN/kBJUSu5mRL/V9Bkf1j7uDZozGAjbzh4o9XYo11qVcIK7IwvfzqrkNerVSDy/cAmZcXbcyWnII8xMA==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-paper": { "version": "5.12.3", "license": "MIT", diff --git a/package.json b/package.json index 2e767b8..14ed514 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react-i18next": "^14.1.2", "react-native": "0.74.2", "react-native-gesture-handler": "~2.16.1", + "react-native-pager-view": "^6.3.3", "react-native-paper": "^5.12.3", "react-native-reanimated": "~3.10.1", "react-native-safe-area-context": "^4.10.1",