Multi-page public site (Next.js), Node/Express backend with Postgres, and Vite admin/account app, restructured from a single-page layout into dedicated service, promotions, cases, about, FAQ, and blog pages for SEO. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
98 lines
3.6 KiB
React
98 lines
3.6 KiB
React
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Star } from 'lucide-react';
|
|
import { clientApi } from '../../lib/clientApi.js';
|
|
|
|
function cx(...args) {
|
|
return args.filter(Boolean).join(' ');
|
|
}
|
|
|
|
function StarRating({ value, onChange }) {
|
|
return (
|
|
<div className="flex gap-1">
|
|
{[1, 2, 3, 4, 5].map((n) => (
|
|
<button key={n} type="button" onClick={() => onChange(n)} aria-label={`${n} звёзд`}>
|
|
<Star className={cx('h-5 w-5', n <= value ? 'fill-amber-400 text-amber-400' : 'text-slate-400')} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ReviewForm() {
|
|
const [form, setForm] = useState({ name: '', role: '', rating: 5, text: '', website: '' });
|
|
const [status, setStatus] = useState('idle'); // idle | sending | sent | error
|
|
|
|
async function handleSubmit(e) {
|
|
e.preventDefault();
|
|
if (!form.name.trim() || !form.text.trim()) return;
|
|
setStatus('sending');
|
|
try {
|
|
await clientApi.submitReview(form);
|
|
setForm({ name: '', role: '', rating: 5, text: '', website: '' });
|
|
setStatus('sent');
|
|
} catch (err) {
|
|
setStatus('error');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="rounded-lg border p-5 space-y-4 bg-white border-slate-200 dark:bg-slate-900 dark:border-slate-800">
|
|
<h3 className="font-bold font-display text-[15px]">Оставить отзыв</h3>
|
|
|
|
{/* honeypot field - hidden from real users via CSS, bots often fill every input */}
|
|
<input
|
|
type="text"
|
|
value={form.website}
|
|
onChange={(e) => setForm({ ...form, website: e.target.value })}
|
|
className="hidden"
|
|
tabIndex={-1}
|
|
autoComplete="off"
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
<div>
|
|
<label className="text-xs block mb-1">Имя*</label>
|
|
<input
|
|
required
|
|
value={form.name}
|
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
|
className="w-full rounded-md border px-3 py-2 text-sm bg-transparent border-slate-300 dark:border-slate-700"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs block mb-1">Должность / компания</label>
|
|
<input
|
|
value={form.role}
|
|
onChange={(e) => setForm({ ...form, role: e.target.value })}
|
|
className="w-full rounded-md border px-3 py-2 text-sm bg-transparent border-slate-300 dark:border-slate-700"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs block mb-1">Оценка*</label>
|
|
<StarRating value={form.rating} onChange={(n) => setForm({ ...form, rating: n })} />
|
|
</div>
|
|
<div>
|
|
<label className="text-xs block mb-1">Отзыв*</label>
|
|
<textarea
|
|
required
|
|
rows={3}
|
|
value={form.text}
|
|
onChange={(e) => setForm({ ...form, text: e.target.value })}
|
|
className="w-full rounded-md border px-3 py-2 text-sm bg-transparent border-slate-300 dark:border-slate-700"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={status === 'sending'}
|
|
className="w-full py-2.5 rounded-md bg-emerald-500 text-white text-sm font-medium hover:bg-emerald-600 transition-colors disabled:opacity-60"
|
|
>
|
|
{status === 'sending' ? 'Отправляем...' : 'Отправить на модерацию'}
|
|
</button>
|
|
{status === 'sent' && <p className="text-xs text-emerald-500">Спасибо! Отзыв появится на сайте после одобрения.</p>}
|
|
{status === 'error' && <p className="text-xs text-rose-500">Не получилось отправить отзыв, попробуйте ещё раз чуть позже.</p>}
|
|
</form>
|
|
);
|
|
}
|