Compare commits
2 Commits
412bae77fb
...
51ca53af88
| Author | SHA1 | Date | |
|---|---|---|---|
| 51ca53af88 | |||
|
|
5318522f37 |
@ -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>
|
||||||
|
|||||||
@ -240,7 +240,7 @@ export async function checkDateAvailability(date: string): Promise<CheckDateAvai
|
|||||||
const data = await parseResponse(response);
|
const data = await parseResponse(response);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
throw new Error(extractErrorMessage(data as unknown as ApiError));
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@ -357,7 +357,7 @@ export async function scheduleAppointment(id: string, input: ScheduleAppointment
|
|||||||
}
|
}
|
||||||
|
|
||||||
return data.appointment || data;
|
return data.appointment || data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rejectAppointment(id: string, input: RejectAppointmentInput): Promise<Appointment> {
|
export async function rejectAppointment(id: string, input: RejectAppointmentInput): Promise<Appointment> {
|
||||||
const tokens = getStoredTokens();
|
const tokens = getStoredTokens();
|
||||||
|
|||||||
@ -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