Add Gatsby to build site (#887) [ci skip]

This commit is contained in:
Callum Macdonald 2020-09-10 17:22:55 +02:00 committed by GitHub
parent 42ec2aa9c2
commit 43958025e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 15249 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.gtm/

10
gatsby/.editorconfig Normal file
View File

@ -0,0 +1,10 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

5
gatsby/.eslintignore Normal file
View File

@ -0,0 +1,5 @@
.cache
package.json
package-lock.json
public
node_modules

40
gatsby/.eslintrc Normal file
View File

@ -0,0 +1,40 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"prettier"
],
"plugins": [
"@typescript-eslint",
"react",
"react-hooks",
"prettier"
],
"env": {
"browser": true,
"es6": true,
"node": true
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"prettier/prettier": "error",
"react/prop-types": 0,
"import/prefer-default-export": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off"
}
}

69
gatsby/.gitignore vendored Normal file
View File

@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# dotenv environment variable files
.env*
# gatsby files
.cache/
public
# Mac files
.DS_Store
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity

5
gatsby/.prettierignore Normal file
View File

@ -0,0 +1,5 @@
.cache
package.json
package-lock.json
public
node_modules

8
gatsby/.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"arrowParens": "avoid",
"semi": false,
"printWidth": 80,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}

53
gatsby/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,53 @@
{
"editor.formatOnSave": true,
"files.encoding": "utf8",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"search.exclude": {
"public/**": true,
"node_modules/**": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"files.exclude": {
"**/logs": true,
"**/*.log": true,
"**/npm-debug.log*": true,
"**/yarn-debug.log*": true,
"**/yarn-error.log*": true,
"**/pids": true,
"**/*.pid": true,
"**/*.seed": true,
"**/*.pid.lock": true,
"**/lib-cov": true,
"**/coverage": true,
"**/.nyc_output": true,
"**/.grunt": true,
"**/bower_components": true,
"**/.lock-wscript": true,
"build/Release": true,
"**/node_modules/": true,
"**/jspm_packages/": true,
"**/typings/": true,
"**/.npm": true,
"**/.eslintcache": true,
"**/.node_repl_history": true,
"**/*.tgz": true,
"**/.env*": true,
"**/.cache/": true,
"**/public": true,
"**/.DS_Store": true,
"**/yarn-error.log": true,
"**/.pnp/": true,
"**/.pnp.js": true,
"**/.yarn-integrity": true
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll": true
}
}

22
gatsby/LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2020 junscuzzy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

117
gatsby/README.md Normal file
View File

@ -0,0 +1,117 @@
# Gatsby's Typescript + Material-ui starter
[![Netlify Status](https://api.netlify.com/api/v1/badges/1b625068-4ac6-42d5-87fb-902d9077bbef/deploy-status)](https://app.netlify.com/sites/gatsby-material-typescript-starter/deploys)
Kick off your project with this [Material-ui](https://material-ui.com/) boilerplate. This starter ships with the main Gatsby configuration files you might need to get up and running blazing fast with the blazing fast app generator for React.
It includes support for Typescript in front-side and node-side and uses Eslint & Prettier.
This starter don't have any source or style supports, it's your choice.
_Have another more specific idea? You may want to check out our vibrant collection of [official and community-created starters](https://www.gatsbyjs.org/docs/gatsby-starters/)._
## 🚀 Quick start
1. **Create a Gatsby site.**
Use the Gatsby CLI to create a new site, specifying the default starter.
```shell
# create a new Gatsby site using the starter
gatsby new gatsby-material-typescript-starter https://github.com/Junscuzzy/gatsby-material-typescript-starter
```
1. **Start developing.**
Navigate into your new sites directory and start it up.
```shell
cd gatsby-material-typescript-starter/
yarn develop
```
1. **Open the source code and start editing!**
Your site is now running at `http://localhost:8000`!
_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)._
Open the `gatsby-material-typescript-starter` directory in your code editor of choice and edit `src/pages/index.tsx`. Save your changes and the browser will update in real time!
1. **Bonus**: Check all linters using
```shell
yarn lint
```
Will execute Prettier, Eslint and Typescript checking
All the commands are in your `package.json > scripts`.
## 🧐 What's inside?
A quick look at the top-level files and directories you'll see in a Gatsby project.
.
├── .vscode/
├── node_modules/
├── src/
├── static/
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .prettierrc
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── gatsby-ssr.js
├── LICENSE
├── package.json
├── README.md
├── tsconfig.json
└── yarn.lock
1. **`/.vscode`**: VSCode projects settings.
1. **`/node_modules`**: This directory contains all of the modules of code that your project depends on (npm packages) are automatically installed.
1. **`/src`**: This directory will contain all of the code related to what you will see on the front-end of your site (what you see in the browser) such as your site header or a page template. `src` is a convention for “source code”.
1. **`/static`**: Static files like `robots.txt` or `favicon.ico`.
1. **`.editorconfig`**: EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs.
1. **`.eslintrc`**: This is a configuration file for [Eslint](https://eslint.org/). Find and fix problems in your JavaScript code
1. **`.gitignore`**: This file tells git which files it should not track / not maintain a version history for.
1. **`.prettierrc`**: This is a configuration file for [Prettier](https://prettier.io/). Prettier is a tool to help keep the formatting of your code consistent.
1. **`gatsby-browser.js`**: This file is where Gatsby expects to find any usage of the [Gatsby browser APIs](https://www.gatsbyjs.org/docs/browser-apis/) (if any). These allow customization/extension of default Gatsby settings affecting the browser.
1. **`gatsby-config.js`**: This is the main configuration file for a Gatsby site. This is where you can specify information about your site (metadata) like the site title and description, which Gatsby plugins youd like to include, etc. (Check out the [config docs](https://www.gatsbyjs.org/docs/gatsby-config/) for more detail).
1. **`gatsby-node.js`**: This file is where Gatsby expects to find any usage of the [Gatsby Node APIs](https://www.gatsbyjs.org/docs/node-apis/) (if any). These allow customization/extension of default Gatsby settings affecting pieces of the site build process.
1. **`gatsby-ssr.js`**: This file is where Gatsby expects to find any usage of the [Gatsby server-side rendering APIs](https://www.gatsbyjs.org/docs/ssr-apis/) (if any). These allow customization of default Gatsby settings affecting server-side rendering.
1. **`LICENSE`**: Gatsby is licensed under the MIT license.
1. **`package.json`**: A manifest file for Node.js projects, which includes things like metadata (the projects name, author, etc). This manifest is how npm knows which packages to install for your project.
1. **`README.md`**: A text file containing useful reference information about your project.
1. **`tsconfig.json`**: This is a configuration file for [Typescript](https://www.typescriptlang.org/). TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
1. **`yarn.lock`** (See `package.json` below, first). This is an automatically generated file based on the exact versions of your npm dependencies that were installed for your project. **(You wont change this file directly).**
## 🎓 Learning Gatsby
Looking for more guidance? Full documentation for Gatsby lives [on the website](https://www.gatsbyjs.org/). Here are some places to start:
- **For most developers, we recommend starting with our [in-depth tutorial for creating a site with Gatsby](https://www.gatsbyjs.org/tutorial/).** It starts with zero assumptions about your level of ability and walks through every step of the process.
- **To dive straight into code samples, head [to our documentation](https://www.gatsbyjs.org/docs/).** In particular, check out the _Guides_, _API Reference_, and _Advanced Tutorials_ sections in the sidebar.
## 💫 Deploy
As a static generated website, you can deploy it on [Netlify](https://www.netlify.com), [Github Page](https://pages.github.com/) or [ZEIT Now](https://zeit.co/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

8
gatsby/gatsby-browser.js Normal file
View File

@ -0,0 +1,8 @@
/**
* Implement Gatsby's Browser APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/browser-apis/
*/
export { default as wrapRootElement } from './src/libs/wrapRootElement'
export { default as wrapPageElement } from './src/libs/wrapPageElement'

56
gatsby/gatsby-config.js Normal file
View File

@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const join = require('path').join
module.exports = {
pathPrefix: '/open-source-ios/',
siteMetadata: {
title: `Open Source iOS Apps`,
description: `A collaborative list of open-source iOS, watchOS and tvOS apps.`,
},
plugins: [
`gatsby-plugin-typescript`,
`gatsby-plugin-react-helmet`,
{
resolve: 'gatsby-plugin-material-ui',
// If you want to use styled components you should change the injection order.
options: {
// stylesProvider: {
// injectFirst: true,
// },
},
},
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `open-source-ios-apps`,
short_name: `os-ios-apps`,
start_url: `/`,
background_color: `#663399`,
theme_color: `#3E3F3A`,
display: `minimal-ui`,
},
},
`gatsby-transformer-json`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `apps`,
path: join(__dirname, `../`),
// This aims to ignore everything EXCEPT the `contents.json` file at the
// root of the repo
ignore: [
`**/.*`,
`**/.*/**`,
`**/gatsby/**`,
`**/*.md`,
`**/*.toml`,
`**/LICENSE`,
`**/Dangerfile`,
],
},
},
// this (optional) plugin enables Progressive Web App + Offline functionality
// To learn more, visit: https://gatsby.dev/offline
// `gatsby-plugin-offline`,
],
}

142
gatsby/gatsby-node.js Normal file
View File

@ -0,0 +1,142 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path')
const crypto = require('crypto')
const Bluebird = require('bluebird')
// Set this to true to enable more logging in this file
const DEBUG = false
const isDev = process.env.NODE_ENV === 'development'
/**
* Implement Gatsby's Node APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/node-apis/
*/
// You can delete this file if you're not using it
exports.onCreateNode = async ({ node, actions }) => {
const { createNode } = actions
if (node.internal.type === 'OpenSourceIosAppsJson') {
const { categories, projects } = node
categories.forEach(category => {
if (typeof category.id !== 'string' || category.id.length < 1) {
console.error('Invalid category #veJYyW', category)
return
}
// Count how many projects are in each category. We can't easily do this
// in Gatsby's GraphQL API.
const projectCount = projects.filter(project =>
project['category-ids'].includes(category.id),
).length
createNode({
...category,
slug: category.id,
parentSlug: category.parent,
projectCount,
id: `Category__${category.id}`,
parent: node.id,
internal: {
type: `AppCategory`,
contentDigest: crypto
.createHash(`md5`)
.update(JSON.stringify(category))
.digest(`hex`),
content: JSON.stringify(category),
},
})
})
await Bluebird.each(projects, async project => {
const contentDigest = crypto
.createHash(`md5`)
.update(JSON.stringify(project))
.digest(`hex`)
const id = `Project__${contentDigest}`
const projectNode = {
...project,
parent: node.id,
id,
internal: {
type: `AppProject`,
contentDigest,
content: JSON.stringify(project),
},
}
await createNode(projectNode)
})
}
}
exports.createPages = async ({ actions, graphql }) => {
const { createPage } = actions
const categoryTemplate = path.resolve('src/templates/category.tsx')
const tagTemplate = path.resolve('src/templates/tag.tsx')
const categoryResult = await graphql(`
query Categories {
allAppCategory {
edges {
node {
id
title
slug
}
}
}
}
`)
const categories = categoryResult.data.allAppCategory.edges
categories.forEach(({ node: category }) => {
createPage({
path: `/category/${category.slug}/`,
component: categoryTemplate,
context: {
id: category.id,
slug: category.slug,
},
})
})
const projectResult = await graphql(`
query Projects {
allAppProject {
edges {
node {
id
tags
}
}
}
}
`)
const projects = projectResult.data.allAppProject.edges
const tags = new Set()
projects.forEach(({ node: project }) => {
if (project.tags && project.tags.length > 0) {
project.tags.forEach(tag => {
tags.add(tag)
})
}
})
tags.forEach(tag => {
createPage({
path: `/tag/${tag}/`,
component: tagTemplate,
context: {
tag,
slug: tag,
},
})
})
}

8
gatsby/gatsby-ssr.js Normal file
View File

@ -0,0 +1,8 @@
/**
* Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/ssr-apis/
*/
export { default as wrapRootElement } from './src/libs/wrapRootElement'
export { default as wrapPageElement } from './src/libs/wrapPageElement'

9
gatsby/netlify.toml Normal file
View File

@ -0,0 +1,9 @@
[[plugins]]
package = "netlify-plugin-gatsby-cache"
[build.environment]
SHARP_IGNORE_GLOBAL_LIBVIPS="true"
[build]
publish = "public/"
command = "yarn run build"

76
gatsby/package.json Normal file
View File

@ -0,0 +1,76 @@
{
"name": "gatsby-material-typescript-starter",
"private": true,
"description": "A simple starter using Typescript & Material-ui",
"version": "0.1.0",
"author": "Julien CARON <juliencaron@protonmail.com>",
"keywords": [
"gatsby",
"typescript",
"eslint",
"prettier",
"material-ui"
],
"license": "MIT",
"scripts": {
"build": "NODE_ENV=production gatsby build",
"develop": "gatsby develop",
"deploy-gh": "yarn build --prefix-paths && yarn gh-pages -d public",
"start": "yarn develop",
"serve": "gatsby serve",
"clean": "gatsby clean",
"lint": "run-p lint:**",
"lint:prettier": "prettier --write \"**/*.{js,jsx,json,md}\"",
"lint:eslint": "eslint 'src/**/*.{ts,tsx}'",
"lint:typescript": "tsc",
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
},
"dependencies": {
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/styles": "^4.10.0",
"@reduxjs/toolkit": "^1.4.0",
"deepmerge": "^4.2.2",
"gatsby": "^2.24.56",
"gatsby-plugin-manifest": "^2.4.28",
"gatsby-plugin-material-ui": "^2.1.10",
"gatsby-plugin-offline": "^3.2.27",
"gatsby-plugin-react-helmet": "^3.3.10",
"gatsby-plugin-typescript": "^2.4.19",
"gatsby-source-filesystem": "^2.3.29",
"gatsby-transformer-json": "^2.4.11",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-helmet": "^6.1.0",
"react-image-lightbox": "^5.1.1",
"react-redux": "^7.2.1",
"redux": "^4.0.5"
},
"devDependencies": {
"@types/node": "^14.6.4",
"@types/react": "^16.9.49",
"@types/react-dom": "^16.9.8",
"@types/react-helmet": "^6.1.0",
"@types/react-redux": "^7.1.9",
"@types/redux": "^3.6.0",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"bluebird": "^3.7.2",
"eslint": "^7.8.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.0",
"gh-pages": "^3.1.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
},
"repository": {
"type": "git",
"url": "https://github.com/Junscuzzy/gatsby-material-typescript-starter"
},
"bugs": {
"url": "https://github.com/Junscuzzy/gatsby-material-typescript-starter/issues"
}
}

View File

@ -0,0 +1,209 @@
import {
Card,
CardContent,
createStyles,
Divider,
GridList,
GridListTile,
makeStyles,
Theme,
Typography,
} from '@material-ui/core'
import Star from '@material-ui/icons/Star'
import { graphql, Link } from 'gatsby'
import React, { useState } from 'react'
import Lightbox from 'react-image-lightbox'
import 'react-image-lightbox/style.css'
import { Project } from '../types'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
description: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
fontSize: '1.2em',
},
tag: {
marginRight: theme.spacing(1),
},
}),
)
const urlToReaddable = (url: string) => {
if (url.indexOf('https://github.com/') === 0) {
return `@${url.substr('https://github.com/'.length)}`
} else if (url.indexOf('http://github.com/') === 0) {
return `@${url.substr('http://github.com/'.length)}`
} else return url
}
const starCountToIconCount = (githubStars: number): number => {
if (githubStars > 2000) {
return 5
} else if (githubStars > 1000) {
return 4
} else if (githubStars > 500) {
return 3
} else if (githubStars > 200) {
return 2
} else if (githubStars > 100) {
return 1
} else return 0
}
const RepeatingStars = ({ count }: { count: number }) => {
return (
<span>
{Array.from({ length: count }).map((_, i) => (
<Star key={i} />
))}
</span>
)
}
const ProjectCard = ({ project }: { project: Project }) => {
const classes = useStyles()
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
const [lightboxSlide, setLightboxSlide] = useState(0)
const showStarCount = starCountToIconCount(project.stars)
const screenshotSources = project.screenshots || []
const tags = project.tags || []
return (
<Card>
<CardContent>
<Typography variant="h4" component="h2">
{project.title}
</Typography>
{showStarCount > 0 ? <RepeatingStars count={showStarCount} /> : null}
<Typography className={classes.description}>
{project.description}
</Typography>
<Divider />
<Typography>Lang: {project.lang || 'en'}</Typography>
<Typography>Added: {project.date_added}</Typography>
<Typography>GitHub Stars: {project.stars}</Typography>
<Typography>
Code:{' '}
{project.source ? (
<a href={project.source} target="_blank" rel="noreferrer">
{urlToReaddable(project.source)}
</a>
) : (
'n/a'
)}
</Typography>
<Typography>
License:{' '}
{project.license ? (
<a
href={`https://choosealicense.com/licenses/${project.license}/`}
target="_blank"
rel="noreferrer"
>
{project.license}
</a>
) : (
'n/a'
)}
</Typography>
<Typography>
iTunes:{' '}
{project.itunes ? (
<a href={project.itunes} target="_blank" rel="noreferrer">
{project.itunes}
</a>
) : (
'n/a'
)}
</Typography>
<Typography>
Homepage:{' '}
{project.homepage ? (
<a href={project.homepage} target="_blank" rel="noreferrer">
{project.homepage}
</a>
) : (
'n/a'
)}
</Typography>
<Typography>
Tags:{' '}
{tags.map(tag => {
return (
<Link key={tag} to={`/tag/${tag}/`} className={classes.tag}>
#{tag}
</Link>
)
})}
</Typography>
{screenshotSources.length === 0 ? null : (
<>
<GridList cellHeight={160} cols={4}>
{screenshotSources.map((url, i) => {
return (
<GridListTile key={i} cols={1}>
<a
href={url}
target="_blank"
rel="noreferrer"
onClick={event => {
event.preventDefault()
setLightboxSlide(i)
setIsLightboxOpen(true)
}}
>
<img src={url} width="120" height="160" />
</a>
</GridListTile>
)
})}
</GridList>
{!isLightboxOpen ? null : (
<Lightbox
mainSrc={screenshotSources[lightboxSlide]}
nextSrc={screenshotSources[lightboxSlide + 1]}
prevSrc={screenshotSources[lightboxSlide - 1]}
onCloseRequest={() => setIsLightboxOpen(false)}
onMoveNextRequest={() => {
setLightboxSlide(
(lightboxSlide + screenshotSources.length + 1) %
screenshotSources.length,
)
}}
onMovePrevRequest={() => {
setLightboxSlide(
(lightboxSlide + screenshotSources.length - 1) %
screenshotSources.length,
)
}}
/>
)}
</>
)}
</CardContent>
</Card>
)
}
export default ProjectCard
export const projectCardFragment = graphql`
fragment ProjectCardFields on AppProject {
category_ids
date_added
description
homepage
id
itunes
lang
license
screenshots
source
stars
suggested_by
tags
title
}
`

View File

@ -0,0 +1,40 @@
import React, { FC } from 'react'
import { makeStyles } from '@material-ui/styles'
import { Typography, Container, Theme } from '@material-ui/core'
const useStyles = makeStyles((theme: Theme) => ({
heroContent: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(8, 0, 6),
},
}))
export interface HeroProps {
title: string
description?: string
}
const Hero: FC<HeroProps> = ({ title, description = '', children }) => {
const classes = useStyles()
return (
<div className={classes.heroContent}>
<Container maxWidth="md">
<Typography
component="h1"
variant="h2"
align="center"
color="textPrimary"
gutterBottom
>
{title}
</Typography>
<Typography variant="h5" align="center" color="textSecondary" paragraph>
{description}
</Typography>
{children}
</Container>
</div>
)
}
export default Hero

View File

@ -0,0 +1,93 @@
/**
* SEO component that queries for data with
* Gatsby's useStaticQuery React hook
*
* See: https://www.gatsbyjs.org/docs/use-static-query/
*/
import React, { FC } from 'react'
import { Helmet } from 'react-helmet'
import { useStaticQuery, graphql } from 'gatsby'
interface MetaProperty {
property: string
content: string
}
interface MetaName {
name: string
content: string
}
type Meta = MetaName | MetaProperty
export interface SEOProps {
title: string
description?: string
lang?: string
meta?: Meta[]
}
const SEO: FC<SEOProps> = ({
description = '',
lang = 'en',
meta = [],
title,
}) => {
const { site } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
}
}
}
`,
)
const metaDescription = description || site.siteMetadata.description
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site.siteMetadata.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
)
}
export default SEO

View File

@ -0,0 +1,21 @@
import { graphql, useStaticQuery } from 'gatsby'
export interface SiteMetadata {
title: string
description: string
}
export default (): SiteMetadata => {
const data = useStaticQuery(graphql`
{
site {
siteMetadata {
title
description
}
}
}
`)
return data.site.siteMetadata
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,34 @@
import React, { FC } from 'react'
import { makeStyles } from '@material-ui/styles'
import { Container, Typography, Theme, Link } from '@material-ui/core'
const useStyles = makeStyles((theme: Theme) => ({
footer: {
padding: theme.spacing(3, 2),
marginTop: theme.spacing(4),
backgroundColor: theme.palette.background.paper,
},
}))
const Footer: FC = () => {
const classes = useStyles()
return (
<footer className={classes.footer}>
<Container maxWidth="md">
<Typography variant="body1" color="textSecondary">
Built with the data from{' '}
<Link
href="https://github.com/dkhamsing/open-source-ios-apps"
target="_blank"
rel="noreferrer"
>
@dkhamsing/open-source-ios-apps
</Link>
</Typography>
</Container>
</footer>
)
}
export default Footer

View File

@ -0,0 +1,50 @@
import { AppBar, Button, Link, Toolbar, Typography } from '@material-ui/core'
import { makeStyles } from '@material-ui/styles'
import { Link as GatsbyLink } from 'gatsby'
import React, { FC } from 'react'
const useStyles = makeStyles({
toolbar: {},
title: {
flexGrow: 1,
},
link: {
textDecoration: 'none',
},
})
export interface HeaderProps {
siteTitle?: string
}
const Header: FC<HeaderProps> = ({ siteTitle = '' }) => {
const classes = useStyles()
return (
<AppBar component="header" position="static">
<Toolbar className={classes.toolbar}>
<Typography variant="h6" className={classes.title}>
<Link
to="/"
component={GatsbyLink}
color="inherit"
className={classes.link}
>
{siteTitle}
</Link>
</Typography>
<Button
color="inherit"
component="a"
href="https://github.com/dkhamsing/open-source-ios-apps"
target="_blank"
rel="noreferrer"
>
GitHub
</Button>
</Toolbar>
</AppBar>
)
}
export default Header

View File

@ -0,0 +1,46 @@
import React, { FC } from 'react'
import { makeStyles, ThemeProvider } from '@material-ui/styles'
import { Theme } from '@material-ui/core'
import { useSelector } from 'react-redux'
import Header from './header'
import Footer from './footer'
import themes from '../theme'
import useSiteMetadata from '../hooks/useSiteMetadata'
import { RootState } from '../redux/store'
const useStyles = makeStyles((theme: Theme) => ({
root: {
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
backgroundColor: theme.palette.background.default,
},
main: {
backgroundColor: theme.palette.background.default,
},
}))
const LayoutComponent: FC = ({ children }) => {
const classes = useStyles()
const { title } = useSiteMetadata()
return (
<div className={classes.root}>
<Header siteTitle={title} />
<main className={classes.main}>{children}</main>
<Footer />
</div>
)
}
const Layout: FC = ({ children }) => {
const { theme } = useSelector((state: RootState) => state.app)
return (
<ThemeProvider theme={themes[theme]}>
<LayoutComponent>{children}</LayoutComponent>
</ThemeProvider>
)
}
export default Layout

View File

@ -0,0 +1,17 @@
import React, { ReactNode } from 'react'
import { ThemeProvider } from '@material-ui/styles'
import { CssBaseline } from '@material-ui/core'
import themes from '../theme'
import Layout from '../layout'
const wrapPageElement = ({ element }: { element: ReactNode }) => {
return (
<ThemeProvider theme={themes['light']}>
<CssBaseline />
<Layout>{element}</Layout>
</ThemeProvider>
)
}
export default wrapPageElement

View File

@ -0,0 +1,10 @@
import React, { ReactNode } from 'react'
import { Provider } from 'react-redux'
import store from '../redux/store'
const wrapRootElement = ({ element }: { element: ReactNode }) => {
return <Provider store={store}>{element}</Provider>
}
export default wrapRootElement

30
gatsby/src/pages/404.tsx Normal file
View File

@ -0,0 +1,30 @@
import React, { FC } from 'react'
import { makeStyles } from '@material-ui/styles'
import { Theme } from '@material-ui/core'
import SEO from '../components/seo'
import { Typography, Container } from '@material-ui/core'
const useStyles = makeStyles((theme: Theme) => ({
root: {
marginTop: theme.spacing(8),
marginBottom: theme.spacing(2),
},
}))
const NotFoundPage: FC = () => {
const classes = useStyles()
return (
<Container maxWidth="md" className={classes.root}>
<SEO title="404: Not found" />
<Typography variant="h2" gutterBottom component="h1">
NOT FOUND
</Typography>
<Typography variant="body1">
You just hit a route that doesn&#39;t exist... the sadness.
</Typography>
</Container>
)
}
export default NotFoundPage

158
gatsby/src/pages/index.tsx Normal file
View File

@ -0,0 +1,158 @@
import {
Button,
Card,
CardContent,
Grid,
List,
ListItem,
ListItemText,
Theme,
Typography,
} from '@material-ui/core'
import { makeStyles } from '@material-ui/styles'
import { graphql, Link } from 'gatsby'
import React, { FC } from 'react'
import Hero from '../components/hero'
import SEO from '../components/seo'
import { Category } from '../types'
const useStyles = makeStyles((theme: Theme) => ({
wrapper: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
button: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
}))
const CategoryItem = ({
category,
categories,
}: {
category: Category
categories: Category[]
}) => {
const classes = useStyles()
const childCategories = categories.filter(
cat => cat.parentSlug === category.slug,
)
return (
<Grid item xs={12} sm={6} md={4}>
<Card>
<CardContent>
<Typography variant="h3">{category.title}</Typography>
<Typography>{category.description}</Typography>
<Typography>{category.projectCount} projects</Typography>
<Button
variant="contained"
component={Link}
fullWidth
to={`/category/${category.slug}/`}
className={classes.button}
>
Browse {category.title}
</Button>
{childCategories.length === 0 ? null : (
<>
<Typography variant="h5" component="h4">
Child categories:
</Typography>
<List>
{childCategories.map(childCategory => {
return (
<ListItem
button
key={childCategory.id}
component={Link}
to={`/category/${childCategory.slug}/`}
>
<ListItemText
primary={`${childCategory.title} (${childCategory.projectCount} projects)`}
/>
</ListItem>
)
})}
</List>
</>
)}
</CardContent>
</Card>
</Grid>
)
}
type IndexPageProps = {
data: {
allAppCategory: {
edges: {
node: Category
}[]
}
allAppProject: {
totalCount: number
}
}
}
const IndexPage: FC<IndexPageProps> = props => {
const classes = useStyles()
const { edges: categoryEdges } = props.data.allAppCategory
const projectCount = props.data.allAppProject.totalCount
const categories = categoryEdges.map(e => e.node)
const topLevelCategories = categories.filter(category => {
return category.parentSlug === null
})
return (
<>
<SEO title="Home" />
<Hero
title="Open Source iOS Apps"
description={`A community curated set of ${projectCount} open source iOS apps.`}
/>
<div className={classes.wrapper}>
<Grid container spacing={2}>
{topLevelCategories.map(cat => {
return (
<CategoryItem
key={cat.id}
category={cat}
categories={categories}
/>
)
})}
</Grid>
</div>
</>
)
}
export const pageQuery = graphql`
query IndexPageQuery {
allAppCategory {
edges {
node {
id
slug
parentSlug
projectCount
description
title
}
}
}
allAppProject {
totalCount
}
}
`
export default IndexPage

View File

@ -0,0 +1,24 @@
import { createSlice } from '@reduxjs/toolkit'
export interface AppState {
theme: 'light' | 'dark'
}
const initialState: AppState = {
theme: 'light',
}
const app = createSlice({
name: 'app',
initialState,
reducers: {
toggleTheme(state) {
const newTheme = state.theme === 'light' ? 'dark' : 'light'
state.theme = newTheme
},
},
})
export const { toggleTheme } = app.actions
export default app.reducer

View File

@ -0,0 +1,23 @@
import { RootState } from './store'
export const loadState: () => RootState = () => {
try {
const serializedState = localStorage.getItem('state')
if (serializedState === null || serializedState === 'undefined') {
return undefined
}
return JSON.parse(serializedState)
} catch (err) {
return undefined
}
}
export const saveState = (state: Partial<RootState>) => {
try {
const serializedState = JSON.stringify(state)
localStorage.setItem('state', serializedState)
} catch (err) {
// Ignore write errors
}
}

26
gatsby/src/redux/store.ts Normal file
View File

@ -0,0 +1,26 @@
import { configureStore } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'
import app from '../redux/appModule'
import { loadState, saveState } from './persistStore'
const preloadedState = loadState()
const rootReducer = combineReducers({
app,
})
const store = configureStore({
reducer: rootReducer,
preloadedState,
})
store.subscribe(() => {
const { app } = store.getState()
saveState({ app })
})
export type RootState = ReturnType<typeof rootReducer>
export type RootDispatch = typeof store.dispatch
export default store

View File

@ -0,0 +1,93 @@
import {
Button,
createStyles,
Grid,
makeStyles,
Theme,
Typography,
} from '@material-ui/core'
import { graphql, Link } from 'gatsby'
import React from 'react'
import Hero from '../components/hero'
import ProjectCard from '../components/ProjectCard'
import SEO from '../components/seo'
import { Category, Project } from '../types'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
wrapper: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
backButton: {
textAlign: 'center',
margin: theme.spacing(4),
},
}),
)
type Props = {
data: {
appCategory: Category
allAppProject: {
edges: {
node: Project
}[]
}
}
}
const CategoryTemplate: React.FC<Props> = props => {
const classes = useStyles()
const category = props.data.appCategory
const projectEdges = props.data.allAppProject.edges
const projects: Project[] = projectEdges.map(n => n.node)
return (
<>
<SEO title="Home" />
<Hero
title={`Category: ${category.title}`}
description={category.description || ''}
></Hero>
<Typography className={classes.backButton}>
<Button component={Link} to="/" variant="contained">
Back to category list
</Button>
</Typography>
<div className={classes.wrapper}>
<Grid container spacing={2}>
{projects.map(project => {
return (
<Grid item xs={12} sm={6} key={project.id}>
<ProjectCard project={project} />
</Grid>
)
})}
</Grid>
</div>
</>
)
}
export default CategoryTemplate
export const pageQuery = graphql`
query CategoryPageQuery($slug: String!) {
appCategory(slug: { eq: $slug }) {
id
description
slug
title
}
allAppProject(filter: { category_ids: { eq: $slug } }) {
edges {
node {
...ProjectCardFields
}
}
}
}
`

View File

@ -0,0 +1,74 @@
import { createStyles, Grid, makeStyles, Theme } from '@material-ui/core'
import { graphql } from 'gatsby'
import React from 'react'
import Hero from '../components/hero'
import ProjectCard from '../components/ProjectCard'
import SEO from '../components/seo'
import { Category, Project } from '../types'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
wrapper: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
backButton: {
textAlign: 'center',
margin: theme.spacing(4),
},
}),
)
type Props = {
data: {
appCategory: Category
allAppProject: {
edges: {
node: Project
}[]
}
}
pageContext: {
tag: string
slug: string
}
}
const TagTemplate: React.FC<Props> = props => {
const classes = useStyles()
const projectEdges = props.data.allAppProject.edges
const projects: Project[] = projectEdges.map(n => n.node)
return (
<>
<SEO title="Home" />
<Hero title={`Tag: #${props.pageContext.tag}`} description=""></Hero>
<div className={classes.wrapper}>
<Grid container spacing={2}>
{projects.map(project => {
return (
<Grid item xs={12} sm={6} key={project.id}>
<ProjectCard project={project} />
</Grid>
)
})}
</Grid>
</div>
</>
)
}
export default TagTemplate
export const pageQuery = graphql`
query TagPageQuery($slug: String!) {
allAppProject(filter: { tags: { eq: $slug } }) {
edges {
node {
...ProjectCardFields
}
}
}
}
`

46
gatsby/src/theme.ts Normal file
View File

@ -0,0 +1,46 @@
import deepMerge from 'deepmerge'
import { red } from '@material-ui/core/colors'
import {
createMuiTheme,
responsiveFontSizes,
ThemeOptions,
Theme,
} from '@material-ui/core/styles'
const makeTheme = (variant: ThemeOptions): Theme => {
const common = {
palette: {
primary: {
main: '#3E3F3A',
},
secondary: {
main: '#19857b',
},
error: {
main: red.A400,
},
},
}
const theme = createMuiTheme(deepMerge(common, variant))
return responsiveFontSizes(theme)
}
const light: ThemeOptions = {
palette: {
type: 'light',
},
}
const dark: ThemeOptions = {
palette: {
type: 'dark',
},
}
const themes = {
light: makeTheme(light),
dark: makeTheme(dark),
}
export default themes

25
gatsby/src/types.ts Normal file
View File

@ -0,0 +1,25 @@
export type Category = {
id: string
slug: string
parentSlug: string | null
projectCount: number
description: string | null
title: string | null
}
export type Project = {
category_ids: string[]
date_added: string
description: string | null
homepage: string | null
id: string
itunes: string | null
lang: string | null
license: string
screenshots: string[]
source: string | null
stars: number
suggested_by: string
tags: string[]
title: string
}

BIN
gatsby/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

2
gatsby/static/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

19
gatsby/tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["dom", "esnext"],
"noImplicitAny": true,
"jsx": "preserve",
"sourceMap": true,
"removeComments": true,
"noEmit": true,
"isolatedModules": true,
"strict": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "public", ".cache"]
}

13578
gatsby/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

2
netlify.toml Normal file
View File

@ -0,0 +1,2 @@
[build]
base = "gatsby"