├── .prettierrc ├── pages ├── index.js ├── 404.js ├── _app.js ├── _document.js └── api │ └── posts │ ├── index.js │ └── [postId].js ├── .gitattributes ├── .babelrc ├── utils.js ├── public ├── favicon.ico └── zeit.svg ├── .eslintrc ├── README.md ├── src ├── components │ ├── GlobalLoader.js │ ├── Sidebar.js │ ├── styled.js │ └── PostForm.js ├── hooks │ ├── useDeletePost.js │ ├── useCreatePost.js │ ├── useSavePost.js │ ├── usePosts.js │ └── usePost.js ├── screens │ ├── blog │ │ ├── Post.js │ │ └── index.js │ └── admin │ │ ├── index.js │ │ └── Post.js └── index.js ├── db └── index.js ├── .gitignore ├── store.json ├── package.json └── next.config.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import App from '../src/' 2 | 3 | export default App 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": ["styled-components"] 4 | } 5 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | export function sleep(time) { 2 | return new Promise((resolve) => setTimeout(resolve, time)) 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tannerlinsley/react-query-cached-in-60-minutes/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["react-app", "prettier"], 4 | "env": { 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "jsx-a11y/anchor-is-valid": 0, 12 | "eqeqeq": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - Run `yarn` and then `yarn dev` locally. 2 | - The Next.js API server is an in-memory local server, so the data is not actually persisted to disk. The posts that are stored or altered will be reset when the server restarts or when a file is saved and the server is recompiled. 3 | - Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 4 | -------------------------------------------------------------------------------- /pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () => ( 4 |
9 |

15 | 404 16 |

17 |

22 | Page not found 23 |

