diff --git a/src/features/playlists/components/playlist-detail.tsx b/src/features/playlists/components/playlist-detail.tsx new file mode 100644 index 0000000..9c55adc --- /dev/null +++ b/src/features/playlists/components/playlist-detail.tsx @@ -0,0 +1,133 @@ +import { GameCard } from "@/features/games/components/game-card" +import { Button } from "@/shared/components/ui/button" +import { Input } from "@/shared/components/ui/input" +import { t } from "@/shared/i18n" +import { useNavigate } from "@tanstack/react-router" +import { Plus, Trash2 } from "lucide-react" +import { useState } from "react" +import { usePlaylistDetail } from "../hooks/use-playlist-detail" + +interface PlaylistDetailProps { + playlistId: string +} + +export function PlaylistDetail({ playlistId }: PlaylistDetailProps) { + const { + playlist, + games, + loading, + reload, + searchText, + setSearchText, + searchResults, + addGame, + removeGame, + rename, + deletePlaylist, + } = usePlaylistDetail(playlistId) + const navigate = useNavigate() + const [editingName, setEditingName] = useState(false) + const [name, setName] = useState("") + + if (loading || !playlist) return null + + const isCustom = !playlist.is_static + + const handleStartRename = () => { + setName(playlist.name) + setEditingName(true) + } + + const handleFinishRename = () => { + if (name.trim() && name !== playlist.name) { + rename(name.trim()) + } + setEditingName(false) + } + + const handleDelete = async () => { + if (!window.confirm(`Delete "${playlist.name}"?`)) return + await deletePlaylist() + navigate({ to: "/playlists" }) + } + + return ( +
+
+ {editingName ? ( + setName(e.target.value)} + onBlur={handleFinishRename} + onKeyDown={(e) => e.key === "Enter" && handleFinishRename()} + autoFocus + className="text-xl font-bold" + /> + ) : ( +

+ {playlist.name} +

+ )} + {isCustom && ( + + )} +
+ +
+ setSearchText(e.target.value)} + placeholder={t("playlists.addGames")} + /> + {searchResults.length > 0 && ( +
+ {searchResults.map((game) => ( + + ))} +
+ )} +
+ + {games.length === 0 ? ( +

{t("playlists.noGames")}

+ ) : ( +
+ {games.map((game) => ( +
+
+ +
+ +
+ ))} +
+ )} +
+ ) +} diff --git a/src/features/playlists/components/playlists-list.tsx b/src/features/playlists/components/playlists-list.tsx new file mode 100644 index 0000000..47ceaf4 --- /dev/null +++ b/src/features/playlists/components/playlists-list.tsx @@ -0,0 +1,76 @@ +import { Button } from "@/shared/components/ui/button" +import { Card } from "@/shared/components/ui/card" +import { usePlaylistMutations, usePlaylists } from "@/shared/db/hooks" +import { t } from "@/shared/i18n" +import { Link } from "@tanstack/react-router" +import { Heart, ListMusic, ThumbsDown, ThumbsUp, Trash2 } from "lucide-react" + +const staticIcons: Record> = { + favorites: Heart, + "want-to-play": ThumbsUp, + "not-interesting": ThumbsDown, +} + +export function PlaylistsList() { + const { playlists, reload } = usePlaylists() + const { createPlaylist, deletePlaylist } = usePlaylistMutations() + + const handleCreate = async () => { + await createPlaylist("New Playlist") + reload() + } + + const handleDelete = async (id: string) => { + await deletePlaylist(id) + reload() + } + + const staticPlaylists = playlists.filter((p) => p.is_static) + const customPlaylists = playlists.filter((p) => !p.is_static) + + return ( +
+
+ {staticPlaylists.map((p) => { + const Icon = staticIcons[p.id] ?? ListMusic + return ( + + +
+ + {p.name} +
+ {p.game_count} +
+ + ) + })} +
+ + {customPlaylists.length > 0 && ( +
+ {customPlaylists.map((p) => ( +
+ + +
+ + {p.name} +
+ {p.game_count} +
+ + +
+ ))} +
+ )} + + +
+ ) +} diff --git a/src/features/playlists/hooks/use-playlist-detail.ts b/src/features/playlists/hooks/use-playlist-detail.ts new file mode 100644 index 0000000..84e6dcc --- /dev/null +++ b/src/features/playlists/hooks/use-playlist-detail.ts @@ -0,0 +1,62 @@ +import { useGames, usePlaylist, usePlaylistMutations } from "@/shared/db/hooks" +import type { Game } from "@/shared/db/schema" +import { useCallback, useMemo, useState } from "react" + +export function usePlaylistDetail(id: string) { + const { playlist, games, loading, reload } = usePlaylist(id) + const { addGame, removeGame, renamePlaylist, deletePlaylist } = usePlaylistMutations() + const { games: allGames } = useGames() + const [searchText, setSearchText] = useState("") + + const gameIds = useMemo(() => new Set(games.map((g) => g.id)), [games]) + + const searchResults = useMemo(() => { + if (!searchText) return [] + const q = searchText.toLowerCase() + return allGames + .filter((g) => !gameIds.has(g.id) && g.title.toLowerCase().includes(q)) + .slice(0, 20) + }, [allGames, gameIds, searchText]) + + const handleAddGame = useCallback( + async (game: Game) => { + await addGame(id, game.id) + reload() + }, + [id, addGame, reload], + ) + + const handleRemoveGame = useCallback( + async (gameId: string) => { + await removeGame(id, gameId) + reload() + }, + [id, removeGame, reload], + ) + + const handleRename = useCallback( + async (name: string) => { + await renamePlaylist(id, name) + reload() + }, + [id, renamePlaylist, reload], + ) + + const handleDelete = useCallback(async () => { + await deletePlaylist(id) + }, [id, deletePlaylist]) + + return { + playlist, + games, + loading, + reload, + searchText, + setSearchText, + searchResults, + addGame: handleAddGame, + removeGame: handleRemoveGame, + rename: handleRename, + deletePlaylist: handleDelete, + } +} diff --git a/src/features/playlists/hooks/use-playlists.ts b/src/features/playlists/hooks/use-playlists.ts new file mode 100644 index 0000000..3104a83 --- /dev/null +++ b/src/features/playlists/hooks/use-playlists.ts @@ -0,0 +1 @@ +export { usePlaylists } from "@/shared/db/hooks" diff --git a/src/routes/playlists/$playlistId.tsx b/src/routes/playlists/$playlistId.tsx index b15199d..496ceca 100644 --- a/src/routes/playlists/$playlistId.tsx +++ b/src/routes/playlists/$playlistId.tsx @@ -1,5 +1,21 @@ -import { createFileRoute } from "@tanstack/react-router" +import { PlaylistDetail } from "@/features/playlists/components/playlist-detail" +import { Link, createFileRoute } from "@tanstack/react-router" +import { ArrowLeft } from "lucide-react" export const Route = createFileRoute("/playlists/$playlistId")({ - component: () =>
Playlist Detail
, + component: PlaylistDetailPage, }) + +function PlaylistDetailPage() { + const { playlistId } = Route.useParams() + + return ( +
+ + + Back + + +
+ ) +} diff --git a/src/routes/playlists/index.tsx b/src/routes/playlists/index.tsx index 3788e09..d003539 100644 --- a/src/routes/playlists/index.tsx +++ b/src/routes/playlists/index.tsx @@ -1,5 +1,16 @@ +import { PlaylistsList } from "@/features/playlists/components/playlists-list" +import { t } from "@/shared/i18n" import { createFileRoute } from "@tanstack/react-router" export const Route = createFileRoute("/playlists/")({ - component: () =>
Playlists
, + component: PlaylistsPage, }) + +function PlaylistsPage() { + return ( +
+

{t("playlists.title")}

+ +
+ ) +}