├── .gitignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── gatsby-config.js
├── gatsby-node.js
├── gatsby-ssr.js
├── package-lock.json
├── package.json
└── src
├── components
├── ads
│ ├── ads-sidebar.js
│ └── style.js
├── blog-post-card
│ ├── blog-post-card.js
│ └── style.js
├── common
│ └── style.js
├── footer
│ ├── footer.js
│ └── style.js
├── header
│ ├── header.js
│ ├── search.js
│ ├── style.js
│ └── themeToggle.js
├── hero
│ ├── featured-post.js
│ ├── hero.js
│ └── style.js
├── latest-posts
│ ├── latest-posts.js
│ └── style.js
├── layout
│ ├── layout.css
│ └── layout.js
├── mobile-header
│ ├── hamburger.js
│ ├── mobile-header.js
│ ├── mobile-nav.js
│ ├── mobile-search.js
│ └── style.js
├── pagination
│ ├── pagination.js
│ └── style.js
└── seo
│ └── seo.js
├── images
├── demo-ad.png
├── favicon.png
├── icons
│ ├── chevron.svg
│ ├── icons.svg
│ ├── info.svg
│ └── logo.svg
├── logo.png
├── moon.svg
└── ogbanner.jpg
├── pages
├── 404.js
└── index.js
├── templates
├── BlogSingle.js
├── blog-page
│ ├── blog-page.js
│ ├── blog-posts.js
│ └── style.js
└── blog-post
│ ├── blog-post-image.js
│ ├── blog-post.js
│ └── style.js
└── utils
├── breakpoints.js
└── themeHelper.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-error.log*
6 | yarn-debug.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
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": false,
4 | "singleQuote": true,
5 | "tabWidth": 2,
6 | "trailingComma": "none",
7 | "arrowParens": "avoid",
8 | "bracketSpacing": false
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Rahul Raj
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 |
3 | Flat Magazine Blog
4 |
5 |
6 |
7 | A flat design Gatsby Starter powered by Sanity.
8 |
9 |
10 | ## ⚡ Features
11 |
12 | - [Sanity.io](https://www.sanity.io/) integration
13 | - Styled with [styled-components](https://styled-components.com/)
14 | - Search Support for Posts with [elasticlunr-search](https://www.gatsbyjs.org/packages/@gatsby-contrib/gatsby-plugin-elasticlunr-search)
15 | - Dark Mode
16 | - Featured Posts Section
17 | - Pagination
18 | - Home Page, Blog Archive Page, Blog Post Page
19 | - Automatic [XML Sitemap](https://www.gatsbyjs.org/packages/gatsby-plugin-sitemap) and [Robots.txt ](https://www.gatsbyjs.org/packages/gatsby-plugin-robots-txt) genration
20 | - Responsive design
21 |
22 | ## 🚀 Quick Start
23 |
24 | #### Create a Gatsby site
25 |
26 | Use the Gatsby CLI to create a new site, specifying the Flat Magazine starter.
27 |
28 | ```sh
29 | # Create a new Gatsby site using the Flat Magazine starter
30 | gatsby new blog https://github.com/damnitrahul/gatsby-starter-flat-magazine
31 | ```
32 |
33 | Supply your sanity.io tokens to `gatsby-source-sanity` options in `gatsby-config.js`
34 |
35 | ```
36 | {
37 | resolve: 'gatsby-source-sanity',
38 | options: {
39 | projectId: [YOUR_SANITY_PROJECT_ID],
40 | dataset: [YOUR_SANITY_PROJECT_DATASET],
41 | token: [YOUR_SANITY_TOKEN]
42 | }
43 | ```
44 |
45 | ### Clone the customised Sanity Studio for Flat Magazine [HERE](https://github.com/damnitrahul/sanity-studio-flat-magazine)
46 |
47 | Setup the Studio with your credential, Deploy GraphQL API and Start publishing your content
48 |
49 | #### Start Developing
50 |
51 | Navigate into your new site’s directory and start it up.
52 |
53 | ```sh
54 | cd blog
55 | gatsby develop
56 | ```
57 |
58 | #### Open the source code and start editing!
59 |
60 | Your site is now running at `http://localhost:8000`!
61 |
62 | Note: You'll also see a second link: `http://localhost:8000/___graphql`. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql).
63 |
64 | Open the `blog` directory in your code editor of choice and edit `src/templates/index-template.js`. Save your changes and the browser will update in real time!
65 |
66 | ## 💫 Deploy with Netlify
67 |
68 | ### Current deployment status
69 |
70 | [](https://app.netlify.com/sites/flat-magazine/deploys)
71 |
72 | [Netlify](https://netlify.com) CMS can run in any frontend web environment, but the quickest way to try it out is by running it on a pre-configured starter site with Netlify. Use the button below to build and deploy your own copy of the repository:
73 |
74 |
75 |
76 | After clicking that button, you’ll authenticate with GitHub and choose a repository name. Netlify will then automatically create a repository in your GitHub account with a copy of the files from the template. Next, it will build and deploy the new site on Netlify, bringing you to the site dashboard when the build is complete.
77 |
--------------------------------------------------------------------------------
/gatsby-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteMetadata: {
3 | title: `Flat Magazine`,
4 | description: `A Blog Theme for Gatsby. Using Sanity.io as backend.`,
5 | author: `@damnitrahul`,
6 | siteURL: 'https://blog.damnitrahul.com',
7 | siteUrl: 'https://blog.damnitrahul.com'
8 | },
9 | plugins: [
10 | `gatsby-plugin-react-helmet`,
11 | {
12 | resolve: `gatsby-source-filesystem`,
13 | options: {
14 | name: `images`,
15 | path: `${__dirname}/src/images`
16 | }
17 | },
18 | `gatsby-plugin-sharp`,
19 | `gatsby-transformer-sharp`,
20 | {
21 | resolve: `gatsby-plugin-manifest`,
22 | options: {
23 | name: `gatsby-starter-default`,
24 | short_name: `starter`,
25 | start_url: `/`,
26 | background_color: `#663399`,
27 | theme_color: `#663399`,
28 | display: `minimal-ui`,
29 | icon: `src/images/favicon.png` // This path is relative to the root of the site.
30 | }
31 | },
32 | {
33 | resolve: 'gatsby-source-sanity',
34 | options: {
35 | projectId: process.env.GATSBY_PROJECT_ID, // Put your credentials
36 | dataset: process.env.GATSBY_PROJECT_DATASET,
37 | token: process.env.GATSBY_MY_SANITY_TOKEN
38 | }
39 | },
40 | {
41 | resolve: `@gatsby-contrib/gatsby-plugin-elasticlunr-search`,
42 | options: {
43 | // Fields to index
44 | fields: [`title`, `slug`],
45 | // How to resolve each field`s value for a supported node type
46 | resolvers: {
47 | // For any node of type MarkdownRemark, list how to resolve the fields` values
48 | SanityPost: {
49 | title: node => node.title,
50 | slug: node => node.slug.current
51 | }
52 | }
53 | }
54 | },
55 | {
56 | resolve: 'gatsby-plugin-page-progress',
57 | options: {
58 | includePaths: [{regex: '^/blog/'}],
59 | height: 3,
60 | prependToBody: false,
61 | color: `#fd413c`,
62 | footerHeight: 500
63 | }
64 | },
65 | 'gatsby-plugin-styled-components',
66 | 'gatsby-plugin-sitemap',
67 | 'gatsby-plugin-robots-txt'
68 | ]
69 | }
70 |
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Implement Gatsby's Node APIs in this file.
3 | *
4 | * See: https://www.gatsbyjs.org/docs/node-apis/
5 | */
6 |
7 | // You can delete this file if you're not using it
8 | const path = require('path')
9 | module.exports.createPages = async ({graphql, actions}) => {
10 | const {createPage} = actions
11 | const blogTemplate = path.resolve('./src/templates/blog-post/blog-post.js')
12 | const blogPageTemplate = path.resolve(
13 | './src/templates/blog-page/blog-page.js'
14 | )
15 |
16 | const res = await graphql(`
17 | query MyQuery {
18 | allSanityPost {
19 | totalCount
20 | edges {
21 | node {
22 | title
23 | slug {
24 | current
25 | }
26 | }
27 | }
28 | }
29 | }
30 | `)
31 |
32 | res.data.allSanityPost.edges.forEach((edge, i, arr) => {
33 | createPage({
34 | path: `/blog/${edge.node.slug.current}`,
35 | component: blogTemplate,
36 | context: {
37 | prev:
38 | i === 0 ? arr[i + 2].node.slug.current : arr[i - 1].node.slug.current,
39 | prevTitle: i === 0 ? arr[i + 2].node.title : arr[i - 1].node.title,
40 | slug: edge.node.slug.current,
41 | next:
42 | i === arr.length - 1
43 | ? arr[i - 2].node.slug.current
44 | : arr[i + 1].node.slug.current,
45 | nextTitle:
46 | i === arr.length - 1 ? arr[i - 2].node.title : arr[i + 1].node.title
47 | }
48 | })
49 | })
50 |
51 | const pages = Math.ceil(res.data.allSanityPost.totalCount / 10)
52 | Array.from({length: pages}).forEach((_, i) => {
53 | createPage({
54 | path: i === 0 ? `/blog` : `/blog/${i + 1}`,
55 | component: blogPageTemplate,
56 | context: {
57 | skip: i * 10,
58 | current: i + 1,
59 | pages: pages
60 | }
61 | })
62 | })
63 | }
64 |
--------------------------------------------------------------------------------
/gatsby-ssr.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const InitialTheme = () => {
4 | let codeToRunOnClient = `
5 | (function() {
6 | function getInitialColorMode() {
7 | const persistedColorPreference = window.localStorage.getItem('color-mode');
8 | const hasPersistedPreference = typeof persistedColorPreference === 'string';
9 |
10 | if (hasPersistedPreference) {
11 | return persistedColorPreference;
12 | }
13 |
14 | const mql = window.matchMedia('(prefers-color-scheme: dark)');
15 | const hasMediaQueryPreference = typeof mql.matches === 'boolean';
16 | if (hasMediaQueryPreference) {
17 | return mql.matches ? 'dark' : 'light';
18 | }
19 | return 'light';
20 | }
21 | const colorMode = getInitialColorMode();
22 | const root = document.documentElement;
23 | root.setAttribute('data-theme', colorMode)
24 | })()`
25 | return
26 | }
27 |
28 | export const onRenderBody = ({setPreBodyComponents}) => {
29 | setPreBodyComponents()
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-starter-flat-magazine",
3 | "private": true,
4 | "description": "A blog starter for Gatsby. Using Sanity.io as backend.",
5 | "version": "0.1.0",
6 | "author": "Rahul Raj ",
7 | "dependencies": {
8 | "@gatsby-contrib/gatsby-plugin-elasticlunr-search": "^2.3.0",
9 | "@sanity/block-content-to-react": "^2.0.7",
10 | "@sanity/image-url": "^0.140.18",
11 | "babel-plugin-styled-components": "^1.10.7",
12 | "env-cmd": "^10.1.0",
13 | "gatsby": "^2.21.0",
14 | "gatsby-image": "^2.4.0",
15 | "gatsby-plugin-manifest": "^2.4.0",
16 | "gatsby-plugin-offline": "^3.2.0",
17 | "gatsby-plugin-page-progress": "^2.1.0",
18 | "gatsby-plugin-react-helmet": "^3.3.0",
19 | "gatsby-plugin-robots-txt": "^1.5.1",
20 | "gatsby-plugin-sharp": "^2.6.0",
21 | "gatsby-plugin-sitemap": "^2.4.11",
22 | "gatsby-plugin-styled-components": "^3.3.2",
23 | "gatsby-source-filesystem": "^2.3.0",
24 | "gatsby-source-sanity": "^6.0.4",
25 | "gatsby-transformer-sharp": "^2.5.0",
26 | "prop-types": "^15.7.2",
27 | "react": "^16.12.0",
28 | "react-dom": "^16.12.0",
29 | "react-helmet": "^6.0.0",
30 | "react-intersection-observer": "^8.26.2",
31 | "react-syntax-highlighter": "^13.3.1",
32 | "styled-components": "^5.1.0"
33 | },
34 | "devDependencies": {
35 | "prettier": "2.0.5"
36 | },
37 | "keywords": [
38 | "gatsby"
39 | ],
40 | "license": "MIT",
41 | "scripts": {
42 | "build": "gatsby build",
43 | "develop": "env-cmd -f .env.development gatsby develop",
44 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"",
45 | "start": "npm run develop",
46 | "serve": "gatsby serve",
47 | "clean": "gatsby clean",
48 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
49 | },
50 | "repository": {
51 | "type": "git",
52 | "url": "https://github.com/gatsbyjs/gatsby-starter-default"
53 | },
54 | "bugs": {
55 | "url": "https://github.com/gatsbyjs/gatsby/issues"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/ads/ads-sidebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {AdContainer} from './style'
3 | import {useStaticQuery, graphql} from 'gatsby'
4 | import Image from 'gatsby-image'
5 |
6 | // Sidebar Ad Component
7 |
8 | function AdsSidebar() {
9 | const data = useStaticQuery(graphql`
10 | query {
11 | file(relativePath: {eq: "demo-ad.png"}) {
12 | childImageSharp {
13 | fluid {
14 | ...GatsbyImageSharpFluid
15 | }
16 | }
17 | }
18 | }
19 | `)
20 |
21 | return (
22 |
23 |
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default AdsSidebar
42 |
--------------------------------------------------------------------------------
/src/components/ads/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const AdContainer = styled.aside`
4 | padding: 1rem 0;
5 | `;
6 | export { AdContainer };
7 |
--------------------------------------------------------------------------------
/src/components/blog-post-card/blog-post-card.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {BlogPostBox} from './style'
3 | import Image from 'gatsby-image'
4 | import {Link} from 'gatsby'
5 |
6 | function BlogPostCard({title, excerpt, author, date, fluid, slug}) {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | {title}
15 |
16 |
{excerpt}
17 |
18 | By {author} • {date}
19 |
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default BlogPostCard
28 |
--------------------------------------------------------------------------------
/src/components/blog-post-card/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import {media} from '../../utils/breakpoints'
3 |
4 | const BlogPostBox = styled.div`
5 | margin: 2rem 0;
6 |
7 | div.post {
8 | h2 {
9 | font-size: 1.2rem;
10 | span {
11 | transition: box-shadow 350ms cubic-bezier(0.68, -0.1, 0.265, 1.55);
12 | }
13 | }
14 | .gatsby-image-wrapper {
15 | height: 220px;
16 | border-radius: 8px;
17 | overflow: hidden;
18 | margin-bottom: 1rem;
19 | }
20 | div.post-details {
21 | display: flex;
22 | flex-direction: column;
23 | p {
24 | padding: 1rem 0 0.5rem;
25 | }
26 | div {
27 | color: var(--text-muted);
28 | }
29 | }
30 |
31 | &:hover {
32 | h2 {
33 | span {
34 | box-shadow: inset 0 -10px 0 var(--highlight);
35 | }
36 | }
37 | }
38 | ${media.md} {
39 | display: flex;
40 | justify-content: start;
41 | align-items: flex-start;
42 | height: 100%;
43 | h2 {
44 | font-size: 1.5rem;
45 | }
46 | .gatsby-image-wrapper {
47 | height: 180px;
48 | margin-bottom: 0;
49 | min-width: 280px;
50 | width: 280px;
51 | img {
52 | display: block;
53 | }
54 | }
55 | div.post-details {
56 | padding: 0 2rem;
57 | }
58 | }
59 | }
60 | `
61 |
62 | export {BlogPostBox}
63 |
--------------------------------------------------------------------------------
/src/components/common/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import {media} from '../../utils/breakpoints'
3 |
4 | const Container = styled.div`
5 | max-width: ${props => props.width || '1240px'};
6 | padding-right: 16px;
7 | padding-left: 16px;
8 | margin-right: auto;
9 | margin-left: auto;
10 | width: 100%;
11 | `
12 |
13 | const BlogPostCols = styled.div`
14 | & > div:last-child {
15 | display: none;
16 | }
17 | ${media.lg} {
18 | display: grid;
19 | grid-template-columns: minmax(0, 1fr) 250px;
20 | grid-gap: 2rem;
21 | & > div:last-child {
22 | display: block;
23 | }
24 | }
25 | `
26 |
27 | export {Container, BlogPostCols}
28 |
--------------------------------------------------------------------------------
/src/components/footer/footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {FooterContainer} from './style'
3 | import {Container} from '../common/style'
4 | import icon from '../../images/icons/icons.svg'
5 | import {Link} from 'gatsby'
6 |
7 | function Footer() {
8 | return (
9 |
10 |
11 |
62 |
63 |
Copyright © Rahul Raj
64 |
65 |
66 |
67 | )
68 | }
69 |
70 | export default Footer
71 |
--------------------------------------------------------------------------------
/src/components/footer/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import {media} from '../../utils/breakpoints'
3 |
4 | const FooterContainer = styled.footer`
5 | div.top-footer {
6 | display: flex;
7 | flex-direction: column-reverse;
8 | .social {
9 | margin: 0 auto;
10 | a {
11 | display: inline-block;
12 | margin: 1rem;
13 | text-align: center;
14 | svg {
15 | fill: var(--text-light);
16 | width: 22px;
17 | height: 22px;
18 | transition: fill 400ms ease;
19 | }
20 | &:hover {
21 | svg {
22 | fill: var(--text-muted);
23 | }
24 | }
25 | }
26 | }
27 | .links {
28 | margin: 0 auto;
29 | a {
30 | color: var(--text-light);
31 | display: block;
32 | transition: color 400ms ease;
33 | text-align: center;
34 | padding: 1rem;
35 | &:hover {
36 | color: var(--text-muted);
37 | }
38 | }
39 | }
40 | }
41 |
42 | div.bottom-footer {
43 | text-align: center;
44 | padding-bottom: 2rem;
45 | padding-top: 1rem;
46 | font-size: 0.8rem;
47 | color: var(--text-light);
48 | }
49 |
50 | ${media.md} {
51 | div.top-footer {
52 | flex-direction: row;
53 | justify-content: space-between;
54 | align-items: center;
55 | .links {
56 | display: flex;
57 | margin: 0;
58 | }
59 | .social {
60 | margin: 0;
61 | a {
62 | &:first-child {
63 | margin: 1rem 1rem 1rem 0;
64 | svg {
65 | height: 40px;
66 | width: 40px;
67 | }
68 | }
69 | }
70 | }
71 | }
72 | }
73 | `
74 | export {FooterContainer}
75 |
--------------------------------------------------------------------------------
/src/components/header/header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Search from './search'
3 | import ThemeToggle from './themeToggle'
4 | import {NavHeader, NavBar, NavGroup, Logo, SearchGroup} from './style'
5 | import logo from '../../images/logo.png'
6 | import {Link} from 'gatsby'
7 | import {Container} from '../common/style'
8 |
9 | function Header() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | -
22 |
23 | Latest
24 |
25 |
26 | -
27 |
28 | Blog
29 |
30 |
31 | -
32 |
37 | Contact
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | )
50 | }
51 |
52 | export default Header
53 |
--------------------------------------------------------------------------------
/src/components/header/search.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react'
2 | import {graphql, useStaticQuery, Link} from 'gatsby'
3 | import {Index} from 'elasticlunr'
4 | import {SearchBox, SearchResults} from './style'
5 | import icon from '../../images/icons/icons.svg'
6 |
7 | function Search() {
8 | const [isOpen, setOpen] = useState(false)
9 | const [state, setState] = useState({
10 | query: ``,
11 | results: [],
12 | isActive: false
13 | })
14 |
15 | const data = useStaticQuery(graphql`
16 | query {
17 | siteSearchIndex {
18 | index
19 | }
20 | }
21 | `)
22 |
23 | let index = null
24 | const getOrCreateIndex = () =>
25 | index ? index : Index.load(data.siteSearchIndex.index)
26 |
27 | const search = evt => {
28 | const query = evt.target.value
29 | index = getOrCreateIndex()
30 | setState({
31 | query,
32 | // Query the index with search string to get an [] of IDs
33 | results: index
34 | .search(query, {expand: true}) // Accept partial matches
35 | // Map over each ID and return the full document
36 | .map(({ref}) => index.documentStore.getDoc(ref)),
37 | isActive: !!query
38 | })
39 | }
40 | const handleSearchInput = e => {
41 | if (e.target.value === '') {
42 | setOpen(false)
43 | }
44 | setState(st => ({
45 | ...st,
46 | isActive: false
47 | }))
48 | }
49 |
50 | return (
51 |
52 |
68 | {state.results.length > 0 && (
69 |
70 | {state.results.map(page => (
71 |
77 | {page.title}
78 |
79 | ))}
80 |
81 | )}
82 |
83 | )
84 | }
85 |
86 | export default Search
87 |
--------------------------------------------------------------------------------
/src/components/header/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import {media} from '../../utils/breakpoints'
3 | const NavHeader = styled.header`
4 | padding: 1.5rem 0;
5 | position: sticky;
6 | top: 0;
7 | background-color: var(--white);
8 | z-index: 99;
9 | position: relative;
10 | display: none;
11 | ${media.md} {
12 | display: block;
13 | }
14 | `
15 |
16 | const NavBar = styled.header`
17 | display: flex;
18 | justify-content: space-between;
19 | align-items: center;
20 | `
21 |
22 | const NavGroup = styled.nav`
23 | display: flex;
24 | justify-content: space-between;
25 | align-items: center;
26 | ul {
27 | display: flex;
28 | justify-content: space-between;
29 | align-items: center;
30 | list-style: none;
31 | padding-left: 2rem;
32 | li a {
33 | font-family: 'Jost';
34 | padding: 1rem;
35 | font-size: 1.3rem;
36 | text-decoration: none;
37 | color: var(--text-dark);
38 | &.active {
39 | span {
40 | font-weight: 600;
41 | box-shadow: inset 0 -2px 0 var(--highlight);
42 | }
43 | }
44 | span {
45 | transition: box-shadow 350ms ease;
46 | }
47 | &:hover {
48 | span {
49 | box-shadow: inset 0 -24px 0 var(--highlight);
50 | }
51 | }
52 | }
53 | }
54 | `
55 |
56 | const SearchGroup = styled.div`
57 | display: flex;
58 | justify-content: space-between;
59 | align-items: center;
60 | list-style: none;
61 | `
62 |
63 | const SearchBox = styled.div`
64 | display: flex;
65 | justify-content: space-between;
66 | align-items: center;
67 | position: relative;
68 | label {
69 | border: none;
70 | border-radius: 5px;
71 | overflow: hidden;
72 | height: 40px;
73 | padding: 0 0.5rem;
74 | margin-left: auto;
75 | border: none;
76 | cursor: pointer;
77 | &:focus,
78 | &:active,
79 | &:focus-within {
80 | box-shadow: 0 0 2px 4px var(--highlight-shadow);
81 | }
82 | input {
83 | font-size: 1.1rem;
84 | padding: 0;
85 | border: none;
86 | background-color: transparent;
87 | height: 100%;
88 | width: ${({open}) => (open ? 300 : 0)}px;
89 | transition: width 300ms ease, padding 300ms ease;
90 | padding: ${({open}) => (open ? '0.5rem' : 0)};
91 | color: var(--text-dark);
92 | &:focus,
93 | &:active {
94 | outline: none;
95 | }
96 | }
97 | svg {
98 | fill: var(--text-dark);
99 | padding: 0;
100 | width: 1.5rem;
101 | height: 100%;
102 | line-height: 2.5rem;
103 | vertical-align: middle;
104 | transition: fill 300ms ease;
105 | }
106 | &:hover,
107 | &:focus,
108 | &:active,
109 | &:focus-within {
110 | svg {
111 | fill: var(--orange);
112 | }
113 | }
114 | }
115 | &:focus-within {
116 | div {
117 | display: block;
118 | }
119 | }
120 | `
121 | const SearchResults = styled.div`
122 | position: absolute;
123 | display: ${({active}) => !active && 'none'};
124 | z-index: 3;
125 | left: 0;
126 | right: 0;
127 | top: 100%;
128 | background: var(--white);
129 | border-radius: 0 0 5px 5px;
130 | max-height: 300px;
131 | overflow-y: auto;
132 | border: 1px solid var(--text-light);
133 | box-shadow: 0 2px 6px var(--highlight-shadow);
134 | a {
135 | padding: 0.5rem 1rem;
136 | display: block;
137 | border-bottom: 1px solid var(--text-light);
138 | min-height: 50px;
139 | display: flex;
140 | justify-content: flex-start;
141 | align-items: center;
142 | span {
143 | display: block;
144 | }
145 | &:hover {
146 | color: var(--orange);
147 | }
148 | &:last-child {
149 | border-bottom: none;
150 | }
151 | }
152 | `
153 |
154 | const Logo = styled.div`
155 | max-width: 160px;
156 | img {
157 | width: 100%;
158 | }
159 | `
160 |
161 | const ToggleIcon = styled.div`
162 | width: 2.35rem;
163 |
164 | cursor: pointer;
165 | svg {
166 | fill: var(--text-dark);
167 | height: 25px;
168 | width: 100%;
169 | line-height: 2.5rem;
170 | vertical-align: middle;
171 | transition: fill 300ms ease;
172 | }
173 | &:hover {
174 | svg {
175 | fill: var(--orange);
176 | }
177 | }
178 | &.mobile {
179 | svg {
180 | display: block;
181 | margin-right: auto;
182 | width: 2rem;
183 | }
184 | }
185 | ${media.md} {
186 | margin-left: 1.5rem;
187 | padding: 0.1rem;
188 | }
189 | `
190 |
191 | export {
192 | NavHeader,
193 | NavBar,
194 | NavGroup,
195 | SearchGroup,
196 | Logo,
197 | SearchBox,
198 | ToggleIcon,
199 | SearchResults
200 | }
201 |
--------------------------------------------------------------------------------
/src/components/header/themeToggle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import icon from '../../images/icons/icons.svg'
3 | import {ToggleIcon} from './style'
4 | import {useState} from 'react'
5 | import {getInitialColorMode} from '../../utils/themeHelper'
6 | import {useEffect} from 'react'
7 |
8 | function ThemeToggle() {
9 | const [theme, setTheme] = useState(getInitialColorMode())
10 |
11 | useEffect(() => {
12 | document.documentElement.setAttribute('data-theme', theme)
13 | if (typeof window !== 'undefined')
14 | window.localStorage.setItem('color-mode', theme)
15 | }, [theme])
16 |
17 | const changeTheme = () => {
18 | setTheme(theme => (theme === 'dark' ? 'light' : 'dark'))
19 | document.documentElement.classList.remove('theme-tansition')
20 | document.documentElement.classList.add('theme-tansition')
21 | if (typeof window !== 'undefined') {
22 | window.setTimeout(() => {
23 | document.documentElement.classList.remove('theme-tansition')
24 | }, 750)
25 | window.localStorage.setItem('color-mode', theme)
26 | }
27 | }
28 |
29 | return (
30 |
31 | {theme === 'light' && (
32 |
35 | )}
36 | {theme === 'dark' && (
37 |
40 | )}
41 |
42 | )
43 | }
44 |
45 | export default ThemeToggle
46 |
--------------------------------------------------------------------------------
/src/components/hero/featured-post.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {FeaturedPostBox} from './style'
3 | import Image from 'gatsby-image'
4 | import {Link} from 'gatsby'
5 |
6 | function FeaturedPost({image, title, excerpt, author, date, fluid, slug}) {
7 | return (
8 |
9 |
10 |
11 |
12 | {title}
13 |
14 | {excerpt}
15 |
16 | By {author} • {date}
17 |
18 |
19 |
20 | )
21 | }
22 |
23 | export default FeaturedPost
24 |
--------------------------------------------------------------------------------
/src/components/hero/hero.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FeaturedPost from './featured-post'
3 | import {useStaticQuery, graphql} from 'gatsby'
4 | import {FeaturedPostGrid} from './style'
5 |
6 | function Hero() {
7 | const data = useStaticQuery(graphql`
8 | query {
9 | allSanityPost(
10 | filter: {featured: {eq: true}}
11 | limit: 4
12 | sort: {fields: publishedAt, order: DESC}
13 | ) {
14 | edges {
15 | node {
16 | title
17 | slug {
18 | current
19 | }
20 | mainImage {
21 | asset {
22 | fluid {
23 | ...GatsbySanityImageFluid
24 | }
25 | }
26 | }
27 | excerpt
28 | authors {
29 | author {
30 | name
31 | }
32 | }
33 | publishedAt(fromNow: true)
34 | }
35 | }
36 | }
37 | }
38 | `)
39 |
40 | return (
41 |
42 | {data.allSanityPost.edges.map(edge => (
43 |
52 | ))}
53 |
54 | )
55 | }
56 |
57 | export default Hero
58 |
--------------------------------------------------------------------------------
/src/components/hero/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import {media} from '../../utils/breakpoints'
3 |
4 | const FeaturedPostBox = styled.div`
5 | .gatsby-image-wrapper {
6 | border-radius: 8px;
7 | overflow: hidden;
8 | height: 220px;
9 | transition: box-shadow 350ms ease;
10 | }
11 | h2 {
12 | margin-top: 1rem;
13 | span {
14 | transition: box-shadow 350ms ease;
15 | }
16 | }
17 | p {
18 | padding: 1rem 0 0.5rem;
19 | }
20 | div {
21 | color: var(--text-muted);
22 | padding-bottom: 1rem;
23 | }
24 | &:hover {
25 | .gatsby-image-wrapper {
26 | box-shadow: rgba(0, 0, 0, 0.25) 0px 18px 26px -18px;
27 | }
28 | h2 {
29 | span {
30 | box-shadow: inset 0 -17px 0 var(--highlight);
31 | }
32 | }
33 | }
34 |
35 | ${media.md} {
36 | .gatsby-image-wrapper {
37 | height: 280px;
38 | }
39 | h2 {
40 | font-size: 1.7rem;
41 | }
42 | }
43 | `
44 |
45 | const FeaturedPostGrid = styled.section`
46 | padding-bottom: 2rem;
47 | display: grid;
48 | gap: 2rem;
49 | grid-template-columns: 1fr;
50 |
51 | ${media.md} {
52 | display: grid;
53 | grid-template-areas:
54 | 'one one one one two two two'
55 | 'three three three four four four four';
56 | grid-template-columns: repeat(7, 1fr);
57 | gap: 1rem;
58 | & > div:nth-child(1) {
59 | grid-area: one;
60 | }
61 | & > div:nth-child(2) {
62 | grid-area: two;
63 | }
64 | & > div:nth-child(3) {
65 | grid-area: three;
66 | }
67 | & > div:nth-child(4) {
68 | grid-area: four;
69 | }
70 | }
71 | `
72 |
73 | export {FeaturedPostBox, FeaturedPostGrid}
74 |
--------------------------------------------------------------------------------
/src/components/latest-posts/latest-posts.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {LatestPostGroup} from './style'
3 | import {useStaticQuery, graphql} from 'gatsby'
4 | import BlogPostCard from '../blog-post-card/blog-post-card'
5 |
6 | function LatestPosts() {
7 | const data = useStaticQuery(graphql`
8 | query {
9 | allSanityPost(
10 | sort: {fields: publishedAt, order: DESC}
11 | filter: {featured: {ne: true}}
12 | ) {
13 | edges {
14 | node {
15 | title
16 | slug {
17 | current
18 | }
19 | mainImage {
20 | asset {
21 | url
22 | fluid {
23 | ...GatsbySanityImageFluid
24 | }
25 | }
26 | }
27 | excerpt
28 | authors {
29 | author {
30 | name
31 | }
32 | }
33 | publishedAt(fromNow: true)
34 | }
35 | }
36 | }
37 | }
38 | `)
39 | return (
40 |
41 | {data.allSanityPost.edges.map(edge => (
42 |
52 | ))}
53 |
54 | )
55 | }
56 |
57 | export default LatestPosts
58 |
--------------------------------------------------------------------------------
/src/components/latest-posts/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import {media} from '../../utils/breakpoints'
3 | const LatestPostGroup = styled.section`
4 | & > div:first-child {
5 | margin: 0;
6 | }
7 | ${media.md} {
8 | & > div:first-child {
9 | margin: 2rem 0;
10 | }
11 | }
12 | `
13 |
14 | export {LatestPostGroup}
15 |
--------------------------------------------------------------------------------
/src/components/layout/layout.css:
--------------------------------------------------------------------------------
1 | /* Style Reset */
2 | @import url('https://fonts.googleapis.com/css2?family=Jost:wght@500;800&family=Sen:wght@400;700;800&family=Fira+Mono:wght@500&display=swap');
3 | *,
4 | *:before,
5 | *:after {
6 | box-sizing: border-box;
7 | padding: 0;
8 | margin: 0;
9 | }
10 | html {
11 | font-family: 'Sen', sans-serif;
12 | -ms-text-size-adjust: 100%;
13 | -webkit-text-size-adjust: 100%;
14 | box-sizing: border-box;
15 | overflow-y: scroll;
16 | }
17 |
18 | body {
19 | font-family: 'Sen', sans-serif;
20 | -webkit-font-smoothing: antialiased;
21 | -moz-osx-font-smoothing: grayscale;
22 | color: var(--text-dark);
23 | font-weight: normal;
24 | word-wrap: break-word;
25 | font-kerning: normal;
26 | -moz-font-feature-settings: 'kern', 'liga', 'clig', 'calt';
27 | -ms-font-feature-settings: 'kern', 'liga', 'clig', 'calt';
28 | -webkit-font-feature-settings: 'kern', 'liga', 'clig', 'calt';
29 | font-feature-settings: 'kern', 'liga', 'clig', 'calt';
30 | background-color: var(--white);
31 | -webkit-tap-highlight-color: transparent;
32 | }
33 |
34 | ::selection {
35 | background-color: #3d5afe26;
36 | }
37 |
38 | ::-webkit-scrollbar {
39 | width: 12px;
40 | background-color: var(--white);
41 | }
42 |
43 | ::-webkit-scrollbar-track {
44 | border-radius: 3px;
45 | background-color: transparent;
46 | }
47 |
48 | ::-webkit-scrollbar-thumb {
49 | border-radius: 5px;
50 | background-color: var(--black);
51 | border: 2px solid var(--white);
52 | }
53 | ::-webkit-scrollbar-thumb:hover {
54 | background-color: #2b333b;
55 | }
56 | h1,
57 | h2,
58 | h3,
59 | h4,
60 | h5,
61 | h6 {
62 | font-family: Jost, sans-serif;
63 | line-height: 1.3;
64 | }
65 |
66 | article,
67 | aside,
68 | details,
69 | figcaption,
70 | figure,
71 | footer,
72 | header,
73 | main,
74 | menu,
75 | nav,
76 | section,
77 | summary {
78 | display: block;
79 | }
80 | audio,
81 | canvas,
82 | progress,
83 | video {
84 | display: inline-block;
85 | }
86 | audio:not([controls]) {
87 | display: none;
88 | height: 0;
89 | }
90 | progress {
91 | vertical-align: baseline;
92 | }
93 | [hidden],
94 | template {
95 | display: none;
96 | }
97 | a {
98 | background-color: transparent;
99 | color: var(--text-dark);
100 | text-decoration: none;
101 | -webkit-text-decoration-skip: objects;
102 | }
103 | a:active,
104 | a:hover {
105 | outline-width: 0;
106 | }
107 | abbr[title] {
108 | border-bottom: none;
109 | text-decoration: underline;
110 | text-decoration: underline dotted;
111 | }
112 | b,
113 | strong {
114 | font-weight: inherit;
115 | font-weight: bolder;
116 | }
117 | dfn {
118 | font-style: italic;
119 | }
120 | mark {
121 | background-color: #ff0;
122 | color: #000;
123 | }
124 | sub,
125 | sup {
126 | line-height: 0;
127 | position: relative;
128 | vertical-align: baseline;
129 | }
130 | sub {
131 | bottom: -0.25em;
132 | }
133 | sup {
134 | top: -0.5em;
135 | }
136 | img {
137 | border-style: none;
138 | }
139 | svg:not(:root) {
140 | overflow: hidden;
141 | }
142 | code,
143 | kbd,
144 | pre,
145 | samp {
146 | font-family: monospace, monospace;
147 | }
148 | hr {
149 | box-sizing: content-box;
150 | height: 0;
151 | overflow: visible;
152 | }
153 | button,
154 | input,
155 | optgroup,
156 | select,
157 | textarea {
158 | font: inherit;
159 | }
160 | optgroup {
161 | font-weight: 700;
162 | }
163 | button,
164 | input {
165 | overflow: visible;
166 | }
167 | button,
168 | select {
169 | text-transform: none;
170 | }
171 | [type='reset'],
172 | [type='submit'],
173 | button,
174 | html [type='button'] {
175 | -webkit-appearance: button;
176 | }
177 | [type='button']::-moz-focus-inner,
178 | [type='reset']::-moz-focus-inner,
179 | [type='submit']::-moz-focus-inner,
180 | button::-moz-focus-inner {
181 | border-style: none;
182 | }
183 | [type='button']:-moz-focusring,
184 | [type='reset']:-moz-focusring,
185 | [type='submit']:-moz-focusring,
186 | button:-moz-focusring {
187 | outline: 1px dotted ButtonText;
188 | }
189 | fieldset {
190 | border: 1px solid silver;
191 | }
192 | legend {
193 | box-sizing: border-box;
194 | color: inherit;
195 | display: table;
196 | max-width: 100%;
197 | white-space: normal;
198 | }
199 | textarea {
200 | overflow: auto;
201 | }
202 | [type='checkbox'],
203 | [type='radio'] {
204 | box-sizing: border-box;
205 | }
206 | [type='number']::-webkit-inner-spin-button,
207 | [type='number']::-webkit-outer-spin-button {
208 | height: auto;
209 | }
210 | [type='search'] {
211 | -webkit-appearance: textfield;
212 | outline-offset: -2px;
213 | }
214 | [type='search']::-webkit-search-cancel-button,
215 | [type='search']::-webkit-search-decoration {
216 | -webkit-appearance: none;
217 | }
218 | ::-webkit-input-placeholder {
219 | color: inherit;
220 | opacity: 0.54;
221 | }
222 | ::-webkit-file-upload-button {
223 | -webkit-appearance: button;
224 | font: inherit;
225 | }
226 |
227 | img {
228 | max-width: 100%;
229 | }
230 |
231 | :root {
232 | --orange: #ff8b64;
233 | --purple: #9980fa;
234 | --blue: #5855e0;
235 | --grey: #7b7b7b;
236 | --black: #1c1c1c;
237 | --green: #82f9a1;
238 | --one: #ff8b64;
239 | --two: #5855e0;
240 | --three: #9980fa;
241 | --white: #fff;
242 | --text-dark: #0a0c10;
243 | --text-muted: #6c7693;
244 | --text-light: #afafaf;
245 | --blur-bg: #ffffffd9;
246 | --highlight: #ff8b646b;
247 | --highlight-shadow: c;
248 | }
249 | html[data-theme='dark'] {
250 | --orange: #fd413c;
251 | --purple: #9980fa;
252 | --blue: #5855e0;
253 | --grey: #7b7b7b;
254 | --black: #1c1c1c;
255 | --green: #82f9a1;
256 | --one: #ff8b64;
257 | --two: #5855e0;
258 | --three: #9980fa;
259 | --white: #212529;
260 | --text-dark: #fff;
261 | --text-muted: #6c7693;
262 | --text-light: #afafaf;
263 | --blur-bg: #212529d9;
264 | --highlight: #fd413cbf;
265 | --highlight-shadow: #fd413c40;
266 | }
267 |
268 | html.theme-tansition,
269 | html.theme-tansition *,
270 | html.theme-tansition *:before,
271 | html.theme-tansition *:after {
272 | transition: background-color 750ms ease 0s !important;
273 | }
274 |
275 | #gatsby-focus-wrapper {
276 | display: flex;
277 | flex-direction: column;
278 | min-height: 100vh;
279 | }
280 | #gatsby-focus-wrapper footer {
281 | margin-top: auto;
282 | }
283 |
284 | .ham {
285 | cursor: pointer;
286 | -webkit-tap-highlight-color: transparent;
287 | transition: transform 400ms;
288 | -moz-user-select: none;
289 | -webkit-user-select: none;
290 | -ms-user-select: none;
291 | user-select: none;
292 | }
293 |
294 | .hamRotate180.active {
295 | transform: rotate(180deg);
296 | }
297 | .line {
298 | fill: none;
299 | transition: stroke-dasharray 400ms, stroke-dashoffset 400ms;
300 | stroke: var(--text-dark);
301 | stroke-width: 5.5;
302 | stroke-linecap: round;
303 | }
304 | .ham5 .top {
305 | stroke-dasharray: 40 82;
306 | }
307 | .ham5 .bottom {
308 | stroke-dasharray: 40 82;
309 | }
310 | .ham5.active .top {
311 | stroke-dasharray: 14 82;
312 | stroke-dashoffset: -72px;
313 | }
314 | .ham5.active .bottom {
315 | stroke-dasharray: 14 82;
316 | stroke-dashoffset: -72px;
317 | }
318 | /* * {
319 | border: 1px solid red;
320 | } */
321 |
--------------------------------------------------------------------------------
/src/components/layout/layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Header from '../header/header'
3 | import Footer from '../footer/footer'
4 | import './layout.css'
5 | import {Container} from '../common/style'
6 | import MobileHeader from '../mobile-header/mobile-header'
7 | import {Helmet} from 'react-helmet'
8 |
9 | function Layout({children}) {
10 | return (
11 | <>
12 |
13 |
14 |
15 | {children}
16 |
17 | >
18 | )
19 | }
20 |
21 | export default Layout
22 |
--------------------------------------------------------------------------------
/src/components/mobile-header/hamburger.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react'
2 |
3 | function Hamburger({toggle, hamIconActive}) {
4 | const [ham, setHam] = useState(false)
5 | return (
6 |
7 |
25 |
26 | )
27 | }
28 |
29 | export default Hamburger
30 |
--------------------------------------------------------------------------------
/src/components/mobile-header/mobile-header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {MobileHead, MobileLogo} from './style'
3 | import logo from '../../images/logo.png'
4 | import MobileSearch from './mobile-search'
5 | import {Link} from 'gatsby'
6 | function MobileHeader() {
7 | return (
8 |
9 |
10 |
11 |
12 |

13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | export default MobileHeader
22 |
--------------------------------------------------------------------------------
/src/components/mobile-header/mobile-nav.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {MobileNavWrapper} from './style'
3 | import {Link} from 'gatsby'
4 |
5 | import icon from '../../images/icons/icons.svg'
6 | import ThemeToggle from '../header/themeToggle'
7 |
8 | function MobileNav({active, toggleNav}) {
9 | return (
10 |
11 |
12 | -
13 |
19 | Lastest
20 |
21 |
22 | -
23 |
29 | Blog
30 |
31 |
32 | -
33 |
40 | Contact
41 |
42 |
43 | -
44 |
45 |
46 |
47 |
48 |
51 |
52 | )
53 | }
54 |
55 | export default MobileNav
56 |
--------------------------------------------------------------------------------
/src/components/mobile-header/mobile-search.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 | import {MobileSearchWrapper, SearchBoxWrapper, SearchBox} from './style'
3 | import {SearchResults} from '../header/style'
4 | import Hamburger from './hamburger'
5 | import icon from '../../images/icons/icons.svg'
6 | import {graphql, useStaticQuery, Link} from 'gatsby'
7 | import {Index} from 'elasticlunr'
8 | import MobileNav from './mobile-nav'
9 | function MobileSearch() {
10 | const data = useStaticQuery(graphql`
11 | query {
12 | siteSearchIndex {
13 | index
14 | }
15 | }
16 | `)
17 |
18 | const [isOpen, setOpen] = useState(false)
19 | const [navOpen, setNavOpen] = useState(false)
20 | const [state, setState] = useState({
21 | query: ``,
22 | results: [],
23 | isActive: false
24 | })
25 |
26 | useEffect(() => {
27 | document.documentElement.style.overflowY = navOpen ? 'hidden' : 'auto'
28 | }, [navOpen])
29 |
30 | useEffect(() => {
31 | setState({query: ``, results: [], isActive: false})
32 | }, [isOpen])
33 |
34 | let index = null
35 | const getOrCreateIndex = () =>
36 | index ? index : Index.load(data.siteSearchIndex.index)
37 |
38 | const search = evt => {
39 | const query = evt.target.value
40 | index = getOrCreateIndex()
41 | setState({
42 | query,
43 | // Query the index with search string to get an [] of IDs
44 | results: index
45 | .search(query, {expand: true}) // Accept partial matches
46 | // Map over each ID and return the full document
47 | .map(({ref}) => index.documentStore.getDoc(ref)),
48 | isActive: !!query
49 | })
50 | }
51 |
52 | const handleToggle = () => {
53 | if (isOpen) return setOpen(false)
54 | if (navOpen) return setNavOpen(false)
55 | return setNavOpen(true)
56 | }
57 |
58 | return (
59 |
60 |
61 |
62 |
63 |
78 |
79 | {state.results.map(page => (
80 | setOpen(false)}
85 | >
86 | {page.title}
87 |
88 | ))}
89 |
90 |
91 |
92 |
93 |
94 | )
95 | }
96 |
97 | export default MobileSearch
98 |
--------------------------------------------------------------------------------
/src/components/mobile-header/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import {media} from '../../utils/breakpoints'
3 |
4 | const MobileHead = styled.header`
5 | padding: 1rem 0;
6 | ${media.md} {
7 | display: none;
8 | }
9 | `
10 | const MobileNavWrapper = styled.nav`
11 | background: var(--blur-bg);
12 | position: fixed;
13 | top: 0;
14 | left: -110%;
15 | width: 100%;
16 | bottom: 0;
17 | z-index: 999;
18 | backdrop-filter: blur(8px);
19 | touch-action: none;
20 | opacity: 0;
21 | transition: left 350ms ease 300ms, opacity 350ms ease 300ms;
22 | display: flex;
23 | div {
24 | height: 100%;
25 | flex: 1;
26 | }
27 |
28 | ul {
29 | width: 100%;
30 | max-width: fit-content;
31 | padding-top: 30%;
32 | height: 100%;
33 | list-style: none;
34 | li {
35 | a,
36 | div {
37 | padding: 1rem 3rem;
38 | display: block;
39 | font-size: 1.5rem;
40 | font-weight: 700;
41 | }
42 | transform: translateX(-100%);
43 | transition: transform 450ms ease 0ms;
44 | a.active {
45 | span {
46 | font-weight: 600;
47 | box-shadow: inset 0 -4px 0 var(--highlight);
48 | }
49 | }
50 | }
51 | }
52 | svg {
53 | width: 40px;
54 | height: 40px;
55 | fill: var(--text-dark);
56 |
57 | &.close {
58 | position: absolute;
59 | top: 20px;
60 | right: 20px;
61 | }
62 | }
63 |
64 | &.active {
65 | left: 0;
66 | opacity: 1;
67 | transform: translate3d(0);
68 | transition: left 350ms ease 0ms, opacity 350ms ease 0ms;
69 | ul {
70 | li {
71 | transform: translateX(0);
72 | transition: transform 450ms ease 250ms;
73 | }
74 | }
75 | }
76 | `
77 | const MobileLogo = styled.div`
78 | margin: 1rem 0;
79 | div {
80 | max-width: 150px;
81 | margin: 0 auto;
82 | img {
83 | width: 100%;
84 | }
85 | }
86 | `
87 | const MobileSearchWrapper = styled.div``
88 | export {MobileHead, MobileNavWrapper, MobileSearchWrapper, MobileLogo}
89 |
90 | // Search Bar
91 |
92 | const SearchBoxWrapper = styled.div`
93 | border-radius: 5px;
94 | margin: 0 1rem;
95 | touch-action: none;
96 | top: 60px;
97 | left: 16px;
98 | right: 16px;
99 | bottom: 85%;
100 | transform: translate3d(0);
101 | transition: margin 350ms ease, top 350ms ease, left 350ms ease,
102 | right 350ms ease, bottom 350ms ease;
103 |
104 | &.active {
105 | position: fixed;
106 | top: 0;
107 | left: 0;
108 | bottom: 0;
109 | right: 0;
110 | margin: 0;
111 | background: var(--white);
112 | padding: 4rem 1rem;
113 | z-index: 999;
114 | border-radius: 0px;
115 |
116 | & > div {
117 | box-shadow: 0 0 2px 4px var(--highlight-shadow);
118 | }
119 | }
120 | `
121 | const SearchBox = styled.div`
122 | position: relative;
123 | display: flex;
124 | justify-content: space-between;
125 | width: 100%;
126 | border-radius: 5px;
127 | box-shadow: 2px 0 24px rgba(0, 0, 0, 0.1);
128 | align-items: center;
129 | label {
130 | display: block;
131 | display: flex;
132 | flex: 1;
133 |
134 | input {
135 | flex: 1;
136 | border: none;
137 | padding: 0.5rem;
138 | background: var(--white);
139 | color: var(--text-dark);
140 | font-size: 17px;
141 | &:focus {
142 | outline: none;
143 | }
144 | }
145 | svg {
146 | fill: var(--text-dark);
147 | padding-top: 3px;
148 | width: 35px;
149 | height: 35px;
150 | line-height: 2.5rem;
151 | vertical-align: middle;
152 | transition: fill 300ms ease;
153 | }
154 | }
155 |
156 | &:hover,
157 | &:focus,
158 | &:active,
159 | &:focus-within {
160 | svg {
161 | fill: var(--orange);
162 | }
163 | }
164 | `
165 |
166 | export {SearchBoxWrapper, SearchBox}
167 |
--------------------------------------------------------------------------------
/src/components/pagination/pagination.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paginate } from './style';
3 | import { Link } from 'gatsby';
4 | import icon from '../../images/icons/icons.svg';
5 | function Pagination({ currentPage, totalPage }) {
6 | const nextPage = currentPage + 1;
7 | const prevPage = currentPage === 2 ? '' : currentPage - 1;
8 |
9 | const isNextDead = currentPage === totalPage && true;
10 | const isPrevDead = currentPage === 1 && true;
11 |
12 | return (
13 |
14 |
18 |
21 |
22 |
23 | {currentPage} / {totalPage}
24 |
25 |
29 |
32 |
33 |
34 | );
35 | }
36 |
37 | export default Pagination;
38 |
--------------------------------------------------------------------------------
/src/components/pagination/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Paginate = styled.div`
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | height: 2rem;
8 | a.pageNav {
9 | border: 2px solid var(--text-light);
10 | border-right: none;
11 | border-radius: 50px 0 0 50px;
12 | &:last-child {
13 | border-radius: 0 50px 50px 0;
14 | border-left: none;
15 | border-right: 2px solid var(--text-light);
16 | }
17 | svg {
18 | fill: var(--text-light);
19 | height: 2rem;
20 | width: 3rem;
21 | display: block;
22 | transition: fill 350ms ease;
23 | &:hover {
24 | fill: var(--text-muted);
25 | }
26 | }
27 | }
28 | & > div {
29 | font-size: 1.2rem;
30 | line-height: 2rem;
31 | vertical-align: middle;
32 | border: 2px solid var(--text-light);
33 | padding: 0 1rem;
34 | font-family: Jost;
35 | }
36 |
37 | a.disabled {
38 | pointer-events: none;
39 | }
40 | `;
41 | export { Paginate };
42 |
--------------------------------------------------------------------------------
/src/components/seo/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 | import ogbanner from '../../images/ogbanner.jpg'
6 | function SEO({description, meta, title}) {
7 | const data = useStaticQuery(
8 | graphql`
9 | query {
10 | sanitySiteSettings {
11 | description
12 | title
13 | siteURL
14 | author {
15 | name
16 | }
17 | }
18 | }
19 | `
20 | )
21 | // Formatting SiteURL if it ends with '/'
22 | const formatSiteURL = data.sanitySiteSettings.siteURL
23 | const lastChar = formatSiteURL.slice(formatSiteURL.length - 1)
24 | const siteURL = lastChar === '/' ? formatSiteURL.slice(0, -1) : formatSiteURL
25 |
26 | const metaDescription = description || data.sanitySiteSettings.description
27 | const metaTitle = title || data.sanitySiteSettings.title
28 | const metaBanner = siteURL + ogbanner
29 |
30 | return (
31 |
83 | )
84 | }
85 |
86 | SEO.defaultProps = {
87 | lang: `en`,
88 | meta: [],
89 | description: ``
90 | }
91 |
92 | SEO.propTypes = {
93 | description: PropTypes.string,
94 | lang: PropTypes.string,
95 | meta: PropTypes.arrayOf(PropTypes.object),
96 | title: PropTypes.string.isRequired
97 | }
98 |
99 | export default SEO
100 |
--------------------------------------------------------------------------------
/src/images/demo-ad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/damnitrahul/gatsby-starter-flat-magazine/98c67290d8fc8615c280ce6366f0ab0ad91ad2d9/src/images/demo-ad.png
--------------------------------------------------------------------------------
/src/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/damnitrahul/gatsby-starter-flat-magazine/98c67290d8fc8615c280ce6366f0ab0ad91ad2d9/src/images/favicon.png
--------------------------------------------------------------------------------
/src/images/icons/chevron.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/icons/icons.svg:
--------------------------------------------------------------------------------
1 |
69 |
--------------------------------------------------------------------------------
/src/images/icons/info.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/icons/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/damnitrahul/gatsby-starter-flat-magazine/98c67290d8fc8615c280ce6366f0ab0ad91ad2d9/src/images/logo.png
--------------------------------------------------------------------------------
/src/images/moon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/ogbanner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/damnitrahul/gatsby-starter-flat-magazine/98c67290d8fc8615c280ce6366f0ab0ad91ad2d9/src/images/ogbanner.jpg
--------------------------------------------------------------------------------
/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Layout from '../components/layout/layout'
3 | import SEO from '../components/seo/seo'
4 |
5 | function Index() {
6 | return (
7 |
8 |
9 | 404 Page Not Found
10 |
11 | )
12 | }
13 | // Site settings sanity fetch
14 | // 404 setup
15 | // Meta iamge for Home
16 | // Seo Setup
17 |
18 | export default Index
19 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Layout from '../components/layout/layout'
3 | import Hero from '../components/hero/hero'
4 | import {BlogPostCols} from '../components/common/style'
5 | import LatestPosts from '../components/latest-posts/latest-posts'
6 | import AdsSidebar from '../components/ads/ads-sidebar'
7 | import SEO from '../components/seo/seo'
8 |
9 | function Index() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default Index
23 |
--------------------------------------------------------------------------------
/src/templates/BlogSingle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {graphql} from 'gatsby'
3 | import BlockContent from '@sanity/block-content-to-react'
4 | import urlBuilder from '@sanity/image-url'
5 |
6 | export const query = graphql`
7 | query($slug: String!) {
8 | sanityPost(slug: {current: {eq: $slug}}) {
9 | title
10 | _rawBody(resolveReferences: {maxDepth: 10})
11 | }
12 | }
13 | `
14 |
15 | const urlFor = source =>
16 | urlBuilder({
17 | projectId: process.env.GATSBY_PROJECT_ID,
18 | dataset: process.env.GATSBY_PROJECT_DATASET
19 | }).image(source)
20 |
21 | const serializers = {
22 | types: {
23 | image: props =>
24 | }
25 | }
26 |
27 | function BlogSingle({data}) {
28 | return (
29 | <>
30 | {data.sanityPost.title}
31 |
35 | >
36 | )
37 | }
38 |
39 | export default BlogSingle
40 |
--------------------------------------------------------------------------------
/src/templates/blog-page/blog-page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BlogPosts from './blog-posts'
3 | import Layout from '../../components/layout/layout'
4 | import {BlogPostCols} from '../../components/common/style'
5 | import AdsSidebar from '../../components/ads/ads-sidebar'
6 | import {graphql} from 'gatsby'
7 | import Pagination from '../../components/pagination/pagination'
8 | import SEO from '../../components/seo/seo'
9 |
10 | export const query = graphql`
11 | query($skip: Int) {
12 | allSanityPost(
13 | skip: $skip
14 | limit: 10
15 | sort: {fields: publishedAt, order: DESC}
16 | ) {
17 | edges {
18 | node {
19 | title
20 | slug {
21 | current
22 | }
23 | mainImage {
24 | asset {
25 | url
26 | fluid {
27 | ...GatsbySanityImageFluid
28 | }
29 | }
30 | }
31 | excerpt
32 | authors {
33 | author {
34 | name
35 | }
36 | }
37 | publishedAt(fromNow: true)
38 | }
39 | }
40 | }
41 | }
42 | `
43 |
44 | function BlogPage({data, pageContext}) {
45 | const {current, pages} = pageContext
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | )
56 | }
57 |
58 | export default BlogPage
59 |
--------------------------------------------------------------------------------
/src/templates/blog-page/blog-posts.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import BlogPostCard from '../../components/blog-post-card/blog-post-card'
3 |
4 | function BlogPosts({data}) {
5 | return (
6 |
7 | {data.allSanityPost.edges.map(edge => (
8 |
18 | ))}
19 |
20 | )
21 | }
22 |
23 | export default BlogPosts
24 |
--------------------------------------------------------------------------------
/src/templates/blog-page/style.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/damnitrahul/gatsby-starter-flat-magazine/98c67290d8fc8615c280ce6366f0ab0ad91ad2d9/src/templates/blog-page/style.js
--------------------------------------------------------------------------------
/src/templates/blog-post/blog-post-image.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Image from 'gatsby-image'
3 | import {getFluidGatsbyImage} from 'gatsby-source-sanity'
4 |
5 | function BlogPostImage({image}) {
6 | const sanityConfig = {
7 | projectId: process.env.GATSBY_PROJECT_ID,
8 | dataset: process.env.GATSBY_PROJECT_DATASET
9 | }
10 | return (
11 |
20 | )
21 | }
22 |
23 | export default BlogPostImage
24 |
--------------------------------------------------------------------------------
/src/templates/blog-post/blog-post.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Layout from '../../components/layout/layout'
3 | import {graphql, Link} from 'gatsby'
4 | import Image from 'gatsby-image'
5 | import BlockContent from '@sanity/block-content-to-react'
6 | import {BlogPostCols} from '../../components/common/style'
7 | import AdsSidebar from '../../components/ads/ads-sidebar'
8 | import {
9 | PostHeader,
10 | PostBody,
11 | AuthorDetails,
12 | PostImage,
13 | NextPrevBtn,
14 | MorePosts
15 | } from './style'
16 | import BlogPostImage from './blog-post-image'
17 | import SyntaxHighlighter from 'react-syntax-highlighter'
18 | import dracula from 'react-syntax-highlighter/dist/esm/styles/hljs/obsidian'
19 | import SEO from '../../components/seo/seo'
20 |
21 | export const query = graphql`
22 | query($slug: String!) {
23 | sanityPost(slug: {current: {eq: $slug}}) {
24 | title
25 | publishedAt(fromNow: true)
26 | excerpt
27 | mainImage {
28 | alt
29 | caption
30 | asset {
31 | url
32 | fluid {
33 | ...GatsbySanityImageFluid
34 | }
35 | }
36 | }
37 | authors {
38 | author {
39 | name
40 | image {
41 | asset {
42 | fixed(toFormat: WEBP, width: 50, height: 50) {
43 | ...GatsbySanityImageFixed
44 | }
45 | }
46 | }
47 | }
48 | }
49 | categories {
50 | title
51 | }
52 | _rawBody(resolveReferences: {maxDepth: 10})
53 | }
54 | }
55 | `
56 |
57 | const serializers = {
58 | types: {
59 | mainImage: ({node}) => ,
60 | myCode: ({node}) => {
61 | if (!node.code) return null
62 | return (
63 |
69 | {node.code}
70 |
71 | )
72 | }
73 | },
74 | marks: {
75 | link: ({children, mark}) => (
76 |
81 | {children}
82 |
83 | ),
84 | internalLink: ({children, mark}) => (
85 | {children}
86 | )
87 | }
88 | }
89 |
90 | function BlogPost({data, pageContext}) {
91 | const post = data.sanityPost
92 | const title = post.title
93 | const authorName = post.authors[0].author.name
94 | const authorImage = post.authors[0].author.image.asset.fixed
95 | const date = post.publishedAt
96 | const postImg = post.mainImage.asset.fluid
97 | const imgAlt = post.mainImage.alt
98 | const {next, prev, nextTitle, prevTitle} = pageContext
99 | return (
100 |
101 |
115 |
116 | {title}
117 |
118 |
119 |
120 | {authorName} {date}
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
133 |
134 | More Recents Posts
135 |
136 | {prev && (
137 |
138 | {prevTitle}
139 |
142 |
143 | )}
144 | {next && (
145 |
146 | {nextTitle}
147 |
150 |
151 | )}
152 |
153 |
154 |
155 |
156 |
157 |
158 | )
159 | }
160 |
161 | export default BlogPost
162 |
--------------------------------------------------------------------------------
/src/templates/blog-post/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import {media} from '../../utils/breakpoints'
3 | import info from '../../images/icons/info.svg'
4 | import arrow from '../../images/icons/chevron.svg'
5 |
6 | const PostHeader = styled.div`
7 | padding: 2rem 0;
8 | h1 {
9 | font-size: 2rem;
10 | text-align: center;
11 | padding: 1rem 0;
12 | }
13 |
14 | ${media.md} {
15 | h1 {
16 | font-size: 4rem;
17 | }
18 | }
19 | `
20 | const AuthorDetails = styled.div`
21 | display: flex;
22 | justify-content: center;
23 | align-items: center;
24 | .gatsby-image-wrapper {
25 | border-radius: 50px;
26 | }
27 | p {
28 | font-weight: 600;
29 | padding: 0 2rem;
30 | span {
31 | display: block;
32 | font-weight: 400;
33 | }
34 | }
35 | `
36 | const PostImage = styled.div`
37 | .gatsby-image-wrapper {
38 | border-radius: 15px;
39 | max-height: 600px;
40 | }
41 | padding: 2rem 0;
42 | `
43 | const PostBody = styled.article`
44 | padding-bottom: 2rem;
45 | h1 {
46 | font-size: 2.2rem;
47 | }
48 | h2 {
49 | font-size: 1.7rem;
50 | }
51 | h3 {
52 | font-size: 1.5rem;
53 | }
54 | h4 {
55 | font-size: 1.2rem;
56 | }
57 | p {
58 | font-size: 1.2rem;
59 | line-height: 1.4;
60 | padding: 1rem 0;
61 | }
62 | a {
63 | font-weight: 600;
64 | box-shadow: inset 0 -2px 0 var(--highlight);
65 | transition: box-shadow 350ms ease;
66 | &:hover {
67 | box-shadow: inset 0 -19px 0 var(--highlight);
68 | }
69 | }
70 | .gatsby-image-wrapper {
71 | border-radius: 15px;
72 | margin: 1.5rem 0;
73 | }
74 | ul {
75 | padding: 2rem;
76 | list-style: none;
77 | li {
78 | font-size: 1.2rem;
79 | line-height: 1.4;
80 | padding: 0.2rem 0;
81 | position: relative;
82 | &:before {
83 | content: '';
84 | position: absolute;
85 | height: 100%;
86 | width: 30px;
87 | left: -30px;
88 | background: url(${arrow}) center center / cover;
89 | }
90 | }
91 | }
92 |
93 | ol {
94 | padding: 2rem 0.4rem;
95 | li {
96 | font-size: 1.2rem;
97 | line-height: 1.4;
98 | padding: 0.2rem 0;
99 | list-style-type: none;
100 | counter-increment: step-counter;
101 | &:before {
102 | content: counter(step-counter);
103 | margin-right: 10px;
104 | font-size: 80%;
105 | background-color: var(--blue);
106 | color: white;
107 | font-weight: bold;
108 | padding: 2px 6px;
109 | border-radius: 3px;
110 | }
111 | }
112 | }
113 |
114 | blockquote {
115 | padding: 1rem 2rem;
116 | margin: 2rem 0;
117 | background: #5855e03a;
118 | border-radius: 10px;
119 | border-left: 4px solid var(--two);
120 | font-size: 1.2rem;
121 | line-height: 1.4;
122 | position: relative;
123 | &:before {
124 | content: '';
125 | position: absolute;
126 | border: var(--white) 5px solid;
127 | border-radius: 50px;
128 | height: 45px;
129 | width: 45px;
130 | left: -22.5px;
131 | top: -22.5px;
132 | background: url(${info}) var(--white) center center / cover;
133 | }
134 | }
135 |
136 | pre {
137 | padding: 2.3rem 1rem 1rem 1rem !important;
138 | background: #3d3d3d;
139 | color: #f8f8f8;
140 | line-height: 1.5;
141 | font-size: 1rem;
142 | border-radius: 10px;
143 | font-family: 'Sen', monospace;
144 | position: relative;
145 | code {
146 | font-family: 'Fira Mono', monospace;
147 | }
148 | &:after {
149 | content: attr(data-language);
150 | position: absolute;
151 | left: 0;
152 | top: 0;
153 | padding: 0.3rem 0.6rem;
154 | border-radius: 5px;
155 | background: rgba(0, 0, 0, 0.1);
156 | /* color: red; */
157 | }
158 | }
159 | `
160 | const NextPrevBtn = styled.div`
161 | display: block;
162 | a {
163 | box-shadow: none;
164 | display: block;
165 | padding: 1rem 0;
166 | /* Arrow Animation from codepen https://codepen.io/aaroniker/pen/pojaBvb?editors=1100 */
167 | --line: var(--orange);
168 | text-decoration: none;
169 | color: var(--color);
170 | position: relative;
171 |
172 | span {
173 | background-image: linear-gradient(0deg, var(--line) 0%, var(--line) 100%);
174 | background-position: 100% 100%;
175 | background-repeat: no-repeat;
176 | background-size: var(--background-size, 100%) 1px;
177 | transition: background-size 0.2s linear var(--background-delay, 0.15s);
178 | font-size: 1.1rem;
179 | line-height: 22px;
180 | transform: translateZ(0);
181 | }
182 | svg {
183 | vertical-align: top;
184 | display: inline;
185 | line-height: 1;
186 | width: 13px;
187 | height: 27px;
188 | position: relative;
189 | left: -2px;
190 | fill: none;
191 | stroke-linecap: round;
192 | stroke-linejoin: round;
193 | stroke-width: 1px;
194 | stroke: var(--line);
195 | stroke-dasharray: 7.95 30;
196 | stroke-dashoffset: var(--stroke-dashoffset, 46);
197 | transition: stroke-dashoffset var(--stroke-duration, 0.15s)
198 | var(--stroke-easing, linear) var(--stroke-delay, 0s);
199 | }
200 | &:hover {
201 | --background-size: 0%;
202 | --background-delay: 0s;
203 | --stroke-dashoffset: 26;
204 | --stroke-duration: 0.3s;
205 | --stroke-easing: cubic-bezier(0.3, 1.5, 0.5, 1);
206 | --stroke-delay: 0.195s;
207 | }
208 | /* Animation End */
209 |
210 | span {
211 | transition: box-shadow 350ms ease;
212 | box-shadow: inset 0 -2px 0 var(--highlight);
213 | }
214 | &:hover {
215 | box-shadow: none;
216 | span {
217 | box-shadow: inset 0 -19px 0 var(--highlight);
218 | }
219 | }
220 |
221 | &.next {
222 | margin-left: auto;
223 | text-align: right;
224 | }
225 | }
226 | ${media.md} {
227 | display: flex;
228 | a {
229 | width: 50%;
230 | }
231 | }
232 | `
233 |
234 | const MorePosts = styled.div`
235 | margin-top: 2rem;
236 | `
237 | export {PostHeader, PostBody, AuthorDetails, PostImage, NextPrevBtn, MorePosts}
238 |
--------------------------------------------------------------------------------
/src/utils/breakpoints.js:
--------------------------------------------------------------------------------
1 | const size = {
2 | sm: 640,
3 | md: 768,
4 | lg: 1024,
5 | xl: 1280
6 | };
7 |
8 | export const media = {
9 | sm: `@media (min-width: ${size.sm}px)`,
10 | md: `@media (min-width: ${size.md}px)`,
11 | lg: `@media (min-width: ${size.lg}px)`,
12 | xl: `@media (min-width: ${size.xl}px)`
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/themeHelper.js:
--------------------------------------------------------------------------------
1 | export function getInitialColorMode() {
2 | if (typeof window !== 'undefined') {
3 | const persistedColorPreference = window.localStorage.getItem('color-mode');
4 | const hasPersistedPreference = typeof persistedColorPreference === 'string';
5 | // If the user has explicitly chosen light or dark,
6 | // let's use it. Otherwise, this value will be null.
7 | if (hasPersistedPreference) {
8 | return persistedColorPreference;
9 | }
10 | // If they haven't been explicit, let's check the media
11 | // query
12 | const mql = window.matchMedia('(prefers-color-scheme: dark)');
13 | const hasMediaQueryPreference = typeof mql.matches === 'boolean';
14 | if (hasMediaQueryPreference) {
15 | return mql.matches ? 'dark' : 'light';
16 | }
17 | // If they are using a browser/OS that doesn't support
18 | // color themes, let's default to 'light'.
19 | return 'light';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------