feat: migrate YMB site to React Vite
This commit is contained in:
1 parent
ab9f50d66b
commit
63430a66de
18 files changed
+556
-38
No files matched your search
@@ -0,0 +1,24 @@
|
||||
// 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 './styles.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 />);
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Author: fruit.com.cn */
|
||||
@font-face{font-family:Jakarta;src:url('/assets/PlusJakartaSans-Regular.ttf')}@font-face{font-family:Jakarta;src:url('/assets/PlusJakartaSans-SemiBold.ttf');font-weight:600}@font-face{font-family:Outfit;src:url('/assets/Outfit-Bold.ttf');font-weight:700}:root{--bg:#0f0f0f;--surface:#181818;--line:#303030;--muted:#aaa;--accent:#ff4b3e;--sidebar:230px}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:#f1f1f1;font:14px Jakarta,Arial,sans-serif}a{color:inherit;text-decoration:none}button,input,select{font:inherit}.topbar{height:64px;position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:24px;padding:0 28px;background:#0f0f0ff2;border-bottom:1px solid #222;backdrop-filter:blur(15px)}.brand{display:flex;align-items:center;gap:8px;font:700 26px Outfit;min-width:150px}.brand-symbol{display:grid;place-items:center;width:28px;height:28px;border-radius:8px;background:var(--accent)}.search-form{display:flex;max-width:620px;flex:1;margin:auto}.search-form input{width:100%;height:40px;padding:0 16px;border:1px solid #3b3b3b;border-radius:22px 0 0 22px;background:#121212;color:#fff}.search-form button{width:54px;border:1px solid #3b3b3b;border-left:0;border-radius:0 22px 22px 0;background:#282828;color:#fff;display:grid;place-items:center}.top-actions{display:flex;align-items:center;gap:14px}.top-actions select{border:1px solid #363636;border-radius:16px;background:#1d1d1d;color:#ddd;padding:6px 9px;font-size:11px}.profile-chip{display:grid;place-items:center;width:32px;height:32px;border-radius:50%;background:#d45a68}.icon-button{border:0;background:transparent;color:#fff;display:grid;place-items:center}.sidebar{position:fixed;top:64px;bottom:0;width:var(--sidebar);padding:20px 12px;background:var(--bg);border-right:1px solid #222;z-index:4}.sidebar nav{display:grid;gap:4px}.sidebar nav a,.genre-link{display:flex;align-items:center;gap:16px;width:100%;padding:11px 14px;border:0;border-radius:10px;background:transparent;color:#ccc;text-align:left;cursor:pointer}.sidebar nav a:hover,.genre-link:hover{background:#272727;color:#fff}.sidebar small{display:block;padding:14px;color:#888;font-size:10px;letter-spacing:.12em}.nav-rule{height:1px;background:var(--line);margin:18px 10px}.content{max-width:1500px;padding:28px 34px 80px calc(var(--sidebar) + 34px);margin:auto}.hero-card{position:relative;min-height:350px;overflow:hidden;border-radius:18px;background:#27191a;margin-bottom:44px}.hero-copy{position:relative;z-index:1;width:52%;padding:54px}.hero-copy small,.section-heading span{color:#ff786f;font-size:11px;letter-spacing:.12em;font-weight:600}.hero-copy h1{margin:12px 0;font:700 56px/1 Outfit;letter-spacing:-.04em}.hero-copy h1 em{color:#ff786f;font-style:normal}.hero-copy p{max-width:390px;color:#ccc;line-height:1.7}.primary-button,.chip,.channel-card button{border:0;border-radius:22px;padding:11px 17px;cursor:pointer;font-weight:600}.primary-button{background:#fff;color:#111}.hero-art{position:absolute;inset:0 0 0 40%}.hero-art img{width:100%;height:100%;object-fit:cover}.hero-art:after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,#27191a,rgba(39,25,26,.72) 28%,transparent 80%)}section{margin-bottom:46px}.section-heading{display:flex;justify-content:space-between;align-items:end;margin-bottom:17px}.section-heading h2{margin:0;font:700 24px Outfit}.chip-row{display:flex;gap:8px;overflow:auto;margin-bottom:18px}.chip{background:#2b2b2b;color:#fff;white-space:nowrap}.chip.active,.chip:hover{background:#fff;color:#111}.video-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:24px 16px}.video-card{cursor:pointer;min-width:0}.thumb{position:relative;aspect-ratio:16/9;overflow:hidden;border-radius:10px;background:#222}.thumb img{width:100%;height:100%;object-fit:cover}.thumb b{position:absolute;right:7px;bottom:7px;padding:3px 6px;background:#000c;border-radius:4px;font-size:11px}.video-meta{display:flex;gap:9px;padding-top:9px}.channel-avatar{display:grid;place-items:center;flex:none;width:32px;height:32px;border-radius:50%;font-weight:700}.video-meta strong,.channel-card strong{display:block;line-height:1.4}.video-meta small,.channel-card small{display:block;color:#999;font-size:11px;line-height:1.45;margin-top:4px}.shorts-row{display:grid;grid-template-columns:repeat(6,1fr);gap:14px}.short-card{position:relative;aspect-ratio:9/14;overflow:hidden;border-radius:11px;cursor:pointer}.short-card img{width:100%;height:100%;object-fit:cover}.short-card:after{content:'';position:absolute;inset:45% 0 0;background:linear-gradient(transparent,#000c)}.short-card strong{position:absolute;z-index:1;bottom:10px;left:9px;right:9px;font-size:12px}.channel-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.channel-card{display:flex;align-items:center;gap:10px;padding:14px;border:1px solid var(--line);border-radius:11px}.channel-card button{margin-left:auto;padding:8px 11px;background:#fff;color:#111}.bottom-nav,.mobile-only{display:none}.toast{position:fixedLine truncated
|
||||
Reference in new issue
Block a user