├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── gatsby-config.js ├── gatsby-node.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── components │ ├── Box.jsx │ ├── FancyBox.jsx │ ├── Icon.jsx │ ├── ImageSwitcher.jsx │ ├── Navbar.jsx │ ├── NavbarItem.jsx │ ├── NavbarToggle.jsx │ ├── ProjectSnippet.jsx │ ├── layout.js │ ├── styles │ │ ├── FancyBox.scss │ │ ├── ImageSwitcher.scss │ │ ├── NavbarToggle.scss │ │ ├── box.scss │ │ └── tailwind.css │ └── utility │ │ ├── TransitionLinkDefault.jsx │ │ └── seo.js ├── data │ ├── bdg-connect.md │ ├── codepen-challenges.md │ ├── livestream-radio.md │ ├── midi-particles.md │ ├── sparling-creations.md │ ├── sparling-dev.md │ ├── studio-planner.md │ └── twitch-recreation.md ├── images │ ├── bdg-connect.png │ ├── codepen-challenges.png │ ├── favicon.png │ ├── livestream-radio.png │ ├── midi-particles.png │ ├── sparling-creations.png │ ├── sparling-dev.png │ ├── studio-planner.png │ └── twitch-recreation.png ├── pages │ ├── 404.js │ ├── about.js │ ├── contact.js │ ├── index.js │ ├── index.scss │ └── projects.js ├── styles │ └── styles.scss └── templates │ ├── project.js │ └── project.scss └── tailwind.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": true, 5 | "jsxSingleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Stephen Sparling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Stephen Sparling, Fullstack Web Developer & Designer 3 | 4 | Here you will find the source code for my [development portfolio](https://dev.sparlingcreations.com). 5 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: `Stephen Sparling Development`, 4 | description: `The portfolio for Stephen Sparling, web developer & designer.`, 5 | author: `@sparlos`, 6 | }, 7 | plugins: [ 8 | { 9 | resolve: `gatsby-plugin-transition-link`, 10 | options: { 11 | layout: require.resolve("./src/components/layout.js"), 12 | }, 13 | }, 14 | `gatsby-plugin-postcss`, 15 | { 16 | resolve: `gatsby-plugin-sass`, 17 | }, 18 | `gatsby-transformer-remark`, 19 | { 20 | resolve: `gatsby-plugin-web-font-loader`, 21 | options: { 22 | google: { 23 | families: ["Raleway:300,400,500,600,700"], 24 | }, 25 | }, 26 | }, 27 | `gatsby-plugin-react-helmet`, 28 | { 29 | resolve: `gatsby-source-filesystem`, 30 | options: { 31 | name: `images`, 32 | path: `${__dirname}/src/images`, 33 | }, 34 | }, 35 | { 36 | resolve: `gatsby-source-filesystem`, 37 | options: { 38 | name: `data`, 39 | path: `${__dirname}/src/data`, 40 | }, 41 | }, 42 | `gatsby-transformer-sharp`, 43 | `gatsby-plugin-sharp`, 44 | { 45 | resolve: `gatsby-plugin-manifest`, 46 | options: { 47 | name: `Stephen Sparling | Fullstack Dev`, 48 | short_name: `Stephen Sparling`, 49 | start_url: `/`, 50 | background_color: `#1A202C`, 51 | theme_color: `#1A202C`, 52 | display: `minimal-ui`, 53 | icon: `src/images/favicon.png`, // This path is relative to the root of the site. 54 | }, 55 | } 56 | ], 57 | } 58 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`) 2 | const { createFilePath } = require(`gatsby-source-filesystem`) 3 | 4 | exports.onCreateNode = ({ node, getNode, actions }) => { 5 | const { createNodeField } = actions 6 | 7 | if (node.internal.type === `MarkdownRemark`) { 8 | const slug = createFilePath({ node, getNode, basePath: `pages/projects/` }) 9 | createNodeField({ 10 | node, 11 | name: `slug`, 12 | value: `/projects${slug}`, 13 | }) 14 | } 15 | } 16 | 17 | exports.createPages = async ({ graphql, actions }) => { 18 | const { createPage } = actions 19 | const result = await graphql(` 20 | query { 21 | allMarkdownRemark { 22 | edges { 23 | node { 24 | fields { 25 | slug 26 | } 27 | } 28 | } 29 | } 30 | } 31 | `) 32 | 33 | result.data.allMarkdownRemark.edges.forEach(({ node }) => { 34 | createPage({ 35 | path: node.fields.slug, 36 | component: path.resolve(`./src/templates/project.js`), 37 | context: { 38 | slug: node.fields.slug 39 | } 40 | }) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-default", 3 | "private": true, 4 | "description": "A simple starter to get up and developing quickly with Gatsby", 5 | "version": "0.1.0", 6 | "author": "Kyle Mathews ", 7 | "dependencies": { 8 | "framer-motion": "^1.7.0", 9 | "gatsby": "^2.18.8", 10 | "gatsby-image": "^2.2.37", 11 | "gatsby-plugin-manifest": "^2.2.31", 12 | "gatsby-plugin-offline": "^3.0.27", 13 | "gatsby-plugin-postcss": "^2.1.18", 14 | "gatsby-plugin-react-helmet": "^3.1.16", 15 | "gatsby-plugin-sass": "^2.1.26", 16 | "gatsby-plugin-sharp": "^2.3.9", 17 | "gatsby-plugin-transition-link": "^1.17.5", 18 | "gatsby-plugin-web-font-loader": "^1.0.4", 19 | "gatsby-source-filesystem": "^2.1.42", 20 | "gatsby-transformer-remark": "^2.6.42", 21 | "gatsby-transformer-sharp": "^2.3.9", 22 | "gsap": "^3.0.4", 23 | "prop-types": "^15.7.2", 24 | "react": "^16.12.0", 25 | "react-dom": "^16.12.0", 26 | "react-helmet": "^5.2.1", 27 | "react-icons": "^3.8.0" 28 | }, 29 | "devDependencies": { 30 | "node-sass": "^4.14.1", 31 | "prettier": "^1.19.1", 32 | "tailwindcss": "^1.1.4" 33 | }, 34 | "keywords": [ 35 | "gatsby" 36 | ], 37 | "license": "MIT", 38 | "scripts": { 39 | "build": "gatsby build", 40 | "develop": "gatsby develop", 41 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"", 42 | "start": "npm run develop", 43 | "serve": "gatsby serve", 44 | "clean": "gatsby clean", 45 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1", 46 | "develop:network": "gatsby develop -H 0.0.0.0" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/gatsbyjs/gatsby/issues" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | plugins: [require("tailwindcss")] 3 | }) -------------------------------------------------------------------------------- /src/components/Box.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { motion } from 'framer-motion' 4 | 5 | const Box = ({ size, position, rotation, custom, path }) => { 6 | const boxInitialColor = '#edf2f7' 7 | const boxHiddenColor = '#f7fafc' 8 | 9 | let randomPosition = Math.floor(Math.random() * 20) 10 | randomPosition *= Math.floor(Math.random() * 2) === 1 ? 1 : -1 11 | 12 | const boxVariants = { 13 | visible: i => ({ 14 | borderRadius: 0, 15 | backgroundColor: boxInitialColor, 16 | opacity: 1, 17 | scale: 1, 18 | rotate: rotation, 19 | y: 0, 20 | x: 0, 21 | transition: { 22 | type: 'spring', 23 | damping: 300, 24 | delay: i * 0.1, 25 | }, 26 | }), 27 | hidden: { opacity: 0, y: 200, rotate: 0, scale: 1, borderRadius: 0 }, 28 | about: { 29 | borderRadius: 0, 30 | backgroundColor: boxInitialColor, 31 | opacity: 1, 32 | scale: 1, 33 | y: randomPosition, 34 | x: randomPosition, 35 | rotate: rotation * 5, 36 | transition: { 37 | type: 'spring', 38 | mass: 5, 39 | damping: 300, 40 | }, 41 | }, 42 | contact: { 43 | backgroundColor: boxHiddenColor, 44 | borderRadius: 0, 45 | opacity: 1, 46 | rotate: rotation * 5, 47 | transition: { 48 | default: { 49 | type: 'spring', 50 | mass: 1, 51 | damping: 300, 52 | }, 53 | }, 54 | }, 55 | projects: { 56 | backgroundColor: boxInitialColor, 57 | opacity: 1, 58 | scale: 1, 59 | rotate: rotation * 8, 60 | borderRadius: '100%', 61 | transition: { 62 | rotate: { 63 | type: 'spring', 64 | mass: 5, 65 | damping: 300, 66 | }, 67 | borderRadius: { 68 | type: 'tween', 69 | duration: 2 70 | } 71 | }, 72 | } 73 | } 74 | 75 | const animation = () => { 76 | switch (path) { 77 | case '/about/': 78 | return 'about' 79 | case '/contact/': 80 | return 'contact' 81 | case '/projects/': 82 | return 'projects' 83 | default: 84 | return 'visible' 85 | } 86 | } 87 | 88 | return ( 89 | 99 | ) 100 | } 101 | 102 | export default Box 103 | -------------------------------------------------------------------------------- /src/components/FancyBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './styles/FancyBox.scss' 3 | 4 | import { motion } from 'framer-motion' 5 | 6 | const FancyBox = ({ custom }) => { 7 | const boxVariants = { 8 | visible: i => ({ 9 | opacity: 1, 10 | x: 0, 11 | y: 0, 12 | transition: { 13 | type: 'spring', 14 | damping: 300, 15 | delay: i * 0.1, 16 | }, 17 | }), 18 | hidden: { opacity: 0, x: -20, y: -5, rotate: 0 }, 19 | } 20 | 21 | return ( 22 | 29 | ) 30 | } 31 | 32 | export default FancyBox 33 | -------------------------------------------------------------------------------- /src/components/Icon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { motion } from 'framer-motion' 3 | 4 | const Icon = ({ href, linkTitle, customClasses, children }) => { 5 | const variants = { 6 | hidden: { opacity: 0, y: 30 }, 7 | show: { 8 | opacity: 1, 9 | y: 0, 10 | transition: { 11 | type: 'spring', 12 | damping: 100, 13 | }, 14 | }, 15 | } 16 | 17 | return ( 18 | 24 | {children} 25 |

{linkTitle}

26 |
27 | ) 28 | } 29 | 30 | export default Icon 31 | -------------------------------------------------------------------------------- /src/components/ImageSwitcher.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { motion, AnimatePresence } from 'framer-motion' 4 | import './styles/ImageSwitcher.scss' 5 | 6 | const ImageSwitcher = ({ activeImage, images, transitionStatus }) => { 7 | const variants = { 8 | show: { 9 | y: 0, 10 | opacity: 1, 11 | }, 12 | hide: { 13 | y: 100, 14 | opacity: 0, 15 | }, 16 | } 17 | 18 | return ( 19 |
20 | {activeImage !== null && ( 21 |
22 | 23 | 43 | 44 |
45 | )} 46 |
47 | ) 48 | } 49 | 50 | export default ImageSwitcher 51 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { motion } from 'framer-motion' 4 | 5 | import NavbarItem from './NavbarItem' 6 | 7 | const Navbar = ({ toggleNavbar }) => { 8 | const ulVariants = { 9 | open: { 10 | opacity: 1, 11 | }, 12 | closed: { 13 | opacity: 0, 14 | }, 15 | } 16 | 17 | const items = ['projects', 'contact', 'about', 'home'] 18 | 19 | return ( 20 | 21 | 27 | {items.map((item, i) => ( 28 | 34 | ))} 35 | 36 | 37 | ) 38 | } 39 | 40 | export default Navbar 41 | -------------------------------------------------------------------------------- /src/components/NavbarItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { motion } from 'framer-motion' 4 | import TransitionLinkDefault from './utility/TransitionLinkDefault' 5 | 6 | const NavbarItem = ({ custom, label, toggleNavbar }) => { 7 | const variants = { 8 | open: i => ({ 9 | opacity: 1, 10 | y: 0, 11 | transition: { 12 | delay: (i + 1) * 0.1, 13 | type: 'spring', 14 | damping: 100, 15 | mass: 1, 16 | }, 17 | }), 18 | closed: { 19 | y: 50, 20 | opacity: 0, 21 | }, 22 | } 23 | 24 | return ( 25 | 32 | toggleNavbar()} 35 | className="link--plain transition-color" 36 | > 37 | {label} 38 | 39 | 40 | ) 41 | } 42 | 43 | export default NavbarItem 44 | -------------------------------------------------------------------------------- /src/components/NavbarToggle.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { motion } from 'framer-motion' 3 | 4 | import './styles/NavbarToggle.scss' 5 | 6 | const gray900 = '#1A202C' 7 | const gray100 = '#F7FAFC' 8 | 9 | const Bar = props => { 10 | const transition = { 11 | type: 'spring', 12 | damping: 200, 13 | mass: 0.2, 14 | } 15 | 16 | return ( 17 | 39 | ) 40 | } 41 | 42 | const NavbarToggle = ({ navbar, setNavbar, transition }) => { 43 | 44 | const activeClass = navbar ? 'active' : '' 45 | 46 | return ( 47 | 57 | 68 | 75 | 85 | 86 | ) 87 | } 88 | 89 | export default NavbarToggle 90 | -------------------------------------------------------------------------------- /src/components/ProjectSnippet.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Img from 'gatsby-image' 4 | import TransitionLinkDefault from '../components/utility/TransitionLinkDefault' 5 | import { motion } from 'framer-motion' 6 | 7 | const ProjectSnippet = ({ 8 | setActiveImage, 9 | imageIndex, 10 | title, 11 | blurb, 12 | imageFluid, 13 | slug, 14 | maintech, 15 | }) => { 16 | const variants = { 17 | show: i => ({ 18 | opacity: 1, 19 | y: 0, 20 | transition: { 21 | delay: 0.15 + (i + 1) * 0.1, 22 | type: 'spring', 23 | damping: 100, 24 | mass: 1, 25 | }, 26 | }), 27 | hide: { 28 | opacity: 0, 29 | y: 30, 30 | }, 31 | } 32 | 33 | return ( 34 | { 38 | setActiveImage(imageIndex) 39 | }} 40 | > 41 | 47 |

{title}

48 |
49 | {maintech.split(',').map((item, i) => ( 50 | 51 | {i !== 0 ? ' |' : ''} {item} 52 | 53 | ))} 54 |
55 | 56 |

{blurb}

57 |
58 |
59 | ) 60 | } 61 | 62 | export default ProjectSnippet 63 | -------------------------------------------------------------------------------- /src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { motion, AnimatePresence } from 'framer-motion' 5 | 6 | import Box from '../components/Box' 7 | import FancyBox from '../components/FancyBox' 8 | import NavbarToggle from '../components/NavbarToggle' 9 | import Navbar from './Navbar' 10 | 11 | import './styles/tailwind.css' 12 | import './styles/box.scss' 13 | 14 | const Layout = ({ children, path }) => { 15 | const [navbar, setNavbar] = useState(false) 16 | const handleSetNavbar = () => setNavbar(!navbar) 17 | 18 | const backgroundVariants = { 19 | visible: { 20 | opacity: 1, 21 | }, 22 | hidden: { opacity: 0 }, 23 | } 24 | 25 | const transition = { 26 | type: 'spring', 27 | damping: 100, 28 | mass: 0.5, 29 | } 30 | 31 | return ( 32 |
33 |
34 | 35 | {path !== '/' && ( 36 | 41 | )} 42 | 43 |
44 | 45 | {navbar && ( 46 | 54 | 55 | 56 | )} 57 | 58 | 67 |
{children}
68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 | ) 77 | } 78 | 79 | Layout.propTypes = { 80 | children: PropTypes.node.isRequired, 81 | } 82 | 83 | export default Layout 84 | -------------------------------------------------------------------------------- /src/components/styles/FancyBox.scss: -------------------------------------------------------------------------------- 1 | .fancybox { 2 | $size: 40px; 3 | z-index: -100; 4 | 5 | width: $size; 6 | height: $size; 7 | position: absolute; 8 | 9 | left: 5%; 10 | top: 50px; 11 | 12 | &:after { 13 | content: ''; 14 | position: absolute; 15 | width: 75%; 16 | height: 75%; 17 | top: 50%; 18 | left: 50%; 19 | border: 3px solid #718096; 20 | } 21 | } -------------------------------------------------------------------------------- /src/components/styles/ImageSwitcher.scss: -------------------------------------------------------------------------------- 1 | .image-switcher { 2 | position: fixed; 3 | width: 40%; 4 | right: 7%; 5 | top: 220px; 6 | 7 | &__image { 8 | position: absolute; 9 | width: auto; 10 | height: auto; 11 | } 12 | } -------------------------------------------------------------------------------- /src/components/styles/NavbarToggle.scss: -------------------------------------------------------------------------------- 1 | .nav-toggle { 2 | 3 | &:after { 4 | content: ''; 5 | z-index: -2; 6 | position: absolute; 7 | width: 140%; 8 | height: 220%; 9 | border-radius: 5px; 10 | top: 50%; 11 | left: 50%; 12 | transform: translate(-50%, -50%); 13 | background-color: rgba(247, 250, 252, 0.9); 14 | transition: .2s all; 15 | } 16 | 17 | &.active:after { 18 | background-color: rgba(0,0,0,0); 19 | border: none; 20 | box-shadow: none; 21 | } 22 | } -------------------------------------------------------------------------------- /src/components/styles/box.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/styles.scss"; 2 | 3 | .box { 4 | transition: right .3s, top .3s, bottom .3s, left .3s; 5 | z-index: -100; 6 | 7 | &--1 { 8 | right: -30%; 9 | top: -30px; 10 | 11 | @include breakpoint(sm) { 12 | right: 10%; 13 | top: 70px; 14 | } 15 | 16 | } 17 | 18 | &--2 { 19 | left: -10%; 20 | top: 500px; 21 | 22 | @include breakpoint(sm) { 23 | left: 15%; 24 | bottom: 175px; 25 | } 26 | } 27 | 28 | &--3 { 29 | right: 0px; 30 | top: 350px; 31 | 32 | @include breakpoint(sm) { 33 | right: 15%; 34 | top: 80vh; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/components/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | 7 | .link { 8 | @apply underline text-teal-500; 9 | } 10 | 11 | .link--plain { 12 | @apply underline; 13 | } 14 | 15 | .link--plain:hover { 16 | @apply text-teal-400; 17 | } 18 | 19 | .transition-color { 20 | transition: .1s color; 21 | } 22 | 23 | .heading-main { 24 | @apply text-5xl font-normal tracking-widest; 25 | } -------------------------------------------------------------------------------- /src/components/utility/TransitionLinkDefault.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TransitionLink from 'gatsby-plugin-transition-link' 3 | 4 | const TransitionLinkDefault = props => ( 5 | 14 | {props.children} 15 | 16 | ) 17 | 18 | 19 | export default TransitionLinkDefault -------------------------------------------------------------------------------- /src/components/utility/seo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Helmet from 'react-helmet' 4 | import { useStaticQuery, graphql } from 'gatsby' 5 | 6 | const SEO = ({ description, lang, meta, title }) => { 7 | const { site } = useStaticQuery( 8 | graphql` 9 | query { 10 | site { 11 | siteMetadata { 12 | title 13 | description 14 | author 15 | } 16 | } 17 | } 18 | ` 19 | ) 20 | 21 | const metaDescription = description || site.siteMetadata.description 22 | 23 | return ( 24 | 65 | ) 66 | } 67 | 68 | SEO.defaultProps = { 69 | lang: `en`, 70 | meta: [], 71 | description: ``, 72 | } 73 | 74 | SEO.propTypes = { 75 | description: PropTypes.string, 76 | lang: PropTypes.string, 77 | meta: PropTypes.arrayOf(PropTypes.object), 78 | title: PropTypes.string.isRequired, 79 | } 80 | 81 | export default SEO 82 | -------------------------------------------------------------------------------- /src/data/bdg-connect.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "BDG Connect" 3 | blurb: "WordPress theme created for Baseball Development Group with a design focus on maximum customizability for the owner/operator of the website." 4 | image: ../images/bdg-connect.png 5 | maintech: WordPress,PHP,Bootstrap 6 | tags: HTML,CSS,JS,WordPress,PHP,Bootstrap 7 | position: 4 8 | --- 9 | A website created for the Baseball Development Group organization that highlights my ability to produce a WordPress theme from scratch, based on a customer design. The site is highly customizable and can be edited by the owner/operator, making it easy to regularly update content. This is particularly important on the site’s “about us” page where the list of team members varies constantly. 10 | 11 | I used Bootstrap to power mobile responsivity and I hand-coded custom animations to power the button hover effects and the navbar expansion on mobile. 12 | -------------------------------------------------------------------------------- /src/data/codepen-challenges.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "CodePen Weekly Challenges" 3 | blurb: "A collection of music-based CodePen experiments I built for some of the weekly challenges that CodePen held." 4 | image: ../images/codepen-challenges.png 5 | maintech: Vue,SCSS,JS 6 | tags: Vue,SCSS,JS,Animation,Algorithms,Design 7 | position: 5 8 | --- 9 | A [collection of CodePen experiments](https://codepen.io/collection/Xermmq) created for CodePen’s weekly challenges. My background in music allowed me to base each challenge submission around music, exploiting an underused feature in web development: music and audio. 10 | 11 | The collection includes: 12 | 13 | - [generative music based on prime numbers](https://codepen.io/sparlos/pen/oOqMGw) 14 | - [an implementation of the bubble sort algorithm represented using tones](https://codepen.io/sparlos/pen/gydjdL) 15 | - [an infinitely random starfield where every star creates a tone based on the star’s Y position](https://codepen.io/sparlos/pen/QRwVjp) 16 | - [a visual representation of Steve Reich’s ‘Piano Phase’ using planet orbits](https://codepen.io/collection/Xermmq) 17 | - [a beat creation system where each portion of the beat is represented by a component of a hamburger](https://codepen.io/sparlos/pen/argZay). 18 | 19 | Every project was implemented using Vue for logic and SCSS for styling. The source code for all of the projects is available for perusal at the CodePen links provided. 20 | -------------------------------------------------------------------------------- /src/data/livestream-radio.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Livestream Radio" 3 | blurb: "A website for categorizing YouTube livestreams. Written primarily in Vue." 4 | image: ../images/livestream-radio.png 5 | maintech: Vue,Vuetify,SCSS 6 | tags: Vue,Vuetify,SCSS,GitHub,Material Design,YouTube iframe API 7 | position: 3 8 | --- 9 | A [website](https://livestreamradio.netlify.com/) that allows a user to add and categorize YouTube livestreams as “stations”. Open source and maintained on [GitHub](https://github.com/sparlos/livestream-radio). 10 | 11 | The project was written using Vue and Vuetify, a material design framework for Vue. This website allows the user to categorize and easily switch between different YouTube livestreams. This site showcases my ability to take an idea and follow through to full implementation. The project is also completely open source, and has a few contributors whose efforts I manage. 12 | 13 | Time restrictions, which compelled me to complete the build within a week, wound up being extremely beneficial for the project, forcing the most crucial features to the forefront. 14 | -------------------------------------------------------------------------------- /src/data/midi-particles.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "MIDI Particles" 3 | blurb: "A web app built on Vue that takes MIDI input via the web MIDI API to create particle visualizations." 4 | image: ../images/midi-particles.png 5 | maintech: Vue,Vuex,Web MIDI API 6 | tags: Vue,Web MIDI API,SCSS,Vuex,Canvas2D,localStorage,OOP 7 | position: 6 8 | --- 9 | [An extremely customizable application](https://midiparticles.netlify.com/) that takes MIDI input and creates particle visualizations. Because it uses the experimental Web MIDI API, it is only supported in Chrome at the moment. The project is open source and maintained [on GitHub](https://github.com/sparlos/MIDI-Particles). 10 | 11 | The app includes an onscreen keyboard to mimic the user’s real life MIDI input device. The speed of the particles is determined by the input velocity that the MIDI device registers (between 0-127). 12 | 13 | Its many customizable features are all listed on the GitHub repo. All customizable settings are saved in localStorage with the aid of lowDB and, because of the large amount of settings, the project also uses Vuex for state management. 14 | 15 | For the actual animation, the app uses requestAnimationFrame paired with Canvas2D rendering. All of the JavaScript and Vue code was written completely from scratch. The particles and particle systems were also created from scratch using an Object Oriented Approach which utilizes modern ES6 classes. 16 | -------------------------------------------------------------------------------- /src/data/sparling-creations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Sparling Creations" 3 | blurb: "A Single Page Application showcasing my freelance web design, video editing and music production talents. Written primarily in Vue." 4 | image: ../images/sparling-creations.png 5 | maintech: Vue,SCSS,Adobe XD 6 | tags: Vue,Vue Router,SCSS,SoundCloud API,Adobe XD 7 | position: 2 8 | --- 9 | A website built to house my freelance web design, video and audio projects. The site is powered by Vue and uses Vue Router for navigation. 10 | 11 | The design was created completely from scratch in Adobe XD, with animations and transitions all hand-coded using SCSS and BEM methodology. I wrote a mini-app in Vue that wraps the SoundCloud iFrame API in the ‘Music’ section of the site. This was done for aesthetic reasons in order to make the design of the player compliment the design of the site. The data layer of the site is powered using [Cockpit CMS](https://getcockpit.com/) allowing for easy addition of new projects. 12 | -------------------------------------------------------------------------------- /src/data/sparling-dev.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Stephen Sparling Developer Portfolio" 3 | blurb: "The site you are on right now. Powered primarily by React/Gatsby." 4 | image: ../images/sparling-dev.png 5 | maintech: Gatsby,React,Tailwind 6 | tags: React,Gatsby,Figma,GraphQL,Tailwind,SCSS,Framer Motion 7 | position: 1 8 | --- 9 | [A website](http://dev.sparlingcreations.com/) made to showcase the web development & design projects of Stephen Sparling. I attempted a very minimalist, clean looking website that focuses on ease of navigation for a user to find the info they want. The user experience is enhanced by sleek, purposeful animations to bring the user’s attention to the fact that something has changed on the page. 10 | 11 | The design was created entirely using Figma, and is powered by [Tailwind CSS](https://tailwindcss.com/), a great utility-first CSS framework. All animations are either hand-crafted in SCSS or utilize the excellent [Framer Motion library](https://www.framer.com/motion/). The functionality of the site comes from Gatsby, with some custom React code and graphQL queries. 12 | 13 | Soruce code for the site [is avaliable here](https://github.com/sparlos/dev-portfolio). 14 | -------------------------------------------------------------------------------- /src/data/studio-planner.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Studio Planner" 3 | blurb: "A basic authenticated CRUD fullstack app, set up using Next.js and Supabase." 4 | image: ../images/studio-planner.png 5 | maintech: Next.js,Supabase,React 6 | tags: Next.js,Supabase,React,Chakra UI,dnd-kit 7 | position: 0 8 | --- 9 | [A website](https://studio-planner.vercel.app/) created to showcase a simple authenticated app that allows users to sign up/log in, and then access an instrument dashboard. This dashboard allows a user to view, create and delete instruments. It uses [Next.js](https://nextjs.org/) to power the front end, with the backend and database layers handled by [Supbase.io](https://supabase.io/). 10 | 11 | I usually do styling from the ground up using SCSS, but as the purpose of this project was to display my fullstack abilities, I opted to use [Chakra UI](https://chakra-ui.com/) to rapidly build out the front end (and because I've been dying to play around with it). The dashboard allows drag and drop functionality, which is powered by [dnd kit](https://docs.dndkit.com/). 12 | -------------------------------------------------------------------------------- /src/data/twitch-recreation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Twitch Homepage Recreation" 3 | blurb: "A recreation of the twitch.tv homepage layout (circa February 2019)." 4 | image: ../images/twitch-recreation.png 5 | maintech: Design,SCSS,Vue 6 | tags: HTML,SCSS,JS,Vue,Design 7 | position: 7 8 | --- 9 | [A recreation of the twitch.tv homepage layout](https://codepen.io/sparlos/full/JxyRWr) from February 2019; the layout has since been changed. This project nicely showcases my ability to take a completely visual design and implement it in code. All of the CSS (SCSS) was hand-coded and no framework was used. Most of the page was created using flexbox, and all of the SCSS follows the BEM methodology. 10 | 11 | This project was mostly focused on design, so there isn’t much actual functionality. The only exception is the video carousel, which was implemented using Vue. The design is semi-responsive, but doesn’t scale fully down to mobile. This is because at the time the actual twitch.tv main page did not either. [You can find the source code here](https://codepen.io/sparlos/pen/JxyRWr). 12 | -------------------------------------------------------------------------------- /src/images/bdg-connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparlos/dev-portfolio/4500a988529c07251de3073a2ebebeeacaa2a149/src/images/bdg-connect.png -------------------------------------------------------------------------------- /src/images/codepen-challenges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparlos/dev-portfolio/4500a988529c07251de3073a2ebebeeacaa2a149/src/images/codepen-challenges.png -------------------------------------------------------------------------------- /src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparlos/dev-portfolio/4500a988529c07251de3073a2ebebeeacaa2a149/src/images/favicon.png -------------------------------------------------------------------------------- /src/images/livestream-radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparlos/dev-portfolio/4500a988529c07251de3073a2ebebeeacaa2a149/src/images/livestream-radio.png -------------------------------------------------------------------------------- /src/images/midi-particles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparlos/dev-portfolio/4500a988529c07251de3073a2ebebeeacaa2a149/src/images/midi-particles.png -------------------------------------------------------------------------------- /src/images/sparling-creations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparlos/dev-portfolio/4500a988529c07251de3073a2ebebeeacaa2a149/src/images/sparling-creations.png -------------------------------------------------------------------------------- /src/images/sparling-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparlos/dev-portfolio/4500a988529c07251de3073a2ebebeeacaa2a149/src/images/sparling-dev.png -------------------------------------------------------------------------------- /src/images/studio-planner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparlos/dev-portfolio/4500a988529c07251de3073a2ebebeeacaa2a149/src/images/studio-planner.png -------------------------------------------------------------------------------- /src/images/twitch-recreation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparlos/dev-portfolio/4500a988529c07251de3073a2ebebeeacaa2a149/src/images/twitch-recreation.png -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { TransitionState } from 'gatsby-plugin-transition-link' 4 | import TransitionLinkDefault from '../components/utility/TransitionLinkDefault' 5 | 6 | import SEO from '../components/utility/seo' 7 | 8 | import { motion } from 'framer-motion' 9 | 10 | 11 | const NotFoundPage = () => { 12 | const variants = { 13 | hidden: { 14 | opacity: 0 15 | }, 16 | show: { 17 | opacity: 1 18 | } 19 | } 20 | 21 | return ( 22 | 23 | {({ transitionStatus }) => ( 24 | 34 | 35 |

Not Found

36 |

The page you've navigated to doesn't exist.

37 | 38 | Back home 39 | 40 |
41 | )} 42 |
43 | ) 44 | } 45 | 46 | export default NotFoundPage 47 | -------------------------------------------------------------------------------- /src/pages/about.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { TransitionState } from 'gatsby-plugin-transition-link' 4 | import TransitionLinkDefault from '../components/utility/TransitionLinkDefault' 5 | 6 | import SEO from '../components/utility/seo' 7 | 8 | import { motion } from 'framer-motion' 9 | 10 | const About = () => { 11 | const variants = { 12 | hidden: { 13 | opacity: 0, 14 | x: -100, 15 | y: 0, 16 | }, 17 | show: { 18 | opacity: 1, 19 | x: 0, 20 | y: 0, 21 | transition: { 22 | y: { 23 | from: 0, 24 | }, 25 | type: 'spring', 26 | mass: 1, 27 | damping: 50, 28 | }, 29 | }, 30 | exit: { 31 | opacity: 0, 32 | y: 100, 33 | x: 0, 34 | transition: { 35 | type: 'spring', 36 | mass: 0.3, 37 | damping: 50, 38 | }, 39 | }, 40 | } 41 | 42 | return ( 43 | 44 | {({ transitionStatus }) => ( 45 | 55 | 56 |

About

57 |
58 |

59 | I'm Stephen, a driven young professional in the world of full stack web design & development. I've spent the last 60 | few years honing my skills in top web technologies including  61 | 62 | React, jQuery, SQL, PHP, Node, SASS & SCSS, Bootstrap, VS Code, 63 | Figma, WordPress, Python, Vue, MongoDB, and GraphQL.  64 | 65 |

66 |

67 | At my last job, I worked within a large team of other sales 68 | associates and multiple managers. I gained a lot of valuable 69 | experience pertaining to  70 | 71 | communication, teamwork, meeting the varied expectations of 72 | multiple people, and achieving goals on a deadline. 73 | 74 |

75 |

76 | 77 | I'm hardworking, receptive and unrelenting when it comes to 78 | doing the very best that I can.  79 | 80 | This is demonstrated by my 4 year study at York University, 81 | culminating in a Bachelor of Fine Arts with Honours. I was awarded 82 | a Magna Cum Laude for my efforts across those 4 years. 83 |

84 |

85 | If all of this sounds good to you, I'm ready to work. Preferably 86 | full-time; I'm open to either local (Toronto area) or remote 87 | work.  88 | 89 | Please reach out to me and I'd love to start a dialogue. 90 | 91 |

92 |

93 | 94 | Back Home 95 | 96 |

97 |
98 |
99 | )} 100 |
101 | ) 102 | } 103 | 104 | export default About 105 | -------------------------------------------------------------------------------- /src/pages/contact.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { TransitionState } from 'gatsby-plugin-transition-link' 4 | import TransitionLinkDefault from '../components/utility/TransitionLinkDefault' 5 | 6 | import SEO from '../components/utility/seo' 7 | 8 | import { motion } from 'framer-motion' 9 | 10 | import { 11 | FaLinkedin, 12 | FaTwitterSquare, 13 | FaEnvelopeSquare, 14 | FaGithubSquare, 15 | FaCodepen, 16 | } from 'react-icons/fa' 17 | 18 | import Icon from '../components/Icon' 19 | 20 | const contact = () => { 21 | const containerVariants = { 22 | hidden: { 23 | opacity: 0, 24 | scaleX: 0, 25 | }, 26 | show: { 27 | opacity: 1, 28 | scaleX: 1, 29 | y: 0, 30 | transition: { 31 | type: 'spring', 32 | damping: 200, 33 | mass: 0.3, 34 | delayChildren: 0.3, 35 | staggerChildren: 0.05, 36 | }, 37 | }, 38 | exit: { 39 | opacity: 0, 40 | y: 100, 41 | transition: { 42 | type: 'spring', 43 | damping: 200, 44 | mass: 0.3, 45 | }, 46 | }, 47 | } 48 | 49 | const headingVariants = { 50 | hidden: { opacity: 0, x: -30 }, 51 | show: { 52 | opacity: 1, 53 | x: 0, 54 | transition: { 55 | type: 'spring', 56 | damping: 100, 57 | }, 58 | }, 59 | } 60 | 61 | const iconVariants = { 62 | hidden: { opacity: 0, y: 30 }, 63 | show: { 64 | opacity: 1, 65 | y: 0, 66 | transition: { 67 | type: 'spring', 68 | damping: 100, 69 | }, 70 | }, 71 | } 72 | 73 | return ( 74 | 75 | {({ transitionStatus }) => { 76 | return ( 77 | 87 | 88 | 92 | Contact 93 | 94 |
95 | 100 | 101 | 102 | 103 | 108 | 109 | 110 | 111 | 116 | 117 | 118 | 119 | 124 | 125 | 126 | 127 | 132 | 133 | 134 |
135 | 136 | 140 | Back Home 141 | 142 | 143 |
144 | ) 145 | }} 146 |
147 | ) 148 | } 149 | 150 | export default contact 151 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { TransitionState } from 'gatsby-plugin-transition-link' 4 | import TransitionLinkDefault from '../components/utility/TransitionLinkDefault' 5 | 6 | import SEO from '../components/utility/seo' 7 | 8 | import { motion } from 'framer-motion' 9 | 10 | import './index.scss' 11 | 12 | const IndexPage = () => { 13 | const variants = { 14 | hidden: { 15 | y: 150, 16 | opacity: 0, 17 | transition: { 18 | type: 'spring', 19 | damping: 200, 20 | mass: 0.1, 21 | }, 22 | }, 23 | show: { 24 | y: 0, 25 | opacity: 1, 26 | transition: { 27 | type: 'spring', 28 | damping: 20, 29 | }, 30 | }, 31 | } 32 | 33 | return ( 34 |
35 | 36 | 37 | {({ transitionStatus }) => { 38 | return ( 39 | 49 |
50 | 51 | S 52 | 53 | tephen 54 | parling 55 |
56 | Fullstack Web Developer & Designer 57 |
58 |
    59 |
  • 60 | 64 | Projects 65 | 66 |
  • 67 |
  • 68 | 72 | About 73 | 74 |
  • 75 |
  • 76 | 80 | Contact 81 | 82 |
  • 83 |
84 |
85 |
86 | ) 87 | }} 88 |
89 |
90 | ) 91 | } 92 | 93 | export default IndexPage 94 | -------------------------------------------------------------------------------- /src/pages/index.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/styles.scss"; 2 | 3 | .heading { 4 | position: relative; 5 | 6 | &__text { 7 | position: relative; 8 | } 9 | 10 | 11 | &__top, &__bottom { 12 | margin-left: 8px; 13 | letter-spacing: 3px; 14 | 15 | @include breakpoint(sm) { 16 | letter-spacing: 4px; 17 | } 18 | 19 | @include breakpoint(lg) { 20 | letter-spacing: 8px; 21 | } 22 | 23 | @include breakpoint(xl) { 24 | letter-spacing: 15px; 25 | } 26 | } 27 | 28 | &__top { 29 | position: absolute; 30 | top: 100px; 31 | 32 | @include breakpoint(sm) { 33 | top: 160px; 34 | } 35 | 36 | @include breakpoint(lg) { 37 | top: 220px; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/pages/projects.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { TransitionState } from 'gatsby-plugin-transition-link' 4 | 5 | import SEO from '../components/utility/seo' 6 | 7 | import { motion } from 'framer-motion' 8 | 9 | import { graphql } from 'gatsby' 10 | 11 | import ProjectSnippet from '../components/ProjectSnippet' 12 | import ImageSwitcher from '../components/ImageSwitcher' 13 | 14 | const Projects = ({ data }) => { 15 | const variants = { 16 | hidden: { 17 | opacity: 0, 18 | x: -100, 19 | y: 0, 20 | }, 21 | show: { 22 | opacity: 1, 23 | x: 0, 24 | y: 0, 25 | transition: { 26 | y: { 27 | from: 0, 28 | }, 29 | type: 'spring', 30 | mass: 1, 31 | damping: 50, 32 | }, 33 | }, 34 | exit: { 35 | opacity: 0, 36 | y: 100, 37 | x: 0, 38 | transition: { 39 | type: 'spring', 40 | mass: 0.3, 41 | damping: 50, 42 | }, 43 | }, 44 | } 45 | 46 | const [activeImage, setActiveImage] = useState(null) 47 | 48 | const handleSetActiveImage = imageIndex => { 49 | setActiveImage(imageIndex) 50 | } 51 | 52 | const sortedNodes = () => 53 | data.allMarkdownRemark.edges.sort((a, b) => 54 | (a.node.frontmatter.position < b.node.frontmatter.position) ? -1 : 1 55 | ) 56 | 57 | const projects = () => 58 | sortedNodes().map(({ node }, i) => { 59 | const id = node.id 60 | const { title, blurb, image, maintech } = node.frontmatter 61 | const slug = node.fields.slug 62 | 63 | const imageFluid = image.childImageSharp.fluid 64 | 65 | return ( 66 | 76 | ) 77 | }) 78 | 79 | const sortedProjects = () => 80 | projects.sort((a, b) => (a.position < b.position) ? 1 : -1) 81 | 82 | const images = () => 83 | data.allMarkdownRemark.edges.map( 84 | ({ node }) => node.frontmatter.image.childImageSharp.fluid 85 | ) 86 | 87 | return ( 88 | 89 | {({ transitionStatus }) => ( 90 |
91 | 92 | 102 |

