fix: sub/you tabs now use backend API, sidebar always visible on desktop, channel subscribe button wired up
This commit is contained in:
parent
7358a92bd8
commit
7e05778840
5 changed files with 88 additions and 38 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
51
frontend/app/components/ChannelSubscribeButton.tsx
Normal file
51
frontend/app/components/ChannelSubscribeButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
channel_id: video.channelId,
|
||||||
|
channel_name: video.channelTitle || video.channelId,
|
||||||
|
channel_avatar: '',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
setSubscribed(nowSubscribed);
|
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',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
video_id: videoId,
|
||||||
title: video.title,
|
title: video.title,
|
||||||
thumbnail: video.thumbnail,
|
thumbnail: video.thumbnail,
|
||||||
channelTitle: video.channelTitle,
|
}),
|
||||||
});
|
}).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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue