DEV Community

Gavin
Gavin

Posted on

Building a Portfolio/Resume Site with Gatsby, Part 2: Customizing your site and merging your changes

Quick recap. My personal website sucks. I'm updating it using Gatsby and the Novela theme by narative, because it's popular, and I'm basic.

FYI, I'm using a Mac, so there may be some Mac-specific commands.


Alright, part 2, customizing your site. Part 1 was very entry-level, very basic. Part 2 is much more involved. Part 2 is where you make your site your own. Let's not waste any more time. Let's get started.

Update your site metadata

  • In Terminal, cd to your website directory
  • git checkout -b initial_customizations

Your terminal output should be something like the following.

Switched to a new branch 'initial_customizations'
Enter fullscreen mode Exit fullscreen mode
  • Open gatsby-config.js in your code editor (I use Visual Studio Code)
    • Update your site metadata and save. Here's mine.
module.exports = {
  siteMetadata: {
    title: `thtmnisamnstr`,
    name: `thtmnisamnstr`,
    siteUrl: `https://thtmnisamnstr.com/`,
    description: `Gavin Johnson's personal site. Resume, blog posts, tech product marketing stuff, maybe some tech-y stuff too.`,
    hero: {
      heading: `Hi. I'm Gavin. My handle is usually thtmnisamnstr. This is my site.`,
      maxWidth: 652,
    },
    social: [
      {
        name: `twitter`,
        url: `https://twitter.com/gavinjtech`,
      },
      {
        name: `devto`,
        url: `https://dev.to/thtmnisamnstr`,
      },
      {
        name: `github`,
        url: `https://github.com/thtmnisamnstr`,
      },
      {
        name: `linkedin`,
        url: `https://www.linkedin.com/in/gavin-johnson/`,
      },
      {
        name: `instagram`,
        url: `https://www.instagram.com/thtmnisamnstr`,
      },
    ],
  },
  plugins: [
    {
      resolve: "@narative/gatsby-theme-novela",
      options: {
        contentPosts: "content/posts",
        contentAuthors: "content/authors",
        basePath: "/",
        authorsPage: true,
        sources: {
          local: true,
        },
      },
    },
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `Novela by Narative`,
        short_name: `Novela`,
        start_url: `/`,
        background_color: `#fff`,
        theme_color: `#fff`,
        display: `standalone`,
        icon: `src/assets/favicon.png`,
      },
    },
    {
      resolve: `gatsby-plugin-netlify-cms`,
      options: {
      },
    },
  ],
};

Enter fullscreen mode Exit fullscreen mode

I also changed the favicon,././src/assets/favicon.png

Add your author info

  • Add your author info for your author page
    • Create a new file under ./content/authors/
    • Name it authors.yml and add your name, bio, avatar, featured, social parameters, and save. Here's mine.
- name: Gavin Johnson
  bio: |
    Gavin is a programmer-turned-marketer with experience in IT, consulting, and marketing. 
    He has been at New Relic since 2018, focusing his time and efforts on helping New Relic 
    be more open and telling New Relic's open source story. Gavin started his career as a 
    programmer and sys admin prior to attending business school at the University of Southern 
    California. Following b-school, he worked for Deloitte Digital as a Senior Consultant and 
    Manager, designing and delivering custom applications for clients. He then moved to AT&T, 
    working as a Lead Product Marketing Manager for their digital video products. Gavin is an 
    Oregon native but has made Los Angeles his home since 2009. He is a happily married, 
    Brazilian Jiu Jitsu purple belt, film photographer, homebrewer, and former marathoner.
  shortBio: |
    Open Source marketing person at New Relic. Ex-AT&T marketer. Ex-Deloitte consultant. Ex-sys 
    admin. (Sometimes)Ex-developer.
  avatar: ./avatars/gavin-johnson.jpg
  social:
    - url: https://twitter.com/gavinjtech
    - url: https://dev.to/thtmnisamnstr
    - url: https://github.com/thtmnisamnstr
    - url: https://www.linkedin.com/in/gavin-johnson/
    - url: https://www.instagram.com/thtmnisamnstr
  featured: true
Enter fullscreen mode Exit fullscreen mode
  • Create a new folder under ./content/authors/. Name it avatars.
  • Add your avatar image to ./content/authors/avatars/. Name it [your-name].jpg. Here's mine.

Alt Text

  • Update the existing example blog post to use your author info
    • Open ./content/posts/2020-01-01-my-first-post-using-novela-by-narative/index.md
    • Change the author parameter to [your name] and save. Here's the first few lines of mine.
---
title: "My first post using Novela by Narative"
author: Gavin Johnson
date: 2019-04-30
hero: ./images/hero.jpg
excerpt: With the growing community interest in Gatsby, we hope to create more resources that make it easier for anyone to grasp the power of this incredible tool.
---
Enter fullscreen mode Exit fullscreen mode
  • Delete the example author files
    • Delete ./content/authors/authors/ and all files under it

Change the site logo

  • I'm changing the site logo to simple text by shadowing the Logo component.
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/components/Logo/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/components/Logo/Logo.tsx' './src/@narative/gatsby-theme-novela/components/Logo/Logo.tsx'
    • Open ./src/@narative/gatsby-theme-novela/components/Logo/Logo.tsx
    • Update your SVG info and save. Here's mine.
import React from "react";
import styled from "@emotion/styled";

import mediaqueries from "@styles/media";

import { Icon } from '@types';

const Logo: Icon = ({ fill = "white" }) => {
  return (
    <LogoContainer>
      <svg
        width="192"
        height="23"
        viewBox="0 0 192 23"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
        className="Logo__Desktop"
      >
        {/* Gavin 20200719: Changed SVG logo to text */}
        <text x="0" y="11" dx fontFamily="Helvetica Neue" fontWeight="bold" textAnchor="start" fill={fill}>
          thtmnisamnstr
        </text>
        <defs>
          <clipPath id="clip0">
            <rect width="191.156" height="23" fill="white" />
          </clipPath>
        </defs>
      </svg>

      <svg
        width="18"
        height="23"
        viewBox="0 0 18 23"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
        className="Logo__Mobile"
      >
        <text x="0" y="11" dx fontFamily="Helvetica Neue" fontWeight="bold" textAnchor="start" fill={fill}>
          thtmnisamnstr
        </text>
      </svg>
    </LogoContainer>
  );
};

export default Logo;

const LogoContainer = styled.div`
  .Logo__Mobile {
    display: none;
  }

  ${mediaqueries.tablet`
    .Logo__Desktop {
      display: none;
    }

    .Logo__Mobile{
      display: block;
    }
  `}
`;
Enter fullscreen mode Exit fullscreen mode
  • Run gatsby develop to see what your site looks like. Here's mine. Alt Text

