diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..1167e82 --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "@babel/preset-react", + "babel-preset-gatsby" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b6330b2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..2e115a9 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": "@upstatement/eslint-config/react" +} diff --git a/.gitignore b/.gitignore index 6704566..0a1ceba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,104 +1,11 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) +# Project dependencies .cache +node_modules +yarn-error.log +package-lock.json -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ +# Build directory +/public +.DS_Store -# TernJS port file -.tern-port +.vscode/ diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..57757f4 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm run lint-staged diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2a0dc9a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +14.16.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d17fd03 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +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. diff --git a/README.md b/README.md index 783f7e2..990c3f0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,87 @@ -# portfolio-web -Portfolio website for future project (WIP) +
+ Logo +
+

+ brittanychiang.com - v4 +

+

+ The fourth iteration of brittanychiang.com built with Gatsby and hosted with Netlify +

+

+ Previous iterations: + v1, + v2, + v3 +

+

+ + Netlify Status + +

+ +![demo](https://raw.githubusercontent.com/bchiang7/v4/main/src/images/demo.png) + +## 🚨 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](https://via.placeholder.com/10/0a192f?text=+) `#0a192f` | +| Light Navy | ![#112240](https://via.placeholder.com/10/0a192f?text=+) `#112240` | +| Lightest Navy | ![#233554](https://via.placeholder.com/10/303C55?text=+) `#233554` | +| Slate | ![#8892b0](https://via.placeholder.com/10/8892b0?text=+) `#8892b0` | +| Light Slate | ![#a8b2d1](https://via.placeholder.com/10/a8b2d1?text=+) `#a8b2d1` | +| Lightest Slate | ![#ccd6f6](https://via.placeholder.com/10/ccd6f6?text=+) `#ccd6f6` | +| White | ![#e6f1ff](https://via.placeholder.com/10/e6f1ff?text=+) `#e6f1ff` | +| Green | ![#64ffda](https://via.placeholder.com/10/64ffda?text=+) `#64ffda` | diff --git a/content/featured/HalcyonTheme/demo.png b/content/featured/HalcyonTheme/demo.png new file mode 100644 index 0000000..055fdb9 Binary files /dev/null and b/content/featured/HalcyonTheme/demo.png differ diff --git a/content/featured/HalcyonTheme/halcyon.png b/content/featured/HalcyonTheme/halcyon.png new file mode 100644 index 0000000..a7cfa31 Binary files /dev/null and b/content/featured/HalcyonTheme/halcyon.png differ diff --git a/content/featured/HalcyonTheme/index.md b/content/featured/HalcyonTheme/index.md new file mode 100644 index 0000000..3ceb961 --- /dev/null +++ b/content/featured/HalcyonTheme/index.md @@ -0,0 +1,16 @@ +--- +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 +showInProjects: true +--- + +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). diff --git a/content/featured/OctoProfile/index.md b/content/featured/OctoProfile/index.md new file mode 100644 index 0000000..d66564c --- /dev/null +++ b/content/featured/OctoProfile/index.md @@ -0,0 +1,14 @@ +--- +date: '3' +title: 'OctoProfile' +cover: './octoprofile.png' +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 repository stats with data visualizations of your top languages and stars. Sort through your top repos by number of stars, forks, and size. diff --git a/content/featured/OctoProfile/octoprofile.png b/content/featured/OctoProfile/octoprofile.png new file mode 100644 index 0000000..3d0c4f6 Binary files /dev/null and b/content/featured/OctoProfile/octoprofile.png differ diff --git a/content/featured/SpotifyProfile/demo.png b/content/featured/SpotifyProfile/demo.png new file mode 100644 index 0000000..ddfff03 Binary files /dev/null and b/content/featured/SpotifyProfile/demo.png differ diff --git a/content/featured/SpotifyProfile/index.md b/content/featured/SpotifyProfile/index.md new file mode 100644 index 0000000..4264e14 --- /dev/null +++ b/content/featured/SpotifyProfile/index.md @@ -0,0 +1,15 @@ +--- +date: '2' +title: 'Spotify Profile' +cover: './demo.png' +github: 'https://github.com/bchiang7/spotify-profile' +external: 'https://spotify-profile.herokuapp.com/' +tech: + - React + - Node.js + - Express + - Spotify Web API +showInProjects: true +--- + +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. diff --git a/content/jobs/Apple/index.md b/content/jobs/Apple/index.md new file mode 100644 index 0000000..8cf3943 --- /dev/null +++ b/content/jobs/Apple/index.md @@ -0,0 +1,13 @@ +--- +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 shipped highly interactive web applications for Apple Music using Ember.js +- Built and shipped the Apple Music Extension within Facebook Messenger leveraging third-party and internal APIs +- Architected and implemented the front-end of Apple Music's embeddable web player widget, which lets users log in and listen to full songs in the browser +- Contributed extensively to MusicKit.js, a JavaScript framework that allows developers to add an Apple Music player to their web apps diff --git a/content/jobs/Mullen/index.md b/content/jobs/Mullen/index.md new file mode 100644 index 0000000..9949399 --- /dev/null +++ b/content/jobs/Mullen/index.md @@ -0,0 +1,12 @@ +--- +date: '2015-12-21' +title: 'Creative Technologist Co-op' +company: 'MullenLowe' +location: 'Boston, MA' +range: 'July - December 2015' +url: 'https://us.mullenlowe.com/' +--- + +- Developed and maintained code for in-house and client websites primarily using HTML, CSS, Sass, JavaScript, and jQuery +- Manually tested sites in various browsers and mobile devices to ensure cross-browser compatibility and responsiveness +- Clients included JetBlue, Lovesac, U.S. Cellular, U.S. Department of Defense, and more diff --git a/content/jobs/Scout2017/index.md b/content/jobs/Scout2017/index.md new file mode 100644 index 0000000..58c0f75 --- /dev/null +++ b/content/jobs/Scout2017/index.md @@ -0,0 +1,11 @@ +--- +date: '2017-04-01' +title: 'Studio Developer' +company: 'Scout' +location: 'Northeastern University' +range: 'January - June 2017' +url: 'https://web.northeastern.edu/scout/' +--- + +- Collaborated with a small team of student designers to spearhead a new brand and design system for Scout’s inaugural student-led design conference at Northeastern +- Worked closely with designers and management team to develop, document, and manage the conference’s marketing website using Jekyll, Sass, and JavaScript diff --git a/content/jobs/Scout2018/index.md b/content/jobs/Scout2018/index.md new file mode 100644 index 0000000..30bbf49 --- /dev/null +++ b/content/jobs/Scout2018/index.md @@ -0,0 +1,12 @@ +--- +date: '2018-04-01' +title: 'Studio Developer' +company: 'Scout' +location: 'Northeastern University' +range: 'January - April 2018' +url: 'https://web.northeastern.edu/scout/' +--- + +- Worked with a team of three designers to build a marketing website and e-commerce platform for [blistabloc](https://blistabloc.com), an ambitious startup originating from Northeastern +- Helped solidify a brand direction for blistabloc that spans both packaging and web +- Interfaced with clients on a weekly basis, providing technological expertise diff --git a/content/jobs/Starry/index.md b/content/jobs/Starry/index.md new file mode 100644 index 0000000..b03c1ec --- /dev/null +++ b/content/jobs/Starry/index.md @@ -0,0 +1,12 @@ +--- +date: '2016-12-21' +title: 'Software Engineer Co-op' +company: 'Starry' +location: 'Boston, MA' +range: 'July - December 2016' +url: 'https://starry.com/' +--- + +- Engineered and maintained major features of Starry's customer-facing 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 Starry Station +- Interfaced with user experience designers and other developers to ensure thoughtful and coherent user experiences across Starry’s iOS and Android mobile apps diff --git a/content/jobs/Upstatement/index.md b/content/jobs/Upstatement/index.md new file mode 100644 index 0000000..11a361d --- /dev/null +++ b/content/jobs/Upstatement/index.md @@ -0,0 +1,12 @@ +--- +date: '2018-05-14' +title: 'Engineer' +company: 'Upstatement' +location: 'Boston, MA' +range: 'May 2018 - Present' +url: 'https://www.upstatement.com/' +--- + +- Write modern, performant, maintainable code for a diverse array of client and internal projects +- Work with a variety of different languages, platforms, frameworks, and content management systems such as JavaScript, TypeScript, Gatsby, React, Craft, WordPress, Prismic, and Netlify +- Communicate with multi-disciplinary teams of engineers, designers, producers, and clients on a daily basis diff --git a/content/posts/clickable-cards/index.md b/content/posts/clickable-cards/index.md new file mode 100644 index 0000000..c0fecc6 --- /dev/null +++ b/content/posts/clickable-cards/index.md @@ -0,0 +1,54 @@ +--- +title: Accessible Clickable Cards +description: Clickable cards with multiple child links +date: 2021-04-21 +draft: false +slug: /pensieve/clickable-cards +tags: + - Accessibility + - CSS +--- + +[Codepen Demo](https://codepen.io/bchiang7/pen/xxRBvgd?editors=1100) + +Card layout where the card itself isn't an anchor link, but the whole card is clickable (with a `:before` pseudo element on the main ``). Links inside of the card are still clickable. + +## CSS + +```css +.grid__item { + &:hover, + &:focus-within { + background-color: #eee; + } + + a { + position: relative; + z-index: 1; + } + + h2 { + a { + position: static; + + &:hover, + &:focus { + color: blue; + } + + &:before { + content: ''; + display: block; + position: absolute; + z-index: 0; + width: 100%; + height: 100%; + top: 0; + left: 0; + transition: background-color 0.1s ease-out; + background-color: transparent; + } + } + } +} +``` diff --git a/content/posts/dark-mode-toggle/index.md b/content/posts/dark-mode-toggle/index.md new file mode 100644 index 0000000..43ccff2 --- /dev/null +++ b/content/posts/dark-mode-toggle/index.md @@ -0,0 +1,99 @@ +--- +title: Dark Mode Toggle +description: Dark mode without the flash of default theme +date: 2021-04-21 +draft: false +slug: /pensieve/dark-mode-toggle +tags: + - Theming + - Dark Mode +--- + +Dark mode toggle without the flash of default theme. Important bits: + +- CSS variables for color theming +- Put `data-theme` attribute on ``, not ``, so we can run the JS before the DOM finishes rendering +- Run local storage check in the `` +- JS for toggle button click handler can come after render + +## HTML + +```html + + + + + + ... + + + +
+ +
+ + + + +``` + +## 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 + +- +- +- +- +- diff --git a/content/posts/docker-compose-error/index.md b/content/posts/docker-compose-error/index.md new file mode 100644 index 0000000..d83a5bb --- /dev/null +++ b/content/posts/docker-compose-error/index.md @@ -0,0 +1,38 @@ +--- +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: +``` diff --git a/content/posts/markdown-playground/image.jpg b/content/posts/markdown-playground/image.jpg new file mode 100644 index 0000000..3932f72 Binary files /dev/null and b/content/posts/markdown-playground/image.jpg differ diff --git a/content/posts/markdown-playground/index.md b/content/posts/markdown-playground/index.md new file mode 100644 index 0000000..48d94b8 --- /dev/null +++ b/content/posts/markdown-playground/index.md @@ -0,0 +1,359 @@ +--- +title: Markdown Test File +description: abc234 +date: 2019-12-07 +draft: true +slug: /pensieve/markdown-playground +tags: + - Testing +--- + +![Image Alt](./image.jpg) + +```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} */ } +
+ + +
+ ); + } +} +``` + +```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 ( +
+ {block} +
+ ) +} + +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 + + + +> 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. diff --git a/content/posts/wordpress-publish-error/console-errors.png b/content/posts/wordpress-publish-error/console-errors.png new file mode 100644 index 0000000..9eefb61 Binary files /dev/null and b/content/posts/wordpress-publish-error/console-errors.png differ diff --git a/content/posts/wordpress-publish-error/draft-fail.png b/content/posts/wordpress-publish-error/draft-fail.png new file mode 100644 index 0000000..086bda8 Binary files /dev/null and b/content/posts/wordpress-publish-error/draft-fail.png differ diff --git a/content/posts/wordpress-publish-error/index.md b/content/posts/wordpress-publish-error/index.md new file mode 100644 index 0000000..47b44a7 --- /dev/null +++ b/content/posts/wordpress-publish-error/index.md @@ -0,0 +1,31 @@ +--- +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: + +![Draft fail](./draft-fail.png) + +Sometimes the error message would be slightly more helpful: `Publishing failed. Error message: The response is not a valid JSON response.` + +![Publish error](./publish-error.png) + +And if I popped open the console, I saw these errors: + +![Console errors](./console-errors.png) + +## 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. diff --git a/content/posts/wordpress-publish-error/publish-error.png b/content/posts/wordpress-publish-error/publish-error.png new file mode 100644 index 0000000..037f697 Binary files /dev/null and b/content/posts/wordpress-publish-error/publish-error.png differ diff --git a/content/projects/AMFM.md b/content/projects/AMFM.md new file mode 100644 index 0000000..c6cdff6 --- /dev/null +++ b/content/projects/AMFM.md @@ -0,0 +1,14 @@ +--- +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). diff --git a/content/projects/AlgoliaWordPressMediumPost.md b/content/projects/AlgoliaWordPressMediumPost.md new file mode 100644 index 0000000..b3baa16 --- /dev/null +++ b/content/projects/AlgoliaWordPressMediumPost.md @@ -0,0 +1,14 @@ +--- +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 diff --git a/content/projects/AppleMusicEmbedPlayer.md b/content/projects/AppleMusicEmbedPlayer.md new file mode 100644 index 0000000..8d4ecbb --- /dev/null +++ b/content/projects/AppleMusicEmbedPlayer.md @@ -0,0 +1,14 @@ +--- +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/). diff --git a/content/projects/Blistabloc.md b/content/projects/Blistabloc.md new file mode 100644 index 0000000..0eafb79 --- /dev/null +++ b/content/projects/Blistabloc.md @@ -0,0 +1,14 @@ +--- +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. diff --git a/content/projects/CourseSource.md b/content/projects/CourseSource.md new file mode 100644 index 0000000..577011e --- /dev/null +++ b/content/projects/CourseSource.md @@ -0,0 +1,15 @@ +--- +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. diff --git a/content/projects/CrowdDJ.md b/content/projects/CrowdDJ.md new file mode 100644 index 0000000..0f07d41 --- /dev/null +++ b/content/projects/CrowdDJ.md @@ -0,0 +1,14 @@ +--- +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. diff --git a/content/projects/Devoted.md b/content/projects/Devoted.md new file mode 100644 index 0000000..490fd2d --- /dev/null +++ b/content/projects/Devoted.md @@ -0,0 +1,14 @@ +--- +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 diff --git a/content/projects/Flagship.md b/content/projects/Flagship.md new file mode 100644 index 0000000..4315373 --- /dev/null +++ b/content/projects/Flagship.md @@ -0,0 +1,13 @@ +--- +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. diff --git a/content/projects/Fontipsums.md b/content/projects/Fontipsums.md new file mode 100644 index 0000000..fda7bc1 --- /dev/null +++ b/content/projects/Fontipsums.md @@ -0,0 +1,12 @@ +--- +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. diff --git a/content/projects/GoogleKeepClone.md b/content/projects/GoogleKeepClone.md new file mode 100644 index 0000000..36091e3 --- /dev/null +++ b/content/projects/GoogleKeepClone.md @@ -0,0 +1,12 @@ +--- +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. diff --git a/content/projects/HalcyonTheme.md b/content/projects/HalcyonTheme.md new file mode 100644 index 0000000..8c5b506 --- /dev/null +++ b/content/projects/HalcyonTheme.md @@ -0,0 +1,15 @@ +--- +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). diff --git a/content/projects/HeadlessCMSMediumPost.md b/content/projects/HeadlessCMSMediumPost.md new file mode 100644 index 0000000..9d18890 --- /dev/null +++ b/content/projects/HeadlessCMSMediumPost.md @@ -0,0 +1,15 @@ +--- +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 diff --git a/content/projects/Interventions.md b/content/projects/Interventions.md new file mode 100644 index 0000000..8b7e5f8 --- /dev/null +++ b/content/projects/Interventions.md @@ -0,0 +1,14 @@ +--- +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. diff --git a/content/projects/JetBlueHumanKinda.md b/content/projects/JetBlueHumanKinda.md new file mode 100644 index 0000000..8a5778a --- /dev/null +++ b/content/projects/JetBlueHumanKinda.md @@ -0,0 +1,15 @@ +--- +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/). diff --git a/content/projects/LonelyPlanetDBMS.md b/content/projects/LonelyPlanetDBMS.md new file mode 100644 index 0000000..b225094 --- /dev/null +++ b/content/projects/LonelyPlanetDBMS.md @@ -0,0 +1,15 @@ +--- +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. diff --git a/content/projects/MichelleWu.md b/content/projects/MichelleWu.md new file mode 100644 index 0000000..188ff3d --- /dev/null +++ b/content/projects/MichelleWu.md @@ -0,0 +1,11 @@ +--- +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 +--- diff --git a/content/projects/MomsDemandAction.md b/content/projects/MomsDemandAction.md new file mode 100644 index 0000000..207a2a1 --- /dev/null +++ b/content/projects/MomsDemandAction.md @@ -0,0 +1,14 @@ +--- +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 +--- diff --git a/content/projects/MyNEURedesign.md b/content/projects/MyNEURedesign.md new file mode 100644 index 0000000..913188a --- /dev/null +++ b/content/projects/MyNEURedesign.md @@ -0,0 +1,14 @@ +--- +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. diff --git a/content/projects/NUWITSite.md b/content/projects/NUWITSite.md new file mode 100644 index 0000000..5472e8e --- /dev/null +++ b/content/projects/NUWITSite.md @@ -0,0 +1,13 @@ +--- +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. diff --git a/content/projects/NortheasternCSSH.md b/content/projects/NortheasternCSSH.md new file mode 100644 index 0000000..3cb7e72 --- /dev/null +++ b/content/projects/NortheasternCSSH.md @@ -0,0 +1,15 @@ +--- +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 +--- diff --git a/content/projects/OctoProfile.md b/content/projects/OctoProfile.md new file mode 100644 index 0000000..9252f05 --- /dev/null +++ b/content/projects/OctoProfile.md @@ -0,0 +1,13 @@ +--- +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: false +--- + +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. diff --git a/content/projects/OneCardForAll.md b/content/projects/OneCardForAll.md new file mode 100644 index 0000000..864925a --- /dev/null +++ b/content/projects/OneCardForAll.md @@ -0,0 +1,15 @@ +--- +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. diff --git a/content/projects/ReactResume.md b/content/projects/ReactResume.md new file mode 100644 index 0000000..31dff11 --- /dev/null +++ b/content/projects/ReactResume.md @@ -0,0 +1,12 @@ +--- +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. diff --git a/content/projects/Screentime.md b/content/projects/Screentime.md new file mode 100644 index 0000000..510c800 --- /dev/null +++ b/content/projects/Screentime.md @@ -0,0 +1,15 @@ +--- +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. diff --git a/content/projects/SpotifyProfile.md b/content/projects/SpotifyProfile.md new file mode 100644 index 0000000..4c93c2b --- /dev/null +++ b/content/projects/SpotifyProfile.md @@ -0,0 +1,13 @@ +--- +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. diff --git a/content/projects/SpotifyTopTracks2017.md b/content/projects/SpotifyTopTracks2017.md new file mode 100644 index 0000000..562020f --- /dev/null +++ b/content/projects/SpotifyTopTracks2017.md @@ -0,0 +1,13 @@ +--- +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. diff --git a/content/projects/The19th.md b/content/projects/The19th.md new file mode 100644 index 0000000..3c40252 --- /dev/null +++ b/content/projects/The19th.md @@ -0,0 +1,16 @@ +--- +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 +--- diff --git a/content/projects/TimeToHaveMoreFun.md b/content/projects/TimeToHaveMoreFun.md new file mode 100644 index 0000000..c7357a9 --- /dev/null +++ b/content/projects/TimeToHaveMoreFun.md @@ -0,0 +1,14 @@ +--- +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 diff --git a/content/projects/UpstatementDotCom.md b/content/projects/UpstatementDotCom.md new file mode 100644 index 0000000..3cafa7a --- /dev/null +++ b/content/projects/UpstatementDotCom.md @@ -0,0 +1,12 @@ +--- +date: '2019-11-25' +title: 'Upstatement.com' +github: '' +external: 'https://www.upstatement.com/' +tech: + - Nuxt + - Vue + - Prismic +company: 'Upstatement' +showInProjects: false +--- diff --git a/content/projects/WeatherWidget.md b/content/projects/WeatherWidget.md new file mode 100644 index 0000000..6d74ba5 --- /dev/null +++ b/content/projects/WeatherWidget.md @@ -0,0 +1,13 @@ +--- +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. diff --git a/content/projects/images/SpotifyProfile.png b/content/projects/images/SpotifyProfile.png new file mode 100644 index 0000000..3350a30 Binary files /dev/null and b/content/projects/images/SpotifyProfile.png differ diff --git a/content/projects/images/blistabloc.png b/content/projects/images/blistabloc.png new file mode 100644 index 0000000..b4b7abb Binary files /dev/null and b/content/projects/images/blistabloc.png differ diff --git a/content/projects/images/google-keep-clone.png b/content/projects/images/google-keep-clone.png new file mode 100644 index 0000000..80c1ca7 Binary files /dev/null and b/content/projects/images/google-keep-clone.png differ diff --git a/content/projects/images/halcyon-demo.png b/content/projects/images/halcyon-demo.png new file mode 100644 index 0000000..af987f7 Binary files /dev/null and b/content/projects/images/halcyon-demo.png differ diff --git a/content/projects/images/halcyon.png b/content/projects/images/halcyon.png new file mode 100644 index 0000000..a5a68af Binary files /dev/null and b/content/projects/images/halcyon.png differ diff --git a/content/projects/images/v3-og.png b/content/projects/images/v3-og.png new file mode 100644 index 0000000..02cc889 Binary files /dev/null and b/content/projects/images/v3-og.png differ diff --git a/content/projects/v1.md b/content/projects/v1.md new file mode 100644 index 0000000..d4b53f7 --- /dev/null +++ b/content/projects/v1.md @@ -0,0 +1,14 @@ +--- +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. diff --git a/content/projects/v2.md b/content/projects/v2.md new file mode 100644 index 0000000..cf382db --- /dev/null +++ b/content/projects/v2.md @@ -0,0 +1,13 @@ +--- +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. diff --git a/content/projects/v3.md b/content/projects/v3.md new file mode 100644 index 0000000..651e30b --- /dev/null +++ b/content/projects/v3.md @@ -0,0 +1,13 @@ +--- +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. diff --git a/gatsby-browser.js b/gatsby-browser.js new file mode 100644 index 0000000..50a4a2a --- /dev/null +++ b/gatsby-browser.js @@ -0,0 +1,5 @@ +/** + * Implement Gatsby's Browser APIs in this file. + * + * See: https://www.gatsbyjs.org/docs/browser-apis/ + */ diff --git a/gatsby-config.js b/gatsby-config.js new file mode 100644 index 0000000..e924c3a --- /dev/null +++ b/gatsby-config.js @@ -0,0 +1,161 @@ +const config = require('./src/config'); + +module.exports = { + siteMetadata: { + title: 'Ron Friedman', + description: + "Computer Science student at Vanier College with a passion for anything hardware and software", + siteUrl: 'https://ronfriedman.com', // No trailing slash allowed! + image: '/og.png', // Path to your image you placed in the 'static' folder + twitterUsername: '@cryotechnical', + }, + 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`, + 'gatsby-plugin-dark-mode', + 'babel-plugin-styled-components', + { + resolve: `gatsby-plugin-manifest`, + options: { + name: 'RonFriedman', + short_name: 'RonFriedman', + 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
 tags containing syntax highlighting;
+              // defaults to 'language-' (e.g. 
).
+              // If your site loads Prism into the browser at runtime,
+              // (e.g. for use with libraries like react-live),
+              // you may use this to prevent Prism from re-processing syntax.
+              // This is an uncommon use-case though;
+              // If you're unsure, it's best to use the default value.
+              classPrefix: 'language-',
+              // This is used to allow setting a language for inline code
+              // (i.e. single backticks) by creating a separator.
+              // This separator is a string and will do no white-space
+              // stripping.
+              // A suggested value for English speakers is the non-ascii
+              // character '›'.
+              inlineCodeMarker: null,
+              // This lets you set up language aliases.  For example,
+              // setting this to '{ sh: "bash" }' will let you use
+              // the language "sh" which will highlight using the
+              // bash highlighter.
+              aliases: {},
+              // This toggles the display of line numbers globally alongside the code.
+              // To use it, add the following line in gatsby-browser.js
+              // right after importing the prism color scheme:
+              //  require("prismjs/plugins/line-numbers/prism-line-numbers.css")
+              // Defaults to false.
+              // If you wish to only show line numbers on certain code blocks,
+              // leave false and use the {numberLines: true} syntax below
+              showLineNumbers: false,
+              // If setting this to true, the parser won't handle and highlight inline
+              // code used in markdown i.e. single backtick code like `this`.
+              noInlineHighlight: false,
+              // This adds a new language definition to Prism or extend an already
+              // existing language definition. More details on this option can be
+              // found under the header "Add new language definition or extend an
+              // existing language" below.
+              languageExtensions: [
+                {
+                  language: 'superscript',
+                  extend: 'javascript',
+                  definition: {
+                    superscript_types: /(SuperType)/,
+                  },
+                  insertBefore: {
+                    function: {
+                      superscript_keywords: /(superif|superelse)/,
+                    },
+                  },
+                },
+              ],
+              // Customize the prompt used in shell output
+              // Values below are default
+              prompt: {
+                user: 'root',
+                host: 'localhost',
+                global: false,
+              },
+            },
+          },
+        ],
+      },
+    },
+    {
+      resolve: `gatsby-plugin-google-analytics`,
+      options: {
+        trackingId: 'UA-45666519-2',
+      },
+    },
+  ],
+};
diff --git a/gatsby-node.js b/gatsby-node.js
new file mode 100644
index 0000000..89e0eda
--- /dev/null
+++ b/gatsby-node.js
@@ -0,0 +1,107 @@
+/**
+ * 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: "/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'),
+      },
+    },
+  });
+};
diff --git a/gatsby-ssr.js b/gatsby-ssr.js
new file mode 100644
index 0000000..af8846b
--- /dev/null
+++ b/gatsby-ssr.js
@@ -0,0 +1,8 @@
+/**
+ * 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
+ 
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ca5c9fa
--- /dev/null
+++ b/package.json
@@ -0,0 +1,85 @@
+{
+  "name": "v4",
+  "description": "Personal Website V4",
+  "version": "1.0.0",
+  "author": "Brittany Chiang ",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/bchiang7/v4"
+  },
+  "keywords": [
+    "gatsby"
+  ],
+  "license": "MIT",
+  "browserslist": "> 0.25%, not dead",
+  "scripts": {
+    "build": "gatsby build",
+    "develop": "gatsby develop",
+    "format": "prettier --write \"**/*.{js,jsx,json,md}\"",
+    "start": "npm run develop",
+    "serve": "gatsby serve",
+    "clean": "gatsby clean",
+    "prepare": "husky install",
+    "lint-staged": "lint-staged"
+  },
+  "lint-staged": {
+    "*.{js,css,json,md}": [
+      "prettier --write"
+    ],
+    "*.js": [
+      "eslint --fix"
+    ]
+  },
+  "dependencies": {
+    "animejs": "^3.1.0",
+    "babel-plugin-styled-components": "^1.12.0",
+    "gatsby": "^3.13.0",
+    "gatsby-plugin-dark-mode": "^1.1.2",
+    "gatsby-plugin-google-analytics": "^3.4.0",
+    "gatsby-plugin-image": "^1.4.0",
+    "gatsby-plugin-manifest": "^3.13.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.13.0",
+    "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-images-contentful": "^4.10.0",
+    "gatsby-remark-prismjs": "^5.1.0",
+    "gatsby-source-contentful": "^5.13.0",
+    "gatsby-source-filesystem": "^3.4.0",
+    "gatsby-transformer-remark": "^4.1.0",
+    "gatsby-transformer-sharp": "^3.13.0",
+    "gatsby-transformer-sqip": "^3.13.0",
+    "lodash": "^4.17.19",
+    "prismjs": "^1.24.1",
+    "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",
+    "sharp": "^0.29.0",
+    "smooth-scroll": "^16.1.0",
+    "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"
+  }
+}
diff --git a/prettier.config.js b/prettier.config.js
new file mode 100644
index 0000000..992b968
--- /dev/null
+++ b/prettier.config.js
@@ -0,0 +1 @@
+module.exports = require('@upstatement/prettier-config');
diff --git a/src/components/email.js b/src/components/email.js
new file mode 100644
index 0000000..31a9c57
--- /dev/null
+++ b/src/components/email.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import { email } from '@config';
+import { Side } from '@components';
+
+const StyledLinkWrapper = styled.div`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+
+  &:after {
+    content: '';
+    display: block;
+    width: 1px;
+    height: 90px;
+    margin: 0 auto;
+    background-color: var(--light-slate);
+  }
+
+  a {
+    margin: 20px auto;
+    padding: 10px;
+    font-family: var(--font-mono);
+    font-size: var(--fz-xxs);
+    line-height: var(--fz-lg);
+    letter-spacing: 0.1em;
+    writing-mode: vertical-rl;
+
+    &:hover,
+    &:focus {
+      transform: translateY(-3px);
+    }
+  }
+`;
+
+const Email = ({ isHome }) => (
+  
+    
+      {email}
+    
+  
+);
+
+Email.propTypes = {
+  isHome: PropTypes.bool,
+};
+
+export default Email;
diff --git a/src/components/footer.js b/src/components/footer.js
new file mode 100644
index 0000000..109ad22
--- /dev/null
+++ b/src/components/footer.js
@@ -0,0 +1,133 @@
+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 (
+    
+      
+        
    + {socialMedia && + socialMedia.map(({ name, url }, i) => ( +
  • + + + +
  • + ))} +
+
+ + + +
Designed & Built by Brittany Chiang
+ + {githubInfo.stars && githubInfo.forks && ( +
+ + + {githubInfo.stars.toLocaleString()} + + + + {githubInfo.forks.toLocaleString()} + +
+ )} +
+
+
+ ); +}; + +Footer.propTypes = { + githubInfo: PropTypes.object, +}; + +export default Footer; diff --git a/src/components/head.js b/src/components/head.js new file mode 100644 index 0000000..8378bb1 --- /dev/null +++ b/src/components/head.js @@ -0,0 +1,79 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Helmet } from 'react-helmet'; +import { useLocation } from '@reach/router'; +import { useStaticQuery, graphql } from 'gatsby'; + +// https://www.gatsbyjs.com/docs/add-seo-component/ + +const Head = ({ title, description, image }) => { + const { pathname } = useLocation(); + + const { site } = useStaticQuery( + graphql` + query { + site { + siteMetadata { + defaultTitle: title + defaultDescription: description + siteUrl + defaultImage: image + twitterUsername + } + } + } + `, + ); + + const { + defaultTitle, + defaultDescription, + siteUrl, + defaultImage, + twitterUsername, + } = site.siteMetadata; + + const seo = { + title: title || defaultTitle, + description: description || defaultDescription, + image: `${siteUrl}${image || defaultImage}`, + url: `${siteUrl}${pathname}`, + }; + + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default Head; + +Head.propTypes = { + title: PropTypes.string, + description: PropTypes.string, + image: PropTypes.string, +}; + +Head.defaultProps = { + title: null, + description: null, + image: null, +}; diff --git a/src/components/icons/appstore.js b/src/components/icons/appstore.js new file mode 100644 index 0000000..573fe19 --- /dev/null +++ b/src/components/icons/appstore.js @@ -0,0 +1,51 @@ +import React from 'react'; + +const IconAppStore = () => ( + + Apple App Store + + + + + + + + + + + + + + + + + + + + + +); + +export default IconAppStore; diff --git a/src/components/icons/bookmark.js b/src/components/icons/bookmark.js new file mode 100644 index 0000000..d77a285 --- /dev/null +++ b/src/components/icons/bookmark.js @@ -0,0 +1,18 @@ +import React from 'react'; + +const IconBookmark = () => ( + + Bookmark + + +); + +export default IconBookmark; diff --git a/src/components/icons/codepen.js b/src/components/icons/codepen.js new file mode 100644 index 0000000..657fdc4 --- /dev/null +++ b/src/components/icons/codepen.js @@ -0,0 +1,23 @@ +import React from 'react'; + +const IconCodepen = () => ( + + CodePen + + + + + + +); + +export default IconCodepen; diff --git a/src/components/icons/external.js b/src/components/icons/external.js new file mode 100644 index 0000000..b903d56 --- /dev/null +++ b/src/components/icons/external.js @@ -0,0 +1,21 @@ +import React from 'react'; + +const IconExternal = () => ( + + External Link + + + + +); + +export default IconExternal; diff --git a/src/components/icons/folder.js b/src/components/icons/folder.js new file mode 100644 index 0000000..8aaf805 --- /dev/null +++ b/src/components/icons/folder.js @@ -0,0 +1,19 @@ +import React from 'react'; + +const IconFolder = () => ( + + Folder + + +); + +export default IconFolder; diff --git a/src/components/icons/fork.js b/src/components/icons/fork.js new file mode 100644 index 0000000..a568022 --- /dev/null +++ b/src/components/icons/fork.js @@ -0,0 +1,20 @@ +import React from 'react'; + +const IconFork = () => ( + + Git Fork + + + + + +); + +export default IconFork; diff --git a/src/components/icons/github.js b/src/components/icons/github.js new file mode 100644 index 0000000..6be6665 --- /dev/null +++ b/src/components/icons/github.js @@ -0,0 +1,19 @@ +import React from 'react'; + +const IconGitHub = () => ( + + GitHub + + +); + +export default IconGitHub; diff --git a/src/components/icons/icon.js b/src/components/icons/icon.js new file mode 100644 index 0000000..6feb64a --- /dev/null +++ b/src/components/icons/icon.js @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + IconAppStore, + IconBookmark, + IconCodepen, + IconExternal, + IconFolder, + IconFork, + IconGitHub, + IconInstagram, + IconLinkedin, + IconLoader, + IconLogo, + IconPlayStore, + IconStar, + IconTwitter, +} from '@components/icons'; + +const Icon = ({ name }) => { + switch (name) { + case 'AppStore': + return ; + case 'Bookmark': + return ; + case 'Codepen': + return ; + case 'External': + return ; + case 'Folder': + return ; + case 'Fork': + return ; + case 'GitHub': + return ; + case 'Instagram': + return ; + case 'Linkedin': + return ; + case 'Loader': + return ; + case 'Logo': + return ; + case 'PlayStore': + return ; + case 'Star': + return ; + case 'Twitter': + return ; + default: + return ; + } +}; + +Icon.propTypes = { + name: PropTypes.string.isRequired, +}; + +export default Icon; diff --git a/src/components/icons/index.js b/src/components/icons/index.js new file mode 100644 index 0000000..9761fa6 --- /dev/null +++ b/src/components/icons/index.js @@ -0,0 +1,15 @@ +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 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'; diff --git a/src/components/icons/instagram.js b/src/components/icons/instagram.js new file mode 100644 index 0000000..777eaa6 --- /dev/null +++ b/src/components/icons/instagram.js @@ -0,0 +1,21 @@ +import React from 'react'; + +const IconInstagram = () => ( + + Instagram + + + + +); + +export default IconInstagram; diff --git a/src/components/icons/linkedin.js b/src/components/icons/linkedin.js new file mode 100644 index 0000000..ce2c21b --- /dev/null +++ b/src/components/icons/linkedin.js @@ -0,0 +1,21 @@ +import React from 'react'; + +const IconLinkedin = () => ( + + LinkedIn + + + + +); + +export default IconLinkedin; diff --git a/src/components/icons/loader.js b/src/components/icons/loader.js new file mode 100644 index 0000000..3f8b98b --- /dev/null +++ b/src/components/icons/loader.js @@ -0,0 +1,29 @@ +import React from 'react'; + +const IconLoader = () => ( + +); + +export default IconLoader; diff --git a/src/components/icons/logo.js b/src/components/icons/logo.js new file mode 100644 index 0000000..b9c5d0c --- /dev/null +++ b/src/components/icons/logo.js @@ -0,0 +1,17 @@ +import React from 'react'; + +const IconLogo = () => ( + +); + +export default IconLogo; diff --git a/src/components/icons/playstore.js b/src/components/icons/playstore.js new file mode 100644 index 0000000..3d66a18 --- /dev/null +++ b/src/components/icons/playstore.js @@ -0,0 +1,17 @@ +import React from 'react'; + +const IconPlayStore = () => ( + + Google Play Store + + +); + +export default IconPlayStore; diff --git a/src/components/icons/star.js b/src/components/icons/star.js new file mode 100644 index 0000000..6bb90e7 --- /dev/null +++ b/src/components/icons/star.js @@ -0,0 +1,17 @@ +import React from 'react'; + +const IconStar = () => ( + + Star + + +); + +export default IconStar; diff --git a/src/components/icons/twitter.js b/src/components/icons/twitter.js new file mode 100644 index 0000000..623f035 --- /dev/null +++ b/src/components/icons/twitter.js @@ -0,0 +1,19 @@ +import React from 'react'; + +const IconTwitter = () => ( + + Twitter + + +); + +export default IconTwitter; diff --git a/src/components/index.js b/src/components/index.js new file mode 100644 index 0000000..353ce43 --- /dev/null +++ b/src/components/index.js @@ -0,0 +1,15 @@ +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'; diff --git a/src/components/layout.js b/src/components/layout.js new file mode 100644 index 0000000..4bbf3d6 --- /dev/null +++ b/src/components/layout.js @@ -0,0 +1,92 @@ +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'; + +// https://medium.com/@chrisfitkin/how-to-smooth-scroll-links-in-gatsby-3dc445299558 +if (typeof window !== 'undefined') { + // eslint-disable-next-line global-require + require('smooth-scroll')('a[href*="#"]'); +} + +const StyledContent = styled.div` + display: flex; + flex-direction: column; + min-height: 100vh; +`; + +const Layout = ({ children, location }) => { + const isHome = location.pathname === '/'; + const [isLoading, setIsLoading] = useState(isHome); + + // Sets target="_blank" rel="noopener noreferrer" on external links + const handleExternalLinks = () => { + const allLinks = Array.from(document.querySelectorAll('a')); + if (allLinks.length > 0) { + allLinks.forEach(link => { + if (link.host !== window.location.host) { + link.setAttribute('rel', 'noopener noreferrer'); + link.setAttribute('target', '_blank'); + } + }); + } + }; + + useEffect(() => { + if (isLoading) { + return; + } + + if (location.hash) { + const id = location.hash.substring(1); // location.hash without the '#' + setTimeout(() => { + const el = document.getElementById(id); + if (el) { + el.scrollIntoView(); + el.focus(); + } + }, 0); + } + + handleExternalLinks(); + }, [isLoading]); + + return ( + <> + + +
+ + + + + Skip to Content + + + {isLoading && isHome ? ( + setIsLoading(false)} /> + ) : ( + +
+ + ); +}; + +Layout.propTypes = { + children: PropTypes.node.isRequired, + location: PropTypes.object.isRequired, +}; + +export default Layout; diff --git a/src/components/loader.js b/src/components/loader.js new file mode 100644 index 0000000..82560e5 --- /dev/null +++ b/src/components/loader.js @@ -0,0 +1,99 @@ +import React, { useState, useEffect } from 'react'; +import { Helmet } from 'react-helmet'; +import PropTypes from 'prop-types'; +import anime from 'animejs'; +import styled from 'styled-components'; +import { IconLoader } from '@components/icons'; + +const StyledLoader = styled.div` + ${({ theme }) => theme.mixins.flexCenter}; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: 100%; + height: 100%; + background-color: var(--dark-navy); + z-index: 99; + + .logo-wrapper { + width: max-content; + max-width: 100px; + transition: var(--transition); + opacity: ${props => (props.isMounted ? 1 : 0)}; + svg { + display: block; + width: 100%; + height: 100%; + margin: 0 auto; + fill: none; + user-select: none; + #B { + opacity: 0; + } + } + } +`; + +const Loader = ({ finishLoading }) => { + const [isMounted, setIsMounted] = useState(false); + + const animate = () => { + const loader = anime.timeline({ + complete: () => finishLoading(), + }); + + loader + .add({ + targets: '#logo path', + delay: 300, + duration: 1500, + easing: 'easeInOutQuart', + strokeDashoffset: [anime.setDashoffset, 0], + }) + .add({ + targets: '#logo #B', + duration: 700, + easing: 'easeInOutQuart', + opacity: 1, + }) + .add({ + targets: '#logo', + delay: 500, + duration: 300, + easing: 'easeInOutQuart', + opacity: 0, + scale: 0.1, + }) + .add({ + targets: '.loader', + duration: 200, + easing: 'easeInOutQuart', + opacity: 0, + zIndex: -1, + }); + }; + + useEffect(() => { + const timeout = setTimeout(() => setIsMounted(true), 10); + animate(); + return () => clearTimeout(timeout); + }, []); + + return ( + + + +
+ +
+
+ ); +}; + +Loader.propTypes = { + finishLoading: PropTypes.func.isRequired, +}; + +export default Loader; diff --git a/src/components/menu.js b/src/components/menu.js new file mode 100644 index 0000000..0cfe24d --- /dev/null +++ b/src/components/menu.js @@ -0,0 +1,279 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Helmet } from 'react-helmet'; +import { Link } from 'gatsby'; +import styled from 'styled-components'; +import { navLinks } from '@config'; +import { KEY_CODES } from '@utils'; +import { useOnClickOutside } from '@hooks'; + +const StyledMenu = styled.div` + display: none; + + @media (max-width: 768px) { + display: block; + } +`; + +const StyledHamburgerButton = styled.button` + display: none; + + @media (max-width: 768px) { + ${({ theme }) => theme.mixins.flexCenter}; + position: relative; + z-index: 10; + margin-right: -15px; + padding: 15px; + border: 0; + background-color: transparent; + color: inherit; + text-transform: none; + transition-timing-function: linear; + transition-duration: 0.15s; + transition-property: opacity, filter; + } + + .ham-box { + display: inline-block; + position: relative; + width: var(--hamburger-width); + height: 24px; + } + + .ham-box-inner { + position: absolute; + top: 50%; + right: 0; + width: var(--hamburger-width); + height: 2px; + border-radius: var(--border-radius); + background-color: var(--green); + transition-duration: 0.22s; + transition-property: transform; + transition-delay: ${props => (props.menuOpen ? `0.12s` : `0s`)}; + transform: rotate(${props => (props.menuOpen ? `225deg` : `0deg`)}); + transition-timing-function: cubic-bezier( + ${props => (props.menuOpen ? `0.215, 0.61, 0.355, 1` : `0.55, 0.055, 0.675, 0.19`)} + ); + &:before, + &:after { + content: ''; + display: block; + position: absolute; + left: auto; + right: 0; + width: var(--hamburger-width); + height: 2px; + border-radius: 4px; + background-color: var(--green); + transition-timing-function: ease; + transition-duration: 0.15s; + transition-property: transform; + } + &:before { + width: ${props => (props.menuOpen ? `100%` : `120%`)}; + top: ${props => (props.menuOpen ? `0` : `-10px`)}; + opacity: ${props => (props.menuOpen ? 0 : 1)}; + transition: ${({ menuOpen }) => + menuOpen ? 'var(--ham-before-active)' : 'var(--ham-before)'}; + } + &:after { + width: ${props => (props.menuOpen ? `100%` : `80%`)}; + bottom: ${props => (props.menuOpen ? `0` : `-10px`)}; + transform: rotate(${props => (props.menuOpen ? `-90deg` : `0`)}); + transition: ${({ menuOpen }) => (menuOpen ? 'var(--ham-after-active)' : 'var(--ham-after)')}; + } + } +`; + +const StyledSidebar = styled.aside` + display: none; + + @media (max-width: 768px) { + ${({ theme }) => theme.mixins.flexCenter}; + position: fixed; + top: 0; + bottom: 0; + right: 0; + padding: 50px 10px; + width: min(75vw, 400px); + height: 100vh; + outline: 0; + background-color: var(--light-navy); + box-shadow: -10px 0px 30px -15px var(--navy-shadow); + z-index: 9; + transform: translateX(${props => (props.menuOpen ? 0 : 100)}vw); + visibility: ${props => (props.menuOpen ? 'visible' : 'hidden')}; + transition: var(--transition); + } + + nav { + ${({ theme }) => theme.mixins.flexBetween}; + width: 100%; + flex-direction: column; + color: var(--lightest-slate); + font-family: var(--font-mono); + text-align: center; + } + + ol { + padding: 0; + margin: 0; + list-style: none; + width: 100%; + + li { + position: relative; + margin: 0 auto 20px; + counter-increment: item 1; + font-size: clamp(var(--fz-sm), 4vw, var(--fz-lg)); + + @media (max-width: 600px) { + margin: 0 auto 10px; + } + + &:before { + content: '0' counter(item) '.'; + display: block; + margin-bottom: 5px; + color: var(--green); + font-size: var(--fz-sm); + } + } + + a { + ${({ theme }) => theme.mixins.link}; + width: 100%; + padding: 3px 20px 20px; + } + } + + .resume-link { + ${({ theme }) => theme.mixins.bigButton}; + padding: 18px 50px; + margin: 10% auto 0; + width: max-content; + } +`; + +const Menu = () => { + const [menuOpen, setMenuOpen] = useState(false); + + const toggleMenu = () => setMenuOpen(!menuOpen); + + const buttonRef = useRef(null); + const navRef = useRef(null); + + let menuFocusables; + let firstFocusableEl; + let lastFocusableEl; + + const setFocusables = () => { + menuFocusables = [buttonRef.current, ...Array.from(navRef.current.querySelectorAll('a'))]; + firstFocusableEl = menuFocusables[0]; + lastFocusableEl = menuFocusables[menuFocusables.length - 1]; + }; + + const handleBackwardTab = e => { + if (document.activeElement === firstFocusableEl) { + e.preventDefault(); + lastFocusableEl.focus(); + } + }; + + const handleForwardTab = e => { + if (document.activeElement === lastFocusableEl) { + e.preventDefault(); + firstFocusableEl.focus(); + } + }; + + const onKeyDown = e => { + switch (e.key) { + case KEY_CODES.ESCAPE: + case KEY_CODES.ESCAPE_IE11: { + setMenuOpen(false); + break; + } + + case KEY_CODES.TAB: { + if (menuFocusables && menuFocusables.length === 1) { + e.preventDefault(); + break; + } + if (e.shiftKey) { + handleBackwardTab(e); + } else { + handleForwardTab(e); + } + break; + } + + default: { + break; + } + } + }; + + const onResize = e => { + if (e.currentTarget.innerWidth > 768) { + setMenuOpen(false); + } + }; + + useEffect(() => { + document.addEventListener('keydown', onKeyDown); + window.addEventListener('resize', onResize); + + setFocusables(); + + return () => { + document.removeEventListener('keydown', onKeyDown); + window.removeEventListener('resize', onResize); + }; + }, []); + + const wrapperRef = useRef(); + useOnClickOutside(wrapperRef, () => setMenuOpen(false)); + + return ( + + + + + +
+ +
+
+
+ + + + + +
+ + ); +}; + +export default Menu; diff --git a/src/components/nav.js b/src/components/nav.js new file mode 100644 index 0000000..3f19dd6 --- /dev/null +++ b/src/components/nav.js @@ -0,0 +1,259 @@ +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 } 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: #bc85a3; + 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: #bc85a3; + 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; + + &:hover, + &:focus { + svg { + fill: var(--green-tint); + } + } + + svg { + fill: none; + transition: var(--transition); + user-select: none; + } + svg:hover { + background-color: var(--green-tint); + } + } + } +`; + +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 = ( +
+ {isHome ? ( + + + + ) : ( + + + + )} +
+ ); + + const ResumeLink = ( + + Resume + + ); + + return ( + + + {prefersReducedMotion ? ( + <> + {Logo} + + +
    + {navLinks && + navLinks.map(({ url, name }, i) => ( +
  1. + {name} +
  2. + ))} +
+
{ResumeLink}
+
+ + + + ) : ( + <> + + {isMounted && ( + + <>{Logo} + + )} + + + +
    + + {isMounted && + navLinks && + navLinks.map(({ url, name }, i) => ( + +
  1. + {name} +
  2. +
    + ))} +
    +
+ + + {isMounted && ( + +
+ {ResumeLink} +
+
+ )} +
+
+ + + {isMounted && ( + + + + )} + + + )} + + + ); +}; + +Nav.propTypes = { + isHome: PropTypes.bool, +}; + +export default Nav; diff --git a/src/components/sections/about.js b/src/components/sections/about.js new file mode 100644 index 0000000..152eafa --- /dev/null +++ b/src/components/sections/about.js @@ -0,0 +1,180 @@ +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)); + 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 { + background: transparent; + outline: 0; + + &:after { + top: 15px; + left: 15px; + } + + .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: 20px; + left: 20px; + 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+)', 'React', 'Eleventy', 'Vue', 'Node.js', 'WordPress']; + + return ( + +

