├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── netlify.toml ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── assets │ └── fonts │ │ ├── font-name-lowercase │ │ └── .gitkeep │ │ └── loader.css ├── components │ ├── Links.tsx │ ├── SEO.tsx │ └── layout │ │ ├── HtmlHead.tsx │ │ └── Layout.tsx ├── config │ ├── fragments.ts │ ├── gatsby-browser.ts │ ├── gatsby-config.ts │ ├── gatsby-node.ts │ └── gatsby-ssr.ts ├── css │ ├── base.css │ ├── components.css │ ├── main.css │ └── utilities.css ├── data │ └── navigation.ts ├── pages │ ├── 404.tsx │ └── index.tsx ├── store │ └── .gitkeep ├── templates │ └── .gitkeep ├── types │ ├── globals.d.ts │ ├── graphql.ts │ └── index.ts └── utils │ ├── helpers.ts │ ├── mixins.ts │ └── useOnScreen.ts ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | PRISMIC_REPOSITORY_NAME=reponame 2 | PRISMIC_ACCESS_TOKEN=RANDOM123TOKEN 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | public/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['react-app', 'plugin:import/typescript'], 3 | 4 | settings: { 5 | linkComponents: { name: 'Link', linkAttribute: 'to' }, 6 | }, 7 | 8 | rules: { 9 | '@typescript-eslint/no-angle-bracket-type-assertion': 'off', 10 | 'no-shadow': 'warn', 11 | 'import/no-useless-path-segments': 'warn', 12 | 'import/no-unresolved': 'error', 13 | 'jsx-a11y/alt-text': [ 14 | 'warn', 15 | { 16 | img: ['Img'], 17 | }, 18 | ], 19 | 'jsx-a11y/anchor-has-content': [ 20 | 'warn', 21 | { 22 | components: ['Link'], 23 | }, 24 | ], 25 | 'jsx-a11y/anchor-is-valid': [ 26 | 'warn', 27 | { 28 | components: ['Link', 'ExternalLink'], 29 | specialLink: ['to'], 30 | aspects: ['noHref', 'invalidHref'], 31 | }, 32 | ], 33 | 'jsx-a11y/img-redundant-alt': [ 34 | 'warn', 35 | { 36 | components: ['Img'], 37 | }, 38 | ], 39 | 'jsx-a11y/lang': 'error', 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /.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 variables file 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 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.13.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/types/graphql.ts 2 | .cache/ 3 | public/ 4 | node_modules/ 5 | static/ 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "useTabs": true, 5 | "endOfLine": "lf", 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Ryandi Tjia 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanditjia/gatsby-typescript-boilerplate/94e83afdaf4585dd20ecc063e5dbc45d4475fdd1/README.md -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/config/gatsby-browser') 2 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | // Duplicating these `requires` on gatsby-node causes error 2 | require('source-map-support').install() 3 | require('ts-node').register({ files: true }) 4 | 5 | module.exports = require('./src/config/gatsby-config') 6 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/config/gatsby-node') 2 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/config/gatsby-ssr') 2 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "gatsby build" 3 | publish = "public/" 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example.com", 3 | "version": "0.1.0", 4 | "description": "A simple starter to get up and developing quickly with Gatsby and TypeScript", 5 | "private": true, 6 | "author": "Ryandi Tjia ", 7 | "homepage": "https://example.com/", 8 | "license": "MIT", 9 | "keywords": [ 10 | "gatsby", 11 | "typescript", 12 | "emotion" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/ryanditjia/example.com.git" 17 | }, 18 | "scripts": { 19 | "start": "npm run dev", 20 | "dev": "gatsby develop --host 0.0.0.0", 21 | "build": "gatsby build", 22 | "serve": "gatsby serve", 23 | "clean": "gatsby clean", 24 | "gen": "apollo codegen:generate src/types/graphql.ts --endpoint=http://0.0.0.0:8000/___graphql --outputFlat --target=typescript --tagName=graphql --includes='src/**/*.{ts,tsx}' --no-addTypename --watch", 25 | "format": "prettier --write \"**/*.{js,ts,tsx}\"", 26 | "lint": "tsc --noEmit && eslint . --ext .js --ext .ts --ext .tsx" 27 | }, 28 | "dependencies": { 29 | "@emotion/core": "^10.0.22", 30 | "dotenv": "^8.2.0", 31 | "gatsby": "^2.18.6", 32 | "gatsby-image": "^2.2.34", 33 | "gatsby-plugin-canonical-urls": "^2.1.16", 34 | "gatsby-plugin-emotion": "^4.1.16", 35 | "gatsby-plugin-layout": "^1.1.16", 36 | "gatsby-plugin-netlify": "^2.1.27", 37 | "gatsby-plugin-nprogress": "^2.1.15", 38 | "gatsby-plugin-postcss": "^2.1.16", 39 | "gatsby-plugin-purgecss": "^4.0.1", 40 | "gatsby-plugin-react-helmet": "^3.1.16", 41 | "gatsby-plugin-sitemap": "^2.2.22", 42 | "gatsby-plugin-typescript": "^2.1.20", 43 | "gatsby-plugin-webpack-size": "^1.0.0", 44 | "gatsby-source-filesystem": "^2.1.40", 45 | "gatsby-transformer-remark": "^2.6.39", 46 | "react": "^16.12.0", 47 | "react-dom": "^16.12.0", 48 | "react-helmet": "^5.2.1" 49 | }, 50 | "devDependencies": { 51 | "@types/react": "^16.9.13", 52 | "@types/react-dom": "^16.9.4", 53 | "@types/react-helmet": "^5.0.14", 54 | "@typescript-eslint/eslint-plugin": "^2.9.0", 55 | "@typescript-eslint/parser": "^2.9.0", 56 | "apollo": "^2.21.1", 57 | "autoprefixer": "^9.7.3", 58 | "babel-eslint": "^10.0.3", 59 | "eslint": "^6.7.2", 60 | "eslint-config-react-app": "^5.0.2", 61 | "eslint-plugin-import": "^2.18.2", 62 | "eslint-plugin-jsx-a11y": "^6.2.3", 63 | "eslint-plugin-react": "^7.17.0", 64 | "eslint-plugin-react-hooks": "^2.3.0", 65 | "postcss-import": "^12.0.1", 66 | "prettier": "^1.19.1", 67 | "tailwind-css-variables": "^2.0.3", 68 | "tailwindcss": "^1.1.4", 69 | "ts-node": "^8.5.4", 70 | "typescript": "^3.7.2" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | plugins: [ 3 | require('postcss-import'), 4 | require('tailwindcss'), 5 | require('autoprefixer')(), 6 | ], 7 | }) 8 | -------------------------------------------------------------------------------- /src/assets/fonts/font-name-lowercase/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanditjia/gatsby-typescript-boilerplate/94e83afdaf4585dd20ecc063e5dbc45d4475fdd1/src/assets/fonts/font-name-lowercase/.gitkeep -------------------------------------------------------------------------------- /src/assets/fonts/loader.css: -------------------------------------------------------------------------------- 1 | /* FontNamePascalCase Book */ 2 | /* @font-face { 3 | font-family: 'FontNamePascalCase'; 4 | src: url('./font-name-lowercase/font-name-lowercase-book.woff2') 5 | format('woff2'), 6 | url('./font-name-lowercase/font-name-lowercase-book.woff') format('woff'); 7 | font-weight: 400; 8 | font-style: normal; 9 | font-display: swap; 10 | } */ 11 | 12 | /* FontNamePascalCase Book Italic */ 13 | /* @font-face { 14 | font-family: 'FontNamePascalCase'; 15 | src: url('./font-name-lowercase/font-name-lowercase-book-italic.woff2') 16 | format('woff2'), 17 | url('./font-name-lowercase/font-name-lowercase-book-italic.woff') 18 | format('woff'); 19 | font-weight: 400; 20 | font-style: italic; 21 | font-display: swap; 22 | } */ 23 | 24 | /* FontNamePascalCase Bold */ 25 | /* @font-face { 26 | font-family: 'FontNamePascalCase'; 27 | src: url('./font-name-lowercase/font-name-lowercase-bold.woff2') 28 | format('woff2'), 29 | url('./font-name-lowercase/font-name-lowercase-bold.woff') format('woff'); 30 | font-weight: 700; 31 | font-style: normal; 32 | font-display: swap; 33 | } */ 34 | 35 | /* FontNamePascalCase Bold Italic */ 36 | /* @font-face { 37 | font-family: 'FontNamePascalCase'; 38 | src: url('./font-name-lowercase/font-name-lowercase-bold-italic.woff2') 39 | format('woff2'), 40 | url('./font-name-lowercase/font-name-lowercase-bold-italic.woff') 41 | format('woff'); 42 | font-weight: 700; 43 | font-style: italic; 44 | font-display: swap; 45 | } */ 46 | -------------------------------------------------------------------------------- /src/components/Links.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Anchor, AnchorExcludeHref } from '../types' 3 | import { createPhoneNumber, createWhatsAppLink } from '../utils/helpers' 4 | 5 | type ExternalLinkProps = Anchor & { href: string; children: React.ReactNode } 6 | export function ExternalLink({ 7 | href, 8 | children, 9 | ...restProps 10 | }: ExternalLinkProps): React.ReactElement { 11 | return ( 12 | 13 | {children} 14 | 15 | ) 16 | } 17 | 18 | type MailtoLinkProps = AnchorExcludeHref & { 19 | email: string 20 | children: React.ReactNode 21 | } 22 | export function MailtoLink({ 23 | email, 24 | children, 25 | ...restProps 26 | }: MailtoLinkProps): React.ReactElement { 27 | return ( 28 | 29 | {children} 30 | 31 | ) 32 | } 33 | 34 | type PhoneLinkProps = AnchorExcludeHref & { 35 | phone: string 36 | children: React.ReactNode 37 | } 38 | export function PhoneLink({ 39 | phone, 40 | children, 41 | ...restProps 42 | }: PhoneLinkProps): React.ReactElement { 43 | return ( 44 | 45 | {children} 46 | 47 | ) 48 | } 49 | 50 | type WhatsAppLinkProps = AnchorExcludeHref & { 51 | phone: string 52 | text?: string 53 | children: React.ReactNode 54 | } 55 | export function WhatsAppLink({ 56 | phone, 57 | text, 58 | children, 59 | ...restProps 60 | }: WhatsAppLinkProps): React.ReactElement { 61 | return ( 62 | 68 | {children} 69 | 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/components/SEO.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Helmet from 'react-helmet' 3 | 4 | type Props = { 5 | title?: string 6 | metaDescription: string 7 | } 8 | 9 | const separator = '·' 10 | 11 | export function SEO({ 12 | title = '', 13 | metaDescription, 14 | }: Props): React.ReactElement { 15 | return ( 16 | 17 | 18 | {title && `${title} ${separator} `} 19 | Example Site - tagline 20 | 21 | {/* General tags */} 22 | 27 | {/* */} 28 | 29 | {/* Schema.org tags */} 30 | {/* */} 31 | 32 | {/* OpenGraph tags */} 33 | {/* */} 34 | {/* {postSEO ? : null} */} 35 | 36 | {/* */} 37 | 38 | {/* Twitter Card tags */} 39 | {/* */} 40 | {/* */} 41 | {/* */} 42 | {/* */} 43 | {/* */} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/components/layout/HtmlHead.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Helmet from 'react-helmet' 3 | 4 | export function HtmlHead(): React.ReactElement { 5 | return ( 6 | 7 | 8 | 12 | 13 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import '../../assets/fonts/loader.css' 3 | import '../../css/main.css' 4 | import { HtmlHead } from './HtmlHead' 5 | 6 | type Props = { 7 | children: React.ReactNode 8 | } 9 | 10 | function Layout({ children }: Props): React.ReactElement { 11 | return ( 12 | <> 13 | 14 | 15 |
Header
16 | 17 | 18 |
{children}
19 |
Footer
20 | 21 | ) 22 | } 23 | 24 | export default Layout 25 | -------------------------------------------------------------------------------- /src/config/fragments.ts: -------------------------------------------------------------------------------- 1 | // import { graphql } from 'gatsby' 2 | 3 | // export const fragments = graphql` 4 | // fragment SharpFluid on ImageSharpFluid { 5 | // base64 6 | // aspectRatio 7 | // src 8 | // srcSet 9 | // srcWebp 10 | // srcSetWebp 11 | // sizes 12 | // } 13 | // ` 14 | -------------------------------------------------------------------------------- /src/config/gatsby-browser.ts: -------------------------------------------------------------------------------- 1 | import { GatsbyBrowser } from 'gatsby' 2 | 3 | const gatsbyBrowser: GatsbyBrowser = {} 4 | 5 | module.exports = gatsbyBrowser 6 | -------------------------------------------------------------------------------- /src/config/gatsby-config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import { GatsbyConfig } from 'gatsby' 3 | 4 | dotenv.config() 5 | 6 | const gatsbyConfig: GatsbyConfig = { 7 | siteMetadata: { 8 | // read by gatsby-plugin-sitemap 9 | siteUrl: 'https://example.com/', 10 | title: 'Boilerplate for Gatsby + TypeScript', 11 | }, 12 | plugins: [ 13 | 'gatsby-plugin-typescript', 14 | 'gatsby-plugin-emotion', 15 | 'gatsby-plugin-react-helmet', 16 | 'gatsby-plugin-postcss', 17 | 'gatsby-transformer-remark', 18 | { 19 | resolve: 'gatsby-plugin-layout', 20 | options: { 21 | component: require.resolve('../components/layout/Layout.tsx'), 22 | }, 23 | }, 24 | { 25 | resolve: 'gatsby-plugin-nprogress', 26 | options: { 27 | color: 'rebeccapurple', 28 | showSpinner: false, 29 | trickle: true, 30 | minimum: 0.08, 31 | }, 32 | }, 33 | { 34 | resolve: 'gatsby-plugin-purgecss', 35 | options: { 36 | tailwind: true, 37 | }, 38 | }, 39 | { 40 | resolve: 'gatsby-plugin-canonical-urls', 41 | options: { 42 | // TODO: change this URL 43 | siteUrl: 'https://www.example.com', 44 | }, 45 | }, 46 | 'gatsby-plugin-webpack-size', 47 | 'gatsby-plugin-sitemap', 48 | 'gatsby-plugin-netlify', 49 | ], 50 | } 51 | 52 | module.exports = gatsbyConfig 53 | -------------------------------------------------------------------------------- /src/config/gatsby-node.ts: -------------------------------------------------------------------------------- 1 | import { GatsbyNode } from 'gatsby' 2 | import { fakeGraphQLTag as graphql } from '../utils/helpers' 3 | 4 | const gatsbyNode: GatsbyNode = { 5 | createPages: async ({ graphql: graphqlQuery, actions }) => { 6 | const { createPage } = actions 7 | 8 | const query = await graphqlQuery(graphql` 9 | query CreatePagesQuery { 10 | site { 11 | siteMetadata { 12 | siteUrl 13 | } 14 | } 15 | } 16 | `) 17 | 18 | console.log(query) 19 | console.log(createPage) 20 | }, 21 | } 22 | 23 | module.exports = gatsbyNode 24 | -------------------------------------------------------------------------------- /src/config/gatsby-ssr.ts: -------------------------------------------------------------------------------- 1 | import { GatsbySSR } from 'gatsby' 2 | 3 | const gatsbySsr: GatsbySSR = {} 4 | 5 | module.exports = gatsbySsr 6 | -------------------------------------------------------------------------------- /src/css/base.css: -------------------------------------------------------------------------------- 1 | html { 2 | /* prettier-ignore */ 3 | font-family: 4 | system-ui, 5 | -apple-system /* macOS 10.11-10.12 */, 6 | 'Segoe UI' /* Windows 6+ */, 7 | 'Roboto' /* Android 4+ */, 8 | 'Ubuntu' /* Ubuntu 10.10+ */, 9 | 'Cantarell' /* Gnome 3+ */, 10 | 'Noto Sans' /* KDE Plasma 5+ */, 11 | sans-serif /* fallback */, 12 | 'Apple Color Emoji' /* macOS emoji */, 13 | 'Segoe UI Emoji' /* Windows emoji */, 14 | 'Segoe UI Symbol' /* Windows emoji */, 15 | 'Noto Color Emoji' /* Linux emoji */; 16 | } 17 | 18 | @media (min-width: 640px) { 19 | html { 20 | font-size: 103.125%; 21 | } 22 | } 23 | 24 | @media (min-width: 960px) { 25 | html { 26 | font-size: 106.25%; 27 | } 28 | } 29 | 30 | @media (min-width: 1280px) { 31 | html { 32 | font-size: 109.375%; 33 | } 34 | } 35 | 36 | @media (min-width: 1600px) { 37 | html { 38 | font-size: 112.5%; 39 | } 40 | } 41 | 42 | /* body { 43 | background: #fbfbfc; 44 | @apply text-gray-700; 45 | } */ 46 | 47 | a, 48 | button { 49 | transition: all 0.1s ease; 50 | } 51 | 52 | abbr[title] { 53 | cursor: help; 54 | } 55 | 56 | summary { 57 | cursor: pointer; 58 | } 59 | -------------------------------------------------------------------------------- /src/css/components.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanditjia/gatsby-typescript-boilerplate/94e83afdaf4585dd20ecc063e5dbc45d4475fdd1/src/css/components.css -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | /* purgecss start ignore */ 2 | @import 'tailwindcss/base'; 3 | /* purgecss end ignore */ 4 | @import './base.css'; 5 | 6 | @import 'tailwindcss/components'; 7 | /* @import './components.css'; */ 8 | 9 | @import 'tailwindcss/utilities'; 10 | @import './utilities.css'; 11 | -------------------------------------------------------------------------------- /src/css/utilities.css: -------------------------------------------------------------------------------- 1 | .ui-transition { 2 | transition: all 0.25s cubic-bezier(0.165, 0.84, 0.44, 1); 3 | } 4 | -------------------------------------------------------------------------------- /src/data/navigation.ts: -------------------------------------------------------------------------------- 1 | import { NavItem } from '../types' 2 | 3 | export const mainNavMenu: NavItem[] = [ 4 | { 5 | id: 'home', 6 | text: 'Home', 7 | href: '/', 8 | }, 9 | { 10 | id: 'works', 11 | text: 'Works', 12 | href: '/works/', 13 | }, 14 | ] 15 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { SEO } from '../components/SEO' 2 | import React from 'react' 3 | 4 | function FourOhFourPage(): React.ReactElement { 5 | return ( 6 | <> 7 | 11 |

