diff --git a/src/app/(default)/track/[id]/[[...slug]]/page.tsx b/src/app/(default)/track/[id]/[[...slug]]/page.tsx index 13c6c45..b34f531 100644 --- a/src/app/(default)/track/[id]/[[...slug]]/page.tsx +++ b/src/app/(default)/track/[id]/[[...slug]]/page.tsx @@ -61,7 +61,11 @@ export default async function TrackPage({ params: paramsFromProps }: Props) { } > - + diff --git a/src/app/(default)/tv/[id]/[[...slug]]/page.tsx b/src/app/(default)/tv/[id]/[[...slug]]/page.tsx index e1e367c..3233f6f 100644 --- a/src/app/(default)/tv/[id]/[[...slug]]/page.tsx +++ b/src/app/(default)/tv/[id]/[[...slug]]/page.tsx @@ -157,11 +157,11 @@ export default async function TvSeriesDetailsPage({
} + fallback={} > - + -
+
diff --git a/src/components/Buttons/ActionButtons.tsx b/src/components/Buttons/ActionButtons.tsx index a6e6141..5ff7533 100644 --- a/src/components/Buttons/ActionButtons.tsx +++ b/src/components/Buttons/ActionButtons.tsx @@ -14,16 +14,20 @@ import { } from '@/lib/tmdb'; import { type TvSeries } from '@/types/tv-series'; +import ActionButtonsProvider from './ActionButtonsProvider'; import AddButton from './AddButton'; +import ContextMenuButton from './ContextMenuButton'; import LikeButton from './LikeButton'; import WatchButton from './WatchButton'; export default async function ActionButtons({ id, showWatchButton = true, + showContextMenuButton = true, }: Readonly<{ id: number | string; showWatchButton?: boolean; + showContextMenuButton?: boolean; }>) { const tvSeries = (await cachedTvSeries(id)) as TvSeries; const shouldShowWatchButton = @@ -111,19 +115,28 @@ export default async function ActionButtons({ const isWatchlisted = await isInWatchlist(payload); return ( - <> + {shouldShowWatchButton && } - - - + + + {showContextMenuButton && ( + + )} + ); } return ( - <> + {shouldShowWatchButton && } - + {showContextMenuButton && ( + + )} + ); } diff --git a/src/components/Buttons/ActionButtonsProvider.tsx b/src/components/Buttons/ActionButtonsProvider.tsx new file mode 100644 index 0000000..3f521b4 --- /dev/null +++ b/src/components/Buttons/ActionButtonsProvider.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { + createContext, + useContext, + useState, + type PropsWithChildren, +} from 'react'; + +type State = Readonly<{ + isFavorited: boolean; + isWatchlisted: boolean; +}>; + +const ActionButtonsContext = createContext< + [State, (action: State | ((prevState: State) => State)) => void] | null +>(null); + +export const ActionButtonsProvider = ({ + children, + isFavorited, + isWatchlisted, +}: PropsWithChildren & State) => { + const state = useState({ + isFavorited, + isWatchlisted, + }); + + return ( + + {children} + + ); +}; + +export const useActionButtons = () => { + const context = useContext(ActionButtonsContext); + + if (!context) { + throw new Error( + `useActionButtons must be used within `, + ); + } + + return context; +}; + +export default ActionButtonsProvider; diff --git a/src/components/Buttons/AddButton.tsx b/src/components/Buttons/AddButton.tsx index 4211a3b..97657ff 100644 --- a/src/components/Buttons/AddButton.tsx +++ b/src/components/Buttons/AddButton.tsx @@ -1,23 +1,25 @@ 'use client'; -import { useCallback, useState, useTransition } from 'react'; +import { useCallback, useTransition } from 'react'; import { motion } from 'framer-motion'; +import { useActionButtons } from './ActionButtonsProvider'; import CircleButton from './CircleButton'; export default function AddButton({ - isActive: isActiveFromProps = false, action, }: Readonly<{ - isActive?: boolean; action: (value: boolean, listType: 'favorites' | 'watchlist') => void; }>) { - const [isActive, setIsActive] = useState(isActiveFromProps); + const [{ isWatchlisted }, setState] = useActionButtons(); const [isPending, startTransition] = useTransition(); const handleOnClick = useCallback( (value: boolean) => { - setIsActive(value); + setState((prevState) => ({ + ...prevState, + isWatchlisted: value, + })); startTransition(async () => { try { await action(value, 'watchlist'); @@ -26,21 +28,21 @@ export default function AddButton({ } }); }, - [action], + [action, setState], ); return ( - {isActive ? ( + {isWatchlisted ? ( ; className?: string; children: React.ReactNode; onClick?: ( @@ -49,10 +53,11 @@ export default function CircleButton({ return ( diff --git a/src/components/Buttons/ContextMenuButton.tsx b/src/components/Buttons/ContextMenuButton.tsx index 14de23b..8f1777b 100644 --- a/src/components/Buttons/ContextMenuButton.tsx +++ b/src/components/Buttons/ContextMenuButton.tsx @@ -1,36 +1,66 @@ 'use client'; -import { useCallback, useState, useTransition } from 'react'; +import { useCallback, useRef, useState, useTransition } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import Link from 'next/link'; +import { type TvSeries } from '@/types/tv-series'; + +import { useActionButtons } from './ActionButtonsProvider'; import CircleButton from './CircleButton'; +import DropdownContainer from '../Dropdown/DropdownContainer'; export default function ContextMenuButton({ + action, className, - isActive: isActiveFromProps = false, + tvSeries, }: Readonly<{ + action: (value: boolean, listType: 'favorites' | 'watchlist') => void; className?: string; - isActive?: boolean; + isOpen?: boolean; + tvSeries: TvSeries; }>) { - const [isActive, setIsActive] = useState(isActiveFromProps); + const [{ isFavorited, isWatchlisted }, setState] = useActionButtons(); + const triggerRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); const [isPending, startTransition] = useTransition(); - const handleOnClick = useCallback((value: boolean) => { - setIsActive(value); + const handleOnClick = useCallback(() => { + setIsOpen((prev) => !prev); }, []); + const handleActionClick = useCallback( + (value: boolean, listType: 'favorites' | 'watchlist') => { + setState((prevState) => ({ + ...prevState, + isFavorited: listType === 'favorites' ? value : prevState.isFavorited, + isWatchlisted: + listType === 'watchlist' ? value : prevState.isWatchlisted, + })); + + startTransition(async () => { + try { + await action(value, listType); + } catch (error) { + console.error(error); + } + }); + }, + [action, setState], + ); + return (
- {isActive && ( - <> + {isOpen && ( + setIsOpen(false)} + variants={{ + visible: { + opacity: 1, + y: 0, + }, + hidden: { + opacity: 0, + y: 0, + }, + }} + > setIsActive(false)} - /> - - + + + + + Edit watch status + + + - + )}
diff --git a/src/components/Buttons/LikeButton.tsx b/src/components/Buttons/LikeButton.tsx index 83b9fcd..6d15819 100644 --- a/src/components/Buttons/LikeButton.tsx +++ b/src/components/Buttons/LikeButton.tsx @@ -1,21 +1,24 @@ 'use client'; -import { useCallback, useState, useTransition } from 'react'; +import { useCallback, useTransition } from 'react'; +import { useActionButtons } from './ActionButtonsProvider'; import CircleButton from './CircleButton'; export default function LikeButton({ - isActive: isActiveFromProps = false, action, }: Readonly<{ - isActive?: boolean; action: (value: boolean, listType: 'favorites' | 'watchlist') => void; }>) { - const [isActive, setIsActive] = useState(isActiveFromProps); + const [{ isFavorited }, setState] = useActionButtons(); const [isPending, startTransition] = useTransition(); const handleOnClick = useCallback( (value: boolean) => { - setIsActive(value); + setState((prevState) => ({ + ...prevState, + isFavorited: value, + })); + startTransition(async () => { try { await action(value, 'favorites'); @@ -24,17 +27,17 @@ export default function LikeButton({ } }); }, - [action], + [action, setState], ); return ( ; -const variants = { - visible: { - opacity: 1, - y: 0, - }, - hidden: { - opacity: 0, - y: 40, - }, -}; - const calculateAlignedPosition = ( alignment: Alignment, start: number, @@ -141,6 +130,7 @@ type Props = Readonly<{ shouldRenderOverlay?: boolean; shouldRenderInModal?: boolean; viewportOffset?: number; + variants?: Variants; }>; export default function DropdownContainer({ @@ -152,6 +142,16 @@ export default function DropdownContainer({ shouldRenderOverlay = true, shouldRenderInModal = true, viewportOffset = 16, + variants = { + visible: { + opacity: 1, + y: 0, + }, + hidden: { + opacity: 0, + y: 40, + }, + }, }: Props) { const containerRef = useRef(null); const [isVisible, setIsVisible] = useState(false); @@ -244,7 +244,14 @@ export default function DropdownContainer({ ); - }, [children, isVisible, onOutsideClick, shouldRenderOverlay, reposition]); + }, [ + shouldRenderOverlay, + onOutsideClick, + isVisible, + variants, + children, + reposition, + ]); if (shouldRenderInModal) { return {renderContent()}; diff --git a/src/components/List/MostPopularThisMonth.tsx b/src/components/List/MostPopularThisMonth.tsx index 6b99302..ef60f78 100644 --- a/src/components/List/MostPopularThisMonth.tsx +++ b/src/components/List/MostPopularThisMonth.tsx @@ -26,7 +26,7 @@ export default async function MostPopularThisMonthList({ return ( diff --git a/src/components/Track/EpisodesSkeleton.tsx b/src/components/Track/EpisodesSkeleton.tsx index 20b846e..5092398 100644 --- a/src/components/Track/EpisodesSkeleton.tsx +++ b/src/components/Track/EpisodesSkeleton.tsx @@ -18,27 +18,27 @@ export default function EpisodesSkeleton() {
-
+
-
+
-
+
-
+
-
+