feat: update UI to glassmorphic design and copper theme

This commit is contained in:
Behrad
2026-09-08 12:07:34 +03:30
parent 2ad3ad7367
commit 17bddd3adf
13 changed files with 368 additions and 247 deletions
+17 -13
View File
@@ -7,39 +7,43 @@ export function ArticleCard({ article }: { article: Article }) {
return (
<Link
to={`/blog/${article.slug}`}
className="group flex h-full flex-col border border-ivory/10 bg-char transition-all duration-300 ease-out hover:-translate-y-2 hover:border-copper/40 hover:shadow-lift transform-gpu will-change-transform focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-char/80 backdrop-blur-xl shadow-[0_10px_30px_rgba(0,0,0,0.3)] transition-all duration-500 ease-lux hover:-translate-y-2 hover:border-copper/50 hover:shadow-[0_20px_40px_rgba(0,0,0,0.6),0_0_30px_rgba(197,143,92,0.18)] focus:outline-none focus-visible:ring-2 focus-visible:ring-copper"
aria-label={`خواندن مقاله ${article.title}`}
>
<div className="overflow-hidden">
<div className="relative overflow-hidden">
<img
src={article.image}
alt={`تصویر مقاله ${article.title}`}
className="h-52 w-full object-cover transition-transform duration-300 ease-out group-hover:scale-105 transform-gpu"
className="h-52 w-full object-cover transition-transform duration-700 ease-lux group-hover:scale-105"
/>
{/* برچسب دسته‌بندی کپسولی شیشه‌ای روی تصویر */}
<div className="absolute right-4 top-4 rounded-full border border-white/20 bg-ink/75 px-3.5 py-1 text-xs font-light text-copper backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.2),0_4px_15px_rgba(0,0,0,0.4)]">
{article.category}
</div>
</div>
<div className="flex flex-1 flex-col p-8">
<p className="text-xs font-light text-copper">{article.category}</p>
<h3 className="mt-4 text-lg font-bold leading-9 text-ivory transition-colors duration-300 ease-out group-hover:text-copper">
<div className="flex flex-1 flex-col p-7">
<h3 className="text-lg font-bold leading-8 text-ivory transition-colors duration-300 group-hover:text-copper">
{article.title}
</h3>
<p className="mt-4 mb-8 text-sm font-light leading-[2.2rem] text-ivory/55">
<p className="mt-3 mb-8 text-sm font-light leading-7 text-ivory/60 line-clamp-3">
{article.excerpt}
</p>
<div className="mt-auto flex items-center justify-between border-t border-ivory/10 pt-6 text-xs font-light text-ivory/50">
<span className="flex items-center gap-4">
<div className="mt-auto flex items-center justify-between border-t border-white/10 pt-5 text-xs font-light text-ivory/50">
<span className="flex items-center gap-3">
<span>{article.date}</span>
<span className="flex items-center gap-1.5">
<ClockIcon className="h-3.5 w-3.5" />
<ClockIcon className="h-3.5 w-3.5 text-copper/80" />
{article.read}
</span>
</span>
<span className="flex items-center gap-2 text-copper">
{/* دکمه کپسولی ادامه مطلب با افکت شیشه‌ای */}
<span className="inline-flex items-center gap-2 rounded-full border border-copper/40 bg-copper/5 px-3.5 py-1.5 text-xs font-medium text-copper backdrop-blur-md shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux group-hover:border-copper group-hover:bg-copper group-hover:text-char group-hover:shadow-[0_0_15px_rgba(197,143,92,0.5)]">
ادامه مطلب
<ArrowLeftIcon className="h-3.5 w-3.5 transition-transform duration-300 ease-out group-hover:-translate-x-1.5" />
<ArrowLeftIcon className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-x-1" />
</span>
</div>
</div>
+32 -15
View File
@@ -9,26 +9,33 @@ type BaseProps = {
className?: string
}
const STYLES: Record<Variant, { base: string; fill: string; text: string }> = {
// استایل‌های فوق‌لوکس شیشه‌ای (Glassmorphic) با رنگ‌های اختصاصی پروژه
const STYLES: Record<Variant, { base: string; fill: string; text: string; glow: string }> = {
copper: {
base: 'bg-copper text-char',
fill: 'bg-char',
text: 'group-hover:text-ivory',
// دکمه اصلی: شیشه دودی تیره با خط نوری مسی، هاله بازتاب و حاشیه کریستالی
base: 'bg-char/75 backdrop-blur-xl text-ivory border border-copper/50 shadow-[inset_0_1px_1px_rgba(255,255,255,0.25),0_8px_20px_rgba(0,0,0,0.4)]',
fill: 'bg-gradient-to-r from-copper via-[#d89f6e] to-copper',
text: 'group-hover:text-char font-semibold',
glow: 'hover:border-copper hover:shadow-[0_0_30px_rgba(197,143,92,0.5)]',
},
outline: {
base: 'border border-ivory/30 text-ivory',
fill: 'bg-copper',
text: 'group-hover:text-char',
// دکمه ثانویه: شیشه دودی کاملاً مات (Frosted Smoked Glass) با بازتاب لبه‌ها
base: 'bg-white/[0.04] backdrop-blur-xl border border-white/20 text-ivory shadow-[inset_0_1px_1px_rgba(255,255,255,0.15),0_8px_20px_rgba(0,0,0,0.3)]',
fill: 'bg-white/10',
text: 'group-hover:text-copper',
glow: 'hover:border-copper/70 hover:bg-white/[0.08] hover:shadow-[0_0_25px_rgba(197,143,92,0.25)]',
},
dark: {
base: 'bg-char text-ivory',
base: 'bg-ink/85 backdrop-blur-xl text-ivory border border-white/10 shadow-[inset_0_1px_1px_rgba(255,255,255,0.1)]',
fill: 'bg-copper',
text: 'group-hover:text-char',
glow: 'hover:border-copper/50 hover:shadow-[0_0_20px_rgba(197,143,92,0.3)]',
},
'dark-ivory': {
base: 'bg-char text-ivory',
base: 'bg-ink/85 backdrop-blur-xl text-ivory border border-white/10 shadow-[inset_0_1px_1px_rgba(255,255,255,0.1)]',
fill: 'bg-ivory',
text: 'group-hover:text-char',
glow: 'hover:shadow-[0_0_20px_rgba(255,255,255,0.2)]',
},
}
@@ -36,12 +43,18 @@ function Inner({ children, variant = 'copper' }: { children: React.ReactNode; va
const style = STYLES[variant]
return (
<>
{/* خط انعکاس نوری شیشه در بالای دکمه */}
<span className="pointer-events-none absolute -top-1/2 left-0 right-0 h-full rounded-full bg-gradient-to-b from-white/20 to-transparent" />
{/* انیمیشن پر شدن موجی و نرم */}
<span
aria-hidden="true"
className={`absolute inset-0 translate-y-full transition-transform duration-[600ms] ease-lux group-hover:translate-y-0 ${style.fill}`}
className={`absolute inset-0 translate-y-full rounded-full transition-transform duration-500 ease-out group-hover:translate-y-0 ${style.fill}`}
/>
{/* متن و آیکون دکمه */}
<span
className={`relative z-10 flex items-center justify-center gap-3 transition-colors duration-[600ms] ease-lux ${style.text}`}
className={`relative z-10 flex items-center justify-center gap-3 transition-colors duration-400 ease-out ${style.text}`}
>
{children}
</span>
@@ -49,8 +62,9 @@ function Inner({ children, variant = 'copper' }: { children: React.ReactNode; va
)
}
// بدنه اصلی دکمه: کپسول کامل (rounded-full)، کلیک نرم (active:scale-95) و انتقال شیشه‌ای
const SHELL =
'group relative inline-flex items-center justify-center overflow-hidden px-8 py-4 text-sm font-bold focus:outline-none focus-visible:ring-2 focus-visible:ring-copper focus-visible:ring-offset-2 focus-visible:ring-offset-ink'
'group relative inline-flex items-center justify-center overflow-hidden rounded-full px-8 py-3.5 text-sm font-medium tracking-wide focus:outline-none focus-visible:ring-2 focus-visible:ring-copper focus-visible:ring-offset-2 focus-visible:ring-offset-ink active:scale-[0.98] transition-all duration-400 ease-out cursor-pointer select-none'
export function ButtonLink({
to,
@@ -58,8 +72,9 @@ export function ButtonLink({
variant = 'copper',
className = '',
}: BaseProps & { to: string }) {
const style = STYLES[variant]
return (
<Link to={to} className={`${SHELL} ${STYLES[variant].base} ${className}`}>
<Link to={to} className={`${SHELL} ${style.base} ${style.glow} ${className}`}>
<Inner variant={variant}>{children}</Inner>
</Link>
)
@@ -72,8 +87,9 @@ export function ButtonAnchor({
className = '',
...rest
}: BaseProps & { href: string } & React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const style = STYLES[variant]
return (
<a href={href} className={`${SHELL} ${STYLES[variant].base} ${className}`} {...rest}>
<a href={href} className={`${SHELL} ${style.base} ${style.glow} ${className}`} {...rest}>
<Inner variant={variant}>{children}</Inner>
</a>
)
@@ -85,8 +101,9 @@ export function Button({
className = '',
...rest
}: BaseProps & React.ButtonHTMLAttributes<HTMLButtonElement>) {
const style = STYLES[variant]
return (
<button className={`${SHELL} ${STYLES[variant].base} ${className}`} {...rest}>
<button className={`${SHELL} ${style.base} ${style.glow} ${className}`} {...rest}>
<Inner variant={variant}>{children}</Inner>
</button>
)
+50 -27
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
import { CheckCircle2Icon, MailIcon, MapPinIcon, PhoneIcon } from 'lucide-react'
import { CheckCircle2Icon, MailIcon, MapPinIcon, PhoneIcon, SendIcon } from 'lucide-react'
import Swal from 'sweetalert2'
import { Reveal } from './Reveal'
import { Button } from './Button'
@@ -37,11 +37,11 @@ export function ContactSection() {
confirmButtonText: 'متوجه شدم',
buttonsStyling: false,
customClass: {
container: 'backdrop-blur-[8px] bg-ink/60',
popup: 'font-pinar border border-ivory/10 rounded-none shadow-lift',
container: 'backdrop-blur-md bg-ink/70',
popup: 'font-pinar border border-white/10 rounded-2xl shadow-2xl backdrop-blur-xl bg-char/95',
title: 'font-pinar font-bold text-lg text-ivory pt-2',
htmlContainer: 'font-pinar font-light text-sm leading-8 text-ivory/70',
confirmButton: 'mt-6 bg-copper text-char font-bold px-10 py-3 transition-transform duration-300 ease-lux hover:scale-105',
confirmButton: 'mt-6 rounded-full bg-copper text-char font-bold px-10 py-3 transition-all duration-300 ease-lux hover:scale-105 hover:shadow-[0_0_20px_rgba(197,143,92,0.5)]',
},
showClass: {
popup: 'swal2-show',
@@ -50,7 +50,7 @@ export function ContactSection() {
hideClass: {
popup: 'swal2-hide',
backdrop: 'swal2-backdrop-hide',
}
},
})
}
@@ -104,19 +104,21 @@ export function ContactSection() {
aria-hidden="true"
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-ink/75 backdrop-blur-[2px]" />
<div className="relative mx-auto max-w-site px-6 lg:px-10">
<div className="mx-auto grid max-w-6xl items-stretch gap-6 lg:grid-cols-12">
<div className="mx-auto grid max-w-6xl items-stretch gap-8 lg:grid-cols-12">
<div className="border border-ivory/20 bg-char/45 p-8 backdrop-blur-[8px] md:p-10 lg:col-span-5">
{/* باکس اطلاعات تماس */}
<div className="rounded-3xl border border-white/10 bg-char/60 p-8 backdrop-blur-2xl shadow-[0_20px_50px_rgba(0,0,0,0.4),inset_0_1px_1px_rgba(255,255,255,0.15)] md:p-10 lg:col-span-5">
<Reveal>
<p className="text-sm font-light text-copper">
<p className="text-sm font-light tracking-wide text-copper">
<InlineEdit value={content.label} onSave={(v) => saveContent('label', v)} />
</p>
<h2 id="contact-title" className="mt-5 text-section font-bold text-ivory">
<h2 id="contact-title" className="mt-4 text-section font-bold text-ivory">
<InlineEdit value={content.title} onSave={(v) => saveContent('title', v)} />
</h2>
<p className="mt-6 text-base font-light leading-9 text-ivory/75">
<p className="mt-5 text-base font-light leading-9 text-ivory/75">
<InlineEdit multiline value={content.lead} onSave={(v) => saveContent('lead', v)} />
</p>
</Reveal>
@@ -124,7 +126,9 @@ export function ContactSection() {
<Reveal delay={0.1}>
<ul className="mt-10 space-y-6">
<li className="flex items-start gap-4">
<MapPinIcon className="mt-1 h-5 w-5 shrink-0 text-copper" />
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-copper/30 bg-copper/10 text-copper shadow-[inset_0_1px_1px_rgba(255,255,255,0.2)]">
<MapPinIcon className="h-5 w-5" />
</div>
<div>
<p className="text-sm font-bold text-ivory">
<InlineEdit value={content.address_label} onSave={(v) => saveContent('address_label', v)} />
@@ -134,8 +138,11 @@ export function ContactSection() {
</p>
</div>
</li>
<li className="flex items-start gap-4">
<PhoneIcon className="mt-1 h-5 w-5 shrink-0 text-copper" />
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-copper/30 bg-copper/10 text-copper shadow-[inset_0_1px_1px_rgba(255,255,255,0.2)]">
<PhoneIcon className="h-5 w-5" />
</div>
<div>
<p className="text-sm font-bold text-ivory">
<InlineEdit value={content.phone_label} onSave={(v) => saveContent('phone_label', v)} />
@@ -149,8 +156,11 @@ export function ContactSection() {
</a>
</div>
</li>
<li className="flex items-start gap-4">
<MailIcon className="mt-1 h-5 w-5 shrink-0 text-copper" />
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-copper/30 bg-copper/10 text-copper shadow-[inset_0_1px_1px_rgba(255,255,255,0.2)]">
<MailIcon className="h-5 w-5" />
</div>
<div>
<p className="text-sm font-bold text-ivory">
<InlineEdit value={content.email_label} onSave={(v) => saveContent('email_label', v)} />
@@ -168,11 +178,14 @@ export function ContactSection() {
</Reveal>
</div>
<div className="relative border border-ivory/20 bg-char/45 p-8 backdrop-blur-[9px] md:p-10 lg:col-span-7">
{/* باکس فرم ارسال درخواست */}
<div className="relative rounded-3xl border border-white/10 bg-char/60 p-8 backdrop-blur-2xl shadow-[0_20px_50px_rgba(0,0,0,0.4),inset_0_1px_1px_rgba(255,255,255,0.15)] md:p-10 lg:col-span-7">
<SingletonPill resource="contact_section" label="ویرایش برچسب‌ها و متن‌های فرم" className="absolute left-4 top-4" />
{sent ? (
<div className="flex h-full flex-col items-center justify-center py-14 text-center">
<CheckCircle2Icon className="h-12 w-12 text-copper" />
<div className="flex h-16 w-16 items-center justify-center rounded-full border border-copper/40 bg-copper/15 text-copper shadow-[0_0_30px_rgba(197,143,92,0.4)]">
<CheckCircle2Icon className="h-8 w-8" />
</div>
<h3 className="mt-6 text-xl font-bold text-ivory">
<InlineEdit value={content.success_title} onSave={(v) => saveContent('success_title', v)} />
</h3>
@@ -194,7 +207,7 @@ export function ContactSection() {
) : (
<form onSubmit={submit} noValidate className="grid gap-6 md:grid-cols-2">
<div>
<label htmlFor="contact-name" className="block text-sm font-bold text-ivory/90">
<label htmlFor="contact-name" className="block text-sm font-medium text-ivory/90">
<InlineEdit value={content.name_label} onSave={(v) => saveContent('name_label', v)} />
</label>
<input
@@ -203,12 +216,12 @@ export function ContactSection() {
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder={content.name_placeholder}
className="mt-3 w-full border border-ivory/20 bg-ink/45 px-5 py-4 text-sm font-light text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none"
className="mt-3 w-full rounded-2xl border border-white/10 bg-ink/60 px-5 py-3.5 text-sm font-light text-ivory placeholder:text-ivory/35 backdrop-blur-md transition-all duration-300 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] focus:border-copper/70 focus:bg-ink/80 focus:shadow-[0_0_15px_rgba(197,143,92,0.25)] focus:outline-none"
/>
</div>
<div>
<label htmlFor="contact-phone" className="block text-sm font-bold text-ivory/90">
<label htmlFor="contact-phone" className="block text-sm font-medium text-ivory/90">
<InlineEdit value={content.phone_field_label} onSave={(v) => saveContent('phone_field_label', v)} /> <span className="text-copper">*</span>
</label>
<input
@@ -219,28 +232,38 @@ export function ContactSection() {
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder={content.phone_placeholder}
className="mt-3 w-full border border-ivory/20 bg-ink/45 px-5 py-4 text-right text-sm font-light text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none"
className="mt-3 w-full rounded-2xl border border-white/10 bg-ink/60 px-5 py-3.5 text-right text-sm font-light text-ivory placeholder:text-ivory/35 backdrop-blur-md transition-all duration-300 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] focus:border-copper/70 focus:bg-ink/80 focus:shadow-[0_0_15px_rgba(197,143,92,0.25)] focus:outline-none"
/>
</div>
<div className="md:col-span-2">
<label htmlFor="contact-note" className="block text-sm font-bold text-ivory/90">
<label htmlFor="contact-note" className="block text-sm font-medium text-ivory/90">
<InlineEdit value={content.note_label} onSave={(v) => saveContent('note_label', v)} /> <span className="font-light text-ivory/50">{content.note_optional_label}</span>
</label>
<textarea
id="contact-note"
rows={6}
rows={5}
value={form.note}
onChange={(e) => setForm({ ...form, note: e.target.value })}
placeholder={content.note_placeholder}
className="mt-3 w-full resize-none border border-ivory/20 bg-ink/45 px-5 py-4 text-sm font-light leading-8 text-ivory placeholder:text-ivory/40 transition-colors duration-200 ease-lux focus:border-copper focus:outline-none"
className="mt-3 w-full resize-none rounded-2xl border border-white/10 bg-ink/60 px-5 py-3.5 text-sm font-light leading-8 text-ivory placeholder:text-ivory/35 backdrop-blur-md transition-all duration-300 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] focus:border-copper/70 focus:bg-ink/80 focus:shadow-[0_0_15px_rgba(197,143,92,0.25)] focus:outline-none"
/>
</div>
<div className="md:col-span-2">
<Button type="submit" variant="copper" className="w-full md:w-auto md:px-12" disabled={sending}>
{sending ? content.sending_label : (
<InlineEdit value={content.submit_label} onSave={(v) => saveContent('submit_label', v)} />
<div className="md:col-span-2 flex justify-start">
<Button
type="submit"
variant="copper"
className="w-full md:w-auto md:px-12 flex items-center justify-center gap-3"
disabled={sending}
>
{sending ? (
content.sending_label
) : (
<>
<InlineEdit value={content.submit_label} onSave={(v) => saveContent('submit_label', v)} />
<SendIcon className="h-4 w-4 transition-transform duration-300 group-hover:-translate-x-1" />
</>
)}
</Button>
</div>
@@ -251,4 +274,4 @@ export function ContactSection() {
</div>
</section>
)
}
}
+18 -5
View File
@@ -4,14 +4,27 @@ import { useSite } from '../api';
export function FloatingContact() {
const { brand } = useSite();
return (
<a
href={brand.phone_href}
aria-label="تماس تلفنی با گروه ساختمانی آرامش"
className="group fixed bottom-8 left-8 z-40 flex h-14 w-14 items-center justify-center rounded-full bg-copper text-char shadow-[0_10px_30px_rgba(0,0,0,0.45)] transition-transform duration-200 ease-lux hover:scale-105">
<span className="absolute inset-0 rounded-full border border-copper/50 opacity-0 transition-transform duration-300 ease-lux group-hover:scale-125 group-hover:opacity-100" />
<PhoneIcon className="h-5 w-5" />
</a>);
className="group fixed bottom-8 left-8 z-40 flex h-14 w-14 items-center justify-center rounded-full border border-copper/50 bg-char/80 text-copper backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.3),0_12px_35px_rgba(0,0,0,0.5),0_0_20px_rgba(197,143,92,0.25)] transition-all duration-500 ease-lux hover:scale-110 hover:border-copper hover:bg-copper hover:text-char hover:shadow-[0_0_35px_rgba(197,143,92,0.7)] active:scale-95"
>
{/* پالس هاله نوری در پس‌زمینه مانند رادار */}
<span
aria-hidden="true"
className="pointer-events-none absolute inset-0 rounded-full border border-copper/60 animate-ping opacity-30"
/>
{/* خط انعکاس نوری کریستال در بالای گوی */}
<span
aria-hidden="true"
className="pointer-events-none absolute -top-1/2 left-0 right-0 h-full rounded-full bg-gradient-to-b from-white/25 to-transparent"
/>
{/* آیکون تلفن با انیمیشن چرخش ظریف در زمان هاور */}
<PhoneIcon className="relative z-10 h-6 w-6 transition-transform duration-300 ease-lux group-hover:rotate-12" />
</a>
);
}
+46 -28
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { InstagramIcon, LinkedinIcon, MailIcon, MapPinIcon, PhoneIcon, SendIcon } from 'lucide-react';
import { ArrowUpIcon, InstagramIcon, LinkedinIcon, MailIcon, MapPinIcon, PhoneIcon, SendIcon } from 'lucide-react';
import { useSite } from '../api';
import { useFieldSaver } from '../admin/AdminContext';
import { InlineEdit } from './admin/InlineEdit';
@@ -27,8 +27,12 @@ export function Footer() {
if (item) Object.assign(item, changes);
});
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return (
<footer className="border-t border-ivory/10 bg-char">
<footer className="relative border-t border-white/10 bg-char/95 backdrop-blur-2xl">
<div className="mx-auto max-w-site px-6 py-16 lg:px-10">
<div className="grid gap-12 lg:grid-cols-12">
<div className="lg:col-span-4">
@@ -36,10 +40,11 @@ export function Footer() {
<img src={brand.logo} alt={brand.name} className="h-20 w-auto object-contain" />
<SingletonPill resource="brand" label="ویرایش برند، لوگو و تماس" className="absolute -left-10 top-1/2 -translate-y-1/2" />
</div>
<p className="mt-6 max-w-sm text-sm font-light leading-8 text-ivory/55">
<p className="mt-6 max-w-sm text-sm font-light leading-8 text-ivory/60">
<InlineEdit multiline value={footer.about_text} onSave={(v) => saveFooter('about_text', v)} />
</p>
<div className="mt-6 flex gap-3">
{/* آیکون‌های شبکه‌های اجتماعی: گوی‌های شیشه‌ای شناور */}
<div className="mt-8 flex items-center gap-3">
{footer.socials.map(({ label, href, icon }) => {
const Icon = SOCIAL_ICONS[icon] ?? InstagramIcon;
return (
@@ -47,9 +52,9 @@ export function Footer() {
key={label}
href={href}
aria-label={label}
className="flex h-10 w-10 items-center justify-center border border-ivory/15 text-ivory/70 transition-colors duration-200 ease-lux hover:border-copper hover:text-copper">
<Icon className="h-4 w-4" />
className="group flex h-11 w-11 items-center justify-center rounded-full border border-white/15 bg-white/[0.04] text-ivory/70 backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.15),0_4px_12px_rgba(0,0,0,0.3)] transition-all duration-300 ease-lux hover:-translate-y-1 hover:border-copper/70 hover:bg-copper/10 hover:text-copper hover:shadow-[0_0_20px_rgba(197,143,92,0.4)] active:scale-95"
>
<Icon className="h-4 w-4 transition-transform duration-300 group-hover:scale-110" />
</a>
);
})}
@@ -61,21 +66,21 @@ export function Footer() {
<InlineEdit value={footer.pages_heading} onSave={(v) => saveFooter('pages_heading', v)} />
</h3>
<ul className="mt-5 space-y-3">
{nav.map((item) =>
<EditableItem key={item.id} resource="nav" item={item}>
<li>
{nav.map((item) => (
<EditableItem key={item.id} resource="nav" item={item}>
<li>
<Link
to={item.href}
className="text-sm font-light text-ivory/60 transition-colors duration-200 ease-lux hover:text-copper">
<InlineEdit
value={item.label}
onSave={(v) => saveNav(item.id, { label: v })}
/>
</Link>
</li>
</EditableItem>
)}
to={item.href}
className="inline-block text-sm font-light text-ivory/60 transition-colors duration-200 ease-lux hover:text-copper"
>
<InlineEdit
value={item.label}
onSave={(v) => saveNav(item.id, { label: v })}
/>
</Link>
</li>
</EditableItem>
))}
</ul>
<div className="mt-4">
<AddItemButton resource="nav" />
@@ -87,11 +92,11 @@ export function Footer() {
<InlineEdit value={footer.services_heading} onSave={(v) => saveFooter('services_heading', v)} />
</h3>
<ul className="mt-5 space-y-3">
{services.map((service) =>
<li key={service.id} className="text-sm font-light text-ivory/60">
{services.map((service) => (
<li key={service.id} className="text-sm font-light text-ivory/60">
{service.title}
</li>
)}
))}
</ul>
</div>
@@ -120,15 +125,28 @@ export function Footer() {
</div>
</div>
<div className="mt-14 flex flex-col items-center justify-between gap-4 border-t border-ivory/10 pt-6 text-xs font-light text-ivory/40 md:flex-row">
{/* خط پایانی به همراه دکمه کپسولی بازگشت به بالا */}
<div className="mt-14 flex flex-col items-center justify-between gap-6 border-t border-white/10 pt-6 text-xs font-light text-ivory/40 md:flex-row">
<p>
<InlineEdit value={footer.copyright} onSave={(v) => saveFooter('copyright', v)} />
</p>
<button
type="button"
onClick={scrollToTop}
className="group flex items-center gap-2 rounded-full border border-white/15 bg-white/[0.04] px-4 py-2 text-xs font-medium text-ivory/70 backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux hover:border-copper hover:bg-copper/10 hover:text-copper hover:shadow-[0_0_20px_rgba(197,143,92,0.3)] active:scale-95 cursor-pointer"
>
<span>بازگشت به بالا</span>
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-white/10 text-copper transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:bg-copper group-hover:text-char">
<ArrowUpIcon className="h-3 w-3" />
</span>
</button>
<p>
<InlineEdit value={footer.tagline} onSave={(v) => saveFooter('tagline', v)} />
</p>
</div>
</div>
</footer>);
}
</footer>
);
}
+66 -67
View File
@@ -31,27 +31,27 @@ export function Header() {
return (
<header
className={`fixed inset-x-0 top-0 z-50 transition-[background-color,border-color,backdrop-filter] duration-300 ease-lux ${
className={`fixed inset-x-0 top-0 z-50 transition-all duration-300 ease-lux ${
scrolled
? "border-b border-ivory/10 bg-ink/90 backdrop-blur-md"
? "border-b border-white/10 bg-ink/80 backdrop-blur-xl shadow-[0_10px_30px_rgba(0,0,0,0.5)]"
: "border-b border-transparent bg-transparent"
}`}
>
<div className="mx-auto flex max-w-site items-center justify-between gap-8 px-6 py-4 lg:px-10">
<div className="relative flex items-center">
<Link
to="/"
className="flex items-center gap-3"
aria-label={brand.name}
>
<img
src={brand.logo}
alt={brand.name}
className={`w-auto object-contain transition-[height] duration-300 ease-lux ${
scrolled ? "h-14" : "h-20"
}`}
/>
</Link>
<Link
to="/"
className="flex items-center gap-3"
aria-label={brand.name}
>
<img
src={brand.logo}
alt={brand.name}
className={`w-auto object-contain transition-[height] duration-300 ease-lux ${
scrolled ? "h-14" : "h-20"
}`}
/>
</Link>
<SingletonPill resource="brand" label="ویرایش برند و لوگو" className="absolute -left-10 top-1/2 -translate-y-1/2" />
</div>
@@ -59,55 +59,54 @@ export function Header() {
<ul className="flex items-center gap-7">
{nav.map((item) => (
<EditableItem key={item.id} resource="nav" item={item}>
<li>
<NavLink
to={item.href}
className={({ isActive }) =>
`group relative py-2 text-[0.95rem] font-light transition-colors duration-200 ease-lux hover:text-copper [text-shadow:_0_2px_4px_rgba(0,0,0,0.6)] ${
isActive ? "text-copper" : "text-ivory/80"
}`
}
>
{({ isActive }) => (
<>
<InlineEdit
value={item.label}
onSave={(v) => saveNav(item.id, { label: v })}
/>
<span
className={`absolute -bottom-0.5 left-0 right-0 h-px w-full origin-center bg-copper transition-transform duration-300 ease-lux ${
isActive
? "scale-x-100"
: "scale-x-0 group-hover:scale-x-100"
}`}
/>
</>
)}
</NavLink>
</li>
<li>
<NavLink
to={item.href}
className={({ isActive }) =>
`group relative py-2 text-[0.95rem] font-light transition-colors duration-200 ease-lux hover:text-copper [text-shadow:_0_2px_4px_rgba(0,0,0,0.6)] ${
isActive ? "text-copper font-normal" : "text-ivory/80"
}`
}
>
{({ isActive }) => (
<>
<InlineEdit
value={item.label}
onSave={(v) => saveNav(item.id, { label: v })}
/>
<span
className={`absolute -bottom-0.5 left-0 right-0 h-0.5 w-full origin-center rounded-full bg-copper transition-transform duration-300 ease-lux ${
isActive
? "scale-x-100 shadow-[0_0_8px_rgba(197,143,92,0.8)]"
: "scale-x-0 group-hover:scale-x-100"
}`}
/>
</>
)}
</NavLink>
</li>
</EditableItem>
))}
</ul>
</nav>
<div className="flex items-center gap-3">
{/* دکمه تماس کپسولی شیشه‌ای لوکس */}
<a
href={brand.phone_href}
className="group relative hidden items-center gap-2 overflow-hidden border border-copper/50 px-5 py-2.5 text-sm font-medium text-copper [text-shadow:_0_1px_2px_rgba(0,0,0,0.5)] lg:flex"
className="group relative hidden items-center gap-3 rounded-full border border-copper/40 bg-white/[0.04] px-5 py-2 text-sm font-medium text-copper backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.2),0_4px_20px_rgba(0,0,0,0.3)] transition-all duration-300 ease-lux hover:border-copper hover:bg-copper/10 hover:shadow-[0_0_25px_rgba(197,143,92,0.35)] active:scale-95 lg:flex"
>
<span
aria-hidden="true"
className="absolute inset-0 translate-y-full bg-copper transition-transform duration-300 ease-lux group-hover:translate-y-0"
/>
<span className="relative z-10 flex items-center gap-2 transition-colors duration-300 ease-lux group-hover:text-char">
<PhoneIcon className="h-4 w-4" />
<span dir="ltr">{brand.phone}</span>
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-copper/20 text-copper transition-all duration-300 group-hover:bg-copper group-hover:text-char">
<PhoneIcon className="h-3.5 w-3.5 transition-transform duration-300 group-hover:rotate-12" />
</span>
<span dir="ltr" className="tracking-wide">{brand.phone}</span>
</a>
{/* دکمه منوی موبایل دایره‌ای شیشه‌ای */}
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex h-11 w-11 items-center justify-center border border-ivory/20 text-ivory transition-colors duration-200 ease-lux hover:border-copper hover:text-copper xl:hidden"
className="flex h-11 w-11 items-center justify-center rounded-full border border-white/15 bg-white/5 text-ivory backdrop-blur-lg shadow-[inset_0_1px_1px_rgba(255,255,255,0.15)] transition-all duration-300 ease-lux hover:border-copper/60 hover:bg-white/10 hover:text-copper active:scale-95 xl:hidden"
aria-label={open ? "بستن منو" : "باز کردن منو"}
aria-expanded={open}
>
@@ -123,26 +122,26 @@ export function Header() {
{open && (
<nav
aria-label="ناوبری موبایل"
className="border-t border-ivory/10 bg-ink/98 px-6 py-4 xl:hidden"
className="border-t border-white/10 bg-ink/95 backdrop-blur-2xl px-6 py-4 xl:hidden shadow-2xl"
>
<ul className="grid gap-1">
{nav.map((item) => (
<EditableItem key={item.id} resource="nav" item={item}>
<li>
<NavLink
to={item.href}
className={({ isActive }) =>
`block border-b border-ivory/5 py-3 text-base font-light transition-colors duration-200 ease-lux ${
isActive ? "text-copper" : "text-ivory/80"
}`
}
>
<InlineEdit
value={item.label}
onSave={(v) => saveNav(item.id, { label: v })}
/>
</NavLink>
</li>
<li>
<NavLink
to={item.href}
className={({ isActive }) =>
`block border-b border-ivory/5 py-3 text-base font-light transition-colors duration-200 ease-lux ${
isActive ? "text-copper" : "text-ivory/80"
}`
}
>
<InlineEdit
value={item.label}
onSave={(v) => saveNav(item.id, { label: v })}
/>
</NavLink>
</li>
</EditableItem>
))}
</ul>
@@ -150,4 +149,4 @@ export function Header() {
)}
</header>
);
}
}
View File
+17 -11
View File
@@ -1,49 +1,55 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { ArrowLeftIcon } from 'lucide-react'
import { useStatusLabel } from '../api'
export function ProjectCard({ project }: any) {
const isOngoing = project.status === 'progress'
const statusText = useStatusLabel(project.status)
const dotColor = isOngoing ? 'bg-copper animate-pulse' : 'bg-ivory/60'
const textColor = isOngoing ? 'text-copper' : 'text-ivory/90'
return (
<article className="group flex h-full flex-col overflow-hidden border border-ivory/10 bg-char transition-[transform,box-shadow,border-color] duration-500 ease-lux hover:-translate-y-2 hover:border-copper/50 hover:shadow-lift focus:outline-none focus-visible:ring-2 focus-visible:ring-copper">
<article className="group flex h-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-char/85 backdrop-blur-xl shadow-[0_10px_30px_rgba(0,0,0,0.3)] transition-all duration-500 ease-lux hover:-translate-y-2 hover:border-copper/50 hover:shadow-[0_20px_40px_rgba(0,0,0,0.6),0_0_30px_rgba(197,143,92,0.2)] focus:outline-none focus-visible:ring-2 focus-visible:ring-copper">
<Link to={`/projects/${project.slug || '#'}`} className="flex h-full flex-col focus:outline-none">
{/* کادر تصویر به همراه نشانگر وضعیت کپسولی و گوی دکمه شیشه‌ای */}
<div className="relative aspect-[4/3] w-full overflow-hidden">
<img
src={project.image}
alt={project.title}
className="h-full w-full object-cover transition-transform duration-700 ease-lux group-hover:scale-105"
/>
<div className={`absolute right-4 top-4 flex items-center gap-2 bg-ink/85 px-4 py-2 text-xs font-light backdrop-blur-md ${textColor}`}>
<span className={`h-1.5 w-1.5 rounded-full ${dotColor}`} />
{/* نشانگر وضعیت کپسولی شیشه‌ای */}
<div className={`absolute right-4 top-4 flex items-center gap-2 rounded-full border border-white/15 bg-ink/75 px-4 py-1.5 text-xs font-light backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.2),0_4px_15px_rgba(0,0,0,0.4)] ${textColor}`}>
<span className={`h-2 w-2 rounded-full ${dotColor} ${isOngoing ? 'shadow-[0_0_10px_rgba(197,143,92,0.9)]' : ''}`} />
{statusText}
</div>
{/* گوی دکمه شیشه‌ای شناور برای ورود به پروژه */}
<div className="absolute bottom-4 left-4 flex h-11 w-11 items-center justify-center rounded-full border border-white/25 bg-ink/65 text-copper backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.25),0_6px_20px_rgba(0,0,0,0.4)] transition-all duration-400 ease-lux group-hover:scale-110 group-hover:border-copper group-hover:bg-copper group-hover:text-char group-hover:shadow-[0_0_25px_rgba(197,143,92,0.6)]">
<ArrowLeftIcon className="h-5 w-5 transition-transform duration-300 group-hover:-translate-x-0.5" />
</div>
</div>
<div className="flex flex-1 flex-col p-8">
<div className="flex flex-1 flex-col p-7">
<div className="text-center">
<h3 className="font-pinar text-xl font-bold text-ivory">
<h3 className="font-pinar text-xl font-bold text-ivory transition-colors duration-300 group-hover:text-copper">
{project.title}
</h3>
<p className="mt-7 text-sm font-light text-ivory/60">
<p className="mt-4 text-sm font-light text-ivory/60">
{project.location}
</p>
</div>
<div className="my-7 h-px w-full bg-ivory/10" />
<div className="my-6 h-px w-full bg-white/10" />
<div className="mt-auto flex items-center justify-between text-sm font-light text-ivory/60">
<span>سال: {project.year}</span>
<span>{project.area} متر مربع</span>
</div>
</div>
</Link>
</article>
@@ -36,34 +36,35 @@ export function FeaturedProjects() {
<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 flex-wrap items-center gap-3" role="tablist" aria-label="فیلتر وضعیت پروژه">
{FILTERS.map((filter) => (
<button
key={filter.key}
type="button"
role="tab"
aria-selected={status === filter.key}
onClick={() => setStatus(filter.key)}
className={`group relative overflow-hidden border px-7 py-3 text-sm transition-colors duration-300 ease-out ${
status === filter.key
? 'border-copper bg-copper font-bold text-char'
: 'border-ivory/15 font-light text-ivory/65'
}`}
>
{status !== filter.key && (
<span
aria-hidden="true"
className="absolute inset-0 translate-y-full bg-copper transition-transform duration-300 ease-out group-hover:translate-y-0"
/>
)}
<span className="relative z-10 transition-colors duration-300 ease-out group-hover:text-char">
<InlineEdit
value={filter.label}
onSave={(v) => saveStatusLabel(filter.key, v)}
/>
</span>
</button>
))}
{FILTERS.map((filter) => {
const isSelected = status === filter.key
return (
<button
key={filter.key}
type="button"
role="tab"
aria-selected={isSelected}
onClick={() => setStatus(filter.key)}
className={`group relative flex items-center justify-center overflow-hidden rounded-full px-7 py-3 text-sm transition-all duration-300 ease-out active:scale-95 cursor-pointer select-none ${
isSelected
? 'border border-copper bg-copper font-bold text-char shadow-[0_0_25px_rgba(197,143,92,0.45),inset_0_1px_1px_rgba(255,255,255,0.4)]'
: 'border border-white/15 bg-white/[0.04] font-light text-ivory/70 backdrop-blur-xl shadow-[inset_0_1px_1px_rgba(255,255,255,0.1)] hover:border-copper/60 hover:bg-white/[0.08] hover:text-ivory hover:shadow-[0_0_20px_rgba(197,143,92,0.2)]'
}`}
>
{/* خط بازتاب نوری لبه شیشه */}
<span className="pointer-events-none absolute -top-1/2 left-0 right-0 h-full rounded-full bg-gradient-to-b from-white/20 to-transparent" />
<span className="relative z-10 transition-colors duration-300 ease-out">
<InlineEdit
value={filter.label}
onSave={(v) => saveStatusLabel(filter.key, v)}
/>
</span>
</button>
)
})}
</div>
<div className="mt-10 min-h-[420px]">
@@ -93,10 +94,15 @@ export function FeaturedProjects() {
</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="font-light">
<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) =>
@@ -106,7 +112,9 @@ export function FeaturedProjects() {
})
}
/>
<ArrowLeftIcon className="h-4 w-4" />
<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>
@@ -114,4 +122,4 @@ export function FeaturedProjects() {
</div>
</section>
)
}
}
+39 -28
View File
@@ -20,23 +20,9 @@ export function Hero() {
;(d.hero as unknown as Record<string, unknown>)[field] = value
})
// واژه highlight در میانه تیتر با رنگ مسی رندر می‌شود
const renderTitle = () => {
if (hero.title_highlight && hero.title.includes(hero.title_highlight)) {
const [before, ...rest] = hero.title.split(hero.title_highlight)
return (
<>
{before}
<span className="text-copper">{hero.title_highlight}</span>
{rest.join(hero.title_highlight)}
</>
)
}
return hero.title
}
return (
<section className="relative h-screen min-h-[640px] w-full overflow-hidden" aria-label="معرفی گروه ساختمانی آرامش">
{/* تصویر پس‌زمینه */}
<motion.img
src={brand.hero}
alt="نمای شبانه برج مسکونی ساخته‌شده توسط گروه ساختمانی آرامش"
@@ -53,61 +39,82 @@ export function Hero() {
aria-hidden="true"
/>
{/* محتوای اصلی هیرو */}
<motion.div
style={{ y: contentY }}
className="relative mx-auto flex h-full max-w-site flex-col justify-center px-6 lg:px-10"
>
<div className="w-full max-w-[700px] lg:max-w-[58%]">
{/* کیکر بالای تیتر */}
<motion.p
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 2.1, ease: [0.23, 1, 0.32, 1] }}
className="text-sm font-light text-copper"
className="text-sm font-light text-copper tracking-wide"
>
<InlineEdit value={hero.kicker} onSave={(v) => saveHero('kicker', v)} />
</motion.p>
{/* تیتر اصلی */}
<motion.h1
initial={{ opacity: 0, y: 28 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.85, delay: 2.2, ease: [0.23, 1, 0.32, 1] }}
className="mt-6 text-hero font-bold text-ivory"
className="mt-6 text-hero font-bold text-ivory leading-[1.3]"
>
<InlineEdit value={hero.title} onSave={(v) => saveHero('title', v)} aria-label="تیتر اصلی صفحه" />
</motion.h1>
{/* زیرتیتر */}
<motion.p
initial={{ opacity: 0, y: 22 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.85, delay: 2.32, ease: [0.23, 1, 0.32, 1] }}
className="mt-7 max-w-xl text-base font-light leading-9 text-ivory/70"
className="mt-7 max-w-xl text-base font-light leading-9 text-ivory/75"
>
<InlineEdit multiline value={hero.subtitle} onSave={(v) => saveHero('subtitle', v)} />
</motion.p>
{/* داک دکمه‌ها: استایل کپسولی، شیشه‌ای و دایره‌ای */}
<motion.div
initial={{ opacity: 0, y: 22 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.85, delay: 2.44, ease: [0.23, 1, 0.32, 1] }}
className="mt-10 flex flex-wrap items-center gap-4"
>
<ButtonLink to={hero.cta_primary_href} variant="copper">
<InlineEdit value={hero.cta_primary_label} onSave={(v) => saveHero('cta_primary_label', v)} />
<ArrowLeftIcon className="h-4 w-4" />
{/* دکمه اصلی: کپسولی شیشه‌ای دودی با دایره کهربایی اختصاصی برای آیکون */}
<ButtonLink
to={hero.cta_primary_href}
variant="copper"
className="!p-1.5 !pr-7 !rounded-full shadow-lg shadow-black/40 hover:shadow-copper/25 transition-all duration-300"
>
<span className="text-sm font-medium tracking-wide text-ivory">
<InlineEdit value={hero.cta_primary_label} onSave={(v) => saveHero('cta_primary_label', v)} />
</span>
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-copper text-char shadow-md transition-all duration-300 group-hover:scale-110 group-hover:-translate-x-1">
<ArrowLeftIcon className="h-4 w-4 stroke-[2.5]" />
</span>
</ButtonLink>
<ButtonLink to={hero.cta_secondary_href} variant="outline" className="font-light">
{/* دکمه ثانویه: شیشه دودی مات (Smoked Glass) با کادر ظریف */}
<ButtonLink
to={hero.cta_secondary_href}
variant="outline"
className="!rounded-full !px-8 !py-3.5 bg-char/40 backdrop-blur-md border border-ivory/20 text-ivory hover:border-copper/70 hover:text-copper transition-all duration-300 shadow-sm"
>
<InlineEdit value={hero.cta_secondary_label} onSave={(v) => saveHero('cta_secondary_label', v)} />
</ButtonLink>
</motion.div>
</div>
{/* کادر آمار شناور (گوشه پایین چپ): شیشه‌ای با لبه‌های گرد مدرن */}
<motion.div
initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.9, delay: 2.6, ease: [0.23, 1, 0.32, 1] }}
className="absolute bottom-28 left-6 hidden w-60 border border-ivory/15 bg-char/60 p-6 backdrop-blur-md lg:block lg:left-10"
className="absolute bottom-28 left-6 hidden w-64 rounded-2xl border border-ivory/15 bg-char/70 p-6 backdrop-blur-xl shadow-2xl shadow-black/40 lg:block lg:left-10"
>
<p className="text-3xl font-bold text-copper">
<p className="text-3xl font-bold text-copper font-mono">
<InlineEdit value={hero.badge_value} onSave={(v) => saveHero('badge_value', v)} />
</p>
<p className="mt-2 text-sm font-light leading-7 text-ivory/70">
@@ -116,17 +123,21 @@ export function Hero() {
</motion.div>
</motion.div>
{/* دکمه اسکرول پایین: تبدیل‌شده به یک گوی شیشه‌ای دایره‌ای */}
<a
href="#story"
aria-label="رفتن به بخش بعدی"
className="absolute bottom-8 right-1/2 z-10 flex translate-x-1/2 flex-col items-center gap-2 text-ivory/50 transition-colors duration-200 ease-lux hover:text-copper"
className="group absolute bottom-7 right-1/2 z-10 flex translate-x-1/2 flex-col items-center gap-2 text-ivory/60 transition-colors duration-300 hover:text-copper"
>
<span className="text-xs font-light">
<span className="text-[11px] font-light tracking-widest opacity-80 group-hover:opacity-100">
<InlineEdit value={hero.scroll_label} onSave={(v) => saveHero('scroll_label', v)} />
</span>
<ChevronDownIcon className="h-4 w-4 animate-bounce" />
<div className="flex h-10 w-10 items-center justify-center rounded-full border border-ivory/20 bg-char/40 backdrop-blur-md transition-all duration-300 group-hover:border-copper group-hover:bg-char/70 group-hover:shadow-lg group-hover:shadow-copper/20">
<ChevronDownIcon className="h-4 w-4 animate-bounce text-ivory group-hover:text-copper" />
</div>
</a>
{/* کنترل‌های ادمین */}
<div className="absolute left-6 top-24 z-20 flex flex-col gap-2 lg:left-10">
<SingletonPill resource="hero" label="تنظیمات هیرو (واژه مسی، دکمه‌ها)" />
<SingletonPill
@@ -137,4 +148,4 @@ export function Hero() {
</div>
</section>
)
}
}
+28 -16
View File
@@ -32,11 +32,11 @@ export function Newsletter() {
confirmButtonText: 'متوجه شدم',
buttonsStyling: false,
customClass: {
container: 'backdrop-blur-[8px] bg-ink/60',
popup: 'font-pinar border border-ivory/10 rounded-none shadow-lift',
container: 'backdrop-blur-md bg-ink/70',
popup: 'font-pinar border border-white/10 rounded-2xl shadow-2xl backdrop-blur-xl bg-char/95',
title: 'font-pinar font-bold text-lg text-ivory pt-2',
htmlContainer: 'font-pinar font-light text-sm leading-8 text-ivory/70',
confirmButton: 'mt-6 bg-copper text-char font-bold px-10 py-3 transition-transform duration-300 ease-lux hover:scale-105',
confirmButton: 'mt-6 rounded-full bg-copper text-char font-bold px-10 py-3 transition-all duration-300 ease-lux hover:scale-105 hover:shadow-[0_0_20px_rgba(197,143,92,0.5)]',
},
showClass: {
popup: 'swal2-show',
@@ -96,20 +96,25 @@ export function Newsletter() {
}
return (
<section className="relative bg-copper py-16" aria-labelledby="newsletter-title">
<section className="relative overflow-hidden bg-gradient-to-r from-copper via-[#d8965e] to-copper py-20 shadow-[inset_0_1px_1px_rgba(255,255,255,0.3)]" aria-labelledby="newsletter-title">
<SingletonPill resource="newsletter" label="ویرایش متن‌های خبرنامه" className="absolute left-4 top-4" />
<div className="mx-auto grid max-w-site items-center gap-8 px-6 lg:grid-cols-2 lg:px-10">
{/* هاله نور پس‌زمینه برای عمق بصری */}
<div className="pointer-events-none absolute -right-20 -top-20 h-64 w-64 rounded-full bg-white/20 blur-3xl" />
<div className="pointer-events-none absolute -bottom-20 -left-20 h-64 w-64 rounded-full bg-black/15 blur-3xl" />
<div className="relative mx-auto grid max-w-site items-center gap-10 px-6 lg:grid-cols-2 lg:px-10">
<Reveal>
<h2 id="newsletter-title" className="text-2xl font-bold leading-relaxed text-char md:text-3xl">
<InlineEdit value={newsletter.title} onSave={(v) => saveContent('title', v)} />
</h2>
<p className="mt-3 max-w-md text-sm font-light leading-8 text-char/75">
<p className="mt-3 max-w-md text-sm font-light leading-8 text-char/80">
<InlineEdit multiline value={newsletter.lead} onSave={(v) => saveContent('lead', v)} />
</p>
</Reveal>
<Reveal delay={0.08}>
<form onSubmit={submit} className="flex flex-col gap-3 sm:flex-row" noValidate>
<form onSubmit={submit} className="flex flex-col gap-3.5 sm:flex-row" noValidate>
<div className="flex-1">
<label htmlFor="newsletter-email" className="sr-only">
{newsletter.placeholder}
@@ -124,26 +129,33 @@ export function Newsletter() {
if (status !== 'idle') setStatus('idle')
}}
placeholder={newsletter.placeholder}
className="w-full border border-transparent bg-char px-5 py-4 text-left text-sm font-light text-ivory placeholder:text-ivory/40 transition-all duration-300 ease-lux focus:border-ivory focus:outline-none"
className="w-full rounded-full border border-black/15 bg-char/90 px-6 py-4 text-left text-sm font-light text-ivory placeholder:text-ivory/40 shadow-[inset_0_2px_4px_rgba(0,0,0,0.35)] backdrop-blur-xl transition-all duration-300 ease-lux focus:border-ivory focus:bg-char focus:shadow-[0_0_20px_rgba(255,255,255,0.25)] focus:outline-none"
aria-invalid={status === 'error'}
/>
{status === 'done' && (
<p className="mt-2 flex items-center gap-1.5 text-xs font-light text-char" role="status">
<CheckIcon className="h-3.5 w-3.5" />
<p className="mt-2.5 flex items-center gap-1.5 px-3 text-xs font-medium text-char" role="status">
<CheckIcon className="h-4 w-4" />
{newsletter.success_text}
</p>
)}
</div>
<Button type="submit" variant="dark-ivory" disabled={sending}>
<SendIcon className="h-4 w-4" />
{sending ? newsletter.sending_label : (
<InlineEdit value={newsletter.button_label} onSave={(v) => saveContent('button_label', v)} />
)}
<Button
type="submit"
variant="dark"
disabled={sending}
className="group flex items-center justify-center gap-2 rounded-full px-8 py-4 shadow-[0_8px_20px_rgba(0,0,0,0.3)] transition-all duration-300 hover:shadow-[0_12px_25px_rgba(0,0,0,0.45)] active:scale-95"
>
<SendIcon className="h-4 w-4 transition-transform duration-300 group-hover:-translate-x-1" />
<span>
{sending ? newsletter.sending_label : (
<InlineEdit value={newsletter.button_label} onSave={(v) => saveContent('button_label', v)} />
)}
</span>
</Button>
</form>
</Reveal>
</div>
</section>
)
}
}
+10 -4
View File
@@ -5,10 +5,16 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
// در حالت توسعه، /api و /media به Traefik پروکسی می‌شوند (پورت ۵۱۷۳ روی هاست)
proxy: {
'/api': 'http://localhost:5173',
'/media': 'http://localhost:5173',
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
'/media': {
target: 'http://localhost:9000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/media/, '/aramesh-media'),
},
},
},
})
})