24 |
25 | ) 26 | -------------------------------------------------------------------------------- /src/components/GlobalLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Loader } from './styled' 3 | 4 | export default function GlobalLoader() { 5 | return ( 6 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | 4 | const storeLocation = path.resolve(process.cwd(), 'store.json') 5 | 6 | export default { 7 | set, 8 | get, 9 | } 10 | 11 | async function set(updater) { 12 | const file = await fs.readJSON(storeLocation) 13 | const newFile = updater(file) 14 | await fs.writeJSON(storeLocation, newFile) 15 | } 16 | 17 | function get() { 18 | return fs.readJSON(storeLocation) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | -------------------------------------------------------------------------------- /src/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | import { SidebarStyles } from './styled' 5 | 6 | export default function Sidebar() { 7 | return ( 8 | 9 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/hooks/useDeletePost.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | 4 | export default function useDeletePost() { 5 | const [state, setState] = React.useReducer((_, action) => action, { 6 | isIdle: true, 7 | }) 8 | 9 | const mutate = React.useCallback(async (postId) => { 10 | setState({ isLoading: true }) 11 | try { 12 | await axios.delete(`/api/posts/${postId}`).then((res) => res.data) 13 | setState({ isSuccess: true }) 14 | } catch (error) { 15 | setState({ isError: true, error }) 16 | } 17 | }, []) 18 | 19 | return [mutate, state] 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/useCreatePost.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | 4 | export default function useCreatePost() { 5 | const [state, setState] = React.useReducer((_, action) => action, { 6 | isIdle: true, 7 | }) 8 | 9 | const mutate = React.useCallback(async (values) => { 10 | setState({ isLoading: true }) 11 | try { 12 | const data = axios.post('/api/posts', values).then((res) => res.data) 13 | setState({ isSuccess: true, data }) 14 | } catch (error) { 15 | setState({ isError: true, error }) 16 | } 17 | }, []) 18 | 19 | return [mutate, state] 20 | } 21 | -------------------------------------------------------------------------------- /src/screens/blog/Post.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useParams } from 'react-router-dom' 3 | 4 | // 5 | 6 | import usePost from '../../hooks/usePost' 7 | 8 | export default function Post() { 9 | const { postId } = useParams() 10 | const postQuery = usePost(postId) 11 | 12 | return ( 13 | <> 14 | {postQuery.isLoading ? ( 15 | Loading... 16 | ) : postQuery.isError ? ( 17 | postQuery.error.message 18 | ) : ( 19 |
20 |

{postQuery.data.title}

21 |

{postQuery.data.body}

22 |
23 | )} 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/useSavePost.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | 4 | export default function useSavePost() { 5 | const [state, setState] = React.useReducer((_, action) => action, { 6 | isIdle: true, 7 | }) 8 | 9 | const mutate = React.useCallback(async (values) => { 10 | setState({ isLoading: true }) 11 | try { 12 | const data = await axios 13 | .patch(`/api/posts/${values.id}`, values) 14 | .then((res) => res.data) 15 | setState({ isSuccess: true, data }) 16 | } catch (error) { 17 | setState({ isError: true, error }) 18 | } 19 | }, []) 20 | 21 | return [mutate, state] 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/usePosts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | 4 | export default function usePosts() { 5 | const [state, setState] = React.useReducer((_, action) => action, { 6 | isLoading: true, 7 | }) 8 | 9 | const fetch = async () => { 10 | setState({ isLoading: true }) 11 | try { 12 | const data = await axios.get('/api/posts').then((res) => res.data) 13 | setState({ isSuccess: true, data }) 14 | } catch (error) { 15 | setState({ isError: true, error }) 16 | } 17 | } 18 | 19 | React.useEffect(() => { 20 | fetch() 21 | }, []) 22 | 23 | return { 24 | ...state, 25 | fetch, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/usePost.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | 4 | export const fetchPost = (postId) => 5 | axios.get(`/api/posts/${postId}`).then((res) => res.data) 6 | 7 | export default function usePost(postId) { 8 | const [state, setState] = React.useReducer((_, action) => action, { 9 | isLoading: true, 10 | }) 11 | 12 | const fetch = React.useCallback(async () => { 13 | setState({ isLoading: true }) 14 | try { 15 | const data = await fetchPost(postId) 16 | setState({ isSuccess: true, data }) 17 | } catch (error) { 18 | setState({ isError: true, error }) 19 | } 20 | }, [postId]) 21 | 22 | React.useEffect(() => { 23 | fetch() 24 | }, [fetch]) 25 | 26 | return { 27 | ...state, 28 | fetch, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/zeit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import App from 'next/app' 3 | import { createGlobalStyle } from 'styled-components' 4 | import normalize from 'styled-normalize' 5 | 6 | // 7 | 8 | const GlobalStyles = createGlobalStyle` 9 | ${normalize}; 10 | html, body, body, [data-reactroot] { 11 | min-height: 100%; 12 | max-width: 100%; 13 | } 14 | 15 | html, body { 16 | width: 100%; 17 | font-size: 16px; 18 | font-family: "Helvetica", "Georgia", sans-serif; 19 | } 20 | 21 | * { 22 | box-sizing: border-box; 23 | } 24 | 25 | input { 26 | max-width: 100%; 27 | } 28 | 29 | a { 30 | text-decoration: none; 31 | cursor: pointer; 32 | } 33 | ` 34 | 35 | export default class MyApp extends App { 36 | render() { 37 | const { Component, pageProps } = this.props 38 | return ( 39 | <> 40 | 41 | 42 | 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/screens/blog/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | // 4 | import usePosts from '../../hooks/usePosts' 5 | import { PostStyles } from '../../components/styled' 6 | 7 | export default function Home() { 8 | const postsQuery = usePosts() 9 | 10 | return ( 11 |
12 |

Blog

13 | 14 |
21 | {postsQuery.isLoading ? ( 22 | Loading... 23 | ) : postsQuery.isError ? ( 24 | postsQuery.error.message 25 | ) : ( 26 | postsQuery.data.map((post) => ( 27 | 28 |

{post.title}

29 |

{post.body}

30 |
31 | )) 32 | )} 33 |
34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /store.json: -------------------------------------------------------------------------------- 1 | {"posts":[{"userId":1,"id":1,"title":"sunt aut facere repellat provident occaecati excepturi optio reprehenderit","body":"quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"},{"userId":1,"id":2,"title":"qui est esse","body":"est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"},{"userId":1,"id":3,"title":"ea molestias quasi exercitationem repellat qui ipsa sit aut","body":"et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"},{"userId":1,"id":4,"title":"eum et est occaecati","body":"ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit"},{"userId":1,"id":5,"title":"nesciunt quas odio","body":"repudiandae veniam quaerat sunt sed\nalias aut fugiat sit autem sed est\nvoluptatem omnis possimus esse voluptatibus quis\nest aut tenetur dolor neque"},{"id":"a8nX64wMH","title":"test","body":"asdfasd"}]} 2 | -------------------------------------------------------------------------------- /src/components/styled.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, { css, keyframes } from 'styled-components' 3 | import { ImSpinner2 } from 'react-icons/im' 4 | 5 | export const Wrapper = styled.div` 6 | display: flex; 7 | height: 96vh; 8 | ` 9 | 10 | export const SidebarStyles = styled.div` 11 | width: 175px; 12 | border-right: 1px solid black; 13 | padding: 1rem; 14 | ` 15 | 16 | export const Main = styled.div` 17 | flex: 1; 18 | padding: 1rem; 19 | ` 20 | 21 | export const PostStyles = styled.div` 22 | display: inline-block; 23 | border: solid 1px rgba(130, 130, 130, 0.3); 24 | padding: 1rem; 25 | color: inherit; 26 | 27 | :hover { 28 | text-decoration: none; 29 | h3 { 30 | text-decoration: underline; 31 | } 32 | } 33 | 34 | ${(props) => 35 | props.disabled && 36 | css` 37 | opacity: 0.5; 38 | pointer-events: none; 39 | `} 40 | ` 41 | 42 | const rotate = keyframes` 43 | from { 44 | transform: rotate(0deg); 45 | } 46 | 47 | to { 48 | transform: rotate(360deg); 49 | } 50 | ` 51 | 52 | export function Loader(props) { 53 | return ( 54 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Document, { Html, Head, Main, NextScript } from 'next/document' 3 | import { ServerStyleSheet } from 'styled-components' 4 | 5 | export default class MyDocument extends Document { 6 | static async getInitialProps(ctx) { 7 | const sheet = new ServerStyleSheet() 8 | const originalRenderPage = ctx.renderPage 9 | 10 | try { 11 | ctx.renderPage = () => 12 | originalRenderPage({ 13 | enhanceApp: (App) => (props) => 14 | sheet.collectStyles(), 15 | }) 16 | 17 | const initialProps = await Document.getInitialProps(ctx) 18 | 19 | return { 20 | ...initialProps, 21 | styles: ( 22 | <> 23 | {initialProps.styles} 24 | {sheet.getStyleElement()} 25 | 26 | ), 27 | } 28 | } finally { 29 | sheet.seal() 30 | } 31 | } 32 | 33 | render() { 34 | return ( 35 | 36 | 37 | 41 | 42 | 43 |
44 | 45 | 46 | 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "temp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.20.0", 12 | "fs-extra": "^9.0.1", 13 | "history": "^5.0.0", 14 | "next": "9.5.4", 15 | "react": "16.13.1", 16 | "react-dom": "16.13.1", 17 | "react-icons": "^3.11.0", 18 | "react-query": "^2.23.1", 19 | "react-query-devtools": "^2.5.1", 20 | "react-router-dom": "^6.0.0-beta.0", 21 | "shortid": "^2.2.15", 22 | "styled-components": "^5.2.0", 23 | "styled-normalize": "^8.0.7" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.26.3", 27 | "babel-eslint": "10.1.0", 28 | "eslint": "6.6.0", 29 | "eslint-config-prettier": "^6.7.0", 30 | "eslint-config-react-app": "^5.0.2", 31 | "eslint-config-standard": "^14.1.0", 32 | "eslint-config-standard-react": "^9.2.0", 33 | "eslint-plugin-flowtype": "4.4.1", 34 | "eslint-plugin-import": "2.18.2", 35 | "eslint-plugin-jsx-a11y": "6.2.3", 36 | "eslint-plugin-node": "^10.0.0", 37 | "eslint-plugin-prettier": "^3.1.1", 38 | "eslint-plugin-promise": "^4.2.1", 39 | "eslint-plugin-react": "7.16.0", 40 | "eslint-plugin-react-hooks": "2.3.0", 41 | "eslint-plugin-standard": "^4.0.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter, Routes, Route } from 'react-router-dom' 3 | // 4 | 5 | import { Wrapper, Main } from './components/styled' 6 | import Sidebar from './components/Sidebar' 7 | 8 | import Admin from './screens/admin' 9 | import AdminPost from './screens/admin/Post' 10 | import Blog from './screens/blog' 11 | import BlogPost from './screens/blog/Post' 12 | 13 | function SafeHydrate({ children }) { 14 | return ( 15 |
16 | {typeof document === 'undefined' ? null : children} 17 |
18 | ) 19 | } 20 | 21 | export default function App() { 22 | return ( 23 | 24 | 25 | 26 | 27 |
28 | 29 | 33 |

Welcome!

34 | 35 | } 36 | /> 37 | } /> 38 | } /> 39 | } /> 40 | } /> 41 |
42 |
43 |
44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/components/PostForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const defaultFormValues = { 4 | title: '', 5 | body: '', 6 | } 7 | 8 | export default function PostForm({ 9 | onSubmit, 10 | initialValues = defaultFormValues, 11 | submitText, 12 | clearOnSubmit, 13 | }) { 14 | const [values, setValues] = React.useState(initialValues) 15 | 16 | const setValue = (field, value) => 17 | setValues((old) => ({ ...old, [field]: value })) 18 | 19 | const handleSubmit = (e) => { 20 | if (clearOnSubmit) { 21 | setValues(defaultFormValues) 22 | } 23 | e.preventDefault() 24 | onSubmit(values) 25 | } 26 | 27 | React.useEffect(() => { 28 | setValues(initialValues) 29 | }, [initialValues]) 30 | 31 | return ( 32 |
33 | 34 |
35 | setValue('title', e.target.value)} 40 | required 41 | /> 42 |
43 |
44 | 45 |
46 |