Improve app's language switching and add Omani professionals directory

Fixes RTL layout issues, persists language preference, adds Omani professional directory and translates sidebar navigation.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: c5f0c281-8dd8-4846-b452-4a07bcd21062
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/7b0998af-3fae-48c5-b382-eb5130b5525d.jpg
This commit is contained in:
ghaddaditw
2025-06-08 07:04:20 +00:00
parent beb211df0a
commit 2f4868a419
4 changed files with 554 additions and 259 deletions
+20 -5
View File
@@ -7,22 +7,37 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { Languages } from 'lucide-react'; import { Languages } from 'lucide-react';
import { useEffect } from 'react';
export function LanguageSwitcher() { export function LanguageSwitcher() {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const languages = [ const languages = [
{ code: 'en', name: 'English', flag: '🇺🇸' }, { code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'ar', name: 'العربية', flag: '🇸🇦' } { code: 'ar', name: 'العربية', flag: '🇴🇲' }
]; ];
const changeLanguage = (langCode: string) => { const changeLanguage = async (langCode: string) => {
i18n.changeLanguage(langCode); await i18n.changeLanguage(langCode);
// Update document direction for Arabic // Update document direction and language for proper RTL support
document.documentElement.dir = langCode === 'ar' ? 'rtl' : 'ltr'; document.documentElement.dir = langCode === 'ar' ? 'rtl' : 'ltr';
document.documentElement.lang = langCode; document.documentElement.lang = langCode;
// Store in localStorage for persistence
localStorage.setItem('language', langCode);
// Force re-render of components
window.location.reload();
}; };
// Initialize language on component mount
useEffect(() => {
const savedLanguage = localStorage.getItem('language');
if (savedLanguage && savedLanguage !== i18n.language) {
i18n.changeLanguage(savedLanguage);
document.documentElement.dir = savedLanguage === 'ar' ? 'rtl' : 'ltr';
document.documentElement.lang = savedLanguage;
}
}, [i18n]);
const currentLanguage = languages.find(lang => lang.code === i18n.language) || languages[0]; const currentLanguage = languages.find(lang => lang.code === i18n.language) || languages[0];
return ( return (
@@ -30,7 +45,7 @@ export function LanguageSwitcher() {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="gap-2"> <Button variant="ghost" size="sm" className="gap-2">
<Languages className="h-4 w-4" /> <Languages className="h-4 w-4" />
<span className="hidden sm:inline">{currentLanguage.flag} {currentLanguage.name}</span> <span className="hidden sm:inline">{currentLanguage?.flag} {currentLanguage?.name}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
+18 -11
View File
@@ -1,5 +1,6 @@
import { Link, useLocation } from "wouter"; import { Link, useLocation } from "wouter";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -16,57 +17,63 @@ import {
BarChart3, BarChart3,
Shield, Shield,
Zap, Zap,
Monitor Monitor,
UserCheck
} from "lucide-react"; } from "lucide-react";
interface SidebarProps { interface SidebarProps {
className?: string; className?: string;
} }
const navigation = [ const getNavigation = (t: any) => [
{ {
name: "Dashboard", name: t('navigation.dashboard'),
href: "/dashboard", href: "/dashboard",
icon: LayoutDashboard, icon: LayoutDashboard,
}, },
{ {
name: "Tasks", name: t('navigation.tasks'),
href: "/tasks", href: "/tasks",
icon: Calendar, icon: Calendar,
badge: "3", // This would come from task count badge: "3", // This would come from task count
}, },
{ {
name: "Finances", name: t('navigation.finances'),
href: "/finances", href: "/finances",
icon: DollarSign, icon: DollarSign,
}, },
{ {
name: "Voice Commands", name: t('navigation.professionals'),
href: "/professionals",
icon: UserCheck,
},
{
name: t('navigation.voice'),
href: "/voice", href: "/voice",
icon: Mic, icon: Mic,
}, },
{ {
name: "AI Assistant", name: t('navigation.ai'),
href: "/ai", href: "/ai",
icon: Brain, icon: Brain,
}, },
]; ];
const managementNavigation = [ const getManagementNavigation = (t: any) => [
{ {
name: "Analytics", name: t('navigation.analytics'),
href: "/analytics", href: "/analytics",
icon: BarChart3, icon: BarChart3,
roles: ["pro", "admin"], roles: ["pro", "admin"],
}, },
{ {
name: "System Monitor", name: t('navigation.monitoring'),
href: "/monitoring", href: "/monitoring",
icon: Monitor, icon: Monitor,
roles: ["pro", "admin"], roles: ["pro", "admin"],
}, },
{ {
name: "Settings", name: t('navigation.settings'),
href: "/settings", href: "/settings",
icon: Settings, icon: Settings,
}, },
+126
View File
@@ -28,6 +28,8 @@
"ai": "المساعد الذكي", "ai": "المساعد الذكي",
"voice": "الصوت", "voice": "الصوت",
"analytics": "التحليلات", "analytics": "التحليلات",
"monitoring": "مراقبة النظام",
"professionals": "الدليل المهني",
"settings": "الإعدادات", "settings": "الإعدادات",
"logout": "تسجيل الخروج", "logout": "تسجيل الخروج",
"profile": "الملف الشخصي" "profile": "الملف الشخصي"
@@ -216,5 +218,129 @@
"deleted": "تم الحذف بنجاح", "deleted": "تم الحذف بنجاح",
"updated": "تم التحديث بنجاح", "updated": "تم التحديث بنجاح",
"created": "تم الإنشاء بنجاح" "created": "تم الإنشاء بنجاح"
},
"monitoring": {
"title": "مراقبة النظام",
"realTimeMetrics": "المقاييس في الوقت الفعلي",
"systemHealth": "صحة النظام",
"performance": "الأداء",
"analytics": "التحليلات",
"systemResources": "موارد النظام",
"systemStatus": "حالة النظام",
"healthy": "سليم",
"degraded": "متدهور",
"unhealthy": "غير سليم",
"uptime": "مدة التشغيل",
"responseTime": "وقت الاستجابة",
"memoryUsage": "استخدام الذاكرة",
"serviceHealth": "صحة الخدمات",
"database": "قاعدة البيانات",
"cache": "التخزين المؤقت",
"ai": "الذكاء الاصطناعي",
"voice": "الصوت",
"performanceOverview": "نظرة عامة على الأداء",
"errorRate": "معدل الأخطاء",
"throughput": "الإنتاجية",
"usageTrends": "اتجاهات الاستخدام",
"totalUsers": "إجمالي المستخدمين",
"voiceCommands": "الأوامر الصوتية",
"aiInteractions": "تفاعلات الذكاء الاصطناعي",
"tasksCreated": "المهام المنشأة",
"cacheStatistics": "إحصائيات التخزين المؤقت",
"cacheType": "نوع التخزين المؤقت",
"redisConnected": "اتصال Redis",
"memoryCacheSize": "حجم التخزين المؤقت",
"systemMetrics": "مقاييس النظام",
"userCount": "عدد المستخدمين",
"activeUsers": "المستخدمون النشطون",
"totalTasks": "إجمالي المهام",
"financialRecords": "السجلات المالية",
"autoRefresh": "تحديث تلقائي",
"heapUsed": "الذاكرة المستخدمة",
"heapTotal": "إجمالي الذاكرة",
"external": "خارجي",
"rss": "RSS",
"24Hours": "24 ساعة",
"7Days": "7 أيام",
"30Days": "30 يوم"
},
"professionals": {
"title": "الدليل المهني العماني",
"subtitle": "اكتشف المهنيين المعتمدين في سلطنة عمان",
"searchProfessionals": "البحث عن المهنيين",
"filterByType": "تصفية حسب النوع",
"filterByLocation": "تصفية حسب الموقع",
"bookAppointment": "حجز موعد",
"viewProfile": "عرض الملف الشخصي",
"contactInfo": "معلومات الاتصال",
"workingHours": "ساعات العمل",
"services": "الخدمات",
"experience": "الخبرة",
"education": "التعليم",
"certifications": "الشهادات",
"rating": "التقييم",
"reviews": "المراجعات",
"languages": "اللغات",
"specialization": "التخصص",
"availableSlots": "المواعيد المتاحة",
"bookingConfirmed": "تم تأكيد الحجز",
"types": {
"doctor": "طبيب",
"lawyer": "محامي",
"engineer": "مهندس",
"accountant": "محاسب",
"consultant": "استشاري",
"teacher": "معلم",
"dentist": "طبيب أسنان",
"veterinarian": "طبيب بيطري",
"architect": "معماري",
"therapist": "معالج",
"nutritionist": "أخصائي تغذية",
"trainer": "مدرب"
},
"locations": {
"muscat": "مسقط",
"salalah": "صلالة",
"nizwa": "نزوى",
"sur": "صور",
"sohar": "صحار",
"rustaq": "الرستاق",
"ibri": "عبري",
"khasab": "خصب",
"bahla": "بهلاء",
"buraimi": "البريمي",
"adam": "آدم",
"bidiyah": "بدية"
},
"days": {
"sunday": "الأحد",
"monday": "الاثنين",
"tuesday": "الثلاثاء",
"wednesday": "الأربعاء",
"thursday": "الخميس",
"friday": "الجمعة",
"saturday": "السبت"
},
"appointmentStatus": {
"pending": "معلق",
"confirmed": "مؤكد",
"completed": "مكتمل",
"cancelled": "ملغي"
},
"connectionStatus": {
"pending": "معلق",
"accepted": "مقبول",
"declined": "مرفوض"
},
"noResults": "لا توجد نتائج",
"noProfessionals": "لا يوجد مهنيون مطابقون للبحث",
"loadingProfessionals": "جاري تحميل المهنيين...",
"selectTimeSlot": "اختر الوقت المناسب",
"appointmentNotes": "ملاحظات الموعد",
"emergencyContact": "جهة اتصال الطوارئ",
"consultationFee": "رسوم الاستشارة",
"acceptedInsurance": "التأمين المقبول",
"onlineConsultation": "استشارة عبر الإنترنت",
"inPersonConsultation": "استشارة شخصية"
} }
} }
+355 -208
View File
@@ -1,263 +1,410 @@
import { useState } from "react"; import { useState } from 'react';
import { useQuery } from "@tanstack/react-query"; import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from "@/components/ui/input"; import { Input } from '@/components/ui/input';
import { Button } from "@/components/ui/button"; import { Button } from '@/components/ui/button';
import { Badge } from "@/components/ui/badge"; import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Star, MapPin, Clock, Phone, Mail, Calendar } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Link } from "wouter"; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import {
Search,
MapPin,
Star,
Phone,
Mail,
Clock,
Calendar,
User,
GraduationCap,
Award,
Languages as LanguagesIcon,
MessageCircle
} from 'lucide-react';
interface Professional { interface Professional {
id: number; id: number;
firstName: string; name: string;
lastName: string; type: string;
businessName: string; location: string;
professionalType: string; rating: number;
bio: string; reviews: number;
specializations: string[]; experience: number;
address: string; languages: string[];
phoneNumber: string; specialization: string;
phone: string;
email: string; email: string;
profileImageUrl: string; workingHours: string;
averageRating: number; education: string;
totalReviews: number; certifications: string[];
allowAppointmentBooking: boolean; consultationFee: number;
avatar?: string;
services: string[];
onlineConsultation: boolean;
inPersonConsultation: boolean;
} }
export default function ProfessionalDirectory() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedType, setSelectedType] = useState("");
const { data: professionals = [], isLoading } = useQuery({
queryKey: ["/api/professionals/search", { searchTerm, type: selectedType }],
enabled: true,
});
// Mock data for now since the API endpoint doesn't exist yet
const mockProfessionals: Professional[] = [ const mockProfessionals: Professional[] = [
{ {
id: 1, id: 1,
firstName: "Dr. Ahmed", name: "د. أحمد المحرزي",
lastName: "Al-Rashid", type: "doctor",
businessName: "Muscat Medical Center", location: "muscat",
professionalType: "doctor", rating: 4.8,
bio: "Experienced cardiologist with 15+ years of practice in Oman", reviews: 127,
specializations: ["Cardiology", "Internal Medicine", "Preventive Care"], experience: 15,
address: "Al Khuwair, Muscat, Oman", languages: ["Arabic", "English"],
phoneNumber: "+968 2234 5678", specialization: "طب القلب",
email: "ahmed.rashid@medical.om", phone: "+968 9123 4567",
profileImageUrl: "", email: "ahmed.almahrezi@example.om",
averageRating: 4.8, workingHours: "الأحد - الخميس: 8:00 ص - 6:00 م",
totalReviews: 127, education: "جامعة السلطان قابوس - كلية الطب",
allowAppointmentBooking: true, certifications: ["البورد العماني في طب القلب", "زمالة الكلية الأمريكية لأطباء القلب"],
consultationFee: 25,
services: ["فحص القلب", "تخطيط القلب", "قسطرة القلب"],
onlineConsultation: true,
inPersonConsultation: true
}, },
{ {
id: 2, id: 2,
firstName: "Fatima", name: "أ. فاطمة الزدجالية",
lastName: "Al-Zahra", type: "lawyer",
businessName: "Legal Consultancy Oman", location: "muscat",
professionalType: "lawyer", rating: 4.9,
bio: "Corporate law specialist focusing on business formation and compliance", reviews: 89,
specializations: ["Corporate Law", "Business Formation", "Contract Law"], experience: 12,
address: "Ruwi, Muscat, Oman", languages: ["Arabic", "English"],
phoneNumber: "+968 2445 6789", specialization: "القانون التجاري",
email: "fatima.zahra@legal.om", phone: "+968 9234 5678",
profileImageUrl: "", email: "fatima.alzadjali@example.om",
averageRating: 4.9, workingHours: "الأحد - الخميس: 9:00 ص - 5:00 م",
totalReviews: 89, education: "جامعة السلطان قابوس - كلية الحقوق",
allowAppointmentBooking: true, certifications: ["عضو نقابة المحامين العمانيين", "دبلوم التحكيم التجاري"],
consultationFee: 30,
services: ["استشارات قانونية", "صياغة العقود", "التقاضي"],
onlineConsultation: true,
inPersonConsultation: true
}, },
{
id: 3,
name: "م. سالم البلوشي",
type: "engineer",
location: "sohar",
rating: 4.7,
reviews: 64,
experience: 10,
languages: ["Arabic", "English"],
specialization: "الهندسة المدنية",
phone: "+968 9345 6789",
email: "salem.albalushi@example.om",
workingHours: "الأحد - الخميس: 7:00 ص - 4:00 م",
education: "الجامعة الألمانية للتكنولوجيا في عمان",
certifications: ["مهندس مدني معتمد", "إدارة المشاريع PMP"],
consultationFee: 20,
services: ["تصميم المباني", "إشراف التنفيذ", "استشارات هندسية"],
onlineConsultation: false,
inPersonConsultation: true
},
{
id: 4,
name: "د. مريم الهنائية",
type: "dentist",
location: "nizwa",
rating: 4.6,
reviews: 92,
experience: 8,
languages: ["Arabic", "English"],
specialization: "طب الأسنان التجميلي",
phone: "+968 9456 7890",
email: "mariam.alhinai@example.om",
workingHours: "السبت - الأربعاء: 10:00 ص - 7:00 م",
education: "كلية طب الأسنان - جامعة السلطان قابوس",
certifications: ["دبلوم طب الأسنان التجميلي", "شهادة زراعة الأسنان"],
consultationFee: 15,
services: ["تنظيف الأسنان", "تجميل الأسنان", "زراعة الأسنان"],
onlineConsultation: true,
inPersonConsultation: true
}
]; ];
const displayProfessionals = Array.isArray(professionals) && professionals.length > 0 ? professionals as Professional[] : mockProfessionals; export default function ProfessionalDirectory() {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [selectedType, setSelectedType] = useState('');
const [selectedLocation, setSelectedLocation] = useState('');
const [selectedProfessional, setSelectedProfessional] = useState<Professional | null>(null);
const professionalTypes = [ const professionalTypes = [
"doctor", "dentist", "therapist", "consultant", "lawyer", 'doctor', 'lawyer', 'engineer', 'accountant', 'consultant',
"accountant", "coach", "tutor", "other" 'teacher', 'dentist', 'veterinarian', 'architect', 'therapist'
]; ];
const filteredProfessionals = displayProfessionals.filter((prof: Professional) => const locations = [
prof.firstName.toLowerCase().includes(searchTerm.toLowerCase()) || 'muscat', 'salalah', 'nizwa', 'sur', 'sohar', 'rustaq',
prof.lastName.toLowerCase().includes(searchTerm.toLowerCase()) || 'ibri', 'khasab', 'bahla', 'buraimi', 'adam', 'bidiyah'
prof.businessName?.toLowerCase().includes(searchTerm.toLowerCase()) || ];
prof.specializations?.some((spec: string) =>
spec.toLowerCase().includes(searchTerm.toLowerCase())
)
);
return ( const filteredProfessionals = mockProfessionals.filter(professional => {
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-violet-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900"> const matchesSearch = professional.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
<div className="container mx-auto px-4 py-8"> professional.specialization.toLowerCase().includes(searchTerm.toLowerCase());
<div className="max-w-6xl mx-auto"> const matchesType = !selectedType || professional.type === selectedType;
{/* Header */} const matchesLocation = !selectedLocation || professional.location === selectedLocation;
<div className="text-center mb-8"> return matchesSearch && matchesType && matchesLocation;
<h1 className="text-4xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent mb-4"> });
Professional Directory
</h1>
<p className="text-lg text-muted-foreground">
Discover and connect with trusted professionals in your area
</p>
</div>
{/* Search and Filters */} const ProfessionalCard = ({ professional }: { professional: Professional }) => (
<div className="flex flex-col md:flex-row gap-4 mb-8"> <Card className="hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex-1">
<Input
placeholder="Search professionals, services, or specializations..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
<div className="flex gap-2 flex-wrap">
<Button
variant={selectedType === "" ? "default" : "outline"}
onClick={() => setSelectedType("")}
size="sm"
>
All
</Button>
{professionalTypes.map((type) => (
<Button
key={type}
variant={selectedType === type ? "default" : "outline"}
onClick={() => setSelectedType(type)}
size="sm"
className="capitalize"
>
{type}
</Button>
))}
</div>
</div>
{/* Results */}
{isLoading ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<Card key={i} className="animate-pulse">
<CardHeader> <CardHeader>
<div className="flex items-center space-x-3"> <div className="flex items-start gap-4">
<div className="w-12 h-12 bg-gray-200 rounded-full"></div> <Avatar className="h-16 w-16">
<div className="space-y-2"> <AvatarImage src={professional.avatar} alt={professional.name} />
<div className="h-4 bg-gray-200 rounded w-32"></div> <AvatarFallback className="text-lg">
<div className="h-3 bg-gray-200 rounded w-24"></div> {professional.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<CardTitle className="text-lg mb-1">{professional.name}</CardTitle>
<CardDescription className="mb-2">
{t(`professionals.types.${professional.type}`)} {professional.specialization}
</CardDescription>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<MapPin className="h-4 w-4" />
{t(`professionals.locations.${professional.location}`)}
</div>
<div className="flex items-center gap-1">
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
{professional.rating} ({professional.reviews})
</div>
<div className="flex items-center gap-1">
<User className="h-4 w-4" />
{professional.experience} {t('professionals.experience')}
</div>
</div>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-3"> <div className="flex flex-wrap gap-2 mb-4">
<div className="h-3 bg-gray-200 rounded"></div> {professional.languages.map((lang, index) => (
<div className="h-3 bg-gray-200 rounded w-3/4"></div> <Badge key={index} variant="secondary" className="text-xs">
</div> {lang}
</CardContent> </Badge>
</Card>
))} ))}
</div> </div>
) : filteredProfessionals.length === 0 ? ( <div className="flex justify-between items-center">
<div className="text-center py-12"> <div className="text-sm">
<p className="text-lg text-muted-foreground"> <span className="font-medium">{professional.consultationFee} ر.ع</span>
No professionals found matching your criteria. <span className="text-muted-foreground ml-1">/استشارة</span>
</p>
<p className="text-sm text-muted-foreground mt-2">
Try adjusting your search terms or filters.
</p>
</div> </div>
) : ( <Dialog>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> <DialogTrigger asChild>
{filteredProfessionals.map((professional: Professional) => ( <Button size="sm" onClick={() => setSelectedProfessional(professional)}>
<Card key={professional.id} className="hover:shadow-lg transition-shadow"> {t('professionals.viewProfile')}
<CardHeader> </Button>
<div className="flex items-center space-x-3"> </DialogTrigger>
<Avatar className="w-12 h-12"> <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<AvatarImage src={professional.profileImageUrl} /> <DialogHeader>
<DialogTitle className="flex items-center gap-3">
<Avatar className="h-12 w-12">
<AvatarImage src={professional.avatar} alt={professional.name} />
<AvatarFallback> <AvatarFallback>
{professional.firstName[0]}{professional.lastName[0]} {professional.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1"> <div>
<CardTitle className="text-lg"> <div className="text-xl">{professional.name}</div>
{professional.firstName} {professional.lastName} <div className="text-sm text-muted-foreground font-normal">
</CardTitle> {t(`professionals.types.${professional.type}`)} {professional.specialization}
{professional.businessName && (
<p className="text-sm text-muted-foreground">
{professional.businessName}
</p>
)}
<div className="flex items-center gap-1 mt-1">
<Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
<span className="text-sm font-medium">
{professional.averageRating || "New"}
</span>
<span className="text-sm text-muted-foreground">
({professional.totalReviews || 0} reviews)
</span>
</div> </div>
</div> </div>
</div> </DialogTitle>
</CardHeader> </DialogHeader>
<CardContent className="space-y-4">
<Badge variant="secondary" className="capitalize">
{professional.professionalType}
</Badge>
{professional.bio && ( <Tabs defaultValue="info" className="w-full">
<p className="text-sm text-muted-foreground line-clamp-2"> <TabsList className="grid w-full grid-cols-3">
{professional.bio} <TabsTrigger value="info">{t('professionals.contactInfo')}</TabsTrigger>
</p> <TabsTrigger value="services">{t('professionals.services')}</TabsTrigger>
)} <TabsTrigger value="booking">{t('professionals.bookAppointment')}</TabsTrigger>
</TabsList>
{professional.specializations && professional.specializations.length > 0 && ( <TabsContent value="info" className="space-y-4">
<div className="flex flex-wrap gap-1"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{professional.specializations.slice(0, 3).map((spec, index) => ( <div className="space-y-3">
<Badge key={index} variant="outline" className="text-xs"> <div className="flex items-center gap-2">
{spec} <Phone className="h-4 w-4" />
<span>{professional.phone}</span>
</div>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4" />
<span>{professional.email}</span>
</div>
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
<span>{t(`professionals.locations.${professional.location}`)}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span>{professional.workingHours}</span>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<GraduationCap className="h-4 w-4" />
<span>{professional.education}</span>
</div>
<div className="flex items-center gap-2">
<LanguagesIcon className="h-4 w-4" />
<span>{professional.languages.join(', ')}</span>
</div>
<div className="flex items-center gap-2">
<Star className="h-4 w-4" />
<span>{professional.rating}/5 ({professional.reviews} مراجعة)</span>
</div>
</div>
</div>
<div>
<h4 className="font-medium mb-2 flex items-center gap-2">
<Award className="h-4 w-4" />
{t('professionals.certifications')}
</h4>
<div className="flex flex-wrap gap-2">
{professional.certifications.map((cert, index) => (
<Badge key={index} variant="outline">
{cert}
</Badge> </Badge>
))} ))}
{professional.specializations.length > 3 && (
<Badge variant="outline" className="text-xs">
+{professional.specializations.length - 3} more
</Badge>
)}
</div> </div>
)} </div>
</TabsContent>
<div className="space-y-2 text-sm"> <TabsContent value="services" className="space-y-4">
{professional.address && ( <div>
<div className="flex items-center gap-2 text-muted-foreground"> <h4 className="font-medium mb-3">{t('professionals.services')}</h4>
<MapPin className="w-4 h-4" /> <div className="grid gap-2">
<span>{professional.address}</span> {professional.services.map((service, index) => (
<div key={index} className="flex items-center gap-2 p-2 border rounded">
<MessageCircle className="h-4 w-4" />
<span>{service}</span>
</div> </div>
)} ))}
{professional.phoneNumber && (
<div className="flex items-center gap-2 text-muted-foreground">
<Phone className="w-4 h-4" />
<span>{professional.phoneNumber}</span>
</div> </div>
)}
</div> </div>
<div className="flex gap-2 pt-4"> <div className="grid grid-cols-2 gap-4">
<Button asChild className="flex-1" size="sm"> <div className="flex items-center gap-2">
<Link href={`/professional/${professional.id}`}> <input
View Profile type="checkbox"
</Link> checked={professional.onlineConsultation}
readOnly
className="rounded"
/>
<span>{t('professionals.onlineConsultation')}</span>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={professional.inPersonConsultation}
readOnly
className="rounded"
/>
<span>{t('professionals.inPersonConsultation')}</span>
</div>
</div>
<div className="bg-muted p-4 rounded">
<div className="font-medium">{t('professionals.consultationFee')}</div>
<div className="text-2xl font-bold text-primary">
{professional.consultationFee} ر.ع
</div>
</div>
</TabsContent>
<TabsContent value="booking" className="space-y-4">
<div className="text-center p-8 border-2 border-dashed rounded-lg">
<Calendar className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h3 className="text-lg font-medium mb-2">
{t('professionals.bookAppointment')}
</h3>
<p className="text-muted-foreground mb-4">
اختر الوقت المناسب لك من المواعيد المتاحة
</p>
<Button className="w-full">
{t('professionals.selectTimeSlot')}
</Button> </Button>
{professional.allowAppointmentBooking && ( </div>
<Button variant="outline" size="sm" className="flex items-center gap-1"> </TabsContent>
<Calendar className="w-4 h-4" /> </Tabs>
Book </DialogContent>
</Button> </Dialog>
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
);
return (
<div className="space-y-6">
<div className="text-center">
<h1 className="text-3xl font-bold mb-2">{t('professionals.title')}</h1>
<p className="text-muted-foreground">{t('professionals.subtitle')}</p>
</div>
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t('professionals.searchProfessionals')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select value={selectedType} onValueChange={setSelectedType}>
<SelectTrigger className="w-full md:w-48">
<SelectValue placeholder={t('professionals.filterByType')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="">{t('common.all')}</SelectItem>
{professionalTypes.map(type => (
<SelectItem key={type} value={type}>
{t(`professionals.types.${type}`)}
</SelectItem>
))} ))}
</SelectContent>
</Select>
<Select value={selectedLocation} onValueChange={setSelectedLocation}>
<SelectTrigger className="w-full md:w-48">
<SelectValue placeholder={t('professionals.filterByLocation')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="">{t('common.all')}</SelectItem>
{locations.map(location => (
<SelectItem key={location} value={location}>
{t(`professionals.locations.${location}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredProfessionals.length > 0 ? (
filteredProfessionals.map(professional => (
<ProfessionalCard key={professional.id} professional={professional} />
))
) : (
<div className="col-span-full text-center py-12">
<User className="h-16 w-16 mx-auto mb-4 text-muted-foreground" />
<h3 className="text-lg font-medium mb-2">{t('professionals.noResults')}</h3>
<p className="text-muted-foreground">{t('professionals.noProfessionals')}</p>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div>
); );
} }