fix: standardize admin form inputs, validation, and sync with backend constraints

- Unified all inputs to use .input CSS class, consistent label styles, and btn-primary/btn-secondary buttons
- Fixed number inputs to use number | '' pattern allowing field clearing without default replacement
- Added field-level validation with inline error borders and messages
- Synced frontend constraints with backend Pydantic schemas (maxLength, min/max values)
- Fixed toggle switch dimensions across all admin forms
This commit is contained in:
c0mrade
2026-02-06 19:37:07 +03:00
parent 88d9377adb
commit 6e7eb36f76
13 changed files with 364 additions and 227 deletions

View File

@@ -44,8 +44,8 @@ const RefreshIcon = () => (
);
interface PeriodDiscount {
days: number;
percent: number;
days: number | '';
percent: number | '';
}
export default function AdminPromoGroupCreate() {
@@ -121,18 +121,20 @@ export default function AdminPromoGroupCreate() {
setPeriodDiscounts(periodDiscounts.filter((_, i) => i !== index));
};
const updatePeriodDiscount = (index: number, field: 'days' | 'percent', value: number) => {
const updated = [...periodDiscounts];
updated[index][field] = value;
setPeriodDiscounts(updated);
const updatePeriodDiscount = (index: number, field: 'days' | 'percent', value: number | '') => {
setPeriodDiscounts(
periodDiscounts.map((item, i) => (i === index ? { ...item, [field]: value } : item)),
);
};
const handleSubmit = () => {
// Convert periodDiscounts array to Record<number, number>
const periodDiscountsRecord: Record<number, number> = {};
periodDiscounts.forEach((pd) => {
if (pd.days > 0 && pd.percent > 0) {
periodDiscountsRecord[pd.days] = pd.percent;
const days = pd.days === '' ? 0 : pd.days;
const percent = pd.percent === '' ? 0 : pd.percent;
if (days > 0 && percent > 0) {
periodDiscountsRecord[days] = percent;
}
});
@@ -196,9 +198,14 @@ export default function AdminPromoGroupCreate() {
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="input"
className={`input ${name.length > 0 && name.trim().length === 0 ? 'border-error-500/50' : ''}`}
placeholder={t('admin.promoGroups.form.namePlaceholder')}
/>
{name.length > 0 && name.trim().length === 0 && (
<p className="mt-1 text-xs text-error-400">
{t('admin.promoGroups.form.nameRequired')}
</p>
)}
</div>
{/* Category Discounts */}
@@ -299,13 +306,12 @@ export default function AdminPromoGroupCreate() {
<input
type="number"
value={pd.days}
onChange={(e) =>
updatePeriodDiscount(
index,
'days',
Math.max(1, parseInt(e.target.value) || 1),
)
}
onChange={(e) => {
const val = e.target.value;
if (val === '') return updatePeriodDiscount(index, 'days', '');
const num = parseInt(val);
if (!isNaN(num)) updatePeriodDiscount(index, 'days', num);
}}
className="input w-20"
min={1}
placeholder={t('admin.promoGroups.form.daysPlaceholder')}
@@ -314,13 +320,12 @@ export default function AdminPromoGroupCreate() {
<input
type="number"
value={pd.percent}
onChange={(e) =>
updatePeriodDiscount(
index,
'percent',
Math.min(100, Math.max(0, parseInt(e.target.value) || 0)),
)
}
onChange={(e) => {
const val = e.target.value;
if (val === '') return updatePeriodDiscount(index, 'percent', '');
const num = parseInt(val);
if (!isNaN(num)) updatePeriodDiscount(index, 'percent', num);
}}
className="input w-20"
min={0}
max={100}
@@ -371,13 +376,13 @@ export default function AdminPromoGroupCreate() {
<button
type="button"
onClick={() => setApplyToAddons(!applyToAddons)}
className={`relative h-6 w-10 rounded-full transition-colors ${
className={`relative h-6 w-11 rounded-full transition-colors ${
applyToAddons ? 'bg-accent-500' : 'bg-dark-600'
}`}
>
<span
className={`absolute top-1 h-4 w-4 rounded-full bg-white transition-transform ${
applyToAddons ? 'left-5' : 'left-1'
applyToAddons ? 'left-6' : 'left-1'
}`}
/>
</button>
@@ -388,10 +393,7 @@ export default function AdminPromoGroupCreate() {
{/* Footer */}
<div className="card">
<div className="flex justify-end gap-3">
<button
onClick={() => navigate('/admin/promo-groups')}
className="px-4 py-2 text-dark-300 transition-colors hover:text-dark-100"
>
<button onClick={() => navigate('/admin/promo-groups')} className="btn-secondary">
{t('admin.promoGroups.form.cancel')}
</button>
<button