Compare commits

..

2 Commits

4 changed files with 36 additions and 88 deletions

View File

@ -389,13 +389,13 @@ export default function AppointmentDetailPage() {
className={`p-4 rounded-xl border ${isDark ? "bg-gray-700/50 border-gray-600" : "bg-gray-50 border-gray-200"}`} className={`p-4 rounded-xl border ${isDark ? "bg-gray-700/50 border-gray-600" : "bg-gray-50 border-gray-200"}`}
> >
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div> <div>
<p className={`text-base font-semibold ${isDark ? "text-white" : "text-gray-900"}`}> <p className={`text-base font-semibold ${isDark ? "text-white" : "text-gray-900"}`}>
{match.day_name || "Unknown Day"} {match.day_name || "Unknown Day"}
</p> </p>
<p className={`text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}> <p className={`text-sm mt-1 ${isDark ? "text-gray-400" : "text-gray-500"}`}>
{formatShortDate(match.date || match.date_obj || "")} {formatShortDate(match.date || match.date_obj || "")}
</p> </p>
</div> </div>
</div> </div>
{match.available_slots && Array.isArray(match.available_slots) && match.available_slots.length > 0 && ( {match.available_slots && Array.isArray(match.available_slots) && match.available_slots.length > 0 && (
@ -408,19 +408,19 @@ export default function AppointmentDetailPage() {
}; };
const normalizedSlot = String(slot).toLowerCase().trim(); const normalizedSlot = String(slot).toLowerCase().trim();
return ( return (
<span <span
key={slotIdx} key={slotIdx}
className={`px-3 py-1.5 rounded-lg text-sm font-medium ${isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200"}`} className={`px-3 py-1.5 rounded-lg text-sm font-medium ${isDark ? "bg-green-500/20 text-green-300 border border-green-500/30" : "bg-green-50 text-green-700 border border-green-200"}`}
> >
{timeSlotLabels[normalizedSlot] || slot} {timeSlotLabels[normalizedSlot] || slot}
</span> </span>
); );
})} })}
</div> </div>
)} )}
</div> </div>
))} ))}
</div> </div>
</div> </div>
</div> </div>
)} )}

View File

@ -2,7 +2,7 @@
import { motion, useInView } from "framer-motion"; import { motion, useInView } from "framer-motion";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { Send, Loader2 } from "lucide-react"; import { Send } 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";
@ -29,20 +29,15 @@ export function ContactSection() {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await submitContactForm({ await submitContactForm(formData);
name: formData.name,
email: formData.email,
phone: formData.phone,
message: formData.message,
});
toast.success("Message Sent Successfully", { 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) { } catch (error) {
toast.error("Failed to Send Message", { const errorMessage = error instanceof Error ? error.message : "Failed to send message. Please try again.";
description: error instanceof Error ? error.message : "Please try again later.", toast.error("Error Sending Message", {
description: errorMessage,
}); });
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@ -227,17 +222,8 @@ export function ContactSection() {
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" 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 ? ( <Send className="mr-2 h-5 w-5" />
<> {isSubmitting ? "Sending..." : "Send Message"}
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Sending...
</>
) : (
<>
<Send className="mr-2 h-5 w-5" />
Send Message
</>
)}
</Button> </Button>
</form> </form>
</CardContent> </CardContent>

View File

@ -9,6 +9,7 @@ import type {
ResetPasswordInput, ResetPasswordInput,
TokenRefreshInput, TokenRefreshInput,
UpdateProfileInput, UpdateProfileInput,
ContactInput,
} from "@/lib/schema/auth"; } from "@/lib/schema/auth";
import type { AuthResponse, ApiError, AuthTokens, User } from "@/lib/models/auth"; import type { AuthResponse, ApiError, AuthTokens, User } from "@/lib/models/auth";
@ -439,67 +440,18 @@ 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 (public endpoint - no authentication required) * Submit contact form
*/ */
export async function submitContactForm( export async function submitContactForm(input: ContactInput): Promise<{ message: string }> {
data: ContactFormInput const response = await fetch(API_ENDPOINTS.auth.contact, {
): Promise<ContactFormResponse> { method: "POST",
try { headers: {
const response = await fetch(API_ENDPOINTS.auth.contact, { "Content-Type": "application/json",
method: "POST", },
headers: { body: JSON.stringify(input),
"Content-Type": "application/json", });
"Accept": "application/json",
},
body: JSON.stringify({
name: data.name.trim(),
email: data.email.trim().toLowerCase(),
phone: data.phone.trim(),
message: data.message.trim(),
}),
});
// Handle empty responses return handleResponse<{ message: string }>(response);
const contentType = response.headers.get("content-type");
let responseData: any;
if (contentType && contentType.includes("application/json")) {
const text = await response.text();
responseData = text ? JSON.parse(text) : {};
} else {
const text = await response.text();
responseData = text ? { message: text } : {};
}
if (!response.ok) {
// Check for authentication error specifically
if (response.status === 401 || response.status === 403) {
throw new Error("Contact form submission requires authentication. Please contact support if this is a public form.");
}
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. Please try again later.");
}
} }

View File

@ -87,3 +87,13 @@ export const updateProfileSchema = z.object({
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>; export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;
// Contact Form Schema
export const contactSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
phone: z.string().min(1, "Phone number is required"),
message: z.string().min(1, "Message is required"),
});
export type ContactInput = z.infer<typeof contactSchema>;