Projects

103 |
{projects()}
104 |
105 | 110 |
111 | )} 112 |
113 | ) 114 | } 115 | 116 | export const query = graphql` 117 | query MyQuery { 118 | allMarkdownRemark { 119 | edges { 120 | node { 121 | id 122 | fields { 123 | slug 124 | } 125 | frontmatter { 126 | title 127 | blurb 128 | maintech 129 | position 130 | image { 131 | childImageSharp { 132 | fluid(maxWidth: 1000) { 133 | ...GatsbyImageSharpFluid 134 | } 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | ` 143 | 144 | export default Projects 145 | -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | $breakpoint-sm: 640px; 2 | $breakpoint-md: 768px; 3 | $breakpoint-lg: 1024px; 4 | $breakpoint-xl: 1280px; 5 | 6 | :root { 7 | -webkit-overflow-scrolling: touch; 8 | } 9 | 10 | @mixin breakpoint($class) { 11 | @if $class == sm { 12 | @media (min-width: $breakpoint-sm) { 13 | @content; 14 | } 15 | } @else if $class == md { 16 | @media (min-width: $breakpoint-md) { 17 | @content; 18 | } 19 | } @else if $class == lg { 20 | @media (min-width: $breakpoint-lg) { 21 | @content; 22 | } 23 | } @else if $class == xl { 24 | @media (min-width: $breakpoint-xl) { 25 | @content; 26 | } 27 | } @else { 28 | @warn "Breakpoint mixin supports: xs, sm, md, lg"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/templates/project.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { TransitionState } from 'gatsby-plugin-transition-link' 4 | import TransitionLinkDefault from '../components/utility/TransitionLinkDefault' 5 | 6 | import SEO from '../components/utility/seo' 7 | 8 | import { motion } from 'framer-motion' 9 | 10 | import { graphql } from 'gatsby' 11 | import Img from 'gatsby-image' 12 | 13 | import './project.scss' 14 | 15 | const Project = ({ data }) => { 16 | const post = data.markdownRemark 17 | const { title, image, tags } = post.frontmatter 18 | 19 | const tagList = () => 20 | tags.split(',').map((tag, i) => { 21 | return ( 22 |
  • 26 | {tag} 27 |
  • 28 | ) 29 | }) 30 | 31 | const variants = { 32 | hidden: { 33 | opacity: 0, 34 | y: -100, 35 | }, 36 | show: { 37 | opacity: 1, 38 | x: 0, 39 | y: 0, 40 | transition: { 41 | type: 'spring', 42 | mass: 1, 43 | damping: 50, 44 | }, 45 | }, 46 | exit: { 47 | opacity: 0, 48 | y: 100, 49 | x: 0, 50 | transition: { 51 | type: 'spring', 52 | mass: 0.3, 53 | damping: 50, 54 | }, 55 | }, 56 | } 57 | 58 | return ( 59 | 60 | {({ transitionStatus }) => ( 61 | 71 | 72 |
    73 |

    74 | {title} 75 |

    76 |
    77 | 81 |
    82 |

    83 | Technologies Used 84 |

    85 |
      86 | {tagList()} 87 |
    88 |
    89 |
    90 |
    91 |

    92 | Description 93 |

    94 |
    98 | 108 | Back to Projects 109 | 110 |
    111 |
    112 | 113 | )} 114 | 115 | ) 116 | } 117 | 118 | export default Project 119 | 120 | export const query = graphql` 121 | query($slug: String!) { 122 | markdownRemark(fields: { slug: { eq: $slug } }) { 123 | html 124 | frontmatter { 125 | title 126 | tags 127 | image { 128 | childImageSharp { 129 | fluid(maxWidth: 1000) { 130 | ...GatsbyImageSharpFluid 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | ` 138 | -------------------------------------------------------------------------------- /src/templates/project.scss: -------------------------------------------------------------------------------- 1 | .project-text { 2 | & p { 3 | margin-bottom: 1.4rem; 4 | line-height: 1.65rem; 5 | } 6 | 7 | & a { 8 | text-decoration: underline; 9 | color: #38B2AC; 10 | font-weight: bold; 11 | } 12 | 13 | & li { 14 | margin-bottom: 7px; 15 | 16 | & a { 17 | font-weight: normal; 18 | } 19 | } 20 | 21 | & ul { 22 | margin-bottom: 2rem; 23 | } 24 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prefix: '', 3 | important: false, 4 | separator: ':', 5 | theme: { 6 | screens: { 7 | sm: '640px', 8 | md: '768px', 9 | lg: '1024px', 10 | xl: '1280px', 11 | }, 12 | colors: { 13 | transparent: 'transparent', 14 | 15 | black: '#000', 16 | white: '#fff', 17 | 18 | gray: { 19 | 100: '#f7fafc', 20 | 200: '#edf2f7', 21 | 300: '#e2e8f0', 22 | 400: '#cbd5e0', 23 | 500: '#a0aec0', 24 | 600: '#718096', 25 | 700: '#4a5568', 26 | 800: '#2d3748', 27 | 900: '#1a202c', 28 | }, 29 | red: { 30 | 100: '#fff5f5', 31 | 200: '#fed7d7', 32 | 300: '#feb2b2', 33 | 400: '#fc8181', 34 | 500: '#f56565', 35 | 600: '#e53e3e', 36 | 700: '#c53030', 37 | 800: '#9b2c2c', 38 | 900: '#742a2a', 39 | }, 40 | orange: { 41 | 100: '#fffaf0', 42 | 200: '#feebc8', 43 | 300: '#fbd38d', 44 | 400: '#f6ad55', 45 | 500: '#ed8936', 46 | 600: '#dd6b20', 47 | 700: '#c05621', 48 | 800: '#9c4221', 49 | 900: '#7b341e', 50 | }, 51 | yellow: { 52 | 100: '#fffff0', 53 | 200: '#fefcbf', 54 | 300: '#faf089', 55 | 400: '#f6e05e', 56 | 500: '#ecc94b', 57 | 600: '#d69e2e', 58 | 700: '#b7791f', 59 | 800: '#975a16', 60 | 900: '#744210', 61 | }, 62 | green: { 63 | 100: '#f0fff4', 64 | 200: '#c6f6d5', 65 | 300: '#9ae6b4', 66 | 400: '#68d391', 67 | 500: '#48bb78', 68 | 600: '#38a169', 69 | 700: '#2f855a', 70 | 800: '#276749', 71 | 900: '#22543d', 72 | }, 73 | teal: { 74 | 100: '#e6fffa', 75 | 200: '#b2f5ea', 76 | 300: '#81e6d9', 77 | 400: '#4fd1c5', 78 | 500: '#38b2ac', 79 | 600: '#319795', 80 | 700: '#2c7a7b', 81 | 800: '#285e61', 82 | 900: '#234e52', 83 | }, 84 | blue: { 85 | 100: '#ebf8ff', 86 | 200: '#bee3f8', 87 | 300: '#90cdf4', 88 | 400: '#63b3ed', 89 | 500: '#4299e1', 90 | 600: '#3182ce', 91 | 700: '#2b6cb0', 92 | 800: '#2c5282', 93 | 900: '#2a4365', 94 | }, 95 | indigo: { 96 | 100: '#ebf4ff', 97 | 200: '#c3dafe', 98 | 300: '#a3bffa', 99 | 400: '#7f9cf5', 100 | 500: '#667eea', 101 | 600: '#5a67d8', 102 | 700: '#4c51bf', 103 | 800: '#434190', 104 | 900: '#3c366b', 105 | }, 106 | purple: { 107 | 100: '#faf5ff', 108 | 200: '#e9d8fd', 109 | 300: '#d6bcfa', 110 | 400: '#b794f4', 111 | 500: '#9f7aea', 112 | 600: '#805ad5', 113 | 700: '#6b46c1', 114 | 800: '#553c9a', 115 | 900: '#44337a', 116 | }, 117 | pink: { 118 | 100: '#fff5f7', 119 | 200: '#fed7e2', 120 | 300: '#fbb6ce', 121 | 400: '#f687b3', 122 | 500: '#ed64a6', 123 | 600: '#d53f8c', 124 | 700: '#b83280', 125 | 800: '#97266d', 126 | 900: '#702459', 127 | }, 128 | }, 129 | spacing: { 130 | px: '1px', 131 | '0': '0', 132 | '1': '0.25rem', 133 | '2': '0.5rem', 134 | '3': '0.75rem', 135 | '4': '1rem', 136 | '5': '1.25rem', 137 | '6': '1.5rem', 138 | '8': '2rem', 139 | '10': '2.5rem', 140 | '12': '3rem', 141 | '16': '4rem', 142 | '20': '5rem', 143 | '24': '6rem', 144 | '32': '8rem', 145 | '40': '10rem', 146 | '48': '12rem', 147 | '56': '14rem', 148 | '64': '16rem', 149 | }, 150 | backgroundColor: theme => theme('colors'), 151 | backgroundPosition: { 152 | bottom: 'bottom', 153 | center: 'center', 154 | left: 'left', 155 | 'left-bottom': 'left bottom', 156 | 'left-top': 'left top', 157 | right: 'right', 158 | 'right-bottom': 'right bottom', 159 | 'right-top': 'right top', 160 | top: 'top', 161 | }, 162 | backgroundSize: { 163 | auto: 'auto', 164 | cover: 'cover', 165 | contain: 'contain', 166 | }, 167 | borderColor: theme => ({ 168 | ...theme('colors'), 169 | default: theme('colors.gray.300', 'currentColor'), 170 | }), 171 | borderRadius: { 172 | none: '0', 173 | sm: '0.125rem', 174 | default: '0.25rem', 175 | lg: '0.5rem', 176 | full: '9999px', 177 | }, 178 | borderWidth: { 179 | default: '1px', 180 | '0': '0', 181 | '2': '2px', 182 | '4': '4px', 183 | '8': '8px', 184 | }, 185 | boxShadow: { 186 | default: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', 187 | md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', 188 | lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', 189 | xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', 190 | '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', 191 | inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)', 192 | outline: '0 0 0 3px rgba(66, 153, 225, 0.5)', 193 | none: 'none', 194 | }, 195 | container: {}, 196 | cursor: { 197 | auto: 'auto', 198 | default: 'default', 199 | pointer: 'pointer', 200 | wait: 'wait', 201 | text: 'text', 202 | move: 'move', 203 | 'not-allowed': 'not-allowed', 204 | }, 205 | fill: { 206 | current: 'currentColor', 207 | }, 208 | flex: { 209 | '1': '1 1 0%', 210 | auto: '1 1 auto', 211 | initial: '0 1 auto', 212 | none: 'none', 213 | '100': '0 1 100%', 214 | '50': '0 1 50%', 215 | '25': '0 1 25%', 216 | '75': '0 1 75%', 217 | '30': '0 1 30%', 218 | '33': '0 1 33%' 219 | }, 220 | flexGrow: { 221 | '0': '0', 222 | default: '1', 223 | }, 224 | flexShrink: { 225 | '0': '0', 226 | default: '1', 227 | }, 228 | fontFamily: { 229 | sans: [ 230 | 'Raleway', 231 | '-apple-system', 232 | 'BlinkMacSystemFont', 233 | '"Segoe UI"', 234 | 'Roboto', 235 | '"Helvetica Neue"', 236 | 'Arial', 237 | '"Noto Sans"', 238 | 'sans-serif', 239 | '"Apple Color Emoji"', 240 | '"Segoe UI Emoji"', 241 | '"Segoe UI Symbol"', 242 | '"Noto Color Emoji"', 243 | ], 244 | serif: [ 245 | 'Georgia', 246 | 'Cambria', 247 | '"Times New Roman"', 248 | 'Times', 249 | 'serif', 250 | ], 251 | mono: [ 252 | 'Menlo', 253 | 'Monaco', 254 | 'Consolas', 255 | '"Liberation Mono"', 256 | '"Courier New"', 257 | 'monospace', 258 | ], 259 | }, 260 | fontSize: { 261 | xs: '0.75rem', 262 | sm: '0.875rem', 263 | base: '1rem', 264 | lg: '1.125rem', 265 | xl: '1.25rem', 266 | '2xl': '1.5rem', 267 | '3xl': '1.875rem', 268 | '4xl': '2.25rem', 269 | '5xl': '3rem', 270 | '6xl': '4rem', 271 | '7xl': '8rem', 272 | '8xl': '10rem', 273 | '9xl': '15rem', 274 | '10xl': '20rem', 275 | '11xl': '28rem', 276 | '12xl': '30rem' 277 | }, 278 | fontWeight: { 279 | hairline: '100', 280 | thin: '200', 281 | light: '300', 282 | normal: '400', 283 | medium: '500', 284 | semibold: '600', 285 | bold: '700', 286 | extrabold: '800', 287 | black: '900', 288 | }, 289 | height: theme => ({ 290 | auto: 'auto', 291 | ...theme('spacing'), 292 | full: '100%', 293 | screen: '100vh', 294 | }), 295 | inset: { 296 | '0': '0', 297 | auto: 'auto', 298 | }, 299 | letterSpacing: { 300 | tighter: '-0.05em', 301 | tight: '-0.025em', 302 | normal: '0', 303 | wide: '0.025em', 304 | wider: '0.05em', 305 | widest: '0.1em', 306 | }, 307 | lineHeight: { 308 | none: '1', 309 | tight: '1.25', 310 | snug: '1.375', 311 | normal: '1.5', 312 | relaxed: '1.625', 313 | loose: '2', 314 | }, 315 | listStyleType: { 316 | none: 'none', 317 | disc: 'disc', 318 | decimal: 'decimal', 319 | }, 320 | margin: (theme, { negative }) => ({ 321 | auto: 'auto', 322 | ...theme('spacing'), 323 | ...negative(theme('spacing')), 324 | }), 325 | maxHeight: { 326 | full: '100%', 327 | screen: '100vh', 328 | }, 329 | maxWidth: { 330 | xs: '20rem', 331 | sm: '24rem', 332 | md: '28rem', 333 | lg: '32rem', 334 | xl: '36rem', 335 | '2xl': '42rem', 336 | '3xl': '48rem', 337 | '4xl': '56rem', 338 | '5xl': '64rem', 339 | '6xl': '72rem', 340 | full: '100%', 341 | }, 342 | minHeight: { 343 | '0': '0', 344 | full: '100%', 345 | screen: '100vh', 346 | }, 347 | minWidth: { 348 | '0': '0', 349 | full: '100%', 350 | }, 351 | objectPosition: { 352 | bottom: 'bottom', 353 | center: 'center', 354 | left: 'left', 355 | 'left-bottom': 'left bottom', 356 | 'left-top': 'left top', 357 | right: 'right', 358 | 'right-bottom': 'right bottom', 359 | 'right-top': 'right top', 360 | top: 'top', 361 | }, 362 | opacity: { 363 | '0': '0', 364 | '25': '0.25', 365 | '50': '0.5', 366 | '75': '0.75', 367 | '100': '1', 368 | }, 369 | order: { 370 | first: '-9999', 371 | last: '9999', 372 | none: '0', 373 | '1': '1', 374 | '2': '2', 375 | '3': '3', 376 | '4': '4', 377 | '5': '5', 378 | '6': '6', 379 | '7': '7', 380 | '8': '8', 381 | '9': '9', 382 | '10': '10', 383 | '11': '11', 384 | '12': '12', 385 | }, 386 | padding: theme => theme('spacing'), 387 | placeholderColor: theme => theme('colors'), 388 | stroke: { 389 | current: 'currentColor', 390 | }, 391 | textColor: theme => theme('colors'), 392 | width: theme => ({ 393 | auto: 'auto', 394 | ...theme('spacing'), 395 | '1/2': '50%', 396 | '1/3': '33.333333%', 397 | '2/3': '66.666667%', 398 | '1/4': '25%', 399 | '2/4': '50%', 400 | '3/4': '75%', 401 | '1/5': '20%', 402 | '2/5': '40%', 403 | '3/5': '60%', 404 | '4/5': '80%', 405 | '1/6': '16.666667%', 406 | '2/6': '33.333333%', 407 | '3/6': '50%', 408 | '4/6': '66.666667%', 409 | '5/6': '83.333333%', 410 | '1/12': '8.333333%', 411 | '2/12': '16.666667%', 412 | '3/12': '25%', 413 | '4/12': '33.333333%', 414 | '5/12': '41.666667%', 415 | '6/12': '50%', 416 | '7/12': '58.333333%', 417 | '8/12': '66.666667%', 418 | '9/12': '75%', 419 | '10/12': '83.333333%', 420 | '11/12': '91.666667%', 421 | full: '100%', 422 | screen: '100vw', 423 | }), 424 | zIndex: { 425 | auto: 'auto', 426 | '0': '0', 427 | '10': '10', 428 | '20': '20', 429 | '30': '30', 430 | '40': '40', 431 | '50': '50', 432 | }, 433 | }, 434 | variants: { 435 | accessibility: ['responsive', 'focus'], 436 | alignContent: ['responsive'], 437 | alignItems: ['responsive'], 438 | alignSelf: ['responsive'], 439 | appearance: ['responsive'], 440 | backgroundAttachment: ['responsive'], 441 | backgroundColor: ['responsive', 'hover', 'focus'], 442 | backgroundPosition: ['responsive'], 443 | backgroundRepeat: ['responsive'], 444 | backgroundSize: ['responsive'], 445 | borderCollapse: ['responsive'], 446 | borderColor: ['responsive', 'hover', 'focus'], 447 | borderRadius: ['responsive'], 448 | borderStyle: ['responsive'], 449 | borderWidth: ['responsive'], 450 | boxShadow: ['responsive', 'hover', 'focus'], 451 | cursor: ['responsive'], 452 | display: ['responsive'], 453 | fill: ['responsive'], 454 | flex: ['responsive'], 455 | flexDirection: ['responsive'], 456 | flexGrow: ['responsive'], 457 | flexShrink: ['responsive'], 458 | flexWrap: ['responsive'], 459 | float: ['responsive'], 460 | fontFamily: ['responsive'], 461 | fontSize: ['responsive'], 462 | fontSmoothing: ['responsive'], 463 | fontStyle: ['responsive'], 464 | fontWeight: ['responsive', 'hover', 'focus'], 465 | height: ['responsive'], 466 | inset: ['responsive'], 467 | justifyContent: ['responsive'], 468 | letterSpacing: ['responsive'], 469 | lineHeight: ['responsive'], 470 | listStylePosition: ['responsive'], 471 | listStyleType: ['responsive'], 472 | margin: ['responsive'], 473 | maxHeight: ['responsive'], 474 | maxWidth: ['responsive'], 475 | minHeight: ['responsive'], 476 | minWidth: ['responsive'], 477 | objectFit: ['responsive'], 478 | objectPosition: ['responsive'], 479 | opacity: ['responsive', 'hover', 'focus'], 480 | order: ['responsive'], 481 | outline: ['responsive', 'focus'], 482 | overflow: ['responsive'], 483 | padding: ['responsive'], 484 | placeholderColor: ['responsive', 'focus'], 485 | pointerEvents: ['responsive'], 486 | position: ['responsive'], 487 | resize: ['responsive'], 488 | stroke: ['responsive'], 489 | tableLayout: ['responsive'], 490 | textAlign: ['responsive'], 491 | textColor: ['responsive', 'hover', 'focus'], 492 | textDecoration: ['responsive', 'hover', 'focus'], 493 | textTransform: ['responsive'], 494 | userSelect: ['responsive'], 495 | verticalAlign: ['responsive'], 496 | visibility: ['responsive'], 497 | whitespace: ['responsive'], 498 | width: ['responsive'], 499 | wordBreak: ['responsive'], 500 | zIndex: ['responsive'], 501 | }, 502 | corePlugins: {}, 503 | plugins: [], 504 | } 505 | --------------------------------------------------------------------------------