Countactus

This commit is contained in:
amirhosein.ashourloo
2026-08-15 17:15:53 +03:30
parent 2f6c1a547e
commit 6f7e24d21b
5 changed files with 369 additions and 44 deletions
+128
View File
@@ -0,0 +1,128 @@
import type { Metadata } from "next";
import Link from "next/link";
import heroImage from "@/assets/images/hero/hero-3.jpg";
import heroImageMobile from "@/assets/images/hero/hero-3-mobile.jpg";
import { InstagramIcon, MailIcon, PhoneIcon, PinIcon } from "@/components/ContactIcons";
import { ContactForm } from "@/components/contact/ContactForm";
import { Reveal } from "@/components/Reveal";
import { Editorial } from "@/components/shared/Editorial";
import { PageHero } from "@/components/shared/PageHero";
import { SectionLabel } from "@/components/shared/SectionLabel";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { contact, contactPage } from "@/data/site";
export const metadata: Metadata = {
title: "تماس با ما",
description: "راه‌های ارتباط با گروه ساختمانی رئوفی — آدرس، تلفن و فرم تماس.",
};
const MAP_EMBED_SRC =
"https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3905.0312598595406!2d50.83598775206012!3d34.61106958301533!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3f93a3007813166d%3A0x9bfd1a913f50194f!2zUmFvb2ZpIGdyb3VwINqv2LHZiNmHINiz2KfYrtiq2YXYp9mG24wg2LHYptmI2YHbjA!5e0!3m2!1sen!2suk!4v1786800829844!5m2!1sen!2suk";
export default function ContactPage() {
return (
<>
<SiteHeader />
<main>
<PageHero
image={heroImage}
imageMobile={heroImageMobile}
title={contactPage.hero.title}
breadcrumb={contactPage.hero.breadcrumb}
/>
<section className="bg-paper">
<Editorial
caption={contactPage.overview.caption}
captionLatin={contactPage.overview.captionLatin}
title={contactPage.overview.title}
body={contactPage.overview.body}
compact
/>
</section>
<section className="bg-sand py-16 lg:py-24">
<div className="mx-auto grid max-w-[1400px] gap-14 px-6 lg:grid-cols-2 lg:gap-20 lg:px-12">
{/* Contact info + map */}
<Reveal>
<SectionLabel
fa={contactPage.info.caption}
en={contactPage.info.captionLatin}
align="start"
className="mb-10"
/>
<ul className="space-y-6">
<li className="flex gap-4">
<PinIcon className="mt-1 h-5 w-5 shrink-0 text-gold" />
<p className="leading-[2]">{contact.address}</p>
</li>
{contact.phones.map((phone, i) => (
<li key={phone}>
<Link
href={`tel:${contact.phoneLinks[i]}`}
className="group flex items-center gap-4 transition-colors duration-300 hover:text-gold"
>
<PhoneIcon className="h-5 w-5 shrink-0 text-gold" />
<span className="latin">{phone}</span>
</Link>
</li>
))}
<li>
<Link
href={`mailto:${contact.email}`}
className="group flex items-center gap-4 transition-colors duration-300 hover:text-gold"
>
<MailIcon className="h-5 w-5 shrink-0 text-gold" />
<span className="latin">{contact.email}</span>
</Link>
</li>
<li>
<Link
href={contact.instagramUrl}
className="group flex items-center gap-4 transition-colors duration-300 hover:text-gold"
>
<InstagramIcon className="h-5 w-5 shrink-0 text-gold" />
<span className="latin">{contact.instagram}</span>
</Link>
</li>
</ul>
<div className="mt-10 aspect-[4/3] w-full overflow-hidden">
<iframe
src={MAP_EMBED_SRC}
title="نقشه‌ی موقعیت گروه ساختمانی رئوفی"
loading="lazy"
allowFullScreen
referrerPolicy="strict-origin-when-cross-origin"
className="h-full w-full grayscale-[15%]"
/>
</div>
</Reveal>
{/* Form */}
<Reveal delay={90}>
<div className="bg-paper p-7 lg:p-10">
<SectionLabel
fa={contactPage.form.caption}
en={contactPage.form.captionLatin}
align="start"
className="mb-8"
/>
<ContactForm />
</div>
</Reveal>
</div>
</section>
</main>
<SiteFooter />
</>
);
}
+45
View File
@@ -0,0 +1,45 @@
/** Shared icon set for contact details — used by the footer and the contact page. */
export function PinIcon({ className = "mt-1 h-4 w-4 shrink-0 text-gold" }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path
d="M12 21s7-5.5 7-11a7 7 0 1 0-14 0c0 5.5 7 11 7 11Z"
stroke="currentColor"
strokeWidth="1.4"
/>
<circle cx="12" cy="10" r="2.5" stroke="currentColor" strokeWidth="1.4" />
</svg>
);
}
export function PhoneIcon({ className = "h-4 w-4 shrink-0 text-gold" }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path
d="M6.5 3.5h3l1.5 4-2 1.5a12 12 0 0 0 6 6l1.5-2 4 1.5v3a2 2 0 0 1-2.2 2A16.5 16.5 0 0 1 4.5 5.7a2 2 0 0 1 2-2.2Z"
stroke="currentColor"
strokeWidth="1.4"
/>
</svg>
);
}
export function MailIcon({ className = "h-4 w-4 shrink-0 text-gold" }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<rect x="3" y="5.5" width="18" height="13" rx="2" stroke="currentColor" strokeWidth="1.4" />
<path d="m3.5 7 8.5 6 8.5-6" stroke="currentColor" strokeWidth="1.4" />
</svg>
);
}
export function InstagramIcon({ className = "h-4 w-4 shrink-0 text-gold" }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<rect x="3.5" y="3.5" width="17" height="17" rx="5" stroke="currentColor" strokeWidth="1.4" />
<circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="1.4" />
<circle cx="17" cy="7" r="1.1" fill="currentColor" />
</svg>
);
}
+1 -44
View File
@@ -2,52 +2,9 @@ import Image from "next/image";
import Link from "next/link";
import logo from "@/assets/images/brand/logo.png";
import { InstagramIcon, MailIcon, PhoneIcon, PinIcon } from "@/components/ContactIcons";
import { contact, nav, site } from "@/data/site";
function PinIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="mt-1 h-4 w-4 shrink-0 text-gold">
<path
d="M12 21s7-5.5 7-11a7 7 0 1 0-14 0c0 5.5 7 11 7 11Z"
stroke="currentColor"
strokeWidth="1.4"
/>
<circle cx="12" cy="10" r="2.5" stroke="currentColor" strokeWidth="1.4" />
</svg>
);
}
function PhoneIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4 shrink-0 text-gold">
<path
d="M6.5 3.5h3l1.5 4-2 1.5a12 12 0 0 0 6 6l1.5-2 4 1.5v3a2 2 0 0 1-2.2 2A16.5 16.5 0 0 1 4.5 5.7a2 2 0 0 1 2-2.2Z"
stroke="currentColor"
strokeWidth="1.4"
/>
</svg>
);
}
function MailIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4 shrink-0 text-gold">
<rect x="3" y="5.5" width="18" height="13" rx="2" stroke="currentColor" strokeWidth="1.4" />
<path d="m3.5 7 8.5 6 8.5-6" stroke="currentColor" strokeWidth="1.4" />
</svg>
);
}
function InstagramIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-4 w-4 shrink-0 text-gold">
<rect x="3.5" y="3.5" width="17" height="17" rx="5" stroke="currentColor" strokeWidth="1.4" />
<circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="1.4" />
<circle cx="17" cy="7" r="1.1" fill="currentColor" />
</svg>
);
}
export function SiteFooter() {
return (
<footer className="bg-ink-2 pt-20 text-white/70">
+151
View File
@@ -0,0 +1,151 @@
"use client";
import { useState } from "react";
import { contact, contactPage } from "@/data/site";
type FormState = { name: string; phone: string; email: string; message: string };
type Errors = Partial<Record<keyof FormState, string>>;
const EMPTY: FormState = { name: "", phone: "", email: "", message: "" };
const IRAN_PHONE = /^0\d{10}$/;
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function toEnglishDigits(value: string) {
const persian = "۰۱۲۳۴۵۶۷۸۹";
return value.replace(/[۰-۹]/g, (d) => String(persian.indexOf(d)));
}
function validate(form: FormState): Errors {
const errors: Errors = {};
if (!form.name.trim()) errors.name = "نام را وارد کنید.";
const phone = toEnglishDigits(form.phone.trim());
if (!phone) errors.phone = "شماره تماس را وارد کنید.";
else if (!IRAN_PHONE.test(phone)) errors.phone = "شماره تماس معتبر نیست (مثال: ۰۹۱۲۱۲۳۴۵۶۷).";
if (form.email.trim() && !EMAIL_PATTERN.test(form.email.trim())) {
errors.email = "ایمیل معتبر نیست.";
}
if (!form.message.trim()) errors.message = "پیام را بنویسید.";
return errors;
}
const fieldClass =
"w-full rounded-md border border-line bg-paper px-4 py-3 text-ink placeholder:text-muted/60 transition-colors duration-300 focus:border-gold focus:outline-none";
export function ContactForm() {
const [form, setForm] = useState<FormState>(EMPTY);
const [errors, setErrors] = useState<Errors>({});
const [sent, setSent] = useState(false);
const update = (field: keyof FormState) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
setForm((f) => ({ ...f, [field]: e.target.value }));
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const nextErrors = validate(form);
setErrors(nextErrors);
if (Object.keys(nextErrors).length > 0) return;
// TODO: replace with POST /tenants/{domain}/contact-submissions once the
// API is ready — for now this opens a pre-filled email as a real,
// working fallback rather than faking a "sent" confirmation.
const subject = encodeURIComponent(`پیام از سایت — ${form.name}`);
const body = encodeURIComponent(
`نام: ${form.name}\nتلفن: ${toEnglishDigits(form.phone)}${
form.email ? `\nایمیل: ${form.email}` : ""
}\n\nپیام:\n${form.message}`,
);
window.location.href = `mailto:${contact.email}?subject=${subject}&body=${body}`;
setSent(true);
};
return (
<form onSubmit={handleSubmit} noValidate className="flex flex-col gap-5">
<div>
<label htmlFor="contact-name" className="mb-2 block text-sm text-muted">
{contactPage.form.nameLabel}
</label>
<input
id="contact-name"
type="text"
value={form.name}
onChange={update("name")}
placeholder={contactPage.form.namePlaceholder}
aria-invalid={!!errors.name}
className={fieldClass}
/>
{errors.name && <p className="mt-1.5 text-xs text-red-600">{errors.name}</p>}
</div>
<div className="grid gap-5 sm:grid-cols-2">
<div>
<label htmlFor="contact-phone" className="mb-2 block text-sm text-muted">
{contactPage.form.phoneLabel}
</label>
<input
id="contact-phone"
type="tel"
inputMode="numeric"
value={form.phone}
onChange={update("phone")}
placeholder={contactPage.form.phonePlaceholder}
aria-invalid={!!errors.phone}
className={`${fieldClass} latin text-end`}
/>
{errors.phone && <p className="mt-1.5 text-xs text-red-600">{errors.phone}</p>}
</div>
<div>
<label htmlFor="contact-email" className="mb-2 block text-sm text-muted">
{contactPage.form.emailLabel}
</label>
<input
id="contact-email"
type="email"
value={form.email}
onChange={update("email")}
placeholder={contactPage.form.emailPlaceholder}
aria-invalid={!!errors.email}
className={`${fieldClass} latin text-end`}
/>
{errors.email && <p className="mt-1.5 text-xs text-red-600">{errors.email}</p>}
</div>
</div>
<div>
<label htmlFor="contact-message" className="mb-2 block text-sm text-muted">
{contactPage.form.messageLabel}
</label>
<textarea
id="contact-message"
rows={5}
value={form.message}
onChange={update("message")}
placeholder={contactPage.form.messagePlaceholder}
aria-invalid={!!errors.message}
className={`${fieldClass} resize-none`}
/>
{errors.message && <p className="mt-1.5 text-xs text-red-600">{errors.message}</p>}
</div>
<button
type="submit"
className="mt-2 rounded-full bg-gold px-9 py-3.5 text-white transition-colors duration-300 hover:bg-ink"
>
{contactPage.form.submit}
</button>
<p className="text-xs leading-relaxed text-muted">{contactPage.form.fallbackNotice}</p>
{sent && (
<p role="status" className="text-sm text-gold">
نرم‌افزار ایمیل شما باز شد — لطفاً پیام را از آنجا ارسال کنید.
</p>
)}
</form>
);
}
+44
View File
@@ -42,6 +42,50 @@ export const contact = {
loginUrl: "https://app.raoofigroup.com/auth/login",
} as const;
/**
* Contact page copy. The form itself doesn't post anywhere yet — see
* src/components/contact/ContactForm.tsx — it falls back to a pre-filled
* mailto: link until the meshkee contact-submissions endpoint is wired up.
*/
export const contactPage = {
hero: {
title: "تماس با ما",
breadcrumb: [
{ label: "صفحه اصلی", href: "/" },
{ label: "تماس با ما", href: "/contactus" },
],
},
overview: {
caption: "تماس با ما",
captionLatin: "CONTACT US",
title: "برای شروع یک پروژه، همین حالا با ما در تماس باشید",
body: [
"تیم گروه ساختمانی رئوفی آماده‌ی پاسخ‌گویی به سوالات شما درباره‌ی پروژه‌ها، سرمایه‌گذاری و همکاری است.",
],
},
info: {
caption: "راه‌های ارتباطی",
captionLatin: "GET IN TOUCH",
},
form: {
caption: "فرم تماس",
captionLatin: "SEND A MESSAGE",
title: "پیام خود را برای ما بفرستید",
nameLabel: "نام و نام خانوادگی",
namePlaceholder: "نام شما",
phoneLabel: "شماره تماس",
phonePlaceholder: "۰۹۱۲۱۲۳۴۵۶۷",
emailLabel: "ایمیل (اختیاری)",
emailPlaceholder: "email@example.com",
messageLabel: "پیام",
messagePlaceholder: "پیام خود را بنویسید...",
submit: "ارسال پیام",
submitting: "در حال آماده‌سازی...",
fallbackNotice:
"فرم هنوز به سرویس ارسال متصل نشده — با کلیک روی «ارسال پیام»، ایمیل شما با متن پیام باز می‌شود.",
},
} as const;
export type HeroSlide = {
id: string;
desktop: StaticImageData;