feat: new work-experience section
This commit is contained in:
BIN
public/portfolio/cities/Edinburgh.png
Normal file
BIN
public/portfolio/cities/Edinburgh.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
BIN
public/portfolio/cities/London.png
Normal file
BIN
public/portfolio/cities/London.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
BIN
public/portfolio/cities/Manchester.png
Normal file
BIN
public/portfolio/cities/Manchester.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
BIN
public/portfolio/cities/Warsaw.png
Normal file
BIN
public/portfolio/cities/Warsaw.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
@@ -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
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
57
src/portfolio/sections/Experience/CityGroup.tsx
Normal file
57
src/portfolio/sections/Experience/CityGroup.tsx
Normal 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;
|
||||||
79
src/portfolio/sections/Experience/helpers.ts
Normal file
79
src/portfolio/sections/Experience/helpers.ts
Normal 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."
|
||||||
|
)
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user