I don't love how big the bio text is. I want a short bio. So I'mma add a new key to the Authors YAML, shortBio, and put that in the Featured Author section.

  • Add shortBio to the authors YAML
    • Open /content/authors/authors.yml
    • Add a short bio. Here's my authors file.
- name: Gavin Johnson
  bio: |
    Gavin is a programmer-turned-marketer with experience in IT, consulting, and marketing. 
    He has been at New Relic since 2018, focusing his time and efforts on helping New Relic 
    be more open and telling New Relic's open source story. Gavin started his career as a 
    programmer and sys admin prior to attending business school at the University of Southern 
    California. Following b-school, he worked for Deloitte Digital as a Senior Consultant and 
    Manager, designing and delivering custom applications for clients. He then moved to AT&T, 
    working as a Lead Product Marketing Manager for their digital video products. Gavin is an 
    Oregon native but has made Los Angeles his home since 2009. He is a happily married, 
    Brazilian Jiu Jitsu purple belt, film photographer, homebrewer, and former marathoner.
  shortBio: |
    Open Source marketing person at New Relic. Ex-AT&T marketer. Ex-Deloitte consultant. Ex-sys 
    admin. (Sometimes)Ex-developer.
  avatar: ./avatars/gavin-johnson.jpg
  social:
    - url: https://twitter.com/gavinjtech
    - url: https://dev.to/thtmnisamnstr
    - url: https://github.com/thtmnisamnstr
    - url: https://www.linkedin.com/in/gavin-johnson/
    - url: https://www.instagram.com/thtmnisamnstr
  featured: true
Enter fullscreen mode Exit fullscreen mode

Add a short bio to the Author markdown for use in the featured author component

  • Shadow the Bio component, Bio.tsx
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/components/Bio/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/components/Bio/Bio.tsx' './src/@narative/gatsby-theme-novela/components/Bio/Bio.tsx'
    • Open ./src/@narative/gatsby-theme-novela/components/Bio/Bio.tsx
    • Change author.bio to author.shortBio and save. Here's mine.
import React from 'react';
import { Link } from 'gatsby';
import styled from '@emotion/styled';

import Image from '@components/Image';
import { IAuthor } from '@types';

const Bio: React.FC<IAuthor> = ({ author }) => {
  return (
    <BioContainer>
      <BioAvatar
        as={author.authorsPage ? Link : 'div'}
        to={author.slug}
        data-a11y="false"
        aria-label="Author's bio"
      >
        <BioAvatarInner>
          <RoundedImage src={author.avatar.medium} />
        </BioAvatarInner>
      </BioAvatar>
       {/* Gavin, 20200719: Changed to shortBio */}
      <BioText dangerouslySetInnerHTML={{ __html: author.shortBio }} />
    </BioContainer>
  );
};

export default Bio;

const BioContainer = styled.div`
  display: flex;
  align-items: center;
  position: relative;
  left: -10px;
`;

const BioAvatar = styled.div`
  display: block;
  position: relative;
  height: 40px;
  width: 40px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.25);
  margin-right: 16px;
  margin: 10px 26px 10px 10px;

  &::after {
    content: '';
    position: absolute;
    left: -5px;
    top: -5px;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    border: 1px solid rgba(0, 0, 0, 0.25);
  }

  &[data-a11y='true']:focus::after {
    content: '';
    position: absolute;
    left: -5px;
    top: -5px;
    width: 50px;
    height: 50px;
    border: 2px solid ${p => p.theme.colors.accent};
  }
`;

const RoundedImage = styled(Image)`
  border-radius: 50%;
`;

const BioAvatarInner = styled.div`
  height: 40px;
  width: 40px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.25);
  margin-right: 16px;
  overflow: hidden;
`;

const BioText = styled.p`
  max-width: 430px;
  font-size: 14px;
  line-height: 1.45;
  color: ${p => p.theme.colors.grey};

  a {
    color: ${p => p.theme.colors.grey};
    text-decoration: underline;
  }
`;
Enter fullscreen mode Exit fullscreen mode
  • Shadow the IAuthor interface in index.d.ts
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/types/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/types/index.d.ts' './src/@narative/gatsby-theme-novela/types/index.d.ts'
    • Open ./src/@narative/gatsby-theme-novela/types/index.d.ts
    • Add shortBio to your IAuthor interface and save. Here's mine.
import React from "react";

export interface IPaginator {
  pageCount: number;
  index: number;
  pathPrefix: string;
}

interface IGatsbyImage {
  src: string;
  base64?: string;
  srcWebp?: string;
  srcSet?: string;
  srcSetWebp?: string;
  tracedSVG?: string;
}

interface IGatsbyImageFluid extends IGatsbyImage {
  maxHeight: number;
  maxWidth: number;
}

interface IGatsbyImageFixed extends IGatsbyImage {
  height: number;
  width: number;
}

export interface IAuthor {
  authorsPage?: boolean;
  featured?: boolean;
  name: string;
  slug: string;
  bio: string;
  // Gavin, 20200719: Added shortBio
  shortBio: string;
  avatar: {
    image: IGatsbyImageFluid;
    full: IGatsbyImageFluid;
  };
}

export interface IArticle {
  slug: string;
  authors: IAuthor[];
  excerpt: string;
  body: string;
  id: string;
  hero: {
    full: IGatsbyImageFluid;
    preview: IGatsbyImageFluid;
    regular: IGatsbyImageFluid;
    seo: string;
  };
  timeToRead: number;
  date: string;
  secret: boolean;
}

interface IArticleQuery {
  edges: {
    node: IArticle;
  }[];
}

export interface IProgress {
  height: number;
  offset: number;
  title: string;
  mode: string;
  onClose?: () => void;
}

export type Icon = React.FC<{
  fill: string
}>

export type Template = React.FC<{
  pageContext: {
    article: IArticle;
    authors: IAuthor[];
    mailchimp: boolean;
    next: IArticle[];
  };
  location: Location;
}>
Enter fullscreen mode Exit fullscreen mode
  • The correct thing to do next would be to shadow data.query.js and add the shortBio field, but that isn't working for some reason. So let's do it directly in the Novela theme module.
    • Open ./node_modules/@narative/gatsby-theme-novela/src/gatsby/data/data.query.js
    • Add shortBio to your author queries and save. Here's mine.
/* eslint-disable */

// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-transformer-sharp/src/fragments.js

