feat(web): add auth+search i18n translations and filter-bar accessibility

Add missing auth and search translation namespaces to vi.json and en.json
that are required by login/register pages and search filter-bar component.
Update filter-bar with useTranslations('search'), aria-labels, and
role="search" for WCAG 2.1 AA compliance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-04-09 10:22:59 +07:00
parent 8179f1c16e
commit 862078df37
21 changed files with 213 additions and 83 deletions

View File

@@ -1,5 +1,6 @@
'use client';
import { useTranslations } from 'next-intl';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -32,13 +33,15 @@ const CITIES = [
'Bà Rịa - Vũng Tàu',
];
const PRICE_RANGES = [
{ label: 'Dưới 1 tỷ', min: '0', max: '1000000000' },
{ label: '1 - 3 tỷ', min: '1000000000', max: '3000000000' },
{ label: '3 - 5 tỷ', min: '3000000000', max: '5000000000' },
{ label: '5 - 10 tỷ', min: '5000000000', max: '10000000000' },
{ label: '10 - 20 tỷ', min: '10000000000', max: '20000000000' },
{ label: 'Trên 20 tỷ', min: '20000000000', max: '' },
const PRICE_RANGE_KEYS = ['under1b', '1to3b', '3to5b', '5to10b', '10to20b', 'over20b'] as const;
const PRICE_RANGE_VALUES = [
{ min: '0', max: '1000000000' },
{ min: '1000000000', max: '3000000000' },
{ min: '3000000000', max: '5000000000' },
{ min: '5000000000', max: '10000000000' },
{ min: '10000000000', max: '20000000000' },
{ min: '20000000000', max: '' },
];
interface FilterBarProps {
@@ -49,6 +52,8 @@ interface FilterBarProps {
}
export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }: FilterBarProps) {
const t = useTranslations('search');
const update = (key: keyof SearchFilters, value: string) => {
onChange({ ...filters, [key]: value });
};
@@ -58,32 +63,33 @@ export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }
onChange({ ...filters, minPrice: '', maxPrice: '' });
return;
}
const range = PRICE_RANGES[Number(value)];
const range = PRICE_RANGE_VALUES[Number(value)];
if (range) {
onChange({ ...filters, minPrice: range.min, maxPrice: range.max });
}
};
const currentPriceIdx = PRICE_RANGES.findIndex(
const currentPriceIdx = PRICE_RANGE_VALUES.findIndex(
(r) => r.min === filters.minPrice && r.max === filters.maxPrice,
);
const isSidebar = layout === 'sidebar';
return (
<div className={isSidebar ? 'space-y-4' : 'space-y-3'}>
{isSidebar && <h3 className="font-semibold">Bộ lọc</h3>}
<div className={isSidebar ? 'space-y-4' : 'space-y-3'} role="search" aria-label={t('filters')}>
{isSidebar && <h3 className="font-semibold">{t('filters')}</h3>}
<div className={isSidebar ? 'space-y-3' : 'flex flex-wrap gap-3'}>
<Select
value={filters.transactionType}
onChange={(e) => update('transactionType', e.target.value)}
className={isSidebar ? 'w-full' : 'w-40'}
aria-label={t('allTransactions')}
>
<option value="">Tất cả giao dịch</option>
{TRANSACTION_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
<option value="">{t('allTransactions')}</option>
{TRANSACTION_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</Select>
@@ -92,11 +98,12 @@ export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }
value={filters.propertyType}
onChange={(e) => update('propertyType', e.target.value)}
className={isSidebar ? 'w-full' : 'w-44'}
aria-label={t('allPropertyTypes')}
>
<option value="">Tất cả loại BĐS</option>
{PROPERTY_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
<option value="">{t('allPropertyTypes')}</option>
{PROPERTY_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</Select>
@@ -105,8 +112,9 @@ export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }
value={filters.city}
onChange={(e) => update('city', e.target.value)}
className={isSidebar ? 'w-full' : 'w-44'}
aria-label={t('allAreas')}
>
<option value="">Tất cả khu vực</option>
<option value="">{t('allAreas')}</option>
{CITIES.map((c) => (
<option key={c} value={c}>
{c}
@@ -118,11 +126,12 @@ export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }
value={currentPriceIdx >= 0 ? String(currentPriceIdx) : ''}
onChange={(e) => handlePriceRange(e.target.value)}
className={isSidebar ? 'w-full' : 'w-40'}
aria-label={t('allPrices')}
>
<option value="">Tất cả mức giá</option>
{PRICE_RANGES.map((r, i) => (
<option key={i} value={String(i)}>
{r.label}
<option value="">{t('allPrices')}</option>
{PRICE_RANGE_KEYS.map((key, i) => (
<option key={key} value={String(i)}>
{t(`priceRanges.${key}`)}
</option>
))}
</Select>
@@ -130,21 +139,23 @@ export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }
{isSidebar && (
<>
<div>
<label className="mb-1 block text-sm text-muted-foreground">Diện tích (m²)</label>
<label className="mb-1 block text-sm text-muted-foreground">{t('areaLabel')}</label>
<div className="flex gap-2">
<Input
type="number"
placeholder="Từ"
placeholder={t('areaFrom')}
value={filters.minArea}
onChange={(e) => update('minArea', e.target.value)}
className="w-full"
aria-label={`${t('areaLabel')} ${t('areaFrom')}`}
/>
<Input
type="number"
placeholder="Đến"
placeholder={t('areaTo')}
value={filters.maxArea}
onChange={(e) => update('maxArea', e.target.value)}
className="w-full"
aria-label={`${t('areaLabel')} ${t('areaTo')}`}
/>
</div>
</div>
@@ -153,20 +164,22 @@ export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }
value={filters.bedrooms}
onChange={(e) => update('bedrooms', e.target.value)}
className="w-full"
aria-label={t('bedrooms')}
>
<option value="">Số phòng ngủ</option>
<option value="">{t('bedrooms')}</option>
{[1, 2, 3, 4, 5].map((n) => (
<option key={n} value={String(n)}>
{n}+ PN
{t('bedroomsCount', { count: n })}
</option>
))}
</Select>
<Input
placeholder="Quận/huyện"
placeholder={t('district')}
value={filters.district}
onChange={(e) => update('district', e.target.value)}
className="w-full"
aria-label={t('district')}
/>
</>
)}
@@ -176,11 +189,12 @@ export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }
value={filters.bedrooms}
onChange={(e) => update('bedrooms', e.target.value)}
className="w-36"
aria-label={t('bedrooms')}
>
<option value="">Phòng ngủ</option>
<option value="">{t('bedrooms')}</option>
{[1, 2, 3, 4, 5].map((n) => (
<option key={n} value={String(n)}>
{n}+ PN
{t('bedroomsCount', { count: n })}
</option>
))}
</Select>
@@ -189,7 +203,7 @@ export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }
{isSidebar && (
<Button onClick={onSearch} className="w-full">
Tìm kiếm
{t('searchButton')}
</Button>
)}
</div>