feat: migrate YMB site to React Vite

This commit is contained in:
Zhou Weidong committed 2026-11-10 15:05:47 +08:00
1 parent ab9f50d66b
commit 63430a66de
18 files changed
+556 -38

No files matched your search

+7 -3
View File
@@ -2,17 +2,21 @@
# YMB Site
YMB 公开站点响应式首页原型,参考 YouTube 的内容发现信息架构,支持桌面端与移动端。
YMB 公开站点 React + Vite 项目,参考 YouTube 的内容发现信息架构,支持桌面端与移动端。
## 运行
直接用浏览器打开 `index.html`,或在当前目录启动任意静态文件服务器。
```bash
pnpm install
pnpm dev
pnpm build
```
## 包含
- 桌面端:顶部搜索、侧边导航、推荐网格、Shorts 横滑区、频道订阅。
- 移动端:顶部紧凑栏、双列内容、横滑 Shorts、底部导航。
- 原生 JS:搜索、题材筛选、播放提示、关注切换。
- React:搜索、题材筛选、播放提示、关注切换、移动端菜单。
- 多语言:印尼语 `?lang=id`、English `?lang=en`、简体中文 `?lang=zh`。
- SEO:动态 `lang/title/description`、canonical、hreflang、Open Graph、Twitter Card、JSON-LD、robots.txt、sitemap.xml。
+12 -35
View File
@@ -3,46 +3,23 @@
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0f0f0f" />
<meta name="description" id="meta-description" content="YMB — cerita pendek, emosi besar. Temukan drama pendek terbaik dan episode baru setiap hari." />
<meta name="keywords" content="short drama, vertical drama, drama pendek, YMB, serial drama" />
<meta name="description" content="YMB — short stories, big emotions." />
<meta name="robots" content="index,follow,max-image-preview:large" />
<link rel="canonical" id="canonical-link" href="https://ymb.example.com/" />
<link rel="canonical" href="https://ymb.example.com/" />
<link rel="alternate" hreflang="id" href="https://ymb.example.com/?lang=id" />
<link rel="alternate" hreflang="en" href="https://ymb.example.com/?lang=en" />
<link rel="alternate" hreflang="zh-CN" href="https://ymb.example.com/?lang=zh" />
<link rel="alternate" hreflang="x-default" href="https://ymb.example.com/" />
<meta property="og:type" content="website" /><meta property="og:site_name" content="YMB" /><meta property="og:title" id="og-title" content="YMB · Cerita pendek, emosi besar" /><meta property="og:description" id="og-description" content="Temukan drama pendek terbaik di YMB." /><meta property="og:image" content="hero.jpg" />
<meta name="twitter:card" content="summary_large_image" /><meta name="twitter:title" id="twitter-title" content="YMB · Cerita pendek, emosi besar" /><meta name="twitter:description" id="twitter-description" content="Temukan drama pendek terbaik di YMB." />
<script type="application/ld+json" id="structured-data">{"@context":"https://schema.org","@type":"WebSite","name":"YMB","url":"https://ymb.example.com/","potentialAction":{"@type":"SearchAction","target":"https://ymb.example.com/?lang=id&q={search_term_string}","query-input":"required name=search_term_string"}}</script>
<title id="page-title">YMB · Cerita pendek, emosi besar</title>
<link rel="icon" href="../brand-mark.png" />
<link rel="stylesheet" href="styles.css" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="YMB" />
<meta property="og:title" content="YMB · Short stories, big emotions" />
<meta property="og:description" content="Discover the best short dramas and new episodes every day." />
<meta property="og:image" content="https://ymb.example.com/assets/hero.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","name":"YMB","url":"https://ymb.example.com/","potentialAction":{"@type":"SearchAction","target":"https://ymb.example.com/?q={search_term_string}","query-input":"required name=search_term_string"}}</script>
<title>YMB · Short stories, big emotions</title>
</head>
<body>
<div class="app-shell">
<header class="topbar">
<button class="icon-button mobile-only" id="menu-toggle" aria-label="Buka menu">☰</button>
<a class="brand" href="#home" aria-label="YMB Beranda"><span class="brand-symbol">▶</span><span>ymb</span></a>
<form class="search-form" id="search-form" role="search"><input id="global-search" type="search" placeholder="Cari drama, aktor, atau genre" aria-label="Cari drama" autocomplete="off" /><button type="submit" aria-label="Cari">⌕</button></form>
<div class="top-actions"><label class="language-select"><span class="sr-only">Bahasa</span><select id="language-select" aria-label="Pilih bahasa"><option value="id">ID</option><option value="en">EN</option><option value="zh">中文</option></select></label><button class="icon-button desktop-only" aria-label="Buat">+</button><button class="icon-button" aria-label="Notifikasi">♧</button><button class="profile-chip" aria-label="Profil">N</button></div>
</header>
<aside class="sidebar" id="sidebar" aria-label="Navigasi utama">
<nav class="main-nav"><a class="nav-item active" href="#home"><span>⌂</span><b>Beranda</b></a><a class="nav-item" href="#shorts"><span>◉</span><b>Shorts</b></a><a class="nav-item" href="#subscriptions"><span>▣</span><b>Langganan</b></a></nav>
<div class="nav-rule"></div><p class="nav-label">Koleksi kamu</p>
<nav class="main-nav"><a class="nav-item" href="#library"><span>▱</span><b>Perpustakaan</b></a><a class="nav-item" href="#history"><span>◷</span><b>Riwayat</b></a><a class="nav-item" href="#liked"><span>♡</span><b>Drama disukai</b></a></nav>
<div class="nav-rule"></div><p class="nav-label">Genre populer</p><nav class="genre-nav" id="genre-nav"></nav><div class="sidebar-footer">Tentang · Ketentuan · Privasi<br /><span>© 2026 YMB</span></div>
</aside>
<main class="content" id="main">
<section class="hero-card" aria-label="Pilihan utama"><div class="hero-copy"><span class="eyebrow" data-i18n="original">YMB ORIGINAL</span><h1><span data-i18n="heroTitle">Cerita pendek,</span><br /><em data-i18n="heroAccent">emosi besar.</em></h1><p data-i18n="heroDescription">Temukan drama yang bikin kamu terus menonton. Episode baru setiap hari.</p><div class="hero-actions"><button class="primary-button" data-play="hero" data-i18n="watchNow">▶ Mulai menonton</button><button class="ghost-button" data-save="hero" data-i18n="save">♡ Simpan</button></div></div><div class="hero-art"><img src="hero.jpg" alt="Drama pendek YMB" /></div></section>
<section class="section-block"><div class="section-heading"><div><span class="eyebrow" data-i18n="recommendation">REKOMENDASI UNTUKMU</span><h2 data-i18n="weeklyPicks">Drama pilihan minggu ini</h2></div><a href="#discover" class="see-all" data-i18n="seeAll">Lihat semua →</a></div><div class="chip-row" id="filter-chips" role="tablist"></div><div class="video-grid" id="video-grid"></div></section>
<section class="section-block shorts-block"><div class="section-heading"><div><span class="eyebrow">SHORTS</span><h2 data-i18n="shortsTitle">Cerita dalam satu tarikan napas</h2></div><a href="#shorts" class="see-all" data-i18n="explore">Jelajahi →</a></div><div class="shorts-row" id="shorts-row"></div></section>
<section class="section-block channel-block"><div class="section-heading"><div><span class="eyebrow" data-i18n="channels">CHANNEL UNTUKMU</span><h2 data-i18n="followStories">Ikuti cerita favoritmu</h2></div><a href="#subscriptions" class="see-all" data-i18n="manage">Kelola →</a></div><div class="channel-grid" id="channel-grid"></div></section>
</main>
</div>
<nav class="bottom-nav" aria-label="Navigasi seluler"><a class="active" href="#home"><span>⌂</span>Beranda</a><a href="#shorts"><span>◉</span>Shorts</a><a href="#subscriptions"><span>▣</span>Langganan</a><a href="#library"><span>▱</span>Koleksi</a></nav>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="data.js"></script><script src="app.js"></script>
</body>
<body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body>
</html>
+9
View File
@@ -0,0 +1,9 @@
{
"name": "ymb-site",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {"dev":"vite","build":"vite build","preview":"vite preview"},
"dependencies": {"@vitejs/plugin-react":"latest","vite":"latest","react":"latest","react-dom":"latest","lucide-react":"latest"},
"devDependencies": {}
}
+497
View File
@@ -0,0 +1,497 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@vitejs/plugin-react':
specifier: latest
version: 6.3.0(vite@8.3.7)
lucide-react:
specifier: latest
version: 1.71.0(react@19.3.1)
react:
specifier: latest
version: 19.3.1
react-dom:
specifier: latest
version: 19.3.1(react@19.3.1)
vite:
specifier: latest
version: 8.3.7
packages:
'@oxc-project/types@0.156.0':
resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==}
'@rolldown/binding-android-arm-eabi@1.2.18':
resolution: {integrity: sha512-ubTG+4KlidvioxjOFdsdEa2F6JVGZb8m2ByJUqQ7fYzUeT1cEn194POcsQ10PKH4p//q0YQn4SB3YpSuzcA1Yg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@rolldown/binding-android-arm64@1.2.18':
resolution: {integrity: sha512-2Xa1t/JeFGTZn5TbzgPppcCftF8sCVnTNvrIvoSdcZhoFzAuVy1L3xxQUQ4sfWDtwVzg1u6Yi1R3SlJugRNV2Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.2.18':
resolution: {integrity: sha512-iUzw9HshVvrWci9gVXjxWS7/M/IcLvNo67vu2c/w9zKBz4hIvPRixdUpIW/iCfhldLaq9ER2hecsI9UY5z0/yA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.2.18':
resolution: {integrity: sha512-XuDHFzbFEtZl0QGJi92+fmG67tfnp1iZ9VOeDkwasGyTq2VG3/5PnJnvAr+MTzFh/rNScJ3VhN+XpZyyGsN7Jw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.2.18':
resolution: {integrity: sha512-MM8opLfsLK15oEXDz6iBc0t3ahu+ew9jsAmha+ovZ33hSwYgqo7YmpRITqFTfW7xA71BLEQ3jJdTsaCmhB1c9w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.2.18':
resolution: {integrity: sha512-J6EoCWiy5RnFQzHrKyHXXevZ9lukQfGpnchX/zG/k/WC2nc9O9XyS1JgA8kI9VxMFggMbCPjpv7LcikriXfehw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.2.18':
resolution: {integrity: sha512-HIkFuLF7CdDeki+1aCZLLbLq0hk/PHntb+z4/YCTo47JbksTD6jJEELIz92tYROmDmvMrNqUMqBi71BkJLDR+A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-arm64-musl@1.2.18':
resolution: {integrity: sha512-4oqWItLLYt7IceOnElCct8T2ek5p7PmD7LN04TRJRXGo5f3HtKTgGH6PhYRFK4C2ICkq/LMUU0BVPvvlAKhm8g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-ppc64-gnu@1.2.18':
resolution: {integrity: sha512-+AkRly5SrCEgnPhGRfub7pucASYEC2fTsxZg7VCcnozpA2+Wn/gCYy1t7LuJS67/xyJu/7noYBJncDUkbwgw2A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
'@rolldown/binding-linux-s390x-gnu@1.2.18':
resolution: {integrity: sha512-Ttg1r9vVykMnQrE/GwYO9icKtXiuiMfRe/w74xJkeoVuvhRG+WtT6LEMOf58Yl63yd77lhHzO8Y55R8CC3XALA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
'@rolldown/binding-linux-x64-gnu@1.2.18':
resolution: {integrity: sha512-xX1Tt4o5jUMma0vJsXp5eRZA9nOuyigb2dKJMMzZRmzMxLYGE9PaKKSqHPzPvSQgM8ZOEacSTQwDQcwWfh0xmQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-linux-x64-musl@1.2.18':
resolution: {integrity: sha512-4VVlGI5K2kociZlW3lSN8VjG3TW5yQIlAcfCdBXMgGjJxZll15Fa4ocRHXHvIVVnwlibCw/NQajJrNDiqqpiiw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-openharmony-arm64@1.2.18':
resolution: {integrity: sha512-PApTsgH1jLYuyy3tSCz9De82Gq5qXwi7vImVZhEzTgX1C4150Cp5M2ZfL3pLvbCvbEHL0oJeC6pd6podtv8Y5w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-win32-arm64-msvc@1.2.18':
resolution: {integrity: sha512-J2XjWjx/SP+4x0H4r6dz3Vzb5WVkg7re40Do9K7HJ3sK2/xcmZrWwakXlSwaMvOkvruLJCD4CL/3wqsIR/thgg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.2.18':
resolution: {integrity: sha512-XwRJxRQvkuEtS0hIIt29QrqPJ+JzMJSNYHJw4ae/mc6uEarfRG1ceyg4HmmWiBdL/YCb6g7N5BLbSvmRJRYZHw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.1.0':
resolution: {integrity: sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==}
'@vitejs/plugin-react@6.3.0':
resolution: {integrity: sha512-H9E955aVvD2F4zNHCw4eIZbIfF6xKldlh7MNu+hSU7xwQzh65lI/N+CJWqmfhi0PEjDXIt/H7T7J/4wU63KJiw==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
'@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 || ^0.3.0
babel-plugin-react-compiler: ^1.0.0
oxc-transform-react: '>=0.152.0 <0.157.0'
vite: ^8.0.0
peerDependenciesMeta:
'@rolldown/plugin-babel':
optional: true
babel-plugin-react-compiler:
optional: true
oxc-transform-react:
optional: true
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
lightningcss-android-arm64@1.33.0:
resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.33.0:
resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.33.0:
resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.33.0:
resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.33.0:
resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.33.0:
resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.33.0:
resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.33.0:
resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.33.0:
resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.33.0:
resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.33.0:
resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.33.0:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'}
lucide-react@1.71.0:
resolution: {integrity: sha512-8YtvF+bqT/EMqAXU7kWvYdRu+TV9j4kS1eIS7IbN1j1Z1DgupbrTL0nDIygdqgBDTD7AJPP607VdVgzDTar4zA==}
peerDependencies:
'@types/react': '*'
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
nanoid@3.3.20:
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.7:
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
engines: {node: '>=12'}
postcss@8.5.32:
resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==}
engines: {node: ^10 || ^12 || >=14}
react-dom@19.3.1:
resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==}
peerDependencies:
react: ^19.3.1
react@19.3.1:
resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==}
engines: {node: '>=0.10.0'}
rolldown@1.2.18:
resolution: {integrity: sha512-C1uuT3UR60SShuCaPQFAAZyefJCWZlAKB02BV3BuPE2Rs4fevRQltEKoFO2tV5W9/eJhU5RieV6vJj3euulfNQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
scheduler@0.28.0:
resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
source-map-js@1.2.2:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
vite@8.3.7:
resolution: {integrity: sha512-oxh0dMxrayBr33ScJvvBJ2HBQ2jc1TBrXKR2NyL/MJ9WPuX19mN2DXWUmywKdyw5RFVQBo8hmtb2FMC0LHrnng==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.7.1 || ^0.8.0
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
'@vitejs/devtools':
optional: true
esbuild:
optional: true
jiti:
optional: true
less:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
snapshots:
'@oxc-project/types@0.156.0': {}
'@rolldown/binding-android-arm-eabi@1.2.18':
optional: true
'@rolldown/binding-android-arm64@1.2.18':
optional: true
'@rolldown/binding-darwin-arm64@1.2.18':
optional: true
'@rolldown/binding-darwin-x64@1.2.18':
optional: true
'@rolldown/binding-freebsd-x64@1.2.18':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.2.18':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.2.18':
optional: true
'@rolldown/binding-linux-arm64-musl@1.2.18':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.2.18':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.2.18':
optional: true
'@rolldown/binding-linux-x64-gnu@1.2.18':
optional: true
'@rolldown/binding-linux-x64-musl@1.2.18':
optional: true
'@rolldown/binding-openharmony-arm64@1.2.18':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.2.18':
optional: true
'@rolldown/binding-win32-x64-msvc@1.2.18':
optional: true
'@rolldown/pluginutils@1.1.0': {}
'@vitejs/plugin-react@6.3.0(vite@8.3.7)':
dependencies:
'@rolldown/pluginutils': 1.1.0
vite: 8.3.7
detect-libc@2.1.2: {}
fdir@6.5.0(picomatch@4.0.7):
optionalDependencies:
picomatch: 4.0.7
fsevents@2.3.3:
optional: true
lightningcss-android-arm64@1.33.0:
optional: true
lightningcss-darwin-arm64@1.33.0:
optional: true
lightningcss-darwin-x64@1.33.0:
optional: true
lightningcss-freebsd-x64@1.33.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.33.0:
optional: true
lightningcss-linux-arm64-gnu@1.33.0:
optional: true
lightningcss-linux-arm64-musl@1.33.0:
optional: true
lightningcss-linux-x64-gnu@1.33.0:
optional: true
lightningcss-linux-x64-musl@1.33.0:
optional: true
lightningcss-win32-arm64-msvc@1.33.0:
optional: true
lightningcss-win32-x64-msvc@1.33.0:
optional: true
lightningcss@1.33.0:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.33.0
lightningcss-darwin-arm64: 1.33.0
lightningcss-darwin-x64: 1.33.0
lightningcss-freebsd-x64: 1.33.0
lightningcss-linux-arm-gnueabihf: 1.33.0
lightningcss-linux-arm64-gnu: 1.33.0
lightningcss-linux-arm64-musl: 1.33.0
lightningcss-linux-x64-gnu: 1.33.0
lightningcss-linux-x64-musl: 1.33.0
lightningcss-win32-arm64-msvc: 1.33.0
lightningcss-win32-x64-msvc: 1.33.0
lucide-react@1.71.0(react@19.3.1):
dependencies:
react: 19.3.1
nanoid@3.3.20: {}
picocolors@1.1.1: {}
picomatch@4.0.7: {}
postcss@8.5.32:
dependencies:
nanoid: 3.3.20
picocolors: 1.1.1
source-map-js: 1.2.2
react-dom@19.3.1(react@19.3.1):
dependencies:
react: 19.3.1
scheduler: 0.28.0
react@19.3.1: {}
rolldown@1.2.18:
dependencies:
'@oxc-project/types': 0.156.0
'@rolldown/pluginutils': 1.1.0
optionalDependencies:
'@rolldown/binding-android-arm-eabi': 1.2.18
'@rolldown/binding-android-arm64': 1.2.18
'@rolldown/binding-darwin-arm64': 1.2.18
'@rolldown/binding-darwin-x64': 1.2.18
'@rolldown/binding-freebsd-x64': 1.2.18
'@rolldown/binding-linux-arm-gnueabihf': 1.2.18
'@rolldown/binding-linux-arm64-gnu': 1.2.18
'@rolldown/binding-linux-arm64-musl': 1.2.18
'@rolldown/binding-linux-ppc64-gnu': 1.2.18
'@rolldown/binding-linux-s390x-gnu': 1.2.18
'@rolldown/binding-linux-x64-gnu': 1.2.18
'@rolldown/binding-linux-x64-musl': 1.2.18
'@rolldown/binding-openharmony-arm64': 1.2.18
'@rolldown/binding-win32-arm64-msvc': 1.2.18
'@rolldown/binding-win32-x64-msvc': 1.2.18
scheduler@0.28.0: {}
source-map-js@1.2.2: {}
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.7)
picomatch: 4.0.7
vite@8.3.7:
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
postcss: 8.5.32
rolldown: 1.2.18
tinyglobby: 0.2.17
optionalDependencies:
fsevents: 2.3.3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

+24
View File
@@ -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 />);
+2
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
// Author: fruit.com.cn
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({ plugins: [react()] });