const GatsbyFluid_withWebp = `
  base64
  aspectRatio
  src
  srcSet
  srcWebp
  srcSetWebp
  sizes
`;

module.exports.local = {
  articles: `{
    articles: allArticle(
      sort: { fields: [date, title], order: DESC }
      limit: 1000
    ) {
      edges {
        node {
          id
          slug
          secret
          title
          author
          date(formatString: "MMMM Do, YYYY")
          dateForSEO: date
          timeToRead
          excerpt
          canonical_url
          subscription
          body
          hero {
            full: childImageSharp {
              fluid(maxWidth: 944, quality: 100) {
                ${GatsbyFluid_withWebp}
              }
            }
            regular: childImageSharp {
              fluid(maxWidth: 653, quality: 100) {
                ${GatsbyFluid_withWebp}
              }
            }
            narrow: childImageSharp {
              fluid(maxWidth: 457, quality: 100) {
                ${GatsbyFluid_withWebp}
              }
            }
            seo: childImageSharp {
              fixed(width: 1200, quality: 100) {
                src
              }
            }
          }
        }
      }
    }
  }`,
  authors: `{
    authors: allAuthor {
      edges {
        node {
          authorsPage
          bio
          shortBio
          id
          name
          featured
          social {
            url
          }
          slug
          avatar {
            small: childImageSharp {
              fluid(maxWidth: 50, quality: 100) {
                ${GatsbyFluid_withWebp}
              }
            }
            medium: childImageSharp {
              fluid(maxWidth: 100, quality: 100) {
                ${GatsbyFluid_withWebp}
              }
            }
            large: childImageSharp {
              fluid(maxWidth: 328, quality: 100) {
                ${GatsbyFluid_withWebp}
              }
            }
          }
        }
      }
    }
  }`,
};

