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 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"], [ "https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&q=80&w=1600", "https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=1600" ], ["https://images.unsplash.com/photo-1553729459-efe14ef6055d?auto=format&fit=crop&q=80&w=1600"] ], description: "The problem\n\nFlux's internal tools lived across disconnected screens: queue states, exceptions, and customer context were hard to scan under pressure, which slowed agents and increased escalations between teams.\n\nConstraints\n\nWe had to improve live dispatch workflows without freezing day-to-day operations—rolling out in stages on web, aligning with existing data models and engineering capacity, and satisfying both ops leadership (speed, accuracy) and internal compliance around what agents can see and do.\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 IA 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: ['Creative Direction: Sedem Oasis', 'Lead Designer: Jane Doe'] }, { title: "Reduced churn by 40% through userflow and UI redesign", category: "Product Design", year: "2023", services: "UX & UI design, Product research, Mobile UI systems", client: "Pillar Finances", imageRows: [ [ "projects/pillar/pillar.png", "projects/pillar/pillar.png" ], ["projects/pillar/pillar.png"], ["projects/pillar/pillar.png"] ], description: "The problem\n\nPillar’s mobile experience was leaking users after signup: key value moments were hard to reach, money actions felt high-friction, and the app’s navigation and states created uncertainty—showing up as churn in the first weeks.\n\nConstraints\n\nWe had to work within existing backend flows and compliance requirements, ship improvements incrementally, and prioritize clarity on small screens—especially for high-stress moments like transfers, verification, and support.\n\nWhat we did\n\nWe mapped churn points across onboarding and core money actions, simplified the information hierarchy, and redesigned the app’s navigation and key flows with clearer status language, safer defaults, and reusable mobile UI patterns.\n\nScope\n\nMobile app UX/UI redesign across onboarding and primary money flows, core navigation + IA, reusable component patterns, and implementation-ready specs for engineering handoff.", outcome: { summary: "The redesigned app made it easier for customers to reach the first value moment, complete key money actions confidently, and understand status at every step—reducing early drop-off and improving retention.", metrics: [ { value: '−40%', label: 'Customer churn (post-redesign cohort)' }, { value: '+28%', label: 'Completion rate on key onboarding steps' }, { value: '−22%', label: 'Support contacts tied to onboarding confusion' } ] }, credits: ['Lead UX: Sedem Oasis', 'Technical Lead: John Smith'] }, { 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", imageRows: [ [ "projects/juventus/juve.png", "projects/juventus/juve.png" ], ["projects/juventus/juve.png"], ["projects/juventus/juve.png"] ], description: "The problem\n\nDespite strong on-field credibility, the academy’s recruitment communications didn’t speak clearly to the right Ghanaian footballers and parents—messages varied across channels and key trust signals (pathway, coaching, tryout details) were easy to miss.\n\nConstraints\n\nThe system had to be fast to deploy for campaign windows, simple for local production partners, and consistent from small social placements to print/community touchpoints—without losing recognizability.\n\nWhat we did\n\nWe defined a tighter recruitment positioning and built a targeted campaign identity system: clearer hierarchy for tryout info, repeatable layouts for posts and flyers, and a visual language tuned for attention and legibility in-feed.\n\nScope\n\nRecruitment positioning, campaign visual system, logo usage rules, color/type system, social + print templates, and lightweight guidelines for vendors and partner teams.", 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: Sedem Oasis', 'Brand Design: Sedem Oasis'] }, { title: "Increased user trust in an AI voice chat app by redesigning the AI engine", category: "AI Product Design", year: "2024", services: "UX & UI design, Conversation design, AI product strategy", client: "Speakless", imageRows: [ ["projects/speakless/speakless.png"], [ "projects/speakless/speakless.png", "projects/speakless/speakless.png" ], ["projects/speakless/speakless.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: ['Design Team: Sedem Oasis', 'Project Management: Alice Wang'] } ]; 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 Work = forwardRef((props, ref) => { const extraProjects = Array.isArray(props.extraProjects) && props.extraProjects.length > 0 ? props.extraProjects : EMPTY_EXTRA_PROJECTS; 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; 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 () => { 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; setActiveProject(modalProjects[prev]); }; const goModalNext = () => { if (activeModalIndex < 0 || modalProjects.length < 2) return; const next = activeModalIndex >= modalProjects.length - 1 ? 0 : activeModalIndex + 1; setActiveProject(modalProjects[next]); }; const modal = activeProject && ReactDOM.createPortal(
setActiveProject(null)} role="presentation" >
e.stopPropagation()} >
{/* Below lg: vertical stack (copy → gallery → nav). lg+: two columns, gallery left / copy right */}

{activeProject.title}

{[ { label: 'Year', value: activeProject.year }, { label: 'Type', value: activeProject.category }, { label: 'Client', value: activeProject.client }, { label: 'Services', value: activeProject.services } ].map((item) => (
{item.label} {item.value}
))}
{[ { id: 'info', label: 'Project Info', content: activeProject.description }, { id: 'outcome', label: 'Outcome', content: normalizeOutcome(activeProject.outcome) }, { id: 'credits', label: 'Credits', content: normalizeCredits(activeProject.credits) } ].map((acc) => (
{/* 0fr/1fr grid avoids a short max-height trap + nested scroll; parent column scrolls the full copy. */}
{acc.id === 'outcome' ? ( <>
{(String(acc.content.summary || '') .split(/\n\n+/) .map((s) => s.trim()) .filter(Boolean)) .map((para, pIdx) => (

{para}

))}
{acc.content.metrics.length > 0 && (
{acc.content.metrics.map((m, mIdx) => (

{m.value}

{m.label}

))}
)} ) : acc.id === 'credits' ? (
    {acc.content.map((line, lineIdx) => (
  • {line}
  • ))}
) : (
{(String(acc.content || '') .split(/\n\n+/) .map((s) => s.trim()) .filter(Boolean)) .map((para, pIdx) => (

{para}

))}
)}
))}
{modalProjects.length > 1 && (
{activeModalIndex >= 0 ? `${String(activeModalIndex + 1).padStart(2, '0')} / ${String(modalProjects.length).padStart(2, '0')}` : '—'}
)}
{/* Below lg: images stack under copy. lg+: left column */}
{activeProject.imageRows.map((row, rowIdx) => { const cells = row.slice(0, 2); if (cells.length === 0) return null; return (
{cells.map((src, cellIdx) => (
{/* Padding-top % = height/width; avoids WebKit flex + aspect-ratio + abspos img paint bugs */} ))}
); })}
{/* Below lg: prev/next after images */} {modalProjects.length > 1 && (
{activeModalIndex >= 0 ? `${String(activeModalIndex + 1).padStart(2, '0')} / ${String(modalProjects.length).padStart(2, '0')}` : '—'}
)}
, document.body ); return ( <> {modal}

Selected Work

{WORK_FEATURED_PROJECTS.map((project, i) => (
{ // setActiveProject(project); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); // setActiveProject(project); } }} aria-label={project.title} className="project-card group cursor-pointer flex-none w-[min(80vw,20rem)] sm:w-auto sm:h-[min(65vh,100%)] md:h-[min(72vh,100%)] max-h-[min(42rem,100%)] aspect-[1080/1350] flex flex-col justify-end relative overflow-hidden rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-black/20 focus-visible:ring-offset-2" >
{project.title}

{project.title}

{project.category}

))}
); }); window.Work = Work; window.SEDEM_OPEN_ARCHIVE_PROJECT_EVENT = OPEN_ARCHIVE_PROJECT_EVENT;