diff --git a/components/Contact.tsx b/components/Contact.tsx index 9393388..54d5274 100644 --- a/components/Contact.tsx +++ b/components/Contact.tsx @@ -12,6 +12,7 @@ import { Intro } from './layout/StyledLayoutComponents'; import LinkedInIcon from '@material-ui/icons/LinkedIn'; import GitHubIcon from '@material-ui/icons/GitHub'; import InstagramIcon from '@material-ui/icons/Instagram'; +import TwitterIcon from '@material-ui/icons/Twitter'; import SendIcon from '@material-ui/icons/Send'; import { Formik, Form } from 'formik'; import { MyTextField } from './formik/TextField'; @@ -25,7 +26,7 @@ const validationSchema = yup.object({ const Contact = () => { return ( - + { - + - + - + + + + diff --git a/components/ProjectData.tsx b/components/ProjectData.tsx index 9d533f6..345b8a1 100644 --- a/components/ProjectData.tsx +++ b/components/ProjectData.tsx @@ -8,6 +8,7 @@ export const ProjectData: Array = [ description: 'Data Visualization looking at plastic recycling', href: '/lifeofplastic', category: ['Code', 'Design'], + image: '/public/images/thumb_lifeofplastic.png', github: 'https://github.com/interactivethings/LifeofPlastic', link: 'https://life-of-plastic.interactivethings.io/', }, @@ -18,6 +19,7 @@ export const ProjectData: Array = [ description: 'Data Visualization looking at Vietnamese Cuisine', href: '/lifeofplastic', category: ['Design'], + image: '/public/images/thumb_lifeofplastic.png', link: 'https://life-of-plastic.interactivethings.io/', }, { @@ -27,6 +29,7 @@ export const ProjectData: Array = [ description: 'Cateloging the presentations from the Visualized Conference', href: '/lifeofplastic', category: ['Design', 'Code'], + image: '/public/images/thumb_lifeofplastic.png', link: 'https://life-of-plastic.interactivethings.io/', }, { @@ -36,6 +39,7 @@ export const ProjectData: Array = [ description: 'Cateloging the presentations from the Visualized Conference', href: '/lifeofplastic', category: ['Design', 'Code'], + image: '/public/images/thumb_lifeofplastic.png', link: 'https://life-of-plastic.interactivethings.io/', }, ]; diff --git a/components/Projects.tsx b/components/Projects.tsx index 4d97fd3..29050b9 100644 --- a/components/Projects.tsx +++ b/components/Projects.tsx @@ -19,6 +19,7 @@ export interface Project { description: string; href: string; category: Array; + image: string; github?: string; link?: string; } diff --git a/components/d3/TimeLine.tsx b/components/d3/TimeLine.tsx index d90f398..ce113d6 100644 --- a/components/d3/TimeLine.tsx +++ b/components/d3/TimeLine.tsx @@ -1,46 +1,201 @@ -import React, { useRef, useState, useEffect } from 'react'; -import { select, scaleTime, axisLeft } from 'd3'; +import React, { useRef, useEffect } from 'react'; +import { select, scaleTime, axisLeft, timeYear, timeFormat, min } from 'd3'; +import { DarkLinenPaper } from '../layout/StyledLayoutComponents'; +export interface Occupation { + id: string; + selected: boolean; + title: string; + category: keyof Category; + tag: Array; + start: Date; + end: Date; +} + +export interface LifeEvent { + id: string; + title: string; + date: Date; +} + +interface Category { + Education: boolean; + Work: boolean; + Volunteer: boolean; +} + +interface Tag { + architect: boolean; + landscaper: boolean; + baker: boolean; + coder: boolean; + farmer: boolean; + manager: boolean; +} const dim = { width: 400, height: 800, marginLeft: 100, + background: '#f6f3f0', }; -const TimeLine = () => { +interface Props { + width: number; + height: number; + occupations: Array; + events: Array; + background: string; +} + +const TimeLine: React.FC = ({ + width, + height, + occupations, + events, + background, +}) => { const svgRef = useRef(null); - const [data] = useState(timelineData); + + const lookupInRange = ( + corner: Date, + allValues: Array + ): boolean => { + let result: boolean = false; + allValues.forEach((i) => { + if (corner > i.start && corner < i.end) { + result = true; + return true; + } + }); + return result; + }; + + const categoryColor = (category: keyof Category) => { + switch (category) { + case 'Work': + return '#CBE0F2'; + case 'Education': + return '#EECEC9'; + case 'Volunteer': + return '#F0E2CE'; + } + }; + + const orderInRange = ( + value: Occupation, + backArg: any, + midArg: any, + frontArg: any + ): boolean | number | string => { + if ( + lookupInRange(value.end, occupations) && + lookupInRange(value.start, occupations) + ) { + return backArg; + } else if (lookupInRange(value.start, occupations)) { + return midArg; + } else { + return frontArg; + } + }; useEffect(() => { const svg = select(svgRef.current); const yScale = scaleTime() - .domain([new Date('2018-01-01'), new Date('2020-07-22')]) - .range([dim.height, 0]); + .domain([ + min(occupations, (d) => d.start) || new Date('1988-04-18'), + new Date(), + ]) + .range([height, 0]); - const yAxis = axisLeft(yScale); + const yAxis = axisLeft(yScale) + .ticks(timeYear, 1) + .tickFormat(timeFormat('%Y')); const Axis = svg.append('g'); Axis.style('transform', `translateX(${dim.marginLeft}px)`).call(yAxis); - const Boxes = svg.append('g'); - Boxes.selectAll('rect') - .data(data) - .join('rect') - .attr('x', 100) + const BackBoxes = svg + .append('g') + .selectAll('rect') + .data(occupations.filter((d) => orderInRange(d, true, false, false))); + BackBoxes.join('rect') + .transition() + .attr('x', 115) + .attr('y', (value) => yScale(+value.end)) + .attr('width', 35) + .attr('height', (value) => yScale(+value.start) - yScale(+value.end)) + .attr('fill', (value) => + value.selected ? categoryColor(value.category) : 'none' + ) + .attr('stroke', (value) => + value.selected ? background : categoryColor(value.category) + ) + .attr('stroke-width', '2px'); + + const MidBoxes = svg + .append('g') + .selectAll('rect') + .data(occupations.filter((d) => orderInRange(d, false, true, false))); + MidBoxes.join('rect') + .transition() + .attr('x', 110) + .attr('y', (value) => yScale(+value.end)) + .attr('width', 30) + .attr('height', (value) => yScale(+value.start) - yScale(+value.end)) + .attr('fill', (value) => + value.selected ? categoryColor(value.category) : 'none' + ) + .attr('stroke', (value) => + value.selected ? background : categoryColor(value.category) + ) + .attr('stroke-width', '2px'); + + const FrontBoxes = svg + .append('g') + .selectAll('rect') + .data(occupations.filter((d) => orderInRange(d, false, false, true))); + FrontBoxes.join('rect') + .transition() + .attr('x', 105) .attr('y', (value) => yScale(+value.end)) - .attr('width', (_, i) => i * 10 + 25) + .attr('width', 25) .attr('height', (value) => yScale(+value.start) - yScale(+value.end)) - .attr('fill', 'teal') - .attr('stroke', 'lightgrey'); - }, [data]); + .attr('fill', (value) => + value.selected ? categoryColor(value.category) : 'none' + ) + .attr('stroke', (value) => + value.selected ? background : categoryColor(value.category) + ) + .attr('stroke-width', '2px'); + + const LifeEvents = svg.append('g').selectAll('line').data(events); + LifeEvents.join('line') + .transition() + .attr('x1', 25) + .attr('x2', width - 25) + .attr('y1', (value) => yScale(value.date)) + .attr('y2', (value) => yScale(value.date)) + .attr('stroke', 'black') + .attr('stroke-dasharray', '5,10,5'); + + LifeEvents.join('text') + .transition() + .attr('x', 5) + .attr('y', (value) => yScale(value.date) - 5) + .text((value) => value.title) + .attr('font-family', 'Josefin Sans, serif') + .attr('font-size', '0.6em') + .attr('fill', DarkLinenPaper); + }, [occupations]); return (
@@ -48,34 +203,3 @@ const TimeLine = () => { }; export default TimeLine; - -const timelineData = [ - { - id: '001', - title: 'number 1', - category: 'Code', - start: new Date('2020-01-01'), - end: new Date('2020-03-23'), - }, - { - id: '002', - title: 'number 2', - category: 'Design', - start: new Date('2018-06-02'), - end: new Date('2019-01-01'), - }, - { - id: '003', - title: 'number 3', - category: 'Other', - start: new Date('2019-04-29'), - end: new Date('2019-12-31'), - }, - { - id: '004', - title: 'number 4', - category: 'Other', - start: new Date('2019-02-29'), - end: new Date('2019-5-31'), - }, -]; diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx index d6aa7f5..4b47392 100644 --- a/components/layout/Navbar.tsx +++ b/components/layout/Navbar.tsx @@ -6,10 +6,10 @@ const Navbar = () => { return (
{ padding: 14px 16px; text-decoration: none; color: #8b7a70; - font-family: 'Josefin Slab', serif; + font-family: 'Josefin Sans', serif; font-size: 1em; } a.title { @@ -29,16 +29,75 @@ const Navbar = () => { font-family: 'Raleway', sans-serif; font-size: 1.5em; } + /* The dropdown container */ + .dropdown { + float: right; + overflow: hidden; + } + + /* Dropdown button */ + .dropdown .dropbtn { + color: #8b7a70; + font-family: 'Josefin Sans', serif; + font-size: 1em; + border: none; + outline: none; + padding: 14px 16px; + background-color: inherit; + margin: 0; /* Important for vertical align on mobile phones */ + } + + /* Dropdown content (hidden by default) */ + .dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.2); + z-index: 1; + } + + /* Links inside the dropdown */ + .dropdown-content a { + float: none; + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; + text-align: left; + } + + /* Add a grey background color to dropdown links on hover */ + .dropdown-content a:hover { + background-color: #ddd; + } + + /* Show the dropdown menu on hover */ + .dropdown:hover .dropdown-content { + display: block; + } `} > - - Lloyd Richards - - About - Projects - Blog - CV + + Lloyd Richards + Contact + CV +
+ +
+ Recent + Blog +
+
+
+ + +
+ About
); }; diff --git a/components/layout/StyledLayoutComponents.tsx b/components/layout/StyledLayoutComponents.tsx index d5821bb..a330e34 100644 --- a/components/layout/StyledLayoutComponents.tsx +++ b/components/layout/StyledLayoutComponents.tsx @@ -1,5 +1,9 @@ import styled from '@emotion/styled'; +export const LinenPaper = '#f6f3f0'; +export const DarkLinenPaper = '#8B7A70'; +export const Unselected = "#E7DFD8" + export const FullWidthBackground = styled.div` width: 100vw; background: #f6f3f0; @@ -24,6 +28,14 @@ export const H2 = styled.h2` color: black; font-family: 'Josefin Sans', serif; font-size: 2em; + a { + text-decoration: none; + color: ${Unselected}; + :hover { + text-decoration: underline; + color: ${DarkLinenPaper}; + } + } `; export const H3 = styled.h2` diff --git a/pages/blog/index.tsx b/pages/blog/index.tsx index 125a58a..d841568 100644 --- a/pages/blog/index.tsx +++ b/pages/blog/index.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Grid, Card, Button, CardHeader } from '@material-ui/core'; -import {} from '../../components/layout/StyledLayoutComponents'; +import { H2 } from '../../components/layout/StyledLayoutComponents'; import { useRouter } from 'next/router'; import { BlogData } from '../../components/BlogData'; import Layout from '../../components/layout/Layout'; @@ -26,6 +26,7 @@ function Blog() { return ( +

Blog

{posts.map((i) => ( diff --git a/pages/experiment/019.tsx b/pages/experiment/019.tsx index c846473..8263d21 100644 --- a/pages/experiment/019.tsx +++ b/pages/experiment/019.tsx @@ -1,7 +1,9 @@ import React, { useRef, useEffect, useState } from 'react'; import Layout from '../../components/layout/Layout'; import * as d3 from 'd3'; -import TimeLine from '../../components/d3/TimeLine'; +import TimeLine, { Occupation, LifeEvent } from '../../components/d3/TimeLine'; +import { LinenPaper } from '../../components/layout/StyledLayoutComponents'; +import { Grid } from '@material-ui/core'; const Experiment019 = () => { const svgRef = useRef(null); @@ -70,9 +72,72 @@ const Experiment019 = () => { as a list beside the timeline and then using D3, build the timeline and draw lines that connect to the appropriate list item.

- + + + + + +

+ This feels really good. I've built a TimeLine.tsx{' '} + component that can take in props for the data, including occupations + and events which it then renders into this horizontal timeline. The + graph scales to the size of the data and even orders the boxes and + colours them accoring to their overlap and props. So cool to see + this come together so nicely! +

+
+
); }; export default Experiment019; + +const eventSampleData: Array = [ + { id: '001', title: 'Moved to Switzerland', date: new Date('2020-01-03') }, +]; + +const occupationSampleData: Array = [ + { + id: '001', + selected: true, + title: 'number 1', + category: 'Work', + tag: ['coder'], + start: new Date('2019-12-01'), + end: new Date('2020-03-23'), + }, + { + id: '002', + selected: false, + title: 'number 2', + category: 'Education', + tag: ['coder'], + start: new Date('2018-06-02'), + end: new Date('2019-01-01'), + }, + { + id: '003', + selected: true, + title: 'number 3', + category: 'Work', + tag: ['coder'], + start: new Date('2019-04-29'), + end: new Date('2019-12-31'), + }, + { + id: '004', + selected: true, + title: 'number 4', + category: 'Volunteer', + tag: ['coder'], + start: new Date('2019-02-29'), + end: new Date('2019-06-31'), + }, +]; diff --git a/pages/index.tsx b/pages/index.tsx index ae80eb0..531c433 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -49,7 +49,7 @@ const IndexPage: React.FC = () => (
-
+

About

@@ -84,12 +84,14 @@ const IndexPage: React.FC = () => (
-
-

Recent

+
+

+ Recent / Portfolio +

( }} > -

Blog

+

+ Recent / Blog +

-
+

CV

diff --git a/pages/portfolio/index.tsx b/pages/portfolio/index.tsx new file mode 100644 index 0000000..e237494 --- /dev/null +++ b/pages/portfolio/index.tsx @@ -0,0 +1,153 @@ +import React, { useState, useEffect } from 'react'; +import { + Grid, + Card, + CardContent, + CardActions, + Button, + ButtonGroup, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import ExpandLessIcon from '@material-ui/icons/ExpandLess'; +import { + H3, + Description, + H2, +} from '../../components/layout/StyledLayoutComponents'; +import { ProjectData } from '../../components/ProjectData'; +import Layout from '../../components/layout/Layout'; + +export interface Project { + id: string; + date: Date; + title: string; + description: string; + href: string; + category: Array; + github?: string; + link?: string; +} + +interface Category { + Design: boolean; + Garden: boolean; + Code: boolean; + Other: boolean; +} + +const Projects = () => { + const [currentProjects, setCurrentProjects] = useState>( + ProjectData.sort((a, b) => +b.date - +a.date) + ); + const [categories, setCategories] = useState({ + Design: true, + Garden: true, + Code: true, + Other: true, + }); + const [expanded, setExpanded] = useState(false); + useEffect(() => { + setCurrentProjects( + ProjectData.filter((i) => i.category.some((r) => categories[r] === true)) + ); + }, [categories]); + return ( + +

Portfolio

+ + + + + + + + {currentProjects.map((i) => ( + + + +

{i.title}

+ {i.description} +
+ + {i.github ? ( + + ) : null} + + +
+
+ ))} +
+
+ ); +}; + +export default Projects; diff --git a/public/images/thumb_lifeofplastic.png b/public/images/thumb_lifeofplastic.png new file mode 100644 index 0000000..508392e Binary files /dev/null and b/public/images/thumb_lifeofplastic.png differ