fix: sub/you tabs now use backend API, sidebar always visible on desktop, channel subscribe button wired up

This commit is contained in:
Khoa Vo 2026-05-14 17:45:49 +07:00
parent 7358a92bd8
commit 7e05778840
5 changed files with 88 additions and 38 deletions

View file

@ -1,4 +1,5 @@
import VideoCard from '../../components/VideoCard'; import VideoCard from '../../components/VideoCard';
import ChannelSubscribeButton from '../../components/ChannelSubscribeButton';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@ -93,9 +94,7 @@ export default async function ChannelPage({
<span style={{ opacity: 0.5 }}></span> <span style={{ opacity: 0.5 }}></span>
<span>{videos.length} videos</span> <span>{videos.length} videos</span>
</div> </div>
<button className="channel-subscribe-btn"> <ChannelSubscribeButton channelId={info.id} channelName={info.title} />
Subscribe
</button>
</div> </div>
</div> </div>

View file

@ -0,0 +1,51 @@
'use client';
import { useState, useEffect } from 'react';
export default function ChannelSubscribeButton({ channelId, channelName }: { channelId: string; channelName: string }) {
const [subscribed, setSubscribed] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetch(`/api/subscribe?channel_id=${encodeURIComponent(channelId)}`)
.then(r => r.json())
.then(data => setSubscribed(data.subscribed))
.catch(() => setSubscribed(false));
}, [channelId]);
const handleSubscribe = async () => {
if (loading || !channelId) return;
setLoading(true);
try {
if (subscribed) {
const res = await fetch(`/api/subscribe?channel_id=${encodeURIComponent(channelId)}`, { method: 'DELETE' });
if (res.ok) setSubscribed(false);
} else {
const res = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channel_id: channelId,
channel_name: channelName || channelId,
channel_avatar: channelName ? channelName[0].toUpperCase() : '?',
}),
});
if (res.ok) setSubscribed(true);
}
} catch (error) {
console.error('Subscribe error:', error);
} finally {
setLoading(false);
}
};
return (
<button
className={`channel-subscribe-btn ${subscribed ? 'subscribed' : ''}`}
onClick={handleSubscribe}
disabled={loading}
>
{loading ? '...' : subscribed ? 'Subscribed' : 'Subscribe'}
</button>
);
}

View file

