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 ChannelSubscribeButton from '../../components/ChannelSubscribeButton';
import { notFound } from 'next/navigation';
export const dynamic = 'force-dynamic';
@ -93,9 +94,7 @@ export default async function ChannelPage({
<span style={{ opacity: 0.5 }}></span>
<span>{videos.length} videos</span>
</div>
<button className="channel-subscribe-btn">
Subscribe
</button>
<ChannelSubscribeButton channelId={info.id} channelName={info.title} />
</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 */}
<div className="yt-header-left">
<button className="yt-icon-btn hamburger-btn" onClick={() => {
toggleSidebar();
toggleMobileMenu();
}} title="Menu">
<IoMenuOutline size={22} />

View file

@ -16,23 +16,10 @@ interface SidebarContextType {
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
export function SidebarProvider({ children }: { children: ReactNode }) {
// Sidebar is collapsed by default on desktop
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
// Sidebar is open by default on desktop (hidden on mobile via CSS)
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
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 openSidebar = () => setIsSidebarOpen(true);
const closeSidebar = () => setIsSidebarOpen(false);

View file

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