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
+6 -2
View File
@@ -12,7 +12,8 @@ services:
- --providers.docker.exposedbydefault=false - --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80 - --entrypoints.web.address=:80
ports: ports:
- "${TRAEFIK_HTTP_PORT:-5173}:80" # پورت به 8080 تغییر کرد تا با پورت 5173 سرور لوکال Vite تداخل نکند
- "${TRAEFIK_HTTP_PORT:-8080}:80"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
@@ -46,7 +47,8 @@ services:
timeout: 5s timeout: 5s
retries: 12 retries: 12
ports: ports:
- "9001:9001" # کنسول MinIO — در محیط واقعی با فایروال ببندید - "9001:9001" # کنسول MinIO
- "9000:9000" # دسترسی لوکال به تصاویر و مدیا
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.routers.media.rule=PathPrefix(`/media`) - traefik.http.routers.media.rule=PathPrefix(`/media`)
@@ -79,6 +81,8 @@ services:
backend: backend:
build: ./backend build: ./backend
restart: unless-stopped restart: unless-stopped
ports:
- "8000:8000" # اتصال مستقیم بک‌اند به ویندوز برای فرانت‌اند لوکال
environment: environment:
DATABASE_URL: postgresql+psycopg://aramesh:${POSTGRES_PASSWORD}@db:5432/arameshplaza DATABASE_URL: postgresql+psycopg://aramesh:${POSTGRES_PASSWORD}@db:5432/arameshplaza
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY را در .env تنظیم کنید} SECRET_KEY: ${SECRET_KEY:?SECRET_KEY را در .env تنظیم کنید}
+17 -13
View File
@@ -7,39 +7,43 @@ export function ArticleCard({ article }: { article: Article }) {
return ( return (
<Link <Link
to={`/blog/${article.slug}`} 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}`} aria-label={`خواندن مقاله ${article.title}`}
> >
<div className="overflow-hidden"> <div className="relative overflow-hidden">
<img <img
src={article.image} src={article.image}
alt={`تصویر مقاله ${article.title}`} 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>
<div className="flex flex-1 flex-col p-8"> <div className="flex flex-1 flex-col p-7">
<p className="text-xs font-light text-copper">{article.category}</p> <h3 className="text-lg font-bold leading-8 text-ivory transition-colors duration-300 group-hover:text-copper">
<h3 className="mt-4 text-lg font-bold leading-9 text-ivory transition-colors duration-300 ease-out group-hover:text-copper">
{article.title} {article.title}
</h3> </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} {article.excerpt}
</p> </p>
<div className="mt-auto flex items-center justify-between border-t border-ivory/10 pt-6 text-xs font-light text-ivory/50"> <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-4"> <span className="flex items-center gap-3">
<span>{article.date}</span> <span>{article.date}</span>
<span className="flex items-center gap-1.5"> <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} {article.read}
</span> </span>
</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> </span>
</div> </div>
</div> </div>
+32 -15
View File
@@ -9,26 +9,33 @@ type BaseProps = {
className?: string 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: { copper: {
base: 'bg-copper text-char', // دکمه اصلی: شیشه دودی تیره با خط نوری مسی، هاله بازتاب و حاشیه کریستالی
fill: 'bg-char', 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)]',
text: 'group-hover:text-ivory', 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: { outline: {
base: 'border border-ivory/30 text-ivory', // دکمه ثانویه: شیشه دودی کاملاً مات (Frosted Smoked Glass) با بازتاب لبه‌ها
fill: 'bg-copper', 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)]',
text: 'group-hover:text-char', 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: { 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', fill: 'bg-copper',
text: 'group-hover:text-char', text: 'group-hover:text-char',
glow: 'hover:border-copper/50 hover:shadow-[0_0_20px_rgba(197,143,92,0.3)]',
}, },
'dark-ivory': { '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', fill: 'bg-ivory',
text: 'group-hover:text-char', 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] const style = STYLES[variant]
return ( 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 <span
aria-hidden="true" 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 <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} {children}
</span> </span>
@@ -49,8 +62,9 @@ function Inner({ children, variant = 'copper' }: { children: React.ReactNode; va
) )
} }
// بدنه اصلی دکمه: کپسول کامل (rounded-full)، کلیک نرم (active:scale-95) و انتقال شیشه‌ای
const SHELL = 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({ export function ButtonLink({
to, to,
@@ -58,8 +72,9 @@ export function ButtonLink({
variant = 'copper', variant = 'copper',
className = '', className = '',
}: BaseProps & { to: string }) { }: BaseProps & { to: string }) {
const style = STYLES[variant]
return ( 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> <Inner variant={variant}>{children}</Inner>
</Link> </Link>
) )
@@ -72,8 +87,9 @@ export function ButtonAnchor({
className = '', className = '',
...rest ...rest
}: BaseProps & { href: string } & React.AnchorHTMLAttributes<HTMLAnchorElement>) { }: BaseProps & { href: string } & React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const style = STYLES[variant]
return ( 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> <Inner variant={variant}>{children}</Inner>
</a> </a>
) )
@@ -85,8 +101,9 @@ export function Button({
className = '', className = '',
...rest ...rest
}: BaseProps & React.ButtonHTMLAttributes<HTMLButtonElement>) { }: BaseProps & React.ButtonHTMLAttributes<HTMLButtonElement>) {
const style = STYLES[variant]
return ( return (
<button className={`${SHELL} ${STYLES[variant].base} ${className}`} {...rest}> <button className={`${SHELL} ${style.base} ${style.glow} ${className}`} {...rest}>
<Inner variant={variant}>{children}</Inner> <Inner variant={variant}>{children}</Inner>
</button> </button>
) )
+49 -26
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react' 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 Swal from 'sweetalert2'
import { Reveal } from './Reveal' import { Reveal } from './Reveal'
import { Button } from './Button' import { Button } from './Button'
@@ -37,11 +37,11 @@ export function ContactSection() {
confirmButtonText: 'متوجه شدم', confirmButtonText: 'متوجه شدم',
buttonsStyling: false, buttonsStyling: false,
customClass: { customClass: {
container: 'backdrop-blur-[8px] bg-ink/60', container: 'backdrop-blur-md bg-ink/70',
popup: 'font-pinar border border-ivory/10 rounded-none shadow-lift', 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', title: 'font-pinar font-bold text-lg text-ivory pt-2',
htmlContainer: 'font-pinar font-light text-sm leading-8 text-ivory/70', 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: { showClass: {
popup: 'swal2-show', popup: 'swal2-show',
@@ -50,7 +50,7 @@ export function ContactSection() {
hideClass: { hideClass: {
popup: 'swal2-hide', popup: 'swal2-hide',
backdrop: 'swal2-backdrop-hide', backdrop: 'swal2-backdrop-hide',
} },
}) })
} }
@@ -104,19 +104,21 @@ export function ContactSection() {
aria-hidden="true" aria-hidden="true"
className="absolute inset-0 h-full w-full object-cover" 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="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> <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)} /> <InlineEdit value={content.label} onSave={(v) => saveContent('label', v)} />
</p> </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)} /> <InlineEdit value={content.title} onSave={(v) => saveContent('title', v)} />
</h2> </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)} /> <InlineEdit multiline value={content.lead} onSave={(v) => saveContent('lead', v)} />
</p> </p>
</Reveal> </Reveal>
@@ -124,7 +126,9 @@ export function ContactSection() {
<Reveal delay={0.1}> <Reveal delay={0.1}>
<ul className="mt-10 space-y-6"> <ul className="mt-10 space-y-6">
<li className="flex items-start gap-4"> <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> <div>
<p className="text-sm font-bold text-ivory"> <p className="text-sm font-bold text-ivory">
<InlineEdit value={content.address_label} onSave={(v) => saveContent('address_label', v)} /> <InlineEdit value={content.address_label} onSave={(v) => saveContent('address_label', v)} />
@@ -134,8 +138,11 @@ export function ContactSection() {
</p> </p>
</div> </div>
</li> </li>
<li className="flex items-start gap-4"> <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> <div>
<p className="text-sm font-bold text-ivory"> <p className="text-sm font-bold text-ivory">
<InlineEdit value={content.phone_label} onSave={(v) => saveContent('phone_label', v)} /> <InlineEdit value={content.phone_label} onSave={(v) => saveContent('phone_label', v)} />
@@ -149,8 +156,11 @@ export function ContactSection() {
</a> </a>
</div> </div>
</li> </li>
<li className="flex items-start gap-4"> <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> <div>
<p className="text-sm font-bold text-ivory"> <p className="text-sm font-bold text-ivory">
<InlineEdit value={content.email_label} onSave={(v) => saveContent('email_label', v)} /> <InlineEdit value={content.email_label} onSave={(v) => saveContent('email_label', v)} />
@@ -168,11 +178,14 @@ export function ContactSection() {
</Reveal> </Reveal>
</div> </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" /> <SingletonPill resource="contact_section" label="ویرایش برچسب‌ها و متن‌های فرم" className="absolute left-4 top-4" />
{sent ? ( {sent ? (
<div className="flex h-full flex-col items-center justify-center py-14 text-center"> <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"> <h3 className="mt-6 text-xl font-bold text-ivory">
<InlineEdit value={content.success_title} onSave={(v) => saveContent('success_title', v)} /> <InlineEdit value={content.success_title} onSave={(v) => saveContent('success_title', v)} />
</h3> </h3>
@@ -194,7 +207,7 @@ export function ContactSection() {
) : ( ) : (
<form onSubmit={submit} noValidate className="grid gap-6 md:grid-cols-2"> <form onSubmit={submit} noValidate className="grid gap-6 md:grid-cols-2">
<div> <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)} /> <InlineEdit value={content.name_label} onSave={(v) => saveContent('name_label', v)} />
</label> </label>
<input <input
@@ -203,12 +216,12 @@ export function ContactSection() {
value={form.name} value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })} onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder={content.name_placeholder} 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>
<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> <InlineEdit value={content.phone_field_label} onSave={(v) => saveContent('phone_field_label', v)} /> <span className="text-copper">*</span>
</label> </label>
<input <input
@@ -219,28 +232,38 @@ export function ContactSection() {
value={form.phone} value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })} onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder={content.phone_placeholder} 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>
<div className="md:col-span-2"> <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> <InlineEdit value={content.note_label} onSave={(v) => saveContent('note_label', v)} /> <span className="font-light text-ivory/50">{content.note_optional_label}</span>
</label> </label>
<textarea <textarea
id="contact-note" id="contact-note"
rows={6} rows={5}
value={form.note} value={form.note}
onChange={(e) => setForm({ ...form, note: e.target.value })} onChange={(e) => setForm({ ...form, note: e.target.value })}
placeholder={content.note_placeholder} 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>
<div className="md:col-span-2"> <div className="md:col-span-2 flex justify-start">
<Button type="submit" variant="copper" className="w-full md:w-auto md:px-12" disabled={sending}> <Button
{sending ? content.sending_label : ( type="submit"
<InlineEdit value={content.submit_label} onSave={(v) => saveContent('submit_label', v)} /> 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> </Button>
</div> </div>
+17 -4
View File
@@ -4,14 +4,27 @@ import { useSite } from '../api';
export function FloatingContact() { export function FloatingContact() {
const { brand } = useSite(); const { brand } = useSite();
return ( return (
<a <a
href={brand.phone_href} href={brand.phone_href}
aria-label="تماس تلفنی با گروه ساختمانی آرامش" 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"> 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 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" /> <span
</a>); 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>
);
} }
+45 -27
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; 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 { useSite } from '../api';
import { useFieldSaver } from '../admin/AdminContext'; import { useFieldSaver } from '../admin/AdminContext';
import { InlineEdit } from './admin/InlineEdit'; import { InlineEdit } from './admin/InlineEdit';
@@ -27,8 +27,12 @@ export function Footer() {
if (item) Object.assign(item, changes); if (item) Object.assign(item, changes);
}); });
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return ( 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="mx-auto max-w-site px-6 py-16 lg:px-10">
<div className="grid gap-12 lg:grid-cols-12"> <div className="grid gap-12 lg:grid-cols-12">
<div className="lg:col-span-4"> <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" /> <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" /> <SingletonPill resource="brand" label="ویرایش برند، لوگو و تماس" className="absolute -left-10 top-1/2 -translate-y-1/2" />
</div> </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)} /> <InlineEdit multiline value={footer.about_text} onSave={(v) => saveFooter('about_text', v)} />
</p> </p>
<div className="mt-6 flex gap-3"> {/* آیکون‌های شبکه‌های اجتماعی: گوی‌های شیشه‌ای شناور */}
<div className="mt-8 flex items-center gap-3">
{footer.socials.map(({ label, href, icon }) => { {footer.socials.map(({ label, href, icon }) => {
const Icon = SOCIAL_ICONS[icon] ?? InstagramIcon; const Icon = SOCIAL_ICONS[icon] ?? InstagramIcon;
return ( return (
@@ -47,9 +52,9 @@ export function Footer() {
key={label} key={label}
href={href} href={href}
aria-label={label} 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"> 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" /> <Icon className="h-4 w-4 transition-transform duration-300 group-hover:scale-110" />
</a> </a>
); );
})} })}
@@ -61,21 +66,21 @@ export function Footer() {
<InlineEdit value={footer.pages_heading} onSave={(v) => saveFooter('pages_heading', v)} /> <InlineEdit value={footer.pages_heading} onSave={(v) => saveFooter('pages_heading', v)} />
</h3> </h3>
<ul className="mt-5 space-y-3"> <ul className="mt-5 space-y-3">
{nav.map((item) => {nav.map((item) => (
<EditableItem key={item.id} resource="nav" item={item}> <EditableItem key={item.id} resource="nav" item={item}>
<li> <li>
<Link <Link
to={item.href} to={item.href}
className="text-sm font-light text-ivory/60 transition-colors duration-200 ease-lux hover:text-copper"> className="inline-block text-sm font-light text-ivory/60 transition-colors duration-200 ease-lux hover:text-copper"
>
<InlineEdit <InlineEdit
value={item.label} value={item.label}
onSave={(v) => saveNav(item.id, { label: v })} onSave={(v) => saveNav(item.id, { label: v })}
/> />
</Link> </Link>
</li> </li>
</EditableItem> </EditableItem>
)} ))}
</ul> </ul>
<div className="mt-4"> <div className="mt-4">
<AddItemButton resource="nav" /> <AddItemButton resource="nav" />
@@ -87,11 +92,11 @@ export function Footer() {
<InlineEdit value={footer.services_heading} onSave={(v) => saveFooter('services_heading', v)} /> <InlineEdit value={footer.services_heading} onSave={(v) => saveFooter('services_heading', v)} />
</h3> </h3>
<ul className="mt-5 space-y-3"> <ul className="mt-5 space-y-3">
{services.map((service) => {services.map((service) => (
<li key={service.id} className="text-sm font-light text-ivory/60"> <li key={service.id} className="text-sm font-light text-ivory/60">
{service.title} {service.title}
</li> </li>
)} ))}
</ul> </ul>
</div> </div>
@@ -120,15 +125,28 @@ export function Footer() {
</div> </div>
</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> <p>
<InlineEdit value={footer.copyright} onSave={(v) => saveFooter('copyright', v)} /> <InlineEdit value={footer.copyright} onSave={(v) => saveFooter('copyright', v)} />
</p> </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> <p>
<InlineEdit value={footer.tagline} onSave={(v) => saveFooter('tagline', v)} /> <InlineEdit value={footer.tagline} onSave={(v) => saveFooter('tagline', v)} />
</p> </p>
</div> </div>
</div> </div>
</footer>); </footer>
);
} }
+65 -66
View File
@@ -31,27 +31,27 @@ export function Header() {
return ( return (
<header <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 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" : "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="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"> <div className="relative flex items-center">
<Link <Link
to="/" to="/"
className="flex items-center gap-3" className="flex items-center gap-3"
aria-label={brand.name} aria-label={brand.name}
> >
<img <img
src={brand.logo} src={brand.logo}
alt={brand.name} alt={brand.name}
className={`w-auto object-contain transition-[height] duration-300 ease-lux ${ className={`w-auto object-contain transition-[height] duration-300 ease-lux ${
scrolled ? "h-14" : "h-20" scrolled ? "h-14" : "h-20"
}`} }`}
/> />
</Link> </Link>
<SingletonPill resource="brand" label="ویرایش برند و لوگو" className="absolute -left-10 top-1/2 -translate-y-1/2" /> <SingletonPill resource="brand" label="ویرایش برند و لوگو" className="absolute -left-10 top-1/2 -translate-y-1/2" />
</div> </div>
@@ -59,55 +59,54 @@ export function Header() {
<ul className="flex items-center gap-7"> <ul className="flex items-center gap-7">
{nav.map((item) => ( {nav.map((item) => (
<EditableItem key={item.id} resource="nav" item={item}> <EditableItem key={item.id} resource="nav" item={item}>
<li> <li>
<NavLink <NavLink
to={item.href} to={item.href}
className={({ isActive }) => 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)] ${ `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 ? "text-copper font-normal" : "text-ivory/80"
}` }`
} }
> >
{({ isActive }) => ( {({ isActive }) => (
<> <>
<InlineEdit <InlineEdit
value={item.label} value={item.label}
onSave={(v) => saveNav(item.id, { label: v })} onSave={(v) => saveNav(item.id, { label: v })}
/> />
<span <span
className={`absolute -bottom-0.5 left-0 right-0 h-px w-full origin-center bg-copper transition-transform duration-300 ease-lux ${ 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 isActive
? "scale-x-100" ? "scale-x-100 shadow-[0_0_8px_rgba(197,143,92,0.8)]"
: "scale-x-0 group-hover:scale-x-100" : "scale-x-0 group-hover:scale-x-100"
}`} }`}
/> />
</> </>
)} )}
</NavLink> </NavLink>
</li> </li>
</EditableItem> </EditableItem>
))} ))}
</ul> </ul>
</nav> </nav>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* دکمه تماس کپسولی شیشه‌ای لوکس */}
<a <a
href={brand.phone_href} 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 <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">
aria-hidden="true" <PhoneIcon className="h-3.5 w-3.5 transition-transform duration-300 group-hover:rotate-12" />
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> </span>
<span dir="ltr" className="tracking-wide">{brand.phone}</span>
</a> </a>
{/* دکمه منوی موبایل دایره‌ای شیشه‌ای */}
<button <button
type="button" type="button"
onClick={() => setOpen((v) => !v)} 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-label={open ? "بستن منو" : "باز کردن منو"}
aria-expanded={open} aria-expanded={open}
> >
@@ -123,26 +122,26 @@ export function Header() {
{open && ( {open && (
<nav <nav
aria-label="ناوبری موبایل" 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"> <ul className="grid gap-1">
{nav.map((item) => ( {nav.map((item) => (
<EditableItem key={item.id} resource="nav" item={item}> <EditableItem key={item.id} resource="nav" item={item}>
<li> <li>
<NavLink <NavLink
to={item.href} to={item.href}
className={({ isActive }) => className={({ isActive }) =>
`block border-b border-ivory/5 py-3 text-base font-light transition-colors duration-200 ease-lux ${ `block border-b border-ivory/5 py-3 text-base font-light transition-colors duration-200 ease-lux ${
isActive ? "text-copper" : "text-ivory/80" isActive ? "text-copper" : "text-ivory/80"
}` }`
} }
> >
<InlineEdit <InlineEdit
value={item.label} value={item.label}
onSave={(v) => saveNav(item.id, { label: v })} onSave={(v) => saveNav(item.id, { label: v })}
/> />
</NavLink> </NavLink>
</li> </li>
</EditableItem> </EditableItem>
))} ))}
</ul> </ul>
View File
+16 -10
View File
@@ -1,49 +1,55 @@
import React from 'react' import React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ArrowLeftIcon } from 'lucide-react'
import { useStatusLabel } from '../api' import { useStatusLabel } from '../api'
export function ProjectCard({ project }: any) { export function ProjectCard({ project }: any) {
const isOngoing = project.status === 'progress' const isOngoing = project.status === 'progress'
const statusText = useStatusLabel(project.status) const statusText = useStatusLabel(project.status)
const dotColor = isOngoing ? 'bg-copper animate-pulse' : 'bg-ivory/60' const dotColor = isOngoing ? 'bg-copper animate-pulse' : 'bg-ivory/60'
const textColor = isOngoing ? 'text-copper' : 'text-ivory/90' const textColor = isOngoing ? 'text-copper' : 'text-ivory/90'
return ( 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"> <Link to={`/projects/${project.slug || '#'}`} className="flex h-full flex-col focus:outline-none">
{/* کادر تصویر به همراه نشانگر وضعیت کپسولی و گوی دکمه شیشه‌ای */}
<div className="relative aspect-[4/3] w-full overflow-hidden"> <div className="relative aspect-[4/3] w-full overflow-hidden">
<img <img
src={project.image} src={project.image}
alt={project.title} alt={project.title}
className="h-full w-full object-cover transition-transform duration-700 ease-lux group-hover:scale-105" 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} {statusText}
</div> </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>
<div className="flex flex-1 flex-col p-8"> <div className="flex flex-1 flex-col p-7">
<div className="text-center"> <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} {project.title}
</h3> </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} {project.location}
</p> </p>
</div> </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"> <div className="mt-auto flex items-center justify-between text-sm font-light text-ivory/60">
<span>سال: {project.year}</span> <span>سال: {project.year}</span>
<span>{project.area} متر مربع</span> <span>{project.area} متر مربع</span>
</div> </div>
</div> </div>
</Link> </Link>
</article> </article>
@@ -36,34 +36,35 @@ export function FeaturedProjects() {
<div className="mx-auto max-w-site px-6 lg:px-10"> <div className="mx-auto max-w-site px-6 lg:px-10">
<SectionHeading title={heading.title} lead={heading.lead} headingKey="featured_projects" /> <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="فیلتر وضعیت پروژه"> <div className="mt-10 flex flex-wrap items-center gap-3" role="tablist" aria-label="فیلتر وضعیت پروژه">
{FILTERS.map((filter) => ( {FILTERS.map((filter) => {
<button const isSelected = status === filter.key
key={filter.key} return (
type="button" <button
role="tab" key={filter.key}
aria-selected={status === filter.key} type="button"
onClick={() => setStatus(filter.key)} role="tab"
className={`group relative overflow-hidden border px-7 py-3 text-sm transition-colors duration-300 ease-out ${ aria-selected={isSelected}
status === filter.key onClick={() => setStatus(filter.key)}
? 'border-copper bg-copper font-bold text-char' 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 ${
: 'border-ivory/15 font-light text-ivory/65' 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)]'
{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="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">
<span className="relative z-10 transition-colors duration-300 ease-out group-hover:text-char"> <InlineEdit
<InlineEdit value={filter.label}
value={filter.label} onSave={(v) => saveStatusLabel(filter.key, v)}
onSave={(v) => saveStatusLabel(filter.key, v)} />
/> </span>
</span> </button>
</button> )
))} })}
</div> </div>
<div className="mt-10 min-h-[420px]"> <div className="mt-10 min-h-[420px]">
@@ -93,10 +94,15 @@ export function FeaturedProjects() {
</AnimatePresence> </AnimatePresence>
</div> </div>
{/* دکمه مشاهده همه پروژه‌ها: کپسولی شیشه‌ای با گوی آیکون متحرک */}
{heading.cta_label && heading.cta_href && ( {heading.cta_label && heading.cta_href && (
<Reveal delay={0.1}> <Reveal delay={0.1}>
<div className="mt-12 flex justify-center"> <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 <InlineEdit
value={heading.cta_label} value={heading.cta_label}
onSave={(v) => 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> </ButtonLink>
</div> </div>
</Reveal> </Reveal>
+38 -27
View File
@@ -20,23 +20,9 @@ export function Hero() {
;(d.hero as unknown as Record<string, unknown>)[field] = value ;(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 ( return (
<section className="relative h-screen min-h-[640px] w-full overflow-hidden" aria-label="معرفی گروه ساختمانی آرامش"> <section className="relative h-screen min-h-[640px] w-full overflow-hidden" aria-label="معرفی گروه ساختمانی آرامش">
{/* تصویر پس‌زمینه */}
<motion.img <motion.img
src={brand.hero} src={brand.hero}
alt="نمای شبانه برج مسکونی ساخته‌شده توسط گروه ساختمانی آرامش" alt="نمای شبانه برج مسکونی ساخته‌شده توسط گروه ساختمانی آرامش"
@@ -53,61 +39,82 @@ export function Hero() {
aria-hidden="true" aria-hidden="true"
/> />
{/* محتوای اصلی هیرو */}
<motion.div <motion.div
style={{ y: contentY }} style={{ y: contentY }}
className="relative mx-auto flex h-full max-w-site flex-col justify-center px-6 lg:px-10" 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%]"> <div className="w-full max-w-[700px] lg:max-w-[58%]">
{/* کیکر بالای تیتر */}
<motion.p <motion.p
initial={{ opacity: 0, y: 18 }} initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 2.1, ease: [0.23, 1, 0.32, 1] }} 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)} /> <InlineEdit value={hero.kicker} onSave={(v) => saveHero('kicker', v)} />
</motion.p> </motion.p>
{/* تیتر اصلی */}
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 28 }} initial={{ opacity: 0, y: 28 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.85, delay: 2.2, ease: [0.23, 1, 0.32, 1] }} 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="تیتر اصلی صفحه" /> <InlineEdit value={hero.title} onSave={(v) => saveHero('title', v)} aria-label="تیتر اصلی صفحه" />
</motion.h1> </motion.h1>
{/* زیرتیتر */}
<motion.p <motion.p
initial={{ opacity: 0, y: 22 }} initial={{ opacity: 0, y: 22 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.85, delay: 2.32, ease: [0.23, 1, 0.32, 1] }} 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)} /> <InlineEdit multiline value={hero.subtitle} onSave={(v) => saveHero('subtitle', v)} />
</motion.p> </motion.p>
{/* داک دکمه‌ها: استایل کپسولی، شیشه‌ای و دایره‌ای */}
<motion.div <motion.div
initial={{ opacity: 0, y: 22 }} initial={{ opacity: 0, y: 22 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.85, delay: 2.44, ease: [0.23, 1, 0.32, 1] }} transition={{ duration: 0.85, delay: 2.44, ease: [0.23, 1, 0.32, 1] }}
className="mt-10 flex flex-wrap items-center gap-4" 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)} /> <ButtonLink
<ArrowLeftIcon className="h-4 w-4" /> 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>
<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)} /> <InlineEdit value={hero.cta_secondary_label} onSave={(v) => saveHero('cta_secondary_label', v)} />
</ButtonLink> </ButtonLink>
</motion.div> </motion.div>
</div> </div>
{/* کادر آمار شناور (گوشه پایین چپ): شیشه‌ای با لبه‌های گرد مدرن */}
<motion.div <motion.div
initial={{ opacity: 0, x: -24 }} initial={{ opacity: 0, x: -24 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.9, delay: 2.6, ease: [0.23, 1, 0.32, 1] }} 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)} /> <InlineEdit value={hero.badge_value} onSave={(v) => saveHero('badge_value', v)} />
</p> </p>
<p className="mt-2 text-sm font-light leading-7 text-ivory/70"> <p className="mt-2 text-sm font-light leading-7 text-ivory/70">
@@ -116,17 +123,21 @@ export function Hero() {
</motion.div> </motion.div>
</motion.div> </motion.div>
{/* دکمه اسکرول پایین: تبدیل‌شده به یک گوی شیشه‌ای دایره‌ای */}
<a <a
href="#story" href="#story"
aria-label="رفتن به بخش بعدی" 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)} /> <InlineEdit value={hero.scroll_label} onSave={(v) => saveHero('scroll_label', v)} />
</span> </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> </a>
{/* کنترل‌های ادمین */}
<div className="absolute left-6 top-24 z-20 flex flex-col gap-2 lg:left-10"> <div className="absolute left-6 top-24 z-20 flex flex-col gap-2 lg:left-10">
<SingletonPill resource="hero" label="تنظیمات هیرو (واژه مسی، دکمه‌ها)" /> <SingletonPill resource="hero" label="تنظیمات هیرو (واژه مسی، دکمه‌ها)" />
<SingletonPill <SingletonPill
+27 -15
View File
@@ -32,11 +32,11 @@ export function Newsletter() {
confirmButtonText: 'متوجه شدم', confirmButtonText: 'متوجه شدم',
buttonsStyling: false, buttonsStyling: false,
customClass: { customClass: {
container: 'backdrop-blur-[8px] bg-ink/60', container: 'backdrop-blur-md bg-ink/70',
popup: 'font-pinar border border-ivory/10 rounded-none shadow-lift', 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', title: 'font-pinar font-bold text-lg text-ivory pt-2',
htmlContainer: 'font-pinar font-light text-sm leading-8 text-ivory/70', 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: { showClass: {
popup: 'swal2-show', popup: 'swal2-show',
@@ -96,20 +96,25 @@ export function Newsletter() {
} }
return ( 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" /> <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> <Reveal>
<h2 id="newsletter-title" className="text-2xl font-bold leading-relaxed text-char md:text-3xl"> <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)} /> <InlineEdit value={newsletter.title} onSave={(v) => saveContent('title', v)} />
</h2> </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)} /> <InlineEdit multiline value={newsletter.lead} onSave={(v) => saveContent('lead', v)} />
</p> </p>
</Reveal> </Reveal>
<Reveal delay={0.08}> <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"> <div className="flex-1">
<label htmlFor="newsletter-email" className="sr-only"> <label htmlFor="newsletter-email" className="sr-only">
{newsletter.placeholder} {newsletter.placeholder}
@@ -124,22 +129,29 @@ export function Newsletter() {
if (status !== 'idle') setStatus('idle') if (status !== 'idle') setStatus('idle')
}} }}
placeholder={newsletter.placeholder} 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'} aria-invalid={status === 'error'}
/> />
{status === 'done' && ( {status === 'done' && (
<p className="mt-2 flex items-center gap-1.5 text-xs font-light text-char" role="status"> <p className="mt-2.5 flex items-center gap-1.5 px-3 text-xs font-medium text-char" role="status">
<CheckIcon className="h-3.5 w-3.5" /> <CheckIcon className="h-4 w-4" />
{newsletter.success_text} {newsletter.success_text}
</p> </p>
)} )}
</div> </div>
<Button type="submit" variant="dark-ivory" disabled={sending}> <Button
<SendIcon className="h-4 w-4" /> type="submit"
{sending ? newsletter.sending_label : ( variant="dark"
<InlineEdit value={newsletter.button_label} onSave={(v) => saveContent('button_label', v)} /> 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> </Button>
</form> </form>
</Reveal> </Reveal>
+9 -3
View File
@@ -5,10 +5,16 @@ import react from '@vitejs/plugin-react'
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
// در حالت توسعه، /api و /media به Traefik پروکسی می‌شوند (پورت ۵۱۷۳ روی هاست)
proxy: { proxy: {
'/api': 'http://localhost:5173', '/api': {
'/media': 'http://localhost:5173', target: 'http://localhost:8000',
changeOrigin: true,
},
'/media': {
target: 'http://localhost:9000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/media/, '/aramesh-media'),
},
}, },
}, },
}) })