merge(mobile): ادغام شاخهٔ new-one2 — طراحی ریسپانسیو صفحهٔ اصلی (کلاسهای breakpoint در همهٔ بخشها) با حفظ تغییرات طراحی ما: تیم وسطچین بدون دایره، پروژههای شاخص بدون تب، تناوب پسزمینه (tone)، پسزمینهٔ پیوستهٔ تماس و منطق دوزبانهٔ مقالات
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowLeftIcon } from 'lucide-react';
|
||||
import { Reveal } from '../Reveal';
|
||||
import { ButtonLink } from '../Button';
|
||||
@@ -8,6 +9,7 @@ import { useLang } from '../../i18n/lang';
|
||||
import { useFieldSaver } from '../../admin/AdminContext';
|
||||
import { InlineEdit } from '../admin/InlineEdit';
|
||||
import { AddItemButton, EditableItem } from '../../admin/Editable';
|
||||
import { formatJalali } from '../../utils/date';
|
||||
|
||||
export function Articles() {
|
||||
const { articles } = useSite();
|
||||
@@ -17,6 +19,9 @@ export function Articles() {
|
||||
|
||||
// پیشنویسها فقط در صفحه وبلاگ (برای ادمین) دیده میشوند، نه در خانه
|
||||
const published = articles.filter((a) => a.status !== 'draft');
|
||||
const topArticles = published.slice(0, 3);
|
||||
const spotlight = topArticles[0];
|
||||
const compactList = topArticles.slice(1, 3);
|
||||
|
||||
const saveHeading = (field: 'label' | 'title' | 'lead' | 'cta_label', value: string) =>
|
||||
save('section-headings', heading.id, { [field]: value }, (d) => {
|
||||
@@ -25,24 +30,97 @@ export function Articles() {
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="bg-char py-24 lg:py-32" aria-labelledby="articles-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<section className="w-full max-w-full overflow-x-clip bg-char py-16 md:py-24 lg:py-32" aria-labelledby="articles-title">
|
||||
<div className="mx-auto max-w-site px-4 md:px-6 lg:px-10">
|
||||
<Reveal className="max-w-2xl ltr:text-left rtl:text-right">
|
||||
<p className="text-sm font-light text-copper">
|
||||
<InlineEdit value={heading.label} onSave={(v) => saveHeading('label', v)} />
|
||||
</p>
|
||||
<h2 id="articles-title" className="mt-5 text-section font-bold text-ivory">
|
||||
<h2 id="articles-title" className="mt-3 md:mt-5 text-section font-bold text-ivory">
|
||||
<InlineEdit value={heading.title} onSave={(v) => saveHeading('title', v)} />
|
||||
</h2>
|
||||
<p className="mt-5 text-base font-light leading-9 text-ivory/60">
|
||||
<p className="mt-3 md:mt-5 text-sm md:text-base font-light leading-7 md:leading-9 text-ivory/60">
|
||||
<InlineEdit multiline value={heading.lead} onSave={(v) => saveHeading('lead', v)} />
|
||||
</p>
|
||||
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||
<div className="mt-4 md:mt-6 h-px w-24 bg-copper/70" />
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-14 grid gap-6 lg:grid-cols-3">
|
||||
{published.slice(0, 3).map((article, i) =>
|
||||
<Reveal key={article.id} delay={0.06 * i} className="h-full">
|
||||
{/* Mobile View: Magazine Editorial layout (1 Featured Spotlight + Compact Rows) */}
|
||||
<div className="block md:hidden mt-8">
|
||||
{/* First Article (Spotlight) */}
|
||||
{spotlight && (
|
||||
<EditableItem resource="articles" item={spotlight} editHref={`/admin/write/${spotlight.id}`}>
|
||||
<Link
|
||||
className="block rounded-xl border border-ivory/10 bg-char/40 overflow-hidden mb-3 group"
|
||||
to={href(`/blog/${spotlight.slug}`)}
|
||||
>
|
||||
<img
|
||||
src={spotlight.image}
|
||||
alt={spotlight.title}
|
||||
className="w-full aspect-[16/9] object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
<div className="p-3.5">
|
||||
<div className="flex items-center gap-2 text-[11px] text-copper font-medium mb-1">
|
||||
<span>{spotlight.category}</span>
|
||||
<span>•</span>
|
||||
<span>{formatJalali(spotlight.date)}</span>
|
||||
</div>
|
||||
<h4 className="text-sm font-bold text-ivory leading-snug line-clamp-2 mb-1 group-hover:text-copper transition-colors">
|
||||
{spotlight.title}
|
||||
</h4>
|
||||
<p className="text-xs text-ivory/60 line-clamp-2 leading-relaxed">
|
||||
{spotlight.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</EditableItem>
|
||||
)}
|
||||
|
||||
{/* Remaining Articles (Compact List Rows) */}
|
||||
{compactList.length > 0 && (
|
||||
<div className="divide-y divide-ivory/15 border-t border-b border-ivory/15 mb-4">
|
||||
{compactList.map((article) => (
|
||||
<EditableItem key={article.id} resource="articles" item={article} editHref={`/admin/write/${article.id}`}>
|
||||
<Link
|
||||
className="py-2.5 flex items-center gap-3 group"
|
||||
to={href(`/blog/${article.slug}`)}
|
||||
>
|
||||
<img
|
||||
src={article.image}
|
||||
alt={article.title}
|
||||
className="w-14 h-14 rounded-lg object-cover shrink-0 border border-ivory/10"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-[10px] text-copper block mb-0.5">{formatJalali(article.date)}</span>
|
||||
<h5 className="text-xs font-semibold text-ivory line-clamp-2 leading-snug group-hover:text-copper transition-colors">
|
||||
{article.title}
|
||||
</h5>
|
||||
</div>
|
||||
</Link>
|
||||
</EditableItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Button ("همه مقالات") */}
|
||||
{heading.cta_label && heading.cta_href && (
|
||||
<Link
|
||||
to={href(heading.cta_href)}
|
||||
className="w-full py-2.5 rounded-xl border border-ivory/20 bg-char/60 hover:bg-copper/10 hover:border-copper/40 text-xs font-semibold text-ivory text-center block transition-all active:scale-[0.98]"
|
||||
>
|
||||
<InlineEdit value={heading.cta_label} onSave={(v) => saveHeading('cta_label', v)} />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop View: 3-Column Grid */}
|
||||
<div className="hidden md:grid mt-14 grid-cols-3 gap-6">
|
||||
{topArticles.map((article, i) =>
|
||||
<Reveal
|
||||
key={article.id}
|
||||
delay={0.06 * i}
|
||||
className="h-full"
|
||||
>
|
||||
<EditableItem resource="articles" item={article} className="h-full" editHref={`/admin/write/${article.id}`}>
|
||||
<ArticleCard article={article} />
|
||||
</EditableItem>
|
||||
@@ -64,6 +142,6 @@ export function Articles() {
|
||||
<AddItemButton resource="articles" />
|
||||
</div>
|
||||
</div>
|
||||
</section>);
|
||||
|
||||
}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -22,18 +22,18 @@ export function FeaturedProjects() {
|
||||
.sort((a, b) => FEATURED_TITLES.indexOf(a.title) - FEATURED_TITLES.indexOf(b.title))
|
||||
|
||||
return (
|
||||
<section className="bg-char py-24 lg:py-32" aria-labelledby="projects-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<section className="w-full max-w-full overflow-x-clip bg-char py-16 md:py-24 lg:py-32" aria-labelledby="projects-title">
|
||||
<div className="mx-auto max-w-site px-4 md:px-6 lg:px-10">
|
||||
<SectionHeading title={heading.title} lead={heading.lead} headingKey="featured_projects" />
|
||||
|
||||
<div className="mt-10 min-h-[420px]">
|
||||
<div className="mt-8 min-h-[420px] md:mt-10">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="featured"
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: [0.23, 1, 0.32, 1] }}
|
||||
className="grid gap-6 lg:grid-cols-3"
|
||||
className="flex gap-4 overflow-x-auto pb-4 snap-x snap-mandatory [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none] md:grid md:grid-cols-3 md:gap-6 md:overflow-visible md:pb-0"
|
||||
>
|
||||
{list.map((project, i) => {
|
||||
const imageUrl = project.image || undefined;
|
||||
@@ -44,7 +44,7 @@ export function FeaturedProjects() {
|
||||
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"
|
||||
className="h-full w-[82vw] max-w-[320px] shrink-0 snap-center md:w-auto md:max-w-none md:shrink"
|
||||
>
|
||||
<ProjectCard
|
||||
project={project}
|
||||
@@ -61,7 +61,7 @@ export function FeaturedProjects() {
|
||||
{/* دکمه مشاهده همه پروژهها */}
|
||||
{heading.cta_label && heading.cta_href && (
|
||||
<Reveal delay={0.1}>
|
||||
<div className="mt-12 flex justify-center">
|
||||
<div className="mt-10 flex justify-center md:mt-12">
|
||||
<PremiumButton to={href(heading.cta_href)}>
|
||||
<InlineEdit
|
||||
value={heading.cta_label}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import { Reveal } from '../Reveal'
|
||||
import { ServiceIcon } from '../ServiceIcon'
|
||||
import { useSectionHeading, useSite } from '../../api'
|
||||
@@ -18,32 +17,35 @@ export function Services({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
||||
})
|
||||
|
||||
return (
|
||||
<section className={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-24 lg:py-32`} aria-labelledby="services-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<section className={`w-full max-w-full overflow-hidden ${tone === 'black' ? 'bg-char' : 'bg-ink'} py-12 md:py-24 lg:py-32`} aria-labelledby="services-title">
|
||||
<div className="mx-auto max-w-site px-4 md:px-6 lg:px-10">
|
||||
<Reveal className="max-w-2xl text-start">
|
||||
|
||||
<h2 id="services-title" className="text-section font-bold text-ivory">
|
||||
<InlineEdit value={heading.title} onSave={(v) => saveHeading('title', v)} />
|
||||
</h2>
|
||||
<p className="mt-5 text-base font-light leading-9 text-ivory/60">
|
||||
<p className="mt-3 text-sm font-light leading-7 text-ivory/60 md:mt-5 md:text-base md:leading-9">
|
||||
<InlineEdit multiline value={heading.lead} onSave={(v) => saveHeading('lead', v)} />
|
||||
</p>
|
||||
<div className="mt-6 h-px w-24 bg-copper/70" />
|
||||
<div className="mt-4 h-px w-24 bg-copper/70 md:mt-6" />
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-14 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="mt-8 grid grid-cols-2 gap-3 md:mt-14 md:gap-5 md:grid-cols-2 lg:grid-cols-4">
|
||||
{services.map((service, i) => (
|
||||
<Reveal key={service.id} delay={0.04 * i} className="h-full">
|
||||
<EditableItem resource="services" item={service} className="h-full">
|
||||
<article className="group flex h-full flex-col items-center border border-ivory/10 bg-char p-8 text-center transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform">
|
||||
<ServiceIcon
|
||||
name={service.icon}
|
||||
className="h-12 w-12 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper"
|
||||
/>
|
||||
<h3 className="mt-6 text-lg font-bold text-ivory transition-colors duration-300 ease-out group-hover:text-copper">
|
||||
<article className="group flex h-full flex-col items-center rounded-xl border border-ivory/10 bg-char p-3.5 text-center transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform md:rounded-2xl md:p-8">
|
||||
<div className="flex h-10 w-10 items-center justify-center md:h-14 md:w-14">
|
||||
<ServiceIcon
|
||||
name={service.icon}
|
||||
className="h-10 w-10 text-copper/85 transition-colors duration-300 ease-out group-hover:text-copper md:h-14 md:w-14"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="mt-3 text-sm font-bold text-ivory transition-colors duration-300 ease-out group-hover:text-copper md:mt-6 md:text-lg">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">{service.desc}</p>
|
||||
<p className="mt-2 text-xs font-light leading-relaxed text-ivory/55 line-clamp-2 md:mt-3 md:text-sm md:leading-8 md:line-clamp-none">
|
||||
{service.desc}
|
||||
</p>
|
||||
</article>
|
||||
</EditableItem>
|
||||
</Reveal>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useSite } from '../../api';
|
||||
import { useLang, formatNumber } from '../../i18n/lang';
|
||||
import { useFieldSaver } from '../../admin/AdminContext';
|
||||
@@ -71,12 +71,12 @@ export function Stats({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
||||
const save = useFieldSaver();
|
||||
|
||||
return (
|
||||
<section className={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-16`} aria-label={t('statsAria')}>
|
||||
<div className="mx-auto grid max-w-site grid-cols-2 gap-y-12 px-6 lg:grid-cols-4 lg:px-10">
|
||||
<section className={`w-full max-w-full overflow-x-clip ${tone === 'black' ? 'bg-char' : 'bg-ink'} py-8 md:py-16`} aria-label={t('statsAria')}>
|
||||
<div className="mx-auto grid max-w-site grid-cols-2 gap-3.5 p-4 md:grid-cols-4 md:gap-y-12 md:px-10 md:py-0">
|
||||
{stats.map((stat) =>
|
||||
<EditableItem key={stat.id} resource="stats" item={stat}>
|
||||
<div className="text-center">
|
||||
<p className="text-4xl font-bold text-copper md:text-5xl">
|
||||
<div className="p-3.5 text-center md:p-0">
|
||||
<p className="text-2xl font-bold text-copper md:text-5xl">
|
||||
<Counter value={stat.value} />
|
||||
<InlineEdit
|
||||
value={stat.suffix}
|
||||
@@ -84,10 +84,10 @@ export function Stats({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
||||
const s = d.stats.find((x) => x.id === stat.id);
|
||||
if (s) s.suffix = v;
|
||||
})}
|
||||
className="ltr:ml-1 rtl:mr-1 text-2xl"
|
||||
className="ltr:ml-1 rtl:mr-1 text-lg md:text-2xl"
|
||||
/>
|
||||
</p>
|
||||
<p className="mt-3 text-sm font-light text-ivory/55">
|
||||
<p className="mt-2 text-xs font-light text-ivory/55 md:mt-3 md:text-sm">
|
||||
<InlineEdit
|
||||
value={stat.label}
|
||||
onSave={(v) => save('stats', stat.id, { label: v }, (d) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import { Reveal } from '../Reveal'
|
||||
import { SectionHeading } from '../SectionHeading'
|
||||
import { useSite } from '../../api'
|
||||
@@ -9,8 +8,8 @@ export function Team({ tone = 'gray' }: { tone?: 'black' | 'gray' } = {}) {
|
||||
const { team } = useSite()
|
||||
const { t } = useLang()
|
||||
return (
|
||||
<section className={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-24 lg:py-32`} aria-labelledby="team-title" >
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10 ">
|
||||
<section className={`w-full max-w-full overflow-x-clip ${tone === 'black' ? 'bg-char' : 'bg-ink'} py-16 md:py-24 lg:py-32`} aria-labelledby="team-title" >
|
||||
<div className="mx-auto max-w-site px-4 md:px-6 lg:px-10">
|
||||
<SectionHeading
|
||||
title={t('fallbackTeamTitle')}
|
||||
lead={t('fallbackTeamLead')}
|
||||
@@ -18,12 +17,12 @@ export function Team({ tone = 'gray' }: { tone?: 'black' | 'gray' } = {}) {
|
||||
align="center"
|
||||
/>
|
||||
|
||||
<div className="mx-auto mt-16 flex max-w-5xl flex-wrap justify-center gap-8">
|
||||
<div className="mx-auto mt-8 flex max-w-5xl flex-wrap justify-center gap-4 md:mt-16 md:gap-8">
|
||||
{team.map((member, i) => (
|
||||
<Reveal key={member.id} delay={0.06 * i} className="h-full w-full sm:w-[calc(50%-1rem)] lg:w-[19rem]">
|
||||
<Reveal key={member.id} delay={0.06 * i} className="h-full w-[calc(50%-0.5rem)] md:w-[calc(50%-1rem)] lg:w-[19rem]">
|
||||
<EditableItem resource="team" item={member} className="h-full">
|
||||
{/* اضافه شدن rounded-2xl برای گرد شدن گوشهها */}
|
||||
<article className="group flex h-full flex-col items-center rounded-2xl border border-ivory/10 bg-char p-10 text-center transition-[transform,box-shadow,border-color] duration-500 ease-out hover:-translate-y-3 hover:border-copper/50 hover:shadow-lift">
|
||||
<article className="group flex h-full flex-col items-center rounded-xl border border-ivory/10 bg-char p-4 text-center md:rounded-2xl md:p-10 transition-[transform,box-shadow,border-color] duration-500 ease-out hover:-translate-y-3 hover:border-copper/50 hover:shadow-lift">
|
||||
<h3 className="text-lg font-bold text-ivory">{member.name}</h3>
|
||||
<p className="mt-2 text-sm font-light text-copper">{member.role}</p>
|
||||
<p className="mt-6 border-t border-ivory/10 pt-6 text-sm font-light text-ivory/60">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { AnimatePresence, motion, PanInfo } from 'framer-motion';
|
||||
import { ChevronLeftIcon, ChevronRightIcon, QuoteIcon } from 'lucide-react';
|
||||
import { SectionHeading } from '../SectionHeading';
|
||||
import { useSite } from '../../api';
|
||||
@@ -17,39 +17,57 @@ export function Testimonials() {
|
||||
setIndex((prev) => (prev + dir + testimonials.length) % testimonials.length);
|
||||
};
|
||||
|
||||
const handleDragEnd = (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
|
||||
if (info.offset.x > 50) {
|
||||
go(1); // سوایپ به راست
|
||||
} else if (info.offset.x < -50) {
|
||||
go(-1); // سوایپ به چپ
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="border-y border-ivory/10 bg-char py-24 lg:py-32" aria-labelledby="testimonials-title">
|
||||
<section className="border-y border-ivory/10 bg-char py-14 lg:py-28" aria-labelledby="testimonials-title">
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
<SectionHeading headingKey="testimonials" align="center" />
|
||||
|
||||
<div className="relative mx-auto mt-14 max-w-3xl text-center">
|
||||
<QuoteIcon className="mx-auto h-8 w-8 text-copper/60" aria-hidden="true" />
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.blockquote
|
||||
key={current.id}
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}>
|
||||
<div className="relative mx-auto mt-8 max-w-3xl text-center select-none">
|
||||
<QuoteIcon className="mx-auto h-6 w-6 text-copper/60 md:h-8 md:w-8" aria-hidden="true" />
|
||||
|
||||
<div className="relative min-h-[170px] overflow-hidden md:min-h-[190px]">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.blockquote
|
||||
key={current.id}
|
||||
drag="x"
|
||||
dragConstraints={{ left: 0, right: 0 }}
|
||||
dragElastic={0.2}
|
||||
onDragEnd={handleDragEnd}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}
|
||||
className="cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<EditableItem resource="testimonials" item={current}>
|
||||
<p className="mt-5 text-base font-light leading-8 text-ivory/85 md:mt-8 md:text-xl md:leading-[2.4rem]">
|
||||
«{current.quote}»
|
||||
</p>
|
||||
<footer className="mt-5 md:mt-8">
|
||||
<p className="text-sm font-bold text-copper md:text-base">{current.name}</p>
|
||||
<p className="mt-0.5 text-xs font-light text-ivory/45 md:text-sm">{current.role}</p>
|
||||
</footer>
|
||||
</EditableItem>
|
||||
</motion.blockquote>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<EditableItem resource="testimonials" item={current}>
|
||||
<p className="mt-8 text-lg font-light leading-[2.4rem] text-ivory/85 md:text-xl">«{current.quote}»</p>
|
||||
<footer className="mt-8">
|
||||
<p className="text-base font-bold text-copper">{current.name}</p>
|
||||
<p className="mt-1 text-sm font-light text-ivory/45">{current.role}</p>
|
||||
</footer>
|
||||
</EditableItem>
|
||||
</motion.blockquote>
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="mt-12 flex items-center justify-center gap-5">
|
||||
<div className="mt-6 flex items-center justify-center gap-5 md:mt-10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => go(1)}
|
||||
aria-label={t('prevTestimonial')}
|
||||
className="group flex h-12 w-12 items-center justify-center rounded-full border border-copper/35 bg-char/80 text-ivory/80 backdrop-blur-md shadow-[0_4px_20px_rgba(0,0,0,0.35)] transition-all duration-300 ease-lux hover:scale-110 hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_25px_rgba(197,143,92,0.45)] active:scale-95"
|
||||
className="group flex h-10 w-10 items-center justify-center rounded-full border border-copper/35 bg-char/80 text-ivory/80 shadow-[0_4px_20px_rgba(0,0,0,0.35)] backdrop-blur-md transition-all duration-300 ease-lux hover:scale-110 hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_25px_rgba(197,143,92,0.45)] active:scale-95 md:h-12 md:w-12"
|
||||
>
|
||||
<ChevronRightIcon className="h-5 w-5 ltr:rotate-180 transition-transform duration-300 ease-lux rtl:group-hover:translate-x-0.5 ltr:group-hover:-translate-x-0.5" />
|
||||
<ChevronRightIcon className="h-4 w-4 transition-transform duration-300 ease-lux ltr:rotate-180 rtl:group-hover:translate-x-0.5 ltr:group-hover:-translate-x-0.5 md:h-5 md:w-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2" role="tablist" aria-label={t('selectTestimonialAria')}>
|
||||
@@ -63,8 +81,8 @@ export function Testimonials() {
|
||||
onClick={() => setIndex(i)}
|
||||
className={`h-2 rounded-full transition-all duration-300 ease-lux ${
|
||||
i === index
|
||||
? 'w-8 bg-copper shadow-[0_0_12px_rgba(197,143,92,0.6)]'
|
||||
: 'w-2.5 bg-ivory/20 hover:bg-ivory/40'
|
||||
? 'w-7 bg-copper shadow-[0_0_12px_rgba(197,143,92,0.6)] md:w-8'
|
||||
: 'w-2 bg-ivory/20 hover:bg-ivory/40 md:w-2.5'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
@@ -74,9 +92,9 @@ export function Testimonials() {
|
||||
type="button"
|
||||
onClick={() => go(-1)}
|
||||
aria-label={t('nextTestimonial')}
|
||||
className="group flex h-12 w-12 items-center justify-center rounded-full border border-copper/35 bg-char/80 text-ivory/80 backdrop-blur-md shadow-[0_4px_20px_rgba(0,0,0,0.35)] transition-all duration-300 ease-lux hover:scale-110 hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_25px_rgba(197,143,92,0.45)] active:scale-95"
|
||||
className="group flex h-10 w-10 items-center justify-center rounded-full border border-copper/35 bg-char/80 text-ivory/80 shadow-[0_4px_20px_rgba(0,0,0,0.35)] backdrop-blur-md transition-all duration-300 ease-lux hover:scale-110 hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_25px_rgba(197,143,92,0.45)] active:scale-95 md:h-12 md:w-12"
|
||||
>
|
||||
<ChevronLeftIcon className="h-5 w-5 ltr:rotate-180 transition-transform duration-300 ease-lux rtl:group-hover:-translate-x-0.5 ltr:group-hover:translate-x-0.5" />
|
||||
<ChevronLeftIcon className="h-4 w-4 transition-transform duration-300 ease-lux ltr:rotate-180 rtl:group-hover:-translate-x-0.5 ltr:group-hover:translate-x-0.5 md:h-5 md:w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ function Item({ item, index, featured = false, onLi }: { item: Milestone; index:
|
||||
transition={{ duration: 0.55, ease }}
|
||||
>
|
||||
<EditableItem resource="timeline" item={item}>
|
||||
<div className="group relative overflow-hidden rounded-2xl border border-ivory/10 bg-ink/40 p-6 backdrop-blur-md transition-all duration-500 ease-lux hover:-translate-y-1.5 hover:border-copper/50 hover:bg-ink/60 hover:shadow-[0_12px_40px_-10px_rgba(197,143,92,0.35)] md:p-8">
|
||||
<div className="group relative overflow-hidden rounded-2xl border border-ivory/10 bg-ink/40 p-8 backdrop-blur-md transition-all duration-500 ease-lux hover:-translate-y-1.5 hover:border-copper/50 hover:bg-ink/60 hover:shadow-[0_12px_40px_-10px_rgba(197,143,92,0.35)]">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.03] to-transparent opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
|
||||
|
||||
<span className="relative z-10 text-sm font-medium tracking-widest text-copper">{item.year}</span>
|
||||
@@ -85,10 +85,10 @@ export function Timeline({ tone = 'gray' }: { tone?: 'black' | 'gray' } = {}) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section className={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-24 md:py-32`} aria-labelledby="timeline-title">
|
||||
<div className="mx-auto w-full max-w-site px-6 lg:px-10">
|
||||
<section className={`w-full max-w-full overflow-x-clip ${tone === 'black' ? 'bg-char' : 'bg-ink'} py-16 md:py-24 lg:py-32`} aria-labelledby="timeline-title">
|
||||
<div className="mx-auto w-full max-w-site px-4 md:px-6 lg:px-10">
|
||||
|
||||
<div className="mb-16">
|
||||
<div className="mb-8 md:mb-16">
|
||||
<SectionHeading
|
||||
title={t('fallbackTimelineTitle')}
|
||||
lead={t('fallbackTimelineLead')}
|
||||
@@ -120,6 +120,7 @@ export function Timeline({ tone = 'gray' }: { tone?: 'black' | 'gray' } = {}) {
|
||||
<AddItemButton resource="timeline" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Reveal } from '../Reveal';
|
||||
import { SectionHeading } from '../SectionHeading';
|
||||
import { useSectionHeading, useSite } from '../../api';
|
||||
import { AddItemButton, EditableItem, SingletonPill } from '../../admin/Editable';
|
||||
import { AddItemButton, EditableItem } from '../../admin/Editable';
|
||||
import { InlineEdit } from '../admin/InlineEdit';
|
||||
import { useFieldSaver } from '../../admin/AdminContext';
|
||||
import { useLang, formatNumber } from '../../i18n/lang';
|
||||
@@ -19,9 +18,15 @@ export function WhyUs({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
||||
if (it) Object.assign(it, changes);
|
||||
});
|
||||
|
||||
const formatIndex = (index: number) => {
|
||||
return lang === 'fa'
|
||||
? `۰${formatNumber(index + 1, lang)}`
|
||||
: String(index + 1).padStart(2, '0');
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={`${tone === 'black' ? 'bg-char' : 'bg-ink'} py-24 lg:py-32`} aria-labelledby="why-title">
|
||||
<div className="mx-auto grid max-w-site gap-14 px-6 lg:grid-cols-12 lg:px-10">
|
||||
<section className={`w-full max-w-full overflow-x-clip ${tone === 'black' ? 'bg-char' : 'bg-ink'} py-16 md:py-24 lg:py-32`} aria-labelledby="why-title">
|
||||
<div className="mx-auto grid max-w-site gap-8 px-4 md:gap-14 md:px-6 lg:grid-cols-12 lg:px-10">
|
||||
<div className="lg:col-span-5">
|
||||
<SectionHeading
|
||||
title={heading.title}
|
||||
@@ -42,35 +47,78 @@ export function WhyUs({ tone = 'black' }: { tone?: 'black' | 'gray' } = {}) {
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-7">
|
||||
<div className="grid gap-x-10 gap-y-px sm:grid-cols-2">
|
||||
{why_us.map((item, i) =>
|
||||
<Reveal key={item.id} delay={0.05 * i}>
|
||||
<EditableItem resource="why_us" item={item}>
|
||||
<div className="flex h-full flex-col border-t border-ivory/10 py-8">
|
||||
<span className="text-xs font-light text-copper">{lang === 'fa' ? `۰${formatNumber(i + 1, lang)}` : formatNumber(i + 1, lang).padStart(2, '0')}</span>
|
||||
<h3 className="mt-4 text-lg font-bold text-ivory">
|
||||
<InlineEdit
|
||||
value={item.title}
|
||||
onSave={(v) => saveItem(item.id, { title: v })}
|
||||
/>
|
||||
</h3>
|
||||
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">
|
||||
<InlineEdit
|
||||
multiline
|
||||
value={item.desc}
|
||||
onSave={(v) => saveItem(item.id, { desc: v })}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</EditableItem>
|
||||
</Reveal>
|
||||
)}
|
||||
{/* Mobile 2-Column Luxury Micro-Card Grid */}
|
||||
<div className="block md:hidden px-3.5 py-2">
|
||||
<div className="grid grid-cols-2 gap-2.5 items-stretch">
|
||||
{why_us.map((item, i) => (
|
||||
<EditableItem key={item.id} resource="why_us" item={item} className="h-full">
|
||||
<div className="h-full p-3 rounded-xl border border-ivory/10 bg-char/40 flex flex-col justify-between hover:border-copper/40 transition-all duration-200">
|
||||
<div>
|
||||
{/* Top Row: Index Badge */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[11px] font-mono font-bold text-copper tracking-wider px-1.5 py-0.5 rounded bg-copper/10 border border-copper/20">
|
||||
{`0${i + 1}`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h4 className="text-xs font-bold text-ivory mb-1 line-clamp-1 leading-snug">
|
||||
<InlineEdit
|
||||
value={item.title}
|
||||
onSave={(v) => saveItem(item.id, { title: v })}
|
||||
/>
|
||||
</h4>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-[10px] text-ivory/60 leading-relaxed line-clamp-2">
|
||||
<InlineEdit
|
||||
multiline
|
||||
value={item.desc}
|
||||
onSave={(v) => saveItem(item.id, { desc: v })}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</EditableItem>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<AddItemButton resource="why_us" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<AddItemButton resource="why_us" />
|
||||
|
||||
{/* Desktop Grid Layout */}
|
||||
<div className="hidden md:block">
|
||||
<div className="grid gap-x-10 gap-y-px sm:grid-cols-2">
|
||||
{why_us.map((item, i) => (
|
||||
<Reveal key={item.id} delay={0.05 * i}>
|
||||
<EditableItem resource="why_us" item={item}>
|
||||
<div className="flex h-full flex-col border-t border-ivory/10 py-8">
|
||||
<span className="text-xs font-light text-copper">{formatIndex(i)}</span>
|
||||
<h3 className="mt-4 text-lg font-bold text-ivory">
|
||||
<InlineEdit
|
||||
value={item.title}
|
||||
onSave={(v) => saveItem(item.id, { title: v })}
|
||||
/>
|
||||
</h3>
|
||||
<p className="mt-3 text-sm font-light leading-8 text-ivory/55">
|
||||
<InlineEdit
|
||||
multiline
|
||||
value={item.desc}
|
||||
onSave={(v) => saveItem(item.id, { desc: v })}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</EditableItem>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<AddItemButton resource="why_us" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>);
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user