feat: match youtube homepage layout

This commit is contained in:
Zhou Weidong committed 2026-11-11 13:34:16 +08:00
1 parent e29fb4c859
commit 47438186af
7 files changed
+172 -44

No files matched your search

+31 -13
View File
@@ -1,5 +1,5 @@
// Author: fruit.com.cn
import { Bell, Bookmark, Compass, History, Home, Menu, Search, UserRound, X } from 'lucide-react';
import { Bell, Bookmark, ChevronDown, ChevronRight, Compass, Film, History, Home, ListVideo, Menu, Mic, Music2, Plus, Radio, Search, ThumbsUp, UserRound, Video, X } from 'lucide-react';
import { useState } from 'react';
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { useAppStore } from '../app/store';
@@ -9,7 +9,9 @@ export function AppShell() {
const { locale, query, setQuery, sidebarOpen, setSidebarOpen } = useAppStore();
const navigate = useNavigate();
const [searchOpen, setSearchOpen] = useState(false);
const navItems = [[Home, locale.home, '/'], [Compass, locale.shorts, '/shorts'], [UserRound, locale.subscriptions, '/subscriptions'], [Bookmark, locale.library, '/library'], [History, locale.history, '/history']];
const primaryItems = [[Home, locale.home, '/'], [Compass, locale.shorts, '/shorts'], [UserRound, locale.subscriptions, '/subscriptions']];
const libraryItems = [[History, locale.history, '/history'], [ListVideo, locale.playlists, '/library'], [Bookmark, locale.watchLater, '/library'], [ThumbsUp, locale.likedVideos, '/library'], [Video, locale.myVideos, '/library']];
const exploreItems = [[Music2, locale.music, '/search?genre=music'], [Film, locale.movies, '/search?genre=movies'], [Radio, locale.live, '/shorts']];
const submitSearch = (event) => {
event.preventDefault();
navigate(`/search?q=${encodeURIComponent(query)}`);
@@ -28,17 +30,19 @@ export function AppShell() {
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder={locale.search} aria-label={locale.search} />
<button aria-label="Search" type="submit"><Search size={19} /></button>
</form>
<button className="voice-search icon-button" aria-label={locale.voiceSearch} type="button"><Mic size={20} /></button>
<div className="top-actions">
<button className="icon-button mobile-search" onClick={() => setSearchOpen(!searchOpen)} aria-label={locale.search} type="button"><Search size={20} /></button>
<LanguageSwitcher />
<Bell size={19} aria-hidden="true" />
<span className="profile-chip" aria-label="Profile">N</span>
<button className="create-button" type="button"><Plus size={19} /><span>{locale.create}</span></button>
<button className="icon-button notification-button" aria-label={locale.notifications} type="button"><Bell size={19} /></button>
<span className="profile-chip" aria-label={locale.account}>N</span>
</div>
</header>
<aside className={`sidebar ${sidebarOpen ? 'open' : 'closed'}`}>
<nav>
{navItems.map(([Icon, label, to]) => (
<nav className="sidebar-group">
{primaryItems.map(([Icon, label, to]) => (
<NavLink to={to} end={to === '/'} key={to}>
<Icon size={19} />
<span className="sidebar-label">{label}</span>
@@ -46,17 +50,31 @@ export function AppShell() {
))}
</nav>
<div className="nav-rule" />
<small>GENRE POPULER</small>
{locale.genres.slice(1).map((genre) => (
<button className="genre-link" key={genre} onClick={() => navigate(`/search?genre=${encodeURIComponent(genre)}`)} type="button">
<span className="sidebar-label">{genre}</span>
</button>
))}
<div className="sidebar-group">
<div className="sidebar-group-title"><span>{locale.you}</span><ChevronRight size={16} /></div>
{libraryItems.map(([Icon, label, to]) => (
<NavLink to={to} key={label}>
<Icon size={19} />
<span className="sidebar-label">{label}</span>
</NavLink>
))}
</div>
<div className="nav-rule" />
<div className="sidebar-group">
<div className="sidebar-group-title">{locale.explore}</div>
{exploreItems.map(([Icon, label, to]) => (
<NavLink to={to} key={label}>
<Icon size={19} />
<span className="sidebar-label">{label}</span>
</NavLink>
))}
<button className="genre-link" type="button"><ChevronDown size={19} /><span className="sidebar-label">{locale.more}</span></button>
</div>
</aside>
<main className={`content ${sidebarOpen ? '' : 'content-wide'}`}><Outlet /></main>
<nav className="bottom-nav">
{navItems.slice(0, 4).map(([Icon, label, to]) => <NavLink to={to} key={to}><Icon size={19} />{label}</NavLink>)}
{primaryItems.map(([Icon, label, to]) => <NavLink to={to} key={to}><Icon size={19} />{label}</NavLink>)}
</nav>
{sidebarOpen && <button className="scrim mobile-only" onClick={() => setSidebarOpen(false)} aria-label="Close" type="button"><X /></button>}
</>