From 57060bbaf77beda5f36888c8e956bdb013deaac6 Mon Sep 17 00:00:00 2001 From: Juan Alvarado Date: Sun, 15 Sep 2024 23:59:16 -0400 Subject: [PATCH] Select current episode --- .../EpisodesScreen/EpisodeList/index.tsx | 48 +++++++++++-------- src/client/EpisodesScreen/EpisodesScreen.tsx | 29 +++++++---- .../EpisodesScreen/Player/PlayerControls.tsx | 22 ++++++--- src/client/EpisodesScreen/Player/index.tsx | 2 +- src/pages/index.tsx | 25 ++++++++-- 5 files changed, 85 insertions(+), 41 deletions(-) diff --git a/src/client/EpisodesScreen/EpisodeList/index.tsx b/src/client/EpisodesScreen/EpisodeList/index.tsx index 66a9f73..901f83e 100644 --- a/src/client/EpisodesScreen/EpisodeList/index.tsx +++ b/src/client/EpisodesScreen/EpisodeList/index.tsx @@ -1,29 +1,37 @@ -import React, { useRef, useEffect } from "react"; +import React, { useRef, useImperativeHandle } from "react"; type EpisodeListProps = { - focusedEpisodeId?: string; children: React.ReactElement; }; -export function EpisodeList({ focusedEpisodeId, children }: EpisodeListProps) { +export type EpisodeListHandle = { + focusEpisode: (episodeId: string) => void; +}; + +export const EpisodeList = React.forwardRef< + EpisodeListHandle, + EpisodeListProps +>(({ children }: EpisodeListProps, ref) => { const episodeListRef = useRef(null); - useEffect(() => { - if (focusedEpisodeId) { - const episode = episodeListRef.current?.querySelector( - `[data-episode-id="${focusedEpisodeId}"]` - ); - if (episode) { - episode.scrollIntoView({ - block: "center", - }); - } else { - episodeListRef.current?.scrollTo({ - top: 0, - }); - } - } - }, [focusedEpisodeId]); + useImperativeHandle( + ref, + () => { + return { + focusEpisode(episodeId: string) { + const episode = episodeListRef.current?.querySelector( + `[data-episode-id="${episodeId}"]`, + ); + if (episode) { + episode.scrollIntoView({ + block: "center", + }); + } + }, + }; + }, + [], + ); return (
); -} +}); diff --git a/src/client/EpisodesScreen/EpisodesScreen.tsx b/src/client/EpisodesScreen/EpisodesScreen.tsx index 0621406..8eb86db 100644 --- a/src/client/EpisodesScreen/EpisodesScreen.tsx +++ b/src/client/EpisodesScreen/EpisodesScreen.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useMemo, useState, useTransition } from "react"; +import React, { + useContext, + useEffect, + useMemo, + useState, + useTransition, +} from "react"; import Player, { USE_NEW_PLAYER } from "./Player"; import { ShuffleButton } from "../components/ShuffleButton"; import EpisodeListSpinner from "./EpisodeList/EpisodeListSpinner"; @@ -36,6 +42,7 @@ import { IconSearch } from "../components/Icons"; import { cn } from "@/lib/utils"; import { useCollectiveSelectStore, useNavbarStore } from "./Navbar"; import { EpisodeProjection } from "@/server/router"; +import { EpisodeListContext } from "@/pages"; type Props = { searchText: string; @@ -43,10 +50,10 @@ type Props = { export function EpisodesScreen({ searchText }: Props) { const [selectedSection, setSelectedSection] = useState<"all" | "favorites">( - "all" + "all", ); const [activeSection, setActiveSection] = useState<"all" | "favorites">( - "all" + "all", ); const [isPending, startTransition] = useTransition(); @@ -56,9 +63,11 @@ export function EpisodesScreen({ searchText }: Props) { const { data: episodes, error } = useEpisodes(); const loadPersistedCollective = useCollectiveSelectStore( - (s) => s.loadPersisted + (s) => s.loadPersisted, ); + const { ref: episodeListRef } = useContext(EpisodeListContext); + useEffect(() => { loadPersistedCollective(); }, []); @@ -72,7 +81,7 @@ export function EpisodesScreen({ searchText }: Props) { const { addFavorite, removeFavorite } = useFavorites(); const setContextMenuEpisode = useEpisodeOptionsStore( - (state) => state.setEpisode + (state) => state.setEpisode, ); const favoritesCount = useFavoritesCount(); const isFavoriteFast = useIsFavoriteFast(); @@ -111,7 +120,7 @@ export function EpisodesScreen({ searchText }: Props) { const lowerCaseSearch = searchText.toLowerCase(); return eps.filter((episode) => - episode.name.toLowerCase().includes(lowerCaseSearch) + episode.name.toLowerCase().includes(lowerCaseSearch), ); } @@ -145,10 +154,10 @@ export function EpisodesScreen({ searchText }: Props) {
- + <> @@ -310,7 +319,7 @@ function setNavigatorMediaMetadata(episode: ReturnType) { navigator.mediaSession.metadata = new MediaMetadata({ title: episode.name, artist: `${prefixMap[episode.collectiveSlug]} ${formatDate( - episode.releasedAt + episode.releasedAt, )}`, artwork: [ { diff --git a/src/client/EpisodesScreen/Player/PlayerControls.tsx b/src/client/EpisodesScreen/Player/PlayerControls.tsx index 0c61f96..25f18c5 100644 --- a/src/client/EpisodesScreen/Player/PlayerControls.tsx +++ b/src/client/EpisodesScreen/Player/PlayerControls.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { formatDate, formatTime } from "../../helpers"; import { IconPause, @@ -12,6 +12,7 @@ import cx from "classnames"; import { Slider } from "@/client/components/Slider"; import { motion } from "framer-motion"; import { EpisodeProjection } from "@/server/router"; +import { EpisodeListContext } from "@/pages"; export type PlayerControlsProps = { episode: EpisodeProjection; @@ -52,6 +53,8 @@ export function PlayerControls({ const scrubberProgress = seeking ? seekPosition : progress; + const { focusEpisode } = useContext(EpisodeListContext); + return (
@@ -63,7 +66,12 @@ export function PlayerControls({ />
-
{episode.name}
+
{formatDate(episode.releasedAt)}
@@ -80,7 +88,7 @@ export function PlayerControls({ "rounded-full bg-transparent p-2 text-gray-700", "transition-all duration-200 ease-in-out", "hover:text-gray-900", - "focus:bg-gray-200 focus:outline-none" + "focus:bg-gray-200 focus:outline-none", )} > @@ -95,7 +103,7 @@ export function PlayerControls({ "transition-all duration-200 ease-in-out", "hover:bg-accent/90 hover:shadow-lg", "focus:bg-accent/90 focus:outline-none", - loading && "cursor-not-allowed disabled:cursor-not-allowed" + loading && "cursor-not-allowed disabled:cursor-not-allowed", )} > {loading ? ( @@ -135,7 +143,7 @@ export function PlayerControls({ "rounded-full bg-transparent p-2 text-gray-700", "transition-all duration-200 ease-in-out", "hover:text-gray-900", - "focus:bg-gray-200 focus:outline-none" + "focus:bg-gray-200 focus:outline-none", )} > @@ -179,7 +187,7 @@ export function PlayerControls({ className={cx( "inline-block rounded-full p-2", "transition-all duration-200 ease-in-out", - "hover:bg-gray-200 " + "hover:bg-gray-200 ", )} title="Open in SoundCloud" target="_blank" @@ -194,7 +202,7 @@ export function PlayerControls({ "inline-block rounded-full p-1", "transition-all duration-200 ease-in-out", "hover:bg-gray-200", - "focus:outline-none" + "focus:outline-none", )} title={muted ? "Unmute" : "Mute"} onClick={() => { diff --git a/src/client/EpisodesScreen/Player/index.tsx b/src/client/EpisodesScreen/Player/index.tsx index 0729a81..998424b 100644 --- a/src/client/EpisodesScreen/Player/index.tsx +++ b/src/client/EpisodesScreen/Player/index.tsx @@ -70,7 +70,7 @@ export default function Player({ currentEpisodeId }: PlayerProps) { className={classNames( "bg-white", !showMini && "w-full border border-t-gray-200 bg-white px-3 py-3", - showMini && "h-0" + showMini && "h-0", )} > {currentEpisode.source === "MIXCLOUD" && ( diff --git a/src/pages/index.tsx b/src/pages/index.tsx index d49a699..1803ab1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,21 +1,40 @@ -import { useState } from "react"; +import { RefObject, createContext, useRef, useState } from "react"; import { EpisodesScreen } from "@/client/EpisodesScreen/EpisodesScreen"; import { useShortcutHandlers } from "@/client/useKeyboardHandlers"; import Navbar from "@/client/EpisodesScreen/Navbar"; import "react-spring-bottom-sheet/dist/style.css"; import { EpisodeOptionsModal } from "../client/EpisodesScreen/EpisodeOptionsModal"; import { motion, useScroll, useTransform } from "framer-motion"; +import { EpisodeListHandle } from "@/client/EpisodesScreen/EpisodeList"; + +export type EpisodeListContext = { + ref: RefObject; + focusEpisode: (episodeId: string) => void; +}; + +export const EpisodeListContext = createContext( + null as unknown as EpisodeListContext, +); export default function Home() { const [searchText, setSearchText] = useState(""); + const episodeListContextRef = useRef(null); + const focusEpisode = (episodeId: string) => { + if (episodeListContextRef.current) { + episodeListContextRef.current.focusEpisode(episodeId); + } + }; + useShortcutHandlers(); const { scrollY } = useScroll(); const opacity = useTransform(scrollY, [0, 20], [0, 1]); return ( - <> +
- + ); }