Archive
A big list of things I’ve worked on
| Year | Title | Made at | Built with | Link |
|---|---|---|---|---|
| {`${new Date(date).getFullYear()}`} | {title} | {company ? {company} : —} | {tech?.length > 0 && tech.map((item, i) => ( {item} {''} {i !== tech.length - 1 && ·} ))} |
Repository: bchiang7/v4 Branch: main Commit: 539cef0bf60c Files: 123 Total size: 168.9 KB Directory structure: gitextract_o5gnh8vu/ ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .husky/ │ ├── .gitignore │ └── pre-commit ├── .nvmrc ├── LICENSE ├── README.md ├── content/ │ ├── featured/ │ │ ├── HalcyonTheme/ │ │ │ └── index.md │ │ ├── SpotifyProfile/ │ │ │ └── index.md │ │ └── SpotifyProfileV2/ │ │ └── index.md │ ├── jobs/ │ │ ├── Apple/ │ │ │ └── index.md │ │ ├── Mullen/ │ │ │ └── index.md │ │ ├── Scout/ │ │ │ └── index.md │ │ ├── Starry/ │ │ │ └── index.md │ │ └── Upstatement/ │ │ └── index.md │ ├── posts/ │ │ ├── clickable-cards/ │ │ │ └── index.md │ │ ├── dark-mode-toggle/ │ │ │ └── index.md │ │ ├── docker-compose-error/ │ │ │ └── index.md │ │ ├── markdown-playground/ │ │ │ └── index.md │ │ └── wordpress-publish-error/ │ │ └── index.md │ └── projects/ │ ├── AMFM.md │ ├── AlgoliaWordPressMediumPost.md │ ├── AppleMusicEmbedPlayer.md │ ├── Blistabloc.md │ ├── CourseSource.md │ ├── CrowdDJ.md │ ├── Devoted.md │ ├── EverytownIdealState.md │ ├── Flagship.md │ ├── Fontipsums.md │ ├── GoogleKeepClone.md │ ├── HBS.md │ ├── HalcyonTheme.md │ ├── HeadlessCMSMediumPost.md │ ├── Interventions.md │ ├── JetBlueHumanKinda.md │ ├── KoalaHealth.md │ ├── LonelyPlanetDBMS.md │ ├── MichelleWu.md │ ├── MomsDemandAction.md │ ├── MyNEURedesign.md │ ├── NUWITSite.md │ ├── NortheasternCSSH.md │ ├── OctoProfile.md │ ├── OneCardForAll.md │ ├── PhillySports.md │ ├── Pratt.md │ ├── ReactResume.md │ ├── Screentime.md │ ├── SpotifyProfile.md │ ├── SpotifyTopTracks2017.md │ ├── The19th.md │ ├── Threadable.md │ ├── TimeToHaveMoreFun.md │ ├── UpstatementDotCom.md │ ├── Vanderbilt.md │ ├── WeatherWidget.md │ ├── v1.md │ ├── v2.md │ └── v3.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package.json ├── prettier.config.js └── src/ ├── components/ │ ├── email.js │ ├── footer.js │ ├── head.js │ ├── icons/ │ │ ├── appstore.js │ │ ├── bookmark.js │ │ ├── codepen.js │ │ ├── external.js │ │ ├── folder.js │ │ ├── fork.js │ │ ├── github.js │ │ ├── hex.js │ │ ├── icon.js │ │ ├── index.js │ │ ├── instagram.js │ │ ├── linkedin.js │ │ ├── loader.js │ │ ├── logo.js │ │ ├── playstore.js │ │ ├── star.js │ │ └── twitter.js │ ├── index.js │ ├── layout.js │ ├── loader.js │ ├── menu.js │ ├── nav.js │ ├── sections/ │ │ ├── about.js │ │ ├── contact.js │ │ ├── featured.js │ │ ├── hero.js │ │ ├── jobs.js │ │ └── projects.js │ ├── side.js │ └── social.js ├── config.js ├── hooks/ │ ├── index.js │ ├── useOnClickOutside.js │ ├── usePrefersReducedMotion.js │ └── useScrollDirection.js ├── pages/ │ ├── 404.js │ ├── archive.js │ ├── index.js │ └── pensieve/ │ ├── index.js │ └── tags.js ├── styles/ │ ├── GlobalStyle.js │ ├── PrismStyles.js │ ├── TransitionStyles.js │ ├── fonts.js │ ├── index.js │ ├── mixins.js │ ├── theme.js │ └── variables.js ├── templates/ │ ├── post.js │ └── tag.js └── utils/ ├── index.js └── sr.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "@babel/preset-react", "babel-preset-gatsby" ] } ================================================ FILE: .editorconfig ================================================ [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .eslintrc ================================================ { "root": true, "extends": "@upstatement/eslint-config/react" } ================================================ FILE: .gitignore ================================================ # Project dependencies .cache node_modules yarn-error.log package-lock.json # Build directory /public .DS_Store .vscode/ ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" npm run lint-staged ================================================ FILE: .nvmrc ================================================ 14.16.0 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Brittany Chiang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
The fourth iteration of brittanychiang.com built with Gatsby and hosted with Netlify
Previous iterations: v1, v2, v3
 ## 🚨 Forking this repo (please read!) Many people have contacted me asking me if they can use this code for their own website, and the answer to that question is usually **yes, with attribution**. I value keeping my site open source, but as you all know, _**plagiarism is bad**_. It's always disheartening whenever I find that someone has copied my site without giving me credit. I spent a non-trivial amount of effort building and designing this iteration of my website, and I am proud of it! All I ask of you all is to not claim this effort as your own. Please also note that I did not build this site with the intention of it being a starter theme, so if you have questions about implementation, please refer to the [Gatsby docs](https://www.gatsbyjs.org/docs/). ### TL;DR Yes, you can fork this repo. Please give me proper credit by linking back to [brittanychiang.com](https://brittanychiang.com). Thanks! ## 🛠 Installation & Set Up 1. Install the Gatsby CLI ```sh npm install -g gatsby-cli ``` 2. Install and use the correct version of Node using [NVM](https://github.com/nvm-sh/nvm) ```sh nvm install ``` 3. Install dependencies ```sh yarn ``` 4. Start the development server ```sh npm start ``` ## 🚀 Building and Running for Production 1. Generate a full static production build ```sh npm run build ``` 1. Preview the site as it will appear once deployed ```sh npm run serve ``` ## 🎨 Color Reference | Color | Hex | | -------------- | ------------------------------------------------------------------ | | Navy |  `#0a192f` | | Light Navy |  `#112240` | | Lightest Navy |  `#233554` | | Slate |  `#8892b0` | | Light Slate |  `#a8b2d1` | | Lightest Slate |  `#ccd6f6` | | White |  `#e6f1ff` | | Green |  `#64ffda` | ================================================ FILE: content/featured/HalcyonTheme/index.md ================================================ --- date: '1' title: 'Halcyon Theme' cover: './halcyon.png' github: 'https://github.com/bchiang7/halcyon-site' external: 'https://halcyon-theme.netlify.com/' tech: - VS Code - Sublime Text - Atom - iTerm2 - Hyper --- A minimal, dark blue theme for VS Code, Sublime Text, Atom, iTerm, and more. Available on [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=brittanychiang.halcyon-vscode), [Package Control](https://packagecontrol.io/packages/Halcyon%20Theme), [Atom Package Manager](https://atom.io/themes/halcyon-syntax), and [npm](https://www.npmjs.com/package/hyper-halcyon-theme). ================================================ FILE: content/featured/SpotifyProfile/index.md ================================================ --- date: '2' title: 'Spotify Profile' cover: './demo.png' github: 'https://github.com/bchiang7/spotify-profile' external: 'https://spotify-profile.herokuapp.com/' tech: - React - Styled Components - Express - Spotify API - Heroku --- A web app for visualizing personalized Spotify data. View your top artists, top tracks, recently played tracks, and detailed audio information about each track. Create and save new playlists of recommended tracks based on your existing playlists and more. ================================================ FILE: content/featured/SpotifyProfileV2/index.md ================================================ --- date: '3' title: 'Build a Spotify Connected App' cover: './course-card.png' external: 'https://www.newline.co/courses/build-a-spotify-connected-app' cta: 'https://www.newline.co/courses/build-a-spotify-connected-app' tech: - React - Express - Spotify API - Styled Components --- Having struggled with understanding how the Spotify OAuth flow works, I made the course I wish I could have had. Unlike tutorials that only cover a few concepts and leave you with half-baked GitHub repositories, this course covers everything from explaining the principles of REST APIs to implementing Spotify's OAuth flow and fetching API data in a React app. By the end of the course, you’ll have an app deployed to the internet you can add to your portfolio. ================================================ FILE: content/jobs/Apple/index.md ================================================ --- date: '2017-12-21' title: 'UI Engineer Co-op' company: 'Apple' location: 'Cupertino, CA' range: 'July - December 2017' url: 'https://www.apple.com/music/' --- - Developed and styled interactive web applications for Apple Music using Ember and SCSS - Built and shipped the Apple Music Extension for Facebook Messenger leveraging third-party and internal API integrations - Architected and implemented the user interface of Apple Music's embeddable web player widget for in-browser user authorization and full song playback - Contributed extensively to the creation of MusicKit JS, a public-facing JavaScript SDK for embedding Apple Music players into web applications ================================================ FILE: content/jobs/Mullen/index.md ================================================ --- date: '2015-12-21' title: 'Creative Technologist Co-op' company: 'MullenLowe' location: 'Boston, MA' range: 'July - December 2015' url: 'https://us.mullenlowe.com/' --- - Developed, maintained, and shipped production code for client websites primarily using HTML, CSS, Sass, JavaScript, and jQuery - Performed quality assurance tests on various sites to ensure cross-browser compatibility and mobile responsiveness - Clients included JetBlue, Lovesac, U.S. Cellular, U.S. Department of Defense, and more ================================================ FILE: content/jobs/Scout/index.md ================================================ --- date: '2017-04-01' title: 'Developer' company: 'Scout Studio' location: 'Northeastern University' range: 'Spring 2016 & 2017' url: 'https://web.northeastern.edu/scout/' --- - Collaborated with other student designers and engineers on pro-bono projects to create new brands, design systems, and websites for organizations in the community - Built and delivered technical solutions according to stakeholder business requirements ================================================ FILE: content/jobs/Starry/index.md ================================================ --- date: '2016-12-21' title: 'Software Engineer Co-op' company: 'Starry' location: 'Boston, MA' range: 'July - December 2016' url: 'https://starry.com/' --- - Engineered and improved major features of Starry's customer-facing Android web app using ES6, Handlebars, Backbone, Marionette, and CSS - Proposed and implemented scalable solutions to issues identified with cloud services and applications responsible for communicating with the Starry Station internet router - Collaborated with designers and other developers to ensure thoughtful and consistent user experiences across Starry’s iOS and Android mobile apps ================================================ FILE: content/jobs/Upstatement/index.md ================================================ --- date: '2018-05-14' title: 'Lead Engineer' company: 'Upstatement' location: 'Boston, MA' range: 'May 2018 - Present' url: 'https://www.upstatement.com/' --- - Deliver high-quality, robust production code for a diverse array of projects for clients including Harvard Business School, Everytown for Gun Safety, Pratt Institute, Koala Health, Vanderbilt University, The 19th News, and more - Work alongside creative directors to lead the research, development, and architecture of technical solutions to fulfill business requirements - Collaborate with designers, project managers, and other engineers to transform creative concepts into production realities for clients and stakeholders - Provide leadership within engineering department through close collaboration, knowledge shares, and mentorship ================================================ FILE: content/posts/clickable-cards/index.md ================================================ --- title: Accessible Clickable Cards description: Clickable cards with multiple child links date: 2021-04-21 draft: false slug: /pensieve/clickable-cards tags: - Accessibility - CSS --- [Codepen Demo](https://codepen.io/bchiang7/pen/xxRBvgd?editors=1100) Card layout where the card itself isn't an anchor link, but the whole card is clickable (with a `:before` pseudo element on the main ``). Links inside of the card are still clickable. ## CSS ```css .grid__item { &:hover, &:focus-within { background-color: #eee; } a { position: relative; z-index: 1; } h2 { a { position: static; &:hover, &:focus { color: blue; } &:before { content: ''; display: block; position: absolute; z-index: 0; width: 100%; height: 100%; top: 0; left: 0; transition: background-color 0.1s ease-out; background-color: transparent; } } } } ``` ================================================ FILE: content/posts/dark-mode-toggle/index.md ================================================ --- title: Dark Mode Toggle description: Dark mode without the flash of default theme date: 2021-04-21 draft: false slug: /pensieve/dark-mode-toggle tags: - Theming - Dark Mode --- Dark mode toggle without the flash of default theme. Important bits: - CSS variables for color theming - Put `data-theme` attribute on ``, not ``, so we can run the JS before the DOM finishes rendering - Run local storage check in the `` - JS for toggle button click handler can come after render ## HTML ```html ... tags containing syntax highlighting;
// defaults to 'language-' (e.g. ).
// If your site loads Prism into the browser at runtime,
// (e.g. for use with libraries like react-live),
// you may use this to prevent Prism from re-processing syntax.
// This is an uncommon use-case though;
// If you're unsure, it's best to use the default value.
classPrefix: 'language-',
// This is used to allow setting a language for inline code
// (i.e. single backticks) by creating a separator.
// This separator is a string and will do no white-space
// stripping.
// A suggested value for English speakers is the non-ascii
// character '›'.
inlineCodeMarker: null,
// This lets you set up language aliases. For example,
// setting this to '{ sh: "bash" }' will let you use
// the language "sh" which will highlight using the
// bash highlighter.
aliases: {},
// This toggles the display of line numbers globally alongside the code.
// To use it, add the following line in gatsby-browser.js
// right after importing the prism color scheme:
// require("prismjs/plugins/line-numbers/prism-line-numbers.css")
// Defaults to false.
// If you wish to only show line numbers on certain code blocks,
// leave false and use the {numberLines: true} syntax below
showLineNumbers: false,
// If setting this to true, the parser won't handle and highlight inline
// code used in markdown i.e. single backtick code like `this`.
noInlineHighlight: false,
// This adds a new language definition to Prism or extend an already
// existing language definition. More details on this option can be
// found under the header "Add new language definition or extend an
// existing language" below.
languageExtensions: [
{
language: 'superscript',
extend: 'javascript',
definition: {
superscript_types: /(SuperType)/,
},
insertBefore: {
function: {
superscript_keywords: /(superif|superelse)/,
},
},
},
],
// Customize the prompt used in shell output
// Values below are default
prompt: {
user: 'root',
host: 'localhost',
global: false,
},
},
},
],
},
},
{
resolve: `gatsby-plugin-google-analytics`,
options: {
trackingId: 'UA-45666519-2',
},
},
],
};
================================================
FILE: gatsby-node.js
================================================
/**
* Implement Gatsby's Node APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/node-apis/
*/
const path = require('path');
const _ = require('lodash');
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions;
const postTemplate = path.resolve(`src/templates/post.js`);
const tagTemplate = path.resolve('src/templates/tag.js');
const result = await graphql(`
{
postsRemark: allMarkdownRemark(
filter: { fileAbsolutePath: { regex: "/content/posts/" } }
sort: { order: DESC, fields: [frontmatter___date] }
limit: 1000
) {
edges {
node {
frontmatter {
slug
}
}
}
}
tagsGroup: allMarkdownRemark(limit: 2000) {
group(field: frontmatter___tags) {
fieldValue
}
}
}
`);
// Handle errors
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`);
return;
}
// Create post detail pages
const posts = result.data.postsRemark.edges;
posts.forEach(({ node }) => {
createPage({
path: node.frontmatter.slug,
component: postTemplate,
context: {},
});
});
// Extract tag data from query
const tags = result.data.tagsGroup.group;
// Make tag pages
tags.forEach(tag => {
createPage({
path: `/pensieve/tags/${_.kebabCase(tag.fieldValue)}/`,
component: tagTemplate,
context: {
tag: tag.fieldValue,
},
});
});
};
// https://www.gatsbyjs.org/docs/node-apis/#onCreateWebpackConfig
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
// https://www.gatsbyjs.org/docs/debugging-html-builds/#fixing-third-party-modules
if (stage === 'build-html' || stage === 'develop-html') {
actions.setWebpackConfig({
module: {
rules: [
{
test: /scrollreveal/,
use: loaders.null(),
},
{
test: /animejs/,
use: loaders.null(),
},
{
test: /miniraf/,
use: loaders.null(),
},
],
},
});
}
actions.setWebpackConfig({
resolve: {
alias: {
'@components': path.resolve(__dirname, 'src/components'),
'@config': path.resolve(__dirname, 'src/config'),
'@fonts': path.resolve(__dirname, 'src/fonts'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
'@images': path.resolve(__dirname, 'src/images'),
'@pages': path.resolve(__dirname, 'src/pages'),
'@styles': path.resolve(__dirname, 'src/styles'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
},
});
};
================================================
FILE: gatsby-ssr.js
================================================
/**
* Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/ssr-apis/
*/
// You can delete this file if you're not using it
================================================
FILE: package.json
================================================
{
"name": "v4",
"description": "Personal Website V4",
"version": "1.0.0",
"author": "Brittany Chiang ",
"repository": {
"type": "git",
"url": "https://github.com/bchiang7/v4"
},
"keywords": [
"gatsby"
],
"license": "MIT",
"browserslist": "> 0.25%, not dead",
"scripts": {
"build": "gatsby build",
"develop": "gatsby develop",
"format": "prettier --write \"**/*.{js,jsx,json,md}\"",
"start": "npm run develop",
"serve": "gatsby serve",
"clean": "gatsby clean",
"prepare": "husky install",
"lint-staged": "lint-staged"
},
"lint-staged": {
"*.{js,css,json,md}": [
"prettier --write"
],
"*.js": [
"eslint --fix"
]
},
"dependencies": {
"animejs": "^3.1.0",
"babel-plugin-styled-components": "^1.12.0",
"gatsby": "^3.4.1",
"gatsby-plugin-google-analytics": "^3.4.0",
"gatsby-plugin-image": "^1.4.0",
"gatsby-plugin-manifest": "^3.4.0",
"gatsby-plugin-netlify": "^3.4.0",
"gatsby-plugin-offline": "^4.4.0",
"gatsby-plugin-react-helmet": "^4.4.0",
"gatsby-plugin-robots-txt": "^1.5.6",
"gatsby-plugin-sharp": "^3.4.1",
"gatsby-plugin-sitemap": "^4.0.0",
"gatsby-plugin-styled-components": "^4.4.0",
"gatsby-remark-external-links": "0.0.4",
"gatsby-remark-images": "^5.1.0",
"gatsby-remark-prismjs": "^5.1.0",
"gatsby-source-filesystem": "^3.4.0",
"gatsby-transformer-remark": "^4.1.0",
"gatsby-transformer-sharp": "^3.4.0",
"lodash": "^4.17.19",
"prismjs": "^1.27.0",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-transition-group": "^4.3.0",
"scrollreveal": "^4.0.5",
"styled-components": "^5.3.0"
},
"devDependencies": {
"@babel/core": "^7.14.0",
"@babel/eslint-parser": "^7.13.14",
"@babel/preset-react": "^7.13.13",
"@upstatement/eslint-config": "^1.0.0",
"@upstatement/prettier-config": "^1.0.0",
"babel-preset-gatsby": "^1.4.0",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.23.2",
"gatsby-remark-code-titles": "^1.1.0",
"husky": "^6.0.0",
"lint-staged": "^10.1.2",
"prettier": "^2.2.1"
}
}
================================================
FILE: prettier.config.js
================================================
module.exports = require('@upstatement/prettier-config');
================================================
FILE: src/components/email.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { email } from '@config';
import { Side } from '@components';
const StyledLinkWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
position: relative;
&:after {
content: '';
display: block;
width: 1px;
height: 90px;
margin: 0 auto;
background-color: var(--light-slate);
}
a {
margin: 20px auto;
padding: 10px;
font-family: var(--font-mono);
font-size: var(--fz-xxs);
line-height: var(--fz-lg);
letter-spacing: 0.1em;
writing-mode: vertical-rl;
&:hover,
&:focus {
transform: translateY(-3px);
}
}
`;
const Email = ({ isHome }) => (
{email}
);
Email.propTypes = {
isHome: PropTypes.bool,
};
export default Email;
================================================
FILE: src/components/footer.js
================================================
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Icon } from '@components/icons';
import { socialMedia } from '@config';
const StyledFooter = styled.footer`
${({ theme }) => theme.mixins.flexCenter};
flex-direction: column;
height: auto;
min-height: 70px;
padding: 15px;
text-align: center;
`;
const StyledSocialLinks = styled.div`
display: none;
@media (max-width: 768px) {
display: block;
width: 100%;
max-width: 270px;
margin: 0 auto 10px;
color: var(--light-slate);
}
ul {
${({ theme }) => theme.mixins.flexBetween};
padding: 0;
margin: 0;
list-style: none;
a {
padding: 10px;
svg {
width: 20px;
height: 20px;
}
}
}
`;
const StyledCredit = styled.div`
color: var(--light-slate);
font-family: var(--font-mono);
font-size: var(--fz-xxs);
line-height: 1;
a {
padding: 10px;
}
.github-stats {
margin-top: 10px;
& > span {
display: inline-flex;
align-items: center;
margin: 0 7px;
}
svg {
display: inline-block;
margin-right: 5px;
width: 14px;
height: 14px;
}
}
`;
const Footer = () => {
const [githubInfo, setGitHubInfo] = useState({
stars: null,
forks: null,
});
useEffect(() => {
if (process.env.NODE_ENV !== 'production') {
return;
}
fetch('https://api.github.com/repos/bchiang7/v4')
.then(response => response.json())
.then(json => {
const { stargazers_count, forks_count } = json;
setGitHubInfo({
stars: stargazers_count,
forks: forks_count,
});
})
.catch(e => console.error(e));
}, []);
return (
Designed & Built by Brittany Chiang
{githubInfo.stars && githubInfo.forks && (
{githubInfo.stars.toLocaleString()}
{githubInfo.forks.toLocaleString()}
)}
);
};
Footer.propTypes = {
githubInfo: PropTypes.object,
};
export default Footer;
================================================
FILE: src/components/head.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useLocation } from '@reach/router';
import { useStaticQuery, graphql } from 'gatsby';
// https://www.gatsbyjs.com/docs/add-seo-component/
const Head = ({ title, description, image }) => {
const { pathname } = useLocation();
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
defaultTitle: title
defaultDescription: description
siteUrl
defaultImage: image
twitterUsername
}
}
}
`,
);
const {
defaultTitle,
defaultDescription,
siteUrl,
defaultImage,
twitterUsername,
} = site.siteMetadata;
const seo = {
title: title || defaultTitle,
description: description || defaultDescription,
image: `${siteUrl}${image || defaultImage}`,
url: `${siteUrl}${pathname}`,
};
return (
);
};
export default Head;
Head.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
image: PropTypes.string,
};
Head.defaultProps = {
title: null,
description: null,
image: null,
};
================================================
FILE: src/components/icons/appstore.js
================================================
import React from 'react';
const IconAppStore = () => (
);
export default IconAppStore;
================================================
FILE: src/components/icons/bookmark.js
================================================
import React from 'react';
const IconBookmark = () => (
);
export default IconBookmark;
================================================
FILE: src/components/icons/codepen.js
================================================
import React from 'react';
const IconCodepen = () => (
);
export default IconCodepen;
================================================
FILE: src/components/icons/external.js
================================================
import React from 'react';
const IconExternal = () => (
);
export default IconExternal;
================================================
FILE: src/components/icons/folder.js
================================================
import React from 'react';
const IconFolder = () => (
);
export default IconFolder;
================================================
FILE: src/components/icons/fork.js
================================================
import React from 'react';
const IconFork = () => (
);
export default IconFork;
================================================
FILE: src/components/icons/github.js
================================================
import React from 'react';
const IconGitHub = () => (
);
export default IconGitHub;
================================================
FILE: src/components/icons/hex.js
================================================
import React from 'react';
const IconHex = () => (
);
export default IconHex;
================================================
FILE: src/components/icons/icon.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import {
IconAppStore,
IconBookmark,
IconCodepen,
IconExternal,
IconFolder,
IconFork,
IconGitHub,
IconInstagram,
IconLinkedin,
IconLoader,
IconLogo,
IconPlayStore,
IconStar,
IconTwitter,
} from '@components/icons';
const Icon = ({ name }) => {
switch (name) {
case 'AppStore':
return ;
case 'Bookmark':
return ;
case 'Codepen':
return ;
case 'External':
return ;
case 'Folder':
return ;
case 'Fork':
return ;
case 'GitHub':
return ;
case 'Instagram':
return ;
case 'Linkedin':
return ;
case 'Loader':
return ;
case 'Logo':
return ;
case 'PlayStore':
return ;
case 'Star':
return ;
case 'Twitter':
return ;
default:
return ;
}
};
Icon.propTypes = {
name: PropTypes.string.isRequired,
};
export default Icon;
================================================
FILE: src/components/icons/index.js
================================================
export { default as IconAppStore } from './appstore';
export { default as IconBookmark } from './bookmark';
export { default as IconCodepen } from './codepen';
export { default as IconExternal } from './external';
export { default as IconFolder } from './folder';
export { default as IconFork } from './fork';
export { default as Icon } from './icon';
export { default as IconGitHub } from './github';
export { default as IconHex } from './hex';
export { default as IconInstagram } from './instagram';
export { default as IconLinkedin } from './linkedin';
export { default as IconLoader } from './loader';
export { default as IconLogo } from './logo';
export { default as IconPlayStore } from './playstore';
export { default as IconStar } from './star';
export { default as IconTwitter } from './twitter';
================================================
FILE: src/components/icons/instagram.js
================================================
import React from 'react';
const IconInstagram = () => (
);
export default IconInstagram;
================================================
FILE: src/components/icons/linkedin.js
================================================
import React from 'react';
const IconLinkedin = () => (
);
export default IconLinkedin;
================================================
FILE: src/components/icons/loader.js
================================================
import React from 'react';
const IconLoader = () => (
);
export default IconLoader;
================================================
FILE: src/components/icons/logo.js
================================================
import React from 'react';
const IconLogo = () => (
);
export default IconLogo;
================================================
FILE: src/components/icons/playstore.js
================================================
import React from 'react';
const IconPlayStore = () => (
);
export default IconPlayStore;
================================================
FILE: src/components/icons/star.js
================================================
import React from 'react';
const IconStar = () => (
);
export default IconStar;
================================================
FILE: src/components/icons/twitter.js
================================================
import React from 'react';
const IconTwitter = () => (
);
export default IconTwitter;
================================================
FILE: src/components/index.js
================================================
export { default as Head } from './head';
export { default as Layout } from './layout';
export { default as Loader } from './loader';
export { default as Nav } from './nav';
export { default as Menu } from './menu';
export { default as Side } from './side';
export { default as Social } from './social';
export { default as Email } from './email';
export { default as Footer } from './footer';
export { default as Hero } from './sections/hero';
export { default as About } from './sections/about';
export { default as Jobs } from './sections/jobs';
export { default as Featured } from './sections/featured';
export { default as Projects } from './sections/projects';
export { default as Contact } from './sections/contact';
================================================
FILE: src/components/layout.js
================================================
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import styled, { ThemeProvider } from 'styled-components';
import { Head, Loader, Nav, Social, Email, Footer } from '@components';
import { GlobalStyle, theme } from '@styles';
const StyledContent = styled.div`
display: flex;
flex-direction: column;
min-height: 100vh;
`;
const Layout = ({ children, location }) => {
const isHome = location.pathname === '/';
const [isLoading, setIsLoading] = useState(isHome);
// Sets target="_blank" rel="noopener noreferrer" on external links
const handleExternalLinks = () => {
const allLinks = Array.from(document.querySelectorAll('a'));
if (allLinks.length > 0) {
allLinks.forEach(link => {
if (link.host !== window.location.host) {
link.setAttribute('rel', 'noopener noreferrer');
link.setAttribute('target', '_blank');
}
});
}
};
useEffect(() => {
if (isLoading) {
return;
}
if (location.hash) {
const id = location.hash.substring(1); // location.hash without the '#'
setTimeout(() => {
const el = document.getElementById(id);
if (el) {
el.scrollIntoView();
el.focus();
}
}, 0);
}
handleExternalLinks();
}, [isLoading]);
return (
<>
Skip to Content
{isLoading && isHome ? (
setIsLoading(false)} />
) : (
{children}
)}
>
);
};
Layout.propTypes = {
children: PropTypes.node.isRequired,
location: PropTypes.object.isRequired,
};
export default Layout;
================================================
FILE: src/components/loader.js
================================================
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import PropTypes from 'prop-types';
import anime from 'animejs';
import styled from 'styled-components';
import { IconLoader } from '@components/icons';
const StyledLoader = styled.div`
${({ theme }) => theme.mixins.flexCenter};
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
background-color: var(--dark-navy);
z-index: 99;
.logo-wrapper {
width: max-content;
max-width: 100px;
transition: var(--transition);
opacity: ${props => (props.isMounted ? 1 : 0)};
svg {
display: block;
width: 100%;
height: 100%;
margin: 0 auto;
fill: none;
user-select: none;
#B {
opacity: 0;
}
}
}
`;
const Loader = ({ finishLoading }) => {
const [isMounted, setIsMounted] = useState(false);
const animate = () => {
const loader = anime.timeline({
complete: () => finishLoading(),
});
loader
.add({
targets: '#logo path',
delay: 300,
duration: 1500,
easing: 'easeInOutQuart',
strokeDashoffset: [anime.setDashoffset, 0],
})
.add({
targets: '#logo #B',
duration: 700,
easing: 'easeInOutQuart',
opacity: 1,
})
.add({
targets: '#logo',
delay: 500,
duration: 300,
easing: 'easeInOutQuart',
opacity: 0,
scale: 0.1,
})
.add({
targets: '.loader',
duration: 200,
easing: 'easeInOutQuart',
opacity: 0,
zIndex: -1,
});
};
useEffect(() => {
const timeout = setTimeout(() => setIsMounted(true), 10);
animate();
return () => clearTimeout(timeout);
}, []);
return (
);
};
Loader.propTypes = {
finishLoading: PropTypes.func.isRequired,
};
export default Loader;
================================================
FILE: src/components/menu.js
================================================
import React, { useState, useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet';
import { Link } from 'gatsby';
import styled from 'styled-components';
import { navLinks } from '@config';
import { KEY_CODES } from '@utils';
import { useOnClickOutside } from '@hooks';
const StyledMenu = styled.div`
display: none;
@media (max-width: 768px) {
display: block;
}
`;
const StyledHamburgerButton = styled.button`
display: none;
@media (max-width: 768px) {
${({ theme }) => theme.mixins.flexCenter};
position: relative;
z-index: 10;
margin-right: -15px;
padding: 15px;
border: 0;
background-color: transparent;
color: inherit;
text-transform: none;
transition-timing-function: linear;
transition-duration: 0.15s;
transition-property: opacity, filter;
}
.ham-box {
display: inline-block;
position: relative;
width: var(--hamburger-width);
height: 24px;
}
.ham-box-inner {
position: absolute;
top: 50%;
right: 0;
width: var(--hamburger-width);
height: 2px;
border-radius: var(--border-radius);
background-color: var(--green);
transition-duration: 0.22s;
transition-property: transform;
transition-delay: ${props => (props.menuOpen ? `0.12s` : `0s`)};
transform: rotate(${props => (props.menuOpen ? `225deg` : `0deg`)});
transition-timing-function: cubic-bezier(
${props => (props.menuOpen ? `0.215, 0.61, 0.355, 1` : `0.55, 0.055, 0.675, 0.19`)}
);
&:before,
&:after {
content: '';
display: block;
position: absolute;
left: auto;
right: 0;
width: var(--hamburger-width);
height: 2px;
border-radius: 4px;
background-color: var(--green);
transition-timing-function: ease;
transition-duration: 0.15s;
transition-property: transform;
}
&:before {
width: ${props => (props.menuOpen ? `100%` : `120%`)};
top: ${props => (props.menuOpen ? `0` : `-10px`)};
opacity: ${props => (props.menuOpen ? 0 : 1)};
transition: ${({ menuOpen }) =>
menuOpen ? 'var(--ham-before-active)' : 'var(--ham-before)'};
}
&:after {
width: ${props => (props.menuOpen ? `100%` : `80%`)};
bottom: ${props => (props.menuOpen ? `0` : `-10px`)};
transform: rotate(${props => (props.menuOpen ? `-90deg` : `0`)});
transition: ${({ menuOpen }) => (menuOpen ? 'var(--ham-after-active)' : 'var(--ham-after)')};
}
}
`;
const StyledSidebar = styled.aside`
display: none;
@media (max-width: 768px) {
${({ theme }) => theme.mixins.flexCenter};
position: fixed;
top: 0;
bottom: 0;
right: 0;
padding: 50px 10px;
width: min(75vw, 400px);
height: 100vh;
outline: 0;
background-color: var(--light-navy);
box-shadow: -10px 0px 30px -15px var(--navy-shadow);
z-index: 9;
transform: translateX(${props => (props.menuOpen ? 0 : 100)}vw);
visibility: ${props => (props.menuOpen ? 'visible' : 'hidden')};
transition: var(--transition);
}
nav {
${({ theme }) => theme.mixins.flexBetween};
width: 100%;
flex-direction: column;
color: var(--lightest-slate);
font-family: var(--font-mono);
text-align: center;
}
ol {
padding: 0;
margin: 0;
list-style: none;
width: 100%;
li {
position: relative;
margin: 0 auto 20px;
counter-increment: item 1;
font-size: clamp(var(--fz-sm), 4vw, var(--fz-lg));
@media (max-width: 600px) {
margin: 0 auto 10px;
}
&:before {
content: '0' counter(item) '.';
display: block;
margin-bottom: 5px;
color: var(--green);
font-size: var(--fz-sm);
}
}
a {
${({ theme }) => theme.mixins.link};
width: 100%;
padding: 3px 20px 20px;
}
}
.resume-link {
${({ theme }) => theme.mixins.bigButton};
padding: 18px 50px;
margin: 10% auto 0;
width: max-content;
}
`;
const Menu = () => {
const [menuOpen, setMenuOpen] = useState(false);
const toggleMenu = () => setMenuOpen(!menuOpen);
const buttonRef = useRef(null);
const navRef = useRef(null);
let menuFocusables;
let firstFocusableEl;
let lastFocusableEl;
const setFocusables = () => {
menuFocusables = [buttonRef.current, ...Array.from(navRef.current.querySelectorAll('a'))];
firstFocusableEl = menuFocusables[0];
lastFocusableEl = menuFocusables[menuFocusables.length - 1];
};
const handleBackwardTab = e => {
if (document.activeElement === firstFocusableEl) {
e.preventDefault();
lastFocusableEl.focus();
}
};
const handleForwardTab = e => {
if (document.activeElement === lastFocusableEl) {
e.preventDefault();
firstFocusableEl.focus();
}
};
const onKeyDown = e => {
switch (e.key) {
case KEY_CODES.ESCAPE:
case KEY_CODES.ESCAPE_IE11: {
setMenuOpen(false);
break;
}
case KEY_CODES.TAB: {
if (menuFocusables && menuFocusables.length === 1) {
e.preventDefault();
break;
}
if (e.shiftKey) {
handleBackwardTab(e);
} else {
handleForwardTab(e);
}
break;
}
default: {
break;
}
}
};
const onResize = e => {
if (e.currentTarget.innerWidth > 768) {
setMenuOpen(false);
}
};
useEffect(() => {
document.addEventListener('keydown', onKeyDown);
window.addEventListener('resize', onResize);
setFocusables();
return () => {
document.removeEventListener('keydown', onKeyDown);
window.removeEventListener('resize', onResize);
};
}, []);
const wrapperRef = useRef();
useOnClickOutside(wrapperRef, () => setMenuOpen(false));
return (
);
};
export default Menu;
================================================
FILE: src/components/nav.js
================================================
import React, { useState, useEffect } from 'react';
import { Link } from 'gatsby';
import PropTypes from 'prop-types';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import styled, { css } from 'styled-components';
import { navLinks } from '@config';
import { loaderDelay } from '@utils';
import { useScrollDirection, usePrefersReducedMotion } from '@hooks';
import { Menu } from '@components';
import { IconLogo, IconHex } from '@components/icons';
const StyledHeader = styled.header`
${({ theme }) => theme.mixins.flexBetween};
position: fixed;
top: 0;
z-index: 11;
padding: 0px 50px;
width: 100%;
height: var(--nav-height);
background-color: rgba(10, 25, 47, 0.85);
filter: none !important;
pointer-events: auto !important;
user-select: auto !important;
backdrop-filter: blur(10px);
transition: var(--transition);
@media (max-width: 1080px) {
padding: 0 40px;
}
@media (max-width: 768px) {
padding: 0 25px;
}
@media (prefers-reduced-motion: no-preference) {
${props =>
props.scrollDirection === 'up' &&
!props.scrolledToTop &&
css`
height: var(--nav-scroll-height);
transform: translateY(0px);
background-color: rgba(10, 25, 47, 0.85);
box-shadow: 0 10px 30px -10px var(--navy-shadow);
`};
${props =>
props.scrollDirection === 'down' &&
!props.scrolledToTop &&
css`
height: var(--nav-scroll-height);
transform: translateY(calc(var(--nav-scroll-height) * -1));
box-shadow: 0 10px 30px -10px var(--navy-shadow);
`};
}
`;
const StyledNav = styled.nav`
${({ theme }) => theme.mixins.flexBetween};
position: relative;
width: 100%;
color: var(--lightest-slate);
font-family: var(--font-mono);
counter-reset: item 0;
z-index: 12;
.logo {
${({ theme }) => theme.mixins.flexCenter};
a {
color: var(--green);
width: 42px;
height: 42px;
position: relative;
z-index: 1;
.hex-container {
position: absolute;
top: 0;
left: 0;
z-index: -1;
@media (prefers-reduced-motion: no-preference) {
transition: var(--transition);
}
}
.logo-container {
position: relative;
z-index: 1;
svg {
fill: none;
user-select: none;
@media (prefers-reduced-motion: no-preference) {
transition: var(--transition);
}
polygon {
fill: var(--navy);
}
}
}
&:hover,
&:focus {
outline: 0;
transform: translate(-4px, -4px);
.hex-container {
transform: translate(4px, 3px);
}
}
}
}
`;
const StyledLinks = styled.div`
display: flex;
align-items: center;
@media (max-width: 768px) {
display: none;
}
ol {
${({ theme }) => theme.mixins.flexBetween};
padding: 0;
margin: 0;
list-style: none;
li {
margin: 0 5px;
position: relative;
counter-increment: item 1;
font-size: var(--fz-xs);
a {
padding: 10px;
&:before {
content: '0' counter(item) '.';
margin-right: 5px;
color: var(--green);
font-size: var(--fz-xxs);
text-align: right;
}
}
}
}
.resume-button {
${({ theme }) => theme.mixins.smallButton};
margin-left: 15px;
font-size: var(--fz-xs);
}
`;
const Nav = ({ isHome }) => {
const [isMounted, setIsMounted] = useState(!isHome);
const scrollDirection = useScrollDirection('down');
const [scrolledToTop, setScrolledToTop] = useState(true);
const prefersReducedMotion = usePrefersReducedMotion();
const handleScroll = () => {
setScrolledToTop(window.pageYOffset < 50);
};
useEffect(() => {
if (prefersReducedMotion) {
return;
}
const timeout = setTimeout(() => {
setIsMounted(true);
}, 100);
window.addEventListener('scroll', handleScroll);
return () => {
clearTimeout(timeout);
window.removeEventListener('scroll', handleScroll);
};
}, []);
const timeout = isHome ? loaderDelay : 0;
const fadeClass = isHome ? 'fade' : '';
const fadeDownClass = isHome ? 'fadedown' : '';
const Logo = (
);
const ResumeLink = (
Resume
);
return (
{prefersReducedMotion ? (
<>
{Logo}
{navLinks &&
navLinks.map(({ url, name }, i) => (
-
{name}
))}
{ResumeLink}
>
) : (
<>
{isMounted && (
<>{Logo}>
)}
{isMounted &&
navLinks &&
navLinks.map(({ url, name }, i) => (
-
{name}
))}
{isMounted && (
{ResumeLink}
)}
{isMounted && (
)}
>
)}
);
};
Nav.propTypes = {
isHome: PropTypes.bool,
};
export default Nav;
================================================
FILE: src/components/sections/about.js
================================================
import React, { useEffect, useRef } from 'react';
import { StaticImage } from 'gatsby-plugin-image';
import styled from 'styled-components';
import { srConfig } from '@config';
import sr from '@utils/sr';
import { usePrefersReducedMotion } from '@hooks';
const StyledAboutSection = styled.section`
max-width: 900px;
.inner {
display: grid;
grid-template-columns: 3fr 2fr;
grid-gap: 50px;
@media (max-width: 768px) {
display: block;
}
}
`;
const StyledText = styled.div`
ul.skills-list {
display: grid;
grid-template-columns: repeat(2, minmax(140px, 200px));
grid-gap: 0 10px;
padding: 0;
margin: 20px 0 0 0;
overflow: hidden;
list-style: none;
li {
position: relative;
margin-bottom: 10px;
padding-left: 20px;
font-family: var(--font-mono);
font-size: var(--fz-xs);
&:before {
content: '▹';
position: absolute;
left: 0;
color: var(--green);
font-size: var(--fz-sm);
line-height: 12px;
}
}
}
`;
const StyledPic = styled.div`
position: relative;
max-width: 300px;
@media (max-width: 768px) {
margin: 50px auto 0;
width: 70%;
}
.wrapper {
${({ theme }) => theme.mixins.boxShadow};
display: block;
position: relative;
width: 100%;
border-radius: var(--border-radius);
background-color: var(--green);
&:hover,
&:focus {
outline: 0;
transform: translate(-4px, -4px);
&:after {
transform: translate(8px, 8px);
}
.img {
filter: none;
mix-blend-mode: normal;
}
}
.img {
position: relative;
border-radius: var(--border-radius);
mix-blend-mode: multiply;
filter: grayscale(100%) contrast(1);
transition: var(--transition);
}
&:before,
&:after {
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
border-radius: var(--border-radius);
transition: var(--transition);
}
&:before {
top: 0;
left: 0;
background-color: var(--navy);
mix-blend-mode: screen;
}
&:after {
border: 2px solid var(--green);
top: 14px;
left: 14px;
z-index: -1;
}
}
`;
const About = () => {
const revealContainer = useRef(null);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
if (prefersReducedMotion) {
return;
}
sr.reveal(revealContainer.current, srConfig());
}, []);
const skills = ['JavaScript (ES6+)', 'TypeScript', 'React', 'Eleventy', 'Node.js', 'WordPress'];
return (
About Me
Hello! My name is Brittany and I enjoy creating things that live on the internet. My
interest in web development started back in 2012 when I decided to try editing custom
Tumblr themes — turns out hacking together a custom reblog button taught me a lot
about HTML & CSS!
Fast-forward to today, and I’ve had the privilege of working at{' '}
an advertising agency,{' '}
a start-up,{' '}
a huge corporation, and{' '}
a student-led design studio. My
main focus these days is building accessible, inclusive products and digital
experiences at Upstatement for a variety of
clients.
I also recently{' '}
launched a course
{' '}
that covers everything you need to build a web app with the Spotify API using Node
& React.
Here are a few technologies I’ve been working with recently:
{skills && skills.map((skill, i) => - {skill}
)}
);
};
export default About;
================================================
FILE: src/components/sections/contact.js
================================================
import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';
import { srConfig, email } from '@config';
import sr from '@utils/sr';
import { usePrefersReducedMotion } from '@hooks';
const StyledContactSection = styled.section`
max-width: 600px;
margin: 0 auto 100px;
text-align: center;
@media (max-width: 768px) {
margin: 0 auto 50px;
}
.overline {
display: block;
margin-bottom: 20px;
color: var(--green);
font-family: var(--font-mono);
font-size: var(--fz-md);
font-weight: 400;
&:before {
bottom: 0;
font-size: var(--fz-sm);
}
&:after {
display: none;
}
}
.title {
font-size: clamp(40px, 5vw, 60px);
}
.email-link {
${({ theme }) => theme.mixins.bigButton};
margin-top: 50px;
}
`;
const Contact = () => {
const revealContainer = useRef(null);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
if (prefersReducedMotion) {
return;
}
sr.reveal(revealContainer.current, srConfig());
}, []);
return (
What’s Next?
Get In Touch
Although I’m not currently looking for any new opportunities, my inbox is always open.
Whether you have a question or just want to say hi, I’ll try my best to get back to you!
Say Hello
);
};
export default Contact;
================================================
FILE: src/components/sections/featured.js
================================================
import React, { useEffect, useRef } from 'react';
import { useStaticQuery, graphql } from 'gatsby';
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import styled from 'styled-components';
import sr from '@utils/sr';
import { srConfig } from '@config';
import { Icon } from '@components/icons';
import { usePrefersReducedMotion } from '@hooks';
const StyledProjectsGrid = styled.ul`
${({ theme }) => theme.mixins.resetList};
a {
position: relative;
z-index: 1;
}
`;
const StyledProject = styled.li`
position: relative;
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(12, 1fr);
align-items: center;
@media (max-width: 768px) {
${({ theme }) => theme.mixins.boxShadow};
}
&:not(:last-of-type) {
margin-bottom: 100px;
@media (max-width: 768px) {
margin-bottom: 70px;
}
@media (max-width: 480px) {
margin-bottom: 30px;
}
}
&:nth-of-type(odd) {
.project-content {
grid-column: 7 / -1;
text-align: right;
@media (max-width: 1080px) {
grid-column: 5 / -1;
}
@media (max-width: 768px) {
grid-column: 1 / -1;
padding: 40px 40px 30px;
text-align: left;
}
@media (max-width: 480px) {
padding: 25px 25px 20px;
}
}
.project-tech-list {
justify-content: flex-end;
@media (max-width: 768px) {
justify-content: flex-start;
}
li {
margin: 0 0 5px 20px;
@media (max-width: 768px) {
margin: 0 10px 5px 0;
}
}
}
.project-links {
justify-content: flex-end;
margin-left: 0;
margin-right: -10px;
@media (max-width: 768px) {
justify-content: flex-start;
margin-left: -10px;
margin-right: 0;
}
}
.project-image {
grid-column: 1 / 8;
@media (max-width: 768px) {
grid-column: 1 / -1;
}
}
}
.project-content {
position: relative;
grid-column: 1 / 7;
grid-row: 1 / -1;
@media (max-width: 1080px) {
grid-column: 1 / 9;
}
@media (max-width: 768px) {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
grid-column: 1 / -1;
padding: 40px 40px 30px;
z-index: 5;
}
@media (max-width: 480px) {
padding: 30px 25px 20px;
}
}
.project-overline {
margin: 10px 0;
color: var(--green);
font-family: var(--font-mono);
font-size: var(--fz-xs);
font-weight: 400;
}
.project-title {
color: var(--lightest-slate);
font-size: clamp(24px, 5vw, 28px);
@media (min-width: 768px) {
margin: 0 0 20px;
}
@media (max-width: 768px) {
color: var(--white);
a {
position: static;
&:before {
content: '';
display: block;
position: absolute;
z-index: 0;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
}
}
}
.project-description {
${({ theme }) => theme.mixins.boxShadow};
position: relative;
z-index: 2;
padding: 25px;
border-radius: var(--border-radius);
background-color: var(--light-navy);
color: var(--light-slate);
font-size: var(--fz-lg);
@media (max-width: 768px) {
padding: 20px 0;
background-color: transparent;
box-shadow: none;
&:hover {
box-shadow: none;
}
}
a {
${({ theme }) => theme.mixins.inlineLink};
}
strong {
color: var(--white);
font-weight: normal;
}
}
.project-tech-list {
display: flex;
flex-wrap: wrap;
position: relative;
z-index: 2;
margin: 25px 0 10px;
padding: 0;
list-style: none;
li {
margin: 0 20px 5px 0;
color: var(--light-slate);
font-family: var(--font-mono);
font-size: var(--fz-xs);
white-space: nowrap;
}
@media (max-width: 768px) {
margin: 10px 0;
li {
margin: 0 10px 5px 0;
color: var(--lightest-slate);
}
}
}
.project-links {
display: flex;
align-items: center;
position: relative;
margin-top: 10px;
margin-left: -10px;
color: var(--lightest-slate);
a {
${({ theme }) => theme.mixins.flexCenter};
padding: 10px;
&.external {
svg {
width: 22px;
height: 22px;
margin-top: -4px;
}
}
svg {
width: 20px;
height: 20px;
}
}
.cta {
${({ theme }) => theme.mixins.smallButton};
margin: 10px;
}
}
.project-image {
${({ theme }) => theme.mixins.boxShadow};
grid-column: 6 / -1;
grid-row: 1 / -1;
position: relative;
z-index: 1;
@media (max-width: 768px) {
grid-column: 1 / -1;
height: 100%;
opacity: 0.25;
}
a {
width: 100%;
height: 100%;
background-color: var(--green);
border-radius: var(--border-radius);
vertical-align: middle;
&:hover,
&:focus {
background: transparent;
outline: 0;
&:before,
.img {
background: transparent;
filter: none;
}
}
&:before {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 3;
transition: var(--transition);
background-color: var(--navy);
mix-blend-mode: screen;
}
}
.img {
border-radius: var(--border-radius);
mix-blend-mode: multiply;
filter: grayscale(100%) contrast(1) brightness(90%);
@media (max-width: 768px) {
object-fit: cover;
width: auto;
height: 100%;
filter: grayscale(100%) contrast(1) brightness(50%);
}
}
}
`;
const Featured = () => {
const data = useStaticQuery(graphql`
{
featured: allMarkdownRemark(
filter: { fileAbsolutePath: { regex: "/content/featured/" } }
sort: { fields: [frontmatter___date], order: ASC }
) {
edges {
node {
frontmatter {
title
cover {
childImageSharp {
gatsbyImageData(width: 700, placeholder: BLURRED, formats: [AUTO, WEBP, AVIF])
}
}
tech
github
external
cta
}
html
}
}
}
}
`);
const featuredProjects = data.featured.edges.filter(({ node }) => node);
const revealTitle = useRef(null);
const revealProjects = useRef([]);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
if (prefersReducedMotion) {
return;
}
sr.reveal(revealTitle.current, srConfig());
revealProjects.current.forEach((ref, i) => sr.reveal(ref, srConfig(i * 100)));
}, []);
return (
Some Things I’ve Built
{featuredProjects &&
featuredProjects.map(({ node }, i) => {
const { frontmatter, html } = node;
const { external, title, tech, github, cover, cta } = frontmatter;
const image = getImage(cover);
return (
(revealProjects.current[i] = el)}>
);
})}
);
};
export default Featured;
================================================
FILE: src/components/sections/hero.js
================================================
import React, { useState, useEffect } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import styled from 'styled-components';
import { navDelay, loaderDelay } from '@utils';
import { usePrefersReducedMotion } from '@hooks';
const StyledHeroSection = styled.section`
${({ theme }) => theme.mixins.flexCenter};
flex-direction: column;
align-items: flex-start;
min-height: 100vh;
height: 100vh;
padding: 0;
@media (max-height: 700px) and (min-width: 700px), (max-width: 360px) {
height: auto;
padding-top: var(--nav-height);
}
h1 {
margin: 0 0 30px 4px;
color: var(--green);
font-family: var(--font-mono);
font-size: clamp(var(--fz-sm), 5vw, var(--fz-md));
font-weight: 400;
@media (max-width: 480px) {
margin: 0 0 20px 2px;
}
}
h3 {
margin-top: 5px;
color: var(--slate);
line-height: 0.9;
}
p {
margin: 20px 0 0;
max-width: 540px;
}
.email-link {
${({ theme }) => theme.mixins.bigButton};
margin-top: 50px;
}
`;
const Hero = () => {
const [isMounted, setIsMounted] = useState(false);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
if (prefersReducedMotion) {
return;
}
const timeout = setTimeout(() => setIsMounted(true), navDelay);
return () => clearTimeout(timeout);
}, []);
const one = Hi, my name is
;
const two = Brittany Chiang.
;
const three = I build things for the web.
;
const four = (
<>
I’m a software engineer specializing in building (and occasionally designing) exceptional
digital experiences. Currently, I’m focused on building accessible, human-centered products
at{' '}
Upstatement
.
>
);
const five = (
Check out my course!
);
const items = [one, two, three, four, five];
return (
{prefersReducedMotion ? (
<>
{items.map((item, i) => (
{item}
))}
>
) : (
{isMounted &&
items.map((item, i) => (
{item}
))}
)}
);
};
export default Hero;
================================================
FILE: src/components/sections/jobs.js
================================================
import React, { useState, useEffect, useRef } from 'react';
import { useStaticQuery, graphql } from 'gatsby';
import { CSSTransition } from 'react-transition-group';
import styled from 'styled-components';
import { srConfig } from '@config';
import { KEY_CODES } from '@utils';
import sr from '@utils/sr';
import { usePrefersReducedMotion } from '@hooks';
const StyledJobsSection = styled.section`
max-width: 700px;
.inner {
display: flex;
@media (max-width: 600px) {
display: block;
}
// Prevent container from jumping
@media (min-width: 700px) {
min-height: 340px;
}
}
`;
const StyledTabList = styled.div`
position: relative;
z-index: 3;
width: max-content;
padding: 0;
margin: 0;
list-style: none;
@media (max-width: 600px) {
display: flex;
overflow-x: auto;
width: calc(100% + 100px);
padding-left: 50px;
margin-left: -50px;
margin-bottom: 30px;
}
@media (max-width: 480px) {
width: calc(100% + 50px);
padding-left: 25px;
margin-left: -25px;
}
li {
&:first-of-type {
@media (max-width: 600px) {
margin-left: 50px;
}
@media (max-width: 480px) {
margin-left: 25px;
}
}
&:last-of-type {
@media (max-width: 600px) {
padding-right: 50px;
}
@media (max-width: 480px) {
padding-right: 25px;
}
}
}
`;
const StyledTabButton = styled.button`
${({ theme }) => theme.mixins.link};
display: flex;
align-items: center;
width: 100%;
height: var(--tab-height);
padding: 0 20px 2px;
border-left: 2px solid var(--lightest-navy);
background-color: transparent;
color: ${({ isActive }) => (isActive ? 'var(--green)' : 'var(--slate)')};
font-family: var(--font-mono);
font-size: var(--fz-xs);
text-align: left;
white-space: nowrap;
@media (max-width: 768px) {
padding: 0 15px 2px;
}
@media (max-width: 600px) {
${({ theme }) => theme.mixins.flexCenter};
min-width: 120px;
padding: 0 15px;
border-left: 0;
border-bottom: 2px solid var(--lightest-navy);
text-align: center;
}
&:hover,
&:focus {
background-color: var(--light-navy);
}
`;
const StyledHighlight = styled.div`
position: absolute;
top: 0;
left: 0;
z-index: 10;
width: 2px;
height: var(--tab-height);
border-radius: var(--border-radius);
background: var(--green);
transform: translateY(calc(${({ activeTabId }) => activeTabId} * var(--tab-height)));
transition: transform 0.25s cubic-bezier(0.645, 0.045, 0.355, 1);
transition-delay: 0.1s;
@media (max-width: 600px) {
top: auto;
bottom: 0;
width: 100%;
max-width: var(--tab-width);
height: 2px;
margin-left: 50px;
transform: translateX(calc(${({ activeTabId }) => activeTabId} * var(--tab-width)));
}
@media (max-width: 480px) {
margin-left: 25px;
}
`;
const StyledTabPanels = styled.div`
position: relative;
width: 100%;
margin-left: 20px;
@media (max-width: 600px) {
margin-left: 0;
}
`;
const StyledTabPanel = styled.div`
width: 100%;
height: auto;
padding: 10px 5px;
ul {
${({ theme }) => theme.mixins.fancyList};
}
h3 {
margin-bottom: 2px;
font-size: var(--fz-xxl);
font-weight: 500;
line-height: 1.3;
.company {
color: var(--green);
}
}
.range {
margin-bottom: 25px;
color: var(--light-slate);
font-family: var(--font-mono);
font-size: var(--fz-xs);
}
`;
const Jobs = () => {
const data = useStaticQuery(graphql`
query {
jobs: allMarkdownRemark(
filter: { fileAbsolutePath: { regex: "/content/jobs/" } }
sort: { fields: [frontmatter___date], order: DESC }
) {
edges {
node {
frontmatter {
title
company
location
range
url
}
html
}
}
}
}
`);
const jobsData = data.jobs.edges;
const [activeTabId, setActiveTabId] = useState(0);
const [tabFocus, setTabFocus] = useState(null);
const tabs = useRef([]);
const revealContainer = useRef(null);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
if (prefersReducedMotion) {
return;
}
sr.reveal(revealContainer.current, srConfig());
}, []);
const focusTab = () => {
if (tabs.current[tabFocus]) {
tabs.current[tabFocus].focus();
return;
}
// If we're at the end, go to the start
if (tabFocus >= tabs.current.length) {
setTabFocus(0);
}
// If we're at the start, move to the end
if (tabFocus < 0) {
setTabFocus(tabs.current.length - 1);
}
};
// Only re-run the effect if tabFocus changes
useEffect(() => focusTab(), [tabFocus]);
// Focus on tabs when using up & down arrow keys
const onKeyDown = e => {
switch (e.key) {
case KEY_CODES.ARROW_UP: {
e.preventDefault();
setTabFocus(tabFocus - 1);
break;
}
case KEY_CODES.ARROW_DOWN: {
e.preventDefault();
setTabFocus(tabFocus + 1);
break;
}
default: {
break;
}
}
};
return (
Where I’ve Worked
onKeyDown(e)}>
{jobsData &&
jobsData.map(({ node }, i) => {
const { company } = node.frontmatter;
return (
setActiveTabId(i)}
ref={el => (tabs.current[i] = el)}
id={`tab-${i}`}
role="tab"
tabIndex={activeTabId === i ? '0' : '-1'}
aria-selected={activeTabId === i ? true : false}
aria-controls={`panel-${i}`}>
{company}
);
})}
{jobsData &&
jobsData.map(({ node }, i) => {
const { frontmatter, html } = node;
const { title, url, company, range } = frontmatter;
return (
{title}
@
{company}
{range}
);
})}
);
};
export default Jobs;
================================================
FILE: src/components/sections/projects.js
================================================
import React, { useState, useEffect, useRef } from 'react';
import { Link, useStaticQuery, graphql } from 'gatsby';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import styled from 'styled-components';
import { srConfig } from '@config';
import sr from '@utils/sr';
import { Icon } from '@components/icons';
import { usePrefersReducedMotion } from '@hooks';
const StyledProjectsSection = styled.section`
display: flex;
flex-direction: column;
align-items: center;
h2 {
font-size: clamp(24px, 5vw, var(--fz-heading));
}
.archive-link {
font-family: var(--font-mono);
font-size: var(--fz-sm);
&:after {
bottom: 0.1em;
}
}
.projects-grid {
${({ theme }) => theme.mixins.resetList};
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-gap: 15px;
position: relative;
margin-top: 50px;
@media (max-width: 1080px) {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
.more-button {
${({ theme }) => theme.mixins.button};
margin: 80px auto 0;
}
`;
const StyledProject = styled.li`
position: relative;
cursor: default;
transition: var(--transition);
@media (prefers-reduced-motion: no-preference) {
&:hover,
&:focus-within {
.project-inner {
transform: translateY(-7px);
}
}
}
a {
position: relative;
z-index: 1;
}
.project-inner {
${({ theme }) => theme.mixins.boxShadow};
${({ theme }) => theme.mixins.flexBetween};
flex-direction: column;
align-items: flex-start;
position: relative;
height: 100%;
padding: 2rem 1.75rem;
border-radius: var(--border-radius);
background-color: var(--light-navy);
transition: var(--transition);
overflow: auto;
}
.project-top {
${({ theme }) => theme.mixins.flexBetween};
margin-bottom: 35px;
.folder {
color: var(--green);
svg {
width: 40px;
height: 40px;
}
}
.project-links {
display: flex;
align-items: center;
margin-right: -10px;
color: var(--light-slate);
a {
${({ theme }) => theme.mixins.flexCenter};
padding: 5px 7px;
&.external {
svg {
width: 22px;
height: 22px;
margin-top: -4px;
}
}
svg {
width: 20px;
height: 20px;
}
}
}
}
.project-title {
margin: 0 0 10px;
color: var(--lightest-slate);
font-size: var(--fz-xxl);
a {
position: static;
&:before {
content: '';
display: block;
position: absolute;
z-index: 0;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
}
}
.project-description {
color: var(--light-slate);
font-size: 17px;
a {
${({ theme }) => theme.mixins.inlineLink};
}
}
.project-tech-list {
display: flex;
align-items: flex-end;
flex-grow: 1;
flex-wrap: wrap;
padding: 0;
margin: 20px 0 0 0;
list-style: none;
li {
font-family: var(--font-mono);
font-size: var(--fz-xxs);
line-height: 1.75;
&:not(:last-of-type) {
margin-right: 15px;
}
}
}
`;
const Projects = () => {
const data = useStaticQuery(graphql`
query {
projects: allMarkdownRemark(
filter: {
fileAbsolutePath: { regex: "/content/projects/" }
frontmatter: { showInProjects: { ne: false } }
}
sort: { fields: [frontmatter___date], order: DESC }
) {
edges {
node {
frontmatter {
title
tech
github
external
}
html
}
}
}
}
`);
const [showMore, setShowMore] = useState(false);
const revealTitle = useRef(null);
const revealArchiveLink = useRef(null);
const revealProjects = useRef([]);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
if (prefersReducedMotion) {
return;
}
sr.reveal(revealTitle.current, srConfig());
sr.reveal(revealArchiveLink.current, srConfig());
revealProjects.current.forEach((ref, i) => sr.reveal(ref, srConfig(i * 100)));
}, []);
const GRID_LIMIT = 6;
const projects = data.projects.edges.filter(({ node }) => node);
const firstSix = projects.slice(0, GRID_LIMIT);
const projectsToShow = showMore ? projects : firstSix;
const projectInner = node => {
const { frontmatter, html } = node;
const { github, external, title, tech } = frontmatter;
return (
);
};
return (
Other Noteworthy Projects
view the archive
{prefersReducedMotion ? (
<>
{projectsToShow &&
projectsToShow.map(({ node }, i) => (
{projectInner(node)}
))}
>
) : (
{projectsToShow &&
projectsToShow.map(({ node }, i) => (
= GRID_LIMIT ? (i - GRID_LIMIT) * 300 : 300}
exit={false}>
(revealProjects.current[i] = el)}
style={{
transitionDelay: `${i >= GRID_LIMIT ? (i - GRID_LIMIT) * 100 : 0}ms`,
}}>
{projectInner(node)}
))}
)}
);
};
export default Projects;
================================================
FILE: src/components/side.js
================================================
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import styled from 'styled-components';
import { loaderDelay } from '@utils';
import { usePrefersReducedMotion } from '@hooks';
const StyledSideElement = styled.div`
width: 40px;
position: fixed;
bottom: 0;
left: ${props => (props.orientation === 'left' ? '40px' : 'auto')};
right: ${props => (props.orientation === 'left' ? 'auto' : '40px')};
z-index: 10;
color: var(--light-slate);
@media (max-width: 1080px) {
left: ${props => (props.orientation === 'left' ? '20px' : 'auto')};
right: ${props => (props.orientation === 'left' ? 'auto' : '20px')};
}
@media (max-width: 768px) {
display: none;
}
`;
const Side = ({ children, isHome, orientation }) => {
const [isMounted, setIsMounted] = useState(!isHome);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
if (!isHome || prefersReducedMotion) {
return;
}
const timeout = setTimeout(() => setIsMounted(true), loaderDelay);
return () => clearTimeout(timeout);
}, []);
return (
{prefersReducedMotion ? (
<>{children}>
) : (
{isMounted && (
{children}
)}
)}
);
};
Side.propTypes = {
children: PropTypes.node.isRequired,
isHome: PropTypes.bool,
orientation: PropTypes.string,
};
export default Side;
================================================
FILE: src/components/social.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { socialMedia } from '@config';
import { Side } from '@components';
import { Icon } from '@components/icons';
const StyledSocialList = styled.ul`
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 0;
list-style: none;
&:after {
content: '';
display: block;
width: 1px;
height: 90px;
margin: 0 auto;
background-color: var(--light-slate);
}
li {
&:last-of-type {
margin-bottom: 20px;
}
a {
padding: 10px;
&:hover,
&:focus {
transform: translateY(-3px);
}
svg {
width: 20px;
height: 20px;
}
}
}
`;
const Social = ({ isHome }) => (
{socialMedia &&
socialMedia.map(({ url, name }, i) => (
))}
);
Social.propTypes = {
isHome: PropTypes.bool,
};
export default Social;
================================================
FILE: src/config.js
================================================
module.exports = {
email: 'brittany.chiang@gmail.com',
socialMedia: [
{
name: 'GitHub',
url: 'https://github.com/bchiang7',
},
{
name: 'Instagram',
url: 'https://www.instagram.com/bchiang7',
},
{
name: 'Twitter',
url: 'https://twitter.com/bchiang7',
},
{
name: 'Linkedin',
url: 'https://www.linkedin.com/in/bchiang7',
},
{
name: 'Codepen',
url: 'https://codepen.io/bchiang7',
},
],
navLinks: [
{
name: 'About',
url: '/#about',
},
{
name: 'Experience',
url: '/#jobs',
},
{
name: 'Work',
url: '/#projects',
},
{
name: 'Contact',
url: '/#contact',
},
],
colors: {
green: '#64ffda',
navy: '#0a192f',
darkNavy: '#020c1b',
},
srConfig: (delay = 200, viewFactor = 0.25) => ({
origin: 'bottom',
distance: '20px',
duration: 500,
delay,
rotate: { x: 0, y: 0, z: 0 },
opacity: 0,
scale: 1,
easing: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
mobile: true,
reset: false,
useDelay: 'always',
viewFactor,
viewOffset: { top: 0, right: 0, bottom: 0, left: 0 },
}),
};
================================================
FILE: src/hooks/index.js
================================================
export { default as useOnClickOutside } from './useOnClickOutside';
export { default as usePrefersReducedMotion } from './usePrefersReducedMotion';
export { default as useScrollDirection } from './useScrollDirection';
================================================
FILE: src/hooks/useOnClickOutside.js
================================================
import { useEffect } from 'react';
// https://usehooks.com/useOnClickOutside/
const useOnClickOutside = (ref, handler) => {
useEffect(
() => {
const listener = event => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new ...
// ... function on every render that will cause this effect ...
// ... callback/cleanup to run every render. It's not a big deal ...
// ... but to optimize you can wrap handler in useCallback before ...
// ... passing it into this hook.
[ref, handler],
);
};
export default useOnClickOutside;
================================================
FILE: src/hooks/usePrefersReducedMotion.js
================================================
/**
* https://www.joshwcomeau.com/snippets/react-hooks/use-prefers-reduced-motion/
*/
import { useState, useEffect } from 'react';
const QUERY = '(prefers-reduced-motion: no-preference)';
const isRenderingOnServer = typeof window === 'undefined';
const getInitialState = () =>
// For our initial server render, we won't know if the user
// prefers reduced motion, but it doesn't matter. This value
// will be overwritten on the client, before any animations
// occur.
isRenderingOnServer ? true : !window.matchMedia(QUERY).matches;
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(getInitialState);
useEffect(() => {
const mediaQueryList = window.matchMedia(QUERY);
const listener = event => {
setPrefersReducedMotion(!event.matches);
};
mediaQueryList.addListener(listener);
return () => {
mediaQueryList.removeListener(listener);
};
}, []);
return prefersReducedMotion;
}
export default usePrefersReducedMotion;
================================================
FILE: src/hooks/useScrollDirection.js
================================================
const SCROLL_UP = 'up';
const SCROLL_DOWN = 'down';
import { useState, useEffect } from 'react';
const useScrollDirection = ({ initialDirection, thresholdPixels, off } = {}) => {
const [scrollDir, setScrollDir] = useState(initialDirection);
useEffect(() => {
const threshold = thresholdPixels || 0;
let lastScrollY = window.pageYOffset;
let ticking = false;
const updateScrollDir = () => {
const scrollY = window.pageYOffset;
if (Math.abs(scrollY - lastScrollY) < threshold) {
// We haven't exceeded the threshold
ticking = false;
return;
}
setScrollDir(scrollY > lastScrollY ? SCROLL_DOWN : SCROLL_UP);
lastScrollY = scrollY > 0 ? scrollY : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
/**
* Bind the scroll handler if `off` is set to false.
* If `off` is set to true reset the scroll direction.
*/
!off ? window.addEventListener('scroll', onScroll) : setScrollDir(initialDirection);
return () => window.removeEventListener('scroll', onScroll);
}, [initialDirection, thresholdPixels, off]);
return scrollDir;
};
export default useScrollDirection;
================================================
FILE: src/pages/404.js
================================================
import React, { useState, useEffect } from 'react';
import { Link } from 'gatsby';
import { Helmet } from 'react-helmet';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { navDelay } from '@utils';
import { Layout } from '@components';
import { usePrefersReducedMotion } from '@hooks';
const StyledMainContainer = styled.main`
${({ theme }) => theme.mixins.flexCenter};
flex-direction: column;
`;
const StyledTitle = styled.h1`
color: var(--green);
font-family: var(--font-mono);
font-size: clamp(100px, 25vw, 200px);
line-height: 1;
`;
const StyledSubtitle = styled.h2`
font-size: clamp(30px, 5vw, 50px);
font-weight: 400;
`;
const StyledHomeButton = styled(Link)`
${({ theme }) => theme.mixins.bigButton};
margin-top: 40px;
`;
const NotFoundPage = ({ location }) => {
const [isMounted, setIsMounted] = useState(false);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
if (prefersReducedMotion) {
return;
}
const timeout = setTimeout(() => setIsMounted(true), navDelay);
return () => clearTimeout(timeout);
}, []);
const content = (
404
Page Not Found
Go Home
);
return (
{prefersReducedMotion ? (
<>{content}>
) : (
{isMounted && (
{content}
)}
)}
);
};
NotFoundPage.propTypes = {
location: PropTypes.object.isRequired,
};
export default NotFoundPage;
================================================
FILE: src/pages/archive.js
================================================
import React, { useRef, useEffect } from 'react';
import { graphql } from 'gatsby';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import styled from 'styled-components';
import { srConfig } from '@config';
import sr from '@utils/sr';
import { Layout } from '@components';
import { Icon } from '@components/icons';
import { usePrefersReducedMotion } from '@hooks';
const StyledTableContainer = styled.div`
margin: 100px -20px;
@media (max-width: 768px) {
margin: 50px -10px;
}
table {
width: 100%;
border-collapse: collapse;
.hide-on-mobile {
@media (max-width: 768px) {
display: none;
}
}
tbody tr {
&:hover,
&:focus {
background-color: var(--light-navy);
}
}
th,
td {
padding: 10px;
text-align: left;
&:first-child {
padding-left: 20px;
@media (max-width: 768px) {
padding-left: 10px;
}
}
&:last-child {
padding-right: 20px;
@media (max-width: 768px) {
padding-right: 10px;
}
}
svg {
width: 20px;
height: 20px;
}
}
tr {
cursor: default;
td:first-child {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
td:last-child {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
}
td {
&.year {
padding-right: 20px;
@media (max-width: 768px) {
padding-right: 10px;
font-size: var(--fz-sm);
}
}
&.title {
padding-top: 15px;
padding-right: 20px;
color: var(--lightest-slate);
font-size: var(--fz-xl);
font-weight: 600;
line-height: 1.25;
}
&.company {
font-size: var(--fz-lg);
white-space: nowrap;
}
&.tech {
font-size: var(--fz-xxs);
font-family: var(--font-mono);
line-height: 1.5;
.separator {
margin: 0 5px;
}
span {
display: inline-block;
}
}
&.links {
min-width: 100px;
div {
display: flex;
align-items: center;
a {
${({ theme }) => theme.mixins.flexCenter};
flex-shrink: 0;
}
a + a {
margin-left: 10px;
}
}
}
}
}
`;
const ArchivePage = ({ location, data }) => {
const projects = data.allMarkdownRemark.edges;
const revealTitle = useRef(null);
const revealTable = useRef(null);
const revealProjects = useRef([]);
const prefersReducedMotion = usePrefersReducedMotion();
useEffect(() => {
if (prefersReducedMotion) {
return;
}
sr.reveal(revealTitle.current, srConfig());
sr.reveal(revealTable.current, srConfig(200, 0));
revealProjects.current.forEach((ref, i) => sr.reveal(ref, srConfig(i * 10)));
}, []);
return (
Archive
A big list of things I’ve worked on
Year
Title
Made at
Built with
Link
{projects.length > 0 &&
projects.map(({ node }, i) => {
const {
date,
github,
external,
ios,
android,
title,
tech,
company,
} = node.frontmatter;
return (
(revealProjects.current[i] = el)}>
{`${new Date(date).getFullYear()}`}
{title}
{company ? {company} : —}
{tech?.length > 0 &&
tech.map((item, i) => (
{item}
{''}
{i !== tech.length - 1 && ·}
))}
);
})}
);
};
ArchivePage.propTypes = {
location: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
};
export default ArchivePage;
export const pageQuery = graphql`
{
allMarkdownRemark(
filter: { fileAbsolutePath: { regex: "/content/projects/" } }
sort: { fields: [frontmatter___date], order: DESC }
) {
edges {
node {
frontmatter {
date
title
tech
github
external
ios
android
company
}
html
}
}
}
}
`;
================================================
FILE: src/pages/index.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Layout, Hero, About, Jobs, Featured, Projects, Contact } from '@components';
const StyledMainContainer = styled.main`
counter-reset: section;
`;
const IndexPage = ({ location }) => (
);
IndexPage.propTypes = {
location: PropTypes.object.isRequired,
};
export default IndexPage;
================================================
FILE: src/pages/pensieve/index.js
================================================
import React from 'react';
import { graphql, Link } from 'gatsby';
import kebabCase from 'lodash/kebabCase';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import styled from 'styled-components';
import { Layout } from '@components';
import { IconBookmark } from '@components/icons';
const StyledMainContainer = styled.main`
& > header {
margin-bottom: 100px;
text-align: center;
a {
&:hover,
&:focus {
cursor: url("data:image/svg+xml;utf8,")
20 0,
auto;
}
}
}
footer {
${({ theme }) => theme.mixins.flexBetween};
width: 100%;
margin-top: 20px;
}
`;
const StyledGrid = styled.ul`
${({ theme }) => theme.mixins.resetList};
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-gap: 15px;
margin-top: 50px;
position: relative;
@media (max-width: 1080px) {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
`;
const StyledPost = styled.li`
transition: var(--transition);
cursor: default;
@media (prefers-reduced-motion: no-preference) {
&:hover,
&:focus-within {
.post__inner {
transform: translateY(-7px);
}
}
}
a {
position: relative;
z-index: 1;
}
.post__inner {
${({ theme }) => theme.mixins.boxShadow};
${({ theme }) => theme.mixins.flexBetween};
flex-direction: column;
align-items: flex-start;
position: relative;
height: 100%;
padding: 2rem 1.75rem;
border-radius: var(--border-radius);
transition: var(--transition);
background-color: var(--light-navy);
header,
a {
width: 100%;
}
}
.post__icon {
${({ theme }) => theme.mixins.flexBetween};
color: var(--green);
margin-bottom: 30px;
margin-left: -5px;
svg {
width: 40px;
height: 40px;
}
}
.post__title {
margin: 0 0 10px;
color: var(--lightest-slate);
font-size: var(--fz-xxl);
a {
position: static;
&:before {
content: '';
display: block;
position: absolute;
z-index: 0;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
}
}
.post__desc {
color: var(--light-slate);
font-size: 17px;
}
.post__date {
color: var(--light-slate);
font-family: var(--font-mono);
font-size: var(--fz-xxs);
text-transform: uppercase;
}
ul.post__tags {
display: flex;
align-items: flex-end;
flex-wrap: wrap;
padding: 0;
margin: 0;
list-style: none;
li {
color: var(--green);
font-family: var(--font-mono);
font-size: var(--fz-xxs);
line-height: 1.75;
&:not(:last-of-type) {
margin-right: 15px;
}
}
}
`;
const PensievePage = ({ location, data }) => {
const posts = data.allMarkdownRemark.edges;
return (
Pensieve
{posts.length > 0 &&
posts.map(({ node }, i) => {
const { frontmatter } = node;
const { title, description, slug, date, tags } = frontmatter;
const formattedDate = new Date(date).toLocaleDateString();
return (
{title}
{description}
);
})}
);
};
PensievePage.propTypes = {
location: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
};
export default PensievePage;
export const pageQuery = graphql`
{
allMarkdownRemark(
filter: { fileAbsolutePath: { regex: "/content/posts/" }, frontmatter: { draft: { ne: true } } }
sort: { fields: [frontmatter___date], order: DESC }
) {
edges {
node {
frontmatter {
title
description
slug
date
tags
draft
}
html
}
}
}
}
`;
================================================
FILE: src/pages/pensieve/tags.js
================================================
import React from 'react';
import { Link, graphql } from 'gatsby';
import kebabCase from 'lodash/kebabCase';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import styled from 'styled-components';
import { Layout } from '@components';
const StyledTagsContainer = styled.main`
max-width: 1000px;
h1 {
margin-bottom: 50px;
}
ul {
color: var(--light-slate);
li {
font-size: var(--fz-xxl);
a {
color: var(--light-slate);
.count {
color: var(--slate);
font-family: var(--font-mono);
font-size: var(--fz-md);
}
}
}
}
`;
const TagsPage = ({
data: {
allMarkdownRemark: { group },
},
location,
}) => (
←
All memories
Tags
{group.map(tag => (
-
{tag.fieldValue} ({tag.totalCount})
))}
);
TagsPage.propTypes = {
data: PropTypes.shape({
allMarkdownRemark: PropTypes.shape({
group: PropTypes.arrayOf(
PropTypes.shape({
fieldValue: PropTypes.string.isRequired,
totalCount: PropTypes.number.isRequired,
}).isRequired,
),
}),
site: PropTypes.shape({
siteMetadata: PropTypes.shape({
title: PropTypes.string.isRequired,
}),
}),
}),
location: PropTypes.object,
};
export default TagsPage;
export const pageQuery = graphql`
query {
allMarkdownRemark(limit: 2000, filter: { frontmatter: { draft: { ne: true } } }) {
group(field: frontmatter___tags) {
fieldValue
totalCount
}
}
}
`;
================================================
FILE: src/styles/GlobalStyle.js
================================================
import { createGlobalStyle } from 'styled-components';
import fonts from './fonts';
import variables from './variables';
import TransitionStyles from './TransitionStyles';
import PrismStyles from './PrismStyles';
const GlobalStyle = createGlobalStyle`
${fonts};
${variables};
html {
box-sizing: border-box;
width: 100%;
scroll-behavior: smooth;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
::selection {
background-color: var(--lightest-navy);
color: var(--lightest-slate);
}
/* Provide basic, default focus styles.*/
:focus {
outline: 2px dashed var(--green);
outline-offset: 3px;
}
/*
Remove default focus styles for mouse users ONLY if
:focus-visible is supported on this platform.
*/
:focus:not(:focus-visible) {
outline: none;
outline-offset: 0px;
}
/*
Optionally: If :focus-visible is supported on this
platform, provide enhanced focus styles for keyboard
focus.
*/
:focus-visible {
outline: 2px dashed var(--green);
outline-offset: 3px;
}
/* Scrollbar Styles */
html {
scrollbar-width: thin;
scrollbar-color: var(--dark-slate) var(--navy);
}
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: var(--navy);
}
::-webkit-scrollbar-thumb {
background-color: var(--dark-slate);
border: 3px solid var(--navy);
border-radius: 10px;
}
body {
margin: 0;
width: 100%;
min-height: 100%;
overflow-x: hidden;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
background-color: var(--navy);
color: var(--slate);
font-family: var(--font-sans);
font-size: var(--fz-xl);
line-height: 1.3;
@media (max-width: 480px) {
font-size: var(--fz-lg);
}
&.hidden {
overflow: hidden;
}
&.blur {
overflow: hidden;
header {
background-color: transparent;
}
#content > * {
filter: blur(5px) brightness(0.7);
transition: var(--transition);
pointer-events: none;
user-select: none;
}
}
}
#root {
min-height: 100vh;
display: grid;
grid-template-rows: 1fr auto;
grid-template-columns: 100%;
}
main {
margin: 0 auto;
width: 100%;
max-width: 1600px;
min-height: 100vh;
padding: 200px 150px;
@media (max-width: 1080px) {
padding: 200px 100px;
}
@media (max-width: 768px) {
padding: 150px 50px;
}
@media (max-width: 480px) {
padding: 125px 25px;
}
&.fillHeight {
padding: 0 150px;
@media (max-width: 1080px) {
padding: 0 100px;
}
@media (max-width: 768px) {
padding: 0 50px;
}
@media (max-width: 480px) {
padding: 0 25px;
}
}
}
section {
margin: 0 auto;
padding: 100px 0;
max-width: 1000px;
@media (max-width: 768px) {
padding: 80px 0;
}
@media (max-width: 480px) {
padding: 60px 0;
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 10px 0;
font-weight: 600;
color: var(--lightest-slate);
line-height: 1.1;
}
.big-heading {
margin: 0;
font-size: clamp(40px, 8vw, 80px);
}
.medium-heading {
margin: 0;
font-size: clamp(40px, 8vw, 60px);
}
.numbered-heading {
display: flex;
align-items: center;
position: relative;
margin: 10px 0 40px;
width: 100%;
font-size: clamp(26px, 5vw, var(--fz-heading));
white-space: nowrap;
&:before {
position: relative;
bottom: 4px;
counter-increment: section;
content: '0' counter(section) '.';
margin-right: 10px;
color: var(--green);
font-family: var(--font-mono);
font-size: clamp(var(--fz-md), 3vw, var(--fz-xl));
font-weight: 400;
@media (max-width: 480px) {
margin-bottom: -3px;
margin-right: 5px;
}
}
&:after {
content: '';
display: block;
position: relative;
top: -5px;
width: 300px;
height: 1px;
margin-left: 20px;
background-color: var(--lightest-navy);
@media (max-width: 1080px) {
width: 200px;
}
@media (max-width: 768px) {
width: 100%;
}
@media (max-width: 600px) {
margin-left: 10px;
}
}
}
img,
svg,
.gatsby-image-wrapper {
width: 100%;
max-width: 100%;
vertical-align: middle;
}
img[alt=""],
img:not([alt]) {
filter: blur(5px);
}
svg {
width: 100%;
height: 100%;
fill: currentColor;
vertical-align: middle;
&.feather {
fill: none;
}
}
a {
display: inline-block;
text-decoration: none;
text-decoration-skip-ink: auto;
color: inherit;
position: relative;
transition: var(--transition);
&:hover,
&:focus {
color: var(--green);
}
&.inline-link {
${({ theme }) => theme.mixins.inlineLink};
}
}
button {
cursor: pointer;
border: 0;
border-radius: 0;
}
input, textarea {
border-radius: 0;
outline: 0;
&:focus {
outline: 0;
}
&:focus,
&:active {
&::placeholder {
opacity: 0.5;
}
}
}
p {
margin: 0 0 15px 0;
&:last-child,
&:last-of-type {
margin: 0;
}
& > a {
${({ theme }) => theme.mixins.inlineLink};
}
& > code {
background-color: var(--light-navy);
color: var(--white);
font-size: var(--fz-sm);
border-radius: var(--border-radius);
padding: 0.3em 0.5em;
}
}
ul {
&.fancy-list {
padding: 0;
margin: 0;
list-style: none;
font-size: var(--fz-lg);
li {
position: relative;
padding-left: 30px;
margin-bottom: 10px;
&:before {
content: '▹';
position: absolute;
left: 0;
color: var(--green);
}
}
}
}
blockquote {
border-left-color: var(--green);
border-left-style: solid;
border-left-width: 1px;
margin-left: 0px;
margin-right: 0px;
padding-left: 1.5rem;
p {
font-style: italic;
font-size: 24px;
}
}
hr {
background-color: var(--lightest-navy);
height: 1px;
border-width: 0px;
border-style: initial;
border-color: initial;
border-image: initial;
margin: 1rem;
}
code {
font-family: var(--font-mono);
font-size: var(--fz-md);
}
.skip-to-content {
${({ theme }) => theme.mixins.button};
position: absolute;
top: auto;
left: -999px;
width: 1px;
height: 1px;
overflow: hidden;
z-index: -99;
&:hover,
&:focus {
background-color: var(--green);
color: var(--navy);
top: 0;
left: 0;
width: auto;
height: auto;
overflow: auto;
z-index: 99;
box-shadow: none;
transform: none;
}
}
#logo {
color: var(--green);
}
.overline {
color: var(--green);
font-family: var(--font-mono);
font-size: var(--fz-md);
font-weight: 400;
}
.subtitle {
color: var(--green);
margin: 0 0 20px 0;
font-size: var(--fz-md);
font-family: var(--font-mono);
font-weight: 400;
line-height: 1.5;
@media (max-width: 1080px) {
font-size: var(--fz-sm);
}
@media (max-width: 768px) {
font-size: var(--fz-xs);
}
a {
${({ theme }) => theme.mixins.inlineLink};
line-height: 1.5;
}
}
.breadcrumb {
display: flex;
align-items: center;
margin-bottom: 50px;
color: var(--green);
.arrow {
display: block;
margin-right: 10px;
padding-top: 4px;
}
a {
${({ theme }) => theme.mixins.inlineLink};
font-family: var(--font-mono);
font-size: var(--fz-sm);
font-weight: 600;
line-height: 1.5;
text-transform: uppercase;
letter-spacing: 0.1em;
}
}
.gatsby-image-outer-wrapper {
height: 100%;
}
${TransitionStyles};
${PrismStyles};
`;
export default GlobalStyle;
================================================
FILE: src/styles/PrismStyles.js
================================================
import { css } from 'styled-components';
const prismColors = {
bg: `#112340`,
lineHighlight: `#1d2d50`,
blue: `#5ccfe6`,
purple: `#c3a6ff`,
green: `#bae67e`,
yellow: `#ffd580`,
orange: `#ffae57`,
red: `#ef6b73`,
grey: `#a2aabc`,
comment: `#8695b799`,
};
// https://www.gatsbyjs.org/packages/gatsby-remark-prismjs
const PrismStyles = css`
/**
* Add back the container background-color, border-radius, padding, margin
* and overflow that we removed from .
*/
.gatsby-highlight {
background-color: ${prismColors.bg};
color: ${prismColors.grey};
border-radius: var(--border-radius);
margin: 2em 0;
padding: 1.25em;
overflow: auto;
position: relative;
font-family: var(--font-mono);
font-size: var(--fz-md);
}
.gatsby-highlight code[class*='language-'],
.gatsby-highlight pre[class*='language-'] {
height: auto !important;
font-size: var(--fz-sm);
line-height: 1.5;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
tab-size: 2;
hyphens: none;
}
/**
* Remove the default PrismJS theme background-color, border-radius, margin,
* padding and overflow.
* 1. Make the element just wide enough to fit its content.
* 2. Always fill the visible space in .gatsby-highlight.
* 3. Adjust the position of the line numbers
*/
.gatsby-highlight pre[class*='language-'] {
background-color: transparent;
margin: 0;
padding: 0;
overflow: initial;
float: left; /* 1 */
min-width: 100%; /* 2 */
padding-top: 2em;
}
/* File names */
.gatsby-code-title {
padding: 1em 1.5em;
font-family: var(--font-mono);
font-size: var(--fz-xs);
background-color: ${prismColors.bg};
color: ${prismColors.grey};
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
border-bottom: 1px solid ${prismColors.lineHighlight};
& + .gatsby-highlight {
margin-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
/* Line highlighting */
.gatsby-highlight-code-line {
display: block;
background-color: ${prismColors.lineHighlight};
border-left: 2px solid var(--green);
padding-left: calc(1em + 2px);
padding-right: 1em;
margin-right: -1.35em;
margin-left: -1.35em;
}
/* Language badges */
.gatsby-highlight pre[class*='language-']::before {
background: var(--lightest-navy);
color: var(--white);
font-size: var(--fz-xxs);
font-family: var(--font-mono);
line-height: 1.5;
letter-spacing: 0.1em;
text-transform: uppercase;
border-radius: 0 0 3px 3px;
position: absolute;
top: 0;
left: 1.25rem;
padding: 0.25rem 0.5rem;
}
.gatsby-highlight pre[class='language-javascript']::before {
content: 'js';
}
.gatsby-highlight pre[class='language-js']::before {
content: 'js';
}
.gatsby-highlight pre[class='language-jsx']::before {
content: 'jsx';
}
.gatsby-highlight pre[class='language-graphql']::before {
content: 'GraphQL';
}
.gatsby-highlight pre[class='language-html']::before {
content: 'html';
}
.gatsby-highlight pre[class='language-css']::before {
content: 'css';
}
.gatsby-highlight pre[class='language-mdx']::before {
content: 'mdx';
}
.gatsby-highlight pre[class='language-shell']::before {
content: 'shell';
}
.gatsby-highlight pre[class='language-sh']::before {
content: 'sh';
}
.gatsby-highlight pre[class='language-bash']::before {
content: 'bash';
}
.gatsby-highlight pre[class='language-yaml']::before {
content: 'yaml';
}
.gatsby-highlight pre[class='language-markdown']::before {
content: 'md';
}
.gatsby-highlight pre[class='language-json']::before,
.gatsby-highlight pre[class='language-json5']::before {
content: 'json';
}
.gatsby-highlight pre[class='language-diff']::before {
content: 'diff';
}
.gatsby-highlight pre[class='language-text']::before {
content: 'text';
}
.gatsby-highlight pre[class='language-flow']::before {
content: 'flow';
}
/* Prism Styles */
.token {
display: inline;
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: ${prismColors.comment};
}
.token.punctuation {
color: ${prismColors.grey};
}
.token.namespace,
.token.deleted {
color: ${prismColors.red};
}
.token.function-name,
.token.function,
.token.class-name,
.token.constant,
.token.symbol {
color: ${prismColors.yellow};
}
.token.attr-name,
.token.operator,
.token.rule {
color: ${prismColors.orange};
}
.token.keyword,
.token.boolean,
.token.number,
.token.property {
color: ${prismColors.purple};
}
.token.tag,
.token.selector,
.token.important,
.token.atrule,
.token.builtin,
.token.entity,
.token.url {
color: ${prismColors.blue};
}
.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.variable,
.token.inserted {
color: ${prismColors.green};
}
.token.important,
.token.bold {
font-weight: 600;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.namespace {
opacity: 0.7;
}
`;
export default PrismStyles;
================================================
FILE: src/styles/TransitionStyles.js
================================================
import { css } from 'styled-components';
// https://reactcommunity.org/react-transition-group/css-transition
const TransitionStyles = css`
/* Fade up */
.fadeup-enter {
opacity: 0.01;
transform: translateY(20px);
transition: opacity 300ms var(--easing), transform 300ms var(--easing);
}
.fadeup-enter-active {
opacity: 1;
transform: translateY(0px);
transition: opacity 300ms var(--easing), transform 300ms var(--easing);
}
/* Fade down */
.fadedown-enter {
opacity: 0.01;
transform: translateY(-20px);
transition: opacity 300ms var(--easing), transform 300ms var(--easing);
}
.fadedown-enter-active {
opacity: 1;
transform: translateY(0px);
transition: opacity 300ms var(--easing), transform 300ms var(--easing);
}
/* Fade */
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: opacity 300ms var(--easing);
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: opacity 300ms var(--easing);
}
`;
export default TransitionStyles;
================================================
FILE: src/styles/fonts.js
================================================
import { css } from 'styled-components';
import CalibreRegularWoff from '@fonts/Calibre/Calibre-Regular.woff';
import CalibreRegularWoff2 from '@fonts/Calibre/Calibre-Regular.woff2';
import CalibreMediumWoff from '@fonts/Calibre/Calibre-Medium.woff';
import CalibreMediumWoff2 from '@fonts/Calibre/Calibre-Medium.woff2';
import CalibreSemiboldWoff from '@fonts/Calibre/Calibre-Semibold.woff';
import CalibreSemiboldWoff2 from '@fonts/Calibre/Calibre-Semibold.woff2';
import CalibreRegularItalicWoff from '@fonts/Calibre/Calibre-RegularItalic.woff';
import CalibreRegularItalicWoff2 from '@fonts/Calibre/Calibre-RegularItalic.woff2';
import CalibreMediumItalicWoff from '@fonts/Calibre/Calibre-MediumItalic.woff';
import CalibreMediumItalicWoff2 from '@fonts/Calibre/Calibre-MediumItalic.woff2';
import CalibreSemiboldItalicWoff from '@fonts/Calibre/Calibre-SemiboldItalic.woff';
import CalibreSemiboldItalicWoff2 from '@fonts/Calibre/Calibre-SemiboldItalic.woff2';
import SFMonoRegularWoff from '@fonts/SFMono/SFMono-Regular.woff';
import SFMonoRegularWoff2 from '@fonts/SFMono/SFMono-Regular.woff2';
import SFMonoSemiboldWoff from '@fonts/SFMono/SFMono-Semibold.woff';
import SFMonoSemiboldWoff2 from '@fonts/SFMono/SFMono-Semibold.woff2';
import SFMonoRegularItalicWoff from '@fonts/SFMono/SFMono-RegularItalic.woff';
import SFMonoRegularItalicWoff2 from '@fonts/SFMono/SFMono-RegularItalic.woff2';
import SFMonoSemiboldItalicWoff from '@fonts/SFMono/SFMono-SemiboldItalic.woff';
import SFMonoSemiboldItalicWoff2 from '@fonts/SFMono/SFMono-SemiboldItalic.woff2';
const calibreNormalWeights = {
400: [CalibreRegularWoff, CalibreRegularWoff2],
500: [CalibreMediumWoff, CalibreMediumWoff2],
600: [CalibreSemiboldWoff, CalibreSemiboldWoff2],
};
const calibreItalicWeights = {
400: [CalibreRegularItalicWoff, CalibreRegularItalicWoff2],
500: [CalibreMediumItalicWoff, CalibreMediumItalicWoff2],
600: [CalibreSemiboldItalicWoff, CalibreSemiboldItalicWoff2],
};
const sfMonoNormalWeights = {
400: [SFMonoRegularWoff, SFMonoRegularWoff2],
600: [SFMonoSemiboldWoff, SFMonoSemiboldWoff2],
};
const sfMonoItalicWeights = {
400: [SFMonoRegularItalicWoff, SFMonoRegularItalicWoff2],
600: [SFMonoSemiboldItalicWoff, SFMonoSemiboldItalicWoff2],
};
const calibre = {
name: 'Calibre',
normal: calibreNormalWeights,
italic: calibreItalicWeights,
};
const sfMono = {
name: 'SF Mono',
normal: sfMonoNormalWeights,
italic: sfMonoItalicWeights,
};
const createFontFaces = (family, style = 'normal') => {
let styles = '';
for (const [weight, formats] of Object.entries(family[style])) {
const woff = formats[0];
const woff2 = formats[1];
styles += `
@font-face {
font-family: '${family.name}';
src: url(${woff2}) format('woff2'),
url(${woff}) format('woff');
font-weight: ${weight};
font-style: ${style};
font-display: auto;
}
`;
}
return styles;
};
const calibreNormal = createFontFaces(calibre);
const calibreItalic = createFontFaces(calibre, 'italic');
const sfMonoNormal = createFontFaces(sfMono);
const sfMonoItalic = createFontFaces(sfMono, 'italic');
const Fonts = css`
${calibreNormal + calibreItalic + sfMonoNormal + sfMonoItalic}
`;
export default Fonts;
================================================
FILE: src/styles/index.js
================================================
export { default as theme } from './theme';
export { default as GlobalStyle } from './GlobalStyle';
export { default as mixins } from './mixins';
================================================
FILE: src/styles/mixins.js
================================================
import { css } from 'styled-components';
const button = css`
color: var(--green);
background-color: transparent;
border: 1px solid var(--green);
border-radius: var(--border-radius);
font-size: var(--fz-xs);
font-family: var(--font-mono);
line-height: 1;
text-decoration: none;
padding: 1.25rem 1.75rem;
transition: var(--transition);
&:hover,
&:focus-visible {
outline: none;
box-shadow: 4px 4px 0 0 var(--green);
transform: translate(-5px, -5px);
}
&:after {
display: none !important;
}
`;
const mixins = {
flexCenter: css`
display: flex;
justify-content: center;
align-items: center;
`,
flexBetween: css`
display: flex;
justify-content: space-between;
align-items: center;
`,
link: css`
display: inline-block;
text-decoration: none;
text-decoration-skip-ink: auto;
color: inherit;
position: relative;
transition: var(--transition);
&:hover,
&:focus-visible {
color: var(--green);
outline: 0;
}
`,
inlineLink: css`
display: inline-block;
position: relative;
color: var(--green);
transition: var(--transition);
&:hover,
&:focus-visible {
color: var(--green);
outline: 0;
&:after {
width: 100%;
}
& > * {
color: var(--green) !important;
transition: var(--transition);
}
}
&:after {
content: '';
display: block;
width: 0;
height: 1px;
position: relative;
bottom: 0.37em;
background-color: var(--green);
opacity: 0.5;
@media (prefers-reduced-motion: no-preference) {
transition: var(--transition);
}
}
`,
button,
smallButton: css`
color: var(--green);
background-color: transparent;
border: 1px solid var(--green);
border-radius: var(--border-radius);
padding: 0.75rem 1rem;
font-size: var(--fz-xs);
font-family: var(--font-mono);
line-height: 1;
text-decoration: none;
transition: var(--transition);
&:hover,
&:focus-visible {
outline: none;
box-shadow: 3px 3px 0 0 var(--green);
transform: translate(-4px, -4px);
}
&:after {
display: none !important;
}
`,
bigButton: css`
color: var(--green);
background-color: transparent;
border: 1px solid var(--green);
border-radius: var(--border-radius);
padding: 1.25rem 1.75rem;
font-size: var(--fz-sm);
font-family: var(--font-mono);
line-height: 1;
text-decoration: none;
transition: var(--transition);
&:hover,
&:focus-visible {
outline: none;
box-shadow: 4px 4px 0 0 var(--green);
transform: translate(-5px, -5px);
}
&:after {
display: none !important;
}
`,
boxShadow: css`
box-shadow: 0 10px 30px -15px var(--navy-shadow);
transition: var(--transition);
&:hover,
&:focus-visible {
box-shadow: 0 20px 30px -15px var(--navy-shadow);
}
`,
fancyList: css`
padding: 0;
margin: 0;
list-style: none;
font-size: var(--fz-lg);
li {
position: relative;
padding-left: 30px;
margin-bottom: 10px;
&:before {
content: '▹';
position: absolute;
left: 0;
color: var(--green);
}
}
`,
resetList: css`
list-style: none;
padding: 0;
margin: 0;
`,
};
export default mixins;
================================================
FILE: src/styles/theme.js
================================================
import mixins from './mixins';
const theme = {
bp: {
mobileS: `max-width: 330px`,
mobileM: `max-width: 400px`,
mobileL: `max-width: 480px`,
tabletS: `max-width: 600px`,
tabletL: `max-width: 768px`,
desktopXS: `max-width: 900px`,
desktopS: `max-width: 1080px`,
desktopM: `max-width: 1200px`,
desktopL: `max-width: 1400px`,
},
mixins,
};
export default theme;
================================================
FILE: src/styles/variables.js
================================================
import { css } from 'styled-components';
const variables = css`
:root {
--dark-navy: #020c1b;
--navy: #0a192f;
--light-navy: #112240;
--lightest-navy: #233554;
--navy-shadow: rgba(2, 12, 27, 0.7);
--dark-slate: #495670;
--slate: #8892b0;
--light-slate: #a8b2d1;
--lightest-slate: #ccd6f6;
--white: #e6f1ff;
--green: #64ffda;
--green-tint: rgba(100, 255, 218, 0.1);
--pink: #f57dff;
--blue: #57cbff;
--font-sans: 'Calibre', 'Inter', 'San Francisco', 'SF Pro Text', -apple-system, system-ui,
sans-serif;
--font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace;
--fz-xxs: 12px;
--fz-xs: 13px;
--fz-sm: 14px;
--fz-md: 16px;
--fz-lg: 18px;
--fz-xl: 20px;
--fz-xxl: 22px;
--fz-heading: 32px;
--border-radius: 4px;
--nav-height: 100px;
--nav-scroll-height: 70px;
--tab-height: 42px;
--tab-width: 120px;
--easing: cubic-bezier(0.645, 0.045, 0.355, 1);
--transition: all 0.25s cubic-bezier(0.645, 0.045, 0.355, 1);
--hamburger-width: 30px;
--ham-before: top 0.1s ease-in 0.25s, opacity 0.1s ease-in;
--ham-before-active: top 0.1s ease-out, opacity 0.1s ease-out 0.12s;
--ham-after: bottom 0.1s ease-in 0.25s, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19);
--ham-after-active: bottom 0.1s ease-out,
transform 0.22s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s;
}
`;
export default variables;
================================================
FILE: src/templates/post.js
================================================
import React from 'react';
import { graphql, Link } from 'gatsby';
import kebabCase from 'lodash/kebabCase';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import styled from 'styled-components';
import { Layout } from '@components';
const StyledPostContainer = styled.main`
max-width: 1000px;
`;
const StyledPostHeader = styled.header`
margin-bottom: 50px;
.tag {
margin-right: 10px;
}
`;
const StyledPostContent = styled.div`
margin-bottom: 100px;
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 2em 0 1em;
}
p {
margin: 1em 0;
line-height: 1.5;
color: var(--light-slate);
}
a {
${({ theme }) => theme.mixins.inlineLink};
}
code {
background-color: var(--lightest-navy);
color: var(--lightest-slate);
border-radius: var(--border-radius);
font-size: var(--fz-sm);
padding: 0.2em 0.4em;
}
pre code {
background-color: transparent;
padding: 0;
}
`;
const PostTemplate = ({ data, location }) => {
const { frontmatter, html } = data.markdownRemark;
const { title, date, tags } = frontmatter;
return (
←
All memories
{title}
—
{tags &&
tags.length > 0 &&
tags.map((tag, i) => (
#{tag}
))}
);
};
export default PostTemplate;
PostTemplate.propTypes = {
data: PropTypes.object,
location: PropTypes.object,
};
export const pageQuery = graphql`
query($path: String!) {
markdownRemark(frontmatter: { slug: { eq: $path } }) {
html
frontmatter {
title
description
date
slug
tags
}
}
}
`;
================================================
FILE: src/templates/tag.js
================================================
import React from 'react';
import { Link, graphql } from 'gatsby';
import kebabCase from 'lodash/kebabCase';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import styled from 'styled-components';
import { Layout } from '@components';
const StyledTagsContainer = styled.main`
max-width: 1000px;
a {
${({ theme }) => theme.mixins.inlineLink};
}
h1 {
${({ theme }) => theme.mixins.flexBetween};
margin-bottom: 50px;
a {
font-size: var(--fz-lg);
font-weight: 400;
}
}
ul {
li {
font-size: 24px;
h2 {
font-size: inherit;
margin: 0;
a {
color: var(--light-slate);
}
}
.subtitle {
color: var(--slate);
font-size: var(--fz-sm);
.tag {
margin-right: 10px;
}
}
}
}
`;
const TagTemplate = ({ pageContext, data, location }) => {
const { tag } = pageContext;
const { edges } = data.allMarkdownRemark;
return (
←
All memories
#{tag}
View all tags
{edges.map(({ node }) => {
const { title, slug, date, tags } = node.frontmatter;
return (
-
{title}
—
{tags &&
tags.length > 0 &&
tags.map((tag, i) => (
#{tag}
))}
);
})}
);
};
export default TagTemplate;
TagTemplate.propTypes = {
pageContext: PropTypes.shape({
tag: PropTypes.string.isRequired,
}),
data: PropTypes.shape({
allMarkdownRemark: PropTypes.shape({
totalCount: PropTypes.number.isRequired,
edges: PropTypes.arrayOf(
PropTypes.shape({
node: PropTypes.shape({
frontmatter: PropTypes.shape({
title: PropTypes.string.isRequired,
}),
}),
}).isRequired,
),
}),
}),
location: PropTypes.object,
};
export const pageQuery = graphql`
query($tag: String!) {
allMarkdownRemark(
limit: 2000
sort: { fields: [frontmatter___date], order: DESC }
filter: { frontmatter: { tags: { in: [$tag] } } }
) {
totalCount
edges {
node {
frontmatter {
title
description
date
slug
tags
}
}
}
}
}
`;
================================================
FILE: src/utils/index.js
================================================
export const hex2rgba = (hex, alpha = 1) => {
const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
return `rgba(${r},${g},${b},${alpha})`;
};
export const navDelay = 1000;
export const loaderDelay = 2000;
export const KEY_CODES = {
ARROW_LEFT: 'ArrowLeft',
ARROW_LEFT_IE11: 'Left',
ARROW_RIGHT: 'ArrowRight',
ARROW_RIGHT_IE11: 'Right',
ARROW_UP: 'ArrowUp',
ARROW_UP_IE11: 'Up',
ARROW_DOWN: 'ArrowDown',
ARROW_DOWN_IE11: 'Down',
ESCAPE: 'Escape',
ESCAPE_IE11: 'Esc',
TAB: 'Tab',
SPACE: ' ',
SPACE_IE11: 'Spacebar',
ENTER: 'Enter',
};
================================================
FILE: src/utils/sr.js
================================================
import ScrollReveal from 'scrollreveal';
const isSSR = typeof window === 'undefined';
const sr = isSSR ? null : ScrollReveal();
export default sr;