From 60e94f96d51da313fed6698a8d8ca9685960aa89 Mon Sep 17 00:00:00 2001 From: Craig Wayne Date: Fri, 12 Dec 2025 11:26:19 +0000 Subject: [PATCH] Experiences and other data comes from a single json file * Also updated the resume to work with the new data structure * Style updates to the resume to support downloading * Updated the schema for the portfolio --- resume-generator/template.pug | 18 ++-- src/components/Experience.tsx | 72 +++++-------- src/components/Hero.tsx | 10 +- src/data/portfolio.ts | 176 +++---------------------------- src/public/data.json | 110 +++++++++++++++++-- src/public/resume/styles.css | 32 +++--- src/public/schema-portfolio.json | 97 ++++++++++++++++- 7 files changed, 264 insertions(+), 251 deletions(-) diff --git a/resume-generator/template.pug b/resume-generator/template.pug index d7b29d9..0006b63 100644 --- a/resume-generator/template.pug +++ b/resume-generator/template.pug @@ -42,8 +42,11 @@ html(lang="en") .a4-page .action-bar.no-print button(onclick="window.print()") - svg(width='25' height='22' viewbox='0 0 25 22' fill='none' xmlns='http://www.w3.org/2000/svg') - path(d='M19.6113 0.25C19.7975 0.250059 19.9749 0.325939 20.1045 0.458984C20.2338 0.591855 20.3055 0.770971 20.3057 0.956055V4.81543H22.1299C23.5658 4.81543 24.7499 5.94567 24.75 7.34766V16.4785C24.7499 16.6637 24.6782 16.8427 24.5488 16.9756C24.4192 17.1087 24.2419 17.1845 24.0557 17.1846H20.3057V21.0439C20.3055 21.229 20.2338 21.4081 20.1045 21.541C19.9749 21.6741 19.7975 21.7499 19.6113 21.75H5.38867C5.20246 21.7499 5.02513 21.6741 4.89551 21.541C4.76615 21.4081 4.69446 21.229 4.69434 21.0439V17.1846H0.944336C0.758061 17.1845 0.580822 17.1087 0.451172 16.9756C0.321767 16.8427 0.250065 16.6637 0.25 16.4785V7.34766C0.250096 5.94567 1.43415 4.81543 2.87012 4.81543H4.69434V0.956055C4.69446 0.77097 4.76615 0.591854 4.89551 0.458984C5.02513 0.325939 5.20246 0.250059 5.38867 0.25H19.6113ZM6.08301 20.3369H18.917V14.4453H6.08301V20.3369ZM2.87012 6.22852C2.17472 6.22852 1.63877 6.7486 1.63867 7.34766V15.7715H4.69434V13.7393C4.69434 13.5541 4.76612 13.3752 4.89551 13.2422C5.02514 13.109 5.20238 13.0323 5.38867 13.0322H19.6113C19.7976 13.0323 19.9749 13.109 20.1045 13.2422C20.2339 13.3752 20.3057 13.5541 20.3057 13.7393V15.7715H23.3613V7.34766C23.3612 6.7486 22.8253 6.22852 22.1299 6.22852H2.87012ZM19.167 8.46777C19.4711 8.46786 19.7617 8.5918 19.9746 8.81055C20.1873 9.02912 20.3057 9.32462 20.3057 9.63086C20.3056 9.85927 20.2394 10.0835 20.1152 10.2744C19.9909 10.4654 19.8132 10.6153 19.6045 10.7041C19.3956 10.7929 19.1655 10.8159 18.9434 10.7705C18.7212 10.7251 18.5182 10.6133 18.3594 10.4502C18.2007 10.2872 18.0933 10.0806 18.0498 9.85645C18.0064 9.63217 18.0279 9.39917 18.1133 9.1875C18.1987 8.97577 18.3439 8.79363 18.5312 8.66504C18.7187 8.53639 18.9402 8.46777 19.167 8.46777ZM6.08301 4.81543H18.917V1.66309H6.08301V4.81543Z' stroke-width='0.5') + svg(xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='#000000' viewbox='0 0 256 256') + path(d='M214.67,72H200V40a8,8,0,0,0-8-8H64a8,8,0,0,0-8,8V72H41.33C27.36,72,16,82.77,16,96v80a8,8,0,0,0,8,8H56v32a8,8,0,0,0,8,8H192a8,8,0,0,0,8-8V184h32a8,8,0,0,0,8-8V96C240,82.77,228.64,72,214.67,72ZM72,48H184V72H72ZM184,208H72V160H184Zm40-40H200V152a8,8,0,0,0-8-8H64a8,8,0,0,0-8,8v16H32V96c0-4.41,4.19-8,9.33-8H214.67c5.14,0,9.33,3.59,9.33,8Zm-24-52a12,12,0,1,1-12-12A12,12,0,0,1,200,116Z') + a(download="" href="/Resume-CraigWayneGovender.pdf" target="_blank") + svg(xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='#000000' viewbox='0 0 256 256') + path(d='M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z') header h1 #{first_names} #{last_name} h2 #{latest_role} @@ -61,20 +64,21 @@ html(lang="en") | #{contact.phone} span i.fab.fa-linkedin - a(target="_blank" href=`https://www.linkedin.com/in/${contact.linkedin_username}`) - | in/#{contact.linkedin_username} + a(target="_blank" href=`https://www.linkedin.com/in/${social.linkedin}`) + | in/#{social.linkedin} p.summary #{summary} section.experience .title EMPLOYMENT - each experience in work_experience + // last 4 experience + each experience in (work_experience.slice(0,4)) .item header h4.title #{experience.position} .date | #{experience.period.start} - #{experience.period.end} .subtitle - span.organization #{experience.organization} + span.organization #{experience.organization.name} span.location #{experience.location.country} ul each responsibility in experience.responsibilities @@ -101,7 +105,7 @@ html(lang="en") if index < sorted_skills.length - 1 | , .supported-brands.muted.no-print - .title Supported Products + .title ATS Friendly svg.active(viewbox='0 0 51.4 107.7' xmlns='http://www.w3.org/2000/svg') path(fill="#23A47F" d='M44.9 32c0 5.2-2.2 9.8-5.8 13.4-4 4-9.8 5-9.8 8.4 0 4.6 7.4 3.2 14.5 10.3 4.7 4.7 7.6 10.9 7.6 18.1 0 14.2-11.4 25.5-25.7 25.5S0 96.4 0 82.2C0 75 2.9 68.8 7.6 64.1c7.1-7.1 14.5-5.7 14.5-10.3 0-3.4-5.8-4.4-9.8-8.4-3.6-3.6-5.8-8.2-5.8-13.6C6.5 21.4 15 13 25.4 13c2 0 3.8.3 5.3.3 2.7 0 4.1-1.2 4.1-3.1 0-1.1-.5-2.5-.5-4 0-3.4 2.9-6.2 6.4-6.2S47 2.9 47 6.4c0 3.7-2.9 5.4-5.1 6.2-1.8.6-3.2 1.4-3.2 3.2C38.7 19.2 44.9 22.5 44.9 32zM42.9 82.2c0-9.9-7.3-17.9-17.2-17.9s-17.2 8-17.2 17.9c0 9.8 7.3 17.9 17.2 17.9S42.9 92 42.9 82.2zM37 31.8c0-6.3-5.1-11.5-11.3-11.5s-11.3 5.2-11.3 11.5S5.1 38.1 11.3 31.8z') diff --git a/src/components/Experience.tsx b/src/components/Experience.tsx index 7f7c5f5..195f55d 100644 --- a/src/components/Experience.tsx +++ b/src/components/Experience.tsx @@ -1,17 +1,23 @@ -import {Badge} from "./ui/badge"; import {motion} from "motion/react"; import {ImageWithFallback} from "./figma/ImageWithFallback"; interface Position { - company: string; - company_url: string; - companyLogo: string; + organization: { + name: string, + url: string, + logo: string + }, position: string; - date_start: string; - date_end?: string; - location: string; + period: { + start: string, + end?: string + } + location: { + city: string + country: string + } description: string; - achievements: string[]; + responsibilities: string[]; technologies: string[]; } @@ -23,10 +29,8 @@ interface ExperienceProps { } } - -// Function to extract and format start date from duration string function formatTimelineDate(position: Position): { year: number; month: string } { - const start_date_obj = new Date(position.date_start); + const start_date_obj = new Date(position.period.start); const month_index = start_date_obj.getMonth(); const month_lists = [ @@ -157,16 +161,13 @@ export function Experience({data}: ExperienceProps) { viewport={{once: true}} transition={{duration: 0.6}} > - Experience

