import React,{useEffect,useState,useRef,useCallback} from 'react' import {supabase} from '../lib/supabase' import { FiExternalLink,FiGithub,FiClock,FiCode, FiChevronLeft,FiChevronRight,FiPackage,FiAlertCircle,FiRefreshCw } from 'react-icons/fi' import './Projects.css' const BUCKET='Portfolio',FOLDER='projects_image',SLIDE_AT=3 function imgUrl(item){ if(!item?.image_url) return null if(item.image_url.startsWith('http')) return item.image_url return supabase.storage.from(BUCKET).getPublicUrl(`${FOLDER}/${item.image_url}`).data.publicUrl } function parseTools(t){ if(!t) return [] if(Array.isArray(t)) return t try{return JSON.parse(t)}catch{return t.split(',').map(s=>s.trim()).filter(Boolean)} } async function fetchProjects(){ const{data,error}=await supabase.from('projects').select('*').order('id',{ascending:false}) if(error) throw error return data??[] } export default function Projects(){ const[projects,setProjects]=useState([]) const[loading,setLoading]=useState(true) const[fetchErr,setFetchErr]=useState(null) const[filter,setFilter]=useState('All') const[cur,setCur]=useState(0) const[paused,setPaused]=useState(false) const timer=useRef(null) const load=useCallback(()=>{ setLoading(true);setFetchErr(null) fetchProjects() .then(data=>{setProjects(data);setFetchErr(null)}) .catch(err=>setFetchErr(err.message||'Failed to load projects')) .finally(()=>setLoading(false)) },[]) useEffect(()=>{load()},[load]) useEffect(()=>{ const ch=supabase.channel('projects-changes') .on('postgres_changes',{event:'*',schema:'public',table:'projects'},()=>fetchProjects().then(setProjects).catch(console.error)) .subscribe() return()=>{supabase.removeChannel(ch)} },[]) const allTags=['All',...new Set(projects.flatMap(p=>parseTools(p.tools)))] const list=filter==='All'?projects:projects.filter(p=>parseTools(p.tools).includes(filter)) const slide=list.length>SLIDE_AT const next=useCallback(()=>{if(slide)setCur(c=>(c+1)%list.length)},[slide,list.length]) const prev=useCallback(()=>{if(slide)setCur(c=>(c-1+list.length)%list.length)},[slide,list.length]) useEffect(()=>{ if(!slide||paused) return timer.current=setInterval(next,4000) return()=>clearInterval(timer.current) },[slide,paused,next]) useEffect(()=>setCur(0),[filter]) const visible3=()=>{ const v=[] for(let o=-1;o<=1;o++){ const i=(cur+o+list.length)%list.length v.push({p:list[i],pos:o}) } return v } return(
My Work

Featured Projects

{!loading&&!fetchErr&&allTags.length>1&&(
{allTags.slice(0,9).map(t=>( ))}
)} {loading?(
{[1,2,3].map(i=>
)}
):fetchErr?(

Could not load projects.

{fetchErr}

):list.length===0?(

{projects.length===0?'Projects coming soon!':'No projects match this filter.'}

):slide?(
setPaused(true)} onMouseLeave={()=>setPaused(false)}>
{visible3().map(({p,pos})=>( ))}
{list.map((_,i)=>(
{!paused&&
}
):(
{list.map((p,i)=>)}
)}
) } function ProjCard({project:p,index=0,pos=0,grid=false}){ const src=imgUrl(p) const tools=parseTools(p.tools) const pc=pos===-1?'sl-left':pos===1?'sl-right':'sl-center' return(
{src?{p.title} :
}
{p.project_link&&Live} {p.github_link&&Code}
{p.duration&&
{p.duration}
}

{p.title}

{p.description}

{tools.length>0&&
{tools.map((t,i)=>{t})}
}
{p.project_link&&View Project} {p.github_link&&Source}
) }