merge(mobile): ادغام شاخهٔ new-one2 — طراحی ریسپانسیو صفحهٔ اصلی (کلاس‌های breakpoint در همهٔ بخش‌ها) با حفظ تغییرات طراحی ما: تیم وسط‌چین بدون دایره، پروژه‌های شاخص بدون تب، تناوب پس‌زمینه (tone)، پس‌زمینهٔ پیوستهٔ تماس و منطق دوزبانهٔ مقالات

This commit is contained in:
aramesh-site deploy
2026-10-09 15:58:57 +00:00
12 changed files with 626 additions and 220 deletions
+89 -11
View File
@@ -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}
+16 -14
View File
@@ -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>
+7 -7
View File
@@ -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) => {
+5 -6
View File
@@ -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">
+46 -28
View File
@@ -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>
+5 -4
View File
@@ -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>
)
+79 -31
View File
@@ -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>
);
}