resume-extraction/components/extracted-values.tsx (338 lines of code) (raw):

import React, { useState } from 'react' import { EducationItem, ExperienceItem, ResumeValues, Skill as SkillInterface } from '@/lib/resume' import { Braces, ChevronDown, ChevronRight, X } from 'lucide-react' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism' interface ExtractedValuesProps { values: ResumeValues | undefined loading: boolean } const ExtractedValues: React.FC<ExtractedValuesProps> = ({ values, loading }: ExtractedValuesProps) => { const [showJSON, setShowJSON] = useState(false) const toggleShowJSON = () => { setShowJSON(!showJSON) } return ( <div className="relative h-full w-full"> <div className="absolute right-4 top-4 z-10" onClick={toggleShowJSON}> {showJSON ? ( <X size={20} className="cursor-pointer text-neutral-900 hover:text-neutral-700" /> ) : ( <Braces size={20} className="cursor-pointer text-neutral-900 hover:text-neutral-700" /> )} </div> <div className="bg-white p-8 text-stone-900 h-full overflow-y-scroll rounded-xl"> <div className={`text-sm font-mono overflow-x-scroll h-full overflow-y-scroll rounded-xl ${ showJSON ? 'h-full' : 'hidden' }`} > {values ? ( <SyntaxHighlighter language="json" style={coy} customStyle={{ borderRadius: '0.75rem', paddingTop: '16px', paddingBottom: '16px', marginTop: 0 }} > {JSON.stringify(values, null, 2)} </SyntaxHighlighter> ) : ( <div className="text-gray-500 text-center mt-6"> Upload a resume to see the extracted JSON. </div> )} </div> <div className={` ${showJSON ? 'hidden' : ''}`}> {/* HEADER */} <div className="min-h-[100px]"> {!values?.name ? ( <div className={`h-8 w-48 rounded-md bg-stone-100 ${ loading ? 'animate-pulse' : '' }`} ></div> ) : ( <h2 className="text-lg font-medium text-gray-800"> {values.name} </h2> )} {!values?.title ? ( <div className={`h-6 w-96 rounded-md bg-stone-100 ${ loading ? 'animate-pulse' : '' } mt-2`} ></div> ) : ( <h3 className="text-gray-600">{values.title}</h3> )} <div className="flex items-center mt-2"> {!values?.location ? ( <> <div className={`h-6 w-28 rounded-md bg-stone-100 ${ loading ? 'animate-pulse' : '' } mt-2`} ></div> </> ) : ( <div className="text-gray-500 text-xs">{values.location}</div> )} </div> </div> {/* CONTACT */} <div className="mt-4 text-xs min-h-[100px]"> <CategoryTitle title="Contact" /> {!values?.contactInfo ? ( <> {[...Array(3)].map((_, index) => ( <div key={index} className="flex items-center"> <div className={`h-6 w-28 rounded-md bg-stone-100 ${ loading ? 'animate-pulse' : '' } mt-2`} ></div> <div className={`h-6 w-72 ml-2 rounded-md bg-stone-100 ${ loading ? 'animate-pulse' : '' } mt-2`} ></div> </div> ))} </> ) : ( Object.entries(values.contactInfo || {}).map( ([key, value]) => value && ( <LabeledValue key={key} label={key.charAt(0).toUpperCase() + key.slice(1)} value={value} /> ) ) )} </div> <Divider /> {/* WORK EXPERIENCE */} <div className="mt-6 min-h-[100px]"> <CategoryTitle title="Work Experience" /> {!values?.workExperience ? ( <> {[...Array(2)].map((_, index) => ( <div key={index} className="flex items-center"> <div className={`h-6 w-28 rounded-md bg-stone-100 ${ loading ? 'animate-pulse' : '' } mt-2`} ></div> <div className={`h-6 w-72 ml-2 rounded-md bg-stone-100 ${ loading ? 'animate-pulse' : '' } mt-2`} ></div> </div> ))} </> ) : ( values.workExperience.map((experience, index) => ( <WorkExperience key={index} experience={experience} /> )) )} </div> <Divider /> {/* EDUCATION */} <div className="mt-6 min-h-[100px]"> <CategoryTitle title="Education" /> {!values?.education ? ( <> {[...Array(2)].map((_, index) => ( <div key={index} className="flex items-center"> <div className={`h-6 w-28 rounded-md bg-stone-100 ${ loading ? 'animate-pulse' : '' } mt-2`} ></div> <div className={`h-6 w-72 ml-2 rounded-md bg-stone-100 ${ loading ? 'animate-pulse' : '' } mt-2`} ></div> </div> ))} </> ) : ( values.education.map((experience, index) => ( <Education key={index} experience={experience} /> )) )} </div> <Divider /> {/* SKILLS */} <div className="mt-6"> <CategoryTitle title="Skills" /> <div className="flex w-full"> <div className="flex gap-2 flex-wrap"> {!values?.skills ? ( <> {[...Array(6)].map((_, index) => ( <div key={index} className={`flex text-xs px-3 py-1 rounded-full bg-stone-100 text-gray-800 mr-1 w-16 h-6 ${ loading ? 'animate-pulse' : '' }`} ></div> ))} </> ) : ( values.skills.map((skill, index) => ( <Skill key={index} skill={skill} /> )) )} </div> </div> </div> </div> </div> </div> ) } const LabeledValue: React.FC<{ label: string; value: string }> = ({ label, value }) => { return ( <div className="flex"> <div className="w-28 mt-2 text-gray-500 font-light">{label}</div> <div className="mt-2 text-gray-700">{value}</div> </div> ) } const CategoryTitle: React.FC<{ title: string }> = ({ title }) => { return <div className="font-medium text-sm mb-3">{title}</div> } const Divider: React.FC = () => { return <div className="border-b border-gray-200 my-6"></div> } const WorkExperience: React.FC<{ experience: ExperienceItem }> = ({ experience }) => { const [isDescriptionVisible, setIsDescriptionVisible] = useState(false) const toggleDescription = () => { setIsDescriptionVisible(!isDescriptionVisible) } return ( <div className="flex items-start text-xs mb-4"> {!experience?.startYear ? ( <div className={`h-6 w-28 rounded-md bg-stone-100 mt-2`}></div> ) : ( <div className="text-gray-500 min-w-28"> <span>{experience.startYear}</span> -{' '} <span>{experience?.endYear || 'now'}</span> </div> )} {!experience?.title ? ( <div className={`h-6 w-72 ml-2 rounded-md bg-stone-100 mt-2`}></div> ) : ( <div className="text-gray-800 flex flex-col items-start min-w-96 overflow-x-clip"> <div className="flex items-center pb-1 text-nowrap"> <div>{`${experience.title} at `}</div> <div className="border-b border-gray-800 ml-1 text-nowrap"> {experience.company} </div> {experience.location && ( <div className="text-gray-500 ml-1">({experience.location})</div> )} <div onClick={toggleDescription} className="ml-2 cursor-pointer text-gray-400" > {isDescriptionVisible ? ( <ChevronDown className="size-4" /> ) : ( <ChevronRight className="size-4" /> )} </div> </div> {isDescriptionVisible && experience.description && ( <div className="text-gray-700 mt-1 text-xs font-light"> {experience.description} </div> )} </div> )} </div> ) } const Education: React.FC<{ experience: EducationItem }> = ({ experience }) => { const [isDescriptionVisible, setIsDescriptionVisible] = useState(false) const toggleDescription = () => { setIsDescriptionVisible(!isDescriptionVisible) } return ( <div className="flex items-start text-xs mb-4"> <div className="text-gray-500 min-w-28"> {experience.startYear} - {experience.endYear || 'now'} </div> <div className="text-gray-800 flex flex-col items-start max-w-[500px] overflow-x-clip"> <div className="flex items-center pb-1 text-nowrap"> <div>{`${ experience.degree && experience.institution ? experience.degree + ' at ' : experience.degree || '' }`}</div> {experience.institution && ( <div className="border-b border-gray-800 ml-1 text-nowrap"> {experience.institution} </div> )} {experience.location && ( <div className="text-gray-500 ml-1">({experience.location})</div> )} {experience.description && ( <div onClick={toggleDescription} className="ml-2 cursor-pointer text-gray-400" > {isDescriptionVisible ? ( <ChevronDown className="size-4" /> ) : ( <ChevronRight className="size-4" /> )} </div> )} </div> {isDescriptionVisible && experience.description && ( <div className="text-gray-700 mt-1 text-xs font-light"> {experience.description} </div> )} </div> </div> ) } const Skill: React.FC<{ skill: SkillInterface }> = ({ skill }) => { return ( <div className="flex text-xs py-1 px-2 rounded-full bg-stone-100 text-nowrap text-gray-800 h-6"> <div>{skill.name}</div> </div> ) } export default ExtractedValues