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:
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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": "استشارة شخصية"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user