refactor: structure YMB as enterprise React app

This commit is contained in:
Zhou Weidong committed 2026-11-10 15:35:16 +08:00
1 parent e575ebd948
commit eb3eb8dfc6
18 files changed
+182 -20

No files matched your search

+2 -19
View File
@@ -1,24 +1,7 @@
// Author: fruit.com.cn
import React, { useMemo, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Bell, Bookmark, Compass, History, Home, Menu, Play, Search, UserRound, X } from 'lucide-react';
import { App } from './app/App';
import './styles.css';
import './enterprise.css';
const ASSET = '/assets/';
const copy = {
id: { title: 'YMB · Cerita pendek, emosi besar', description: 'Temukan drama pendek terbaik dan episode baru setiap hari.', search: 'Cari drama, aktor, atau genre', hero: 'Cerita pendek,', accent: 'emosi besar.', watch: 'Mulai menonton', picks: 'Drama pilihan minggu ini', shorts: 'Cerita dalam satu tarikan napas', channels: 'Ikuti cerita favoritmu', follow: 'Ikuti', following: 'Mengikuti', genres: ['Semua', 'Romansa', 'Balas dendam', 'Keluarga', 'Rahasia'] },
en: { title: 'YMB · Short stories, big emotions', description: 'Discover the best short dramas and new episodes every day.', search: 'Search dramas, actors, or genres', hero: 'Short stories,', accent: 'big emotions.', watch: 'Watch now', picks: 'Drama picks this week', shorts: 'Stories in one breath', channels: 'Follow your favorite stories', follow: 'Follow', following: 'Following', genres: ['All', 'Romance', 'Revenge', 'Family', 'Secrets'] },
zh: { title: 'YMB · 短故事,大情绪', description: '发现精彩短剧,每天更新新剧集。', search: '搜索短剧、演员或题材', hero: '短故事,', accent: '大情绪。', watch: '立即观看', picks: '本周精选短剧', shorts: '一口气看完的故事', channels: '关注你喜欢的故事', follow: '关注', following: '已关注', genres: ['全部', '爱情', '复仇', '家庭', '秘密'] }
};
const videos = [['Pernikahan yang Tak Terduga','YMB Originals','Romansa','wedding.jpg','12:48'],['Rahasia di Balik Senyuman','Cerita Senja','Rahasia','secret.jpg','09:32'],['Pewaris yang Kembali','Drama Pilihan','Keluarga','heir.jpg','15:20'],['Kesempatan Kedua untuk Kita','YMB Originals','Kisah kedua','second-chance.jpg','11:04'],['Cinta Setelah Balas Dendam','Cerita Senja','Balas dendam','revenge.jpg','13:40'],['Sahabat yang Selalu Ada','Drama Pilihan','Keluarga','friends.jpg','08:16'],['Kota Kecil, Rahasia Besar','YMB Originals','Rahasia','hero.jpg','10:12'],['Satu Janji Terakhir','Drama Pilihan','Romansa','romance.jpg','07:55']];
const shorts = [['Dia kembali setelah 5 tahun','second-chance.jpg'],['Ketika rahasia terbongkar','secret.jpg'],['Satu pesan yang mengubah semuanya','romance.jpg'],['Jangan percaya senyumnya','revenge.jpg'],['Pewaris tanpa nama','heir.jpg'],['Hari pernikahan itu','wedding.jpg']];
const channels = [['YMB Originals','2,4 jt pengikut','Y','#ca4d43'],['Cerita Senja','986 rb pengikut','C','#5c4aa1'],['Drama Pilihan','742 rb pengikut','D','#2e7d71'],['Kisah Hari Ini','428 rb pengikut','K','#a06a39']];
function useLanguage() { const initial = new URLSearchParams(location.search).get('lang'); return useState(copy[initial] ? initial : 'id'); }
function App() {
const [lang, setLang] = useLanguage(); const text = copy[lang]; const [genre, setGenre] = useState(text.genres[0]); const [query, setQuery] = useState(''); const [menu, setMenu] = useState(false); const [toast, setToast] = useState('');
const filtered = useMemo(() => videos.filter((item) => (!query || item.join(' ').toLowerCase().includes(query.toLowerCase())) && (genre === text.genres[0] || item[2] === genre)), [genre, query, text]);
const notify = (message) => { setToast(message); window.setTimeout(() => setToast(''), 1600); };
const changeLanguage = (value) => { setLang(value); setGenre(copy[value].genres[0]); const url = new URL(window.location.href); url.searchParams.set('lang', value); window.history.replaceState(null, '', url); document.title = copy[value].title; document.documentElement.lang = value === 'zh' ? 'zh-CN' : value; };
return <><header className="topbar"><button className="mobile-only icon-button" onClick={() => setMenu(!menu)} aria-label="Menu"><Menu /></button><a className="brand" href="#home"><span className="brand-symbol"><Play size={13} fill="currentColor" /></span>ymb</a><form className="search-form" onSubmit={(event) => event.preventDefault()}><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder={text.search} aria-label={text.search} /><button aria-label="Search"><Search size={19} /></button></form><div className="top-actions"><select value={lang} onChange={(event) => changeLanguage(event.target.value)} aria-label="Language"><option value="id">ID</option><option value="en">EN</option><option value="zh">中文</option></select><Bell size={19} /><span className="profile-chip">N</span></div></header><aside className={`sidebar ${menu ? 'open' : ''}`}><nav>{[[Home,'Beranda'],[Compass,'Shorts'],[UserRound,'Langganan'],[Bookmark,'Koleksi'],[History,'Riwayat']].map(([Icon,label]) => <a href="#home" key={label}><Icon size={19} />{label}</a>)}</nav><div className="nav-rule" /><small>GENRE POPULER</small>{text.genres.slice(1).map((item) => <button className="genre-link" key={item} onClick={() => setGenre(item)}>{item}</button>)}</aside><main className="content" id="home"><section className="hero-card"><div className="hero-copy"><small>YMB ORIGINAL</small><h1>{text.hero}<br /><em>{text.accent}</em></h1><p>{text.description}</p><button className="primary-button" onClick={() => notify(text.watch)}>▶ {text.watch}</button></div><div className="hero-art"><img src={`${ASSET}hero.jpg`} alt="YMB drama" /></div></section><section><div className="section-heading"><h2>{text.picks}</h2><span>Lihat semua →</span></div><div className="chip-row">{text.genres.map((item) => <button className={`chip ${genre === item ? 'active' : ''}`} key={item} onClick={() => setGenre(item)}>{item}</button>)}</div><div className="video-grid">{filtered.map((item) => <article className="video-card" key={item[0]} onClick={() => notify(`▶ ${item[0]}`)}><div className="thumb"><img src={`${ASSET}${item[3]}`} alt={item[0]} /><b>{item[4]}</b></div><div className="video-meta"><span className="channel-avatar">{item[1][0]}</span><div><strong>{item[0]}</strong><small>{item[1]}<br />{(Math.random() * 900 + 100).toFixed(0)} rb tayangan · kemarin</small></div></div></article>)}</div></section><section><div className="section-heading"><h2>{text.shorts}</h2><span>Explore →</span></div><div className="shorts-row">{shorts.map((item) => <article className="short-card" key={item[0]} onClick={() => notify(`▶ ${item[0]}`)}><img src={`${ASSET}${item[1]}`} alt={item[0]} /><strong>{item[0]}</strong></article>)}</div></section><section><div className="section-heading"><h2>{text.channels}</h2></div><div className="channel-grid">{channels.map((item) => <article className="channel-card" key={item[0]}><span className="channel-avatar" style={{ background: item[3] }}>{item[2]}</span><div><strong>{item[0]}</strong><small>{item[1]}</small></div><button onClick={(event) => { event.currentTarget.textContent = event.currentTarget.textContent === text.follow ? text.following : text.follow; }}>{text.follow}</button></article>)}</div></section></main><nav className="bottom-nav"><a href="#home"><Home size={19} />Home</a><a href="#shorts"><Compass size={19} />Shorts</a><a href="#channels"><UserRound size={19} />Follow</a><a href="#library"><Bookmark size={19} />Library</a></nav>{toast && <div className="toast">{toast}</div>}{menu && <button className="scrim mobile-only" onClick={() => setMenu(false)} aria-label="Close"><X /></button>}</>;
}
createRoot(document.getElementById('root')).render(<App />);