Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled
288 lines
9.2 KiB
Markdown
288 lines
9.2 KiB
Markdown
# GoodGo Frontend - Các Vấn Đề Trợ Năng - Yêu Cầu Sửa Code
|
|
|
|
**Ngày**: 2026-04-10
|
|
**Phạm vi**: apps/web (GoodGo Frontend)
|
|
**Trạng thái**: CÁC MỤC CÓ THỂ THỰC HIỆN - Sẵn sàng triển khai
|
|
|
|
---
|
|
|
|
## Tóm tắt
|
|
|
|
Tìm thấy **4 vấn đề trợ năng cụ thể** cần sửa code trên toàn bộ giao diện frontend GoodGo. Dưới đây là các đường dẫn file chính xác, số dòng, đoạn code có vấn đề và các sửa đổi cần thiết.
|
|
|
|
---
|
|
|
|
## VẤN ĐỀ 1: Các Input Form Thiếu aria-label hoặc Label Liên Kết
|
|
|
|
### 1.1 Input Tải File Không Có aria-label
|
|
**File**: `apps/web/components/listings/image-upload.tsx`
|
|
**Dòng**: 118
|
|
**Vấn đề**: Input file ẩn không có aria-label hoặc phần tử label liên kết
|
|
|
|
**Code hiện tại**:
|
|
```tsx
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept="image/jpeg,image/png,image/webp"
|
|
multiple
|
|
className="hidden"
|
|
onChange={(e) => {
|
|
if (e.target.files) addFiles(e.target.files);
|
|
e.target.value = '';
|
|
}}
|
|
/>
|
|
```
|
|
|
|
**Sửa đổi cần thiết**: Thêm `aria-label`
|
|
```tsx
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept="image/jpeg,image/png,image/webp"
|
|
multiple
|
|
className="hidden"
|
|
aria-label="Chọn ảnh để tải lên"
|
|
onChange={(e) => {
|
|
if (e.target.files) addFiles(e.target.files);
|
|
e.target.value = '';
|
|
}}
|
|
/>
|
|
```
|
|
|
|
---
|
|
|
|
### 1.2 Dialog Lưu Tìm Kiếm - Input Văn Bản Không Có aria-label
|
|
**File**: `apps/web/app/[locale]/(public)/search/page.tsx`
|
|
**Dòng**: 189
|
|
**Vấn đề**: Input văn bản để lưu tên tìm kiếm không có label liên kết hoặc aria-label
|
|
|
|
**Code hiện tại**:
|
|
```tsx
|
|
<input
|
|
type="text"
|
|
value={saveName}
|
|
onChange={(e) => setSaveName(e.target.value)}
|
|
placeholder="Tên tìm kiếm (VD: Chung cư Q7 dưới 3 tỷ)"
|
|
className="mb-3 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary"
|
|
maxLength={100}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSaveSearch()}
|
|
/>
|
|
```
|
|
|
|
**Sửa đổi cần thiết**: Thêm `aria-label`
|
|
```tsx
|
|
<input
|
|
type="text"
|
|
value={saveName}
|
|
onChange={(e) => setSaveName(e.target.value)}
|
|
placeholder="Tên tìm kiếm (VD: Chung cư Q7 dưới 3 tỷ)"
|
|
aria-label="Tên bộ lọc tìm kiếm"
|
|
className="mb-3 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary"
|
|
maxLength={100}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSaveSearch()}
|
|
/>
|
|
```
|
|
|
|
---
|
|
|
|
### 1.3 Quản Trị Kiểm Duyệt - Checkbox Chọn Tất Cả Không Có aria-label
|
|
**File**: `apps/web/app/[locale]/(admin)/admin/moderation/page.tsx`
|
|
**Dòng**: 222
|
|
**Vấn đề**: Checkbox trong tiêu đề bảng để "chọn tất cả" không có aria-label
|
|
|
|
**Code hiện tại**:
|
|
```tsx
|
|
<TableHead className="w-10">
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.size === result.data.length && result.data.length > 0}
|
|
onChange={toggleSelectAll}
|
|
className="rounded border-input"
|
|
/>
|
|
</TableHead>
|
|
```
|
|
|
|
**Sửa đổi cần thiết**: Thêm `aria-label`
|
|
```tsx
|
|
<TableHead className="w-10">
|
|
<input
|
|
type="checkbox"
|
|
aria-label="Chọn tất cả tin đăng"
|
|
checked={selected.size === result.data.length && result.data.length > 0}
|
|
onChange={toggleSelectAll}
|
|
className="rounded border-input"
|
|
/>
|
|
</TableHead>
|
|
```
|
|
|
|
---
|
|
|
|
### 1.4 Quản Trị Kiểm Duyệt - Checkbox Từng Dòng Không Có aria-label
|
|
**File**: `apps/web/app/[locale]/(admin)/admin/moderation/page.tsx`
|
|
**Dòng**: 242
|
|
**Vấn đề**: Checkbox từng dòng trong bảng không có aria-label
|
|
|
|
**Code hiện tại**:
|
|
```tsx
|
|
<TableCell>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.has(item.listingId)}
|
|
onChange={() => toggleSelect(item.listingId)}
|
|
className="rounded border-input"
|
|
/>
|
|
</TableCell>
|
|
```
|
|
|
|
**Sửa đổi cần thiết**: Thêm `aria-label` với nội dung động
|
|
```tsx
|
|
<TableCell>
|
|
<input
|
|
type="checkbox"
|
|
aria-label={`Chọn tin đăng: ${item.title || item.listingId}`}
|
|
checked={selected.has(item.listingId)}
|
|
onChange={() => toggleSelect(item.listingId)}
|
|
className="rounded border-input"
|
|
/>
|
|
</TableCell>
|
|
```
|
|
|
|
---
|
|
|
|
## VẤN ĐỀ 2: Component Ảnh Mock Thiếu Thuộc Tính alt
|
|
|
|
### 2.1 Component Ảnh Mock Trong Test
|
|
**File**: `apps/web/app/[locale]/(public)/search/__tests__/search.spec.tsx`
|
|
**Dòng**: 46
|
|
**Vấn đề**: Component Image mock trải tất cả props nhưng thiếu thuộc tính alt
|
|
|
|
**Code hiện tại**:
|
|
```tsx
|
|
default: (props: Record<string, unknown>) => <img {...props} />,
|
|
```
|
|
|
|
**Sửa đổi cần thiết**: Đảm bảo alt luôn có trong mock hoặc thêm giá trị mặc định
|
|
```tsx
|
|
default: (props: Record<string, unknown>) => <img {...props} alt={props.alt || ''} />,
|
|
```
|
|
|
|
HOẶC cách tiếp cận tốt hơn - yêu cầu alt trong cấu hình mock:
|
|
```tsx
|
|
default: (props: Record<string, unknown>) => {
|
|
if (!props.alt) {
|
|
console.warn('Missing alt attribute in Image mock:', props);
|
|
}
|
|
return <img {...props} alt={props.alt || 'image'} />;
|
|
},
|
|
```
|
|
|
|
---
|
|
|
|
## VẤN ĐỀ 3: Input File Ẩn Cần Trợ Năng Tốt Hơn
|
|
|
|
### 3.1 Khu Vực Kéo Thả Tải Ảnh Cần Gán Nhãn Tốt Hơn
|
|
**File**: `apps/web/components/listings/image-upload.tsx`
|
|
**Dòng**: 86-128
|
|
**Vấn đề**: Div có thể nhấp để kích hoạt input file có văn bản mô tả nhưng không có phần tử label liên kết đến input ẩn
|
|
|
|
**Cách triển khai hiện tại**:
|
|
```tsx
|
|
<div
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onClick={() => inputRef.current?.click()}
|
|
className={cn(...)}
|
|
>
|
|
<svg>...</svg>
|
|
<p className="text-sm font-medium">Kéo thả ảnh vào đây hoặc nhấp để chọn</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
JPG, PNG, WebP - Tối đa {maxFiles} ảnh, mỗi ảnh 10MB
|
|
</p>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept="image/jpeg,image/png,image/webp"
|
|
multiple
|
|
className="hidden"
|
|
onChange={(e) => {...}}
|
|
/>
|
|
</div>
|
|
```
|
|
|
|
**Cải tiến đề xuất**: Thêm label hoặc role phù hợp
|
|
```tsx
|
|
<div
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onClick={() => inputRef.current?.click()}
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-label="Khu vực kéo thả hoặc nhấp để tải ảnh lên"
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
inputRef.current?.click();
|
|
}
|
|
}}
|
|
className={cn(...)}
|
|
>
|
|
{/* ... rest of content ... */}
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## VẤN ĐỀ 4: Xác Minh Các Tính Năng Trợ Năng Đã Triển Khai Đúng
|
|
|
|
### ✅ ĐÚNG - Các Component Ảnh Có Thuộc Tính alt
|
|
Các file sau đã có thuộc tính alt đúng và KHÔNG cần thay đổi:
|
|
- `apps/web/components/listings/image-gallery.tsx` - Tất cả component Image có alt (dòng 34, 77)
|
|
- `apps/web/components/listings/image-upload.tsx` - Tất cả thẻ img có alt (dòng 135-138)
|
|
- `apps/web/components/search/property-card.tsx` - Image có alt (dòng 44)
|
|
- `apps/web/app/[locale]/(dashboard)/listings/page.tsx` - Tất cả Image có alt (dòng 192, 272)
|
|
- `apps/web/app/[locale]/(dashboard)/dashboard/page.tsx` - Image có alt (dòng 252)
|
|
- `apps/web/app/[locale]/(admin)/admin/kyc/page.tsx` - Tất cả Image có alt (dòng 102, 116, 130)
|
|
|
|
### ✅ ĐÚNG - Nút Chỉ Có Icon Với aria-label
|
|
Các file sau đã có aria-label đúng và KHÔNG cần thay đổi:
|
|
- `apps/web/components/listings/image-gallery.tsx` - Các nút điều hướng có aria-label (dòng 47, 54)
|
|
- `apps/web/app/[locale]/(public)/layout.tsx` - Nút menu di động có aria-label (dòng 91)
|
|
|
|
### ✅ ĐÚNG - Các Dialog Có Tiêu Đề Ngữ Nghĩa
|
|
Các dialog sau đã có phần tử DialogTitle đúng và KHÔNG cần thay đổi:
|
|
- `apps/web/app/[locale]/(dashboard)/dashboard/subscription/page.tsx` - DialogTitle có mặt (dòng 327-329)
|
|
- `apps/web/app/[locale]/(admin)/admin/kyc/page.tsx` - Cả hai dialog đều có DialogTitle (dialog duyệt và từ chối)
|
|
|
|
### ✅ ĐÚNG - Checkbox Có Label Liên Kết
|
|
- `apps/web/app/[locale]/(public)/search/page.tsx` (dòng 199) - Checkbox có phần tử `<label>` liên kết
|
|
|
|
---
|
|
|
|
## Thứ Tự Ưu Tiên Triển Khai
|
|
|
|
**Ưu tiên 1 (Tác động cao)**:
|
|
1. Thêm aria-label vào input file (image-upload.tsx:118)
|
|
2. Thêm aria-label vào input tên tìm kiếm (search/page.tsx:189)
|
|
|
|
**Ưu tiên 2 (Tác động cao)**:
|
|
3. Thêm aria-label vào checkbox bảng (moderation/page.tsx:222, 242)
|
|
4. Sửa component Image mock để yêu cầu alt (search.spec.tsx:46)
|
|
|
|
**Ưu tiên 3 (Cải tiến)**:
|
|
5. Cải thiện trợ năng khu vực kéo thả tải ảnh với role và hỗ trợ bàn phím (image-upload.tsx:86-128)
|
|
|
|
---
|
|
|
|
## Danh Sách Kiểm Tra Sau Khi Triển Khai
|
|
|
|
Sau khi triển khai các sửa đổi, hãy xác minh:
|
|
- [ ] Trình đọc màn hình thông báo đúng tất cả các input form
|
|
- [ ] Input file có aria-label có ý nghĩa khi được focus
|
|
- [ ] Các input trong dialog tìm kiếm có thể truy cập qua bàn phím
|
|
- [ ] Checkbox bảng có nhãn mô tả cho từng dòng
|
|
- [ ] Không có cảnh báo console về thiếu thuộc tính alt trong các test
|
|
- [ ] Điều hướng bàn phím hoạt động với tất cả các phần tử tương tác
|
|
- [ ] Tuân thủ WCAG 2.1 Level AA được xác minh bằng các công cụ tự động
|