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.title}{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.title}{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