Merge pull request #5 from KuchtaVR6/work-experience-revamp

Work experience revamp
This commit is contained in:
Patryk Kuchta
2026-02-23 20:06:48 +00:00
committed by GitHub
13 changed files with 364 additions and 58 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View File

@@ -7,7 +7,8 @@ const workExperienceData : WorkExperienceArgs[] = [
company: "Softwire",
city: "Manchester",
country: "United Kingdom",
startDate: "November 2025"
startDate: "November 2025",
isImportant: true
},
{
industry: "Software & AI",
@@ -16,15 +17,18 @@ const workExperienceData : WorkExperienceArgs[] = [
city: "Manchester",
country: "United Kingdom",
startDate: "November 2024",
endDate: "November 2025"
endDate: "November 2025",
isImportant: true
},
{
industry: "Artificial Intelligence",
title: "Programming Data Annotator",
company: "DataAnnotation Tech",
city: "New York",
country: "USA",
startDate: "January 2024"
city: "Edinburgh",
country: "United Kingdom",
displayLocation: "New York, United States (Remote)",
startDate: "January 2024",
isImportant: true
},
{
industry: "Education",
@@ -33,7 +37,8 @@ const workExperienceData : WorkExperienceArgs[] = [
city: "London",
country: "United Kingdom",
startDate: "September 2021",
endDate: "April 2024"
endDate: "April 2024",
isImportant: true
},
{
industry: "Software",
@@ -42,7 +47,8 @@ const workExperienceData : WorkExperienceArgs[] = [
city: "London",
country: "United Kingdom",
startDate: "June 2023",
endDate: "August 2023"
endDate: "August 2023",
isImportant: true
},
{
industry: "Education",
@@ -105,10 +111,15 @@ const workExperienceData : WorkExperienceArgs[] = [
city: "Warsaw",
country: "Poland",
startDate: "July 2018",
endDate: "August 2018"
endDate: "August 2018",
isImportant: true
}
];
export const workExperienceIntro =
"Growing up in Warsaw, I moved to London at 18. Since then, my career and education has taken me through " +
"Edinburgh and Manchester, shaping my journey across the UK.";
export const workExperienceParagraph =
"Since the age of 16, I have actively engaged in various professional roles across multiple industries, " +
"including Artificial Intelligence, Software, Education, and Hospitality. My journey began in the hospitality " +

View File

@@ -8,7 +8,9 @@ export type WorkExperienceArgs = {
title : string,
city : string,
country : string,
endDate? : string
endDate? : string,
displayLocation? : string,
isImportant? : boolean
}
const WorkExperience : FC<WorkExperienceArgs> = (props) => {

View File

@@ -1,54 +1,20 @@
import { Splide, SplideSlide } from "@splidejs/react-splide";
import { useEffect, useState } from "react";
import styles from "../styling/experience.module.scss";
import WorkExperience from "@/src/portfolio/helpers/WorkExperience";
import workExperienceData, { workExperienceParagraph } from "@/src/portfolio/data/workExperienceData";
import { getCityGroups } from "./Experience/helpers";
import CityGroup from "./Experience/CityGroup";
import { workExperienceIntro } from "@/src/portfolio/data/workExperienceData";
const Experience = (): JSX.Element => {
const calcPagesOnWidth = (width : number): number => {
return Math.floor(width / 800 + 1);
};
const [pages, setPages] = useState(calcPagesOnWidth(1000));
useEffect(() => {
setPages(calcPagesOnWidth(window.innerWidth));
const handleResize = (): void => {
setPages(calcPagesOnWidth(window.innerWidth));
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
const cityGroups = getCityGroups();
return (
<div className={styles.section} id={"experience"}>
<h2>Work Experience</h2>
<p data-aos={"fade-right"}>
{workExperienceParagraph}
</p>
<Splide
options={{
rewind: true,
perPage: pages,
focus: "center",
start: 0
}}
>
{
workExperienceData.map((entry, index) =>
<SplideSlide key={"outer"+index}>
<WorkExperience
{...entry} key={"inner"+index}
/>
</SplideSlide>
)
}
</Splide>
<p className={styles.intro}>{workExperienceIntro}</p>
<div className={styles.cityGroupsContainer}>
{cityGroups.map((group, index) => (
<CityGroup key={index} group={group} />
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,57 @@
import { FC, useState } from "react";
import { ICityGroup } from "./helpers";
import styles from "@/src/portfolio/styling/experience.module.scss";
import { FiChevronDown, FiChevronUp } from "react-icons/fi";
interface ICityGroupProps {
group: ICityGroup;
}
const CityGroup: FC<ICityGroupProps> = ({ group }) => {
const [showAll, setShowAll] = useState(false);
const displayedJobs = showAll ? group.jobs : group.visibleJobs;
return (
<div
className={`${styles.cityGroup} ${group.offsetDirection === "left" ? styles.offsetLeft : styles.offsetRight}`}
style={{
backgroundImage: group.imageUrl ? `url(${group.imageUrl})` : undefined
}}
>
<div className={styles.overlay}></div>
<h3 className={styles.cityTitle}>{group.city}</h3>
<div className={styles.jobsContainer}>
{displayedJobs.map((job, index) => (
<div key={index} className={styles.job} data-aos="fade-left">
<span className={styles.largerText}>
<b>{job.title}</b><br/>
at <span className={styles.redText}>{job.company}</span><br/>
</span>
in <b>{job.displayLocation || `${job.city}, ${job.country}`}.</b>
<div className={styles.industry}>{job.industry}</div>
<hr/>
{job.endDate ? `${job.startDate} - ${job.endDate}` : `Since ${job.startDate}`}
</div>
))}
{group.hasMore && (
<button className={styles.showMoreBtn} onClick={() => setShowAll(!showAll)}>
{showAll ? (
<>
Show Less <FiChevronUp />
</>
) : (
<>
Show {group.hiddenCount} More {group.hiddenCount === 1 ? "Job" : "Jobs"} <FiChevronDown />
</>
)}
</button>
)}
</div>
<div className={styles.locationMarker}>
{group.city}
</div>
</div>
);
};
export default CityGroup;

View File

@@ -0,0 +1,96 @@
import workExperienceData from "@/src/portfolio/data/workExperienceData";
import { WorkExperienceArgs } from "@/src/portfolio/helpers/WorkExperience";
export interface ICityGroup {
city: string;
jobs: WorkExperienceArgs[];
visibleJobs: WorkExperienceArgs[];
hasMore: boolean;
hiddenCount: number;
imageUrl: string;
offsetDirection: "left" | "right";
}
const CITY_IMAGES: Record<string, string> = {
"Manchester": "/portfolio/cities/Manchester.png",
"Edinburgh": "/portfolio/cities/Edinburgh.png",
"Greater London": "/portfolio/cities/Greater London.png",
"Warsaw": "/portfolio/cities/Warsaw.png"
};
const CITY_ALIASES: Record<string, string> = {
"London": "Greater London",
"Upminster": "Greater London"
};
const createCityGroup = (
city: string,
jobs: WorkExperienceArgs[],
offset: "left" | "right"
): ICityGroup => {
const importantJobs = jobs.filter(job => job.isImportant);
const nonImportantJobs = jobs.filter(job => !job.isImportant);
const needToFill = Math.max(0, 2 - importantJobs.length);
const fillerJobs = nonImportantJobs.slice(0, needToFill);
const remainingNonImportant = nonImportantJobs.slice(needToFill);
return {
city,
jobs,
visibleJobs: [...importantJobs, ...fillerJobs],
hasMore: remainingNonImportant.length > 0,
hiddenCount: remainingNonImportant.length,
imageUrl: CITY_IMAGES[city] || "",
offsetDirection: offset
};
};
const normalizeCityName = (city: string): string => {
return CITY_ALIASES[city] || city;
};
const parseDate = (dateString: string): Date => {
return new Date(dateString);
};
const getEarliestJobDate = (jobs: WorkExperienceArgs[]): Date => {
return jobs.reduce((earliest, job) => {
const jobDate = parseDate(job.startDate);
return jobDate < earliest ? jobDate : earliest;
}, parseDate(jobs[0].startDate));
};
export const getCityGroups = (): ICityGroup[] => {
const cityJobsMap = new Map<string, WorkExperienceArgs[]>();
workExperienceData.forEach(job => {
const normalizedCity = normalizeCityName(job.city);
if (!cityJobsMap.has(normalizedCity)) {
cityJobsMap.set(normalizedCity, []);
}
const cityJobs = cityJobsMap.get(normalizedCity);
if (cityJobs) {
cityJobs.push(job);
}
});
const cityOrder = Array.from(cityJobsMap.entries())
.map(([city, jobs]) => ({ city, earliestDate: getEarliestJobDate(jobs) }))
.sort((a, b) => b.earliestDate.getTime() - a.earliestDate.getTime())
.map(entry => entry.city);
const offsetPattern: ("left" | "right")[] = ["right", "left", "right", "left"];
return cityOrder
.map((city, index) => {
const jobs = cityJobsMap.get(city);
if (!jobs) return null;
return createCityGroup(
city,
jobs,
offsetPattern[index % offsetPattern.length]
);
})
.filter((group): group is ICityGroup => group !== null);
};

View File

@@ -20,21 +20,176 @@
}
}
.cityGroupsContainer {
display: flex;
flex-direction: column;
gap: 0;
margin-top: 3rem;
width: 100%;
position: relative;
}
.cityGroup {
position: relative;
padding: 3rem 2rem;
border-radius: 8px;
min-height: 400px;
width: 65%;
margin: 0 auto 6rem auto;
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: bottom !important;
> * {
position: relative;
z-index: 1;
}
&::after {
content: '';
position: absolute;
bottom: -3rem;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 2px;
background: linear-gradient(to right, transparent, $accent_colour, transparent);
z-index: 1;
}
@media (max-width: 768px) {
width: 90%;
padding: 2rem 1rem;
margin-bottom: 4rem;
&::after {
bottom: -2rem;
width: 80%;
}
}
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6));
z-index: -1;
border-radius: 8px;
}
.cityTitle {
display: none;
}
.cityDescription {
font-size: 1rem;
color: rgba($white, 0.9);
margin-bottom: 2rem;
font-style: italic;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
line-height: 1.5;
}
.jobsContainer {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
}
.offsetLeft .jobsContainer {
align-items: flex-start;
}
.offsetRight .jobsContainer {
align-items: flex-end;
}
.locationMarker {
position: absolute;
bottom: -3rem;
left: 50%;
transform: translate(-50%, 50%);
background: $accent_colour;
color: $white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
z-index: 10;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
@media (max-width: 768px) {
bottom: -2rem;
font-size: 0.75rem;
padding: 0.4rem 0.8rem;
}
}
.job {
@include lightSection;
width: 100%;
padding: 1em;
margin: auto 1em;
background-color: rgba(255, 255, 255, 0.95);
width: 45%;
padding: 0.75em;
margin: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.largerText {
font-size: 1.3em;
font-size: 1.1em;
}
.redText {
color: $accent_colour;
}
.industry {
margin-top: 12px;
margin-top: 8px;
text-align: right;
font-size: 0.9em;
}
@media (max-width: 1024px) {
width: 60%;
}
@media (max-width: 768px) {
width: 85%;
}
@media (max-width: 480px) {
width: 95%;
}
}
.showMoreBtn {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
color: $white;
width: 100%;
padding: 0.5rem;
margin-top: 0.75rem;
border: 1px solid rgba($white, 0.4);
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
transition: all 0.2s ease;
&:hover {
background: rgba(0, 0, 0, 0.5);
color: $white;
border-color: rgba($white, 0.6);
}
svg {
font-size: 1rem;
}
}

View File

@@ -61,6 +61,14 @@
flex-basis: calc(100% / 2);
padding: 2em 2em;
h2 {
font-size: 1.8em;
}
p {
font-size: 1.05em;
line-height: 1.6;
}
.links {

View File

@@ -98,6 +98,17 @@
&:last-child {
border-bottom: none;
}
@media (max-width: 768px) {
display: flex;
flex-direction: column;
gap: 0.25rem;
span {
float: none !important;
text-align: left !important;
}
}
}