first commit

This commit is contained in:
grigorij
2026-08-03 21:02:34 +04:00
commit b0bafe740f
39 changed files with 2326 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
"use client";
import { useState, FormEvent } from "react";
import { Loader2, CheckCircle2, AlertCircle } from "lucide-react";
type Status = "idle" | "loading" | "success" | "error";
export default function ContactForm() {
const [status, setStatus] = useState<Status>("idle");
const [errorMessage, setErrorMessage] = useState("");
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus("loading");
setErrorMessage("");
const form = e.currentTarget;
const data = new FormData(form);
// Honeypot: если поле заполнено — это бот, тихо считаем успехом.
if (data.get("website")) {
setStatus("success");
form.reset();
return;
}
const payload = {
name: String(data.get("name") || ""),
contact: String(data.get("contact") || ""),
message: String(data.get("message") || ""),
};
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const result = await res.json();
if (!res.ok || !result.ok) {
throw new Error(result.error || "Не удалось отправить сообщение");
}
setStatus("success");
form.reset();
} catch (err) {
setStatus("error");
setErrorMessage(
err instanceof Error ? err.message : "Не удалось отправить сообщение"
);
}
}
if (status === "success") {
return (
<div className="flex items-start gap-3 rounded-2xl border border-green-500/25 bg-green-500/[0.06] p-6">
<CheckCircle2 className="mt-0.5 shrink-0 text-green-400" size={22} />
<div>
<p className="font-medium text-ink-100">Сообщение отправлено</p>
<p className="mt-1 text-sm text-ink-500">
Мы получили заявку и ответим в ближайшее время.
</p>
</div>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-5">
{/* Honeypot — скрыто от людей, видно ботам */}
<input
type="text"
name="website"
tabIndex={-1}
autoComplete="off"
className="absolute left-[-9999px] h-0 w-0 opacity-0"
aria-hidden="true"
/>
<div>
<label htmlFor="name" className="mb-1.5 block text-sm text-ink-300">
Имя
</label>
<input
id="name"
name="name"
type="text"
required
maxLength={100}
placeholder="Как к вам обращаться"
className="w-full rounded-xl border border-white/[0.08] bg-base-900/60 px-4 py-3 text-ink-100 placeholder:text-ink-700 focus:border-blue-500/50"
/>
</div>
<div>
<label htmlFor="contact" className="mb-1.5 block text-sm text-ink-300">
Telegram, WhatsApp или email
</label>
<input
id="contact"
name="contact"
type="text"
required
maxLength={150}
placeholder="Как с вами связаться"
className="w-full rounded-xl border border-white/[0.08] bg-base-900/60 px-4 py-3 text-ink-100 placeholder:text-ink-700 focus:border-blue-500/50"
/>
</div>
<div>
<label htmlFor="message" className="mb-1.5 block text-sm text-ink-300">
Сообщение
</label>
<textarea
id="message"
name="message"
required
maxLength={2000}
rows={5}
placeholder="Расскажите о задаче — направление, сроки, пожелания"
className="w-full resize-none rounded-xl border border-white/[0.08] bg-base-900/60 px-4 py-3 text-ink-100 placeholder:text-ink-700 focus:border-blue-500/50"
/>
</div>
{status === "error" && (
<div className="flex items-start gap-2.5 rounded-xl border border-red-500/25 bg-red-500/[0.06] px-4 py-3 text-sm text-red-300">
<AlertCircle size={18} className="mt-0.5 shrink-0" />
{errorMessage}
</div>
)}
<button
type="submit"
disabled={status === "loading"}
className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-blue-500 px-6 py-3.5 font-medium text-white transition-transform hover:scale-[1.01] disabled:opacity-60 sm:w-auto"
>
{status === "loading" && <Loader2 size={18} className="animate-spin" />}
{status === "loading" ? "Отправляем…" : "Отправить сообщение"}
</button>
</form>
);
}