Compare commits
2 Commits
e72c7e105a
...
96ec7f8c2e
| Author | SHA1 | Date | |
|---|---|---|---|
| 96ec7f8c2e | |||
|
|
d51a795191 |
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user