const { useState, useEffect, useLayoutEffect, useRef, useMemo, useImperativeHandle, forwardRef } = React; const Section = window.Section; /* imageRows: each entry is one row: 1 URL (full width) or 2 URLs (side by side, max 2). */ const WORK_FEATURED_PROJECTS = [ { title: "Increased agent efficiency by 40% through AI logistics platform redesign", category: "Product Design", year: "2025", services: "UX & UI design, Product research, Design systems", client: "Flux Logistics", /* Card thumbnail only; modal gallery uses imageRows (first cell opens as flux1). */ thumbnailSrc: "projects/flux/flux.png", imageRows: [ ["projects/flux/flux1.png"], ["projects/flux/flux_2.mov", "projects/flux/flux_3.png"], ["projects/flux/flux_4.mov"], ["projects/flux/flux_5.png"], ], description: "The problem\n\nFlux's user base was growing however, they had some inefficiencies in their internal processes and tools which impacted revenue. Their internal tools lived across disconnected platforms causing errors and inaccuracies, which slowed agents and increased escalations between teams.\n\nConstraints\n\nWe had a few weeks to improve live workflows without freezing day-to-day operations, rolling out in stages on web, aligning with existing data models and engineering capacity.\n\nWhat we did\n\nWe reframed the day-in-the-life of an agent, tightened information hierarchy for critical paths, and redesigned core workspace patterns for triage, resolution, and handoff, grounded in research with agents and team leads.\n\nScope\n\nInternal agent platform UX and UI, workflow and AI exploration, prototyping, and a lightweight component system covering primary operational surfaces (web).", outcome: { summary: "After rollout, operations saw a calmer default workspace for agents: fewer context switches, clearer exception handling, and a UI foundation the product team could extend without fragmenting patterns.", metrics: [ { value: '~40%', label: 'Lift in agent efficiency (ops-reported throughput)' }, { value: '−32%', label: 'Reduction in escalations between dispatch tiers' }, { value: '3 mo', label: 'Staged rollout without freezing live dispatch' } ] }, credits: ['Lead UX: Albert Hodo', 'Technical Lead: Rahul Srinivas'] }, { title: "Reduced churn by 40% through early stage redesign", category: "Product Design", year: "2026", services: "UX & UI design, Product research, Mobile UI systems", client: "Pillar Finances", thumbnailSrc: "projects/pillar/pillar.png", imageRows: [ ["projects/pillar/pillar1.png"], ["projects/pillar/pillar2.mp4"], ["projects/pillar/p2.png"], // ["projects/pillar/p6.mp4", "projects/pillar/p4.mp4"], // ["projects/pillar/p3.mp4"] ], description: "The problem\n\nPillar’s mobile experience struggled to establish user trust after signup. Limited feedback, unclear system states, and friction in sensitive actions like transfers and verification reduced confidence in the product, leading to early churn.\n\nConstraints\n\n Rapid timelines, limited resources, and a small testing base for validation. Solutions had to work within existing backend and compliance constraints while being fast to implement.\n\nWhat we did\n\nDiagnosed trust gaps across onboarding and core flows, then redesigned key interactions to improve clarity, transparency, and feedback. Simplified navigation and introduced consistent UI patterns to reinforce reliability and user confidence.\n\nScope\n\nMobile UX/UI redesign across onboarding and primary money flows, navigation and IA, reusable component system, and engineering-ready specifications, with ongoing iteration post-release.", outcome: { summary: "Improved clarity, transparency, and feedback increased user trust, resulting in higher activation and lower early drop-off.", metrics: [ { value: '−40%', label: 'Churn (early lifecycle)' }, { value: '+28%', label: 'Onboarding completion' }, { value: '+18%', label: 'First successful transaction rate' }, { value: '+25%', label: 'User confidence in key flows' } ] }, credits: ['Lead UX: Rahul Srinivas', 'Product: Albert Hodo'] }, { title: "Boosted academy applications by 30% through targeted recruitment system", category: "Brand Identity", year: "2023", services: "Recruitment positioning, Campaign identity system, Templates & Guidelines", client: "Juventus Academy Ghana", thumbnailSrc: "projects/juventus/juve.png", imageRows: [ ["projects/juventus/Artboard%201.png"], ["projects/juventus/Artboard%202.png"], ["projects/juventus/Artboard%203.png"], ["projects/juventus/Artboard%204.png"], ["projects/juventus/Artboard%205.png"], ["projects/juventus/Artboard%206.png"], ["projects/juventus/Artboard%207.png"], ["projects/juventus/Artboard%208.png"], ["projects/juventus/Artboard%209.png"] ], description: "The problem\n\nDespite strong on-field credibility, the academy’s communications didn’t clearly convey trust and opportunity to Ghanaian players and parents. Recruitment messaging was inconsistent, and key signals: pathway, coaching quality, and tryout details were easy to miss.\n\nConstraints\n\nShort campaign windows, limited production resources, and reliance on local vendors. The system had to be easy to apply across digital, print, and on-ground environments while staying consistent and recognizable.\n\nWhat we did\n\nClarified recruitment positioning and built a cohesive campaign system with clear information hierarchy and repeatable layouts. Extended the identity into physical and experiential touchpoints, including in-field medic uniforms and branded materials for academy events, to reinforce credibility and memorability.\n\nScope\n\nRecruitment positioning, campaign visual system, social and print templates, identity guidelines, plus branded applications across uniforms and event materials for consistent on- and off-field presence.", outcome: { summary: "Recruitment comms became consistent and legible across channels, helping the academy reach more Ghanaian footballers with a clearer story and a stronger, repeatable campaign system.", metrics: [ { value: '+30%', label: 'Applications to academy programs (campaign period)' }, { value: '+42%', label: 'Tryout landing engagement from social (link CTR)' }, { value: '10+', label: 'Reusable templates shipped for recruitment + events' } ] }, credits: ['Lead Creative: Frecy Tenkorang'] }, { title: "Increased user trust in an AI voice chat app by redesigning the AI engine", category: "AI Product Design", year: "2025", services: "UX & UI design, Conversation design, AI product strategy", client: "Speakless", thumbnailSrc: "projects/speakless/speakless.png", imageRows: [ ["projects/speakless/1.png"], ["projects/speakless/2.png"], [ "projects/speakless/3.png"], ["projects/speakless/4.png"], ["projects/speakless/5.png"] ], description: "The problem\n\nUsers didn’t trust the assistant: responses sounded confident but were inconsistent, voice interactions felt unpredictable, and error states made it unclear whether the AI had understood or was guessing.\n\nConstraints\n\nWe had to improve reliability without slowing the experience, work within latency and cost constraints for real-time voice, and ship changes safely while protecting user privacy.\n\nWhat we did\n\nWe redesigned the “AI engine” as a product system, tightening prompt and tool boundaries, clarifying when the assistant should ask questions vs. act, and pairing that with UI patterns that make confidence, sources, and status legible in voice-first flows.\n\nScope\n\nVoice chat UX, conversation patterns and guardrails, model behavior design (prompts, tool use, refusal rules), failure mode handling, and end-to-end interaction prototypes for engineering.", outcome: { summary: "The new engine + UX reduced “mystery moments” in voice chat: users understood what the assistant was doing, why it asked questions, and how to recover when something went wrong, leading to stronger trust signals and retention.", metrics: [ { value: '+38%', label: 'Self-reported trust in the assistant (post-session survey)' }, { value: '−27%', label: '“AI didn’t understand me” reports (support + in-app)' }, { value: '+19%', label: '7-day retention for voice-first users' } ] }, credits: ['AI System: Albert Hodo, Rahul Srinivas'] } ]; const EMPTY_EXTRA_PROJECTS = []; /* AllWorks opens archive rows via this event so it still works when Babel/CDN drops unknown props on custom components. */ const OPEN_ARCHIVE_PROJECT_EVENT = 'sedem-oasis:open-archive-project'; const normalizeOutcome = (outcome) => { if (outcome && typeof outcome === 'object' && outcome.summary != null) { return { summary: String(outcome.summary), metrics: Array.isArray(outcome.metrics) ? outcome.metrics : [] }; } return { summary: String(outcome || ''), metrics: [] }; }; const normalizeCredits = (credits) => { if (Array.isArray(credits)) { return credits.map(String).map((s) => s.trim()).filter(Boolean); } const s = String(credits || '').trim(); if (!s) return []; if (s.includes('\n')) return s.split(/\n+/).map((x) => x.trim()).filter(Boolean); return s.split(/\s*,\s*/).map((x) => x.trim()).filter(Boolean); }; const isVideoSrc = (src) => /\.(mp4|webm|mov)(\?.*)?$/i.test(String(src || '')); const videoMimeType = (src) => { const s = String(src || '').toLowerCase(); if (s.includes('.webm')) return 'video/webm'; if (s.includes('.mp4')) return 'video/mp4'; if (s.includes('.mov')) return 'video/quicktime'; return ''; }; const Work = forwardRef((props, ref) => { const compactWorkLayout = props.compactWorkLayout === true; const extraProjects = Array.isArray(props.extraProjects) && props.extraProjects.length > 0 ? props.extraProjects : EMPTY_EXTRA_PROJECTS; const modalOpenedAtRef = useRef(null); const scrollRef = useRef(null); const closeBtnRef = useRef(null); const modalGalleryRef = useRef(null); const [canScrollLeft, setCanScrollLeft] = useState(false); const [canScrollRight, setCanScrollRight] = useState(true); const [activeProject, setActiveProject] = useState(null); const [openAccordion, setOpenAccordion] = useState('info'); const modalProjects = useMemo( () => [...WORK_FEATURED_PROJECTS, ...extraProjects], [extraProjects] ); useImperativeHandle(ref, () => ({ openProject: (project) => setActiveProject(project) }), []); useEffect(() => { const onOpenArchive = (e) => { const p = e?.detail?.project; if (p && p.title) setActiveProject(p); }; window.addEventListener(OPEN_ARCHIVE_PROJECT_EVENT, onOpenArchive); return () => window.removeEventListener(OPEN_ARCHIVE_PROJECT_EVENT, onOpenArchive); }, []); const checkScroll = () => { if (scrollRef.current) { const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current; setCanScrollLeft(scrollLeft > 5); setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 5); } }; useEffect(() => { const el = scrollRef.current; if (el) { setTimeout(checkScroll, 100); el.addEventListener('scroll', checkScroll); window.addEventListener('resize', checkScroll); } return () => { if (el) el.removeEventListener('scroll', checkScroll); window.removeEventListener('resize', checkScroll); }; }, []); useEffect(() => { if (!activeProject) return; modalOpenedAtRef.current = Date.now(); window.soTrack?.('modal_open', { project_title: activeProject.title, project_client: activeProject.client, project_category: activeProject.category }); const prevOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; const idx = modalProjects.findIndex((p) => p.title === activeProject.title); const onKeyDown = (e) => { if (e.key === 'Escape') { setActiveProject(null); return; } if (modalProjects.length < 2 || idx < 0) return; if (e.key === 'ArrowLeft') { e.preventDefault(); const prev = idx <= 0 ? modalProjects.length - 1 : idx - 1; setActiveProject(modalProjects[prev]); } if (e.key === 'ArrowRight') { e.preventDefault(); const next = idx >= modalProjects.length - 1 ? 0 : idx + 1; setActiveProject(modalProjects[next]); } }; window.addEventListener('keydown', onKeyDown); const t = requestAnimationFrame(() => closeBtnRef.current?.focus()); return () => { const startedAt = modalOpenedAtRef.current; const durationMs = typeof startedAt === 'number' ? Math.max(0, Date.now() - startedAt) : null; window.soTrack?.('modal_close', { project_title: activeProject.title, duration_ms: durationMs }); document.body.style.overflow = prevOverflow; window.removeEventListener('keydown', onKeyDown); cancelAnimationFrame(t); }; }, [activeProject, modalProjects]); /* Scroll reset + layout flush after portal paints. Double rAF avoids flex/aspect first-frame bugs (WebKit: aspect-ratio + abspos img often skips paint until reflow). */ useLayoutEffect(() => { if (!activeProject) return; let innerRaf; const flushScroll = () => { const el = modalGalleryRef.current; if (!el) return; el.scrollTop = 0; void el.getBoundingClientRect(); }; flushScroll(); const outerRaf = requestAnimationFrame(() => { innerRaf = requestAnimationFrame(flushScroll); }); return () => { cancelAnimationFrame(outerRaf); cancelAnimationFrame(innerRaf); }; }, [activeProject]); const scroll = (direction) => { if (scrollRef.current) { const scrollAmount = window.innerWidth > 768 ? window.innerWidth * 0.45 : window.innerWidth * 0.8; scrollRef.current.scrollBy({ left: direction === 'left' ? -(scrollAmount + 32) : (scrollAmount + 32), behavior: 'smooth' }); } }; const activeModalIndex = activeProject ? modalProjects.findIndex((p) => p.title === activeProject.title) : -1; const goModalPrev = () => { if (activeModalIndex < 0 || modalProjects.length < 2) return; const prev = activeModalIndex <= 0 ? modalProjects.length - 1 : activeModalIndex - 1; window.soTrack?.('modal_nav', { direction: 'prev', from_index: activeModalIndex }); setActiveProject(modalProjects[prev]); }; const goModalNext = () => { if (activeModalIndex < 0 || modalProjects.length < 2) return; const next = activeModalIndex >= modalProjects.length - 1 ? 0 : activeModalIndex + 1; window.soTrack?.('modal_nav', { direction: 'next', from_index: activeModalIndex }); setActiveProject(modalProjects[next]); }; const modal = activeProject && ReactDOM.createPortal(
{para}
))}{m.value}
{m.label}
{para}
))}{project.category}