module.exports.contentful = {
  articles: `{
    articles: allContentfulArticle(sort: {fields: [date, title], order: DESC}, limit: 1000) {
      edges {
        node {
          body {
            childMdx {
              body
              timeToRead
            }
          }
          excerpt
          title
          slug
          secret
          date(formatString: "MMMM Do, YYYY")
          dateForSEO: date
          hero {
            full: fluid(maxWidth: 944, quality: 100) {
              ${GatsbyFluid_withWebp}
            }
            regular: fluid(maxWidth: 653, quality: 100) {
              ${GatsbyFluid_withWebp}
            }
            narrow: fluid(maxWidth: 457, quality: 100) {
              ${GatsbyFluid_withWebp}
            }
            seo: fixed(width: 1200, quality: 100) {
              src
            }
          }
          id
          author {
            name
          }
        }
      }
    }
  }
  `,
  authors: `{
    authors: allContentfulAuthor {
      edges {
        node {
          avatar {
            small: fluid(maxWidth: 50, quality: 100) {
              ${GatsbyFluid_withWebp}
            }
            medium: fluid(maxWidth: 100, quality: 100) {
              ${GatsbyFluid_withWebp}
            }
            large: fluid(maxWidth: 328, quality: 100) {
              ${GatsbyFluid_withWebp}
            }
          }
          fields {
            authorsPage
            slug
          }
          bio
          shortBio
          id
          name
          social
          featured
        }
      }
    }
  }`,
};
Enter fullscreen mode Exit fullscreen mode
  • Because data.query.js couldn't be shadowed, add the Novela theme under node_modules to your git repo
    • Open ./.gitignore
    • Update to include the Novela theme folder in your repo and save. Here's mine.
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# 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

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
jspm_packages/
# Gavin, 20200719: Including the Novela theme directory under node_modules
node_modules/*
!node_modules/@narative/
node_modules/@narative/*
!node_modules/@narative/gatsby-theme-novela/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# dotenv environment variables file
.env

# gatsby files
.cache/
public

# Mac files
.DS_Store

# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity

.netlify/
Enter fullscreen mode Exit fullscreen mode
  • Run gatsby develop to see what your site looks like. Here's mine.
    Alt Text

  • Click on the author avatar to go to the Author page. Note that it still uses the standard bio field. This is what I was aiming for. Here's mine.
    Alt Text

Add a resume page and navigation

I also want a resume page as well. I could go and create a new content type, update the query to include it, add it to createPages, ..., or I could create a static page, but I just want a resume I can write in markdown and direct-link to. Plus, I'm trying to learn Gatsby anyway, so I'm going to make a resume page by making a secret post that includes a slug.

  • Create a secret post for the resume page
    • mkdir -p './content/posts/SECRET-20200719-Resume/images/'
    • touch './content/posts/SECRET-20200719-Resume/index.md'
    • Open ./content/posts/SECRET-20200719-Resume/index.md
    • Write your resume and save make sure you set secret: true and slug: [first]-[last]-resume. Here's mine.
---
title: Resume
author: Gavin Johnson
date: 2020-07-19
hero:
secret: true
slug: gavin-johnson-resume
---

*Experienced marketer and strategist with a record of approaching business questions through an analytical lens and driving action through that analysis.*
* *Data analysis and data driven decision making*
* *Marketing strategy and content development*
* *Executive presentation and public speaking*
* *Technical background, both as a dev and dev lead*

## Experience

### New Relic *-- Los Angeles, CA (Remote)*
#### Principal Product Marketing Manager, Open Source *-- May 2020-Present*
Led go-to-market strategy and execution for [New Relic Open Source](https://opensource.newrelic.com) and for [New Relic's open source instrumentation](https://blog.newrelic.com/product-news/introducing-open-source-agents-and-projects/).
#### Principal Solutions Marketing Manager, Digital Customer Experience (now Digital Experience Management) *-- November 2018-May 2020*
Built the strategy for New Relic’s Digital Customer (DCX) solution and executed on it. Led creation of DCX Marketing and Sales content. Integrated DCX into New Relic’s campaigns and tactics. Represented New Relic as an external speaker.
* Built New Relic’s DCX solution strategy. Analyzed internal and external content developed for and tactics previously used by other New Relic solutions. Performed competitive analysis. Developed a DCX content and tactics roadmap and calendar.
* Wrote New Relic’s DCX solution positioning and messaging. Described how we wanted the DCX solution perceived by the market, who our targeted personas were, and wrote taglines, copy blocks, and email templates for use by Marketing and Sales.
* Created DCX solution content for Sales. Collaborated with the solution consulting group from Sales to build industry specific DCX content (e.g. pitch decks, solution guide decks, industry primers, discovery questions, objection handling, etc.). Targeted four industries: Media, Retail, FinServ, and Travel.
* Integrated DCX solution into Marketing campaigns and tactics. Worked cross-functionally with demand generation, content and creative, digital marketing, and field marketing. Led creation of and co-wrote content (e.g. articles, solution sheets, white papers, etc.) and landing pages. Developed an event response campaign and cadence.
* Represented New Relic as an external speaker. Presented on the importance of DCX for modern, digital businesses and connecting technical application performance to business outcomes. Presented at New Relic’s FutureStack Los Angeles 2019, WP Engine Summit 2019, and Gartner IT Symposium Xpo 2019.

### AT&T Mobility & Entertainment *-- El Segundo, CA*
#### Lead Product Marketing Manager, Video Lifecycle Strategy *-- June 2017-November 2018*
Built analytical models and business cases for strategic project analysis. Managed enablement projects for product go-to-market, ensuring ancillary systems and processes worked correctly for customers.
* Built the analytical model and business case that got Netflix on AT&T TV’s upcoming streaming box. Built analytical scenario model comparing multiple levels of integration with Netflix. Collaborated across teams in Marketing and Corporate Strategy to align on assumptions, key takeaways, and to build the executive presentation deck.
* Developed product sunsetting strategy for AT&T’s IPTV service. Built a yearly profitability model for the IPTV service. Used existing subscriber and revenue as well as churn and churn acceleration projections to project revenue 5 years forward. Collaborated with engineering to build a 5-year IPTV cost projection model.
* Managed product enablement projects. Insured customer-impacting issues did not occur because of internal, cross-team coordination issues. (e.g. Got a customer authentication feature for AT&T TV’s upcoming streaming box in the product backlog, engaged a 3rd party to provide at-home delivery for AT&T TV’s upcoming streaming box, etc.).
* Built profitability models, business cases, and recommendations for VP and SVP-initiated innovation proposals (e.g. TV service for Cricket Mobile customers, a hyperlocal video service, etc.).

### Deloitte Digital / Deloitte Consulting (*Los Angeles, CA*)
#### Manager, Consulting *-- August 2016-June 2017*
Trusted partner to clients, consulting at Fox for ~2 years. Led teams to create, expand, and maintain custom-built business applications for a diverse set of companies, from startup (Human Longevity Inc.) to Fortune 10 (AT&T).
* Led expansion and maintenance of Fox Theatrical’s global release planning application. Managed two different teams of onshore and offshore consultants and developers through two major expansion projects. Led a cross-functional teams of marketing users, IT leadership, and our consultants through the entire SDLC for each project.
* Served as a trusted client partner. Traveled with my primary client at Fox to Europe and Asia to introduce Global Marketing to major updates of the global release planning application and promote application use in non-Domestic regions. Also used these trips to gather first-hand feedback on the application that informed future features and fixes.
* Recognized with “Extraordinary Moments” award from company leadership for being a company-wide top performer at my level multiple years consecutively.
#### Senior Consultant *-- July 2014-August 2016*
* Managed tech consulting projects for a variety of companies. Led teams of business analysts, consultants, and developers through project design and delivery (e.g. Led a team through development of a digital marketing transformation roadmap for a major grocery wholesaler, led a team through the deployment of a tailored Salesforce CRM application for a biotech startup, etc.).
#### Summer Associate (*June 2013-August 2013*)
* Defined program management processes and quality management plan for a multi-project, customer service enhancement program at T-Mobile. Established PMO reporting templates and cadence.

### National Technical Systems *-- Calabasas, CA*
#### Microsoft Dynamics ERP Administrator *-- June 2009-July 2012*
Led a cross-company ERP implementation serving 600+ business users at 20+ facilities. Managed 2 internal developers and 7 consultants through design, development, and delivery of system customizations.
* Led cross-functional team of business users from finance, sales, marketing, operations, and IT; developers; and consultants to consolidate fragmented CRM, sales, and financial systems into a single ERP system.
* Owned yearly project CapEx budget for the ERP system. Worked with CIO to build yearly CapEx requests. Managed hardware purchases, software licensing, 3rd party add-on licensing, and consulting costs.

### Timber Products Company *-- Springfield, OR*
#### Computer Programmer / Analyst  *-- June 2007-June 2009*
Collaborated with business users from inventory management, sales, logistics, accounting, and finance to design, development, and delivery of enhancements to the company’s Microsoft Dynamics ERP system.

### ViewPlus Technologies *-- Corvallis, OR*
#### Product Development Assistant *-- November 2004-June 2007*
Developed testing protocols for and tested Braille printer drivers and firmware for release. Developed a text-to-Braille application for release.

## Education

### University of Southern California
#### MBA, Masters of Business Administration *-- Graduated May 2014*
* Beta Gamma Sigma, Business Honor Society.
* Program Lead, Marshall Case Competition Program.
  * Trained fellow MBA candidates on how to compete in case competitions, and coached case competition teams. Program achieved 89% participation rate for the class of 2015.
* Asia-Pacific Economic Cooperation (APEC), Business Advisory Council Research Project. Foreign Direct Investment across APEC: Impediments and Opportunities for Improvement.
  * Large, 12-person, MBA research project sponsored by and presented to APEC.
  * Led data analysis team. Sourced and sanitized data, built SQL database, developed data analysis methodology, and led team of 3 MBA candidates though creation of country data analysis slides.
* Director of Technology Consulting, Marshall Consulting & Strategy Club.
* Business of Entertainment Graduate Certificate from the USC School of Cinematic Arts.

### Oregon State University
#### BS, Computer Science *-- Graduated June 2007*
* Cum Laude.
* Upsilon Pi Epsilon, Computer Science Honor Society.
Other
Enter fullscreen mode Exit fullscreen mode

I also want to change the default behavior of posts to not have a hero image if one isn't defined, like in the resume post.

  • Shadow the Article.Hero.tsx section component
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/sections/article/'
    • cp -R './node_modules/@narative/gatsby-theme-novela/src/sections/article/' './src/@narative/gatsby-theme-novela/sections/article/'

Yes, you have to shadow all the damn files in the folder even though you really only want to change one."

  • Open './src/@narative/gatsby-theme-novela/sections/article/Article.Hero.tsx'
  • Update to not render hero image if none is specified and save. Here's mine.
import React from 'react';
import styled from '@emotion/styled';

import Headings from '@components/Headings';
import Image, { ImagePlaceholder } from '@components/Image';

import mediaqueries from '@styles/media';
import { IArticle, IAuthor } from '@types';

import ArticleAuthors from './Article.Authors';

interface ArticleHeroProps {
  article: IArticle;
  authors: IAuthor[];
}

const ArticleHero: React.FC<ArticleHeroProps> = ({ article, authors }) => {
  const hasCoAUthors = authors.length > 1;
  const hasHeroImage =
    article.hero &&
    Object.keys(article.hero.full).length !== 0 &&
    article.hero.full.constructor === Object;

  return (
    <Hero>
      <Header>
        <HeroHeading>{article.title}</HeroHeading>
        <HeroSubtitle hasCoAUthors={hasCoAUthors}>
          <ArticleAuthors authors={authors} />
          <ArticleMeta hasCoAUthors={hasCoAUthors}>
            {article.date} · {article.timeToRead} min read
          </ArticleMeta>
        </HeroSubtitle>
      </Header>
      <HeroImage id="ArticleImage__Hero">
        {hasHeroImage && (
          <Image src={article.hero.full} />
        )}
      </HeroImage>
    </Hero>
  );
};

export default ArticleHero;

const Hero = styled.div`
  ${p => mediaqueries.phablet`
    &::before {
      content: "";
      width: 100%;
      height: 20px;
      background: ${p.theme.colors.primary};
      position: absolute;
      left: 0;
      top: 0;
      transition: ${p.theme.colorModeTransition};
    }

    &::after {
      content: "";
      width: 100%;
      height: 10px;
      background: ${p.theme.colors.background};
      position: absolute;
      left: 0;
      top: 10px;
      border-top-left-radius: 25px;
      border-top-right-radius: 25px;
      transition: ${p.theme.colorModeTransition};
    }
  `}
`;

const ArticleMeta = styled.div<{ hasCoAUthors: boolean }>`
  margin-left: ${p => (p.hasCoAUthors ? '10px' : '0')};

  ${mediaqueries.phablet`
    margin-left: 0;
  `}
`;

/* Gavin 20200722: Reduced bottom margin on the Header because it looked too
  big w/ no hero */
