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 (
+
+ )
+}
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")}
+
+
+ )
+}