| 'use client'; |
|
|
| import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; |
| import { authFetch } from '@/lib/api'; |
| import { Candidate } from '@/types/candidate-table'; |
| import { Profile } from '@/types/profile'; |
| import { useQuery } from '@tanstack/react-query'; |
| import { Download } from 'lucide-react'; |
| import { Badge } from '../ui/badge'; |
|
|
| interface DetailDialogProps { |
| open?: boolean; |
| onOpenChange?: (open: boolean) => void; |
| candidate?: Candidate | null; |
| } |
|
|
| |
| const isValidValue = (value: any) => { |
| if (value === null || value === undefined) return false |
| if (typeof value === "string" && value.trim() === "") return false |
| if (typeof value === "number" && value === 0) return false |
| return true |
| } |
|
|
| const InfoItem = ({ label, value }: { label: string; value: React.ReactNode }) => { |
| if (!isValidValue(value)) return null |
|
|
| return ( |
| <div> |
| <h3 className="text-sm text-gray-500">{label}</h3> |
| <div className="text-base">{value}</div> |
| </div> |
| ) |
| } |
|
|
| export function DetailDialog({ open, onOpenChange, candidate }: DetailDialogProps) { |
|
|
| const fetchProfile = async (profileId: string): Promise<Profile> => { |
| const res = await authFetch(`/api/cv-profile/profile-detail?profile_id=${profileId}`) |
| if (!res.ok) throw new Error("Failed to fetch profile") |
| return res.json() |
| } |
|
|
| const { data, isLoading } = useQuery({ |
| queryKey: ["profile-detail", candidate?.profile_id], |
| queryFn: () => fetchProfile(candidate!.profile_id), |
| enabled: open && !!candidate?.profile_id, |
| staleTime: 0, |
| refetchOnWindowFocus: false, |
| }) |
|
|
| const handleDownload = () => { |
| if (!data?.url) return |
| window.open(data.url, '_blank') |
| } |
|
|
| return ( |
| <Dialog open={open} onOpenChange={onOpenChange}> |
| <DialogContent className="sm:max-w-2xl"> |
| |
| <DialogHeader> |
| <DialogTitle>Profile Candidate</DialogTitle> |
| </DialogHeader> |
| |
| {/* ===================== */} |
| {/* 🔹 LOADING SKELETON */} |
| {/* ===================== */} |
| {isLoading && ( |
| <div className="grid grid-cols-2 gap-4 animate-pulse"> |
| {Array.from({ length: 10 }).map((_, i) => ( |
| <div key={i}> |
| <div className="h-3 bg-gray-200 rounded w-24 mb-2" /> |
| <div className="h-4 bg-gray-200 rounded w-full" /> |
| </div> |
| ))} |
| </div> |
| )} |
| |
| {/* ===================== */} |
| {/* 🔹 CONTENT */} |
| {/* ===================== */} |
| {!isLoading && data && ( |
| <div className="grid grid-cols-2 gap-4"> |
| |
| <InfoItem label="Full Name" value={data.fullname} /> |
| <InfoItem label="Domicile" value={data.domicile} /> |
| |
| <InfoItem |
| label="Year of Experience" |
| value={ |
| isValidValue(data.yoe) |
| ? `${data.yoe} year${data.yoe !== 1 ? "s" : ""}` |
| : null |
| } |
| /> |
| |
| <InfoItem label="Filename" value={data.filename} /> |
| |
| {/* Education 1 */} |
| <InfoItem label="University 1" value={data.univ_edu_1} /> |
| <InfoItem label="Major 1" value={data.major_edu_1} /> |
| <InfoItem label="GPA 1" value={data.gpa_edu_1} /> |
| |
| {/* Education 2 */} |
| <InfoItem label="University 2" value={data.univ_edu_2} /> |
| <InfoItem label="Major 2" value={data.major_edu_2} /> |
| <InfoItem label="GPA 2" value={data.gpa_edu_2} /> |
| |
| {/* Education 3 */} |
| <InfoItem label="University 3" value={data.univ_edu_3} /> |
| <InfoItem label="Major 3" value={data.major_edu_3} /> |
| <InfoItem label="GPA 3" value={data.gpa_edu_3} /> |
| |
| {/* Arrays */} |
| {data.hardskills?.length > 0 && ( |
| <div className="col-span-2"> |
| <h3 className="text-sm text-gray-500 mb-1">Hardskills</h3> |
| <div className="flex flex-wrap gap-2"> |
| {data.hardskills.map((s) => ( |
| <Badge variant="outline" key={s}>{s}</Badge> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {data.softskills?.length > 0 && ( |
| <div className="col-span-2"> |
| <h3 className="text-sm text-gray-500 mb-1">Softskills</h3> |
| <div className="flex flex-wrap gap-2"> |
| {data.softskills.map((s) => ( |
| <Badge variant="outline" key={s}>{s}</Badge> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {data.certifications?.length > 0 && ( |
| <div className="col-span-2"> |
| <h3 className="text-sm text-gray-500 mb-1">Certifications</h3> |
| <div className="flex flex-wrap gap-2"> |
| {data.certifications.map((s) => ( |
| <Badge variant="outline" key={s}>{s}</Badge> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {data.business_domain?.length > 0 && ( |
| <div className="col-span-2"> |
| <h3 className="text-sm text-gray-500 mb-1">Business Domain</h3> |
| <div className="flex flex-wrap gap-2"> |
| {data.business_domain.map((s) => ( |
| <Badge variant="outline" key={s}>{s}</Badge> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| {/* Download */} |
| {isValidValue(data.url) && ( |
| <div> |
| <h3 className="text-sm text-gray-500">CV</h3> |
| <button |
| className="mt-1 flex items-center gap-1 text-sm text-blue-600 hover:underline" |
| onClick={handleDownload} |
| > |
| <Download className="size-4" /> |
| {data.filename} |
| </button> |
| </div> |
| )} |
| |
| </div> |
| )} |
| |
| </DialogContent> |
| </Dialog> |
| ); |
| } |