├── .github └── workflows │ └── build-hourly-deploy-to-netlify.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── architecture.jpg ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package.json ├── src ├── components │ ├── BuildInfo.js │ ├── GithubCorner.js │ ├── Navigation.js │ ├── Stories.js │ ├── Time.js │ ├── header.js │ └── layout.js ├── favicon.png ├── pages │ ├── 404.js │ ├── best.js │ ├── index.js │ └── new.js └── util │ └── Format.js └── yarn.lock /.github/workflows/build-hourly-deploy-to-netlify.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Gatsby to Netlify every 3 hours 2 | 3 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/configuring-a-workflow#triggering-a-workflow-with-events 4 | on: 5 | schedule: 6 | - cron: '0 */3 * * *' 7 | # on: 8 | # push: 9 | # branches: 10 | # - ci-npm-cache 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | # https://stackoverflow.com/a/62244232/4035 20 | - name: Get yarn cache directory path 21 | id: yarn-cache-dir-path 22 | run: echo "::set-output name=dir::$(yarn cache dir)" 23 | 24 | - name: Cache yarn cache 25 | uses: actions/cache@v2 26 | id: cache-yarn-cache 27 | with: 28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 29 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 30 | restore-keys: | 31 | ${{ runner.os }}-yarn- 32 | 33 | - name: Cache node_modules 34 | id: cache-node-modules 35 | uses: actions/cache@v2 36 | with: 37 | path: node_modules 38 | key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-${{ matrix.node-version }}-nodemodules- 41 | 42 | - run: yarn 43 | if: | 44 | steps.cache-yarn-cache.outputs.cache-hit != 'true' || 45 | steps.cache-node-modules.outputs.cache-hit != 'true' 46 | 47 | - name: Build the site 48 | run: yarn build 49 | 50 | # Deploy the gatsby build to Netlify 51 | #- run: npx netlify-cli deploy --dir=public --prod 52 | # https://github.com/netlify/actions/blob/master/cli/README.md 53 | - uses: netlify/actions/cli@master 54 | env: 55 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 56 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 57 | with: 58 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idstepswithargs 59 | args: deploy --dir=public --prod 60 | secrets: '["NETLIFY_AUTH_TOKEN", "NETLIFY_SITE_ID"]' 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project dependencies 2 | .cache 3 | node_modules 4 | yarn-error.log 5 | 6 | # Build directory 7 | /public 8 | .DS_Store 9 | 10 | # Local Netlify folder 11 | .netlify -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 gatsbyjs 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 | ## 🚀 SHaNc - Static Hacker News clone 2 | 3 | ### ❓ Why? 4 | 5 | Because I want to skim through stories and be done with it. 6 | I don't need up-to-date stories. 7 | 8 | ### ⏲ How often does it refresh? 9 | 10 | Every hour on the hour. 11 | 12 | ### ⚠️Note 13 | 14 | Currently it only shows Top stories. 15 | I will add "new" and "best" stories later on. 16 | 17 | ### 🔨 Technologies (and stuff...) 18 | 19 | * Static Site Generator: [Gatsby](https://www.gatsbyjs.org/) 20 | * [Styled Components](https://www.styled-components.com/) 21 | * Custom Hacker News GraphQL [source](https://github.com/dance2die/SHANc/blob/master/gatsby-node.js) 22 | * Simply calls Official [HN API](https://github.com/HackerNews/API). 23 | * Server: [Netlify](https://www.netlify.com/) 24 | * Build Trigger: [Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/) (PowerShell) 25 | * Netlify exposes Build WebHook. Azure Functions written in PowerShell simply calls it with `Invoke-WebRequest` every hour 26 | * [GitHub Corners](https://github.com/tholman/github-corners) by [Tim Holman](http://tholman.com/) 27 | 28 | ### 📐 Architecture 29 | 30 | The awesome hand-drawn architecture 31 | ![architecture](architecture.jpg) 32 | -------------------------------------------------------------------------------- /architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dance2die/SHANc/ae91438417e95cacb85385e46a0d42d1dfcd77e5/architecture.jpg -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: 'SHaNc', 4 | description: 'Static Hacker News clone', 5 | }, 6 | plugins: [ 7 | 'gatsby-plugin-react-helmet', 8 | 'gatsby-plugin-styled-components', 9 | { 10 | resolve: 'gatsby-plugin-google-analytics', 11 | options: { 12 | trackingId: 'UA-119809136-1', 13 | // Puts tracking script in the head instead of the body 14 | head: false, 15 | anonymize: true, 16 | respectDNT: true, 17 | // Avoids sending pageview hits from custom paths 18 | exclude: ['/preview/**', '/do-not-track/me/too/'], 19 | }, 20 | }, 21 | { 22 | resolve: `gatsby-plugin-favicon`, 23 | options: { 24 | logo: './src/favicon.png', 25 | injectHTML: true, 26 | icons: { 27 | android: true, 28 | appleIcon: true, 29 | appleStartup: true, 30 | coast: false, 31 | favicons: true, 32 | firefox: true, 33 | twitter: false, 34 | yandex: false, 35 | windows: false, 36 | }, 37 | }, 38 | }, 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /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 | const axios = require('axios') 7 | const crypto = require('crypto') 8 | 9 | const buildContentDigest = content => 10 | crypto 11 | .createHash(`md5`) 12 | .update(JSON.stringify(content)) 13 | .digest(`hex`) 14 | 15 | const createStoriesSource = async ({ createNode }) => { 16 | const topStoriesURL = `https://hacker-news.firebaseio.com/v0/topstories.json` 17 | const newStoriesURL = `https://hacker-news.firebaseio.com/v0/newstories.json` 18 | const bestStoriesURL = `https://hacker-news.firebaseio.com/v0/beststories.json` 19 | const getItemURL = storyId => 20 | `https://hacker-news.firebaseio.com/v0/item/${storyId}.json` 21 | 22 | const topResults = await axios.get(topStoriesURL) 23 | const newResults = await axios.get(newStoriesURL) 24 | const bestResults = await axios.get(bestStoriesURL) 25 | 26 | // Combine all story IDs to get all items in one go for "items" map 27 | // We need only distinct SET of IDs. 28 | const storyIds = [ 29 | ...new Set([...topResults.data, ...newResults.data, ...bestResults.data]), 30 | ] 31 | 32 | const getStories = async storyIds => { 33 | const stories = storyIds.map(storyId => axios.get(getItemURL(storyId))) 34 | return Promise.all(stories) 35 | } 36 | 37 | // Build item details map 38 | // for an O(1) look up for fetched item details 39 | const items = (await getStories(storyIds)) 40 | .map(res => res.data) 41 | .filter(item => item !== null) 42 | .reduce((acc, item) => acc.set(item.id, item), new Map()) 43 | 44 | // Expose a hacker new story available for GraphQL query 45 | const createStoryNodes = (data, type) => 46 | data.map(storyId => { 47 | const id = `${type}-${storyId}` 48 | const storyNode = { 49 | id, 50 | parent: null, 51 | internal: { type }, 52 | children: [], 53 | storyId: storyId, 54 | item: items.get(storyId), 55 | } 56 | 57 | storyNode.internal.contentDigest = buildContentDigest(storyNode) 58 | 59 | createNode(storyNode) 60 | }) 61 | 62 | createStoryNodes(topResults.data, 'TopStories') 63 | createStoryNodes(newResults.data, 'NewStories') 64 | createStoryNodes(bestResults.data, 'BestStories') 65 | } 66 | 67 | const createBuildMetadataSource = ({ createNode }) => { 68 | const buildMetadataNode = { 69 | // There is only one record 70 | id: `I am the build metadata source id`, 71 | parent: null, 72 | internal: { type: `BuildMetadata` }, 73 | children: [], 74 | // Unix time format to be consistent with HackerNews API date format 75 | buildDate: new Date().getTime() / 1000, 76 | } 77 | 78 | buildMetadataNode.internal.contentDigest = buildContentDigest( 79 | buildMetadataNode 80 | ) 81 | createNode(buildMetadataNode) 82 | } 83 | 84 | exports.sourceNodes = async ({ actions }) => { 85 | await createBuildMetadataSource(actions) 86 | await createStoriesSource(actions) 87 | } 88 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shanc", 3 | "description": "SHaNc - Static Hacker News clone", 4 | "version": "2.0.0", 5 | "author": "Sung Kim", 6 | "dependencies": { 7 | "axios": "^0.21.1", 8 | "babel-eslint": "^10.1.0", 9 | "eslint": "^7.18.0", 10 | "eslint-plugin-react": "^7.22.0", 11 | "gatsby": "^2.31.1", 12 | "gatsby-cli": "^2.18.0", 13 | "gatsby-link": "^2.10.0", 14 | "gatsby-plugin-favicon": "^3.1.6", 15 | "gatsby-plugin-google-analytics": "^2.10.0", 16 | "gatsby-plugin-react-helmet": "^3.9.0", 17 | "gatsby-plugin-styled-components": "^3.9.0", 18 | "moment": "2.29.1", 19 | "react": "^17.0.1", 20 | "react-dom": "^17.0.1", 21 | "react-helmet": "^6.1.0", 22 | "react-router-dom": "^5.2.0", 23 | "smooth-ui": "^4.3.2", 24 | "styled-components": "^5.2.1", 25 | "url": "^0.11.0" 26 | }, 27 | "keywords": [ 28 | "gatsby, hackernews, clone" 29 | ], 30 | "license": "MIT", 31 | "scripts": { 32 | "build": "gatsby build", 33 | "develop": "gatsby develop", 34 | "start": "npm run develop", 35 | "format": "prettier --write 'src/**/*.js'", 36 | "lint": "eslint ." 37 | }, 38 | "devDependencies": { 39 | "prettier": "^2.2.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/BuildInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import PropTypes from 'prop-types' 4 | 5 | const Container = styled.div` 6 | background: #ffc633; 7 | margin: 0 auto; 8 | width: 100%; 9 | padding: 1.45rem 1.0875rem; 10 | ` 11 | 12 | const BuildInfo = ({ metadata }) => { 13 | const { buildDate } = metadata 14 | const builtOn = new Date(buildDate * 1000) 15 | return Generated on {builtOn.toUTCString()} 16 | } 17 | 18 | BuildInfo.propTypes = { 19 | metadata: PropTypes.object.isRequired, 20 | } 21 | 22 | export default BuildInfo 23 | -------------------------------------------------------------------------------- /src/components/GithubCorner.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | const GithubLink = styled.a.attrs({ 5 | href: 'https://github.com/dance2die/SHANc', 6 | target: '_blank', 7 | className: 'github-corner', 8 | })`` 9 | 10 | // http://tholman.com/github-corners/ 11 | const Octocat = styled.svg.attrs({ viewBox: '0 0 250 250' })` 12 | position: absolute; 13 | top: 0; 14 | right: 0; 15 | border: 0; 16 | 17 | width: 80px; 18 | height: 80px; 19 | 20 | fill: #151513; 21 | color: #fff; 22 | 23 | z-index: 1; 24 | 25 | &:hover .octo-arm { 26 | animation: octocat-wave 560ms ease-in-out; 27 | } 28 | 29 | @keyframes octocat-wave { 30 | 0%, 31 | 100% { 32 | transform: rotate(0); 33 | } 34 | 20%, 35 | 60% { 36 | transform: rotate(-25deg); 37 | } 38 | 40%, 39 | 80% { 40 | transform: rotate(10deg); 41 | } 42 | } 43 | 44 | @media (max-width: 500px) { 45 | &:hover .octo-arm { 46 | animation: none; 47 | } 48 | & .octo-arm { 49 | animation: octocat-wave 560ms ease-in-out; 50 | } 51 | } 52 | ` 53 | 54 | const GithubCorner = () => ( 55 | 56 | 57 | 58 | 64 | 69 | 70 | 71 | ) 72 | 73 | export default GithubCorner 74 | -------------------------------------------------------------------------------- /src/components/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'gatsby-link' 3 | import styled from 'styled-components' 4 | 5 | const List = styled.ul` 6 | list-style-type: none; 7 | display: flex; 8 | justify-content: flex-start; 9 | margin: 0 auto 10px; 10 | max-width: 960px; 11 | padding-left: 0; 12 | ` 13 | 14 | const ListItem = styled.li` 15 | margin: 0 5px; 16 | ` 17 | 18 | const Navigation = () => ( 19 | 20 | 21 | Top 22 | 23 | 24 | New 25 | 26 | 27 | Best 28 | 29 | 30 | ) 31 | 32 | export default Navigation 33 | -------------------------------------------------------------------------------- /src/components/Stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { OutboundLink } from 'gatsby-plugin-google-analytics' 3 | import styled from 'styled-components' 4 | import parser from 'url' 5 | 6 | import Time from '../components/Time' 7 | 8 | const Story = styled.div` 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-start; 12 | padding: 4px 0; 13 | line-height: 18px; 14 | /* Give each story more room */ 15 | margin-bottom: 0.5rem; 16 | 17 | &:hover { 18 | background-color: #ffc6001a; 19 | } 20 | ` 21 | const Rank = styled.span` 22 | color: #ccc; 23 | font-size: 1.2rem; 24 | width: 35px; 25 | margin-right: 10px; 26 | display: flex; 27 | justify-content: flex-start; 28 | margin-right: 2rem; 29 | ` 30 | 31 | const Content = styled.div`` 32 | const Body = styled.div`` 33 | 34 | const Meta = styled.div` 35 | font-size: 0.7rem; 36 | color: #828282; 37 | ` 38 | 39 | // Use gatsby-plugin-google-analytics plugin to track outbound clicks 40 | // https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-google-analytics#outboundlink-component 41 | const BaseLink = styled(OutboundLink).attrs({ 42 | target: '_blank', 43 | })` 44 | &:link { 45 | text-decoration: none; 46 | } 47 | &:hover { 48 | text-decoration: underline; 49 | } 50 | &:visited { 51 | color: #ddd; 52 | } 53 | ` 54 | const TitleLink = styled(BaseLink)` 55 | color: #464134; 56 | cursor: pointer; 57 | ` 58 | 59 | const HostLink = styled(BaseLink)` 60 | font-size: 0.7rem; 61 | color: #828282; 62 | margin-left: 5px; 63 | ` 64 | 65 | class Stories extends React.Component { 66 | static SHOW_ALL_DATES = 0 67 | static SECONDS_IN_MILLISECONDS = 1000 68 | static NOW = new Date() 69 | 70 | state = { 71 | stories: this.props.stories, 72 | showDaysUpto: Stories.SHOW_ALL_DATES, 73 | } 74 | 75 | // For filtering null node items while building stories 76 | nullNodeItems = ({ node }) => node.item !== null 77 | 78 | // For filtering by dates while building stories 79 | byDates = ({ node }, index) => { 80 | const { showDaysUpto } = this.state 81 | 82 | if (showDaysUpto === 0) return true 83 | else { 84 | const postDate = new Date( 85 | node.item.time * Stories.SECONDS_IN_MILLISECONDS 86 | ) 87 | const hoursDifference = Math.abs(Stories.NOW - postDate) / 36e5 88 | 89 | return hoursDifference <= showDaysUpto * 24 90 | } 91 | } 92 | 93 | buildStoriesComponents = () => { 94 | const { stories } = this.state 95 | 96 | return stories 97 | .filter(this.nullNodeItems) 98 | .filter(this.byDates) 99 | .map(({ node }, index) => { 100 | const { title, score, by, time, url } = node.item 101 | 102 | const commentLink = `//news.ycombinator.com/item?id=${node.storyId}` 103 | const host = parser.parse(url || '').host 104 | // Some stories (Jobs, ASK, etc) don't have URLs then use comment URL 105 | const titleUrl = url || commentLink 106 | 107 | const date = new Date(time * Stories.SECONDS_IN_MILLISECONDS) 108 | const rank = (index + 1).toString().padStart(3, '0') 109 | 110 | return ( 111 | 112 | {rank} 113 | 114 | 115 | {title} 116 | {host ? ({host}) : null} 117 | 118 | 119 | {score} points by {by} [ 123 | 124 | ) 125 | }) 126 | } 127 | 128 | handleDateFilter = e => { 129 | this.setState({ showDaysUpto: parseInt(e.target.value) }) 130 | } 131 | 132 | render() { 133 | const storiesComponents = this.buildStoriesComponents() 134 | 135 | return
{storiesComponents}
136 | } 137 | } 138 | 139 | export default Stories 140 | -------------------------------------------------------------------------------- /src/components/Time.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import moment from 'moment' 3 | 4 | import { getLocaleDateString } from '../util/Format' 5 | 6 | class Time extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | const { date } = this.props 10 | const relativeTime = moment(date).fromNow() 11 | const absoluteTime = `${getLocaleDateString(date)}` 12 | 13 | this.state = { 14 | relativeTime, 15 | absoluteTime, 16 | time: relativeTime, 17 | } 18 | } 19 | 20 | handleMouseOver = () => this.setState({ time: this.state.absoluteTime }) 21 | handleMouseOut = () => this.setState({ time: this.state.relativeTime }) 22 | 23 | render() { 24 | const { time } = this.state 25 | return ( 26 | 27 | {time} 28 | 29 | ) 30 | } 31 | } 32 | 33 | export default Time 34 | -------------------------------------------------------------------------------- /src/components/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'gatsby-link' 3 | import styled from 'styled-components' 4 | 5 | import Time from './Time' 6 | import { getLocaleDateString } from '../util/Format' 7 | 8 | const HeaderContainer = styled.div` 9 | /* background: #ffc600; */ 10 | background-image: linear-gradient(to right top, #ffc600, #ff9d1a, #ff7335, #f5494a, #db1f5d); 11 | font-size: 1.5rem; 12 | margin-bottom: 15px; 13 | ` 14 | 15 | const TitleContainer = styled.div` 16 | margin: 0 auto; 17 | max-width: 960px; 18 | padding: 1.45rem 1.0875rem; 19 | ` 20 | 21 | const Title = styled.h1` 22 | margin-bottom: 10px; 23 | ` 24 | 25 | const DescriptionContainer = styled.div` 26 | margin: 0; 27 | font-size: 1.1rem; 28 | ` 29 | 30 | const linkStyle = { 31 | color: 'white', 32 | textDecoration: 'none', 33 | } 34 | 35 | const BuildInfoContainer = styled.div` 36 | margin: 10px auto -10px; 37 | width: 100%; 38 | font-size: 0.75rem; 39 | color: #ffe; 40 | display: flex; 41 | flex-wrap: wrap; 42 | ` 43 | 44 | const BuildInfo = ({ metadata }) => { 45 | const { buildDate } = metadata 46 | const builtOn = new Date(buildDate * 1000) 47 | return ( 48 | 49 | 50 | Generated 52 | ({getLocaleDateString(builtOn)}) 53 | 54 | ) 55 | } 56 | 57 | const Header = ({ siteTitle, description, metadata }) => ( 58 | 59 | 60 | 61 | <Link to="/" style={linkStyle}> 62 | {siteTitle} 63 | </Link> 64 | 65 | 66 | {description} 67 | 68 | 69 | 70 | 71 | ) 72 | 73 | export default Header 74 | -------------------------------------------------------------------------------- /src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Helmet} from 'react-helmet' 3 | import { graphql, useStaticQuery } from 'gatsby' 4 | import styled from 'styled-components' 5 | 6 | import Header from '../components/header' 7 | import GithubCorner from '../components/GithubCorner' 8 | 9 | const Body = styled.div` 10 | margin: 0 auto; 11 | max-width: 960px; 12 | padding: 0px 1.0875rem 1.45rem; 13 | padding-top: 0; 14 | ` 15 | 16 | const Layout = ({ children }) => { 17 | const data = useStaticQuery(graphql` 18 | { 19 | site { 20 | siteMetadata { 21 | title 22 | description 23 | } 24 | } 25 | buildMetadata { 26 | buildDate 27 | } 28 | } 29 | `) 30 | 31 | return ( 32 |
33 | 43 |
48 | 49 | {children} 50 |
51 | ) 52 | } 53 | 54 | export default Layout 55 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dance2die/SHANc/ae91438417e95cacb85385e46a0d42d1dfcd77e5/src/favicon.png -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NotFoundPage = () => ( 4 |
5 |

NOT FOUND

6 |

You just hit a route that doesn't exist... the sadness.

7 |
8 | ) 9 | 10 | export default NotFoundPage 11 | -------------------------------------------------------------------------------- /src/pages/best.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | 4 | import Stories from '../components/Stories' 5 | 6 | const BestPage = ({ data }) => ( 7 | 8 | ) 9 | 10 | export default BestPage 11 | 12 | export const query = graphql` 13 | query BestStoriesQuery { 14 | allBestStories { 15 | edges { 16 | node { 17 | id 18 | storyId 19 | item { 20 | id 21 | title 22 | score 23 | by 24 | time 25 | type 26 | url 27 | } 28 | } 29 | } 30 | } 31 | } 32 | ` 33 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import { createGlobalStyle } from 'styled-components' 4 | 5 | import Stories from '../components/Stories' 6 | import Layout from '../components/layout' 7 | 8 | const GlobalStyle = createGlobalStyle` 9 | body { 10 | margin: 0; 11 | font-family: 'Verdana'; 12 | letter-spacing: 0.05rem; 13 | } 14 | ` 15 | 16 | const IndexPage = ({ data }) => ( 17 | 18 | 19 | 20 | 21 | ) 22 | 23 | export default IndexPage 24 | 25 | export const query = graphql` 26 | query StoriesQuery { 27 | allTopStories { 28 | edges { 29 | node { 30 | id 31 | storyId 32 | item { 33 | id 34 | title 35 | score 36 | by 37 | time 38 | type 39 | url 40 | } 41 | } 42 | } 43 | } 44 | } 45 | ` 46 | -------------------------------------------------------------------------------- /src/pages/new.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | 4 | import Stories from '../components/Stories' 5 | 6 | const NewPage = ({ data }) => ( 7 | 8 | ) 9 | 10 | export default NewPage 11 | 12 | export const query = graphql` 13 | query NewStoriesQuery { 14 | allNewStories { 15 | edges { 16 | node { 17 | id 18 | storyId 19 | item { 20 | id 21 | title 22 | score 23 | by 24 | time 25 | type 26 | url 27 | } 28 | } 29 | } 30 | } 31 | } 32 | ` 33 | -------------------------------------------------------------------------------- /src/util/Format.js: -------------------------------------------------------------------------------- 1 | const getLocaleDateString = date => { 2 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString 3 | const localeOptions = { 4 | weekday: 'long', 5 | year: 'numeric', 6 | month: 'long', 7 | day: 'numeric', 8 | hour: 'numeric', 9 | minute: 'numeric', 10 | second: 'numeric', 11 | timeZoneName: 'short', 12 | } 13 | return date.toLocaleDateString('en-US', localeOptions) 14 | } 15 | 16 | export { getLocaleDateString } 17 | --------------------------------------------------------------------------------