Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ux): timetable swipe and animation #19

Merged
merged 8 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions components/timetable/Event.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useFavorite } from '@/hooks/useFavorite';
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import { Image, View } from 'react-native';
Expand All @@ -12,6 +11,8 @@ interface EventProps {
end: Date;
color: string;
thumbnail: string;
isFavorite: boolean;
toggleFavorite: () => void;
}

const getEventTimeString = (start: Date, end: Date) => {
Expand All @@ -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);

Expand Down Expand Up @@ -105,7 +115,7 @@ const Event = ({ id, title, location, start, end, color, thumbnail }: EventProps
process.env.EXPO_PUBLIC_ENVIRONMENT === 'preview') && (
<IconButton
onPress={() => toggleFavorite()}
icon={favorite ? 'heart' : 'heart-outline'}
icon={isFavorite ? 'heart' : 'heart-outline'}
style={{
position: 'absolute',
top: 0,
Expand Down
6 changes: 5 additions & 1 deletion elements/timetable/EventsBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ScrollView
style={{ paddingHorizontal: 30, paddingBottom: 8 }}
Expand All @@ -22,6 +24,8 @@ const EventsBox = ({ events }: EventsBoxProps) => {
end={event.end}
color={event.color}
thumbnail={event.thumbnail}
toggleFavorite={() => toggleFavorite(event.id)}
isFavorite={favorites.includes(event.id)}
/>
))}
</ScrollView>
Expand Down
68 changes: 42 additions & 26 deletions elements/timetable/Timetable.tsx
Original file line number Diff line number Diff line change
@@ -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<AssemblyEvent[][]>([]);
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) => {
Expand Down Expand Up @@ -51,28 +51,44 @@ const Timetable = () => {
}, []);

return (
<View
style={{
display: 'flex',
gap: 16,
flex: 1,
}}
>
<View style={{ flex: 1 }}>
{events.length === 0 ? (
<ActivityIndicator animating />
) : (
<>
<View
style={{
display: 'flex',
gap: 16,
flex: 1,
}}
>
<DateSelector
date={events[eventDayIndex][0].start}
next={next}
previous={previous}
nextVisible={eventDayIndex < events.length - 1}
previousVisible={eventDayIndex > 0}
next={nextPage}
previous={previousPage}
/>
<View style={{ flex: 1 }}>
<EventsBox events={events[eventDayIndex] ?? []} />
</View>
</>
<AnimatedPager
ref={ref}
useNext={false}
initialPage={eventDayIndex}
onPageSelected={onPageSelected}
layoutDirection='ltr'
orientation='horizontal'
style={{ flex: 1 }}
>
{events.map((day, index) => (
<View collapsable={false} key={index}>
<EventsBox
events={day ?? []}
favorites={favorites}
toggleFavorite={toggleFavorite}
/>
</View>
))}
</AnimatedPager>
</View>
)}
</View>
);
Expand Down
29 changes: 12 additions & 17 deletions hooks/useFavorite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number[]>([]);

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 };
};
50 changes: 50 additions & 0 deletions hooks/useNavigationPanel.ts
Original file line number Diff line number Diff line change
@@ -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<typeof useNavigationPanel>;

export function useNavigationPanel(onPageSelectedCallback: (position: number) => void = () => {}) {
const ref = useRef<PagerView>(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<PagerViewOnPageSelectedEventData>(
[{ nativeEvent: { position: onPageSelectedPosition } }],
{
listener: ({ nativeEvent: { position } }) => {
setActivePage(position);
onPageSelectedCallback(position);
},
useNativeDriver: false,
}
),
[onPageSelectedCallback, onPageSelectedPosition]
);

return {
ref,
activePage,
nextPage,
previousPage,
isAnimated,
setIsAnimated,
setPage,
onPageSelected,
};
}
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading