{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "work-experience", "title": "Work Experience", "author": "ncdai ", "description": "Display work experiences with role details, company logos, and durations.", "dependencies": [ "react-markdown", "lucide-react" ], "registryDependencies": [ "collapsible", "separator", "https://chanhdai.com/r/chevrons-up-down-icon.json", "https://chanhdai.com/r/typography.json" ], "files": [ { "path": "src/registry/components/work-experience/work-experience.tsx", "content": "\"use client\"\n\nimport { differenceInMonths, parse } from \"date-fns\"\nimport { BriefcaseBusinessIcon, InfinityIcon } from \"lucide-react\"\nimport { type ComponentProps, useCallback, useRef } from \"react\"\nimport ReactMarkdown from \"react-markdown\"\n\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { cn } from \"@/lib/utils\"\nimport type { ChevronsUpDownIconHandle } from \"@/registry/components/chevrons-up-down-icon\"\nimport { ChevronsUpDownIcon } from \"@/registry/components/chevrons-up-down-icon\"\n\nexport type ExperiencePositionItemType = {\n /** Unique identifier for the position */\n id: string\n /** The job title or position name */\n title: string\n /**\n * Employment period of the position.\n * Use \"MM.YYYY\" or \"YYYY\" format. Omit `end` for current roles.\n */\n employmentPeriod: {\n /** Start date (e.g., \"10.2022\" or \"2020\"). */\n start: string\n /** End date; leave undefined for \"Present\". */\n end?: string\n }\n /** The type of employment (e.g., \"Full-time\", \"Part-time\", \"Contract\") */\n employmentType?: string\n /** A brief description of the position or responsibilities */\n description?: string\n /** An icon representing the position */\n icon?: React.ReactElement\n /** A list of skills associated with the position */\n skills?: string[]\n /** Indicates if the position details are expanded in the UI */\n isExpanded?: boolean\n}\n\nexport type ExperienceItemType = {\n /** Unique identifier for the experience item */\n id: string\n /** Name of the company where the experience was gained */\n companyName: string\n /** URL or path to the company's logo image */\n companyLogo?: string\n /** URL to the company's website. */\n companyWebsite?: string\n /**\n * List of positions held at the company\n * @fumadocsHref #experiencepositionitemtype\n * */\n positions: ExperiencePositionItemType[]\n /** Indicates if this is the user's current employer */\n isCurrentEmployer?: boolean\n}\n\nexport type WorkExperienceProps = {\n className?: string\n /** @fumadocsHref #experienceitemtype */\n experiences: ExperienceItemType[]\n}\n\nexport function WorkExperience({\n className,\n experiences,\n}: WorkExperienceProps) {\n return (\n
\n {experiences.map((experience) => (\n \n ))}\n
\n )\n}\n\nexport type ExperienceItemProps = {\n experience: ExperienceItemType\n}\n\nexport function ExperienceItem({ experience }: ExperienceItemProps) {\n return (\n
\n
\n
\n {experience.companyLogo ? (\n \n ) : (\n \n )}\n
\n\n

\n {experience.companyWebsite ? (\n \n {experience.companyName}\n \n ) : (\n experience.companyName\n )}\n

\n\n {experience.isCurrentEmployer && (\n \n \n \n \n )}\n
\n\n
\n {experience.positions.map((position) => (\n \n ))}\n
\n
\n )\n}\n\nexport type ExperiencePositionItemProps = {\n position: ExperiencePositionItemType\n}\n\nexport function ExperiencePositionItem({\n position,\n}: ExperiencePositionItemProps) {\n const chevronsIconRef = useRef(null)\n\n const handleOpenChange = useCallback((open: boolean) => {\n const controls = chevronsIconRef.current\n if (!controls) return\n\n if (open) {\n controls.startAnimation()\n } else {\n controls.stopAnimation()\n }\n }, [])\n\n const { start, end } = position.employmentPeriod\n const isOngoing = !end\n const duration = formatDuration(start, end)\n\n return (\n \n
\n \n
\n \n {position.icon ?? }\n
\n\n

\n {position.title}\n

\n\n
\n \n
\n
\n\n
\n {position.employmentType && (\n <>\n
\n
Employment Type
\n
{position.employmentType}
\n
\n\n \n \n )}\n\n
\n
Employment Period
\n
\n {start}\n \n {isOngoing ? (\n \n ) : (\n {end}\n )}\n
\n
\n\n {duration && (\n <>\n \n
\n
Duration
\n
{duration}
\n
\n \n )}\n
\n \n\n \n {position.description && (\n \n {position.description}\n \n )}\n \n\n {Array.isArray(position.skills) && position.skills.length > 0 && (\n
    \n {position.skills.map((skill, index) => (\n
  • \n {skill}\n
  • \n ))}\n
\n )}\n \n \n )\n}\n\nfunction Prose({ className, ...props }: ComponentProps<\"div\">) {\n return (\n \n )\n}\n\nfunction Skill({ className, ...props }: ComponentProps<\"span\">) {\n return (\n \n )\n}\n\nfunction formatDuration(start: string, end?: string): string {\n const startHasMonth = start.includes(\".\")\n const endHasMonth = end ? end.includes(\".\") : true\n\n // Both year-only: granularity is years, no month arithmetic needed.\n if (!startHasMonth && end && !endHasMonth) {\n const years = parseInt(end, 10) - parseInt(start, 10)\n if (years <= 0) {\n return \"\"\n }\n return `${years}y`\n }\n\n const startDate = parsePeriodDate(start, \"first\")\n const endDate = end ? parsePeriodDate(end, \"last\") : new Date()\n\n // +1 to count both the start and end months inclusively.\n const totalMonths = differenceInMonths(endDate, startDate) + 1\n if (totalMonths <= 0) {\n return \"\"\n }\n\n if (totalMonths < 12) {\n return `${totalMonths}m`\n }\n\n const years = Math.floor(totalMonths / 12)\n const months = totalMonths % 12\n if (months === 0) {\n return `${years}y`\n }\n return `${years}y ${months}m`\n}\n\nfunction parsePeriodDate(str: string, fallbackMonth: \"first\" | \"last\"): Date {\n if (str.includes(\".\")) {\n return parse(str, \"MM.yyyy\", new Date())\n }\n return parse(\n `${fallbackMonth === \"last\" ? \"12\" : \"01\"}.${str}`,\n \"MM.yyyy\",\n new Date()\n )\n}\n", "type": "registry:component" } ], "docs": "https://chanhdai.com/components/work-experience-component", "type": "registry:component" }