{data.subtitle}

{data.description}

- {/* Timeline Container */}
- {/* Vertical Timeline Line - Hidden on mobile */}
- {/* Timeline Items */} - {/* Timeline Date - Hidden on mobile */} {timelineDate.month}
- {/* Connection dot */} -
- {/* Experience Card */}
- {/* Card Connector Line */}
- {/* Location in top right */}

- {exp.location} + {exp.location.city}, {exp.location.country}

- {/* Header with company logo and title */} -
+
@@ -253,14 +245,13 @@ export function Experience({data}: ExperienceProps) { {exp.position}

- - {exp.company} + + {exp.organization.name}

- {/* Description and achievements as bullet points */} - {/* Main description as first bullet */} -
-

{exp.description}

- {/* Achievements as additional bullets */} - {exp.achievements.map((achievement, achIndex) => ( + {exp.responsibilities.map((responsibility, responsibility_index) => (
-

{achievement}

+

{responsibility}

))}
- {/* Technology badges */} - - {/* Timeline End Marker - Hidden on mobile */} -
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index df03814..9855496 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -7,8 +7,7 @@ import HeroProfileImage from './HeroProfileImage/HeroProfileImage'; interface HeroProps { data: { title: string; - availabilityStatus: string; - description: string; + summary: string; profileImage: string; socialLinks: { github: string; @@ -59,10 +58,7 @@ export function Hero({ data }: HeroProps) { animate="visible" > - - {data.availabilityStatus} - -

+

{data.title.split(' ').slice(0, 2).join(' ')} {data.title.split(' ').slice(2).join(' ')}

@@ -72,7 +68,7 @@ export function Hero({ data }: HeroProps) { className="text-lg text-muted-foreground max-w-lg" variants={itemVariants} > - {data.description} + {data.summary} * { background: none; border: none; margin: 0; padding: 10px 8px; cursor: pointer; + display: flex; + justify-content: center; + align-items: center; } -.action-bar button svg { +.action-bar svg { fill: var(--color-primary); stroke: var(--color-primary); + user-select: none; } -.action-bar button:first-child:last-child { - min-height: 64px; -} - -.action-bar button:hover { +.action-bar > *:hover { background-color: rgba(11, 87, 208, .08); } @media screen and (max-width: 900px) { + body { + margin-bottom: 64px; + } .action-bar { top: unset; bottom: 0; - right: 0; - transform: translate(-50%, -50%); + right: 50%; + transform: translate(50%, -50%); position: fixed; + flex-direction: row; } - - .action-bar button:first-child:last-child{ - min-height: unset; - min-width: 64px; + .action-bar > * { + padding: 8px 10px; } } @@ -268,6 +271,7 @@ ul li::marker { body { background-color: white; padding: 0; + margin: 0; } .a4-page { diff --git a/src/public/schema-portfolio.json b/src/public/schema-portfolio.json index df50efe..73ffd9c 100644 --- a/src/public/schema-portfolio.json +++ b/src/public/schema-portfolio.json @@ -15,6 +15,9 @@ "last_name": { "type": "string" }, + "summary": { + "type": "string" + }, "latest_role": { "description": "Optional. If this property is not set, the role at your most recent employment will be used", "type": "string" @@ -26,9 +29,6 @@ "type": "string", "format": "email" }, - "linkedin_username": { - "type": "string" - }, "phone": { "type": "string", "description": "Phone number in E.164 format (e.g., +447624401629)", @@ -38,18 +38,105 @@ "additionalProperties": false, "required": [ "email", - "linkedin_username", "phone" ] }, "location": { "description": "Optional. If this property is not set, the location at your most recent employment will be used", "type": "string" + }, + "social": { + "description": "Just the usernames for the social media entries", + "type": "object", + "properties": { + "linkedin": { + "type": "string" + }, + "github": { + "type": "string" + } + }, + "required": ["linkedin"] + }, + "work_experience": { + "type": "array", + "items": { + "type": "object", + "properties": { + "organization": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "logo": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name", + "url", + "logo" + ] + }, + "location": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + } + } + }, + "period": { + "type": "object", + "properties": { + "start": { + "type": "string" + }, + "end": { + "type": "string" + } + }, + "required": ["start"] + }, + "responsibilities": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "technologies": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": [ + "organization", + "location", + "period", + "responsibilities", + "technologies" + ] + } } }, "required": [ "first_names", - "last_name" + "last_name", + "summary", + "work_experience" ], "additionalProperties": false }