Skip to content

Commit c2d4719

Browse files
committed
feat: functional library page
1 parent 597346a commit c2d4719

File tree

17 files changed

+765
-550
lines changed

17 files changed

+765
-550
lines changed

apps/web/src/components/settings/setting-group.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ interface SettingGroupProps {
99
export function SettingGroup({ group }: SettingGroupProps) {
1010
return (
1111
<section className="space-y-3">
12-
<div className="flex items-center justify-between">
12+
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
1313
<div>
14-
<h2 className="text-lg font-semibold">{group.title}</h2>
14+
<h2 className="text-base md:text-lg font-semibold">{group.title}</h2>
1515
{group.description && (
1616
<p className="text-xs text-white/50 mt-0.5">{group.description}</p>
1717
)}
@@ -21,6 +21,7 @@ export function SettingGroup({ group }: SettingGroupProps) {
2121
variant={group.headerAction.variant || "default"}
2222
size="sm"
2323
onClick={group.headerAction.onClick}
24+
className="w-full md:w-auto"
2425
>
2526
{group.headerAction.icon && (
2627
<group.headerAction.icon className="w-4 h-4 mr-2" />

apps/web/src/components/settings/setting-item.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,40 @@ export function SettingItem({ item }: SettingItemProps) {
1616
const itemType = item.type || (hasActions ? "actions" : "display");
1717

1818
return (
19-
<li className="min-h-14 px-6 py-3 flex items-center justify-between transition-colors">
20-
<div className="flex items-center gap-4 flex-1">
19+
<li className="min-h-14 px-4 md:px-6 py-4 md:py-3 flex flex-col md:flex-row md:items-center md:justify-between gap-0 md:gap-0 transition-colors">
20+
<div className="flex items-start gap-3 md:gap-4 flex-1 min-w-0 pb-3 md:pb-0">
2121
{hasIcon && (
2222
<div
23-
className={`w-8 h-8 rounded flex items-center justify-center ${
23+
className={`w-8 h-8 flex-shrink-0 rounded flex items-center justify-center ${
2424
item.iconBgColor || "bg-white/10"
2525
}`}
2626
>
2727
<span className="text-xs font-bold">{item.icon}</span>
2828
</div>
2929
)}
30-
<div className="flex-1">
31-
<span className="font-medium text-sm">{item.label}</span>
30+
<div className="flex-1 min-w-0">
31+
<span className="font-medium text-sm block">{item.label}</span>
3232
{item.description && (
33-
<p className="text-xs text-white/50">{item.description}</p>
33+
<p className="text-xs text-white/50 mt-0.5">{item.description}</p>
3434
)}
3535
</div>
36+
{/* Status badge - positioned right on mobile, stays in flow on desktop */}
37+
{item.status && (
38+
<Badge className={`${item.statusColor || "text-black"} md:hidden`}>
39+
{item.status}
40+
</Badge>
41+
)}
3642
</div>
3743

38-
<div className="flex items-center gap-3">
39-
{/* Status badge */}
44+
{/* Divider on mobile */}
45+
<div className="border-t border-white/10 md:hidden mb-3"></div>
46+
47+
<div className="flex items-center gap-3 md:flex-shrink-0 justify-end md:justify-start">
48+
{/* Status badge - desktop only, moved to actions area */}
4049
{item.status && (
41-
<Badge className={item.statusColor || "text-black"}>
50+
<Badge
51+
className={`hidden md:inline-flex ${item.statusColor || "text-black"}`}
52+
>
4253
{item.status}
4354
</Badge>
4455
)}
@@ -62,7 +73,7 @@ export function SettingItem({ item }: SettingItemProps) {
6273

6374
{/* Slider */}
6475
{itemType === "slider" && item.slider && (
65-
<div className="flex items-center gap-3 min-w-[200px]">
76+
<div className="flex items-center gap-3 w-full md:min-w-[200px] md:w-auto justify-end md:justify-start">
6677
<Slider
6778
min={item.slider.min}
6879
max={item.slider.max}
@@ -71,7 +82,7 @@ export function SettingItem({ item }: SettingItemProps) {
7182
onValueChange={(values: number[]) =>
7283
item.slider!.onChange(values[0])
7384
}
74-
className="flex-1"
85+
className="flex-1 max-w-[200px] md:max-w-none"
7586
/>
7687
<span className="text-xs font-mono text-white/60 min-w-[3rem] text-right">
7788
{item.slider.value}
@@ -88,7 +99,7 @@ export function SettingItem({ item }: SettingItemProps) {
8899
key={index}
89100
variant={action.variant || "ghost"}
90101
size="icon"
91-
className="h-8 w-8 rounded-full"
102+
className="h-9 w-9 md:h-8 md:w-8 rounded-full flex-shrink-0"
92103
onClick={action.onClick}
93104
title={action.label}
94105
disabled={action.disabled}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Link, useLocation } from "@tanstack/react-router";
2+
import { motion } from "motion/react";
3+
import { Home, LibraryIcon, Settings } from "lucide-react";
4+
import { useMemo } from "react";
5+
6+
interface NavItem {
7+
id: string;
8+
label: string;
9+
href: string;
10+
icon: React.ReactNode;
11+
}
12+
13+
const navItems: NavItem[] = [
14+
{
15+
id: "home",
16+
label: "Home",
17+
href: "/",
18+
icon: <Home className="w-5 h-5" />,
19+
},
20+
{
21+
id: "library",
22+
label: "Library",
23+
href: "/library",
24+
icon: <LibraryIcon className="w-5 h-5" />,
25+
},
26+
{
27+
id: "settings",
28+
label: "Settings",
29+
href: "/settings",
30+
icon: <Settings className="w-5 h-5" />,
31+
},
32+
];
33+
34+
const BottomNav = () => {
35+
const location = useLocation();
36+
37+
const activeTab = useMemo(() => {
38+
const path = location.pathname;
39+
if (path === "/") return "home";
40+
if (path.startsWith("/library")) return "library";
41+
if (path.startsWith("/settings")) return "settings";
42+
return "home";
43+
}, [location.pathname]);
44+
45+
return (
46+
<div className="fixed bottom-0 left-0 right-0 z-50 md:hidden">
47+
<div className="p-4">
48+
<div className="bg-neutral-900/60 backdrop-blur-lg border border-white/10 rounded-[50px] p-1">
49+
<div className="grid grid-cols-3">
50+
{navItems.map((item) => (
51+
<Link
52+
key={item.id}
53+
to={item.href}
54+
className={`${
55+
activeTab === item.id ? "" : "hover:text-white/60"
56+
} relative flex flex-col items-center justify-center gap-1 text-white transition-colors h-14 rounded-[20px]`}
57+
style={{
58+
WebkitTapHighlightColor: "transparent",
59+
}}
60+
>
61+
{activeTab === item.id && (
62+
<motion.span
63+
layoutId="bottom-nav-bubble"
64+
className="absolute inset-0 z-10 bg-white mix-blend-difference rounded-[50px]"
65+
transition={{
66+
type: "spring",
67+
bounce: 0.2,
68+
duration: 0.6,
69+
}}
70+
/>
71+
)}
72+
<span className="relative">{item.icon}</span>
73+
<span className="relative text-xs font-medium">
74+
{item.label}
75+
</span>
76+
</Link>
77+
))}
78+
</div>
79+
</div>
80+
</div>
81+
</div>
82+
);
83+
};
84+
85+
export default BottomNav;

apps/web/src/components/ui/card.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const Card = forwardRef<
1414
<img
1515
src={image !== "" ? image : "/placeholder.png"}
1616
alt={title}
17-
className="w-full group-hover:scale-105 group-hover:ring-2 group-hover:ring-offset-2 group-hover:ring-offset-black group-hover:ring-white/20 aspect-video object-cover group-hover:rounded-xl rounded-lg shadow-[0_4px_12px_rgba(0,0,0,0.3)] transition-all duration-200 ease-out hover:shadow-[0_20px_40px_rgba(0,0,0,0.5)]"
17+
className="w-full lg:group-hover:scale-105 group-hover:ring-2 group-hover:ring-offset-2 group-hover:ring-offset-black group-hover:ring-white/20 aspect-video object-cover group-hover:rounded-xl rounded-lg shadow-[0_4px_12px_rgba(0,0,0,0.3)] transition-all duration-200 ease-out hover:shadow-[0_20px_40px_rgba(0,0,0,0.5)]"
1818
/>
1919
<div className="mt-2 group-hover:translate-y-2 ease-out opacity-90 transition-all duration-200">
2020
<h2 className="text-sm font-semibold text-[#f5f5f7] m-0 leading-tight tracking-tight">

apps/web/src/components/ui/header/header-offline.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const HeaderOffline = () => {
5252
});
5353

5454
return (
55-
<div className="w-full fixed top-0 left-0 right-0 z-50">
55+
<div className="hidden md:block w-full fixed top-0 left-0 right-0 z-50">
5656
<nav className="w-fit mx-auto space-y-4 p-4">
5757
<motion.div
5858
layout="preserve-aspect"

apps/web/src/components/ui/header/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ const Header = () => {
123123
}
124124

125125
return (
126-
<div className="w-full fixed top-0 left-0 right-0 z-50">
126+
<div className="hidden md:block w-full fixed top-0 left-0 right-0 z-50">
127127
<nav className="w-fit mx-auto space-y-4 p-4">
128128
<motion.div
129129
layout="position"

apps/web/src/lib/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./useMovies";
22
export * from "./useTVShows";
33
export * from "./useSearch";
4+
export * from "./useMedia";

apps/web/src/lib/hooks/useMedia.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { getApiV1Media, type GetApiV1MediaParams } from "@dester/api-client";
3+
import "@/lib/api-client"; // Import to ensure client is configured
4+
5+
const emptyMediaData = {
6+
media: [],
7+
pagination: { total: 0, limit: 50, offset: 0, hasMore: false },
8+
};
9+
10+
export const useMedia = (filters?: GetApiV1MediaParams) => {
11+
return useQuery({
12+
queryKey: ["media", filters],
13+
queryFn: async () => {
14+
try {
15+
const response = await getApiV1Media(filters);
16+
if (response.status === 200) {
17+
return response.data.data ?? emptyMediaData;
18+
}
19+
return emptyMediaData;
20+
} catch (error) {
21+
// If offline, let React Query handle it with cached data
22+
if (error instanceof Error && !error.message.includes("fetch")) {
23+
console.error("Error fetching media:", error);
24+
}
25+
throw error;
26+
}
27+
},
28+
placeholderData: emptyMediaData,
29+
});
30+
};
31+
32+
export const useMediaById = (id: string) => {
33+
return useQuery({
34+
queryKey: ["media", id],
35+
queryFn: async () => {
36+
try {
37+
const response = await getApiV1Media({ search: id });
38+
if (response.status === 200 && response.data.data?.media) {
39+
return response.data.data.media[0] ?? null;
40+
}
41+
return null;
42+
} catch (error) {
43+
if (error instanceof Error && !error.message.includes("fetch")) {
44+
console.error("Error fetching media:", error);
45+
}
46+
throw error;
47+
}
48+
},
49+
enabled: !!id,
50+
placeholderData: null,
51+
});
52+
};

apps/web/src/routes/__root.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import { createRootRoute, Outlet, useLocation } from "@tanstack/react-router";
22
import Header from "@/components/ui/header";
3+
import BottomNav from "@/components/ui/bottom-nav";
34
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
45

56
const RootLayout = () => {
67
const location = useLocation();
78

8-
// Hide header on authentication pages
9+
// Hide header and bottom nav on authentication pages
910
const authPages = ["/login", "/register", "/setup"];
1011
const isAuthPage = authPages.includes(location.pathname);
1112

1213
return (
1314
<main className="min-h-screen bg-background">
1415
{!isAuthPage && <Header />}
15-
<Outlet />
16+
<div className={!isAuthPage ? "pb-20 md:pb-0" : ""}>
17+
<Outlet />
18+
</div>
19+
{!isAuthPage && <BottomNav />}
1620
<TanStackRouterDevtools />
1721
</main>
1822
);

apps/web/src/routes/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const Route = createFileRoute("/")({
1515
function Index() {
1616
const { appMode } = useAppStore();
1717
return (
18-
<div className="pt-[138px] px-4">
18+
<div className="pt-4 md:pt-[138px] px-4">
1919
{appMode === "watch" && <WatchHome />}
2020
{appMode === "listen" && <ListenHome />}
2121
</div>

0 commit comments

Comments
 (0)