Refactor Booking and AppointmentDetail components to enhance rendering of preferred dates and time slots. Improve UI consistency by updating conditional rendering logic and ensuring proper handling of availability states. Additionally, streamline the C… #34
@ -367,76 +367,13 @@ export default function AppointmentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Preferred Dates & Times */}
|
|
||||||
{((appointment.preferred_dates && appointment.preferred_dates.length > 0) || (appointment.preferred_time_slots && appointment.preferred_time_slots.length > 0)) && (
|
|
||||||
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
|
||||||
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
|
||||||
<h2 className={`text-lg font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
|
|
||||||
Preferred Availability
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{appointment.preferred_dates && (
|
|
||||||
<div>
|
|
||||||
<p className={`text-sm font-medium mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
|
||||||
Preferred Dates
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{Array.isArray(appointment.preferred_dates) ? (
|
|
||||||
(appointment.preferred_dates as string[]).map((date, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium ${isDark ? "bg-gray-700 text-gray-200 border border-gray-600" : "bg-gray-100 text-gray-700 border border-gray-200"}`}
|
|
||||||
>
|
|
||||||
{formatShortDate(date)}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium ${isDark ? "bg-gray-700 text-gray-200 border border-gray-600" : "bg-gray-100 text-gray-700 border border-gray-200"}`}
|
|
||||||
>
|
|
||||||
{appointment.preferred_dates_display || appointment.preferred_dates || 'N/A'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{appointment.preferred_time_slots && (
|
|
||||||
<div>
|
|
||||||
<p className={`text-sm font-medium mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
|
||||||
Preferred Time Slots
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{Array.isArray(appointment.preferred_time_slots) ? (
|
|
||||||
(appointment.preferred_time_slots as string[]).map((slot, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium capitalize ${isDark ? "bg-rose-500/20 text-rose-300 border border-rose-500/30" : "bg-rose-50 text-rose-700 border border-rose-200"}`}
|
|
||||||
>
|
|
||||||
{slot}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium capitalize ${isDark ? "bg-rose-500/20 text-rose-300 border border-rose-500/30" : "bg-rose-50 text-rose-700 border border-rose-200"}`}
|
|
||||||
>
|
|
||||||
{appointment.preferred_time_slots_display || appointment.preferred_time_slots || 'N/A'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Matching Availability */}
|
{/* Matching Availability */}
|
||||||
{appointment.matching_availability && Array.isArray(appointment.matching_availability) && appointment.matching_availability.length > 0 && (
|
{appointment.matching_availability && Array.isArray(appointment.matching_availability) && appointment.matching_availability.length > 0 && (
|
||||||
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||||
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||||
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
|
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
<CalendarCheck className={`w-5 h-5 ${isDark ? "text-green-400" : "text-green-600"}`} />
|
<CalendarCheck className={`w-5 h-5 ${isDark ? "text-green-400" : "text-green-600"}`} />
|
||||||
Matching Availability
|
Preferred Availability
|
||||||
{appointment.are_preferences_available !== undefined && (
|
{appointment.are_preferences_available !== undefined && (
|
||||||
<span className={`ml-auto px-3 py-1 text-xs font-medium rounded-full ${appointment.are_preferences_available ? (isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200") : (isDark ? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30" : "bg-yellow-50 text-yellow-700 border border-yellow-200")}`}>
|
<span className={`ml-auto px-3 py-1 text-xs font-medium rounded-full ${appointment.are_preferences_available ? (isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200") : (isDark ? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30" : "bg-yellow-50 text-yellow-700 border border-yellow-200")}`}>
|
||||||
{appointment.are_preferences_available ? "Available" : "Partially Available"}
|
{appointment.are_preferences_available ? "Available" : "Partially Available"}
|
||||||
|
|||||||
@ -51,7 +51,47 @@ Your Attune Heart Therapy platform includes a comprehensive system for managing
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔗 Quick Access Links
|
## 🎥 Telehealth Sessions Guide
|
||||||
|
|
||||||
|
This section provides step-by-step guidance on how to access and manage telehealth therapy sessions through the Admin Dashboard of the Attune Heart Therapy platform.
|
||||||
|
|
||||||
|
### Accessing the Admin Dashboard
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. Navigate to: \`https://attunehearttherapy.com/login\`
|
||||||
|
2. Enter your admin email address: \`admin@attunehearttherapy.com\`
|
||||||
|
3. Enter your password
|
||||||
|
4. Click **"Sign In"**
|
||||||
|
5. You will be automatically redirected to the **Admin Dashboard**
|
||||||
|
6. Click on **"Bookings"** in the navigation menu to view all appointments
|
||||||
|
|
||||||
|
### Viewing Appointment Details
|
||||||
|
|
||||||
|
1. From the Bookings page, click on any appointment
|
||||||
|
2. View complete client information, preferred dates, and time slots
|
||||||
|
3. Check appointment status and review all details
|
||||||
|
|
||||||
|
### Scheduling an Appointment
|
||||||
|
|
||||||
|
1. From the appointment details page, click **"Schedule Appointment"**
|
||||||
|
2. Select the date and time for the session
|
||||||
|
3. Choose the duration (15, 30, 45, 60, or 120 minutes)
|
||||||
|
4. Click **"Schedule"** to confirm
|
||||||
|
5. The system will automatically create a Jitsi video meeting room, generate a unique meeting URL, send a confirmation email to the client, and update the appointment status to "Scheduled"
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Joining a Video Meeting
|
||||||
|
|
||||||
|
1. Meetings become available **10 minutes before** the scheduled start time
|
||||||
|
2. Navigate to any scheduled appointment's details page
|
||||||
|
3. In the sidebar, find the **"Join Meeting"** button
|
||||||
|
4. Click **"Join Meeting"** when it becomes active (10 minutes before scheduled time)
|
||||||
|
5. The meeting will open in a new browser tab
|
||||||
|
6. Both you and the client use the same link to join the session
|
||||||
|
|
||||||
|
### 🔗 Quick Access Links
|
||||||
|
|
||||||
[Visit Attune Heart Therapy](https://attunehearttherapy.com/) - Official website
|
[Visit Attune Heart Therapy](https://attunehearttherapy.com/) - Official website
|
||||||
|
|
||||||
@ -290,17 +330,32 @@ For technical assistance, questions, or issues:
|
|||||||
</code>
|
</code>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
img: ({ node, ...props }: any) => (
|
||||||
|
<img
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
height: "auto",
|
||||||
|
borderRadius: "8px",
|
||||||
|
marginTop: "1em",
|
||||||
|
marginBottom: "1em",
|
||||||
|
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
|
||||||
|
}}
|
||||||
|
alt={props.alt || "Guide screenshot"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||||
<div className="bg-white p-8 rounded-lg shadow-md">
|
<div className="bg-white dark:bg-gray-900 p-8 rounded-lg shadow-md">
|
||||||
<Button
|
<Button
|
||||||
className="bg-gray-100 hover:bg-gray-50 shadow-md text-black"
|
className="bg-gray-100 hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 shadow-md text-black dark:text-white mb-6"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="mr-2" />
|
<ArrowLeft className="mr-2" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
|
<ReactMarkdown components={components} remarkPlugins={[remarkGfm]}>
|
||||||
{readmeContent}
|
{readmeContent}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
|||||||
@ -306,85 +306,13 @@ export default function UserAppointmentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Preferred Dates & Times */}
|
|
||||||
{((appointment.preferred_dates && (Array.isArray(appointment.preferred_dates) ? appointment.preferred_dates.length > 0 : appointment.preferred_dates)) ||
|
|
||||||
(appointment.preferred_time_slots && (Array.isArray(appointment.preferred_time_slots) ? appointment.preferred_time_slots.length > 0 : appointment.preferred_time_slots))) && (
|
|
||||||
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
|
||||||
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
|
||||||
<h2 className={`text-lg font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
|
|
||||||
Preferred Availability
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-6">
|
|
||||||
{appointment.preferred_dates && (
|
|
||||||
<div>
|
|
||||||
<p className={`text-sm font-medium mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
|
||||||
Preferred Dates
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{Array.isArray(appointment.preferred_dates) ? (
|
|
||||||
(appointment.preferred_dates as string[]).map((date, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium ${isDark ? "bg-gray-700 text-gray-200 border border-gray-600" : "bg-gray-100 text-gray-700 border border-gray-200"}`}
|
|
||||||
>
|
|
||||||
{formatShortDate(date)}
|
|
||||||
</span>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium ${isDark ? "bg-gray-700 text-gray-200 border border-gray-600" : "bg-gray-100 text-gray-700 border border-gray-200"}`}
|
|
||||||
>
|
|
||||||
{appointment.preferred_dates_display || appointment.preferred_dates || 'N/A'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{appointment.preferred_time_slots && (
|
|
||||||
<div>
|
|
||||||
<p className={`text-sm font-medium mb-3 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
|
|
||||||
Preferred Time Slots
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{Array.isArray(appointment.preferred_time_slots) ? (
|
|
||||||
(appointment.preferred_time_slots as string[]).map((slot, idx) => {
|
|
||||||
const timeSlotLabels: Record<string, string> = {
|
|
||||||
morning: "Morning",
|
|
||||||
afternoon: "Lunchtime",
|
|
||||||
evening: "Evening",
|
|
||||||
};
|
|
||||||
const normalizedSlot = String(slot).toLowerCase().trim();
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium capitalize ${isDark ? "bg-rose-500/20 text-rose-300 border border-rose-500/30" : "bg-rose-50 text-rose-700 border border-rose-200"}`}
|
|
||||||
>
|
|
||||||
{timeSlotLabels[normalizedSlot] || slot}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium capitalize ${isDark ? "bg-rose-500/20 text-rose-300 border border-rose-500/30" : "bg-rose-50 text-rose-700 border border-rose-200"}`}
|
|
||||||
>
|
|
||||||
{appointment.preferred_time_slots_display || appointment.preferred_time_slots || 'N/A'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Matching Availability */}
|
{/* Matching Availability */}
|
||||||
{appointment.matching_availability && Array.isArray(appointment.matching_availability) && appointment.matching_availability.length > 0 && (
|
{appointment.matching_availability && Array.isArray(appointment.matching_availability) && appointment.matching_availability.length > 0 && (
|
||||||
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
<div className={`rounded-2xl border shadow-sm overflow-hidden ${isDark ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}>
|
||||||
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
<div className={`px-6 py-4 border-b ${isDark ? "border-gray-700 bg-gray-800/50" : "border-gray-200 bg-gray-50/50"}`}>
|
||||||
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
|
<h2 className={`text-lg font-semibold flex items-center gap-2 ${isDark ? "text-white" : "text-gray-900"}`}>
|
||||||
<CalendarCheck className={`w-5 h-5 ${isDark ? "text-green-400" : "text-green-600"}`} />
|
<CalendarCheck className={`w-5 h-5 ${isDark ? "text-green-400" : "text-green-600"}`} />
|
||||||
Matching Availability
|
Preferred Availability
|
||||||
{appointment.are_preferences_available !== undefined && (
|
{appointment.are_preferences_available !== undefined && (
|
||||||
<span className={`ml-auto px-3 py-1 text-xs font-medium rounded-full ${appointment.are_preferences_available ? (isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200") : (isDark ? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30" : "bg-yellow-50 text-yellow-700 border border-yellow-200")}`}>
|
<span className={`ml-auto px-3 py-1 text-xs font-medium rounded-full ${appointment.are_preferences_available ? (isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200") : (isDark ? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30" : "bg-yellow-50 text-yellow-700 border border-yellow-200")}`}>
|
||||||
{appointment.are_preferences_available ? "Available" : "Partially Available"}
|
{appointment.are_preferences_available ? "Available" : "Partially Available"}
|
||||||
|
|||||||
@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
import { motion, useInView } from "framer-motion";
|
import { motion, useInView } from "framer-motion";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { Send } from "lucide-react";
|
import { Send, Loader2 } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useAppTheme } from "@/components/ThemeProvider";
|
import { useAppTheme } from "@/components/ThemeProvider";
|
||||||
|
import { submitContactForm } from "@/lib/actions/auth";
|
||||||
|
|
||||||
export function ContactSection() {
|
export function ContactSection() {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
@ -21,13 +22,31 @@ export function ContactSection() {
|
|||||||
phone: "",
|
phone: "",
|
||||||
message: "",
|
message: "",
|
||||||
});
|
});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toast("Message Received", {
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await submitContactForm({
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email,
|
||||||
|
phone: formData.phone,
|
||||||
|
message: formData.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Message Sent Successfully", {
|
||||||
description: "Thank you for reaching out. We'll get back to you soon!",
|
description: "Thank you for reaching out. We'll get back to you soon!",
|
||||||
});
|
});
|
||||||
setFormData({ name: "", email: "", phone: "", message: "" });
|
setFormData({ name: "", email: "", phone: "", message: "" });
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to Send Message", {
|
||||||
|
description: error instanceof Error ? error.message : "Please try again later.",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -204,11 +223,21 @@ export function ContactSection() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full cursor-pointer bg-gradient-to-r from-rose-500 to-pink-600 text-white transition-all hover:from-rose-600 hover:to-pink-700 hover:scale-[1.02]"
|
disabled={isSubmitting}
|
||||||
|
className="w-full cursor-pointer bg-gradient-to-r from-rose-500 to-pink-600 text-white transition-all hover:from-rose-600 hover:to-pink-700 hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||||
|
Sending...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Send className="mr-2 h-5 w-5" />
|
<Send className="mr-2 h-5 w-5" />
|
||||||
Send Message
|
Send Message
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -439,3 +439,51 @@ export async function updateProfile(input: UpdateProfileInput): Promise<User> {
|
|||||||
throw new Error("Invalid profile response format");
|
throw new Error("Invalid profile response format");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContactFormInput {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContactFormResponse {
|
||||||
|
message?: string;
|
||||||
|
success?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit contact form
|
||||||
|
*/
|
||||||
|
export async function submitContactForm(
|
||||||
|
data: ContactFormInput
|
||||||
|
): Promise<ContactFormResponse> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_ENDPOINTS.auth.contact, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
phone: data.phone,
|
||||||
|
message: data.message,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error: ApiError = responseData;
|
||||||
|
throw new Error(extractErrorMessage(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error("Failed to submit contact form");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export const API_ENDPOINTS = {
|
|||||||
allUsers: `${API_BASE_URL}/auth/all-users/`,
|
allUsers: `${API_BASE_URL}/auth/all-users/`,
|
||||||
getProfile: `${API_BASE_URL}/auth/profile/`,
|
getProfile: `${API_BASE_URL}/auth/profile/`,
|
||||||
updateProfile: `${API_BASE_URL}/auth/profile/update/`,
|
updateProfile: `${API_BASE_URL}/auth/profile/update/`,
|
||||||
|
contact: `${API_BASE_URL}/auth/contact/`,
|
||||||
},
|
},
|
||||||
meetings: {
|
meetings: {
|
||||||
base: `${API_BASE_URL}/meetings/`,
|
base: `${API_BASE_URL}/meetings/`,
|
||||||
|
|||||||
BIN
public/ss1.png
Normal file
BIN
public/ss1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 718 KiB |
BIN
public/ss2.png
Normal file
BIN
public/ss2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 627 KiB |
BIN
public/ss3.png
Normal file
BIN
public/ss3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 614 KiB |
Loading…
Reference in New Issue
Block a user