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",
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,7 +111,8 @@ const workExperienceData : WorkExperienceArgs[] = [
city: "Warsaw",
country: "Poland",
startDate: "July 2018",
endDate: "August 2018"
endDate: "August 2018",
isImportant: true
}
];

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,18 @@
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";
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>
<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,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 {
@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;
}
}
}