Enhance user experience with voice commands, notifications, and a professional directory

Adds voice command integration, a notification system, and a professional directory page with search filters.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 556aa286-edd2-4cea-8583-f4fc3cfd119b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/81470e0d-8ae8-4335-9301-cd9a69e670fa/96467e4f-00f2-4d88-82b1-142a9b24784c.jpg
This commit is contained in:
ghaddaditw
2025-05-30 17:36:12 +00:00
parent ecbdd49cea
commit 8fea27bbbb
6 changed files with 829 additions and 4 deletions
+225
View File
@@ -0,0 +1,225 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Star, MapPin, Clock, Phone, Mail, Calendar } from "lucide-react";
import { Link } from "wouter";
interface Professional {
id: number;
firstName: string;
lastName: string;
businessName: string;
professionalType: string;
bio: string;
specializations: string[];
address: string;
phoneNumber: string;
email: string;
profileImageUrl: string;
averageRating: number;
totalReviews: number;
allowAppointmentBooking: 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,
});
const professionalTypes = [
"doctor", "dentist", "therapist", "consultant", "lawyer",
"accountant", "coach", "tutor", "other"
];
const filteredProfessionals = professionals.filter((prof: Professional) =>
prof.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
prof.lastName.toLowerCase().includes(searchTerm.toLowerCase()) ||
prof.businessName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
prof.specializations?.some((spec: string) =>
spec.toLowerCase().includes(searchTerm.toLowerCase())
)
);
return (
<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">
<div className="container mx-auto px-4 py-8">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<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 */}
<div className="flex flex-col md:flex-row gap-4 mb-8">
<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>
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-gray-200 rounded-full"></div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded w-32"></div>
<div className="h-3 bg-gray-200 rounded w-24"></div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="h-3 bg-gray-200 rounded"></div>
<div className="h-3 bg-gray-200 rounded w-3/4"></div>
</div>
</CardContent>
</Card>
))}
</div>
) : filteredProfessionals.length === 0 ? (
<div className="text-center py-12">
<p className="text-lg text-muted-foreground">
No professionals found matching your criteria.
</p>
<p className="text-sm text-muted-foreground mt-2">
Try adjusting your search terms or filters.
</p>
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredProfessionals.map((professional: Professional) => (
<Card key={professional.id} className="hover:shadow-lg transition-shadow">
<CardHeader>
<div className="flex items-center space-x-3">
<Avatar className="w-12 h-12">
<AvatarImage src={professional.profileImageUrl} />
<AvatarFallback>
{professional.firstName[0]}{professional.lastName[0]}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<CardTitle className="text-lg">
{professional.firstName} {professional.lastName}
</CardTitle>
{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>
</CardHeader>
<CardContent className="space-y-4">
<Badge variant="secondary" className="capitalize">
{professional.professionalType}
</Badge>
{professional.bio && (
<p className="text-sm text-muted-foreground line-clamp-2">
{professional.bio}
</p>
)}
{professional.specializations && professional.specializations.length > 0 && (
<div className="flex flex-wrap gap-1">
{professional.specializations.slice(0, 3).map((spec, index) => (
<Badge key={index} variant="outline" className="text-xs">
{spec}
</Badge>
))}
{professional.specializations.length > 3 && (
<Badge variant="outline" className="text-xs">
+{professional.specializations.length - 3} more
</Badge>
)}
</div>
)}
<div className="space-y-2 text-sm">
{professional.address && (
<div className="flex items-center gap-2 text-muted-foreground">
<MapPin className="w-4 h-4" />
<span>{professional.address}</span>
</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 className="flex gap-2 pt-4">
<Button asChild className="flex-1" size="sm">
<Link href={`/professional/${professional.id}`}>
View Profile
</Link>
</Button>
{professional.allowAppointmentBooking && (
<Button variant="outline" size="sm" className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
Book
</Button>
)}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
</div>
</div>
);
}