feat: update UI to glassmorphic design and copper theme
This commit is contained in:
+7
-3
@@ -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 تنظیم کنید}
|
||||||
@@ -151,4 +155,4 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_data:
|
db_data:
|
||||||
minio_data:
|
minio_data:
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -251,4 +274,4 @@ export function ContactSection() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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 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 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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -150,4 +149,4 @@ export function Header() {
|
|||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -114,4 +122,4 @@ export function FeaturedProjects() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -137,4 +148,4 @@ export function Hero() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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,26 +129,33 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+10
-4
@@ -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'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user