Initial commit: Negin Dental Clinic website
Next.js (App Router) clone of negindentalclinic.ir, wired to the Meshkee Website API (blog, contact form, favicon), with on-page SEO and a scroll-reveal animation sequence on the homepage. See context.md for full project context. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { submitContact } from "@/lib/meshkee";
|
||||
import styles from "./contact.module.css";
|
||||
|
||||
const EMPTY = { name: "", email: "", phone: "", subject: "", message: "" };
|
||||
|
||||
export default function ContactForm() {
|
||||
const [values, setValues] = useState(EMPTY);
|
||||
const [status, setStatus] = useState("idle"); // idle | sending | error
|
||||
const [error, setError] = useState("");
|
||||
const [toast, setToast] = useState(null); // { text, kind } | null
|
||||
const toastTimer = useRef(null);
|
||||
|
||||
useEffect(() => () => clearTimeout(toastTimer.current), []);
|
||||
|
||||
const showToast = (text, kind = "success") => {
|
||||
setToast({ text, kind });
|
||||
clearTimeout(toastTimer.current);
|
||||
toastTimer.current = setTimeout(() => setToast(null), 4500);
|
||||
};
|
||||
|
||||
const onChange = (e) =>
|
||||
setValues((v) => ({ ...v, [e.target.name]: e.target.value }));
|
||||
|
||||
const onSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setStatus("sending");
|
||||
setError("");
|
||||
try {
|
||||
await submitContact({
|
||||
title: values.subject,
|
||||
name: values.name,
|
||||
text: values.message,
|
||||
email: values.email || undefined,
|
||||
cellNumber: values.phone || undefined,
|
||||
});
|
||||
setValues(EMPTY);
|
||||
setStatus("idle");
|
||||
showToast("پیام شما با موفقیت ارسال شد. همکاران ما در اسرع وقت با شما تماس خواهند گرفت.");
|
||||
} catch (err) {
|
||||
setStatus("error");
|
||||
setError(err.message || "ارسال پیام با خطا مواجه شد. لطفاً دوباره تلاش کنید.");
|
||||
showToast("ارسال پیام ناموفق بود. لطفاً دوباره تلاش کنید.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form className={styles.form} onSubmit={onSubmit} noValidate>
|
||||
<div className={styles.row}>
|
||||
<label>
|
||||
<span>نام و نام خانوادگی</span>
|
||||
<input
|
||||
name="name"
|
||||
value={values.name}
|
||||
onChange={onChange}
|
||||
required
|
||||
autoComplete="name"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>آدرس الکترونیکی</span>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={values.email}
|
||||
onChange={onChange}
|
||||
autoComplete="email"
|
||||
dir="ltr"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.row}>
|
||||
<label>
|
||||
<span>شماره تماس</span>
|
||||
<input
|
||||
name="phone"
|
||||
value={values.phone}
|
||||
onChange={onChange}
|
||||
autoComplete="tel"
|
||||
inputMode="tel"
|
||||
dir="ltr"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>موضوع</span>
|
||||
<input
|
||||
name="subject"
|
||||
value={values.subject}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<span>پیام شما</span>
|
||||
<textarea
|
||||
name="message"
|
||||
rows={6}
|
||||
value={values.message}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button type="submit" className="btn" disabled={status === "sending"}>
|
||||
{status === "sending" ? "در حال ارسال…" : "ارسال پیام"}
|
||||
</button>
|
||||
|
||||
{status === "error" && error ? (
|
||||
<p className={styles.formError} role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
|
||||
{toast ? (
|
||||
<div
|
||||
className={`${styles.toast} ${toast.kind === "error" ? styles.toastError : ""}`}
|
||||
role="status"
|
||||
>
|
||||
{toast.text}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user