main
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
'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 "image of"</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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user