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
This commit is contained in:
commit
130c5382cd
108 files changed
+7146
No files matched your search
@@ -0,0 +1,97 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user