Skip to content

Commit

Permalink
feat(portfolio): ✨ added Timeline
Browse files Browse the repository at this point in the history
  • Loading branch information
lloydrichards committed Jul 2, 2023
1 parent 41f30d8 commit ddd7238
Show file tree
Hide file tree
Showing 20 changed files with 482 additions and 36 deletions.
6 changes: 6 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ const config: StorybookConfig = {
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
{
name: "@storybook/addon-styling",
options: {
postCss: true,
},
},
],
framework: {
name: "@storybook/nextjs",
Expand Down
5 changes: 3 additions & 2 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Preview } from "@storybook/react";
import React from "react";
import "../src/styles/globals.css";

// This is where you can wrap the story in any ContextProviders
export const decorators = [
Expand All @@ -14,7 +15,7 @@ const preview: Preview = {
parameters: {
backgrounds: {
default: "light",
// <- can add more themes here
// <- can add more themes here
},
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
Expand All @@ -26,4 +27,4 @@ const preview: Preview = {
},
};

export default preview;
export default preview;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@storybook/addon-essentials": "^7.0.12",
"@storybook/addon-interactions": "^7.0.12",
"@storybook/addon-links": "^7.0.12",
"@storybook/addon-styling": "^1.3.2",
"@storybook/blocks": "^7.0.12",
"@storybook/nextjs": "^7.0.12",
"@storybook/react": "^7.0.12",
Expand Down
25 changes: 25 additions & 0 deletions src/app/TimelineSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";
import { Timeline } from "@/components/charts/timeline/Timeline";
import { ResponsiveChart } from "@/components/charts/utils/ResponsiveChart";
import { allOccupations } from "contentlayer/generated";
import { isAfter, subYears } from "date-fns";

export const TimelineSection = () => {
return (
<section className="min-h-96 prose mt-8 w-full px-2">
<h2 className="font-serif">CV Timeline</h2>
<ResponsiveChart
className="prose h-full w-full"
render={({ width, height }) => (
<Timeline
data={allOccupations.filter((d) =>
isAfter(new Date(d.start_date), subYears(new Date(), 5))
)}
width={width}
maxHeight={height}
/>
)}
/>
</section>
);
};
11 changes: 2 additions & 9 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SpotlightProjects } from "@/components/projects/spotlight_projects/Spot
import { allBlogs, allLabs, allProjects } from "contentlayer/generated";
import Image from "next/image";
import { TbBarrierBlock } from "react-icons/tb";
import { TimelineSection } from "./TimelineSection";

export default function Home() {
return (
Expand Down Expand Up @@ -42,15 +43,7 @@ export default function Home() {
<div className="w-full bg-accent px-8 py-8">
<RecentPosts posts={[...allLabs, ...allBlogs]} />
</div>
<section className="min-h-96 prose mt-8 w-full px-2">
<h2 className="font-serif">Timeline</h2>
<div className="min-h-96 not-prose rounded bg-secondary px-4 py-6">
<div className="card-body text-error-content flex-row items-center justify-center">
<TbBarrierBlock size={34} />
<h3 className="text-xl font-bold">Currently Under Construction</h3>
</div>
</div>
</section>
<TimelineSection />
</main>
);
}
17 changes: 14 additions & 3 deletions src/app/timeline/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
const AboutPage = () => {
"use client";
import { Timeline } from "@/components/charts/timeline/Timeline";
import { ResponsiveChart } from "@/components/charts/utils/ResponsiveChart";
import { allOccupations } from "contentlayer/generated";

const TimelinePage = () => {
return (
<main className="prose flex min-h-screen flex-col items-center p-16">
<main className="mb-8 flex min-h-screen flex-col items-center gap-8">
<h1>Timeline Page</h1>
<ResponsiveChart
className="prose h-full w-full"
render={({ width, height }) => (
<Timeline data={allOccupations} width={width} maxHeight={height} />
)}
/>
</main>
);
};

export default AboutPage;
export default TimelinePage;
6 changes: 2 additions & 4 deletions src/components/Mdx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { cn } from "@/lib/utils";
import "@/styles/mdx.css";
import { useMDXComponent } from "next-contentlayer/hooks";
import Image from "next/image";

const components = {
h1: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
Expand Down Expand Up @@ -86,7 +85,7 @@ const components = {
}: React.HTMLAttributes<HTMLQuoteElement>) => (
<blockquote
className={cn(
"[&>*]:text-muted-foreground mt-6 border-l-2 pl-6 italic",
"mt-6 border-l-2 pl-6 italic [&>*]:text-muted-foreground",
className
)}
{...props}
Expand All @@ -110,7 +109,7 @@ const components = {
),
tr: ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr
className={cn("even:bg-muted m-0 border-t p-0", className)}
className={cn("m-0 border-t p-0 even:bg-muted", className)}
{...props}
/>
),
Expand Down Expand Up @@ -150,7 +149,6 @@ const components = {
{...props}
/>
),
Image,
};

interface MdxProps {
Expand Down
31 changes: 31 additions & 0 deletions src/components/charts/timeline/Timeline.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Meta, StoryObj } from "@storybook/react";
import { Timeline } from "./Timeline";
import { allOccupations } from "../../../../.contentlayer/generated";
import { ResponsiveChart } from "../utils/ResponsiveChart";

export default {
title: "charts/Timeline",
component: Timeline,
argTypes: {},
parameters: {
controls: {
exclude: ["data"],
},
},
} as Meta<typeof Timeline>;

export const Base: StoryObj<typeof Timeline> = {
render: () => (
<ResponsiveChart
className="h-screen"
render={({ width, height }) => (
<Timeline data={allOccupations} width={width} maxHeight={height} />
)}
/>
),

args: {
data: allOccupations,
},
play: () => {},
};
139 changes: 139 additions & 0 deletions src/components/charts/timeline/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Occupation } from "../../../../.contentlayer/generated";
import { curveStep, line, min, scaleOrdinal, scaleTime } from "d3";
import { isBefore } from "date-fns";
import { FC } from "react";
import { AxisLeft } from "@visx/axis";
import { OccupationItem } from "./internal/OccupationItem";

interface TimelineProps {
data: Occupation[];
maxHeight?: number;
width: number;
margin?: {
top: number;
right: number;
bottom: number;
left: number;
};
}

export const Timeline: FC<TimelineProps> = ({
data,
maxHeight,
width,
margin = {
top: 16,
right: 0,
bottom: 16,
left: 48,
},
}) => {
const textBlockHeight = 160;
const textMargin = 180;
const height = data.length * textBlockHeight || maxHeight || 400;
const innerHeight = height - margin.top - margin.bottom;
const innerWidth = width - margin.left - margin.right;

// Scales
const yScale = scaleTime()
.domain([
min(data, (d) => new Date(d.start_date)) || new Date("1988-04-18"),
new Date(),
])
.range([innerHeight, margin.bottom]);
const xScale = scaleOrdinal<number, number>()
.domain([0, 1, 2])
.range([margin.left, margin.left + 16, margin.left + 32]);
const cScale = scaleOrdinal<string>()
.domain(Array.from(new Set(data.map((d) => d.category))))
.range(["#CBE0F2", "#EECEC9", "#F0E2CE"]);

// Helpers
const lookupInRange = (
corner: Date,
allValues: Array<Occupation>
): boolean => {
let result: boolean = false;
allValues.forEach((i) => {
if (
corner > new Date(i.start_date) &&
corner < new Date(i.end_date || new Date())
) {
result = true;
return true;
}
});
return result;
};

const dataWithChannels = data
.sort((a, b) =>
isBefore(new Date(a.start_date), new Date(b.start_date)) ? 1 : -1
)
.map((d) => {
if (
lookupInRange(new Date(d.end_date || new Date()), data) &&
lookupInRange(new Date(d.start_date), data)
) {
return { ...d, channel: 2 };
} else if (lookupInRange(new Date(d.start_date), data)) {
return { ...d, channel: 1 };
} else {
return { ...d, channel: 0 };
}
});

const lineConnector = line()
.curve(curveStep)
.x((d) => d[0])
.y((d) => d[1]);

return (
<div
style={{
height,
width,
maxHeight: maxHeight || undefined,
overflow: "scroll",
}}
>
<svg width={width} height={height}>
{dataWithChannels.map((d, idx) => {
{
const startDate = new Date(d.start_date);
const endDate = new Date(d.end_date || new Date());
const y1 = yScale(startDate);
const y2 = yScale(endDate);
const height = y1 - y2;
return (
<OccupationItem
key={idx}
data={d}
idx={idx}
x={xScale(d.channel)}
y={y2}
barHeight={height}
textHeight={textBlockHeight}
width={innerWidth}
textMargin={textMargin}
color={cScale(d.category)}
path={
lineConnector([
[xScale(d.channel) + 48, y1 - height / 2],
[textMargin, idx * textBlockHeight + textBlockHeight / 2],
]) || ""
}
/>
);
}
})}
<AxisLeft
scale={yScale}
top={margin.top}
left={margin.left - 2}
strokeWidth={2}
/>
</svg>
</div>
);
};
42 changes: 42 additions & 0 deletions src/components/charts/timeline/internal/OccupationCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Occupation } from "../../../../../.contentlayer/generated";
import { FC } from "react";
import { cn } from "@/lib/utils";
import { format } from "date-fns";

interface OccupationCardProps {
data: Occupation;
className?: string;
}
export const OccupationCard: FC<OccupationCardProps> = ({
data,
className,
}) => {
const formatDate = (date?: string) =>
date && format(new Date(date), "MMM yyyy");
return (
<Card
className={cn("h-full border-none bg-transparent shadow-none", className)}
>
<CardHeader className="p-2">
<CardTitle className="my-0 line-clamp-1">{data.title}</CardTitle>
<CardDescription className="text-sm">{data.company}</CardDescription>
<CardDescription className="text-sm">
{formatDate(data.start_date)} -{" "}
{formatDate(data.end_date) || "Present"}
</CardDescription>
</CardHeader>
<CardContent className="px-2">
<CardDescription className="my-0 line-clamp-3">
{data.description}
</CardDescription>
</CardContent>
</Card>
);
};
Loading

1 comment on commit ddd7238

@vercel
Copy link

@vercel vercel bot commented on ddd7238 Jul 2, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.