Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions components/StyleGuide/Text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import styled from "styled-components";

interface TitleProps {}
export const Title = styled.h1<TitleProps>`
color: white;
font-family: "PT Mono", monospace;
text-align: center;
font-size: 24px;
margin: 20px;
`;

interface TextProps {}
export const Text = styled.p<TextProps>`
width: 100px;
min-width: 100px;
font-weight: bold;
font-size: 14px;
`;
11 changes: 11 additions & 0 deletions components/global/Carousel/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { render } from "@testing-library/react";
import React from "react";
import Carousel from ".";

describe("<Carousel/>", () => {
test("Carousel", () => {
expect(() => {
render(<Carousel />);
}).not.toThrowError();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a snapshot test

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added content based test, but using testing-library, it is a little hard to do snapshot test, because the class name is randomly generated when style-components are being used, because I'm unaware of what they would be. I believe storybook-testing might be better and might provide us with snapshot testing for our scenario, Please advice.

});
});
14 changes: 14 additions & 0 deletions components/global/Carousel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, { FC } from "react";
import { Container } from "./styled";

interface CarouselProps {}

const Carousel: React.FC<CarouselProps> = ({}) => {
return (
<Container>
<h2>Carousel</h2>
</Container>
);
};

export default Carousel;
8 changes: 8 additions & 0 deletions components/global/Carousel/styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import styled from "styled-components";

interface ContainerProps {}
export const Container = styled.div<ContainerProps>`
display: flex;
flex-direction: column;
background-color: #272727;
`;
12 changes: 12 additions & 0 deletions components/global/SectionWrapper/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { render } from "@testing-library/react";
import React from "react";
import SectionWrapper from ".";

describe("<SectionWrapper/>", () => {
const title = "TEST";
const { getByText } = render(<SectionWrapper title={title} />);

test("Contains title", () => {
expect(getByText(title)).toBeInTheDocument();
});
});
23 changes: 23 additions & 0 deletions components/global/SectionWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import { Container } from "./styled";
import { Title } from "../../StyleGuide/Text";

interface SectionWrapperProps {
title: string;
container?: boolean;
}

const SectionWrapper: React.FC<SectionWrapperProps> = ({
title,
container = false,
children,
}) => {
return (
<Container container={container}>
<Title>{title}</Title>
{children}
</Container>
);
};

export default SectionWrapper;
10 changes: 10 additions & 0 deletions components/global/SectionWrapper/styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from "styled-components";

interface ContainerProps {
container: boolean;
}
export const Container = styled.div<ContainerProps>`
display: flex;
flex-direction: column;
background-color: #272727;
`;
18 changes: 18 additions & 0 deletions components/global/Timeline/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { render } from "@testing-library/react";
import React from "react";
import Timeline from ".";
import Data from "../../../mock/timeline";

describe("<Timeline/>", () => {
test("Renders timeline with empty data", () => {
expect(() => {
render(<Timeline timeline={{ data: [] }} />);
}).not.toThrowError();
});

test("Renders timeline with mock data", () => {
expect(() => {
render(<Timeline timeline={{ ...Data }} />);
}).not.toThrowError();
});
});
226 changes: 226 additions & 0 deletions components/global/Timeline/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import React, { useState, useEffect, useRef } from "react";
import Image from "next/image";
import {
Container,
TimelineEntry,
TimelineExperience,
TimelineYears,
ExperienceItem,
TimelineYearLabel,
} from "./styled";
import { Text } from "../../StyleGuide/Text";
import { monthDiff } from "../../../utils/helpers";

export interface IExperience {
duration: {
start: { date?: number; month: number; year: number };
end?: { date?: number; month: number; year: number };
};
company: {
image?: string;
title: string;
website?: string;
industry?: string;
technologies?: string[];
};
meta?: {
width?: number;
left?: number;
level?: number;
};
}

export interface ITimelineEntry {
type: string;
color: string;
experience: IExperience[];
meta?: {
depth: number;
};
}

export interface ITimeline {
data: ITimelineEntry[];
}

interface IRangeYear {
start: number;
end: number;
}

interface TimelineProps {
timeline: ITimeline;
}
const Timeline: React.FC<TimelineProps> = ({ timeline }) => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can merge for now, but let's pair program next week on optimizing this. This is a very important component

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