About Me

+ +
+ +
+

+ Hello! My name is Brittany and I enjoy creating things that live on the internet. My + interest in web development started back in 2012 when I decided to try editing custom + Tumblr themes — turns out hacking together a custom reblog button taught me a lot + about HTML & CSS! +

+ +

+ Fast-forward to today, and I've had the privilege of working at{' '} + an advertising agency,{' '} + a start-up,{' '} + a huge corporation, and{' '} + a student-led design studio. My + main focus these days is building accessible, inclusive products and digital + experiences at Upstatement for a variety of + clients. +

+ +

Here are a few technologies I've been working with recently:

+
+ +
    + {skills && skills.map((skill, i) =>
  • {skill}
  • )} +
+
+ + +
+ +
+
+
+
+ ); +}; + +export default About; diff --git a/src/components/sections/contact.js b/src/components/sections/contact.js new file mode 100644 index 0000000..c4e7725 --- /dev/null +++ b/src/components/sections/contact.js @@ -0,0 +1,74 @@ +import React, { useEffect, useRef } from 'react'; +import styled from 'styled-components'; +import { srConfig, email } from '@config'; +import sr from '@utils/sr'; +import { usePrefersReducedMotion } from '@hooks'; + +const StyledContactSection = styled.section` + max-width: 600px; + margin: 0 auto 100px; + text-align: center; + + @media (max-width: 768px) { + margin: 0 auto 50px; + } + + .overline { + display: block; + margin-bottom: 20px; + color: var(--green); + font-family: var(--font-mono); + font-size: var(--fz-md); + font-weight: 400; + + &:before { + bottom: 0; + font-size: var(--fz-sm); + } + + &:after { + display: none; + } + } + + .title { + font-size: clamp(40px, 5vw, 60px); + } + + .email-link { + ${({ theme }) => theme.mixins.bigButton}; + margin-top: 50px; + } +`; + +const Contact = () => { + const revealContainer = useRef(null); + const prefersReducedMotion = usePrefersReducedMotion(); + + useEffect(() => { + if (prefersReducedMotion) { + return; + } + + sr.reveal(revealContainer.current, srConfig()); + }, []); + + return ( + +

What’s Next?

+ +

Get In Touch

+ +

+ Although I'm not currently looking for any new opportunities, my inbox is always open. + Whether you have a question or just want to say hi, I'll try my best to get back to you! +

+ + + Say Hello + +
+ ); +}; + +export default Contact; diff --git a/src/components/sections/featured.js b/src/components/sections/featured.js new file mode 100644 index 0000000..3f68fdf --- /dev/null +++ b/src/components/sections/featured.js @@ -0,0 +1,401 @@ +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}; + } + } + + .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; + } + } + } + + .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(80%); + } + } + } +`; + +const Featured = () => { + const data = useStaticQuery(graphql` + { + featured: allMarkdownRemark( + filter: { fileAbsolutePath: { regex: "/featured/" } } + sort: { fields: [frontmatter___date], order: DESC } + ) { + edges { + node { + frontmatter { + title + cover { + childImageSharp { + gatsbyImageData(width: 700, placeholder: BLURRED, formats: [AUTO, WEBP, AVIF]) + } + } + tech + github + external + } + html + } + } + } + } + `); + + const featuredProjects = data.featured.edges.filter(({ node }) => node); + const revealTitle = useRef(null); + const revealProjects = useRef([]); + const prefersReducedMotion = usePrefersReducedMotion(); + + useEffect(() => { + if (prefersReducedMotion) { + return; + } + + sr.reveal(revealTitle.current, srConfig()); + revealProjects.current.forEach((ref, i) => sr.reveal(ref, srConfig(i * 100))); + }, []); + + return ( +
+

+ Some Things I’ve Built +

+ + + {featuredProjects && + featuredProjects.map(({ node }, i) => { + const { frontmatter, html } = node; + const { external, title, tech, github, cover } = frontmatter; + const image = getImage(cover); + + return ( + (revealProjects.current[i] = el)}> +
+
+

Featured Project

+ +

+ {title} +

+ +
+ + {tech.length && ( +
    + {tech.map((tech, i) => ( +
  • {tech}
  • + ))} +
+ )} + +
+ {github && ( + + + + )} + {external && ( + + + + )} +
+
+
+ +
+ + + +
+ + ); + })} + +
+ ); +}; + +export default Featured; diff --git a/src/components/sections/hero.js b/src/components/sections/hero.js new file mode 100644 index 0000000..205269f --- /dev/null +++ b/src/components/sections/hero.js @@ -0,0 +1,106 @@ +import React, { useState, useEffect } from 'react'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import styled from 'styled-components'; +import { email } from '@config'; +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; + padding: 0; + + @media (max-width: 480px) and (min-height: 700px) { + padding-bottom: 10vh; + } + + 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: 10px; + color: var(--slate); + line-height: 0.9; + } + + p { + margin: 20px 0 0; + max-width: 500px; + } + + .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 =

Heyo, I'm

; + const two =

Ron Friedman.

; + const three =

I do programming things for people.

; + const four = ( +

+ {/* I'm a Boston-based software engineer who specializes in building (and occasionally designing) + exceptional digital experiences. Currently, I'm an engineer at{' '} + + Upstatement + {' '} + focused on building accessible, human-centered products. */} + Currently a Computer Science student at Vanier College with a passion + for anything hardware and software. Welcome to my portfolio! +

+ ); + const five = ( + + Shoot me an Email! + + ); + + const items = [one, two, three, four, five]; + + return ( + + {prefersReducedMotion ? ( + <> + {items.map((item, i) => ( +
{item}
+ ))} + + ) : ( + + {isMounted && + items.map((item, i) => ( + +
{item}
+
+ ))} +
+ )} +
+ ); +}; + +export default Hero; diff --git a/src/components/sections/jobs.js b/src/components/sections/jobs.js new file mode 100644 index 0000000..c7ae9e0 --- /dev/null +++ b/src/components/sections/jobs.js @@ -0,0 +1,310 @@ +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: "/jobs/" } } + sort: { fields: [frontmatter___date], order: DESC } + ) { + edges { + node { + frontmatter { + title + company + location + range + url + } + html + } + } + } + } + `); + + const jobsData = data.jobs.edges; + + const [activeTabId, setActiveTabId] = useState(0); + const [tabFocus, setTabFocus] = useState(null); + const tabs = useRef([]); + const revealContainer = useRef(null); + const prefersReducedMotion = usePrefersReducedMotion(); + + useEffect(() => { + if (prefersReducedMotion) { + return; + } + + sr.reveal(revealContainer.current, srConfig()); + }, []); + + const focusTab = () => { + if (tabs.current[tabFocus]) { + tabs.current[tabFocus].focus(); + return; + } + // If we're at the end, go to the start + if (tabFocus >= tabs.current.length) { + setTabFocus(0); + } + // If we're at the start, move to the end + if (tabFocus < 0) { + setTabFocus(tabs.current.length - 1); + } + }; + + // Only re-run the effect if tabFocus changes + useEffect(() => focusTab(), [tabFocus]); + + // Focus on tabs when using up & down arrow keys + const onKeyDown = e => { + switch (e.key) { + case KEY_CODES.ARROW_UP: { + e.preventDefault(); + setTabFocus(tabFocus - 1); + break; + } + + case KEY_CODES.ARROW_DOWN: { + e.preventDefault(); + setTabFocus(tabFocus + 1); + break; + } + + default: { + break; + } + } + }; + + return ( + +

Where I’ve Worked

+ +
+ onKeyDown(e)}> + {jobsData && + jobsData.map(({ node }, i) => { + const { company } = node.frontmatter; + return ( + setActiveTabId(i)} + ref={el => (tabs.current[i] = el)} + id={`tab-${i}`} + role="tab" + tabIndex={activeTabId === i ? '0' : '-1'} + aria-selected={activeTabId === i ? true : false} + aria-controls={`panel-${i}`}> + {company} + + ); + })} + + + + + {jobsData && + jobsData.map(({ node }, i) => { + const { frontmatter, html } = node; + const { title, url, company, range } = frontmatter; + + return ( + +