Website / Projects.jsx
Gokul08r's picture
Upload 31 files
e927672 verified
Raw
History Blame Contribute Delete
6.3 kB
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(
<div className="projects">
<div className="container">
<div className="section-label">My Work</div>
<h2 className="section-title">Featured <span>Projects</span></h2>
{!loading&&!fetchErr&&allTags.length>1&&(
<div className="proj-filters">
{allTags.slice(0,9).map(t=>(
<button key={t} className={`pf-btn${filter===t?' active':''}`} onClick={()=>setFilter(t)}>{t}</button>
))}
</div>
)}
{loading?(
<div className="proj-skel-grid">{[1,2,3].map(i=><div key={i} className="proj-skel"/>)}</div>
):fetchErr?(
<div className="proj-error">
<FiAlertCircle/>
<p>Could not load projects.</p>
<p className="proj-error-detail">{fetchErr}</p>
<button className="btn-outline proj-retry" onClick={load}><FiRefreshCw/> Retry</button>
</div>
):list.length===0?(
<div className="proj-empty"><FiPackage/>
<p>{projects.length===0?'Projects coming soon!':'No projects match this filter.'}</p>
</div>
):slide?(
<div className="proj-slide-wrap"
onMouseEnter={()=>setPaused(true)} onMouseLeave={()=>setPaused(false)}>
<div className="proj-stage">
{visible3().map(({p,pos})=>(
<ProjCard key={`${p.id}-${pos}`} project={p} pos={pos}/>
))}
</div>
<button className="slide-arrow left" onClick={()=>{prev();setPaused(true)}}><FiChevronLeft/></button>
<button className="slide-arrow right" onClick={()=>{next();setPaused(true)}}><FiChevronRight/></button>
<div className="slide-dots">
{list.map((_,i)=>(
<button key={i} className={`sd${i===cur?' on':''}`} onClick={()=>{setCur(i);setPaused(true)}}/>
))}
</div>
{!paused&&<div className="slide-prog"><div className="slide-prog-bar" key={cur}/></div>}
</div>
):(
<div className="proj-grid">
{list.map((p,i)=><ProjCard key={p.id} project={p} index={i} grid/>)}
</div>
)}
</div>
</div>
)
}
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(
<div className={`proj-card glass-card${grid?` fade-in`:` sl-card ${pc}`}`}
style={grid?{animationDelay:`${index*0.11}s`}:{}}>
<div className="proj-img-wrap">
{src?<img src={src} alt={p.title} className="proj-img" loading="lazy"/>
:<div className="proj-img-ph"><FiCode/></div>}
<div className="proj-overlay">
{p.project_link&&<a href={p.project_link} target="_blank" rel="noopener noreferrer" className="ov-btn"><FiExternalLink/>Live</a>}
{p.github_link&&<a href={p.github_link} target="_blank" rel="noopener noreferrer" className="ov-btn ghost"><FiGithub/>Code</a>}
</div>
</div>
<div className="proj-body">
{p.duration&&<div className="proj-dur"><FiClock/>{p.duration}</div>}
<h3 className="proj-title">{p.title}</h3>
<p className="proj-desc">{p.description}</p>
{tools.length>0&&<div className="proj-tools">{tools.map((t,i)=><span key={i} className="pt">{t}</span>)}</div>}
<div className="proj-links">
{p.project_link&&<a href={p.project_link} target="_blank" rel="noopener noreferrer" className="pl-btn"><FiExternalLink/>View Project</a>}
{p.github_link&&<a href={p.github_link} target="_blank" rel="noopener noreferrer" className="pl-btn ghost"><FiGithub/>Source</a>}
</div>
</div>
</div>
)
}