Files
unai/src/app/accessibility/page.tsx
T
2025-07-12 22:36:47 +04:00

393 lines
14 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react'
import { useAuth } from '@/components/providers/MockAuthProvider'
import { useLanguage } from '@/components/providers/LanguageProvider'
import { useRouter } from 'next/navigation'
import {
Upload,
Image as ImageIcon,
CheckCircle,
XCircle,
AlertCircle,
Eye,
LogOut,
Languages,
ArrowLeft
} from 'lucide-react'
import Link from 'next/link'
interface AccessibilityAudit {
id: string
altText: string
wcagScore: number
issues: string[]
suggestions: string[]
}
export default function AccessibilityPage() {
const { user, userProfile, logout } = useAuth()
const { t, language, setLanguage } = useLanguage()
const router = useRouter()
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [preview, setPreview] = useState<string | null>(null)
const [audit, setAudit] = useState<AccessibilityAudit | null>(null)
const [isAnalyzing, setIsAnalyzing] = useState(false)
const [recentAudits, setRecentAudits] = useState<AccessibilityAudit[]>([])
useEffect(() => {
if (!user) {
router.push('/')
}
}, [user, router])
useEffect(() => {
fetchRecentAudits()
}, [])
const fetchRecentAudits = async () => {
try {
const response = await fetch('/api/accessibility')
if (response.ok) {
const data = await response.json()
setRecentAudits(data.audits || [])
}
} catch (error) {
console.error('Error fetching audits:', error)
}
}
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
setSelectedFile(file)
// Create preview
const reader = new FileReader()
reader.onload = (e) => {
setPreview(e.target?.result as string)
}
reader.readAsDataURL(file)
}
}
const handleAnalyze = async () => {
if (!selectedFile) return
setIsAnalyzing(true)
try {
const formData = new FormData()
formData.append('file', selectedFile)
const response = await fetch('/api/accessibility', {
method: 'POST',
body: formData,
})
if (response.ok) {
const data = await response.json()
setAudit(data.audit)
fetchRecentAudits()
} else {
console.error('Failed to analyze image')
}
} catch (error) {
console.error('Error analyzing image:', error)
} finally {
setIsAnalyzing(false)
}
}
const handleLogout = async () => {
await logout()
}
const toggleLanguage = () => {
setLanguage(language === 'en' ? 'ar' : 'en')
}
const getScoreColor = (score: number) => {
if (score >= 0.9) return 'text-green-600'
if (score >= 0.7) return 'text-yellow-600'
return 'text-red-600'
}
const getScoreBackground = (score: number) => {
if (score >= 0.9) return 'bg-green-50 border-green-200'
if (score >= 0.7) return 'bg-yellow-50 border-yellow-200'
return 'bg-red-50 border-red-200'
}
if (!user || !userProfile) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading accessibility tools...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm border-b">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Link href="/dashboard" className="flex items-center space-x-2 text-blue-600 hover:text-blue-800">
<ArrowLeft size={20} />
<span>Back to Dashboard</span>
</Link>
<div className="text-gray-300">|</div>
<h1 className="text-xl font-bold text-gray-900">
{t('accessibility')} Tools
</h1>
</div>
<div className="flex items-center space-x-4">
<button
onClick={toggleLanguage}
className="flex items-center space-x-2 px-3 py-2 rounded-lg bg-gray-100 hover:bg-gray-200 transition-colors"
>
<Languages size={16} />
<span className="text-sm font-medium">{language.toUpperCase()}</span>
</button>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-medium">
{userProfile.name.charAt(0)}
</span>
</div>
<div className="hidden md:block">
<p className="text-sm font-medium text-gray-900">{userProfile.name}</p>
<p className="text-xs text-gray-500">{userProfile.role}</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex items-center space-x-2 px-3 py-2 rounded-lg bg-red-100 hover:bg-red-200 transition-colors text-red-700"
>
<LogOut size={16} />
<span className="text-sm font-medium">{t('logout')}</span>
</button>
</div>
</div>
</div>
</header>
{/* Main Content */}
<main className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
{/* Upload Section */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Image Accessibility Analyzer
</h2>
<p className="text-gray-600 mb-6">
Upload an image to automatically generate alt text and check WCAG 2.2 compliance.
Our AI-powered tool helps ensure your content is accessible to everyone.
</p>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Upload Area */}
<div>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
<input
type="file"
accept="image/*"
onChange={handleFileSelect}
className="hidden"
id="image-upload"
/>
<label
htmlFor="image-upload"
className="cursor-pointer flex flex-col items-center"
>
<Upload size={48} className="text-gray-400 mb-4" />
<p className="text-lg font-medium text-gray-900 mb-2">
{t('upload_image')}
</p>
<p className="text-sm text-gray-600">
PNG, JPG, GIF up to 10MB
</p>
</label>
</div>
{selectedFile && (
<div className="mt-4">
<p className="text-sm text-gray-600 mb-2">
Selected: {selectedFile.name}
</p>
<button
onClick={handleAnalyze}
disabled={isAnalyzing}
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isAnalyzing ? 'Analyzing...' : 'Analyze Image'}
</button>
</div>
)}
</div>
{/* Preview */}
{preview && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Image Preview
</h3>
<div className="bg-gray-100 rounded-lg p-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={preview}
alt="Preview"
className="max-w-full h-auto rounded-lg"
/>
</div>
</div>
)}
</div>
</div>
{/* Analysis Results */}
{audit && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Analysis Results
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Alt Text */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="text-lg font-semibold text-blue-900 mb-2">
{t('alt_text_suggestion')}
</h3>
<p className="text-blue-800">{audit.altText}</p>
</div>
{/* WCAG Score */}
<div className={`border rounded-lg p-4 ${getScoreBackground(audit.wcagScore)}`}>
<h3 className="text-lg font-semibold mb-2">
{t('wcag_score')}
</h3>
<div className="flex items-center space-x-2">
<span className={`text-2xl font-bold ${getScoreColor(audit.wcagScore)}`}>
{Math.round(audit.wcagScore * 100)}%
</span>
{audit.wcagScore >= 0.9 ? (
<CheckCircle className="text-green-600" size={24} />
) : audit.wcagScore >= 0.7 ? (
<AlertCircle className="text-yellow-600" size={24} />
) : (
<XCircle className="text-red-600" size={24} />
)}
</div>
</div>
</div>
{/* Issues and Suggestions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
{audit.issues.length > 0 && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
Issues Found
</h3>
<ul className="space-y-2">
{audit.issues.map((issue, index) => (
<li key={index} className="flex items-start space-x-2">
<XCircle className="text-red-500 mt-0.5" size={16} />
<span className="text-sm text-gray-700">{issue}</span>
</li>
))}
</ul>
</div>
)}
{audit.suggestions.length > 0 && (
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
Suggestions
</h3>
<ul className="space-y-2">
{audit.suggestions.map((suggestion, index) => (
<li key={index} className="flex items-start space-x-2">
<CheckCircle className="text-green-500 mt-0.5" size={16} />
<span className="text-sm text-gray-700">{suggestion}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
{/* Recent Audits */}
{recentAudits.length > 0 && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Recent Audits
</h2>
<div className="space-y-4">
{recentAudits.slice(0, 5).map((recentAudit, index) => (
<div key={index} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
<ImageIcon className="text-gray-400" size={20} />
<div>
<p className="font-medium text-gray-900">
{recentAudit.altText.substring(0, 50)}...
</p>
<p className="text-sm text-gray-600">
Score: {Math.round(recentAudit.wcagScore * 100)}%
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Eye className="text-gray-400" size={16} />
<span className="text-sm text-gray-600">View Details</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Guidelines */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mt-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Accessibility Guidelines
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
Alt Text Best Practices
</h3>
<ul className="space-y-2 text-sm text-gray-700">
<li> Be concise but descriptive</li>
<li> Focus on important details</li>
<li> Avoid redundant phrases like &quot;image of&quot;</li>
<li> Keep under 125 characters when possible</li>
</ul>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
WCAG 2.2 Compliance
</h3>
<ul className="space-y-2 text-sm text-gray-700">
<li> Color contrast ratio of 4.5:1 minimum</li>
<li> Keyboard navigation support</li>
<li> Screen reader compatibility</li>
<li> Clear focus indicators</li>
</ul>
</div>
</div>
</div>
</div>
</main>
</div>
)
}