feat: contact sayfasına iletişim formu + POST /contact endpoint'i
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

- Yeni contact modülü: @Public POST /contact, zod validation, @Throttle 5/10dk
  spam koruması; EmailService ile admin@sase.tr'ye mail (reply-to = gönderen),
  kullanıcı girdileri HTML-escape
- EmailService: replyTo desteği eklendi
- contact.tsx: useState + zod + @sase/ui ile iletişim formu (mevcut form
  pattern'iyle tutarlı; yeni form kütüphanesi yok), toast + alan validasyonu

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 01:27:56 +03:00
parent 14a1af0f81
commit 57b3cb7ebe
7 changed files with 293 additions and 59 deletions

View File

@@ -1,13 +1,148 @@
import { usePageMeta } from "@/hooks/use-page-meta";
import { SiteHeader } from "@/components/site-header";
import { Button } from "@sase/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@sase/ui";
import { Link, createFileRoute } from "@tanstack/react-router";
import { usePageMeta } from "@/hooks/use-page-meta";
import { ApiError, api } from "@/lib/api-client";
import { toast } from "@/lib/toast";
import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label } from "@sase/ui";
import { createFileRoute } from "@tanstack/react-router";
import { Loader2 } from "lucide-react";
import { type FormEvent, useState } from "react";
import { z } from "zod";
export const Route = createFileRoute("/contact")({
component: ContactPage,
});
// Backend (POST /contact) ile aynı kurallar
const contactSchema = z.object({
name: z.string().trim().min(2, "Ad en az 2 karakter olmalı").max(100, "Ad çok uzun"),
email: z.string().trim().email("Geçerli bir e-posta adresi girin").max(200),
subject: z.string().trim().max(200, "Konu çok uzun").optional(),
message: z.string().trim().min(10, "Mesaj en az 10 karakter olmalı").max(5000, "Mesaj çok uzun"),
});
type FieldErrors = Partial<Record<"name" | "email" | "subject" | "message", string>>;
const textareaClass =
"flex min-h-[140px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
function ContactForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [subject, setSubject] = useState("");
const [message, setMessage] = useState("");
const [errors, setErrors] = useState<FieldErrors>({});
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const parsed = contactSchema.safeParse({ name, email, subject, message });
if (!parsed.success) {
const next: FieldErrors = {};
for (const issue of parsed.error.issues) {
const key = issue.path[0] as keyof FieldErrors;
if (key && !next[key]) next[key] = issue.message;
}
setErrors(next);
return;
}
setErrors({});
setLoading(true);
try {
await api.post("/contact", parsed.data);
toast.success("Mesajınız gönderildi", {
description: "En kısa sürede size dönüş yapacağız.",
});
setName("");
setEmail("");
setSubject("");
setMessage("");
} catch (err) {
const msg =
err instanceof ApiError ? err.message : "Mesaj gönderilemedi. Lütfen tekrar deneyin.";
toast.error(msg);
} finally {
setLoading(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Bize yazın</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="contact-name">Ad Soyad</Label>
<Input
id="contact-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Adınız"
aria-invalid={!!errors.name}
disabled={loading}
/>
{errors.name && <p className="text-sm text-destructive">{errors.name}</p>}
</div>
<div className="space-y-1.5">
<Label htmlFor="contact-email">E-posta</Label>
<Input
id="contact-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="ornek@eposta.com"
aria-invalid={!!errors.email}
disabled={loading}
/>
{errors.email && <p className="text-sm text-destructive">{errors.email}</p>}
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="contact-subject">Konu (opsiyonel)</Label>
<Input
id="contact-subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Mesajınızın konusu"
aria-invalid={!!errors.subject}
disabled={loading}
/>
{errors.subject && <p className="text-sm text-destructive">{errors.subject}</p>}
</div>
<div className="space-y-1.5">
<Label htmlFor="contact-message">Mesaj</Label>
<textarea
id="contact-message"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Bize nasıl yardımcı olabiliriz?"
aria-invalid={!!errors.message}
disabled={loading}
className={textareaClass}
/>
{errors.message && <p className="text-sm text-destructive">{errors.message}</p>}
</div>
<Button type="submit" disabled={loading} className="w-full sm:w-auto">
{loading ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
Gönderiliyor
</>
) : (
"Gönder"
)}
</Button>
</form>
</CardContent>
</Card>
);
}
function ContactPage() {
usePageMeta({
title: "İletişim — Sase.tr",
@@ -19,68 +154,76 @@ function ContactPage() {
<div className="min-h-screen">
<SiteHeader />
<main className="container mx-auto max-w-3xl px-4 py-16">
<main className="container mx-auto max-w-5xl px-4 py-16">
<h1 className="text-4xl font-bold">İletişim</h1>
<p className="mt-4 text-lg text-muted-foreground">
Sorularınız, önerileriniz veya birliği talepleriniz için bize ulaşın.
</p>
<div className="mt-12 grid gap-6 sm:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-lg">E-posta</CardTitle>
</CardHeader>
<CardContent>
<a href="mailto:info@sase.tr" className="text-primary underline">
info@sase.tr
</a>
<p className="mt-2 text-sm text-muted-foreground">
Genel sorular ve destek talepleri için.
</p>
</CardContent>
</Card>
<div className="mt-12 grid gap-6 lg:grid-cols-5">
{/* İletişim formu */}
<div className="lg:col-span-3">
<ContactForm />
</div>
<Card>
<CardHeader>
<CardTitle className="text-lg">Destek</CardTitle>
</CardHeader>
<CardContent>
<a href="mailto:destek@sase.tr" className="text-primary underline">
destek@sase.tr
</a>
<p className="mt-2 text-sm text-muted-foreground">
Teknik sorunlar ve hesap işlemleri için.
</p>
</CardContent>
</Card>
{/* İletişim bilgileri */}
<div className="space-y-6 lg:col-span-2">
<Card>
<CardHeader>
<CardTitle className="text-lg">E-posta</CardTitle>
</CardHeader>
<CardContent>
<a href="mailto:info@sase.tr" className="text-primary underline">
info@sase.tr
</a>
<p className="mt-2 text-sm text-muted-foreground">
Genel sorular ve destek talepleri için.
</p>
</CardContent>
</Card>
<Card className="sm:col-span-2">
<CardHeader>
<CardTitle className="text-lg">Adres</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-1 text-sm text-muted-foreground">
<p>
<span className="text-foreground">Firma Adı:</span> THINXTRA LLC
<Card>
<CardHeader>
<CardTitle className="text-lg">Destek</CardTitle>
</CardHeader>
<CardContent>
<a href="mailto:destek@sase.tr" className="text-primary underline">
destek@sase.tr
</a>
<p className="mt-2 text-sm text-muted-foreground">
Teknik sorunlar ve hesap işlemleri için.
</p>
<p>
<span className="text-foreground">Adres:</span> 1209 Mountain Road PL NE #11131
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Adres</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-1 text-sm text-muted-foreground">
<p>
<span className="text-foreground">Firma Adı:</span> THINXTRA LLC
</p>
<p>
<span className="text-foreground">Adres:</span> 1209 Mountain Road PL NE #11131
</p>
<p>
<span className="text-foreground">Şehir:</span> Albuquerque, NM 87110
</p>
<p>
<span className="text-foreground">Ülke:</span> United States
</p>
<p>
<span className="text-foreground">Vergi No:</span> 36-5177177
</p>
</div>
<p className="mt-3 text-sm text-muted-foreground">
Çalışma saatleri: Pazartesi Cuma, 09:00 18:00
</p>
<p>
<span className="text-foreground">Şehir:</span> Albuquerque, NM 87110
</p>
<p>
<span className="text-foreground">Ülke:</span> United States
</p>
<p>
<span className="text-foreground">Vergi No:</span> 36-5177177
</p>
</div>
<p className="mt-3 text-sm text-muted-foreground">
Çalışma saatleri: Pazartesi Cuma, 09:00 18:00
</p>
</CardContent>
</Card>
</CardContent>
</Card>
</div>
</div>
</main>
</div>