├── README.md
├── src
├── store
│ └── .gitkeep
├── css
│ ├── components.css
│ ├── utilities.css
│ ├── main.css
│ └── base.css
├── templates
│ └── .gitkeep
├── assets
│ └── fonts
│ │ ├── font-name-lowercase
│ │ └── .gitkeep
│ │ └── loader.css
├── types
│ ├── globals.d.ts
│ ├── index.ts
│ └── graphql.ts
├── config
│ ├── gatsby-ssr.ts
│ ├── gatsby-browser.ts
│ ├── fragments.ts
│ ├── gatsby-node.ts
│ └── gatsby-config.ts
├── data
│ └── navigation.ts
├── pages
│ ├── 404.tsx
│ └── index.tsx
├── components
│ ├── layout
│ │ ├── Layout.tsx
│ │ └── HtmlHead.tsx
│ ├── SEO.tsx
│ └── Links.tsx
└── utils
│ ├── useOnScreen.ts
│ ├── mixins.ts
│ └── helpers.ts
├── .nvmrc
├── .eslintignore
├── gatsby-node.js
├── gatsby-ssr.js
├── gatsby-browser.js
├── netlify.toml
├── .env.example
├── .prettierignore
├── .prettierrc
├── postcss.config.js
├── gatsby-config.js
├── tsconfig.json
├── .eslintrc.js
├── LICENSE
├── tailwind.config.js
├── .gitignore
└── package.json
/README.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/store/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v12.13.0
2 |
--------------------------------------------------------------------------------
/src/css/components.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .cache/
2 | public/
3 |
--------------------------------------------------------------------------------
/src/assets/fonts/font-name-lowercase/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/config/gatsby-node')
2 |
--------------------------------------------------------------------------------
/gatsby-ssr.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/config/gatsby-ssr')
2 |
--------------------------------------------------------------------------------
/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/config/gatsby-browser')
2 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "gatsby build"
3 | publish = "public/"
4 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | PRISMIC_REPOSITORY_NAME=reponame
2 | PRISMIC_ACCESS_TOKEN=RANDOM123TOKEN
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | src/types/graphql.ts
2 | .cache/
3 | public/
4 | node_modules/
5 | static/
6 |
--------------------------------------------------------------------------------
/src/css/utilities.css:
--------------------------------------------------------------------------------
1 | .ui-transition {
2 | transition: all 0.25s cubic-bezier(0.165, 0.84, 0.44, 1);
3 | }
4 |
--------------------------------------------------------------------------------
/src/types/globals.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: any
3 | export default content
4 | }
5 |
--------------------------------------------------------------------------------
/src/config/gatsby-ssr.ts:
--------------------------------------------------------------------------------
1 | import { GatsbySSR } from 'gatsby'
2 |
3 | const gatsbySsr: GatsbySSR = {}
4 |
5 | module.exports = gatsbySsr
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "useTabs": true,
5 | "endOfLine": "lf",
6 | "trailingComma": "all"
7 | }
8 |
--------------------------------------------------------------------------------
/src/config/gatsby-browser.ts:
--------------------------------------------------------------------------------
1 | import { GatsbyBrowser } from 'gatsby'
2 |
3 | const gatsbyBrowser: GatsbyBrowser = {}
4 |
5 | module.exports = gatsbyBrowser
6 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = () => ({
2 | plugins: [
3 | require('postcss-import'),
4 | require('tailwindcss'),
5 | require('autoprefixer')(),
6 | ],
7 | })
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
16 |
17 |
18 | {children}
19 |
20 | >
21 | )
22 | }
23 |
24 | export default Layout
25 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------