feat: new work-experience section

This commit is contained in:
2026-02-22 23:16:23 +00:00
parent 4d5a19f7ad
commit 4b5d1bb9a1
12 changed files with 341 additions and 58 deletions

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", company: "Softwire",
city: "Manchester", city: "Manchester",
country: "United Kingdom", country: "United Kingdom",
startDate: "November 2025" startDate: "November 2025",
isImportant: true
}, },
{ {
industry: "Software & AI", industry: "Software & AI",
@@ -16,15 +17,18 @@ const workExperienceData : WorkExperienceArgs[] = [
city: "Manchester", city: "Manchester",
country: "United Kingdom", country: "United Kingdom",
startDate: "November 2024", startDate: "November 2024",
endDate: "November 2025" endDate: "November 2025",
isImportant: true
}, },
{ {
industry: "Artificial Intelligence", industry: "Artificial Intelligence",
title: "Programming Data Annotator", title: "Programming Data Annotator",
company: "DataAnnotation Tech", company: "DataAnnotation Tech",
city: "New York", city: "Edinburgh",
country: "USA", country: "United Kingdom",
startDate: "January 2024" displayLocation: "New York, United States (Remote)",
startDate: "January 2024",
isImportant: true
}, },
{ {
industry: "Education", industry: "Education",
@@ -33,7 +37,8 @@ const workExperienceData : WorkExperienceArgs[] = [
city: "London", city: "London",
country: "United Kingdom", country: "United Kingdom",
startDate: "September 2021", startDate: "September 2021",
endDate: "April 2024" endDate: "April 2024",
isImportant: true
}, },
{ {
industry: "Software", industry: "Software",
@@ -42,7 +47,8 @@ const workExperienceData : WorkExperienceArgs[] = [
city: "London", city: "London",
country: "United Kingdom", country: "United Kingdom",
startDate: "June 2023", startDate: "June 2023",
endDate: "August 2023" endDate: "August 2023",
isImportant: true
}, },
{ {
industry: "Education", industry: "Education",
@@ -105,7 +111,8 @@ const workExperienceData : WorkExperienceArgs[] = [
city: "Warsaw", city: "Warsaw",
country: "Poland", country: "Poland",
startDate: "July 2018", startDate: "July 2018",
endDate: "August 2018" endDate: "August 2018",
isImportant: true
} }
]; ];

View File

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

View File

@@ -1,54 +1,18 @@
import { Splide, SplideSlide } from "@splidejs/react-splide";
import { useEffect, useState } from "react";
import styles from "../styling/experience.module.scss"; import styles from "../styling/experience.module.scss";
import WorkExperience from "@/src/portfolio/helpers/WorkExperience"; import { getCityGroups } from "./Experience/helpers";
import workExperienceData, { workExperienceParagraph } from "@/src/portfolio/data/workExperienceData"; import CityGroup from "./Experience/CityGroup";
const Experience = (): JSX.Element => { const Experience = (): JSX.Element => {
const calcPagesOnWidth = (width : number): number => { const cityGroups = getCityGroups();
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);
};
}, []);
return ( return (
<div className={styles.section} id={"experience"}> <div className={styles.section} id={"experience"}>
<h2>Work Experience</h2> <h2>Work Experience</h2>
<p data-aos={"fade-right"}> <div className={styles.cityGroupsContainer}>
{workExperienceParagraph} {cityGroups.map((group, index) => (
</p> <CityGroup key={index} group={group} />
<Splide ))}
options={{ </div>
rewind: true,
perPage: pages,
focus: "center",
start: 0
}}
>
{
workExperienceData.map((entry, index) =>
<SplideSlide key={"outer"+index}>
<WorkExperience
{...entry} key={"inner"+index}
/>
</SplideSlide>
)
}
</Splide>
</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,79 @@
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";
description?: string;
}
const CITY_IMAGES: Record<string, string> = {
"Manchester": "/portfolio/cities/Manchester.png",
"Edinburgh": "/portfolio/cities/Edinburgh.png",
"London": "/portfolio/cities/London.png",
"Warsaw": "/portfolio/cities/Warsaw.png"
};
const createCityGroup = (
city: string,
jobs: WorkExperienceArgs[],
offset: "left" | "right",
description?: string
): 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,
description
};
};
export const getCityGroups = (): ICityGroup[] => {
const manchesterJobs = workExperienceData.filter(job => job.city === "Manchester");
const edinburghJobs = workExperienceData.filter(job => job.city === "Edinburgh");
const londonJobs = workExperienceData.filter(job => job.city === "London" || job.city === "Upminster");
const warsawJobs = workExperienceData.filter(job => job.city === "Warsaw");
return [
createCityGroup(
"Manchester",
manchesterJobs,
"right",
"After finishing my master's degree, I moved to Manchester because I really liked the culture and the city."
),
createCityGroup(
"Edinburgh",
edinburghJobs,
"left",
"I moved to Edinburgh to pursue my Master's in Artificial Intelligence at the University of Edinburgh."
),
createCityGroup(
"London",
londonJobs,
"right",
"After Warsaw, I moved to London for my bachelor's education at Queen Mary University."
),
createCityGroup(
"Warsaw",
warsawJobs,
"left",
"Where my journey began - my first job experience in the hospitality industry."
)
];
};

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 { .job {
@include lightSection; @include lightSection;
width: 100%; background-color: rgba(255, 255, 255, 0.95);
padding: 1em; width: 45%;
margin: auto 1em; padding: 0.75em;
margin: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.largerText { .largerText {
font-size: 1.3em; font-size: 1.1em;
} }
.redText { .redText {
color: $accent_colour;
} }
.industry { .industry {
margin-top: 12px; margin-top: 8px;
text-align: right; 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); flex-basis: calc(100% / 2);
padding: 2em 2em; padding: 2em 2em;
h2 {
font-size: 1.8em;
}
p {
font-size: 1.05em;
line-height: 1.6;
}
.links { .links {

View File

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