404: Not Found

12 | 13 | ) 14 | } 15 | 16 | export default FourOhFourPage 17 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { graphql } from 'gatsby' 2 | import React from 'react' 3 | import { SEO } from '../components/SEO' 4 | import { HomepageQuery } from '../types/graphql' 5 | 6 | export const query = graphql` 7 | query HomepageQuery { 8 | site { 9 | siteMetadata { 10 | title 11 | } 12 | } 13 | } 14 | ` 15 | 16 | type Props = { 17 | data: HomepageQuery 18 | } 19 | 20 | function Homepage({ data }: Props): React.ReactElement { 21 | return ( 22 | <> 23 | 24 | 25 |

{data!.site!.siteMetadata!.title}

26 | 27 | ) 28 | } 29 | 30 | export default Homepage 31 | -------------------------------------------------------------------------------- /src/store/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanditjia/gatsby-typescript-boilerplate/94e83afdaf4585dd20ecc063e5dbc45d4475fdd1/src/store/.gitkeep -------------------------------------------------------------------------------- /src/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanditjia/gatsby-typescript-boilerplate/94e83afdaf4585dd20ecc063e5dbc45d4475fdd1/src/templates/.gitkeep -------------------------------------------------------------------------------- /src/types/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: any 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /src/types/graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | // ==================================================== 6 | // GraphQL query operation: CreatePagesQuery 7 | // ==================================================== 8 | 9 | export interface CreatePagesQuery_site_siteMetadata { 10 | __typename: "SiteSiteMetadata"; 11 | siteUrl: string | null; 12 | } 13 | 14 | export interface CreatePagesQuery_site { 15 | __typename: "Site"; 16 | siteMetadata: CreatePagesQuery_site_siteMetadata | null; 17 | } 18 | 19 | export interface CreatePagesQuery { 20 | site: CreatePagesQuery_site | null; 21 | } 22 | 23 | /* tslint:disable */ 24 | /* eslint-disable */ 25 | // This file was automatically generated and should not be edited. 26 | 27 | // ==================================================== 28 | // GraphQL query operation: HomepageQuery 29 | // ==================================================== 30 | 31 | export interface HomepageQuery_site_siteMetadata { 32 | __typename: "SiteSiteMetadata"; 33 | title: string | null; 34 | } 35 | 36 | export interface HomepageQuery_site { 37 | __typename: "Site"; 38 | siteMetadata: HomepageQuery_site_siteMetadata | null; 39 | } 40 | 41 | export interface HomepageQuery { 42 | site: HomepageQuery_site | null; 43 | } 44 | 45 | /* tslint:disable */ 46 | /* eslint-disable */ 47 | // This file was automatically generated and should not be edited. 48 | 49 | //============================================================== 50 | // START Enums and Input Objects 51 | //============================================================== 52 | 53 | //============================================================== 54 | // END Enums and Input Objects 55 | //============================================================== 56 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface GraphQLNode { 2 | node: T 3 | } 4 | 5 | export interface GraphQLEdges { 6 | edges: GraphQLNode[] 7 | } 8 | 9 | export type Anchor = React.AnchorHTMLAttributes 10 | export type AnchorExcludeHref = Omit 11 | 12 | export interface NavItem { 13 | id: string 14 | text: string 15 | href: string 16 | } 17 | 18 | interface BaseFormData { 19 | 'form-name': string 20 | } 21 | 22 | export type FormDataWithFile = BaseFormData & Record 23 | export type FormDataWithoutFile = BaseFormData & Record 24 | -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { LinkGetProps } from '@reach/router' 2 | import { FormDataWithFile, FormDataWithoutFile, GraphQLEdges } from '../types' 3 | 4 | /* 5 | * merge classNames together 6 | */ 7 | export function cx(...args: Array) { 8 | return args 9 | .filter( 10 | cls => 11 | typeof cls !== 'boolean' && 12 | typeof cls !== 'undefined' && 13 | cls.trim() !== '', 14 | ) 15 | .join(' ') 16 | } 17 | 18 | /* 19 | * formal to how many decimal places 20 | */ 21 | export function numberFormat(val: number, decimalPlaces: number) { 22 | var multiplier = Math.pow(10, decimalPlaces) 23 | return Number( 24 | (Math.round(val * multiplier) / multiplier).toFixed(decimalPlaces), 25 | ) 26 | } 27 | 28 | /* 29 | * hex to rgb or rgba 30 | */ 31 | export function hexToRGB(hex: string, alpha: number = 1) { 32 | const r = parseInt(hex.slice(1, 3), 16) 33 | const g = parseInt(hex.slice(3, 5), 16) 34 | const b = parseInt(hex.slice(5, 7), 16) 35 | 36 | if (alpha < 0 || alpha > 1) { 37 | throw new Error('Alpha value must be between 0 and 1.') 38 | } 39 | 40 | return alpha === 1 41 | ? `rgb(${r}, ${g}, ${b})` 42 | : `rgba(${r}, ${g}, ${b}, ${alpha})` 43 | } 44 | 45 | /* 46 | * Check if email is valid 47 | */ 48 | export function isValidEmail(email: string) { 49 | return /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(email) 50 | } 51 | 52 | /* 53 | * Create valid tel: from Indonesia style of writing phone numbers 54 | */ 55 | export function createPhoneNumber({ 56 | phone, 57 | countryCode = '62', 58 | }: { 59 | phone: string 60 | countryCode?: string 61 | }) { 62 | const splitNumbers = 63 | phone 64 | .replace(/-|\s/g, '') // remove hyphens and whitespaces 65 | .match(/\d+/g) || [] // split into array of numbers (divided by non-numerical characters) 66 | 67 | // check if there’s any number at all 68 | if (splitNumbers.length > 0) { 69 | // get the first set of numbers 70 | const sanitizedNumber = splitNumbers.length > 0 ? splitNumbers[0] : '' 71 | 72 | if (sanitizedNumber.startsWith('0')) { 73 | // converting to number removes the leading 0 74 | return countryCode + Number(sanitizedNumber) 75 | } 76 | 77 | if (sanitizedNumber.startsWith(countryCode)) { 78 | // if starts with country code, return as is 79 | return sanitizedNumber 80 | } 81 | 82 | return countryCode + sanitizedNumber 83 | } 84 | 85 | throw new Error('Please pass a valid phone number') 86 | } 87 | 88 | /* 89 | *Create WhatsApp link from phone number, with optional text 90 | */ 91 | export function createWhatsAppLink({ 92 | phone, 93 | text, 94 | }: { 95 | phone: string 96 | text?: string 97 | }) { 98 | let link = `https://wa.me/${createPhoneNumber({ phone })}` 99 | 100 | if (text) { 101 | link += `?text=${encodeURIComponent(text)}` 102 | } 103 | 104 | return link 105 | } 106 | 107 | /* 108 | * https://reach.tech/router/api/Link 109 | * Set prop to Link 110 | */ 111 | export function setPartiallyCurrent({ 112 | href, 113 | isPartiallyCurrent, 114 | isCurrent, 115 | }: LinkGetProps) { 116 | if (isPartiallyCurrent && !isCurrent && href !== '/') { 117 | return { 118 | 'data-partially-current': true, 119 | } 120 | } 121 | 122 | return {} 123 | } 124 | 125 | /* 126 | * Extract data from Relay GraphQL style edge node 127 | */ 128 | export function extractNodes(arr: GraphQLEdges): T[] { 129 | return arr.edges.map(({ node }) => node) 130 | } 131 | 132 | /* 133 | * Helper for Gatsby’s createPages graphql query 134 | */ 135 | export function fakeGraphQLTag(query: TemplateStringsArray) { 136 | const tagArgs = arguments 137 | 138 | return tagArgs[0].reduce( 139 | (accumulator: string, string: string, index: number) => { 140 | accumulator += string 141 | if (index + 1 in tagArgs) accumulator += tagArgs[index + 1] 142 | return accumulator 143 | }, 144 | '', 145 | ) 146 | } 147 | 148 | /* 149 | * Encoding forms with attachments (input type file) 150 | */ 151 | function encodeWithFile(data: FormDataWithFile): FormData { 152 | const formData = new FormData() 153 | 154 | Object.entries(data).forEach(([key, value]) => { 155 | formData.append(key, value) 156 | }) 157 | 158 | return formData 159 | } 160 | 161 | /* 162 | * Encoding forms without attachments 163 | */ 164 | function encodeWithoutFile(data: FormDataWithoutFile): string { 165 | return Object.entries(data) 166 | .map( 167 | ([key, value]) => 168 | encodeURIComponent(key) + '=' + encodeURIComponent(value), 169 | ) 170 | .join('&') 171 | } 172 | 173 | function isFormDataWithFile( 174 | data: FormDataWithFile | FormDataWithoutFile, 175 | ): data is FormDataWithFile { 176 | return Object.values(data).some(value => value instanceof File) 177 | } 178 | 179 | /* 180 | * AJAX Netlify Forms submission 181 | */ 182 | export function submitFormToNetlify( 183 | data: FormDataWithFile | FormDataWithoutFile, 184 | ) { 185 | return fetch('/', { 186 | method: 'POST', 187 | ...(!isFormDataWithFile(data) && { 188 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 189 | }), 190 | body: isFormDataWithFile(data) 191 | ? encodeWithFile(data) 192 | : encodeWithoutFile(data), 193 | }) 194 | } 195 | -------------------------------------------------------------------------------- /src/utils/mixins.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/core' 2 | import { hexToRGB, numberFormat } from './helpers' 3 | 4 | const scrimStops = [ 5 | [1, 0], 6 | [0.738, 19], 7 | [0.541, 34], 8 | [0.382, 47], 9 | [0.278, 56.5], 10 | [0.194, 65], 11 | [0.126, 73], 12 | [0.075, 80.2], 13 | [0.042, 86.1], 14 | [0.021, 91], 15 | [0.008, 95.2], 16 | [0.002, 98.2], 17 | [0, 100], 18 | ].map(stop => ({ 19 | alpha: stop[0], 20 | stopPositionInPercent: stop[1], 21 | })) 22 | 23 | export function scrimGradient({ 24 | color, 25 | direction, 26 | startAlpha = 1, 27 | }: { 28 | color: string 29 | direction: string 30 | startAlpha?: number 31 | }) { 32 | const stopsWithRecomputedAlphas = scrimStops.map( 33 | ({ alpha, stopPositionInPercent }) => ({ 34 | alpha: numberFormat(alpha * startAlpha, 3), 35 | stopPositionInPercent, 36 | }), 37 | ) 38 | 39 | return css` 40 | background-image: linear-gradient( 41 | ${direction}, 42 | ${stopsWithRecomputedAlphas 43 | .map( 44 | ({ alpha, stopPositionInPercent }) => 45 | `${hexToRGB(color, alpha)} ${stopPositionInPercent}%`, 46 | ) 47 | .join(',')} 48 | ); 49 | ` 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/useOnScreen.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export function useOnScreen({ 4 | ref, 5 | rootMargin = '0px', 6 | initialState = false, 7 | }: { 8 | ref: React.RefObject 9 | rootMargin?: string 10 | initialState?: boolean 11 | }): boolean { 12 | const [isIntersecting, setIntersecting] = useState(initialState) 13 | 14 | useEffect(() => { 15 | const observedNode = ref.current 16 | 17 | const observer = new IntersectionObserver( 18 | ([entry]) => { 19 | setIntersecting(entry.isIntersecting) 20 | }, 21 | { 22 | rootMargin, 23 | }, 24 | ) 25 | 26 | if (observedNode) { 27 | observer.observe(observedNode) 28 | } 29 | 30 | return () => { 31 | if (observedNode) { 32 | observer.unobserve(observedNode) 33 | } 34 | } 35 | }, [ref, rootMargin]) 36 | 37 | return isIntersecting 38 | } 39 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | colors: { 5 | brand: '#364c8b', 6 | accent: { 7 | // generated by Adobe color wheel (monochromatic scheme) based on brand blue 8 | 200: '#b0c4ff', 9 | 400: '#638bff', 10 | default: '#4f70cc', 11 | 800: '#58627f', 12 | }, 13 | }, 14 | lineHeight: { 15 | tighter: 1.125, 16 | }, 17 | }, 18 | }, 19 | plugins: [ 20 | require('tailwind-css-variables')( 21 | { 22 | // modules 23 | // colors: 'color', 24 | screens: false, 25 | fontFamily: false, 26 | // fontSize: 'text', 27 | // fontWeight: 'font', 28 | // lineHeight: 'leading', 29 | // letterSpacing: 'tracking', 30 | backgroundSize: false, 31 | // borderWidth: 'border', 32 | // borderRadius: 'rounded', 33 | width: false, 34 | height: false, 35 | minWidth: false, 36 | minHeight: false, 37 | maxWidth: 'max-w', 38 | maxHeight: false, 39 | padding: false, 40 | margin: 'space', 41 | boxShadow: 'shadow', 42 | zIndex: false, 43 | opacity: false, 44 | }, 45 | { 46 | // options 47 | }, 48 | ), 49 | ], 50 | } 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"], 3 | 4 | "compilerOptions": { 5 | "module": "commonjs", 6 | "target": "esnext", 7 | "jsx": "preserve", 8 | "lib": ["dom", "esnext"], 9 | "strict": true, 10 | "noEmit": true, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true 15 | } 16 | } 17 | --------------------------------------------------------------------------------