const Header = styled.header`
  position: relative;
  z-index: 10;
  margin:50px auto 120px;
  padding-left: 68px;
  max-width: 749px;

  ${mediaqueries.desktop`
    padding-left: 53px;
    max-width: calc(507px + 53px);
    margin: 50px auto 70px;
  `}

  ${mediaqueries.tablet`
    padding-left: 0;
    margin: 50px auto 70px;
    max-width: 480px;
  `}

  ${mediaqueries.phablet`
    margin: 50px auto 180px;
    padding: 0 40px;
  `}

  @media screen and (max-height: 700px) {
    margin: 50px auto;
  }
`;

const HeroHeading = styled(Headings.h1)`
  font-size: 48px;
  font-family: ${p => p.theme.fonts.serif};
  margin-bottom: 25px;
  font-weight: bold;
  line-height: 1.32;

  ${mediaqueries.tablet`
    margin-bottom: 20px;
    font-size: 36px;
  `}

  ${mediaqueries.phablet`
    font-size: 32px;
  `}
`;

const HeroSubtitle = styled.div<{ hasCoAUthors: boolean }>`
  position: relative;
  display: flex;
  font-size: 18px;
  color: ${p => p.theme.colors.grey};

  ${p => mediaqueries.phablet`
    font-size: 14px;
    flex-direction: column;

    ${p.hasCoAUthors &&
      `
        &::before {
          content: '';
          position: absolute;
          left: -20px;
          right: -20px;
          top: -10px;
          bottom: -10px;
          border: 1px solid ${p.theme.colors.horizontalRule};
          opacity: 0.5;
          border-radius: 5px;
        }
    `}


    strong {
      display: block;
      font-weight: 500;
      margin-bottom: 5px;
    }
  `}
`;

/* Gavin 20200724: Changed from 'height: 220px' to 'max-width: 100%' for phablet */
const HeroImage = styled.div`
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 944px;
  overflow: hidden;
  margin: 0 auto;
  box-shadow: 0 30px 60px -10px rgba(0, 0, 0, 0.2),
    0 18px 36px -18px rgba(0, 0, 0, 0.22);

  ${mediaqueries.tablet`
    max-width: 100%;
  `}

  ${mediaqueries.phablet`
    margin: 0 auto;
    max-width: 100%;

    & > div {
      max-width: 100%;
    }
`}
`;
Enter fullscreen mode Exit fullscreen mode

