// FloatingPortfolio // -------------------------------------------------------------------------- // Auto-loads every file dropped into /assets/portfolio/ and renders it as a // tile in the scrolling strip. The folder is scanned by assets/portfolio/list.php // (PHP runs on Hostinger by default). Drop a .mp4/.webm/.gif in that folder and // it appears here on refresh — no code editing. // // The caption is derived from the file name: "01-haya-bitar.mp4" -> "Haya Bitar" // // If list.php can't be reached (e.g. a plain local preview with no PHP), it // falls back to the demo tiles below so the section never looks empty. // -------------------------------------------------------------------------- const PORTFOLIO_DIR = 'assets/portfolio/'; const VIDEO_EXTS = ['mp4', 'webm', 'mov', 'm4v', 'ogv']; // Shown only when the live folder can't be read (local preview without PHP). // Kept in sync with the real files in assets/portfolio/ so the preview matches. const FALLBACK_TILES = [ { src: 'assets/portfolio/1.mp4', caption: '' }, { src: 'assets/portfolio/2.mp4', caption: '' }, { src: 'assets/portfolio/3.mp4', caption: '' }, { src: 'assets/portfolio/4.mp4', caption: '' }, { src: 'assets/portfolio/5.mp4', caption: '' }, { src: 'assets/portfolio/6.mp4', caption: '' }, { src: 'assets/portfolio/7.mp4', caption: '' }, { src: 'assets/portfolio/8.mp4', caption: '' }, { src: 'assets/portfolio/9.mp4', caption: '' }, ]; function captionFromFilename(name) { const base = name.replace(/\.[^.]+$/, ''); // drop extension // Treat order-only / code-style names (e.g. "1", "01", "a1", "b2") as no caption. if (/^\d+$/.test(base) || /^[a-z]\d+$/i.test(base)) return ''; return base .replace(/^\s*\d+\s*[-_.]\s*/, '') // drop a leading order prefix like "01-" .replace(/[-_]+/g, ' ') // dashes/underscores -> spaces .replace(/\s+/g, ' ') .trim() .replace(/\b\w/g, (c) => c.toUpperCase()); // Title Case } function isVideo(src) { const ext = (src.split('.').pop() || '').toLowerCase(); return VIDEO_EXTS.includes(ext); } function FloatingPortfolio() { const [tiles, setTiles] = React.useState(null); // null = still loading React.useEffect(() => { let cancelled = false; fetch(PORTFOLIO_DIR + 'list.php', { cache: 'no-cache' }) .then((r) => (r.ok ? r.json() : Promise.reject())) .then((files) => { if (cancelled) return; if (Array.isArray(files) && files.length) { setTiles( files.map((f) => ({ src: PORTFOLIO_DIR + f, caption: captionFromFilename(f) })) ); } else { setTiles(FALLBACK_TILES); // folder reachable but empty } }) .catch(() => { if (!cancelled) setTiles(FALLBACK_TILES); // no PHP / not found }); return () => { cancelled = true; }; }, []); const list = tiles || FALLBACK_TILES; // Duplicate the array for a seamless marquee loop. const loop = [...list, ...list]; return (
{/* edge fades */}
{loop.map((tile, i) => (
{isVideo(tile.src) ? (
))}
); } Object.assign(window, { FloatingPortfolio });