const yearsRef = useRef<HTMLDivElement>(null);
const [range, setRange] = useState<IRangeYear | null>(null);
const [yearWidth, setYearWidth] = useState<number>(0);
const [processedTimeline, setProcessedTimeline] = useState<ITimeline | null>(
null
);

useEffect(() => {
setRange({ ...calculateRange(timeline) });
}, [timeline]);

useEffect(() => {
window.addEventListener("resize", () => {
calculateYearWidth();
});

return () => {
window.removeEventListener("resize", () => {});
};
});

useEffect(() => {
calculateYearWidth();
}, [range]);

useEffect(() => {
// process timeline
const _timeline = { data: [...timeline.data] };
// Calculate Level, Left & Width
for (let i = 0; i < _timeline.data.length; i++) {
let depth = 0;
for (let j = 0; j < _timeline.data[i].experience.length; j++) {
if ((_timeline.data[i].experience[j].meta?.level || 0) > depth) {
depth = _timeline.data[i].experience[j].meta?.level || depth;
}

_timeline.data[i].experience[j].meta = {
...calculatePositionAndWidth(_timeline.data[i].experience[j]),
level: _timeline.data[i].experience[j].meta?.level || 0, // TODO: Calculate overlap & give levels based on that
};
}
_timeline.data[i].meta = { depth: depth + 1 };
}

setProcessedTimeline({ ..._timeline });
}, [yearWidth]);

const calculateYearWidth = () => {
if (!yearsRef.current || !range) return;

const width = yearsRef?.current?.clientWidth - 40;
const singleYearWidth = width / (range.end - range.start);
setYearWidth(singleYearWidth);
};

const calculateRange = (_timeline: ITimeline) => {
const current = new Date();
if (_timeline.data.length === 0)
return { start: current.getFullYear(), end: current.getFullYear() };

const r: IRangeYear = { start: 9999, end: -1 };
for (let i = 0; i < _timeline.data.length; i++) {
for (let j = 0; j < _timeline.data[i].experience.length; j++) {
const { start, end } = _timeline.data[i].experience[j].duration;

const s = new Date();
s.setFullYear(start.year);
s.setMonth(start.month - 1);

let e = null;
if (end) {
e = new Date();
e.setFullYear(end.year);
e.setMonth(end.month - 1);
}

if (s.getFullYear() < r.start) {
r.start = s.getFullYear();
}

if (e && e.getFullYear() > r.end) {
r.end = e.getFullYear();
}
}
}

r.end++;
return r;
};

const calculatePositionAndWidth = (experienceItem: IExperience) => {
if (!range) return;

const start = new Date();
start.setDate(1);
start.setMinutes(0);
start.setSeconds(0);
start.setFullYear(experienceItem.duration.start.year);
start.setMonth(experienceItem.duration.start.month - 1);

const end = new Date();
end.setDate(1);
end.setMinutes(0);
end.setSeconds(0);
if (experienceItem.duration.end) {
end.setFullYear(experienceItem.duration.end.year);
end.setMonth(experienceItem.duration.end.month - 1);
}

const diff = monthDiff(start, end);
const startYear = new Date();
startYear.setFullYear(range.start);
startYear.setMonth(0);
startYear.setDate(1);
const startDiff = monthDiff(start, startYear);
const monthWidth = yearWidth / 12;

return {
left: monthWidth * startDiff,
width: monthWidth * diff,
};
};

if (!processedTimeline || !range) return <p>Loading...</p>;

return (
<Container>
{processedTimeline.data.map((item, index) => (
<TimelineEntry key={`${index}`}>
<Text>{item.type}</Text>
<TimelineExperience depth={item.meta?.depth || 1}>
{item.experience.map((exp, index) => {
return (
<ExperienceItem
theme={item.color}
key={`${index}`}
area={exp.meta?.width || 0}
left={exp.meta?.left || 0}
level={exp.meta?.level || 0}
>
{exp.company.image ? (
<div>
<Image
src={exp.company.image}
alt={exp.company.title}
layout={"fill"}
objectFit={"contain"}
/>
</div>
) : (
<>
<p>{exp.company.title}</p>
</>
)}
</ExperienceItem>
);
})}
</TimelineExperience>
</TimelineEntry>
))}
<TimelineYears ref={yearsRef}>
{[...Array(range.end - range.start + 1)].map((_, index) => (
<TimelineYearLabel key={`${index}`}>
{index % 2 === 1 ? "" : index + range.start}
</TimelineYearLabel>
))}
</TimelineYears>
</Container>
);
};

export default Timeline;
Loading