mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 17:18:09 +00:00
displaying vacancy completed
This commit is contained in:
@ -244,12 +244,8 @@ const HeaderClient = ({
|
||||
</div>
|
||||
<div className="hover:text-opacity-80 transition-opacity">
|
||||
<DropdownMenu trigger={<span>Join Our Team</span>}>
|
||||
<DropdownLink href="/join-us/vacancies">
|
||||
Vacancies
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/join-us/portal">
|
||||
Recruitment Portal
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/vacancies">Vacancies</DropdownLink>
|
||||
<DropdownLink href="/portal">Recruitment Portal</DropdownLink>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@ -1,46 +1,81 @@
|
||||
// src/components/VacancyApplicationForm.tsx (Example structure and styling)
|
||||
"use client"; // This likely needs to be a client component for form handling
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, ChangeEvent, DragEvent } from "react";
|
||||
import { FaUpload, FaFileAlt, FaTimes } from "react-icons/fa"; // Import icons
|
||||
import { FaUpload, FaFileAlt, FaTimes } from "react-icons/fa";
|
||||
import { useForm, SubmitHandler } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import Button from "./ui/Button";
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const ACCEPTED_FILE_TYPES = [
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"text/plain",
|
||||
];
|
||||
|
||||
const applicationSchema = z.object({
|
||||
name: z.string().min(2, "Full name must be at least 2 characters"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
phone: z.string().optional(),
|
||||
linkedin: z.string().url("Invalid URL").optional().or(z.literal("")),
|
||||
coverLetter: z.string().optional(),
|
||||
resume: z
|
||||
.custom<File | null>((val) => val instanceof File, "Resume/CV is required")
|
||||
.refine(
|
||||
(file) => file && file.size <= MAX_FILE_SIZE,
|
||||
`Max file size is 5MB.`
|
||||
)
|
||||
.refine(
|
||||
(file) => file && ACCEPTED_FILE_TYPES.includes(file.type),
|
||||
"Unsupported file format. Please upload PDF, DOC, DOCX, or TXT."
|
||||
),
|
||||
});
|
||||
|
||||
type ApplicationFormData = z.infer<typeof applicationSchema>;
|
||||
|
||||
interface VacancyApplicationFormProps {
|
||||
vacancyId: string;
|
||||
vacancyTitle: string;
|
||||
// onFormClose?: () => void; // Optional: If you add a close button
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function VacancyApplicationForm({
|
||||
vacancyId,
|
||||
vacancyTitle,
|
||||
onClose,
|
||||
}: VacancyApplicationFormProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
linkedin: "",
|
||||
coverLetter: "",
|
||||
// Add other fields as needed
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors, isSubmitting },
|
||||
reset,
|
||||
} = useForm<ApplicationFormData>({
|
||||
resolver: zodResolver(applicationSchema),
|
||||
mode: "onChange",
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
linkedin: "",
|
||||
coverLetter: "",
|
||||
resume: null,
|
||||
},
|
||||
});
|
||||
const [resumeFile, setResumeFile] = useState<File | null>(null);
|
||||
|
||||
const resumeFile = watch("resume");
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<
|
||||
"idle" | "success" | "error"
|
||||
>("idle");
|
||||
|
||||
const handleInputChange = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
// --- File Input Handling ---
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setResumeFile(e.target.files[0]);
|
||||
e.target.value = ""; // Reset input value for potential re-upload
|
||||
setValue("resume", e.target.files[0], { shouldValidate: true });
|
||||
e.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
@ -58,161 +93,190 @@ export default function VacancyApplicationForm({
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault(); // Necessary to allow drop
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isDragging) setIsDragging(true); // Ensure state is true while over
|
||||
if (!isDragging) setIsDragging(true);
|
||||
},
|
||||
[isDragging]
|
||||
);
|
||||
|
||||
const handleDrop = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
setResumeFile(e.dataTransfer.files[0]);
|
||||
e.dataTransfer.clearData();
|
||||
}
|
||||
}, []);
|
||||
const handleDrop = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
setValue("resume", e.dataTransfer.files[0], { shouldValidate: true });
|
||||
e.dataTransfer.clearData();
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const removeFile = () => {
|
||||
setResumeFile(null);
|
||||
setValue("resume", null, { shouldValidate: true });
|
||||
};
|
||||
// --- End File Input Handling ---
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
const onSubmit: SubmitHandler<ApplicationFormData> = async (data) => {
|
||||
setSubmitStatus("idle");
|
||||
|
||||
const data = new FormData();
|
||||
data.append("vacancyId", vacancyId);
|
||||
data.append("vacancyTitle", vacancyTitle);
|
||||
data.append("name", formData.name);
|
||||
data.append("email", formData.email);
|
||||
data.append("phone", formData.phone);
|
||||
data.append("linkedin", formData.linkedin);
|
||||
data.append("coverLetter", formData.coverLetter);
|
||||
if (resumeFile) {
|
||||
data.append("resume", resumeFile, resumeFile.name);
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append("vacancyId", vacancyId);
|
||||
formData.append("vacancyTitle", vacancyTitle);
|
||||
formData.append("name", data.name);
|
||||
formData.append("email", data.email);
|
||||
if (data.phone) formData.append("phone", data.phone);
|
||||
if (data.linkedin) formData.append("linkedin", data.linkedin);
|
||||
if (data.coverLetter) formData.append("coverLetter", data.coverLetter);
|
||||
if (data.resume) formData.append("resume", data.resume, data.resume.name);
|
||||
|
||||
try {
|
||||
// Replace with your actual API endpoint for form submission
|
||||
const response = await fetch("/api/apply", {
|
||||
method: "POST",
|
||||
body: data, // FormData handles multipart/form-data automatically
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Submission failed");
|
||||
}
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(`Submission failed: ${response.statusText}`);
|
||||
setSubmitStatus("success");
|
||||
// Optionally reset form:
|
||||
// setFormData({ name: '', email: '', ... });
|
||||
// setResumeFile(null);
|
||||
// Optionally close form: onFormClose?.();
|
||||
reset();
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
setSubmitStatus("idle");
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error("Application submission error:", error);
|
||||
console.error(error);
|
||||
setSubmitStatus("error");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Base Input Styling ---
|
||||
const inputBaseStyle =
|
||||
"block w-full px-4 py-2 mt-1 text-gray-700 bg-white border border-gray-300 rounded-md focus:border-gold-500 focus:ring focus:ring-gold-500 focus:ring-opacity-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:border-gold-500 font-poppins";
|
||||
const errorStyle = "border-red-500 focus:border-red-500 focus:ring-red-500";
|
||||
const errorMessageStyle = "text-red-600 text-xs mt-1";
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6 font-poppins">
|
||||
{/* Example Fields */}
|
||||
<div>
|
||||
<label htmlFor="name" className="text-gray-700 dark:text-gray-200">
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
className={inputBaseStyle}
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="text-gray-700 dark:text-gray-200">
|
||||
Email Address *
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className={inputBaseStyle}
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="phone" className="text-gray-700 dark:text-gray-200">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
className={inputBaseStyle}
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="linkedin" className="text-gray-700 dark:text-gray-200">
|
||||
LinkedIn Profile URL
|
||||
</label>
|
||||
<input
|
||||
id="linkedin"
|
||||
name="linkedin"
|
||||
type="url"
|
||||
className={inputBaseStyle}
|
||||
value={formData.linkedin}
|
||||
onChange={handleInputChange}
|
||||
placeholder="https://linkedin.com/in/yourprofile"
|
||||
/>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 font-poppins">
|
||||
{/* Row 1: Name & Email */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Name Field */}
|
||||
<div>
|
||||
<label htmlFor="name" className="text-gray-700 dark:text-gray-200">
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
{...register("name")}
|
||||
className={`${inputBaseStyle} ${errors.name ? errorStyle : ""}`}
|
||||
aria-invalid={errors.name ? "true" : "false"}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label htmlFor="email" className="text-gray-700 dark:text-gray-200">
|
||||
Email Address *
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register("email")}
|
||||
className={`${inputBaseStyle} ${errors.email ? errorStyle : ""}`}
|
||||
aria-invalid={errors.email ? "true" : "false"}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- Styled File Upload Area --- */}
|
||||
{/* Row 2: Phone & LinkedIn */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Phone Field */}
|
||||
<div>
|
||||
<label htmlFor="phone" className="text-gray-700 dark:text-gray-200">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
type="tel"
|
||||
{...register("phone")}
|
||||
className={`${inputBaseStyle} ${errors.phone ? errorStyle : ""}`}
|
||||
aria-invalid={errors.phone ? "true" : "false"}
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.phone.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* LinkedIn Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="linkedin"
|
||||
className="text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
LinkedIn Profile URL
|
||||
</label>
|
||||
<input
|
||||
id="linkedin"
|
||||
type="url"
|
||||
{...register("linkedin")}
|
||||
className={`${inputBaseStyle} ${errors.linkedin ? errorStyle : ""}`}
|
||||
placeholder="https://linkedin.com/in/yourprofile"
|
||||
aria-invalid={errors.linkedin ? "true" : "false"}
|
||||
/>
|
||||
{errors.linkedin && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.linkedin.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resume/CV Upload */}
|
||||
<div>
|
||||
<label className="text-gray-700 dark:text-gray-200 block mb-2">
|
||||
Resume/CV *
|
||||
</label>
|
||||
<input
|
||||
id="resume-upload-hidden"
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
accept=".pdf,.doc,.docx,.txt"
|
||||
className="hidden"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
className={`relative flex flex-col items-center justify-center w-full p-6 transition-colors duration-300 ease-in-out border-2 border-dashed rounded-lg cursor-pointer hover:border-gold-500/80 ${
|
||||
isDragging
|
||||
? "border-gold-500 bg-gold-50"
|
||||
: errors.resume
|
||||
? "border-red-500"
|
||||
: "border-gray-300 hover:bg-gray-50"
|
||||
}`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => document.getElementById("resume-upload")?.click()} // Trigger hidden input
|
||||
onClick={() =>
|
||||
document.getElementById("resume-upload-hidden")?.click()
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ")
|
||||
document.getElementById("resume-upload-hidden")?.click();
|
||||
}}
|
||||
aria-label="Upload Resume/CV"
|
||||
>
|
||||
{/* Hidden Actual Input */}
|
||||
<input
|
||||
id="resume-upload"
|
||||
name="resume"
|
||||
type="file"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
onChange={handleFileChange}
|
||||
accept=".pdf,.doc,.docx,.txt" // Specify accepted file types
|
||||
aria-hidden="true" // Hide from accessibility tree as visual element handles interaction
|
||||
/>
|
||||
|
||||
{/* Visual Cue */}
|
||||
<FaUpload
|
||||
className={`w-8 h-8 mb-3 ${
|
||||
isDragging ? "text-gold-600" : "text-gray-400"
|
||||
@ -229,10 +293,15 @@ export default function VacancyApplicationForm({
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
or click to browse
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">(PDF, DOC, DOCX, TXT)</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
(PDF, DOC, DOCX, TXT - Max 5MB)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Display Selected File */}
|
||||
{errors.resume && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{(errors.resume as unknown as { message: string }).message}
|
||||
</p>
|
||||
)}
|
||||
{resumeFile && (
|
||||
<div className="mt-3 flex items-center justify-between px-3 py-2 text-sm text-gray-700 bg-gray-100 border border-gray-200 rounded-md">
|
||||
<div className="flex items-center gap-2">
|
||||
@ -255,8 +324,8 @@ export default function VacancyApplicationForm({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* --- End File Upload Area --- */}
|
||||
|
||||
{/* Cover Letter Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="coverLetter"
|
||||
@ -266,34 +335,37 @@ export default function VacancyApplicationForm({
|
||||
</label>
|
||||
<textarea
|
||||
id="coverLetter"
|
||||
name="coverLetter"
|
||||
rows={4}
|
||||
className={`${inputBaseStyle} resize-vertical`}
|
||||
value={formData.coverLetter}
|
||||
onChange={handleInputChange}
|
||||
{...register("coverLetter")} // Register with RHF
|
||||
className={`${inputBaseStyle} resize-vertical ${
|
||||
errors.coverLetter ? errorStyle : ""
|
||||
}`}
|
||||
aria-invalid={errors.coverLetter ? "true" : "false"}
|
||||
></textarea>
|
||||
{errors.coverLetter && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.coverLetter.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button & Status */}
|
||||
<div className="flex flex-col items-center space-y-3 pt-4">
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={
|
||||
isSubmitting || !formData.name || !formData.email || !resumeFile
|
||||
} // Basic validation example
|
||||
className="w-full md:w-auto px-8 py-3 font-bold text-white bg-gray-800 rounded-md hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-300"
|
||||
disabled={isSubmitting || submitStatus === "success"} // Disable while submitting or on success
|
||||
>
|
||||
{isSubmitting ? "Submitting..." : "Submit Application"}
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
{submitStatus === "success" && (
|
||||
<p className="text-sm text-green-600">
|
||||
Application submitted successfully!
|
||||
Application submitted successfully! Closing form...
|
||||
</p>
|
||||
)}
|
||||
{submitStatus === "error" && (
|
||||
<p className="text-sm text-red-600">
|
||||
Submission failed. Please try again.
|
||||
Submission failed. Please check your details and try again.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
126
components/ui/Modal.tsx
Normal file
126
components/ui/Modal.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
// src/components/ui/Modal.tsx
|
||||
import React, { ReactNode, useEffect } from "react";
|
||||
import { FaTimes } from "react-icons/fa";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
children: ReactNode;
|
||||
size?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl"; // Optional size control
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
size = "2xl", // Default size
|
||||
}) => {
|
||||
// Close modal on Escape key press
|
||||
useEffect(() => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
}
|
||||
// Cleanup listener on component unmount or when modal closes
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Prevent scrolling on the body when the modal is open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
// Cleanup overflow style on component unmount
|
||||
return () => {
|
||||
document.body.style.overflow = "auto";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Map size prop to Tailwind max-width classes
|
||||
const sizeClasses = {
|
||||
sm: "max-w-sm",
|
||||
md: "max-w-md",
|
||||
lg: "max-w-lg",
|
||||
xl: "max-w-xl",
|
||||
"2xl": "max-w-2xl",
|
||||
"3xl": "max-w-3xl",
|
||||
"4xl": "max-w-4xl",
|
||||
"5xl": "max-w-5xl",
|
||||
};
|
||||
|
||||
return (
|
||||
// Backdrop
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60 backdrop-blur-sm p-4 transition-opacity duration-300 ease-in-out"
|
||||
onClick={onClose} // Close when clicking the backdrop
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={title ? "modal-title" : undefined}
|
||||
>
|
||||
{/* Modal Panel */}
|
||||
<div
|
||||
className={`relative w-full ${sizeClasses[size]} bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 ease-in-out transform scale-95 opacity-0 animate-modal-appear`}
|
||||
onClick={(e) => e.stopPropagation()} // Prevent clicks inside the modal from closing it
|
||||
>
|
||||
{/* Modal Header (Optional) */}
|
||||
{title && (
|
||||
<div className="flex items-center justify-between p-4 sm:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||
{title && (
|
||||
<h2
|
||||
id="modal-title"
|
||||
className="text-lg font-semibold text-gray-900 dark:text-white font-poppins"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="p-5 sm:p-6 space-y-4 max-h-[80vh] overflow-y-auto">
|
||||
{" "}
|
||||
{/* Added max-height and scroll */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add animation keyframes to your global CSS (e.g., src/app/globals.css) */}
|
||||
<style jsx global>{`
|
||||
@keyframes modal-appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.animate-modal-appear {
|
||||
animation: modal-appear 0.3s ease-out forwards;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
Reference in New Issue
Block a user