setIsNavigating(true)}
style={{ position: 'relative', display: 'block', width: '100%', aspectRatio: '16/9', overflow: 'hidden', borderRadius: '12px' }}
>
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

- {video.duration && (
+ {video.duration && !video.is_mix && (
{video.duration}
)}
+
+ {video.is_mix && (
+
+ )}
{isNavigating && (
{/* Video Info */}
-
+
{video.title}
@@ -95,3 +116,5 @@ export default function VideoCard({ video, hideChannelAvatar }: { video: VideoDa
);
}
+
+export default memo(VideoCard);
diff --git a/frontend/app/constants.ts b/frontend/app/constants.ts
old mode 100755
new mode 100644
index d9bf44d..8304d78
--- a/frontend/app/constants.ts
+++ b/frontend/app/constants.ts
@@ -8,6 +8,8 @@ export interface VideoData {
view_count: number;
duration: string;
avatar_url?: string;
+ list_id?: string;
+ is_mix?: boolean;
}
export const CATEGORY_MAP: Record
= {
diff --git a/frontend/app/context/SidebarContext.tsx b/frontend/app/context/SidebarContext.tsx
new file mode 100644
index 0000000..ed16d8d
--- /dev/null
+++ b/frontend/app/context/SidebarContext.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+
+interface SidebarContextType {
+ isSidebarOpen: boolean;
+ toggleSidebar: () => void;
+ openSidebar: () => void;
+ closeSidebar: () => void;
+ isMobileMenuOpen: boolean;
+ toggleMobileMenu: () => void;
+ openMobileMenu: () => void;
+ closeMobileMenu: () => void;
+}
+
+const SidebarContext = createContext(undefined);
+
+export function SidebarProvider({ children }: { children: ReactNode }) {
+ // Sidebar is collapsed by default on desktop
+ const [isSidebarOpen, setIsSidebarOpen] = 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 openSidebar = () => setIsSidebarOpen(true);
+ const closeSidebar = () => setIsSidebarOpen(false);
+
+ const toggleMobileMenu = () => setIsMobileMenuOpen(prev => !prev);
+ const openMobileMenu = () => setIsMobileMenuOpen(true);
+ const closeMobileMenu = () => setIsMobileMenuOpen(false);
+
+ // Prevent body scroll when mobile menu is open
+ useEffect(() => {
+ if (isMobileMenuOpen) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = '';
+ }
+ return () => {
+ document.body.style.overflow = '';
+ };
+ }, [isMobileMenuOpen]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useSidebar() {
+ const context = useContext(SidebarContext);
+ if (context === undefined) {
+ throw new Error('useSidebar must be used within a SidebarProvider');
+ }
+ return context;
+}
\ No newline at end of file
diff --git a/frontend/app/context/ThemeContext.tsx b/frontend/app/context/ThemeContext.tsx
old mode 100755
new mode 100644
diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico
old mode 100755
new mode 100644
diff --git a/frontend/app/feed/library/page.tsx b/frontend/app/feed/library/page.tsx
old mode 100755
new mode 100644
diff --git a/frontend/app/feed/subscriptions/page.tsx b/frontend/app/feed/subscriptions/page.tsx
old mode 100755
new mode 100644
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
old mode 100755
new mode 100644
index a3d981b..1090fef
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -255,6 +255,20 @@
gap: 4px;
}
+/* Sidebar collapsed by default on desktop */
+.yt-sidebar-mini.sidebar-collapsed {
+ display: none;
+}
+
+.yt-sidebar-mini.sidebar-open {
+ display: flex;
+}
+
+/* When sidebar is open, shift main content */
+.yt-main-content.sidebar-open {
+ margin-left: var(--yt-sidebar-width-mini);
+}
+
@media (max-width: 768px) {
.yt-sidebar-mini {
display: none;
@@ -263,7 +277,7 @@
.yt-main-content {
margin-top: var(--yt-header-height);
- margin-left: var(--yt-sidebar-width-mini);
+ margin-left: 0;
min-height: calc(100vh - var(--yt-header-height));
background-color: var(--yt-background);
}
@@ -619,41 +633,237 @@ a {
animation: fadeIn 0.2s ease-out;
}
+/* ===== HAMBURGER MENU / DRAWER ===== */
+
+.drawer-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 1000;
+ opacity: 0;
+ visibility: hidden;
+ pointer-events: none;
+ transition: opacity 0.3s ease, visibility 0.3s ease;
+}
+
+[data-theme='light'] .drawer-backdrop {
+ background-color: rgba(0, 0, 0, 0.4);
+}
+
+.drawer-backdrop.open {
+ opacity: 1;
+ visibility: visible;
+ pointer-events: auto;
+}
+
+.hamburger-drawer {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 240px;
+ background-color: var(--yt-background);
+ z-index: 1100; /* Ensure it is above EVERYTHING including backdrops and players */
+ transform: translateX(-100%);
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
+ display: flex;
+ flex-direction: column;
+ pointer-events: none;
+ visibility: hidden;
+}
+
+.hamburger-drawer.open {
+ transform: translateX(0);
+ visibility: visible;
+ pointer-events: auto;
+ box-shadow: 4px 0 24px rgba(0, 0, 0, 0.5);
+}
+
+[data-theme='light'] .hamburger-drawer.open {
+ box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
+}
+
+.drawer-header {
+ height: var(--yt-header-height);
+ padding: 0 16px;
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid var(--yt-border);
+}
+
+.drawer-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 12px 0;
+}
+
+.drawer-nav-item {
+ display: flex;
+ align-items: center;
+ padding: 0 12px 0 24px;
+ height: 48px;
+ color: var(--yt-text-primary);
+ text-decoration: none;
+ transition: background-color 0.2s;
+ cursor: pointer;
+}
+
+.drawer-nav-item:hover,
+.drawer-nav-item:focus {
+ background-color: var(--yt-hover);
+}
+
+.drawer-nav-item.active {
+ background-color: var(--yt-hover);
+ font-weight: 500;
+}
+
+.drawer-nav-item.active .drawer-nav-icon {
+ color: var(--yt-text-primary);
+}
+
+.drawer-nav-icon {
+ margin-right: 24px;
+ color: var(--yt-text-primary);
+ display: flex;
+ align-items: center;
+}
+
+.drawer-nav-label {
+ font-size: 14px;
+ line-height: 20px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.drawer-divider {
+ border-top: 1px solid var(--yt-border);
+ margin: 12px 0;
+}
+
/* ===== WATCH PAGE ===== */
.watch-container {
- display: flex;
+ display: grid;
+ grid-template-columns: 1fr 402px;
+ grid-template-rows: auto auto;
gap: 24px;
max-width: 1750px;
width: 100%;
margin: 0 auto;
- padding: 24px;
- justify-content: center;
+ padding: 24px 40px;
+ box-sizing: border-box;
}
.watch-primary {
- flex: 1;
+ grid-column: 1;
+ grid-row: 1;
min-width: 0;
}
-.watch-secondary {
- width: 402px;
- flex-shrink: 0;
- position: sticky;
- top: 80px;
- max-height: calc(100vh - 104px);
- overflow-y: auto;
- padding-right: 12px;
- padding-left: 6px;
- /* Hide scrollbar for clean look */
- -ms-overflow-style: none;
- /* IE and Edge */
- scrollbar-width: none;
- /* Firefox */
+.watch-container > .comments-section {
+ grid-column: 1;
+ grid-row: 2;
+ padding-right: 16px; /* Space before sidebar */
}
-.watch-secondary::-webkit-scrollbar {
- display: none;
+.watch-secondary {
+ grid-column: 2;
+ grid-row: 1 / span 2;
+ width: 100%;
+ padding-right: 12px; /* Breathing room for items from edge/scrollbar */
+}
+
+/* Responsive Watch Layout */
+@media (max-width: 1024px) {
+ .watch-container {
+ display: flex;
+ flex-direction: column;
+ padding: 16px 12px;
+ }
+ .watch-primary, .watch-secondary, .comments-section {
+ padding: 0 4px;
+ }
+ .watch-primary {
+ order: 1;
+ }
+ .watch-secondary {
+ width: 100%;
+ margin-top: 24px;
+ order: 3;
+ }
+ .watch-container > .comments-section {
+ order: 2;
+ margin-top: 24px;
+ }
+
+ /* On mobile: show collapsed header, hide full header when not expanded */
+ .comments-collapsed-header {
+ display: flex !important;
+ }
+ .comments-section:not(:has(.comments-list.expanded)) .comments-full-header {
+ display: none;
+ }
+ .comments-section:has(.comments-list.expanded) .comments-collapsed-header {
+ display: none !important;
+ }
+ .comments-section:not(:has(.comments-list.expanded)) .comments-list {
+ max-height: 200px;
+ overflow: hidden;
+ position: relative;
+ }
+ .comments-section:not(:has(.comments-list.expanded)) .comments-list::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 60px;
+ background: linear-gradient(transparent, var(--background));
+ pointer-events: none;
+ }
+ .comments-toggle-btn {
+ display: block !important;
+ }
+}
+
+/* Sidebar grid styles on large screens */
+@media (min-width: 1025px) {
+ .watch-secondary {
+ position: sticky;
+ top: calc(var(--yt-header-height) + 24px);
+ height: calc(100vh - var(--yt-header-height) - 48px);
+ overflow-y: auto;
+ scrollbar-width: thin; /* Clean scrollbar appearance */
+ }
+
+ .watch-video-grid .video-grid-mobile {
+ display: flex !important;
+ flex-direction: column !important;
+ gap: 12px !important;
+ }
+ .watch-video-grid .videocard-container {
+ flex-direction: row !important;
+ gap: 8px !important;
+ align-items: flex-start !important;
+ margin-bottom: 8px !important;
+ }
+ .watch-video-grid .videocard-container > a {
+ width: 168px !important;
+ min-width: 168px !important;
+ border-radius: 8px !important;
+ }
+ .watch-video-grid .videocard-info {
+ padding: 0 !important;
+ }
+ .watch-video-grid h3.truncate-2-lines {
+ font-size: 14px !important;
+ line-height: 20px !important;
+ }
}
/* Player Wrapper */
@@ -1394,4 +1604,53 @@ a {
100% {
transform: rotate(360deg);
}
+}
+
+/* Download dropdown mobile improvements */
+.download-dropdown {
+ animation: fadeIn 0.2s ease-out;
+}
+
+.download-backdrop {
+ animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+/* Format item hover effect */
+.format-item-hover:hover {
+ background-color: var(--yt-hover) !important;
+}
+
+/* Action button hover effect */
+.action-btn-hover:hover {
+ background-color: var(--yt-active) !important;
+}
+
+/* Better loading state visibility */
+.skeleton {
+ background: linear-gradient(
+ 90deg,
+ var(--yt-hover) 25%,
+ var(--yt-active) 50%,
+ var(--yt-hover) 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
}
\ No newline at end of file
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
old mode 100755
new mode 100644
index e5d6943..c447fd4
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -5,6 +5,8 @@ import './globals.css';
import Header from './components/Header';
import Sidebar from './components/Sidebar';
import MobileNav from './components/MobileNav';
+import HamburgerMenu from './components/HamburgerMenu';
+import MainContent from './components/MainContent';
const roboto = Roboto({
weight: ['400', '500', '700'],
@@ -14,12 +16,24 @@ const roboto = Roboto({
export const metadata: Metadata = {
title: 'KV-Tube',
- description: 'A pixel perfect YouTube clone',
+ description: 'A modern YouTube-like video streaming platform with background playback',
manifest: '/manifest.json',
appleWebApp: {
capable: true,
- statusBarStyle: 'default',
+ statusBarStyle: 'black-translucent',
title: 'KV-Tube',
+ startupImage: [
+ {
+ url: '/icons/icon-512x512.png',
+ media: '(device-width: 1024px)',
+ },
+ ],
+ },
+ other: {
+ 'mobile-web-app-capable': 'yes',
+ 'apple-mobile-web-app-capable': 'yes',
+ 'apple-mobile-web-app-status-bar-style': 'black-translucent',
+ 'theme-color': '#ff0000',
},
};
@@ -28,6 +42,7 @@ export const viewport = {
};
import { ThemeProvider } from './context/ThemeContext';
+import { SidebarProvider } from './context/SidebarContext';
export default function RootLayout({
children,
@@ -63,12 +78,15 @@ export default function RootLayout({
-
-
-
- {children}
-
-
+
+
+
+
+
+ {children}
+
+
+