Now I need to create a link to the resume in the navigation. I'm throwing in a Home L1 as well, because it looks weird without one.

  • Shadow Navigation.Header.tsx component
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/components/Navigation/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/components/Navigation/Navigation.Header.tsx' './src/@narative/gatsby-theme-novela/components/Navigation/Navigation.Header.tsx'
    • Open ./src/@narative/gatsby-theme-novela/components/Navigation/Navigation.Header.tsx
    • Hardcode and style (I know, I'm lazy) the navigation links and save. Here's mine.
import React, { useState, useEffect } from "react";
import styled from "@emotion/styled";
import { Link, navigate, graphql, useStaticQuery } from "gatsby";
import { useColorMode } from "theme-ui";

import Section from "@components/Section";
import Logo from "@components/Logo";

import Icons from "@icons";
import mediaqueries from "@styles/media";
import {
  copyToClipboard,
  getWindowDimensions,
  getBreakpointFromTheme,
} from "@utils";

const siteQuery = graphql`
  {
    sitePlugin(name: { eq: "@narative/gatsby-theme-novela" }) {
      pluginOptions {
        rootPath
        basePath
      }
    }
  }
`;

const DarkModeToggle: React.FC<{}> = () => {
  const [colorMode, setColorMode] = useColorMode();
  const isDark = colorMode === `dark`;

  function toggleColorMode(event) {
    event.preventDefault();
    setColorMode(isDark ? `light` : `dark`);
  }

  return (
    <IconWrapper
      isDark={isDark}
      onClick={toggleColorMode}
      data-a11y="false"
      aria-label={isDark ? "Activate light mode" : "Activate dark mode"}
      title={isDark ? "Activate light mode" : "Activate dark mode"}
    >
      <MoonOrSun isDark={isDark} />
      <MoonMask isDark={isDark} />
    </IconWrapper>
  );
};

const SharePageButton: React.FC<{}> = () => {
  const [hasCopied, setHasCopied] = useState<boolean>(false);
  const [colorMode] = useColorMode();
  const isDark = colorMode === `dark`;
  const fill = isDark ? "#fff" : "#000";

  function copyToClipboardOnClick() {
    if (hasCopied) return;

    copyToClipboard(window.location.href);
    setHasCopied(true);

    setTimeout(() => {
      setHasCopied(false);
    }, 1000);
  }

  return (
    <IconWrapper
      isDark={isDark}
      onClick={copyToClipboardOnClick}
      data-a11y="false"
      aria-label="Copy URL to clipboard"
      title="Copy URL to clipboard"
    >
      <Icons.Link fill={fill} />
      <ToolTip isDark={isDark} hasCopied={hasCopied}>
        Copied
      </ToolTip>
    </IconWrapper>
  );
};

const NavigationHeader: React.FC<{}> = () => {
  const [showBackArrow, setShowBackArrow] = useState<boolean>(false);
  const [previousPath, setPreviousPath] = useState<string>("/");
  const { sitePlugin } = useStaticQuery(siteQuery);

  const [colorMode] = useColorMode();
  const fill = colorMode === "dark" ? "#fff" : "#000";
  const { rootPath, basePath } = sitePlugin.pluginOptions;

  useEffect(() => {
    const { width } = getWindowDimensions();
    const phablet = getBreakpointFromTheme("phablet");

    const prev = localStorage.getItem("previousPath");
    const previousPathWasHomepage =
      prev === (rootPath || basePath) || (prev && prev.includes("/page/"));
    const currentPathIsHomepage =
      location.pathname === (rootPath || basePath) || location.pathname.includes("/page/");

    setShowBackArrow(
      previousPathWasHomepage && !currentPathIsHomepage && width <= phablet,
    );
    setPreviousPath(prev);
  }, []);

  return (
    <Section>
      <NavContainer>
        <LogoLink
          to={rootPath || basePath}
          data-a11y="false"
          title="Navigate back to the homepage"
          aria-label="Navigate back to the homepage"
          back={showBackArrow ? "true" : "false"}
        >
          {showBackArrow && (
            <BackArrowIconContainer>
              <Icons.ChevronLeft fill={fill} />
            </BackArrowIconContainer>
          )}
          <Logo fill={fill} />
          <Hidden>Navigate back to the homepage</Hidden>
          {/* Gavin 20200719: Lazy ass navigation links */}
          <div style={{marginLeft: '25px', marginRight: '25px'}}><Link to="/" style={{fontWeight: 'bolder', color: fill}}>Home</Link></div>
          <div style={{marginLeft: '25px', marginRight: '25px'}}><Link to="/gavin-johnson-resume" style={{fontWeight: 'bolder', color: fill}}>Resume</Link></div>
        </LogoLink>
        <NavControls>
          {showBackArrow ? (
            <button
              onClick={() => navigate(previousPath)}
              title="Navigate back to the homepage"
              aria-label="Navigate back to the homepage"
            >
              <Icons.Ex fill={fill} />
            </button>
          ) : (
            <>
              <SharePageButton />
              <DarkModeToggle />
            </>
          )}
        </NavControls>
      </NavContainer>
    </Section>
  );
};

export default NavigationHeader;

const BackArrowIconContainer = styled.div`
  transition: 0.2s transform var(--ease-out-quad);
  opacity: 0;
  padding-right: 30px;
  animation: fadein 0.3s linear forwards;

  @keyframes fadein {
    to {
      opacity: 1;
    }
  }

  ${mediaqueries.desktop_medium`
    display: none;
  `}
`;

const NavContainer = styled.div`
  position: relative;
  z-index: 100;
  padding-top: 100px;
  display: flex;
  justify-content: space-between;

  ${mediaqueries.desktop_medium`
    padding-top: 50px;
  `};

  @media screen and (max-height: 800px) {
    padding-top: 50px;
  }
`;

const LogoLink = styled(Link)<{ back: string }>`
  position: relative;
  display: flex;
  align-items: center;
  left: ${p => (p.back === "true" ? "-54px" : 0)};

  ${mediaqueries.desktop_medium`
    left: 0
  `}

  &[data-a11y="true"]:focus::after {
    content: "";
    position: absolute;
    left: -10%;
    top: -30%;
    width: 120%;
    height: 160%;
    border: 2px solid ${p => p.theme.colors.accent};
    background: rgba(255, 255, 255, 0.01);
    border-radius: 5px;
  }

  &:hover {
    ${BackArrowIconContainer} {
      transform: translateX(-3px);
    }
  }
`;

const NavControls = styled.div`
  position: relative;
  display: flex;
  align-items: center;

  ${mediaqueries.phablet`
    right: -5px;
  `}
`;

const ToolTip = styled.div<{ isDark: boolean; hasCopied: boolean }>`
  position: absolute;
  padding: 4px 13px;
  background: ${p => (p.isDark ? "#000" : "rgba(0,0,0,0.1)")};
  color: ${p => (p.isDark ? "#fff" : "#000")};
  border-radius: 5px;
  font-size: 14px;
  top: -35px;
  opacity: ${p => (p.hasCopied ? 1 : 0)};
  transform: ${p => (p.hasCopied ? "translateY(-3px)" : "none")};
  transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;

  &::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -6px;
    margin: 0 auto;
    width: 0;
    height: 0;
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 6px solid ${p => (p.isDark ? "#000" : "rgba(0,0,0,0.1)")};
  }
`;

const IconWrapper = styled.button<{ isDark: boolean }>`
  opacity: 0.5;
  position: relative;
  border-radius: 5px;
  width: 40px;
  height: 25px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 0.3s ease;
  margin-left: 30px;

  &:hover {
    opacity: 1;
  }

  &[data-a11y="true"]:focus::after {
    content: "";
    position: absolute;
    left: 0;
    top: -30%;
    width: 100%;
    height: 160%;
    border: 2px solid ${p => p.theme.colors.accent};
    background: rgba(255, 255, 255, 0.01);
    border-radius: 5px;
  }

  ${mediaqueries.tablet`
    display: inline-flex;
    transform: scale(0.708);
    margin-left: 10px;


    &:hover {
      opacity: 0.5;
    }
  `}
`;

// This is based off a codepen! Much appreciated to: https://codepen.io/aaroniker/pen/KGpXZo
const MoonOrSun = styled.div<{ isDark: boolean }>`
  position: relative;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: ${p => (p.isDark ? "4px" : "2px")} solid
    ${p => p.theme.colors.primary};
  background: ${p => p.theme.colors.primary};
  transform: scale(${p => (p.isDark ? 0.55 : 1)});
  transition: all 0.45s ease;
  overflow: ${p => (p.isDark ? "visible" : "hidden")};

  &::before {
    content: "";
    position: absolute;
    right: -9px;
    top: -9px;
    height: 24px;
    width: 24px;
    border: 2px solid ${p => p.theme.colors.primary};
    border-radius: 50%;
    transform: translate(${p => (p.isDark ? "14px, -14px" : "0, 0")});
    opacity: ${p => (p.isDark ? 0 : 1)};
    transition: transform 0.45s ease;
  }

  &::after {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin: -4px 0 0 -4px;
    position: absolute;
    top: 50%;
    left: 50%;
    box-shadow: 0 -23px 0 ${p => p.theme.colors.primary},
      0 23px 0 ${p => p.theme.colors.primary},
      23px 0 0 ${p => p.theme.colors.primary},
      -23px 0 0 ${p => p.theme.colors.primary},
      15px 15px 0 ${p => p.theme.colors.primary},
      -15px 15px 0 ${p => p.theme.colors.primary},
      15px -15px 0 ${p => p.theme.colors.primary},
      -15px -15px 0 ${p => p.theme.colors.primary};
    transform: scale(${p => (p.isDark ? 1 : 0)});
    transition: all 0.35s ease;

    ${p => mediaqueries.tablet`
      transform: scale(${p.isDark ? 0.92 : 0});
    `}
  }
`;

const MoonMask = styled.div<{ isDark: boolean }>`
  position: absolute;
  right: -1px;
  top: -8px;
  height: 24px;
  width: 24px;
  border-radius: 50%;
  border: 0;
  background: ${p => p.theme.colors.background};
  transform: translate(${p => (p.isDark ? "14px, -14px" : "0, 0")});
  opacity: ${p => (p.isDark ? 0 : 1)};
  transition: ${p => p.theme.colorModeTransition}, transform 0.45s ease;
`;

const Hidden = styled.span`
  position: absolute;
  display: inline-block;
  opacity: 0;
  width: 0px;
  height: 0px;
  visibility: hidden;
  overflow: hidden;
`;
Enter fullscreen mode Exit fullscreen mode
  • Run gatsby develop to see what your site looks like. Here's mine.
    Alt Text

  • Click on the Resume L1 menu item to go to the Resume page. Here's mine.
    Alt Text

Now that we've gotten rid of the hero image, there is a gap between the author info and the resume body that I don't like. So let's fix that.

  • Shadow the article.template.tsx template
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/templates/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/templates/article.template.tsx' './src/@narative/gatsby-theme-novela/templates/article.template.tsx'
    • Open './src/@narative/gatsby-theme-novela/templates/article.template.tsx'
    • Update the top padding on ArticleBody and save. Here's mine.
import React, { useRef, useState, useEffect } from "react";
import styled from "@emotion/styled";
import throttle from "lodash/throttle";
import { graphql, useStaticQuery } from "gatsby";

import Layout from "@components/Layout";
import MDXRenderer from "@components/MDX";
import Progress from "@components/Progress";
import Section from "@components/Section";
import Subscription from "@components/Subscription";

import mediaqueries from "@styles/media";
import { debounce } from "@utils";

import ArticleAside from "../sections/article/Article.Aside";
import ArticleHero from "../sections/article/Article.Hero";
import ArticleControls from "../sections/article/Article.Controls";
import ArticlesNext from "../sections/article/Article.Next";
import ArticleSEO from "../sections/article/Article.SEO";
import ArticleShare from "../sections/article/Article.Share";

import { Template } from "@types";

const siteQuery = graphql`
  {
    allSite {
      edges {
        node {
          siteMetadata {
            name
          }
        }
      }
    }
  }
`;

const Article: Template = ({ pageContext, location }) => {
  const contentSectionRef = useRef<HTMLElement>(null);

  const [hasCalculated, setHasCalculated] = useState<boolean>(false);
  const [contentHeight, setContentHeight] = useState<number>(0);

  const results = useStaticQuery(siteQuery);
  const name = results.allSite.edges[0].node.siteMetadata.name;

  const { article, authors, mailchimp, next } = pageContext;

  useEffect(() => {
    const calculateBodySize = throttle(() => {
      const contentSection = contentSectionRef.current;

      if (!contentSection) return;

      /**
       * If we haven't checked the content's height before,
       * we want to add listeners to the content area's
       * imagery to recheck when it's loaded
       */
      if (!hasCalculated) {
        const debouncedCalculation = debounce(calculateBodySize);
        const $imgs = contentSection.querySelectorAll("img");

        $imgs.forEach($img => {
          // If the image hasn't finished loading then add a listener
          if (!$img.complete) $img.onload = debouncedCalculation;
        });

        // Prevent rerun of the listener attachment
        setHasCalculated(true);
      }

      // Set the height and offset of the content area
      setContentHeight(contentSection.getBoundingClientRect().height);
    }, 20);

    calculateBodySize();
    window.addEventListener("resize", calculateBodySize);

    return () => window.removeEventListener("resize", calculateBodySize);
  }, []);

  return (
    <Layout>
      <ArticleSEO article={article} authors={authors} location={location} />
      <ArticleHero article={article} authors={authors} />
      <ArticleAside contentHeight={contentHeight}>
        <Progress contentHeight={contentHeight} />
      </ArticleAside>
      <MobileControls>
        <ArticleControls />
      </MobileControls>
      <ArticleBody ref={contentSectionRef}>
        <MDXRenderer content={article.body}>
          <ArticleShare />
        </MDXRenderer>
      </ArticleBody>
      {mailchimp && article.subscription && <Subscription />}
      {next.length > 0 && (
        <NextArticle narrow>
          <FooterNext>More articles from {name}</FooterNext>
          <ArticlesNext articles={next} />
          <FooterSpacer />
        </NextArticle>
      )}
    </Layout>
  );
};

export default Article;

const MobileControls = styled.div`
  position: relative;
  padding-top: 60px;
  transition: background 0.2s linear;
  text-align: center;

  ${mediaqueries.tablet_up`
    display: none;
  `}
`;

/* Gavin 20200722: Reduced top padding on the ArticleBody because it looked too
  big w/ no hero */
const ArticleBody = styled.article`
  position: relative;
  padding: 50px 0 35px;
  padding-left: 68px;
  transition: background 0.2s linear;

  ${mediaqueries.desktop`
    padding-left: 53px;
  `}

  ${mediaqueries.tablet`
    padding: 25px 0 80px;
  `}

  ${mediaqueries.phablet`
    padding: 20px 0;
  `}
`;

const NextArticle = styled(Section)`
  display: block;
`;

const FooterNext = styled.h3`
  position: relative;
  opacity: 0.25;
  margin-bottom: 100px;
  font-weight: 400;
  color: ${p => p.theme.colors.primary};

  ${mediaqueries.tablet`
    margin-bottom: 60px;
  `}

  &::after {
    content: '';
    position: absolute;
    background: ${p => p.theme.colors.grey};
    width: ${(910 / 1140) * 100}%;
    height: 1px;
    right: 0;
    top: 11px;

    ${mediaqueries.tablet`
      width: ${(600 / 1140) * 100}%;
    `}

    ${mediaqueries.phablet`
      width: ${(400 / 1140) * 100}%;
    `}

    ${mediaqueries.phone`
      width: 90px
    `}
  }
`;

const FooterSpacer = styled.div`
  margin-bottom: 65px;
`;
Enter fullscreen mode Exit fullscreen mode
  • Open './src/@narative/gatsby-theme-novela/templates/article.template.tsx'
    • Reduce the bottom margin on Header and save. Here's mine.
import React from 'react';
import styled from '@emotion/styled';

import Headings from '@components/Headings';
import Image, { ImagePlaceholder } from '@components/Image';

import mediaqueries from '@styles/media';
import { IArticle, IAuthor } from '@types';

import ArticleAuthors from './Article.Authors';

interface ArticleHeroProps {
  article: IArticle;
  authors: IAuthor[];
}

const ArticleHero: React.FC<ArticleHeroProps> = ({ article, authors }) => {
  const hasCoAUthors = authors.length > 1;
  const hasHeroImage =
    article.hero &&
    Object.keys(article.hero.full).length !== 0 &&
    article.hero.full.constructor === Object;

  return (
    <Hero>
      <Header>
        <HeroHeading>{article.title}</HeroHeading>
        <HeroSubtitle hasCoAUthors={hasCoAUthors}>
          <ArticleAuthors authors={authors} />
          <ArticleMeta hasCoAUthors={hasCoAUthors}>
            {article.date} · {article.timeToRead} min read
          </ArticleMeta>
        </HeroSubtitle>
      </Header>
      <HeroImage id="ArticleImage__Hero">
        {hasHeroImage && (
          <Image src={article.hero.full} />
        )}
      </HeroImage>
    </Hero>
  );
};

export default ArticleHero;

const Hero = styled.div`
  ${p => mediaqueries.phablet`
    &::before {
      content: "";
      width: 100%;
      height: 20px;
      background: ${p.theme.colors.primary};
      position: absolute;
      left: 0;
      top: 0;
      transition: ${p.theme.colorModeTransition};
    }

    &::after {
      content: "";
      width: 100%;
      height: 10px;
      background: ${p.theme.colors.background};
      position: absolute;
      left: 0;
      top: 10px;
      border-top-left-radius: 25px;
      border-top-right-radius: 25px;
      transition: ${p.theme.colorModeTransition};
    }
  `}
`;

const ArticleMeta = styled.div<{ hasCoAUthors: boolean }>`
  margin-left: ${p => (p.hasCoAUthors ? '10px' : '0')};

  ${mediaqueries.phablet`
    margin-left: 0;
  `}
`;

/* Gavin 20200722: Reduced bottom margin on the Header because it looked too
  big w/ no hero */
const Header = styled.header`
  position: relative;
  z-index: 10;
  margin:50px auto 120px;
  padding-left: 68px;
  max-width: 749px;

  ${mediaqueries.desktop`
    padding-left: 53px;
    max-width: calc(507px + 53px);
    margin: 50px auto 70px;
  `}

  ${mediaqueries.tablet`
    padding-left: 0;
    margin: 50px auto 70px;
    max-width: 480px;
  `}

  ${mediaqueries.phablet`
    margin: 50px auto 180px;
    padding: 0 40px;
  `}

  @media screen and (max-height: 700px) {
    margin: 50px auto;
  }
`;

const HeroHeading = styled(Headings.h1)`
  font-size: 48px;
  font-family: ${p => p.theme.fonts.serif};
  margin-bottom: 25px;
  font-weight: bold;
  line-height: 1.32;

  ${mediaqueries.tablet`
    margin-bottom: 20px;
    font-size: 36px;
  `}

  ${mediaqueries.phablet`
    font-size: 32px;
  `}
`;

const HeroSubtitle = styled.div<{ hasCoAUthors: boolean }>`
  position: relative;
  display: flex;
  font-size: 18px;
  color: ${p => p.theme.colors.grey};

  ${p => mediaqueries.phablet`
    font-size: 14px;
    flex-direction: column;

    ${p.hasCoAUthors &&
      `
        &::before {
          content: '';
          position: absolute;
          left: -20px;
          right: -20px;
          top: -10px;
          bottom: -10px;
          border: 1px solid ${p.theme.colors.horizontalRule};
          opacity: 0.5;
          border-radius: 5px;
        }
    `}


    strong {
      display: block;
      font-weight: 500;
      margin-bottom: 5px;
    }
  `}
`;

const HeroImage = styled.div`
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 944px;
  overflow: hidden;
  margin: 0 auto;
  box-shadow: 0 30px 60px -10px rgba(0, 0, 0, 0.2),
    0 18px 36px -18px rgba(0, 0, 0, 0.22);

  ${mediaqueries.tablet`
    max-width: 100%;
  `}

  ${mediaqueries.phablet`
    margin: 0 auto;
    width: calc(100vw - 40px);
    height: 220px;

    & > div {
      height: 220px;
    }
`}
`;
Enter fullscreen mode Exit fullscreen mode
  • Run gatsby develop and click on the Resume L1 menu item to go to the Resume page. Here's mine. Alt Text

Much better. Now let's commit your changes to your repo.

Merge your changes into your repo

  • Add all new files
    • git add --all
  • Commit your changes
    • git commit -a -m "[your commit message]"
  • Push your branch to your repo on GitHub
    • git push -u origin initial_customizations

Boom! Your branch and changes have been saved to your repo. Now create a pull request on GitHub and merge your changes into the main branch.

If you got an "Authentication failed..." error even though you entered your GitHub username and password correctly, you may have to create and use a personal access token. Read this Medium blog for details on how to do that.

  • In your repo on GitHub, click on the "Pull Requests" tab Alt Text
  • Click on the "New pull request" button Alt Text
  • Click the "Compare" dropdown and select the initial_customizations branch Alt Text
  • Compare your changes
  • When you are good with all of the change made, click the "Create pull request" button Alt Text
  • Write your PR notes and click the "Create pull request" button Alt Text
  • Click the "Merge pull request" button Alt Text
  • Click the "Confirm merge" button Alt Text You should see "Pull request successfully merged and closed."
  • In Terminal, git checkout main

Boom!
Changes have been merged, and you are back on the main branch, ready to move forward.

That's it for today.
I'll post tutorial next week on how to deploy your site to Firebase and maybe how to automate deploys with GitHub Actions. We'll see what I have time to get around to.

Top comments (0)