diff --git a/AUDIT_SUMMARY.txt b/AUDIT_SUMMARY.txt deleted file mode 100644 index a17a7e8..0000000 --- a/AUDIT_SUMMARY.txt +++ /dev/null @@ -1,185 +0,0 @@ -╔════════════════════════════════════════════════════════════════════════════════╗ -║ GoodGo Platform - Code Quality Audit Summary ║ -║ Audit Date: April 9, 2026 ║ -║ Depth: VERY THOROUGH ║ -╚════════════════════════════════════════════════════════════════════════════════╝ - -┌─ CODEBASE METRICS ─────────────────────────────────────────────────────────┐ -│ │ -│ Total Files Analyzed: 13 modules + shared infrastructure │ -│ TypeScript Lines (API): ~25,700 lines │ -│ Configuration Files: 3 (tsconfig.base.json, eslint.config.mjs,│ -│ .dependency-cruiser.cjs) │ -│ Modules: 13 (auth, payments, listings, subscriptions, -│ admin, search, analytics, notifications, -│ reviews, health, mcp, metrics) │ -└──────────────────────────────────────────────────────────────────────────────┘ - -┌─ ISSUE SEVERITY BREAKDOWN ──────────────────────────────────────────────────┐ -│ │ -│ 🔴 CRITICAL: 3 issues (Domain errors, API versioning, imports) │ -│ 🟠 HIGH: 3 issues (Env validation, events, logging) │ -│ 🟡 MEDIUM: 5 issues (Duplication, files, validators, N+1, rules)│ -│ 🟢 LOW: 4 issues (Module exports, caching, test logger) │ -│ │ -│ Total Issues: 15 findings with actionable remediation │ -└──────────────────────────────────────────────────────────────────────────────┘ - -┌─ AREA SCORES ───────────────────────────────────────────────────────────────┐ -│ │ -│ 1. Error Handling ██████░░░░ 70% (Good pattern, bad usage) -│ 2. Import Order & Aliases ███████░░░ 75% (Config good, usage bad) -│ 3. TypeScript Strictness █████████░ 90% (Excellent settings) -│ 4. Code Duplication ██████░░░░ 65% (Logger, Prisma, pagination) -│ 5. Dependency Injection ████████░░ 85% (Well-structured modules) -│ 6. Event Handling ██████░░░░ 70% (Listeners good, publishing bad) -│ 7. Validation ████████░░ 80% (DTOs good, custom validators missing) -│ 8. Logging ███████░░░ 75% (Service good, injection inconsistent) -│ 9. API Versioning ░░░░░░░░░░ 0% (MISSING - Critical) -│ 10. File Size Violations ███████░░░ 70% (3 critical, 6 acceptable files) -│ 11. ESLint Configuration ████████░░ 85% (Good, missing advanced rules) -│ 12. Performance Patterns ███████░░░ 75% (Pagination good, N+1 risks exist) -│ │ -│ 📊 OVERALL SCORE: ██████████ 74% (Good baseline, significant room for improvement) -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ - -┌─ CRITICAL FINDINGS (MUST ADDRESS IMMEDIATELY) ─────────────────────────────┐ -│ │ -│ ❌ NO API VERSIONING │ -│ • All routes lack /api/v1/ prefix │ -│ • Breaking change risk for future versions │ -│ → FIX: Add app.setGlobalPrefix('api/v1') in main.ts │ -│ │ -│ ❌ DOMAIN ENTITIES THROWING PLAIN Error (NOT DomainException) │ -│ • payments/domain/entities/payment.entity.ts (Lines 94, 107, 134) │ -│ • subscriptions/domain/entities/subscription.entity.ts (Lines 75, 90) │ -│ → FIX: Use Result pattern or throw DomainException │ -│ │ -│ ❌ CROSS-MODULE INTERNAL IMPORTS (158 violations) │ -│ • @modules/auth/infrastructure imported directly │ -│ • @modules/shared/infrastructure imported directly │ -│ → FIX: Update barrel exports and use @modules/* imports │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ - -┌─ STRENGTHS (KEEP & MAINTAIN) ──────────────────────────────────────────────┐ -│ │ -│ ✅ Strong TypeScript Configuration │ -│ • strict: true, noUncheckedIndexedAccess, noImplicitOverride enabled │ -│ • Advanced type checking flags properly set │ -│ │ -│ ✅ Global Exception Filter Pattern │ -│ • Centralized error handling at boundary │ -│ • Proper HTTP status mapping and logging │ -│ │ -│ ✅ NestJS Dependency Injection │ -│ • Module structure well-organized │ -│ • CQRS pattern properly integrated │ -│ • Provider registration clear and consistent │ -│ │ -│ ✅ Result Functional Pattern │ -│ • Good support for domain-level error handling │ -│ • Well-implemented with map, andThen, match operations │ -│ │ -│ ✅ Event Listener Pattern │ -│ • @OnEvent decorators properly used │ -│ • Async event handling implemented │ -│ │ -│ ✅ Pagination & Query Optimization │ -│ • Repositories use select/include correctly │ -│ • Promise.all for parallel queries (no sequential N+1) │ -│ │ -│ ✅ Validation with class-validator │ -│ • Comprehensive DTO decorators │ -│ • Global validation pipe configured properly │ -│ │ -│ ✅ Custom Logger Service │ -│ • Pino-based with PII masking │ -│ • Environment-aware configuration │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ - -┌─ HIGH PRIORITY ISSUES (NEXT SPRINT) ────────────────────────────────────────┐ -│ │ -│ 1. Environment Variables Validation (HIGH) │ -│ • Services throw Error during instantiation │ -│ • Files: vnpay.service.ts, momo.service.ts, zalopay.service.ts │ -│ • Should validate at module bootstrap, not runtime │ -│ │ -│ 2. Event Publishing Not Implemented (HIGH) │ -│ • Domain events defined but not published by entities │ -│ • Event sourcing pattern incomplete │ -│ • Only 10 event listeners for entire platform (should have 20+) │ -│ │ -│ 3. Logger Injection Inconsistency (HIGH) │ -│ • 50+ files use: private readonly logger = new Logger(Class.name) │ -│ • Should inject LoggerService instead │ -│ • Prevents PII masking and centralized configuration │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ - -┌─ FILES EXCEEDING 200-LINE CONVENTION ──────────────────────────────────────┐ -│ │ -│ ⚠️ CRITICAL VIOLATIONS (>250 lines): │ -│ • admin/infrastructure/repositories/prisma-admin-query.repository.ts │ -│ → 313 lines (Multiple query methods, should split by domain) │ -│ • admin/presentation/controllers/admin.controller.ts │ -│ → 289 lines (All admin endpoints, should split by resource type) │ -│ • listings/infrastructure/repositories/prisma-listing.repository.ts │ -│ → 274 lines (Should split read/write operations) │ -│ │ -│ ⚠️ ACCEPTABLE VIOLATIONS (200-250 lines): │ -│ • analytics/infrastructure/__tests__/... (254 lines - test file) │ -│ • listings/domain/__tests__/... (234 lines - test file) │ -│ • listings/presentation/controllers/... (213 lines - monitor) │ -│ • payments/infrastructure/services/zalopay.service.ts (211 lines) │ -│ • payments/infrastructure/services/momo.service.ts (209 lines) │ -│ • auth/presentation/controllers/auth.controller.ts (200 lines - limit) │ -│ │ -│ 📊 Total: 9 files >200 lines (3 critical, 6 acceptable) │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ - -┌─ QUICK WINS (1-2 DAYS) ────────────────────────────────────────────────────┐ -│ │ -│ • Add app.setGlobalPrefix('api/v1') to main.ts (2 min) │ -│ • Export TokenService in auth/index.ts (1 min) │ -│ • Export CacheService in shared/index.ts (1 min) │ -│ • Add no-restricted-imports ESLint rule (10 min) │ -│ • Create @IsVietnamPhone() custom validator (30 min) │ -│ │ -│ 📈 Estimated Impact: +15-20% code quality score │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ - -┌─ NEXT STEPS ───────────────────────────────────────────────────────────────┐ -│ │ -│ PHASE 1 (IMMEDIATE - Critical Issues) │ -│ ├─ Fix API versioning (1 hour) │ -│ ├─ Add import restriction ESLint rule (2 hours) │ -│ └─ Fix domain entity error handling (4 hours) │ -│ │ -│ PHASE 2 (THIS WEEK - High Priority) │ -│ ├─ Implement event publishing in entities (4 hours) │ -│ ├─ Standardize logger injection (6 hours) │ -│ ├─ Move env validation to factories (2 hours) │ -│ └─ Create base classes for DI consistency (3 hours) │ -│ │ -│ PHASE 3 (NEXT WEEK - Medium Priority) │ -│ ├─ Split oversized files (admin repo, controller) (8 hours) │ -│ ├─ Add custom validators (2 hours) │ -│ ├─ Implement caching strategy (6 hours) │ -│ └─ Add domain event listeners (4 hours) │ -│ │ -│ PHASE 4 (LONG TERM - Polish) │ -│ ├─ Extended ESLint rules (cognitive complexity, decorator rules) │ -│ ├─ Performance profiling (N+1 query optimization) │ -│ └─ Test coverage improvements │ -│ │ -│ 📋 Total Estimated Effort: ~40 hours for full remediation │ -│ │ -└──────────────────────────────────────────────────────────────────────────────┘ - -📄 Full detailed report saved to: CODE_QUALITY_AUDIT.md - diff --git a/K6_LOAD_TESTING_GUIDE.md b/K6_LOAD_TESTING_GUIDE.md index 1a1cff9..b83cc03 100644 --- a/K6_LOAD_TESTING_GUIDE.md +++ b/K6_LOAD_TESTING_GUIDE.md @@ -773,7 +773,7 @@ playwright.config.ts # Playwright config - **API Swagger Docs**: `http://localhost:3001/api/v1/docs` - **Project Root Docs**: `CLAUDE.md` - **Existing Analysis**: `CODEBASE_ANALYSIS.md`, `EXPLORATION_REPORT.md` -- **Frontend Docs**: `FRONTEND_EXPLORATION.md` +- **Frontend Docs**: `docs/audits/FRONTEND_EXPLORATION.md` --- diff --git a/README_FRONTEND_DOCS.md b/README_FRONTEND_DOCS.md index 2c37861..f7ff3d7 100644 --- a/README_FRONTEND_DOCS.md +++ b/README_FRONTEND_DOCS.md @@ -21,7 +21,7 @@ High-level summary of findings: --- -#### 2. **FRONTEND_EXPLORATION.md** 📋 DETAILED REFERENCE +#### 2. **docs/audits/FRONTEND_EXPLORATION.md** 📋 DETAILED REFERENCE **45-minute read | Comprehensive analysis** Extremely thorough breakdown: @@ -102,7 +102,7 @@ Organized by file complexity: ### Scenario 3: I'm a Developer Implementing i18n 1. Quickly scan **EXPLORATION_SUMMARY.txt** (5 min) -2. Deep dive **FRONTEND_EXPLORATION.md** sections relevant to your task +2. Deep dive **docs/audits/FRONTEND_EXPLORATION.md** sections relevant to your task 3. Use **FILE_MAPPING_GUIDE.md** as step-by-step instructions 4. Reference code examples and pseudo-code provided @@ -242,7 +242,7 @@ The docs reference: If you're new to this codebase: 1. Start with **EXPLORATION_SUMMARY.txt** for overview -2. Read **FRONTEND_EXPLORATION.md** section "Directory Structure Overview" +2. Read **docs/audits/FRONTEND_EXPLORATION.md** section "Directory Structure Overview" 3. Understand the App Router structure 4. Review current component patterns 5. Then start implementation with **FILE_MAPPING_GUIDE.md** diff --git a/docs/audits/ACCESSIBILITY_AUDIT_2026-04-10.md b/docs/audits/ACCESSIBILITY_AUDIT_2026-04-10.md new file mode 100644 index 0000000..d5d6896 --- /dev/null +++ b/docs/audits/ACCESSIBILITY_AUDIT_2026-04-10.md @@ -0,0 +1,1552 @@ +# GoodGo Platform Frontend - Comprehensive Accessibility Audit Report +**Date:** April 10, 2026 +**Audited:** apps/web (Next.js 14) +**Total Files Analyzed:** 90+ TSX/JSX files +**ARIA Attributes Found:** 75 instances across 14 files + +--- + +## Executive Summary + +The GoodGo Platform frontend demonstrates **good foundational accessibility practices** with a skip-to-content link, proper semantic HTML roles, ARIA labels on interactive elements, and form validation. However, there are several areas requiring improvement: + +**Key Findings:** +- ✅ Skip-to-content link implemented +- ✅ 75 ARIA attributes across codebase +- ✅ Dialog component with focus management +- ✅ Error handling with proper ARIA attributes +- ⚠️ Icon-only buttons missing aria-labels in some areas +- ⚠️ Thumbnail buttons in image gallery lack accessible names +- ⚠️ Dialog component missing accessibility features (role, focus trap) +- ⚠️ No color contrast definitions documented +- ❌ Some form inputs missing proper label associations + +--- + +## 1. CURRENT ARIA USAGE ANALYSIS + +### Summary +- **Total ARIA Attributes:** 75 instances +- **Files Using ARIA:** 14 files +- **Most Common:** aria-label (41 instances), aria-hidden (17 instances) + +### Detailed ARIA Breakdown by File + +#### Public Layout (`apps/web/app/[locale]/(public)/layout.tsx`) +**Line 49:** `aria-label={t('nav.mainNav')}` +- Location: Desktop navigation element +- Type: Semantic navigation landmark + +**Line 91:** `aria-label={mobileMenuOpen ? t('nav.closeMenu') : t('nav.openMenu')}` +- Location: Mobile hamburger button +- Type: Dynamically updated label based on state +- Status: ✅ Properly implemented + +#### Root Layout (`apps/web/app/[locale]/layout.tsx`) +**Lines 105-109:** Skip-to-content link +```tsx + + {t('skipToContent')} + +``` +- Status: ✅ Properly implemented with focus visibility +- Hidden by default with -translate-y-16, visible on focus +- Links to `id="main-content"` on main element + +#### Dashboard Layout (`apps/web/app/[locale]/(dashboard)/layout.tsx`) +**Line 47:** `aria-label={t('nav.dashboardNav')}` +- Location: Mobile sidebar navigation +- Type: Navigation landmark + +**Line 58:** `aria-label={t('nav.closeMenu')}` +- Location: Mobile sidebar close button +- Type: Icon-only button with label + +**Line 79:** `aria-hidden="true"` +- Location: Emoji icon in navigation items +- Type: Decorative content hiding + +**Line 95:** `aria-hidden="true"` +- Location: LogOut icon +- Type: Decorative icon hiding + +**Line 108:** `aria-label={t('nav.openMenu')}` +- Location: Mobile hamburger button +- Type: Icon-only button + +**Line 120:** `aria-label={t('nav.dashboardNav')}` +- Location: Desktop navigation +- Type: Navigation landmark + +**Line 125:** `aria-label={item.label}` +- Location: Navigation items +- Type: Redundant label (text already visible) +- Status: ⚠️ Not needed when text is visible + +**Line 133:** `aria-hidden="true"` +- Location: Icon in navigation items +- Type: Decorative content + +**Line 150:** `aria-label={theme === 'light' ? t('dashboard.darkMode') : t('dashboard.lightMode')}` +- Location: Theme toggle button +- Type: Dynamic aria-label ✅ Good + +**Lines 154, 158:** `aria-hidden="true"` on SVGs +- Location: Theme toggle icons +- Type: Decorative content hiding + +#### Admin Layout (`apps/web/app/[locale]/(admin)/layout.tsx`) +**Line 60:** `aria-hidden="true"` +- Location: Mobile overlay backdrop + +**Line 67:** `aria-label={t('nav.adminNav')}` +- Location: Sidebar navigation + +**Line 81:** `aria-label={t('adminNav.closeMenu')}` +- Location: Close button + +**Line 89:** `aria-label={t('nav.adminNav')}` +- Location: Navigation container (duplicate) + +**Line 108:** `aria-hidden="true"` +- Location: Navigation icon + +**Line 126:** `aria-hidden="true"` +- Location: LogOut icon + +**Line 135:** `aria-label={t('adminNav.openMenu')}` +- Location: Mobile hamburger button + +#### Language Switcher (`apps/web/components/ui/language-switcher.tsx`) +**Line 29:** `aria-label={`${t('label')}: ${t(locale)} → ${t(nextLocale)}`}` +- Location: Language toggle button +- Status: ✅ Descriptive label indicating action + +**Line 31:** `aria-hidden="true"` +- Location: Emoji display +- Type: Decorative content + +#### Search/Filter Components (`apps/web/components/search/filter-bar.tsx`) +**Line 79:** `role="search" aria-label={t('filters')}` +- Location: Filter section +- Status: ✅ Proper semantic role with label + +**Lines 87, 101, 115, 129:** `aria-label` on select inputs +- Location: Filter dropdowns +- Pattern: `aria-label={t('allTransactions')}`, `aria-label={t('allPropertyTypes')}`, etc. +- Status: ✅ Accessible form controls + +**Lines 150, 158, 167, 182, 192:** `aria-label` on range inputs +- Location: Area and bedroom filters +- Status: ✅ Clear labels for input ranges + +#### Image Gallery (`apps/web/components/listings/image-gallery.tsx`) +**Line 47:** `aria-label="Ảnh trước"` (Previous image) +- Location: Previous button +- Status: ✅ Icon-only button with aria-label + +**Line 54:** `aria-label="Ảnh tiếp"` (Next image) +- Location: Next button +- Status: ✅ Icon-only button with aria-label + +#### Property Card (`apps/web/components/search/property-card.tsx`) +**Line 36:** `aria-label={`${listing.property.title} — ${transactionLabel} ${propertyTypeLabel}, ${formatPrice(listing.priceVND)} VNĐ`}` +- Location: Article element wrapping property card +- Status: ✅ Descriptive accessible name for card + +**Line 50:** `aria-hidden="true"` +- Location: "No image" placeholder +- Type: Decorative content + +**Line 64:** `aria-label={`${listing.property.media.length} ảnh`}` +- Location: Image count badge +- Status: ⚠️ Badge is decorative, aria-label not needed + +**Line 81:** `aria-label="Thông tin bất động sản"` +- Location: Property details list +- Status: ✅ List with semantic label + +#### Authentication Pages + +**Login Page (`apps/web/app/[locale]/(auth)/login/page.tsx`)** +- Line 76: `aria-describedby={errors.phone ? 'phone-error' : undefined}` +- Line 77: `aria-invalid={!!errors.phone}` +- Line 92: `aria-label={showPassword ? t('hidePassword') : t('showPassword')}` +- Line 102: `aria-describedby={errors.password ? 'password-error' : undefined}` +- Line 103: `aria-invalid={!!errors.password}` +- Line 112: `aria-hidden="true"` on loading spinner +- Status: ✅ Comprehensive form accessibility + +**Register Page (`apps/web/app/[locale]/(auth)/register/page.tsx`)** +- Similar pattern to login page with aria-describedby and aria-invalid +- Lines 70, 71, 86, 87, 102, 103, 118, 128, 129, 144, 145 +- Status: ✅ Properly implemented + +#### Landing/Home Page (`apps/web/app/[locale]/(public)/page.tsx`) +**Line 85:** `role="search" aria-label={t('common.search')}` +- Location: Main search form +- Status: ✅ Proper semantic role + +**Line 92:** `aria-label={t('landing.searchPlaceholder')}` +- Location: Search input +- Status: ⚠️ Redundant with placeholder + +**Line 99:** `aria-label={t('landing.transactionTypeLabel')}` +- Location: Transaction type select +- Status: ✅ Good for form accessibility + +**Line 114:** `aria-hidden="true"` +- Location: Decorative SVG + +**Line 147:** `aria-labelledby="featured-heading"` +- Location: Featured listings section +- Status: ✅ Section properly labeled + +**Line 162:** `role="status" aria-label={t('common.loading')}` +- Location: Loading indicator +- Status: ✅ Proper role for status messages + +**Line 163:** `aria-hidden="true"` +- Location: Spinner animation + +**Line 188:** `aria-labelledby="districts-heading"` +- Location: Districts section +- Status: ✅ Section label + +**Line 204:** `aria-hidden="true"` +- Location: Emoji decorations + +**Line 219:** `aria-labelledby="stats-heading"` +- Location: Statistics section +- Status: ✅ Section label + +**Line 234:** `aria-hidden="true"` +- Location: Emoji icons in stats + +#### Error and Not-Found Pages +**error.tsx:** +- Line 51: `aria-hidden="true"` on decorative emoji +- Line 85: `aria-hidden="true"` on SVG spinner + +**not-found.tsx:** +- Line 10: `aria-hidden="true"` on 404 number + +#### UI Component Tests (`components/ui/__tests__/select.spec.tsx`) +- Lines 9, 22, 34: `aria-label` on select components +- Status: ✅ Tests verify accessibility + +--- + +## 2. ICON-ONLY BUTTONS ANALYSIS + +### Buttons Requiring aria-label + +#### ✅ Properly Labeled Icon Buttons + +1. **Mobile Menu Toggle** (`apps/web/app/[locale]/(public)/layout.tsx:90-96`) + ```tsx + + ``` + - Status: ✅ Dynamic aria-label based on state + +2. **Theme Toggle** (`apps/web/app/[locale]/(dashboard)/layout.tsx:146-160`) + ```tsx + + ``` + - Status: ✅ Proper button with icon and aria-label + +3. **Language Switcher** (`apps/web/components/ui/language-switcher.tsx:25-34`) + ```tsx + + ``` + - Status: ✅ Uses both aria-label and sr-only for redundancy + +4. **Image Gallery Navigation** (`apps/web/components/listings/image-gallery.tsx:44-57`) + ```tsx + + ``` + - Status: ✅ Previous/Next buttons have clear labels + +#### ⚠️ Icon-Only Buttons Requiring Attention + +1. **Thumbnail Buttons** (`apps/web/components/listings/image-gallery.tsx:69-84`) + ```tsx + + ``` + - **Issue:** No aria-label on thumbnail buttons + - **Impact:** Screen reader users can't identify which thumbnail they're selecting + - **Fix:** Add `aria-label={`Select image ${index + 1}`}` + +2. **Admin/Dashboard Mobile Close Button** (`apps/web/app/[locale]/(admin)/layout.tsx:80-86`) + ```tsx + + ``` + - Status: ✅ Has aria-label (verified) + +3. **Dashboard Mobile Close Button** (`apps/web/app/[locale]/(dashboard)/layout.tsx:57-63`) + ```tsx + + ``` + - Status: ✅ Has aria-label (verified) + +4. **Admin Mobile Menu Button** (`apps/web/app/[locale]/(admin)/layout.tsx:135`) + ```tsx + + ``` + - Status: ✅ Has aria-label (verified) + +5. **Password Show/Hide Button** (`apps/web/app/[locale]/(auth)/login/page.tsx:88-95`) + ```tsx + + ``` + - Status: ✅ Has aria-label (verified) - though it also has visible text + +6. **Save Search Dialog Toggle** (`apps/web/app/[locale]/(public)/search/page.tsx`) + - **Issue:** Dialog toggle button may be icon-only + - **Status:** Requires verification in full file + +### Summary of Icon-Only Buttons +- **Properly Labeled:** 10+ buttons +- **Missing Labels:** Thumbnail buttons in image gallery (1 instance with multiple buttons) +- **Recommended Action:** Add aria-label to thumbnail buttons for better screen reader support + +--- + +## 3. FORM INPUTS WITHOUT LABELS + +### Analyzed Form Components + +#### ✅ Properly Associated Labels + +1. **Login Form** (`apps/web/app/[locale]/(auth)/login/page.tsx`) + ```tsx +
+ + + {errors.phone && ( + + )} +
+ ``` + - Status: ✅ Label with htmlFor, aria-describedby for errors + +2. **Register Form** (`apps/web/app/[locale]/(auth)/register/page.tsx`) + - Full Name, Phone, Email, Password, Confirm Password inputs + - All have Label components with htmlFor + - Status: ✅ Properly labeled + +3. **Search/Filter Forms** (`apps/web/components/search/filter-bar.tsx`) + ```tsx + + ``` + - Status: ✅ Uses aria-label instead of visual label (acceptable for filters) + +4. **Valuation Form** (`apps/web/components/valuation/valuation-form.tsx`) + ```tsx + + setSearchQuery(e.target.value)} + className="border-0 shadow-none focus-visible:ring-0" + aria-label={t('landing.searchPlaceholder')} + /> + ``` + - Issue: No visible label, relies on aria-label + - Status: ⚠️ Acceptable but should have visible label for better UX + - Recommendation: Add visible label with sr-only utility + +2. **Search Results Sidebar** (`apps/web/app/[locale]/(public)/search/page.tsx`) + - Area range inputs (Lines 150, 158 in filter-bar.tsx) + ```tsx + update('minArea', e.target.value)} + aria-label={`${t('areaLabel')} ${t('areaFrom')}`} + /> + ``` + - Status: ⚠️ No visible labels, only aria-label + - Recommendation: Consider adding visible labels or placeholder context + +### Form Input Summary +- **Properly Labeled:** 95%+ of form inputs +- **Missing Visible Labels:** Landing page search, some range inputs +- **Using aria-label as Primary:** Filter selects, range inputs +- **Status:** Good overall, with minor improvements needed + +--- + +## 4. SKIP-TO-CONTENT LINK + +### Implementation Status: ✅ PROPERLY IMPLEMENTED + +**Location:** `apps/web/app/[locale]/layout.tsx:105-110` + +```tsx + + {t('skipToContent')} + +``` + +**Target:** `apps/web/app/[locale]/(public)/layout.tsx:148` +```tsx +
+ {children} +
+``` + +#### Accessibility Features +✅ Hidden by default with `-translate-y-16` +✅ Visible on focus with `focus:translate-y-0` +✅ Proper link semantics using `` tag +✅ Internationalized text from `t('skipToContent')` +✅ High z-index (z-[100]) ensures visibility +✅ Clear visual styling (primary color, contrast) +✅ Links to main element with id="main-content" + +#### Additional Main Elements Found +1. **Public Layout:** `id="main-content" role="main"` (Line 148) +2. **Auth Layout:** `id="main-content" role="main"` (implicitly defined) +3. **Dashboard Layout:** `id="main-content" role="main"` (Line 141) +4. **Admin Layout:** `id="main-content" role="main"` (Line 141) + +#### Recommendations +- Consider adding skip links to other major sections (navigation, sidebar, footer) +- Test focus visibility in all browsers +- Ensure sufficient color contrast ratio (should meet WCAG AA standards) + +--- + +## 5. INTERACTIVE ELEMENTS WITHOUT ACCESSIBLE NAMES + +### Comprehensive Search Results + +#### ✅ Well-Named Interactive Elements + +1. **Buttons:** All primary buttons have visible text +2. **Links:** All navigation links have visible text (except icons within them) +3. **Form Controls:** Most have labels or aria-labels +4. **Navigation Elements:** All navigation menus properly labeled + +#### ⚠️ Elements Requiring Attention + +1. **Thumbnail Selection Buttons** (CRITICAL) + - **File:** `apps/web/components/listings/image-gallery.tsx:69-84` + - **Issue:** Buttons selecting image thumbnails lack accessible names + - **Current Code:** + ```tsx + + ``` + - **Problem:** No aria-label or aria-labelledby + - **Impact:** Screen reader users see only "button" without context + - **Fix:** Add `aria-label={`Select image ${index + 1}: ${img.caption || 'unlabeled'}`}` + +2. **Navigation Links with Only Icons** (in Dashboard/Admin) + - **File:** `apps/web/app/[locale]/(dashboard)/layout.tsx:122-135` + - **Current:** Links on mobile with icon only + - **Status:** ✅ Actually has text on desktop, hidden on mobile + - **Code:** + ```tsx + + + {item.label} + + ``` + - **Note:** aria-label is redundant here since text is visible on some screens + - **Recommendation:** Consider removing aria-label, as visible text is preferred + +3. **OAuth Buttons** (Potentially Problematic) + - **File:** `apps/web/components/auth/oauth-buttons.tsx:10-53` + - **Current:** + ```tsx + + ``` + - **Status:** ✅ Has visible text "Google" and "Zalo" + - **Issue:** SVG icons lack alt text, but not critical since text label exists + +4. **Search Form with Select Dropdown** (MINOR) + - **File:** `apps/web/app/[locale]/(public)/page.tsx:95-114` + - **Status:** ✅ All controls have aria-labels + - **Note:** Dropdowns are properly accessible + +### Summary: Interactive Elements +- **Properly Named:** ~95% of interactive elements +- **Critical Issues:** Thumbnail buttons (1 location, multiple instances) +- **Minor Issues:** Some redundant aria-labels on visible text links +- **Action Items:** Add accessible names to image thumbnails + +--- + +## 6. LAYOUT STRUCTURE & LANDMARK REGIONS + +### Main Layout Files + +#### 1. **Root Layout** (`apps/web/app/[locale]/layout.tsx`) +**Structure:** +``` + + + Skip to main content + + + + + + {children} + + + + + + +``` +- Status: ✅ Proper language attribute +- Providers properly nested +- Skip-to-content link present + +#### 2. **Public Layout** (`apps/web/app/[locale]/(public)/layout.tsx`) +**Landmarks:** +- `
` (Line 39-146) + - Navigation: `