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 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">
|
||||
|
||||
@@ -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",
|
||||
} 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;
|
||||
|
||||
Reference in New Issue
Block a user