128 lines
5.7 KiB
TypeScript
128 lines
5.7 KiB
TypeScript
import React, { useState } from 'react'
|
|
import { ArrowLeftIcon } from 'lucide-react'
|
|
import { AnimatePresence, motion } from 'framer-motion'
|
|
import { Reveal } from '../Reveal'
|
|
import { ButtonLink } from '../Button'
|
|
import { ProjectCard } from '../ProjectCard'
|
|
import { SectionHeading } from '../SectionHeading'
|
|
import { useSectionHeading, useSite, useStatusLabel } from '../../api'
|
|
import { useFieldSaver } from '../../admin/AdminContext'
|
|
import { InlineEdit } from '../admin/InlineEdit'
|
|
import { AddItemButton, EditableItem } from '../../admin/Editable'
|
|
import type { ProjectStatus } from '../../data/site'
|
|
|
|
export function FeaturedProjects() {
|
|
const { projects, status_labels } = useSite()
|
|
const heading = useSectionHeading('featured_projects')
|
|
const save = useFieldSaver()
|
|
const saveStatusLabel = (key: string, value: string) => {
|
|
const label = status_labels.find((s) => s.key === key)
|
|
if (!label) return
|
|
void save('status-labels', label.id, { label: value }, (d) => {
|
|
const s = d.status_labels.find((x) => x.id === label.id)
|
|
if (s) s.label = value
|
|
})
|
|
}
|
|
|
|
const FILTERS: { key: ProjectStatus; label: string }[] = [
|
|
{ key: 'done', label: useStatusLabel('done') },
|
|
{ key: 'progress', label: useStatusLabel('progress') },
|
|
]
|
|
const [status, setStatus] = useState<ProjectStatus>('done')
|
|
const list = projects.filter((p) => p.status === status).slice(0, 3)
|
|
|
|
return (
|
|
<section className="bg-ink py-24 lg:py-32" aria-labelledby="projects-title">
|
|
<div className="mx-auto max-w-site px-6 lg:px-10">
|
|
<SectionHeading title={heading.title} lead={heading.lead} headingKey="featured_projects" />
|
|
|
|
{/* دکمههای فیلتر مینیمال و شیشهای (بدون کادر) */}
|
|
<div className="mt-10 flex items-center justify-center gap-4 md:gap-6" role="tablist" aria-label="فیلتر وضعیت پروژه">
|
|
{FILTERS.map((filter, index) => {
|
|
const isSelected = status === filter.key
|
|
return (
|
|
<React.Fragment key={filter.key}>
|
|
{/* خط جداکننده بین دکمهها */}
|
|
{index > 0 && (
|
|
<span className="h-6 w-[2px] rounded-full bg-ivory/10" aria-hidden="true" />
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={isSelected}
|
|
onClick={() => setStatus(filter.key)}
|
|
className={`group relative rounded-lg px-6 py-2.5 text-sm font-medium transition-all duration-300 ease-lux active:scale-95 cursor-pointer select-none backdrop-blur-md ${
|
|
isSelected
|
|
? 'bg-copper/15 text-copper shadow-[0_4px_15px_rgba(197,143,92,0.1)]'
|
|
: 'bg-transparent text-ivory/60 hover:bg-white/5 hover:text-ivory'
|
|
}`}
|
|
>
|
|
<span className="relative z-10 transition-colors duration-300 ease-out">
|
|
<InlineEdit
|
|
value={filter.label}
|
|
onSave={(v) => saveStatusLabel(filter.key, v)}
|
|
/>
|
|
</span>
|
|
</button>
|
|
</React.Fragment>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<div className="mt-10 min-h-[420px]">
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={status}
|
|
initial={{ opacity: 0, y: 15 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -15 }}
|
|
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
|
|
className="grid gap-6 lg:grid-cols-3"
|
|
>
|
|
{list.map((project, i) => (
|
|
<motion.div
|
|
key={project.id}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: i * 0.1, ease: [0.23, 1, 0.32, 1] }}
|
|
className="h-full"
|
|
>
|
|
<EditableItem resource="projects" item={project} className="h-full">
|
|
<ProjectCard project={project} />
|
|
</EditableItem>
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
{/* دکمه مشاهده همه پروژهها */}
|
|
{heading.cta_label && heading.cta_href && (
|
|
<Reveal delay={0.1}>
|
|
<div className="mt-12 flex justify-center">
|
|
<ButtonLink
|
|
to={heading.cta_href}
|
|
variant="outline"
|
|
className="group flex items-center gap-3 rounded-full border border-white/20 bg-white/[0.04] px-8 py-3.5 text-sm backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.15),0_8px_20px_rgba(0,0,0,0.3)] transition-all duration-300 hover:border-copper hover:bg-copper/10 hover:shadow-[0_0_30px_rgba(197,143,92,0.35)]"
|
|
>
|
|
<InlineEdit
|
|
value={heading.cta_label}
|
|
onSave={(v) =>
|
|
save('section-headings', heading.id, { cta_label: v }, (d) => {
|
|
const h = d.section_headings.find((s) => s.id === heading.id)
|
|
if (h) h.cta_label = v
|
|
})
|
|
}
|
|
/>
|
|
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-white/10 text-copper transition-all duration-300 group-hover:-translate-x-1 group-hover:bg-copper group-hover:text-char">
|
|
<ArrowLeftIcon className="h-3.5 w-3.5" />
|
|
</span>
|
|
</ButtonLink>
|
|
</div>
|
|
</Reveal>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)
|
|
} |