Countactus
This commit is contained in:
@@ -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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,52 +2,9 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import logo from "@/assets/images/brand/logo.png";
|
import logo from "@/assets/images/brand/logo.png";
|
||||||
|
import { InstagramIcon, MailIcon, PhoneIcon, PinIcon } from "@/components/ContactIcons";
|
||||||
import { contact, nav, site } from "@/data/site";
|
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() {
|
export function SiteFooter() {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-ink-2 pt-20 text-white/70">
|
<footer className="bg-ink-2 pt-20 text-white/70">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,6 +42,50 @@ export const contact = {
|
|||||||
loginUrl: "https://app.raoofigroup.com/auth/login",
|
loginUrl: "https://app.raoofigroup.com/auth/login",
|
||||||
} as const;
|
} 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 = {
|
export type HeroSlide = {
|
||||||
id: string;
|
id: string;
|
||||||
desktop: StaticImageData;
|
desktop: StaticImageData;
|
||||||
|
|||||||
Reference in New Issue
Block a user