Add interactive onboarding process and update professional directory
Implements onboarding page, fixes login redirect, adds mock data to professional directory, and includes a screenshot asset. 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/6009c02a-0a60-49de-bce6-3126cf528401.jpg
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
+2
-1
@@ -11,7 +11,7 @@ import LandingPage from "@/pages/LandingPage";
|
||||
import DashboardPage from "@/pages/DashboardPage";
|
||||
import LoginPage from "@/pages/LoginPage";
|
||||
import ProfessionalDirectory from "@/pages/ProfessionalDirectory";
|
||||
// import OnboardingPage from "@/pages/OnboardingPage";
|
||||
import OnboardingPage from "@/pages/OnboardingPage";
|
||||
import NotFound from "@/pages/not-found";
|
||||
|
||||
function Router() {
|
||||
@@ -20,6 +20,7 @@ function Router() {
|
||||
<Route path="/" component={LandingPage} />
|
||||
<Route path="/login" component={LoginPage} />
|
||||
<Route path="/register" component={LoginPage} />
|
||||
<Route path="/onboarding" component={OnboardingPage} />
|
||||
<Route path="/dashboard" component={DashboardPage} />
|
||||
<Route path="/professionals" component={ProfessionalDirectory} />
|
||||
<Route component={NotFound} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
@@ -35,10 +35,11 @@ export default function LoginPage() {
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// Redirect if already authenticated
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setLocation("/dashboard");
|
||||
return null;
|
||||
}
|
||||
}, [user, setLocation]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useLocation } from "wouter";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
CheckCircle,
|
||||
ArrowRight,
|
||||
User,
|
||||
Briefcase,
|
||||
Calendar,
|
||||
Clock,
|
||||
MapPin,
|
||||
Phone,
|
||||
Users
|
||||
} from "lucide-react";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const { user, updateProfile } = useAuth();
|
||||
const [, setLocation] = useLocation();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [formData, setFormData] = useState({
|
||||
// Professional profile fields
|
||||
businessName: "",
|
||||
bio: "",
|
||||
specializations: "",
|
||||
address: "",
|
||||
phoneNumber: "",
|
||||
allowAppointmentBooking: false,
|
||||
|
||||
// Availability settings
|
||||
workingHours: {
|
||||
monday: { enabled: true, start: "09:00", end: "17:00" },
|
||||
tuesday: { enabled: true, start: "09:00", end: "17:00" },
|
||||
wednesday: { enabled: true, start: "09:00", end: "17:00" },
|
||||
thursday: { enabled: true, start: "09:00", end: "17:00" },
|
||||
friday: { enabled: true, start: "09:00", end: "17:00" },
|
||||
saturday: { enabled: false, start: "09:00", end: "17:00" },
|
||||
sunday: { enabled: false, start: "09:00", end: "17:00" },
|
||||
}
|
||||
});
|
||||
|
||||
const totalSteps = user?.role === "pro" ? 3 : 2;
|
||||
const progress = (currentStep / totalSteps) * 100;
|
||||
|
||||
const handleInputChange = (field: string, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < totalSteps) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
await updateProfile({
|
||||
onboardingComplete: true,
|
||||
businessName: formData.businessName || null,
|
||||
bio: formData.bio || null,
|
||||
specializations: formData.specializations ? formData.specializations.split(',').map(s => s.trim()).join(',') : null,
|
||||
address: formData.address || null,
|
||||
phoneNumber: formData.phoneNumber || null,
|
||||
allowAppointmentBooking: formData.allowAppointmentBooking,
|
||||
});
|
||||
setLocation("/dashboard");
|
||||
} catch (error) {
|
||||
console.error("Failed to complete onboarding:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderStep1 = () => (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-primary to-secondary rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<User className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-2">Welcome to TaskFin AI Suite!</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Let's set up your account to get the most out of your experience.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Overview</CardTitle>
|
||||
<CardDescription>Your account details and features</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">Account Type</span>
|
||||
<Badge variant={user?.role === "pro" ? "default" : "secondary"} className="capitalize">
|
||||
{user?.role}
|
||||
</Badge>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Available Features:</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm">Task Management</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm">Financial Tracking</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm">Voice Commands (Arabic/English)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm">AI Insights</span>
|
||||
</div>
|
||||
{user?.role === "pro" && (
|
||||
<>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm">Professional Profiles</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm">Appointment Booking</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderStep2 = () => (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Briefcase className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-2">Professional Profile</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Set up your professional information for better networking and discovery.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Business Information</CardTitle>
|
||||
<CardDescription>Help others find and connect with you</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="businessName">Business Name (Optional)</Label>
|
||||
<Input
|
||||
id="businessName"
|
||||
placeholder="Your business or practice name"
|
||||
value={formData.businessName}
|
||||
onChange={(e) => handleInputChange("businessName", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bio">Professional Bio</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
placeholder="Tell others about your expertise and services..."
|
||||
value={formData.bio}
|
||||
onChange={(e) => handleInputChange("bio", e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="specializations">Specializations</Label>
|
||||
<Input
|
||||
id="specializations"
|
||||
placeholder="e.g., Cardiology, Tax Planning, Project Management (comma-separated)"
|
||||
value={formData.specializations}
|
||||
onChange={(e) => handleInputChange("specializations", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address">Address</Label>
|
||||
<Input
|
||||
id="address"
|
||||
placeholder="Your business address"
|
||||
value={formData.address}
|
||||
onChange={(e) => handleInputChange("address", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phoneNumber">Phone Number</Label>
|
||||
<Input
|
||||
id="phoneNumber"
|
||||
placeholder="+968 XXXX XXXX"
|
||||
value={formData.phoneNumber}
|
||||
onChange={(e) => handleInputChange("phoneNumber", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{user?.role === "pro" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Allow Appointment Booking</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Let clients book appointments with you online
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={formData.allowAppointmentBooking}
|
||||
onCheckedChange={(checked) => handleInputChange("allowAppointmentBooking", checked)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderStep3 = () => (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Calendar className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-2">Availability Settings</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Set your working hours for appointment booking.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Working Hours</CardTitle>
|
||||
<CardDescription>Configure when clients can book appointments</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{Object.entries(formData.workingHours).map(([day, settings]) => (
|
||||
<div key={day} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Switch
|
||||
checked={settings.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
handleInputChange("workingHours", {
|
||||
...formData.workingHours,
|
||||
[day]: { ...settings, enabled: checked }
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="font-medium capitalize">{day}</span>
|
||||
</div>
|
||||
{settings.enabled && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
type="time"
|
||||
value={settings.start}
|
||||
onChange={(e) =>
|
||||
handleInputChange("workingHours", {
|
||||
...formData.workingHours,
|
||||
[day]: { ...settings, start: e.target.value }
|
||||
})
|
||||
}
|
||||
className="w-24"
|
||||
/>
|
||||
<span className="text-muted-foreground">to</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={settings.end}
|
||||
onChange={(e) =>
|
||||
handleInputChange("workingHours", {
|
||||
...formData.workingHours,
|
||||
[day]: { ...settings, end: e.target.value }
|
||||
})
|
||||
}
|
||||
className="w-24"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
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-2xl mx-auto">
|
||||
{/* Progress */}
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between text-sm text-muted-foreground mb-2">
|
||||
<span>Step {currentStep} of {totalSteps}</span>
|
||||
<span>{Math.round(progress)}% complete</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{currentStep === 1 && renderStep1()}
|
||||
{currentStep === 2 && renderStep2()}
|
||||
{currentStep === 3 && user?.role === "pro" && renderStep3()}
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between mt-8">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleBack}
|
||||
disabled={currentStep === 1}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
{currentStep < totalSteps ? (
|
||||
<Button onClick={handleNext}>
|
||||
Next
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleComplete}>
|
||||
Complete Setup
|
||||
<CheckCircle className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,12 +34,50 @@ export default function ProfessionalDirectory() {
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
// Mock data for now since the API endpoint doesn't exist yet
|
||||
const mockProfessionals: Professional[] = [
|
||||
{
|
||||
id: 1,
|
||||
firstName: "Dr. Ahmed",
|
||||
lastName: "Al-Rashid",
|
||||
businessName: "Muscat Medical Center",
|
||||
professionalType: "doctor",
|
||||
bio: "Experienced cardiologist with 15+ years of practice in Oman",
|
||||
specializations: ["Cardiology", "Internal Medicine", "Preventive Care"],
|
||||
address: "Al Khuwair, Muscat, Oman",
|
||||
phoneNumber: "+968 2234 5678",
|
||||
email: "ahmed.rashid@medical.om",
|
||||
profileImageUrl: "",
|
||||
averageRating: 4.8,
|
||||
totalReviews: 127,
|
||||
allowAppointmentBooking: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
firstName: "Fatima",
|
||||
lastName: "Al-Zahra",
|
||||
businessName: "Legal Consultancy Oman",
|
||||
professionalType: "lawyer",
|
||||
bio: "Corporate law specialist focusing on business formation and compliance",
|
||||
specializations: ["Corporate Law", "Business Formation", "Contract Law"],
|
||||
address: "Ruwi, Muscat, Oman",
|
||||
phoneNumber: "+968 2445 6789",
|
||||
email: "fatima.zahra@legal.om",
|
||||
profileImageUrl: "",
|
||||
averageRating: 4.9,
|
||||
totalReviews: 89,
|
||||
allowAppointmentBooking: true,
|
||||
},
|
||||
];
|
||||
|
||||
const displayProfessionals = Array.isArray(professionals) && professionals.length > 0 ? professionals as Professional[] : mockProfessionals;
|
||||
|
||||
const professionalTypes = [
|
||||
"doctor", "dentist", "therapist", "consultant", "lawyer",
|
||||
"accountant", "coach", "tutor", "other"
|
||||
];
|
||||
|
||||
const filteredProfessionals = professionals.filter((prof: Professional) =>
|
||||
const filteredProfessionals = displayProfessionals.filter((prof: Professional) =>
|
||||
prof.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prof.lastName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prof.businessName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
|
||||
Reference in New Issue
Block a user