@ -41,7 +41,6 @@ export default function Header() {
{/* Left */} {/* Left */}
<div className="yt-header-left"> <div className="yt-header-left">
<button className="yt-icon-btn hamburger-btn" onClick={() => { <button className="yt-icon-btn hamburger-btn" onClick={() => {
toggleSidebar();
toggleMobileMenu(); toggleMobileMenu();
}} title="Menu"> }} title="Menu">
<IoMenuOutline size={22} /> <IoMenuOutline size={22} />

View file

@ -16,23 +16,10 @@ interface SidebarContextType {
const SidebarContext = createContext<SidebarContextType | undefined>(undefined); const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
export function SidebarProvider({ children }: { children: ReactNode }) { export function SidebarProvider({ children }: { children: ReactNode }) {
// Sidebar is collapsed by default on desktop // Sidebar is open by default on desktop (hidden on mobile via CSS)
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
// Load saved preference from localStorage
useEffect(() => {
const saved = localStorage.getItem('sidebarOpen');
if (saved !== null) {
setIsSidebarOpen(saved === 'true');
}
}, []);
// Save preference to localStorage
useEffect(() => {
localStorage.setItem('sidebarOpen', isSidebarOpen.toString());
}, [isSidebarOpen]);
const toggleSidebar = () => setIsSidebarOpen(prev => !prev); const toggleSidebar = () => setIsSidebarOpen(prev => !prev);
const openSidebar = () => setIsSidebarOpen(true); const openSidebar = () => setIsSidebarOpen(true);
const closeSidebar = () => setIsSidebarOpen(false); const closeSidebar = () => setIsSidebarOpen(false);

View file

@ -5,7 +5,7 @@ import { useSearchParams, useRouter } from 'next/navigation';
import YouTubePlayer from './YouTubePlayer'; import YouTubePlayer from './YouTubePlayer';
import { getVideoDetailsClient, getRelatedVideosClient, getCommentsClient, searchVideosClient } from '../clientActions'; import { getVideoDetailsClient, getRelatedVideosClient, getCommentsClient, searchVideosClient } from '../clientActions';
import { VideoData } from '../constants'; import { VideoData } from '../constants';
import { isSubscribed, toggleSubscription, addToHistory, isVideoSaved, toggleSaveVideo } from '../storage'; import { isVideoSaved, toggleSaveVideo } from '../storage';
import LoadingSpinner from '../components/LoadingSpinner'; import LoadingSpinner from '../components/LoadingSpinner';
import Link from 'next/link'; import Link from 'next/link';
@ -39,33 +39,44 @@ function VideoInfo({ video }: { video: any }) {
const [isSaved, setIsSaved] = useState(false); const [isSaved, setIsSaved] = useState(false);
const [subscribing, setSubscribing] = useState(false); const [subscribing, setSubscribing] = useState(false);
// Check subscription and save status on mount // Check subscription status via API and save status on mount
useEffect(() => { useEffect(() => {
if (video?.channelId) { if (video?.channelId) {
setSubscribed(isSubscribed(video.channelId)); fetch(`/api/subscribe?channel_id=${encodeURIComponent(video.channelId)}`)
.then(r => r.json())
.then(data => setSubscribed(data.subscribed))
.catch(() => setSubscribed(false));
} }
if (video?.id) { if (video?.id) {
setIsSaved(isVideoSaved(video.id)); setIsSaved(isVideoSaved(video.id));
} }
}, [video?.channelId, video?.id]); }, [video?.channelId, video?.id]);
const handleSubscribe = useCallback(() => { const handleSubscribe = useCallback(async () => {
if (!video?.channelId || subscribing) return; if (!video?.channelId || subscribing) return;
setSubscribing(true); setSubscribing(true);
try { try {
const nowSubscribed = toggleSubscription({ if (subscribed) {
channelId: video.channelId, const res = await fetch(`/api/subscribe?channel_id=${encodeURIComponent(video.channelId)}`, { method: 'DELETE' });
channelName: video.channelTitle, if (res.ok) setSubscribed(false);
channelAvatar: '', } else {
}); const res = await fetch('/api/subscribe', {
setSubscribed(nowSubscribed); method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channel_id: video.channelId,
channel_name: video.channelTitle || video.channelId,
channel_avatar: '',
}),
});
if (res.ok) setSubscribed(true);
}
} catch (error) { } catch (error) {
console.error('Subscribe error:', error); console.error('Subscribe error:', error);
} finally { } finally {
setSubscribing(false); setSubscribing(false);
} }
}, [video?.channelId, video?.channelTitle, subscribing]); }, [video?.channelId, video?.channelTitle, subscribed, subscribing]);
const handleSave = useCallback(() => { const handleSave = useCallback(() => {
if (!video?.id) return; if (!video?.id) return;
@ -636,14 +647,17 @@ export default function ClientWatchPage() {
} }
setVideoInfo(video); setVideoInfo(video);
// Add to watch history (localStorage) // Add to watch history via API
if (video) { if (video) {
addToHistory({ fetch('/api/history', {
videoId: videoId, method: 'POST',
title: video.title, headers: { 'Content-Type': 'application/json' },
thumbnail: video.thumbnail, body: JSON.stringify({
channelTitle: video.channelTitle, video_id: videoId,
}); title: video.title,
thumbnail: video.thumbnail,
}),
}).catch(() => {});
} }
// Get related videos - use channel name and video title for better results // Get related videos - use channel name and video title for better results