diff --git a/docs/plans/2026-11-10-ymb-api-logo-player.md b/docs/plans/2026-11-10-ymb-api-logo-player.md
new file mode 100644
index 0000000..e69de29
diff --git a/public/brand/ymb-logo-blue.png b/public/brand/ymb-logo-blue.png
new file mode 100644
index 0000000..0990ed2
Binary files /dev/null and b/public/brand/ymb-logo-blue.png differ
diff --git a/public/brand/ymb-logo-gold.png b/public/brand/ymb-logo-gold.png
new file mode 100644
index 0000000..406a788
Binary files /dev/null and b/public/brand/ymb-logo-gold.png differ
diff --git a/src/app/App.jsx b/src/app/App.jsx
index 8a17e09..edcef91 100644
--- a/src/app/App.jsx
+++ b/src/app/App.jsx
@@ -6,5 +6,6 @@ import { HomePage } from '../pages/HomePage';
import { ListingPage } from '../pages/ListingPage';
import { SearchPage } from '../pages/SearchPage';
import { WatchPage } from '../pages/WatchPage';
+import { ShortsFeedPage } from '../pages/ShortsFeedPage';
-export function App() { return }>} />} />} />} />} />} />} />; }
+export function App() { return }>} />} />} />} />} />} />} />; }
diff --git a/src/app/store.jsx b/src/app/store.jsx
index 560fb3a..b21d0ed 100644
--- a/src/app/store.jsx
+++ b/src/app/store.jsx
@@ -1,7 +1,9 @@
// Author: fruit.com.cn
import { createContext, useContext, useMemo, useState } from 'react';
import { getLocale } from '../i18n';
+import { applySeo } from '../lib/seo';
+import { useEffect } from 'react';
const StoreContext = createContext(null);
-export function StoreProvider({ children }) { const initial = new URLSearchParams(location.search).get('lang'); const [lang, setLangState] = useState(initial || 'id'); const [query, setQuery] = useState(''); const [sidebarOpen, setSidebarOpen] = useState(false); const setLang = (next) => { setLangState(next); const url = new URL(location.href); url.searchParams.set('lang', next); history.replaceState(null, '', url); }; const value = useMemo(() => ({ lang, locale: getLocale(lang), setLang, query, setQuery, sidebarOpen, setSidebarOpen }), [lang, query, sidebarOpen]); return {children}; }
+export function StoreProvider({ children }) { const initial = new URLSearchParams(location.search).get('lang'); const [lang, setLangState] = useState(initial || 'id'); const [query, setQuery] = useState(''); const [sidebarOpen, setSidebarOpen] = useState(false); const setLang = (next) => { setLangState(next); const url = new URL(location.href); url.searchParams.set('lang', next); history.replaceState(null, '', url); }; const locale = getLocale(lang); useEffect(() => applySeo(locale, location.pathname), [lang, locale]); const value = useMemo(() => ({ lang, locale, setLang, query, setQuery, sidebarOpen, setSidebarOpen }), [lang, locale, query, sidebarOpen]); return {children}; }
export function useAppStore() { return useContext(StoreContext); }
diff --git a/src/components/LanguageSwitcher.jsx b/src/components/LanguageSwitcher.jsx
new file mode 100644
index 0000000..4e8f778
--- /dev/null
+++ b/src/components/LanguageSwitcher.jsx
@@ -0,0 +1,10 @@
+// Author: fruit.com.cn
+import { Check, ChevronDown, Globe2 } from 'lucide-react';
+import { useState } from 'react';
+import { useAppStore } from '../app/store';
+
+const languages = [{ code: 'id', label: 'Bahasa Indonesia', short: 'ID', flag: '🇮🇩' }, { code: 'en', label: 'English', short: 'EN', flag: '🇺🇸' }, { code: 'zh', label: '简体中文', short: '中文', flag: '🇨🇳' }];
+export function LanguageSwitcher() {
+ const { lang, setLang } = useAppStore(); const [open, setOpen] = useState(false); const current = languages.find((item) => item.code === lang) || languages[0];
+ return
{open && <>
;
+}
diff --git a/src/components/MediaCard.jsx b/src/components/MediaCard.jsx
index 49db136..237c4f0 100644
--- a/src/components/MediaCard.jsx
+++ b/src/components/MediaCard.jsx
@@ -3,5 +3,6 @@ import { Play } from 'lucide-react';
import { Link } from 'react-router-dom';
export function MediaCard({ video, compact = false }) {
- return
{video.duration}{video.channel[0]}{video.title}{video.channel}
{video.views} · 2 hari lalu
;
+ const image = video.image?.startsWith('/') || video.image?.startsWith('http') ? video.image : `/assets/${video.image}`;
+ return
{video.duration}{video.channel?.[0] || 'Y'}{video.title}{video.channel}
{video.views} · 2 hari lalu
;
}
diff --git a/src/components/player/VideoPlayer.jsx b/src/components/player/VideoPlayer.jsx
new file mode 100644
index 0000000..27c676c
--- /dev/null
+++ b/src/components/player/VideoPlayer.jsx
@@ -0,0 +1,8 @@
+// Author: fruit.com.cn
+import { useEffect, useRef } from 'react';
+
+export function VideoPlayer({ src, poster, autoPlay = true, muted = true, onProgress }) {
+ const ref = useRef(null);
+ useEffect(() => { const video = ref.current; if (!video || !src) return undefined; const attempt = video.play(); attempt?.catch(() => {}); return () => video.pause(); }, [src]);
+ return