Files
topsysops-app/public-site/components/sections/ReviewForm.jsx
T
ogrechkoandClaude Sonnet 5 130c5382cd Initial commit: TopSysOps site + admin panel
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
2026-08-25 23:12:16 +03:00

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>
);
}