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
================================================
<div align="center">
<img alt="Logo" src="https://raw.githubusercontent.com/bchiang7/v4/main/src/images/logo.png" width="100" />
</div>
<h1 align="center">
brittanychiang.com - v4
</h1>
<p align="center">
The fourth iteration of <a href="https://brittanychiang.com" target="_blank">brittanychiang.com</a> built with <a href="https://www.gatsbyjs.org/" target="_blank">Gatsby</a> and hosted with <a href="https://www.netlify.com/" target="_blank">Netlify</a>
</p>
<p align="center">
Previous iterations:
<a href="https://github.com/bchiang7/v1" target="_blank">v1</a>,
<a href="https://github.com/bchiang7/v2" target="_blank">v2</a>,
<a href="https://github.com/bchiang7/bchiang7.github.io" target="_blank">v3</a>
</p>
<p align="center">
<a href="https://app.netlify.com/sites/brittanychiang/deploys" target="_blank">
<img src="https://api.netlify.com/api/v1/badges/1963b488-7b78-48c9-9e2d-6fb5e47ab3af/deploy-status" alt="Netlify Status" />
</a>
</p>

## 🚨 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 `<a>`). 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 `<html>`, not `<body>`, so we can run the JS before the DOM finishes rendering
- Run local storage check in the `<head>`
- JS for toggle button click handler can come after render
## HTML
```html
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
...
<script>
// If there's a theme stored in localStorage, use it on the <html>
const localStorageTheme = localStorage.getItem('theme');
if (localStorageTheme) {
document.documentElement.setAttribute('data-theme', localStorageTheme);
}
</script>
</head>
<body>
<div class="theme-toggle">
<button
class="theme-toggle-btn js-theme-toggle"
aria-label="Activate dark mode"
title="Activate dark mode"
>
<!--
<svg class="light-mode">
<use xlink:href="#sun"></use>
</svg>
<svg class="dark-mode">
<use xlink:href="#moon"></use>
</svg>
-->
</button>
</div>
<script src="app.js"></script>
</body>
</html>
```
## CSS Variables
```css
:root {
--bg: #ffffff;
--text: #000000;
}
[data-theme='dark'] {
--bg: #000000;
--text: #ffffff;
}
```
## JavaScript
```js:title=app.js
const themeToggleBtn = document.querySelector('.js-theme-toggle');
themeToggleBtn.addEventListener('click', () => onToggleClick());
const onToggleClick = () => {
const { theme } = document.documentElement.dataset;
const themeTo = theme && theme === 'light' ? 'dark' : 'light';
const label = `Activate ${theme} mode`;
document.documentElement.setAttribute('data-theme', themeTo);
localStorage.setItem('theme', themeTo);
themeToggleBtn.setAttribute('aria-label', label);
themeToggleBtn.setAttribute('title', label);
};
```
## Resources
- <https://css-tricks.com/a-complete-guide-to-dark-mode-on-the-web/>
- <https://css-tricks.com/flash-of-inaccurate-color-theme-fart/>
- <https://mxb.dev/blog/color-theme-switcher/>
- <https://www.joshwcomeau.com/react/dark-mode/>
- <https://web.dev/prefers-color-scheme/>
================================================
FILE: content/posts/docker-compose-error/index.md
================================================
---
title: Docker Compose Error
description: docker-compose version discrepancies
date: '2019-12-13'
draft: false
slug: '/pensieve/docker-error'
tags:
- WordPress
- Docker
---
## Problem
Recently while updating with [Skela](https://github.com/Upstatement/skela-wp-theme) with webpack, I encountered a weird error where I wasn't able to run a simple script:
```shell:title=bin/composer
#!/bin/bash
docker-compose exec -w /var/www/html/wp-content/themes/skela wordpress composer "$@"
```
When trying to run this script via `./bin/composer install`, I got this error in my terminal:
```shell
ERROR: Setting workdir for exec is not supported in API < 1.35 (1.30)
```
The error was coming from the `-w` flag in the `docker-compose exec` command in the `composer` script.
## Solution
Turns The fix was to update the version in my `docker-compose.yml` file to from version `3.5` to `3.6`. It's strange because 3.5 isn't anywhere close to the API version `1.35` from the error message 🤷♀️
```yaml:title=docker-compose.yml
version: '3.6' # highlight-line
services:
wordpress:
build:
```
================================================
FILE: content/posts/markdown-playground/index.md
================================================
---
title: Markdown Test File
description: abc234
date: 2019-12-07
draft: true
slug: /pensieve/markdown-playground
tags:
- Testing
---

```jsx
class FlavorForm extends React.Component { // highlight-line
constructor(props) {
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
// highlight-next-line
this.setState({value: event.target.value});
}
// highlight-start
handleSubmit(event) {
alert('Your favorite flavor is: ' + this.state.value);
event.preventDefault();
}
// highlight-end
render() {
return (
{ /* highlight-range{1,4-9,12} */ }
<form onSubmit={this.handleSubmit}>
<label>
Pick your favorite flavor:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
```
```javascript:title=highlight.js
// Here is a comment
function $initHighlight(block, cls) {
try {
if (cls.search(/\bno\-highlight\b/) != -1)
return process(block, true, 0x0F) +
` class="${cls}"`;
} catch (e) {
/* handle exception */
}
for (var i = 0 / 2; i < classes.length; i++) {
if (checkCondition(classes[i]) === undefined) {
console.log('undefined');
}
}
return (
<div>
<web-component>{block}</web-component>
</div>
)
}
export $initHighlight;
```
This is a paragraph.
This is a paragraph.
# Header 1
## Header 2
Header 1
========
Header 2
--------
```css
@import 'compass/reset';
// variables
$colorGreen: #008000;
$colorGreenDark: darken($colorGreen, 10);
@mixin container {
max-width: 980px;
}
// mixins with parameters
@mixin button($color: green) {
@if ($color == green) {
background-color: #008000;
} @else if ($color == red) {
background-color: #b22222;
}
}
button {
@include button(red);
}
div,
.navbar,
#header,
input[type='input'] {
font-family: 'Helvetica Neue', Arial, sans-serif;
width: auto;
margin: 0 auto;
display: block;
}
.row-12 > [class*='spans'] {
border-left: 1px solid #b5c583;
}
// nested definitions
ul {
width: 100%;
padding: {
left: 5px;
right: 5px;
}
li {
float: left;
margin-right: 10px;
.home {
background: url('http://placehold.it/20') scroll no-repeat 0 0;
}
}
}
.banner {
@extend .container;
}
a {
color: $colorGreen;
&:hover {
color: $colorGreenDark;
}
&:visited {
color: #c458cb;
}
}
@for $i from 1 through 5 {
.span#{$i} {
width: 20px * $i;
}
}
@mixin mobile {
@media screen and (max-width: 600px) {
@content;
}
}
```
```markdown
# hello world
you can write text [with links](http://example.com) inline or [link references][1].
- one _thing_ has *em*phasis
- two **things** are **bold**
[1]: http://example.com
---
# hello world
<this_is inline="xml"></this_is>
> markdown is so cool
so are code segments
1. one thing (yeah!)
2. two thing `i can write code`, and `more` wipee!
```
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
# Header 1 #
## Header 2 ##
### Header 3 ###
#### Header 4 ####
##### Header 5 #####
###### Header 6 ######
> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
> ## This is a header
>
> 1. This is the first list item.
> 2. This is the second list item.
>
> Here's some example code:
>
> Markdown.generate();
> ## This is a header.
> 1. This is the first list item.
> 2. This is the second list item.
>
> Here's some example code:
>
> Markdown.generate();
- Red
- Green
- Blue
- Red
- Green
- Blue
- Red
- Green
- Blue
```markdown
- Red
- Green
- Blue
* Red
* Green
* Blue
- Red
- Green
- Blue
```
1. Buy flour and salt
2. Mix together with water
3. Bake
```markdown
1. Buy flour and salt
1. Mix together with water
1. Bake
```
Paragraph:
Code
<!-- -->
Paragraph:
Code
---
---
---
---
---
* * *
***
*****
- - -
---------------------------------------
This is [an example](http://example.com 'Example') link.
[This link](http://example.com) has no title attr.
This is [an example][id] reference-style link.
[id]: http://example.com 'Optional Title'
This is [an example](http://example.com "Example") link.
[This link](http://example.com) has no title attr.
This is [an example] [id] reference-style link.
[id]: http://example.com "Optional Title"
_single asterisks_
_single underscores_
**double asterisks**
**double underscores**
*single asterisks*
_single underscores_
**double asterisks**
__double underscores__
This paragraph has some `code` in it.
This paragraph has some `code` in it.
================================================
FILE: content/posts/wordpress-publish-error/index.md
================================================
---
title: WordPress Publishing Error
description: Trying to create a simple post in WordPress
date: 2019-12-03
draft: false
slug: /pensieve/wordpress-publish-error
tags:
- WordPress
---
## Problem
Recently while working on a WordPress project with [Ups Dock](https://github.com/Upstatement/ups-dock), I encountered a weird error where I wasn't able to update or publish a simple post in my local WP admin.
It looked something like this:

Sometimes the error message would be slightly more helpful: `Publishing failed. Error message: The response is not a valid JSON response.`

And if I popped open the console, I saw these errors:

## Solution
Since the error message had to do with a JSON response, I initially thought it was a Gutenberg or ACF issue. But it turned out this was happening because I was on the https WP admin (i.e. [https://project.ups.dock/wp-admin](https://project.ups.dock/wp-admin)), not the unsecure WP admin ([http://project.ups.dock/wp-admin](http://project.ups.dock/wp-admin)).
It was a CORS error!! I was trying to modify a non-https domain from a https domain. Switching to a non-https WP admin allowed me to publish posts with no problem.
================================================
FILE: content/projects/AMFM.md
================================================
---
date: '2017-11-01'
title: 'Apple Music Facebook Messenger Integration'
github: ''
external: 'https://www.theverge.com/2017/10/5/16433770/facebook-messenger-apple-music-bot-song-streaming'
tech:
- Ember
- JS
- SCSS
company: 'Apple'
showInProjects: true
---
Facebook Messenger chat bot extension featuring authentication and full song streaming from within the Messenger app. Read more about it on [The Verge](https://www.theverge.com/2017/10/5/16433770/facebook-messenger-apple-music-bot-song-streaming).
================================================
FILE: content/projects/AlgoliaWordPressMediumPost.md
================================================
---
date: '2020-03-27'
title: 'Integrating Algolia Search with WordPress Multisite'
github: ''
external: 'https://medium.com/stories-from-upstatement/integrating-algolia-search-with-wordpress-multisite-e2dea3ed449c'
tech:
- Algolia
- WordPress
- PHP
company: 'Upstatement'
showInProjects: true
---
Building a custom multisite compatible WordPress plugin to build global search with Algolia
================================================
FILE: content/projects/AppleMusicEmbedPlayer.md
================================================
---
date: '2017-12-01'
title: 'Apple Music Embeddable Web Player Widget'
github: ''
external: 'https://tools.applemusic.com/en-us'
tech:
- MusicKit.js
- JS
- SCSS
company: 'Apple'
showInProjects: true
---
Embeddable web player widget for Apple Music that lets users log in and listen to full song playback in the browser leveraging [MusicKit.js](https://developer.apple.com/documentation/musickitjs). Read more about this project on [9to5Mac](https://9to5mac.com/2018/06/03/apple-music-embeddable-web-player-listen-browser/).
================================================
FILE: content/projects/Blistabloc.md
================================================
---
date: '2018-05-01'
title: 'blistabloc'
github: ''
external: 'https://blistabloc.com/'
tech:
- WordPress
- Timber
- WooCommerce
company: 'Scout'
showInProjects: false
---
Custom WordPress theme and e-commerce site built with Timber and WooCommerce for blistabloc, a start-up selling the only reactive shoe insert that prevents blisters from forming.
================================================
FILE: content/projects/CourseSource.md
================================================
---
date: '2016-04-01'
title: 'CourseSource'
github: 'https://github.com/bchiang7/WebDevSpring2016/tree/master/public/project'
external: ''
tech:
- Angular
- Node
- Express
- MongoDB
company: 'Northeastern'
showInProjects: false
---
Web application built on the MEAN (MongoDB, Express, Angular, Node) stack with the intention of providing Northeastern students a better experience browsing the courses offered at Northeastern.
================================================
FILE: content/projects/CrowdDJ.md
================================================
---
date: '2017-03-01'
title: 'Crowd DJ'
github: 'https://github.com/crowddj/crowddj-react'
external: ''
tech:
- React
- Firebase
- Spotify API
company: HackBeanpot 2017
showInProjects: false
---
Web app that allows people to crowdsource a party's music queue. Allows people to request songs, upvote songs, rate songs, etc. so the DJ can see how the crowd is feeling and queue songs accordingly. Won Best UI/UX Design at Hackbeanpot 2017.
================================================
FILE: content/projects/Devoted.md
================================================
---
date: '2018-12-01'
title: 'Devoted Health'
github: ''
external: 'https://www.devoted.com/'
tech:
- Gatsby
- TypeScript
- Algolia
company: 'Upstatement'
showInProjects: false
---
A site for a revolutionary healthcare company, including an Algolia instant search integration
================================================
FILE: content/projects/EverytownIdealState.md
================================================
---
date: '2022-01-20'
title: 'Everytown Gun Law Rankings'
github: ''
external: 'https://everytownresearch.org/rankings/'
tech:
- WordPress
- Timber
- PHP
- Airtable API
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/Flagship.md
================================================
---
date: '2018-10-01'
title: 'Flagship Pioneering'
github: ''
external: 'https://www.flagshippioneering.com/'
tech:
- Craft CMS
- Chart.js
company: 'Upstatement'
showInProjects: false
---
A marketing site for an ambitious life sciences venture capital company.
================================================
FILE: content/projects/Fontipsums.md
================================================
---
date: '2016-01-01'
title: 'Fontipsums'
github: 'https://github.com/bchiang7/fontipsums/'
external: 'http://bchiang7.github.io/fontipsums/'
tech:
- HTML
- SCSS
showInProjects: true
---
Simple website to display some of my favorite font pairings combined with some fun lorem ipsum variations found on the web.
================================================
FILE: content/projects/GoogleKeepClone.md
================================================
---
date: '2018-12-29'
title: 'Google Keep Clone'
github: 'https://github.com/bchiang7/google-keep-vue-firebase'
external: 'https://keep-vue.netlify.com/'
tech:
- Vue
- Firebase
showInProjects: true
---
A simple Google Keep clone built with Vue and Firebase.
================================================
FILE: content/projects/HBS.md
================================================
---
date: '2022-10-08'
title: 'Harvard Business School Design System'
github: ''
external: ''
tech:
- Storybook
- React
- TypeScript
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/HalcyonTheme.md
================================================
---
date: '2017-12-27'
title: 'Halcyon Theme'
github: 'https://github.com/bchiang7/halcyon-site'
external: 'https://halcyon-theme.netlify.com/'
tech:
- VS Code
- Sublime Text
- Atom
- iTerm2
- Hyper
showInProjects: false
---
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/projects/HeadlessCMSMediumPost.md
================================================
---
date: '2019-11-12'
title: 'Building a Headless Mobile App CMS From Scratch'
github: ''
external: 'https://medium.com/stories-from-upstatement/building-a-headless-mobile-app-cms-from-scratch-bab2d17744d9'
tech:
- Node
- Express
- Firebase
- Vue
company: 'Upstatement'
showInProjects: true
---
Find out how we built a custom headless CMS with Node, Express, and Firebase for a project at Upstatement
================================================
FILE: content/projects/Interventions.md
================================================
---
date: '2017-08-01'
title: 'Interventions'
github: ''
external: 'https://interventions.design/'
tech:
- Jekyll
- SCSS
- JS
company: 'Scout'
showInProjects: false
---
Interactive marketing website for Northeastern's first annual student-led design conference, Interventions.
================================================
FILE: content/projects/JetBlueHumanKinda.md
================================================
---
date: '2015-10-01'
title: 'JetBlue HumanKinda'
github: ''
external: 'https://us.mullenlowe.com/work/humankinda/'
tech:
- Tumblr
- HTML
- CSS
- JS
company: 'MullenLowe'
showInProjects: false
---
Tumblr site complementing JetBlue's HumanKinda campaign and documentary. Includes an interactive quiz to determine how "HumanKinda" you are. Learn more about this project [here](https://us.mullenlowe.com/work/humankinda/).
================================================
FILE: content/projects/KoalaHealth.md
================================================
---
date: '2021-09-01'
title: 'Koala Health'
github: ''
external: 'https://www.koala.health/'
tech:
- Next.js
- TypeScript
- Redux Toolkit
- Stripe
- Algolia
- Firebase Auth
- Formik
- Yup
- Vercel
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/LonelyPlanetDBMS.md
================================================
---
date: '2017-06-22'
title: 'Lonely Planet DBMS'
github: 'https://github.com/bchiang7/CS3200-Project'
external: ''
tech:
- Python
- MySQL
- Flask
- JS
company: 'Northeastern'
showInProjects: false
---
A simple web application that allows users to filter through and leave reviews in a database of Lonely Planet's Top 500 Travel Destinations.
================================================
FILE: content/projects/MichelleWu.md
================================================
---
date: '2020-09-15'
title: 'Michelle Wu for Boston Grassroots Toolkit'
github: ''
external: 'https://toolkit.michelleforboston.com/'
tech:
- Gatsby
- Styled Components
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/MomsDemandAction.md
================================================
---
date: '2019-11-12'
title: 'Moms Demand Action Mobile App'
github: ''
external: 'https://www.upstatement.com/work/moms-demand-action/'
ios: 'https://apps.apple.com/us/app/demand-action/id1475502876'
android: 'https://play.google.com/store/apps/details?id=com.momsdemandaction.app'
tech:
- NativeScript Vue
- iOS
- Android
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/MyNEURedesign.md
================================================
---
date: '2017-04-03'
title: 'myNEU Redesign'
github: 'https://github.com/bchiang7/Redesign-myNEU'
external: 'https://bchiang7.github.io/Redesign-myNEU/'
tech:
- Jekyll
- SCSS
- JS
company: 'Northeastern'
showInProjects: false
---
Student web portal prototype built after conducting multiple rounds of user testing that aimed to improve the current portal to provide students at Northeastern University with a better user experience.
================================================
FILE: content/projects/NUWITSite.md
================================================
---
date: '2015-12-20'
title: 'NU Women in Tech'
github: 'https://github.com/nuwit/website'
external: 'https://nuwit.ccs.neu.edu/'
tech:
- Jekyll
- Bootstrap
company: 'Northeastern'
showInProjects: true
---
Complete overhaul and redesign of NU Women in Tech’s club website using Jekyll, built while serving as web chair on the e-board.
================================================
FILE: content/projects/NortheasternCSSH.md
================================================
---
date: '2020-07-16'
title: 'Northeastern CSSH'
github: ''
external: 'https://cssh.northeastern.edu/'
tech:
- WordPress
- Timber
- WordPress Multisite
- PHP
- Algolia
- JS
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/OctoProfile.md
================================================
---
date: '2019-07-15'
title: 'OctoProfile'
github: 'https://github.com/bchiang7/octoprofile'
external: 'https://octoprofile.now.sh'
tech:
- Next.js
- Chart.js
- GitHub API
showInProjects: true
---
A nicer look at your GitHub profile and repo stats. Includes data visualizations of your top languages, starred repositories, and sort through your top repos by number of stars, forks, and size.
================================================
FILE: content/projects/OneCardForAll.md
================================================
---
date: '2015-12-01'
title: 'One Card For All'
github: ''
external: 'https://us.mullenlowe.com/work/one-card-for-all/'
tech:
- HTML
- SCSS
- JS
- jQuery
company: 'MullenLowe'
showInProjects: false
---
Interactive holiday site for MullenLowe built around an algorithm that generated a holiday greeting to each and every person on the planet. Check out this short [video](https://us.mullenlowe.com/work/one-card-for-all/) describing the project.
================================================
FILE: content/projects/PhillySports.md
================================================
---
date: '2021-07-01'
title: 'Philadelphia Inquirer Sports Scoreboards'
github: ''
external: 'https://www.inquirer.com/sports/'
tech:
- React
- TypeScript
- Stats Perform API
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/Pratt.md
================================================
---
date: '2022-08-08'
title: 'Pratt'
github: ''
external: 'https://www.pratt.edu/'
tech:
- WordPress
- Timber
- WordPress Multisite
- Gutenberg
- JS
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/ReactResume.md
================================================
---
date: '2016-08-01'
title: 'React Profile'
github: 'https://github.com/bchiang7/react-profile'
external: 'https://bchiang7.github.io/react-profile/'
tech:
- React
- CSS
showInProjects: true
---
Online version of my 2016 resume made for fun. I was interested in learning React.js, so I found a simple tutorial and it spun into a weekend project.
================================================
FILE: content/projects/Screentime.md
================================================
---
date: '2016-11-01'
title: 'Screentime 2.0'
github: ''
external: 'https://starry.com/blog/product/whats-new-screentime-just-got-better-for-parents'
android: 'https://play.google.com/store/apps/details?id=com.starry.management&hl=en_US'
tech:
- Cordova
- Backbone
- Marionette
company: 'Starry'
showInProjects: true
---
Starry Station android app feature that provided users with the ability to easily filter content, pause the internet, and even create custom rules for blocking apps like Facebook and Twitter right from their phones.
================================================
FILE: content/projects/SpotifyProfile.md
================================================
---
date: '2018-12-18'
title: 'Spotify Profile'
github: 'https://github.com/bchiang7/spotify-profile'
external: 'https://spotify-profile.herokuapp.com/'
tech:
- React
- Express
- Styled Components
showInProjects: false
---
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/projects/SpotifyTopTracks2017.md
================================================
---
date: '2018-04-20'
title: 'Spotify’s Top Tracks of 2017'
github: 'https://github.com/bchiang7/spotify-top-tracks-2017'
external: ''
tech:
- R
- Spotify Web API
company: 'Northeastern'
showInProjects: false
---
R Project for my Data Science class at Northeastern to analyze the top Spotify tracks of 2017 and their audio features.
================================================
FILE: content/projects/The19th.md
================================================
---
date: '2020-08-02'
title: 'The 19th News'
github: ''
external: 'https://19thnews.org/'
tech:
- WordPress
- Timber
- Gutenberg
- PHP
- JS
- Mailchimp
- AMP
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/Threadable.md
================================================
---
date: '2022-09-08'
title: 'Threadable'
github: ''
external: 'https://www.threadablebooks.com/'
ios: 'https://apps.apple.com/us/app/threadable/id1550995547'
tech:
- React Native
- Ruby on Rails
- Firebase
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/TimeToHaveMoreFun.md
================================================
---
date: '2020-01-10'
title: 'Time to Have More Fun'
github: 'https://github.com/bchiang7/time-to-have-more-fun'
external: 'https://time-to-have-more-fun.now.sh/'
tech:
- Next.js
- Tailwind CSS
- Firebase
company: ''
showInProjects: true
---
A single page web app for helping me choose where to travel, built with Next.js, Firebase, and Tailwind CSS
================================================
FILE: content/projects/UpstatementDotCom.md
================================================
---
date: '2019-11-25'
title: 'Upstatement.com'
github: ''
external: 'https://www.upstatement.com/'
tech:
- Nuxt
- Vue
- Prismic
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/Vanderbilt.md
================================================
---
date: '2021-06-01'
title: 'Vanderbilt Design System'
github: ''
external: 'https://www.vanderbilt.edu/'
tech:
- Twig
- Puppy
- JS
company: 'Upstatement'
showInProjects: false
---
================================================
FILE: content/projects/WeatherWidget.md
================================================
---
date: '2016-11-16'
title: 'Weather Widget'
github: 'https://github.com/bchiang7/DemoWebApp'
external: 'http://quiet-dusk-89245.herokuapp.com/'
tech:
- Node
- Express
- EJS
showInProjects: false
---
Simple weather app made with Node.js, Express, and Heroku. Utilized the OpenWeatherMap API and Google Maps API.
================================================
FILE: content/projects/v1.md
================================================
---
date: '2016-03-01'
title: 'Personal Website V1'
github: 'https://github.com/bchiang7/v1'
external: 'https://bchiang7.github.io/v1/'
tech:
- HTML
- CSS
- JS
- Bootstrap
showInProjects: true
---
My first portfolio website I designed and built in 2014. I learned quite a bit about HTML, CSS, and SEO. Since then, I think my web development and design skills have improved immensely.
================================================
FILE: content/projects/v2.md
================================================
---
date: '2016-12-01'
title: 'Personal Website V2'
github: 'https://github.com/bchiang7/v2'
external: 'https://bchiang7.github.io/v2/'
tech:
- Jekyll
- SCSS
- JS
showInProjects: true
---
Second iteration of my personal website. Designed and developed with a conscious effort to avoid using any superfluous frameworks like Bootstrap.
================================================
FILE: content/projects/v3.md
================================================
---
date: '2017-10-01'
title: 'Personal Website V3'
github: 'https://github.com/bchiang7/bchiang7.github.io'
external: 'https://bchiang7.github.io/v3/'
tech:
- Jekyll
- SCSS
- JS
showInProjects: true
---
Third iteration of my personal website built with Jekyll and hosted on GitHub Pages.
================================================
FILE: gatsby-browser.js
================================================
/**
* Implement Gatsby's Browser APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/browser-apis/
*/
================================================
FILE: gatsby-config.js
================================================
const config = require('./src/config');
module.exports = {
siteMetadata: {
title: 'Brittany Chiang',
description:
'Brittany Chiang is a software engineer who specializes in building (and occasionally designing) exceptional digital experiences.',
siteUrl: 'https://brittanychiang.com', // No trailing slash allowed!
image: '/og.png', // Path to your image you placed in the 'static' folder
twitterUsername: '@bchiang7',
},
plugins: [
`gatsby-plugin-react-helmet`,
`gatsby-plugin-styled-components`,
`gatsby-plugin-image`,
`gatsby-plugin-sharp`,
`gatsby-transformer-sharp`,
`gatsby-plugin-sitemap`,
`gatsby-plugin-robots-txt`,
{
resolve: `gatsby-plugin-manifest`,
options: {
name: 'Brittany Chiang',
short_name: 'Brittany Chiang',
start_url: '/',
background_color: config.colors.darkNavy,
theme_color: config.colors.navy,
display: 'minimal-ui',
icon: 'src/images/logo.png',
},
},
`gatsby-plugin-offline`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images`,
},
},
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'content',
path: `${__dirname}/content/`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `posts`,
path: `${__dirname}/content/posts`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `projects`,
path: `${__dirname}/content/projects`,
},
},
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
{
// https://www.gatsbyjs.org/packages/gatsby-remark-external-links
resolve: 'gatsby-remark-external-links',
options: {
target: '_blank',
rel: 'nofollow noopener noreferrer',
},
},
{
// https://www.gatsbyjs.org/packages/gatsby-remark-images
resolve: 'gatsby-remark-images',
options: {
maxWidth: 700,
linkImagesToOriginal: true,
quality: 90,
tracedSVG: { color: config.colors.green },
},
},
{
// https://www.gatsbyjs.org/packages/gatsby-remark-code-titles/
resolve: 'gatsby-remark-code-titles',
}, // IMPORTANT: this must be ahead of other plugins that use code blocks
{
// https://www.gatsbyjs.org/packages/gatsby-remark-prismjs
resolve: `gatsby-remark-prismjs`,
options: {
// Class prefix for <pre> tags containing syntax highlighting;
// defaults to 'language-' (e.g. <pre class="language-js">).
// 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 <brittany.chiang@gmail.com>",
"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 }) => (
<Side isHome={isHome} orientation="right">
<StyledLinkWrapper>
<a href={`mailto:${email}`}>{email}</a>
</StyledLinkWrapper>
</Side>
);
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 (
<StyledFooter>
<StyledSocialLinks>
<ul>
{socialMedia &&
socialMedia.map(({ name, url }, i) => (
<li key={i}>
<a href={url} aria-label={name}>
<Icon name={name} />
</a>
</li>
))}
</ul>
</StyledSocialLinks>
<StyledCredit tabindex="-1">
<a href="https://github.com/bchiang7/v4">
<div>Designed & Built by Brittany Chiang</div>
{githubInfo.stars && githubInfo.forks && (
<div className="github-stats">
<span>
<Icon name="Star" />
<span>{githubInfo.stars.toLocaleString()}</span>
</span>
<span>
<Icon name="Fork" />
<span>{githubInfo.forks.toLocaleString()}</span>
</span>
</div>
)}
</a>
</StyledCredit>
</StyledFooter>
);
};
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 (
<Helmet title={title} defaultTitle={seo.title} titleTemplate={`%s | ${defaultTitle}`}>
<html lang="en" />
<meta name="description" content={seo.description} />
<meta name="image" content={seo.image} />
<meta property="og:title" content={seo.title} />
<meta property="og:description" content={seo.description} />
<meta property="og:image" content={seo.image} />
<meta property="og:url" content={seo.url} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content={twitterUsername} />
<meta name="twitter:title" content={seo.title} />
<meta name="twitter:description" content={seo.description} />
<meta name="twitter:image" content={seo.image} />
<meta name="google-site-verification" content="DCl7VAf9tcz6eD9gb67NfkNnJ1PKRNcg8qQiwpbx9Lk" />
</Helmet>
);
};
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 = () => (
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 512 512"
xmlSpace="preserve">
<title>Apple App Store</title>
<g>
<g>
<path
d="M407,0H105C47.103,0,0,47.103,0,105v302c0,57.897,47.103,105,105,105h302c57.897,0,105-47.103,105-105V105
C512,47.103,464.897,0,407,0z M482,407c0,41.355-33.645,75-75,75H105c-41.355,0-75-33.645-75-75V105c0-41.355,33.645-75,75-75h302
c41.355,0,75,33.645,75,75V407z"
/>
</g>
</g>
<g>
<g>
<path
d="M305.646,123.531c-1.729-6.45-5.865-11.842-11.648-15.18c-11.936-6.892-27.256-2.789-34.15,9.151L256,124.166
l-3.848-6.665c-6.893-11.937-22.212-16.042-34.15-9.151h-0.001c-11.938,6.893-16.042,22.212-9.15,34.151l18.281,31.664
L159.678,291H110.5c-13.785,0-25,11.215-25,25c0,13.785,11.215,25,25,25h189.86l-28.868-50h-54.079l85.735-148.498
C306.487,136.719,307.375,129.981,305.646,123.531z"
/>
</g>
</g>
<g>
<g>
<path
d="M401.5,291h-49.178l-55.907-96.834l-28.867,50l86.804,150.348c3.339,5.784,8.729,9.921,15.181,11.65
c2.154,0.577,4.339,0.863,6.511,0.863c4.332,0,8.608-1.136,12.461-3.361c11.938-6.893,16.042-22.213,9.149-34.15L381.189,341
H401.5c13.785,0,25-11.215,25-25C426.5,302.215,415.285,291,401.5,291z"
/>
</g>
</g>
<g>
<g>
<path
d="M119.264,361l-4.917,8.516c-6.892,11.938-2.787,27.258,9.151,34.15c3.927,2.267,8.219,3.345,12.458,3.344
c8.646,0,17.067-4.484,21.693-12.495L176.999,361H119.264z"
/>
</g>
</g>
</svg>
);
export default IconAppStore;
================================================
FILE: src/components/icons/bookmark.js
================================================
import React from 'react';
const IconBookmark = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-bookmark">
<title>Bookmark</title>
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
</svg>
);
export default IconBookmark;
================================================
FILE: src/components/icons/codepen.js
================================================
import React from 'react';
const IconCodepen = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-codepen">
<title>CodePen</title>
<polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"></polygon>
<line x1="12" y1="22" x2="12" y2="15.5"></line>
<polyline points="22 8.5 12 15.5 2 8.5"></polyline>
<polyline points="2 15.5 12 8.5 22 15.5"></polyline>
<line x1="12" y1="2" x2="12" y2="8.5"></line>
</svg>
);
export default IconCodepen;
================================================
FILE: src/components/icons/external.js
================================================
import React from 'react';
const IconExternal = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-external-link">
<title>External Link</title>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
);
export default IconExternal;
================================================
FILE: src/components/icons/folder.js
================================================
import React from 'react';
const IconFolder = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-folder">
<title>Folder</title>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
);
export default IconFolder;
================================================
FILE: src/components/icons/fork.js
================================================
import React from 'react';
const IconFork = () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-git-branch">
<title>Git Fork</title>
<line x1="6" y1="3" x2="6" y2="15"></line>
<circle cx="18" cy="6" r="3"></circle>
<circle cx="6" cy="18" r="3"></circle>
<path d="M18 9a9 9 0 0 1-9 9"></path>
</svg>
);
export default IconFork;
================================================
FILE: src/components/icons/github.js
================================================
import React from 'react';
const IconGitHub = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-github">
<title>GitHub</title>
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
);
export default IconGitHub;
================================================
FILE: src/components/icons/hex.js
================================================
import React from 'react';
const IconHex = () => (
<svg id="hex" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 84 96">
<title>Hexagon</title>
<g transform="translate(-8.000000, -2.000000)">
<g transform="translate(11.000000, 5.000000)">
<polygon
stroke="currentColor"
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
points="39 0 0 22 0 67 39 90 78 68 78 23"
fill="currentColor"
/>
</g>
</g>
</svg>
);
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 <IconAppStore />;
case 'Bookmark':
return <IconBookmark />;
case 'Codepen':
return <IconCodepen />;
case 'External':
return <IconExternal />;
case 'Folder':
return <IconFolder />;
case 'Fork':
return <IconFork />;
case 'GitHub':
return <IconGitHub />;
case 'Instagram':
return <IconInstagram />;
case 'Linkedin':
return <IconLinkedin />;
case 'Loader':
return <IconLoader />;
case 'Logo':
return <IconLogo />;
case 'PlayStore':
return <IconPlayStore />;
case 'Star':
return <IconStar />;
case 'Twitter':
return <IconTwitter />;
default:
return <IconExternal />;
}
};
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 = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-instagram">
<title>Instagram</title>
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
);
export default IconInstagram;
================================================
FILE: src/components/icons/linkedin.js
================================================
import React from 'react';
const IconLinkedin = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-linkedin">
<title>LinkedIn</title>
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
<rect x="2" y="9" width="4" height="12"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
);
export default IconLinkedin;
================================================
FILE: src/components/icons/loader.js
================================================
import React from 'react';
const IconLoader = () => (
<svg id="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<title>Loader Logo</title>
<g>
<g id="B" transform="translate(11.000000, 5.000000)">
<path
d="M45.691667,45.15 C48.591667,46.1 50.691667,48.95 50.691667,52.2 C50.691667,57.95 46.691667,61 40.291667,61 L28.541667,61 L28.541667,30.3 L39.291667,30.3 C45.691667,30.3 49.691667,33.15 49.691667,38.65 C49.691667,41.95 47.941667,44.35 45.691667,45.15 Z M33.591667,43.2 L39.241667,43.2 C42.791667,43.2 44.691667,41.85 44.691667,38.95 C44.691667,36.05 42.791667,34.8 39.241667,34.8 L33.591667,34.8 L33.591667,43.2 Z M33.591667,47.5 L33.591667,56.5 L40.191667,56.5 C43.691667,56.5 45.591667,54.75 45.591667,52 C45.591667,49.2 43.691667,47.5 40.191667,47.5 L33.591667,47.5 Z"
fill="currentColor"
/>
</g>
<path
stroke="currentColor"
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
d="M 50, 5
L 11, 27
L 11, 72
L 50, 95
L 89, 73
L 89, 28 z"
/>
</g>
</svg>
);
export default IconLoader;
================================================
FILE: src/components/icons/logo.js
================================================
import React from 'react';
const IconLogo = () => (
<svg id="logo" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 84 96">
<title>Logo</title>
<g transform="translate(-8.000000, -2.000000)">
<g transform="translate(11.000000, 5.000000)">
<polygon
id="Shape"
stroke="currentColor"
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
points="39 0 0 22 0 67 39 90 78 68 78 23"
/>
<path
d="M45.691667,45.15 C48.591667,46.1 50.691667,48.95 50.691667,52.2 C50.691667,57.95 46.691667,61 40.291667,61 L28.541667,61 L28.541667,30.3 L39.291667,30.3 C45.691667,30.3 49.691667,33.15 49.691667,38.65 C49.691667,41.95 47.941667,44.35 45.691667,45.15 Z M33.591667,43.2 L39.241667,43.2 C42.791667,43.2 44.691667,41.85 44.691667,38.95 C44.691667,36.05 42.791667,34.8 39.241667,34.8 L33.591667,34.8 L33.591667,43.2 Z M33.591667,47.5 L33.591667,56.5 L40.191667,56.5 C43.691667,56.5 45.591667,54.75 45.591667,52 C45.591667,49.2 43.691667,47.5 40.191667,47.5 L33.591667,47.5 Z"
fill="currentColor"
/>
</g>
</g>
</svg>
);
export default IconLogo;
================================================
FILE: src/components/icons/playstore.js
================================================
import React from 'react';
const IconPlayStore = () => (
<svg xmlns="http://www.w3.org/2000/svg" role="img" x="0px" y="0px" viewBox="0 0 512.001 512.001">
<title>Google Play Store</title>
<path
d="M464.252,212.09L99.624,8.07C84.247-1.873,64.754-2.691,48.574,5.967C32.183,14.74,22,31.737,22,50.329v411.342
c0,18.592,10.183,35.59,26.573,44.361c16.097,8.617,35.593,7.891,51.052-2.101l364.628-204.022
c16.121-9.02,25.747-25.435,25.747-43.908C490,237.527,480.374,221.111,464.252,212.09z M341.677,181.943l-50.339,50.339
L113.108,54.051L341.677,181.943z M55.544,467.323V44.676L267.621,256L55.544,467.323z M113.108,457.949l178.232-178.231
l50.339,50.339L113.108,457.949z M447.874,270.637l-75.779,42.401l-57.038-57.037l57.037-57.037l75.778,42.4
c7.746,4.335,8.583,11.68,8.583,14.637C456.455,258.958,455.62,266.302,447.874,270.637z"
/>
</svg>
);
export default IconPlayStore;
================================================
FILE: src/components/icons/star.js
================================================
import React from 'react';
const IconStar = () => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-star">
<title>Star</title>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
);
export default IconStar;
================================================
FILE: src/components/icons/twitter.js
================================================
import React from 'react';
const IconTwitter = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-twitter">
<title>Twitter</title>
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
</svg>
);
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 (
<>
<Head />
<div id="root">
<ThemeProvider theme={theme}>
<GlobalStyle />
<a className="skip-to-content" href="#content">
Skip to Content
</a>
{isLoading && isHome ? (
<Loader finishLoading={() => setIsLoading(false)} />
) : (
<StyledContent>
<Nav isHome={isHome} />
<Social isHome={isHome} />
<Email isHome={isHome} />
<div id="content">
{children}
<Footer />
</div>
</StyledContent>
)}
</ThemeProvider>
</div>
</>
);
};
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 (
<StyledLoader className="loader" isMounted={isMounted}>
<Helmet bodyAttributes={{ class: `hidden` }} />
<div className="logo-wrapper">
<IconLoader />
</div>
</StyledLoader>
);
};
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 (
<StyledMenu>
<Helmet>
<body className={menuOpen ? 'blur' : ''} />
</Helmet>
<div ref={wrapperRef}>
<StyledHamburgerButton
onClick={toggleMenu}
menuOpen={menuOpen}
ref={buttonRef}
aria-label="Menu">
<div className="ham-box">
<div className="ham-box-inner" />
</div>
</StyledHamburgerButton>
<StyledSidebar menuOpen={menuOpen} aria-hidden={!menuOpen} tabIndex={menuOpen ? 1 : -1}>
<nav ref={navRef}>
{navLinks && (
<ol>
{navLinks.map(({ url, name }, i) => (
<li key={i}>
<Link to={url} onClick={() => setMenuOpen(false)}>
{name}
</Link>
</li>
))}
</ol>
)}
<a href="/resume.pdf" className="resume-link">
Resume
</a>
</nav>
</StyledSidebar>
</div>
</StyledMenu>
);
};
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 = (
<div className="logo" tabIndex="-1">
{isHome ? (
<a href="/" aria-label="home">
<div className="hex-container">
<IconHex />
</div>
<div className="logo-container">
<IconLogo />
</div>
</a>
) : (
<Link to="/" aria-label="home">
<div className="hex-container">
<IconHex />
</div>
<div className="logo-container">
<IconLogo />
</div>
</Link>
)}
</div>
);
const ResumeLink = (
<a className="resume-button" href="/resume.pdf" target="_blank" rel="noopener noreferrer">
Resume
</a>
);
return (
<StyledHeader scrollDirection={scrollDirection} scrolledToTop={scrolledToTop}>
<StyledNav>
{prefersReducedMotion ? (
<>
{Logo}
<StyledLinks>
<ol>
{navLinks &&
navLinks.map(({ url, name }, i) => (
<li key={i}>
<Link to={url}>{name}</Link>
</li>
))}
</ol>
<div>{ResumeLink}</div>
</StyledLinks>
<Menu />
</>
) : (
<>
<TransitionGroup component={null}>
{isMounted && (
<CSSTransition classNames={fadeClass} timeout={timeout}>
<>{Logo}</>
</CSSTransition>
)}
</TransitionGroup>
<StyledLinks>
<ol>
<TransitionGroup component={null}>
{isMounted &&
navLinks &&
navLinks.map(({ url, name }, i) => (
<CSSTransition key={i} classNames={fadeDownClass} timeout={timeout}>
<li key={i} style={{ transitionDelay: `${isHome ? i * 100 : 0}ms` }}>
<Link to={url}>{name}</Link>
</li>
</CSSTransition>
))}
</TransitionGroup>
</ol>
<TransitionGroup component={null}>
{isMounted && (
<CSSTransition classNames={fadeDownClass} timeout={timeout}>
<div style={{ transitionDelay: `${isHome ? navLinks.length * 100 : 0}ms` }}>
{ResumeLink}
</div>
</CSSTransition>
)}
</TransitionGroup>
</StyledLinks>
<TransitionGroup component={null}>
{isMounted && (
<CSSTransition classNames={fadeClass} timeout={timeout}>
<Menu />
</CSSTransition>
)}
</TransitionGroup>
</>
)}
</StyledNav>
</StyledHeader>
);
};
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 (
<StyledAboutSection id="about" ref={revealContainer}>
<h2 className="numbered-heading">About Me</h2>
<div className="inner">
<StyledText>
<div>
<p>
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!
</p>
<p>
Fast-forward to today, and I’ve had the privilege of working at{' '}
<a href="https://us.mullenlowe.com/">an advertising agency</a>,{' '}
<a href="https://starry.com/">a start-up</a>,{' '}
<a href="https://www.apple.com/">a huge corporation</a>, and{' '}
<a href="https://scout.camd.northeastern.edu/">a student-led design studio</a>. My
main focus these days is building accessible, inclusive products and digital
experiences at <a href="https://upstatement.com/">Upstatement</a> for a variety of
clients.
</p>
<p>
I also recently{' '}
<a href="https://www.newline.co/courses/build-a-spotify-connected-app">
launched a course
</a>{' '}
that covers everything you need to build a web app with the Spotify API using Node
& React.
</p>
<p>Here are a few technologies I’ve been working with recently:</p>
</div>
<ul className="skills-list">
{skills && skills.map((skill, i) => <li key={i}>{skill}</li>)}
</ul>
</StyledText>
<StyledPic>
<div className="wrapper">
<StaticImage
className="img"
src="../../images/me.jpg"
width={500}
quality={95}
formats={['AUTO', 'WEBP', 'AVIF']}
alt="Headshot"
/>
</div>
</StyledPic>
</div>
</StyledAboutSection>
);
};
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 (
<StyledContactSection id="contact" ref={revealContainer}>
<h2 className="numbered-heading overline">What’s Next?</h2>
<h2 className="title">Get In Touch</h2>
<p>
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!
</p>
<a className="email-link" href={`mailto:${email}`}>
Say Hello
</a>
</StyledContactSection>
);
};
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 (
<section id="projects">
<h2 className="numbered-heading" ref={revealTitle}>
Some Things I’ve Built
</h2>
<StyledProjectsGrid>
{featuredProjects &&
featuredProjects.map(({ node }, i) => {
const { frontmatter, html } = node;
const { external, title, tech, github, cover, cta } = frontmatter;
const image = getImage(cover);
return (
<StyledProject key={i} ref={el => (revealProjects.current[i] = el)}>
<div className="project-content">
<div>
<p className="project-overline">Featured Project</p>
<h3 className="project-title">
<a href={external}>{title}</a>
</h3>
<div
className="project-description"
dangerouslySetInnerHTML={{ __html: html }}
/>
{tech.length && (
<ul className="project-tech-list">
{tech.map((tech, i) => (
<li key={i}>{tech}</li>
))}
</ul>
)}
<div className="project-links">
{cta && (
<a href={cta} aria-label="Course Link" className="cta">
Learn More
</a>
)}
{github && (
<a href={github} aria-label="GitHub Link">
<Icon name="GitHub" />
</a>
)}
{external && !cta && (
<a href={external} aria-label="External Link" className="external">
<Icon name="External" />
</a>
)}
</div>
</div>
</div>
<div className="project-image">
<a href={external ? external : github ? github : '#'}>
<GatsbyImage image={image} alt={title} className="img" />
</a>
</div>
</StyledProject>
);
})}
</StyledProjectsGrid>
</section>
);
};
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 = <h1>Hi, my name is</h1>;
const two = <h2 className="big-heading">Brittany Chiang.</h2>;
const three = <h3 className="big-heading">I build things for the web.</h3>;
const four = (
<>
<p>
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{' '}
<a href="https://upstatement.com/" target="_blank" rel="noreferrer">
Upstatement
</a>
.
</p>
</>
);
const five = (
<a
className="email-link"
href="https://www.newline.co/courses/build-a-spotify-connected-app"
target="_blank"
rel="noreferrer">
Check out my course!
</a>
);
const items = [one, two, three, four, five];
return (
<StyledHeroSection>
{prefersReducedMotion ? (
<>
{items.map((item, i) => (
<div key={i}>{item}</div>
))}
</>
) : (
<TransitionGroup component={null}>
{isMounted &&
items.map((item, i) => (
<CSSTransition key={i} classNames="fadeup" timeout={loaderDelay}>
<div style={{ transitionDelay: `${i + 1}00ms` }}>{item}</div>
</CSSTransition>
))}
</TransitionGroup>
)}
</StyledHeroSection>
);
};
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 (
<StyledJobsSection id="jobs" ref={revealContainer}>
<h2 className="numbered-heading">Where I’ve Worked</h2>
<div className="inner">
<StyledTabList role="tablist" aria-label="Job tabs" onKeyDown={e => onKeyDown(e)}>
{jobsData &&
jobsData.map(({ node }, i) => {
const { company } = node.frontmatter;
return (
<StyledTabButton
key={i}
isActive={activeTabId === i}
onClick={() => 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}`}>
<span>{company}</span>
</StyledTabButton>
);
})}
<StyledHighlight activeTabId={activeTabId} />
</StyledTabList>
<StyledTabPanels>
{jobsData &&
jobsData.map(({ node }, i) => {
const { frontmatter, html } = node;
const { title, url, company, range } = frontmatter;
return (
<CSSTransition key={i} in={activeTabId === i} timeout={250} classNames="fade">
<StyledTabPanel
id={`panel-${i}`}
role="tabpanel"
tabIndex={activeTabId === i ? '0' : '-1'}
aria-labelledby={`tab-${i}`}
aria-hidden={activeTabId !== i}
hidden={activeTabId !== i}>
<h3>
<span>{title}</span>
<span className="company">
@
<a href={url} className="inline-link">
{company}
</a>
</span>
</h3>
<p className="range">{range}</p>
<div dangerouslySetInnerHTML={{ __html: html }} />
</StyledTabPanel>
</CSSTransition>
);
})}
</StyledTabPanels>
</div>
</StyledJobsSection>
);
};
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 (
<div className="project-inner">
<header>
<div className="project-top">
<div className="folder">
<Icon name="Folder" />
</div>
<div className="project-links">
{github && (
<a href={github} aria-label="GitHub Link" target="_blank" rel="noreferrer">
<Icon name="GitHub" />
</a>
)}
{external && (
<a
href={external}
aria-label="External Link"
className="external"
target="_blank"
rel="noreferrer">
<Icon name="External" />
</a>
)}
</div>
</div>
<h3 className="project-title">
<a href={external} target="_blank" rel="noreferrer">
{title}
</a>
</h3>
<div className="project-description" dangerouslySetInnerHTML={{ __html: html }} />
</header>
<footer>
{tech && (
<ul className="project-tech-list">
{tech.map((tech, i) => (
<li key={i}>{tech}</li>
))}
</ul>
)}
</footer>
</div>
);
};
return (
<StyledProjectsSection>
<h2 ref={revealTitle}>Other Noteworthy Projects</h2>
<Link className="inline-link archive-link" to="/archive" ref={revealArchiveLink}>
view the archive
</Link>
<ul className="projects-grid">
{prefersReducedMotion ? (
<>
{projectsToShow &&
projectsToShow.map(({ node }, i) => (
<StyledProject key={i}>{projectInner(node)}</StyledProject>
))}
</>
) : (
<TransitionGroup component={null}>
{projectsToShow &&
projectsToShow.map(({ node }, i) => (
<CSSTransition
key={i}
classNames="fadeup"
timeout={i >= GRID_LIMIT ? (i - GRID_LIMIT) * 300 : 300}
exit={false}>
<StyledProject
key={i}
ref={el => (revealProjects.current[i] = el)}
style={{
transitionDelay: `${i >= GRID_LIMIT ? (i - GRID_LIMIT) * 100 : 0}ms`,
}}>
{projectInner(node)}
</StyledProject>
</CSSTransition>
))}
</TransitionGroup>
)}
</ul>
<button className="more-button" onClick={() => setShowMore(!showMore)}>
Show {showMore ? 'Less' : 'More'}
</button>
</StyledProjectsSection>
);
};
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 (
<StyledSideElement orientation={orientation}>
{prefersReducedMotion ? (
<>{children}</>
) : (
<TransitionGroup component={null}>
{isMounted && (
<CSSTransition classNames={isHome ? 'fade' : ''} timeout={isHome ? loaderDelay : 0}>
{children}
</CSSTransition>
)}
</TransitionGroup>
)}
</StyledSideElement>
);
};
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 }) => (
<Side isHome={isHome} orientation="left">
<StyledSocialList>
{socialMedia &&
socialMedia.map(({ url, name }, i) => (
<li key={i}>
<a href={url} aria-label={name} target="_blank" rel="noreferrer">
<Icon name={name} />
</a>
</li>
))}
</StyledSocialList>
</Side>
);
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 = (
<StyledMainContainer className="fillHeight">
<StyledTitle>404</StyledTitle>
<StyledSubtitle>Page Not Found</StyledSubtitle>
<StyledHomeButton to="/">Go Home</StyledHomeButton>
</StyledMainContainer>
);
return (
<Layout location={location}>
<Helmet title="Page Not Found" />
{prefersReducedMotion ? (
<>{content}</>
) : (
<TransitionGroup component={null}>
{isMounted && (
<CSSTransition timeout={500} classNames="fadeup">
{content}
</CSSTransition>
)}
</TransitionGroup>
)}
</Layout>
);
};
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 (
<Layout location={location}>
<Helmet title="Archive" />
<main>
<header ref={revealTitle}>
<h1 className="big-heading">Archive</h1>
<p className="subtitle">A big list of things I’ve worked on</p>
</header>
<StyledTableContainer ref={revealTable}>
<table>
<thead>
<tr>
<th>Year</th>
<th>Title</th>
<th className="hide-on-mobile">Made at</th>
<th className="hide-on-mobile">Built with</th>
<th>Link</th>
</tr>
</thead>
<tbody>
{projects.length > 0 &&
projects.map(({ node }, i) => {
const {
date,
github,
external,
ios,
android,
title,
tech,
company,
} = node.frontmatter;
return (
<tr key={i} ref={el => (revealProjects.current[i] = el)}>
<td className="overline year">{`${new Date(date).getFullYear()}`}</td>
<td className="title">{title}</td>
<td className="company hide-on-mobile">
{company ? <span>{company}</span> : <span>—</span>}
</td>
<td className="tech hide-on-mobile">
{tech?.length > 0 &&
tech.map((item, i) => (
<span key={i}>
{item}
{''}
{i !== tech.length - 1 && <span className="separator">·</span>}
</span>
))}
</td>
<td className="links">
<div>
{external && (
<a href={external} aria-label="External Link">
<Icon name="External" />
</a>
)}
{github && (
<a href={github} aria-label="GitHub Link">
<Icon name="GitHub" />
</a>
)}
{ios && (
<a href={ios} aria-label="Apple App Store Link">
<Icon name="AppStore" />
</a>
)}
{android && (
<a href={android} aria-label="Google Play Store Link">
<Icon name="PlayStore" />
</a>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</StyledTableContainer>
</main>
</Layout>
);
};
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 }) => (
<Layout location={location}>
<StyledMainContainer className="fillHeight">
<Hero />
<About />
<Jobs />
<Featured />
<Projects />
<Contact />
</StyledMainContainer>
</Layout>
);
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,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='48' viewport='0 0 100 100' style='fill:black;font-size:24px;'><text y='50%'>⚡</text></svg>")
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 (
<Layout location={location}>
<Helmet title="Pensieve" />
<StyledMainContainer>
<header>
<h1 className="big-heading">Pensieve</h1>
<p className="subtitle">
<a href="https://www.wizardingworld.com/writing-by-jk-rowling/pensieve">
a collection of memories
</a>
</p>
</header>
<StyledGrid>
{posts.length > 0 &&
posts.map(({ node }, i) => {
const { frontmatter } = node;
const { title, description, slug, date, tags } = frontmatter;
const formattedDate = new Date(date).toLocaleDateString();
return (
<StyledPost key={i}>
<div className="post__inner">
<header>
<div className="post__icon">
<IconBookmark />
</div>
<h5 className="post__title">
<Link to={slug}>{title}</Link>
</h5>
<p className="post__desc">{description}</p>
</header>
<footer>
<span className="post__date">{formattedDate}</span>
<ul className="post__tags">
{tags.map((tag, i) => (
<li key={i}>
<Link to={`/pensieve/tags/${kebabCase(tag)}/`} className="inline-link">
#{tag}
</Link>
</li>
))}
</ul>
</footer>
</div>
</StyledPost>
);
})}
</StyledGrid>
</StyledMainContainer>
</Layout>
);
};
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,
}) => (
<Layout location={location}>
<Helmet title="Tags" />
<StyledTagsContainer>
<span className="breadcrumb">
<span className="arrow">←</span>
<Link to="/pensieve">All memories</Link>
</span>
<h1>Tags</h1>
<ul className="fancy-list">
{group.map(tag => (
<li key={tag.fieldValue}>
<Link to={`/pensieve/tags/${kebabCase(tag.fieldValue)}/`} className="inline-link">
{tag.fieldValue} <span className="count">({tag.totalCount})</span>
</Link>
</li>
))}
</ul>
</StyledTagsContainer>
</Layout>
);
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 <pre>.
*/
.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 (
<Layout location={location}>
<Helmet title={title} />
<StyledPostContainer>
<span className="breadcrumb">
<span className="arrow">←</span>
<Link to="/pensieve">All memories</Link>
</span>
<StyledPostHeader>
<h1 className="medium-heading">{title}</h1>
<p className="subtitle">
<time>
{new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
<span> — </span>
{tags &&
tags.length > 0 &&
tags.map((tag, i) => (
<Link key={i} to={`/pensieve/tags/${kebabCase(tag)}/`} className="tag">
#{tag}
</Link>
))}
</p>
</StyledPostHeader>
<StyledPostContent dangerouslySetInnerHTML={{ __html: html }} />
</StyledPostContainer>
</Layout>
);
};
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 (
<Layout location={location}>
<Helmet title={`Tagged: #${tag}`} />
<StyledTagsContainer>
<span className="breadcrumb">
<span className="arrow">←</span>
<Link to="/pensieve">All memories</Link>
</span>
<h1>
<span>#{tag}</span>
<span>
<Link to="/pensieve/tags">View all tags</Link>
</span>
</h1>
<ul className="fancy-list">
{edges.map(({ node }) => {
const { title, slug, date, tags } = node.frontmatter;
return (
<li key={slug}>
<h2>
<Link to={slug}>{title}</Link>
</h2>
<p className="subtitle">
<time>
{new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
<span> — </span>
{tags &&
tags.length > 0 &&
tags.map((tag, i) => (
<Link key={i} to={`/pensieve/tags/${kebabCase(tag)}/`} className="tag">
#{tag}
</Link>
))}
</p>
</li>
);
})}
</ul>
</StyledTagsContainer>
</Layout>
);
};
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;
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
SYMBOL INDEX (5 symbols across 3 files)
FILE: src/hooks/usePrefersReducedMotion.js
constant QUERY (line 6) | const QUERY = '(prefers-reduced-motion: no-preference)';
function usePrefersReducedMotion (line 15) | function usePrefersReducedMotion() {
FILE: src/hooks/useScrollDirection.js
constant SCROLL_UP (line 1) | const SCROLL_UP = 'up';
constant SCROLL_DOWN (line 2) | const SCROLL_DOWN = 'down';
FILE: src/utils/index.js
constant KEY_CODES (line 9) | const KEY_CODES = {
Condensed preview — 123 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (188K chars).
[
{
"path": ".babelrc",
"chars": 76,
"preview": "{\n \"presets\": [\n \"@babel/preset-react\",\n \"babel-preset-gatsby\"\n ]\n}\n"
},
{
"path": ".editorconfig",
"chars": 134,
"preview": "[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_n"
},
{
"path": ".eslintrc",
"chars": 68,
"preview": "{\n \"root\": true,\n \"extends\": \"@upstatement/eslint-config/react\"\n}\n"
},
{
"path": ".gitignore",
"chars": 123,
"preview": "# Project dependencies\n.cache\nnode_modules\nyarn-error.log\npackage-lock.json\n\n# Build directory\n/public\n.DS_Store\n\n.vscod"
},
{
"path": ".husky/.gitignore",
"chars": 2,
"preview": "_\n"
},
{
"path": ".husky/pre-commit",
"chars": 62,
"preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpm run lint-staged\n"
},
{
"path": ".nvmrc",
"chars": 8,
"preview": "14.16.0\n"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2018 Brittany Chiang\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "README.md",
"chars": 3418,
"preview": "<div align=\"center\">\n <img alt=\"Logo\" src=\"https://raw.githubusercontent.com/bchiang7/v4/main/src/images/logo.png\" widt"
},
{
"path": "content/featured/HalcyonTheme/index.md",
"chars": 626,
"preview": "---\ndate: '1'\ntitle: 'Halcyon Theme'\ncover: './halcyon.png'\ngithub: 'https://github.com/bchiang7/halcyon-site'\nexternal:"
},
{
"path": "content/featured/SpotifyProfile/index.md",
"chars": 502,
"preview": "---\ndate: '2'\ntitle: 'Spotify Profile'\ncover: './demo.png'\ngithub: 'https://github.com/bchiang7/spotify-profile'\nexterna"
},
{
"path": "content/featured/SpotifyProfileV2/index.md",
"chars": 755,
"preview": "---\ndate: '3'\ntitle: 'Build a Spotify Connected App'\ncover: './course-card.png'\nexternal: 'https://www.newline.co/course"
},
{
"path": "content/jobs/Apple/index.md",
"chars": 672,
"preview": "---\ndate: '2017-12-21'\ntitle: 'UI Engineer Co-op'\ncompany: 'Apple'\nlocation: 'Cupertino, CA'\nrange: 'July - December 201"
},
{
"path": "content/jobs/Mullen/index.md",
"chars": 509,
"preview": "---\ndate: '2015-12-21'\ntitle: 'Creative Technologist Co-op'\ncompany: 'MullenLowe'\nlocation: 'Boston, MA'\nrange: 'July - "
},
{
"path": "content/jobs/Scout/index.md",
"chars": 432,
"preview": "---\ndate: '2017-04-01'\ntitle: 'Developer'\ncompany: 'Scout Studio'\nlocation: 'Northeastern University'\nrange: 'Spring 201"
},
{
"path": "content/jobs/Starry/index.md",
"chars": 619,
"preview": "---\ndate: '2016-12-21'\ntitle: 'Software Engineer Co-op'\ncompany: 'Starry'\nlocation: 'Boston, MA'\nrange: 'July - December"
},
{
"path": "content/jobs/Upstatement/index.md",
"chars": 802,
"preview": "---\ndate: '2018-05-14'\ntitle: 'Lead Engineer'\ncompany: 'Upstatement'\nlocation: 'Boston, MA'\nrange: 'May 2018 - Present'\n"
},
{
"path": "content/posts/clickable-cards/index.md",
"chars": 995,
"preview": "---\ntitle: Accessible Clickable Cards\ndescription: Clickable cards with multiple child links\ndate: 2021-04-21\ndraft: fal"
},
{
"path": "content/posts/dark-mode-toggle/index.md",
"chars": 2457,
"preview": "---\ntitle: Dark Mode Toggle\ndescription: Dark mode without the flash of default theme\ndate: 2021-04-21\ndraft: false\nslug"
},
{
"path": "content/posts/docker-compose-error/index.md",
"chars": 1098,
"preview": "---\ntitle: Docker Compose Error\ndescription: docker-compose version discrepancies\ndate: '2019-12-13'\ndraft: false\nslug: "
},
{
"path": "content/posts/markdown-playground/index.md",
"chars": 5628,
"preview": "---\ntitle: Markdown Test File\ndescription: abc234\ndate: 2019-12-07\ndraft: true\nslug: /pensieve/markdown-playground\ntags:"
},
{
"path": "content/posts/wordpress-publish-error/index.md",
"chars": 1289,
"preview": "---\ntitle: WordPress Publishing Error\ndescription: Trying to create a simple post in WordPress\ndate: 2019-12-03\ndraft: f"
},
{
"path": "content/projects/AMFM.md",
"chars": 515,
"preview": "---\ndate: '2017-11-01'\ntitle: 'Apple Music Facebook Messenger Integration'\ngithub: ''\nexternal: 'https://www.theverge.co"
},
{
"path": "content/projects/AlgoliaWordPressMediumPost.md",
"chars": 397,
"preview": "---\ndate: '2020-03-27'\ntitle: 'Integrating Algolia Search with WordPress Multisite'\ngithub: ''\nexternal: 'https://medium"
},
{
"path": "content/projects/AppleMusicEmbedPlayer.md",
"chars": 533,
"preview": "---\ndate: '2017-12-01'\ntitle: 'Apple Music Embeddable Web Player Widget'\ngithub: ''\nexternal: 'https://tools.applemusic."
},
{
"path": "content/projects/Blistabloc.md",
"chars": 360,
"preview": "---\ndate: '2018-05-01'\ntitle: 'blistabloc'\ngithub: ''\nexternal: 'https://blistabloc.com/'\ntech:\n - WordPress\n - Timber"
},
{
"path": "content/projects/CourseSource.md",
"chars": 436,
"preview": "---\ndate: '2016-04-01'\ntitle: 'CourseSource'\ngithub: 'https://github.com/bchiang7/WebDevSpring2016/tree/master/public/pr"
},
{
"path": "content/projects/CrowdDJ.md",
"chars": 446,
"preview": "---\ndate: '2017-03-01'\ntitle: 'Crowd DJ'\ngithub: 'https://github.com/crowddj/crowddj-react'\nexternal: ''\ntech:\n - React"
},
{
"path": "content/projects/Devoted.md",
"chars": 284,
"preview": "---\ndate: '2018-12-01'\ntitle: 'Devoted Health'\ngithub: ''\nexternal: 'https://www.devoted.com/'\ntech:\n - Gatsby\n - Type"
},
{
"path": "content/projects/EverytownIdealState.md",
"chars": 227,
"preview": "---\ndate: '2022-01-20'\ntitle: 'Everytown Gun Law Rankings'\ngithub: ''\nexternal: 'https://everytownresearch.org/rankings/"
},
{
"path": "content/projects/Flagship.md",
"chars": 267,
"preview": "---\ndate: '2018-10-01'\ntitle: 'Flagship Pioneering'\ngithub: ''\nexternal: 'https://www.flagshippioneering.com/'\ntech:\n -"
},
{
"path": "content/projects/Fontipsums.md",
"chars": 317,
"preview": "---\ndate: '2016-01-01'\ntitle: 'Fontipsums'\ngithub: 'https://github.com/bchiang7/fontipsums/'\nexternal: 'http://bchiang7."
},
{
"path": "content/projects/GoogleKeepClone.md",
"chars": 264,
"preview": "---\ndate: '2018-12-29'\ntitle: 'Google Keep Clone'\ngithub: 'https://github.com/bchiang7/google-keep-vue-firebase'\nexterna"
},
{
"path": "content/projects/HBS.md",
"chars": 188,
"preview": "---\ndate: '2022-10-08'\ntitle: 'Harvard Business School Design System'\ngithub: ''\nexternal: ''\ntech:\n - Storybook\n - Re"
},
{
"path": "content/projects/HalcyonTheme.md",
"chars": 634,
"preview": "---\ndate: '2017-12-27'\ntitle: 'Halcyon Theme'\ngithub: 'https://github.com/bchiang7/halcyon-site'\nexternal: 'https://halc"
},
{
"path": "content/projects/HeadlessCMSMediumPost.md",
"chars": 411,
"preview": "---\ndate: '2019-11-12'\ntitle: 'Building a Headless Mobile App CMS From Scratch'\ngithub: ''\nexternal: 'https://medium.com"
},
{
"path": "content/projects/Interventions.md",
"chars": 284,
"preview": "---\ndate: '2017-08-01'\ntitle: 'Interventions'\ngithub: ''\nexternal: 'https://interventions.design/'\ntech:\n - Jekyll\n - "
},
{
"path": "content/projects/JetBlueHumanKinda.md",
"chars": 430,
"preview": "---\ndate: '2015-10-01'\ntitle: 'JetBlue HumanKinda'\ngithub: ''\nexternal: 'https://us.mullenlowe.com/work/humankinda/'\ntec"
},
{
"path": "content/projects/KoalaHealth.md",
"chars": 265,
"preview": "---\ndate: '2021-09-01'\ntitle: 'Koala Health'\ngithub: ''\nexternal: 'https://www.koala.health/'\ntech:\n - Next.js\n - Type"
},
{
"path": "content/projects/LonelyPlanetDBMS.md",
"chars": 353,
"preview": "---\ndate: '2017-06-22'\ntitle: 'Lonely Planet DBMS'\ngithub: 'https://github.com/bchiang7/CS3200-Project'\nexternal: ''\ntec"
},
{
"path": "content/projects/MichelleWu.md",
"chars": 224,
"preview": "---\ndate: '2020-09-15'\ntitle: 'Michelle Wu for Boston Grassroots Toolkit'\ngithub: ''\nexternal: 'https://toolkit.michelle"
},
{
"path": "content/projects/MomsDemandAction.md",
"chars": 380,
"preview": "---\ndate: '2019-11-12'\ntitle: 'Moms Demand Action Mobile App'\ngithub: ''\nexternal: 'https://www.upstatement.com/work/mom"
},
{
"path": "content/projects/MyNEURedesign.md",
"chars": 442,
"preview": "---\ndate: '2017-04-03'\ntitle: 'myNEU Redesign'\ngithub: 'https://github.com/bchiang7/Redesign-myNEU'\nexternal: 'https://b"
},
{
"path": "content/projects/NUWITSite.md",
"chars": 341,
"preview": "---\ndate: '2015-12-20'\ntitle: 'NU Women in Tech'\ngithub: 'https://github.com/nuwit/website'\nexternal: 'https://nuwit.ccs"
},
{
"path": "content/projects/NortheasternCSSH.md",
"chars": 235,
"preview": "---\ndate: '2020-07-16'\ntitle: 'Northeastern CSSH'\ngithub: ''\nexternal: 'https://cssh.northeastern.edu/'\ntech:\n - WordPr"
},
{
"path": "content/projects/OctoProfile.md",
"chars": 400,
"preview": "---\ndate: '2019-07-15'\ntitle: 'OctoProfile'\ngithub: 'https://github.com/bchiang7/octoprofile'\nexternal: 'https://octopro"
},
{
"path": "content/projects/OneCardForAll.md",
"chars": 455,
"preview": "---\ndate: '2015-12-01'\ntitle: 'One Card For All'\ngithub: ''\nexternal: 'https://us.mullenlowe.com/work/one-card-for-all/'"
},
{
"path": "content/projects/PhillySports.md",
"chars": 231,
"preview": "---\ndate: '2021-07-01'\ntitle: 'Philadelphia Inquirer Sports Scoreboards'\ngithub: ''\nexternal: 'https://www.inquirer.com/"
},
{
"path": "content/projects/Pratt.md",
"chars": 209,
"preview": "---\ndate: '2022-08-08'\ntitle: 'Pratt'\ngithub: ''\nexternal: 'https://www.pratt.edu/'\ntech:\n - WordPress\n - Timber\n - W"
},
{
"path": "content/projects/ReactResume.md",
"chars": 353,
"preview": "---\ndate: '2016-08-01'\ntitle: 'React Profile'\ngithub: 'https://github.com/bchiang7/react-profile'\nexternal: 'https://bch"
},
{
"path": "content/projects/Screentime.md",
"chars": 545,
"preview": "---\ndate: '2016-11-01'\ntitle: 'Screentime 2.0'\ngithub: ''\nexternal: 'https://starry.com/blog/product/whats-new-screentim"
},
{
"path": "content/projects/SpotifyProfile.md",
"chars": 486,
"preview": "---\ndate: '2018-12-18'\ntitle: 'Spotify Profile'\ngithub: 'https://github.com/bchiang7/spotify-profile'\nexternal: 'https:/"
},
{
"path": "content/projects/SpotifyTopTracks2017.md",
"chars": 339,
"preview": "---\ndate: '2018-04-20'\ntitle: 'Spotify’s Top Tracks of 2017'\ngithub: 'https://github.com/bchiang7/spotify-top-tracks-201"
},
{
"path": "content/projects/The19th.md",
"chars": 222,
"preview": "---\ndate: '2020-08-02'\ntitle: 'The 19th News'\ngithub: ''\nexternal: 'https://19thnews.org/'\ntech:\n - WordPress\n - Timbe"
},
{
"path": "content/projects/Threadable.md",
"chars": 263,
"preview": "---\ndate: '2022-09-08'\ntitle: 'Threadable'\ngithub: ''\nexternal: 'https://www.threadablebooks.com/'\nios: 'https://apps.ap"
},
{
"path": "content/projects/TimeToHaveMoreFun.md",
"chars": 358,
"preview": "---\ndate: '2020-01-10'\ntitle: 'Time to Have More Fun'\ngithub: 'https://github.com/bchiang7/time-to-have-more-fun'\nextern"
},
{
"path": "content/projects/UpstatementDotCom.md",
"chars": 184,
"preview": "---\ndate: '2019-11-25'\ntitle: 'Upstatement.com'\ngithub: ''\nexternal: 'https://www.upstatement.com/'\ntech:\n - Nuxt\n - V"
},
{
"path": "content/projects/Vanderbilt.md",
"chars": 189,
"preview": "---\ndate: '2021-06-01'\ntitle: 'Vanderbilt Design System'\ngithub: ''\nexternal: 'https://www.vanderbilt.edu/'\ntech:\n - Tw"
},
{
"path": "content/projects/WeatherWidget.md",
"chars": 321,
"preview": "---\ndate: '2016-11-16'\ntitle: 'Weather Widget'\ngithub: 'https://github.com/bchiang7/DemoWebApp'\nexternal: 'http://quiet-"
},
{
"path": "content/projects/v1.md",
"chars": 393,
"preview": "---\ndate: '2016-03-01'\ntitle: 'Personal Website V1'\ngithub: 'https://github.com/bchiang7/v1'\nexternal: 'https://bchiang7"
},
{
"path": "content/projects/v2.md",
"chars": 341,
"preview": "---\ndate: '2016-12-01'\ntitle: 'Personal Website V2'\ngithub: 'https://github.com/bchiang7/v2'\nexternal: 'https://bchiang7"
},
{
"path": "content/projects/v3.md",
"chars": 296,
"preview": "---\ndate: '2017-10-01'\ntitle: 'Personal Website V3'\ngithub: 'https://github.com/bchiang7/bchiang7.github.io'\nexternal: '"
},
{
"path": "gatsby-browser.js",
"chars": 112,
"preview": "/**\n * Implement Gatsby's Browser APIs in this file.\n *\n * See: https://www.gatsbyjs.org/docs/browser-apis/\n */\n"
},
{
"path": "gatsby-config.js",
"chars": 5802,
"preview": "const config = require('./src/config');\n\nmodule.exports = {\n siteMetadata: {\n title: 'Brittany Chiang',\n descript"
},
{
"path": "gatsby-node.js",
"chars": 2734,
"preview": "/**\n * Implement Gatsby's Node APIs in this file.\n *\n * See: https://www.gatsbyjs.org/docs/node-apis/\n */\n\nconst path = "
},
{
"path": "gatsby-ssr.js",
"chars": 180,
"preview": "/**\n * Implement Gatsby's SSR (Server Side Rendering) APIs in this file.\n *\n * See: https://www.gatsbyjs.org/docs/ssr-ap"
},
{
"path": "package.json",
"chars": 2322,
"preview": "{\n \"name\": \"v4\",\n \"description\": \"Personal Website V4\",\n \"version\": \"1.0.0\",\n \"author\": \"Brittany Chiang <brittany.c"
},
{
"path": "prettier.config.js",
"chars": 58,
"preview": "module.exports = require('@upstatement/prettier-config');\n"
},
{
"path": "src/components/email.js",
"chars": 987,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport styled from 'styled-components';\nimport { email } "
},
{
"path": "src/components/footer.js",
"chars": 2847,
"preview": "import React, { useState, useEffect } from 'react';\nimport PropTypes from 'prop-types';\nimport styled from 'styled-compo"
},
{
"path": "src/components/head.js",
"chars": 2122,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Helmet } from 'react-helmet';\nimport { useLocati"
},
{
"path": "src/components/icons/appstore.js",
"chars": 1717,
"preview": "import React from 'react';\n\nconst IconAppStore = () => (\n <svg\n version=\"1.1\"\n xmlns=\"http://www.w3.org/2000/svg\""
},
{
"path": "src/components/icons/bookmark.js",
"chars": 426,
"preview": "import React from 'react';\n\nconst IconBookmark = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 "
},
{
"path": "src/components/icons/codepen.js",
"chars": 657,
"preview": "import React from 'react';\n\nconst IconCodepen = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n role=\"img\"\n "
},
{
"path": "src/components/icons/external.js",
"chars": 557,
"preview": "import React from 'react';\n\nconst IconExternal = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n role=\"img\"\n "
},
{
"path": "src/components/icons/folder.js",
"chars": 459,
"preview": "import React from 'react';\n\nconst IconFolder = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n role=\"img\"\n "
},
{
"path": "src/components/icons/fork.js",
"chars": 484,
"preview": "import React from 'react';\n\nconst IconFork = () => (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentC"
},
{
"path": "src/components/icons/github.js",
"chars": 675,
"preview": "import React from 'react';\n\nconst IconGitHub = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n role=\"img\"\n "
},
{
"path": "src/components/icons/hex.js",
"chars": 557,
"preview": "import React from 'react';\n\nconst IconHex = () => (\n <svg id=\"hex\" xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" viewBo"
},
{
"path": "src/components/icons/icon.js",
"chars": 1179,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport {\n IconAppStore,\n IconBookmark,\n IconCodepen,\n "
},
{
"path": "src/components/icons/index.js",
"chars": 806,
"preview": "export { default as IconAppStore } from './appstore';\nexport { default as IconBookmark } from './bookmark';\nexport { def"
},
{
"path": "src/components/icons/instagram.js",
"chars": 567,
"preview": "import React from 'react';\n\nconst IconInstagram = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n role=\"img\"\n "
},
{
"path": "src/components/icons/linkedin.js",
"chars": 564,
"preview": "import React from 'react';\n\nconst IconLinkedin = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n role=\"img\"\n "
},
{
"path": "src/components/icons/loader.js",
"chars": 1221,
"preview": "import React from 'react';\n\nconst IconLoader = () => (\n <svg id=\"logo\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 "
},
{
"path": "src/components/icons/logo.js",
"chars": 1187,
"preview": "import React from 'react';\n\nconst IconLogo = () => (\n <svg id=\"logo\" xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" view"
},
{
"path": "src/components/icons/playstore.js",
"chars": 920,
"preview": "import React from 'react';\n\nconst IconPlayStore = () => (\n <svg xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" x=\"0px\" y"
},
{
"path": "src/components/icons/star.js",
"chars": 419,
"preview": "import React from 'react';\n\nconst IconStar = () => (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentC"
},
{
"path": "src/components/icons/twitter.js",
"chars": 559,
"preview": "import React from 'react';\n\nconst IconTwitter = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n role=\"img\"\n "
},
{
"path": "src/components/index.js",
"chars": 724,
"preview": "export { default as Head } from './head';\nexport { default as Layout } from './layout';\nexport { default as Loader } fro"
},
{
"path": "src/components/layout.js",
"chars": 2145,
"preview": "import React, { useState, useEffect } from 'react';\nimport PropTypes from 'prop-types';\nimport styled, { ThemeProvider }"
},
{
"path": "src/components/loader.js",
"chars": 2126,
"preview": "import React, { useState, useEffect } from 'react';\nimport { Helmet } from 'react-helmet';\nimport PropTypes from 'prop-t"
},
{
"path": "src/components/menu.js",
"chars": 6907,
"preview": "import React, { useState, useEffect, useRef } from 'react';\nimport { Helmet } from 'react-helmet';\nimport { Link } from "
},
{
"path": "src/components/nav.js",
"chars": 7285,
"preview": "import React, { useState, useEffect } from 'react';\nimport { Link } from 'gatsby';\nimport PropTypes from 'prop-types';\ni"
},
{
"path": "src/components/sections/about.js",
"chars": 4857,
"preview": "import React, { useEffect, useRef } from 'react';\nimport { StaticImage } from 'gatsby-plugin-image';\nimport styled from "
},
{
"path": "src/components/sections/contact.js",
"chars": 1626,
"preview": "import React, { useEffect, useRef } from 'react';\nimport styled from 'styled-components';\nimport { srConfig, email } fro"
},
{
"path": "src/components/sections/featured.js",
"chars": 9419,
"preview": "import React, { useEffect, useRef } from 'react';\nimport { useStaticQuery, graphql } from 'gatsby';\nimport { GatsbyImage"
},
{
"path": "src/components/sections/hero.js",
"chars": 2813,
"preview": "import React, { useState, useEffect } from 'react';\nimport { CSSTransition, TransitionGroup } from 'react-transition-gro"
},
{
"path": "src/components/sections/jobs.js",
"chars": 7551,
"preview": "import React, { useState, useEffect, useRef } from 'react';\nimport { useStaticQuery, graphql } from 'gatsby';\nimport { C"
},
{
"path": "src/components/sections/projects.js",
"chars": 7504,
"preview": "import React, { useState, useEffect, useRef } from 'react';\nimport { Link, useStaticQuery, graphql } from 'gatsby';\nimpo"
},
{
"path": "src/components/side.js",
"chars": 1746,
"preview": "import React, { useState, useEffect } from 'react';\nimport PropTypes from 'prop-types';\nimport { CSSTransition, Transiti"
},
{
"path": "src/components/social.js",
"chars": 1230,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport styled from 'styled-components';\nimport { socialMe"
},
{
"path": "src/config.js",
"chars": 1214,
"preview": "module.exports = {\n email: 'brittany.chiang@gmail.com',\n\n socialMedia: [\n {\n name: 'GitHub',\n url: 'https"
},
{
"path": "src/hooks/index.js",
"chars": 218,
"preview": "export { default as useOnClickOutside } from './useOnClickOutside';\nexport { default as usePrefersReducedMotion } from '"
},
{
"path": "src/hooks/useOnClickOutside.js",
"chars": 1094,
"preview": "import { useEffect } from 'react';\n\n// https://usehooks.com/useOnClickOutside/\n\nconst useOnClickOutside = (ref, handler)"
},
{
"path": "src/hooks/usePrefersReducedMotion.js",
"chars": 1026,
"preview": "/**\n * https://www.joshwcomeau.com/snippets/react-hooks/use-prefers-reduced-motion/\n */\n\nimport { useState, useEffect } "
},
{
"path": "src/hooks/useScrollDirection.js",
"chars": 1297,
"preview": "const SCROLL_UP = 'up';\nconst SCROLL_DOWN = 'down';\n\nimport { useState, useEffect } from 'react';\n\nconst useScrollDirect"
},
{
"path": "src/pages/404.js",
"chars": 1964,
"preview": "import React, { useState, useEffect } from 'react';\nimport { Link } from 'gatsby';\nimport { Helmet } from 'react-helmet'"
},
{
"path": "src/pages/archive.js",
"chars": 6802,
"preview": "import React, { useRef, useEffect } from 'react';\nimport { graphql } from 'gatsby';\nimport PropTypes from 'prop-types';\n"
},
{
"path": "src/pages/index.js",
"chars": 619,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport styled from 'styled-components';\nimport { Layout, "
},
{
"path": "src/pages/pensieve/index.js",
"chars": 5485,
"preview": "import React from 'react';\nimport { graphql, Link } from 'gatsby';\nimport kebabCase from 'lodash/kebabCase';\nimport Prop"
},
{
"path": "src/pages/pensieve/tags.js",
"chars": 2078,
"preview": "import React from 'react';\nimport { Link, graphql } from 'gatsby';\nimport kebabCase from 'lodash/kebabCase';\nimport Prop"
},
{
"path": "src/styles/GlobalStyle.js",
"chars": 8127,
"preview": "import { createGlobalStyle } from 'styled-components';\nimport fonts from './fonts';\nimport variables from './variables';"
},
{
"path": "src/styles/PrismStyles.js",
"chars": 5327,
"preview": "import { css } from 'styled-components';\n\nconst prismColors = {\n bg: `#112340`,\n lineHighlight: `#1d2d50`,\n blue: `#5"
},
{
"path": "src/styles/TransitionStyles.js",
"chars": 1086,
"preview": "import { css } from 'styled-components';\n\n// https://reactcommunity.org/react-transition-group/css-transition\n\nconst Tra"
},
{
"path": "src/styles/fonts.js",
"chars": 3284,
"preview": "import { css } from 'styled-components';\n\nimport CalibreRegularWoff from '@fonts/Calibre/Calibre-Regular.woff';\nimport C"
},
{
"path": "src/styles/index.js",
"chars": 146,
"preview": "export { default as theme } from './theme';\nexport { default as GlobalStyle } from './GlobalStyle';\nexport { default as "
},
{
"path": "src/styles/mixins.js",
"chars": 3426,
"preview": "import { css } from 'styled-components';\n\nconst button = css`\n color: var(--green);\n background-color: transparent;\n "
},
{
"path": "src/styles/theme.js",
"chars": 403,
"preview": "import mixins from './mixins';\n\nconst theme = {\n bp: {\n mobileS: `max-width: 330px`,\n mobileM: `max-width: 400px`"
},
{
"path": "src/styles/variables.js",
"chars": 1476,
"preview": "import { css } from 'styled-components';\n\nconst variables = css`\n :root {\n --dark-navy: #020c1b;\n --navy: #0a192f"
},
{
"path": "src/templates/post.js",
"chars": 2513,
"preview": "import React from 'react';\nimport { graphql, Link } from 'gatsby';\nimport kebabCase from 'lodash/kebabCase';\nimport Prop"
},
{
"path": "src/templates/tag.js",
"chars": 3440,
"preview": "import React from 'react';\nimport { Link, graphql } from 'gatsby';\nimport kebabCase from 'lodash/kebabCase';\nimport Prop"
},
{
"path": "src/utils/index.js",
"chars": 578,
"preview": "export const hex2rgba = (hex, alpha = 1) => {\n const [r, g, b] = hex.match(/\\w\\w/g).map(x => parseInt(x, 16));\n return"
},
{
"path": "src/utils/sr.js",
"chars": 149,
"preview": "import ScrollReveal from 'scrollreveal';\n\nconst isSSR = typeof window === 'undefined';\nconst sr = isSSR ? null : ScrollR"
}
]
About this extraction
This page contains the full source code of the bchiang7/v4 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 123 files (168.9 KB), approximately 50.5k tokens, and a symbol index with 5 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.