├── .babelrc
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .prettierrc
├── README.md
├── db
└── index.js
├── next.config.js
├── package.json
├── pages
├── 404.js
├── _app.js
├── _document.js
├── api
│ └── posts
│ │ ├── [postId].js
│ │ └── index.js
└── index.js
├── public
├── favicon.ico
└── zeit.svg
├── src
├── components
│ ├── GlobalLoader.js
│ ├── PostForm.js
│ ├── Sidebar.js
│ └── styled.js
├── hooks
│ ├── useCreatePost.js
│ ├── useDeletePost.js
│ ├── usePost.js
│ ├── usePosts.js
│ └── useSavePost.js
├── index.js
└── screens
│ ├── admin
│ ├── Post.js
│ └── index.js
│ └── blog
│ ├── Post.js
│ └── index.js
├── store.json
├── utils.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": ["styled-components"]
4 | }
5 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | All About React Query (with Tanner Linsley)
8 |
9 |
10 | This app was built live on Learn With Jason and it was super fun and I’m sad you weren’t there.
11 |
12 |
13 | But don’t worry! You can still:
14 | watch the video ·
15 | see upcoming episodes
16 |
17 |
18 | > **NOTE:** This repo was forked from [tannerlinsley/react-summit-2020-react-query](https://github.com/tannerlinsley/react-summit-2020-react-query)
19 |
20 |
21 |
22 | There‘s a lot of buzz about React Query and how much it can simplify your development workflow. In this episode, we’ll learn all about it from the #TanStack creator himself!
23 |
24 |
25 |
26 | ## More Information
27 |
28 | - [Watch this app get built live + see links and additional resources][episode]
29 | - [Follow _Learn With Jason_ on Twitch][twitch] to watch future episodes live
30 | - [Add the _Learn With Jason_ schedule to your Google Calendar][cal]
31 |
32 | [episode]: https://www.learnwithjason.dev/all-about-react-query
33 | [twitch]: https://jason.af/twitch
34 | [cal]: https://jason.af/lwj/cal
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const Module = require('module')
2 | const path = require('path')
3 | const resolveFrom = require('resolve-from')
4 |
5 | const node_modules = path.resolve(__dirname, 'node_modules')
6 |
7 | const originalRequire = Module.prototype.require
8 |
9 | // The following ensures that there is always only a single (and same)
10 | // copy of React in an app at any given moment.
11 | Module.prototype.require = function (modulePath) {
12 | // Only redirect resolutions to non-relative and non-absolute modules
13 | if (
14 | ['/react/', '/react-dom/', '/react-query/'].some((d) => {
15 | try {
16 | return require.resolve(modulePath).includes(d)
17 | } catch (err) {
18 | return false
19 | }
20 | })
21 | ) {
22 | try {
23 | modulePath = resolveFrom(node_modules, modulePath)
24 | } catch (err) {
25 | //
26 | }
27 | }
28 |
29 | return originalRequire.call(this, modulePath)
30 | }
31 |
32 | module.exports = {
33 | target: 'serverless',
34 | async rewrites() {
35 | return [
36 | {
37 | source: '/api/:any*',
38 | destination: '/api/:any*',
39 | },
40 | {
41 | source: '/:any*',
42 | destination: '/',
43 | },
44 | ]
45 | },
46 | webpack: (config) => {
47 | config.resolve = {
48 | ...config.resolve,
49 | alias: {
50 | ...config.resolve.alias,
51 | react$: resolveFrom(path.resolve('node_modules'), 'react'),
52 | 'react-query$': resolveFrom(
53 | path.resolve('node_modules'),
54 | 'react-query'
55 | ),
56 | 'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'),
57 | },
58 | }
59 | return config
60 | },
61 | }
62 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/pages/api/posts/[postId].js:
--------------------------------------------------------------------------------
1 | import db from '../../../db'
2 | import { sleep } from '../../../utils'
3 |
4 | const deleteFailureRate = 0
5 |
6 | export default async (req, res) => {
7 | await sleep(1000)
8 |
9 | try {
10 | if (req.method === 'GET') {
11 | return await GET(req, res)
12 | } else if (req.method === 'PATCH') {
13 | return await PATCH(req, res)
14 | } else if (req.method === 'DELETE') {
15 | return await DELETE(req, res)
16 | }
17 | } catch (err) {
18 | console.error(err)
19 | res.status(500)
20 | res.json({ message: 'An unknown error occurred!' })
21 | }
22 | }
23 |
24 | async function GET(req, res) {
25 | const {
26 | query: { postId },
27 | } = req
28 |
29 | const row = (await db.get()).posts.find((d) => d.id == postId)
30 |
31 | if (!row) {
32 | res.status(404)
33 | return res.send('Not found')
34 | }
35 |
36 | res.json(row)
37 | }
38 |
39 | async function PATCH(req, res) {
40 | const {
41 | query: { postId },
42 | body,
43 | } = req
44 |
45 | if (body.body.includes('fail')) {
46 | res.status(500)
47 | res.json({ message: 'An unknown error occurred!' })
48 | return
49 | }
50 |
51 | const row = (await db.get()).posts.find((d) => d.id == postId)
52 |
53 | if (!row) {
54 | res.status(404)
55 | return res.send('Not found')
56 | }
57 |
58 | delete body.id
59 |
60 | const newRow = {
61 | ...row,
62 | ...body,
63 | }
64 |
65 | await db.set((old) => {
66 | return {
67 | ...old,
68 | posts: old.posts.map((d) => (d.id == postId ? newRow : d)),
69 | }
70 | })
71 |
72 | res.json(newRow)
73 | }
74 |
75 | async function DELETE(req, res) {
76 | const {
77 | query: { postId },
78 | } = req
79 |
80 | if (Math.random() < deleteFailureRate) {
81 | res.status(500)
82 | res.json({ message: 'An unknown error occurred!' })
83 | return
84 | }
85 |
86 | const row = (await db.get()).posts.find((d) => d.id == postId)
87 |
88 | if (!row) {
89 | res.status(404)
90 | return res.send('Not found')
91 | }
92 |
93 | await db.set((old) => {
94 | return {
95 | ...old,
96 | posts: old.posts.filter((d) => d.id != postId),
97 | }
98 | })
99 |
100 | res.status(200)
101 | res.send('Resource Deleted')
102 | }
103 |
--------------------------------------------------------------------------------
/pages/api/posts/index.js:
--------------------------------------------------------------------------------
1 | import shortid from 'shortid'
2 | import db from '../../../db'
3 | import { sleep } from '../../../utils'
4 |
5 | //
6 | const failureRate = 0
7 |
8 | export default async (req, res) => {
9 | await sleep(1000)
10 |
11 | try {
12 | if (req.method === 'GET') {
13 | return await GET(req, res)
14 | } else if (req.method === 'POST') {
15 | return await POST(req, res)
16 | }
17 | } catch (err) {
18 | console.error(err)
19 | res.status(500)
20 | res.json({ message: 'An unknown error occurred!' })
21 | }
22 | }
23 |
24 | async function GET(req, res) {
25 | const {
26 | query: { pageOffset, pageSize },
27 | } = req
28 |
29 | const posts = (await db.get()).posts.map((d) => ({
30 | ...d,
31 | body: d.body.substring(0, 50) + (d.body.length > 100 ? '...' : ''), // Don't return full body in list calls
32 | }))
33 |
34 | if (Number(pageSize)) {
35 | const start = Number(pageSize) * Number(pageOffset)
36 | const end = start + Number(pageSize)
37 | const page = posts.slice(start, end)
38 |
39 | return res.json({
40 | items: page,
41 | nextPageOffset: posts.length > end ? Number(pageOffset) + 1 : undefined,
42 | })
43 | }
44 |
45 | res.json(posts)
46 | }
47 |
48 | async function POST(req, res) {
49 | if (Math.random() < failureRate) {
50 | res.status(500)
51 | res.json({ message: 'An unknown error occurred!' })
52 | return
53 | }
54 |
55 | const row = {
56 | id: shortid.generate(),
57 | ...req.body,
58 | }
59 |
60 | await db.set((old) => {
61 | return {
62 | ...old,
63 | posts: [...old.posts, row],
64 | }
65 | })
66 |
67 | res.json(row)
68 | }
69 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import App from '../src/'
2 |
3 | export default App
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/learnwithjason/all-about-react-query/36b8edb0393158ad6855ea796aba894f5fd061d4/public/favicon.ico
--------------------------------------------------------------------------------
/public/zeit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/components/GlobalLoader.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Loader } from './styled'
3 | import { useIsFetching } from 'react-query'
4 |
5 | export default function GlobalLoader() {
6 | const isFetching = useIsFetching()
7 |
8 | return (
9 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/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 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/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 |
10 |
11 | Home
12 |
13 |
14 | Blog
15 |
16 |
17 |
18 | Admin
19 |
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/hooks/useCreatePost.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { useMutation, queryCache } from 'react-query';
3 |
4 | export default function useCreatePost() {
5 | return useMutation(
6 | (values) => axios.post('/api/posts', values).then((res) => res.data),
7 | {
8 | onMutate: (newPost) => {
9 | const oldPosts = queryCache.getQueryData('posts')
10 |
11 | if (queryCache.getQueryData('posts')) {
12 | queryCache.setQueryData('posts', old => [...old, newPost])
13 | }
14 |
15 | return () => queryCache.setQueryData('posts', oldPosts)
16 | },
17 | onError: (error, _newPost, rollback) => {
18 | console.error(error);
19 | if (rollback) rollback()
20 | },
21 | onSettled: () => {
22 | queryCache.invalidateQueries('posts');
23 | }
24 | }
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/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/usePost.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import axios from 'axios'
3 | import { useQuery, queryCache } from 'react-query';
4 |
5 | export const fetchPost = (postId) =>
6 | axios.get(`/api/posts/${postId}`).then((res) => res.data)
7 |
8 | export default function usePost(postId) {
9 | return useQuery(
10 | ['posts', postId],
11 | () => fetchPost(postId), {
12 | initialData: () => {
13 | return queryCache.getQueryData('posts')?.find(d => d.id == postId)
14 | },
15 | initialStale: true
16 | }
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/hooks/usePosts.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import axios from 'axios'
3 | import { useQuery, queryCache } from 'react-query';
4 |
5 | export default function usePosts() {
6 | return useQuery(
7 | 'posts',
8 | () => axios.get('/api/posts').then((res) => res.data),
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/src/hooks/useSavePost.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { useMutation, queryCache } from 'react-query'
3 |
4 | export default function useSavePost() {
5 | return useMutation(
6 | (newPost) => axios
7 | .patch(`/api/posts/${newPost.id}`, newPost)
8 | .then((res) => res.data),
9 | {
10 | onMutate: (newPost) => {
11 | // update the data
12 | queryCache.setQueryData(['posts', newPost.id], newPost)
13 | },
14 | onSuccess: (newPost) => {
15 | queryCache.setQueryData(['posts', newPost.id], newPost)
16 |
17 | if (queryCache.getQueryData('posts')) {
18 | queryCache.setQueryData('posts', old => {
19 | return old.map(d => {
20 | if (d.id === newPost.id) {
21 | return newPost
22 | }
23 | return d
24 | })
25 | })
26 | } else {
27 | queryCache.setQueryData('posts', [newPost])
28 | queryCache.invalidateQueries('posts')
29 | }
30 | },
31 | }
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { BrowserRouter, Routes, Route } from 'react-router-dom'
3 | import { ReactQueryDevtools } from 'react-query-devtools'
4 | //
5 |
6 | import { Wrapper, Main } from './components/styled'
7 | import GlobalLoader from './components/GlobalLoader'
8 | import Sidebar from './components/Sidebar'
9 |
10 | import Admin from './screens/admin'
11 | import AdminPost from './screens/admin/Post'
12 | import Blog from './screens/blog'
13 | import BlogPost from './screens/blog/Post'
14 |
15 | function SafeHydrate({ children }) {
16 | return (
17 |
18 | {typeof document === 'undefined' ? null : children}
19 |
20 | )
21 | }
22 |
23 | export default function App() {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
36 | Welcome!
37 | >
38 | }
39 | />
40 | } />
41 | } />
42 | } />
43 | } />
44 |
45 |
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/screens/admin/Post.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link, useParams, useNavigate } from 'react-router-dom'
3 |
4 | //
5 |
6 | import usePost from '../../hooks/usePost'
7 | import useSavePost from '../../hooks/useSavePost'
8 | import useDeletePost from '../../hooks/useDeletePost'
9 |
10 | import PostForm from '../../components/PostForm'
11 | import { Loader } from '../../components/styled'
12 |
13 | export default function Post() {
14 | const { postId } = useParams()
15 | const navigate = useNavigate()
16 |
17 | const postQuery = usePost(postId)
18 | const [savePost, savePostInfo] = useSavePost()
19 | const [deletePost, deletePostInfo] = useDeletePost()
20 |
21 | const onSubmit = async (values) => {
22 | await savePost(values)
23 | }
24 |
25 | const onDelete = async () => {
26 | await deletePost(postId)
27 | navigate('/admin')
28 | }
29 |
30 | return (
31 | <>
32 | {postQuery.isLoading ? (
33 |
34 | Loading...
35 |
36 | ) : (
37 |
38 |
{postQuery.data.title}
39 |
40 | View Post
41 |
42 |
55 |
56 |
57 |
58 | {deletePostInfo.isLoading
59 | ? 'Deleting...'
60 | : deletePostInfo.isError
61 | ? 'Error!'
62 | : deletePostInfo.isSuccess
63 | ? 'Deleted!'
64 | : 'Delete Post'}
65 |
66 |
67 |
68 | )}
69 | >
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/src/screens/admin/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | import PostForm from '../../components/PostForm'
5 | import { Loader } from '../../components/styled'
6 |
7 | import usePosts from '../../hooks/usePosts'
8 | import useCreatePost from '../../hooks/useCreatePost'
9 |
10 | export default function Posts() {
11 | const postsQuery = usePosts()
12 | const [createPost, createPostInfo] = useCreatePost()
13 |
14 | return (
15 |
16 |
17 |
18 | {postsQuery.isLoading ? (
19 |
20 | Loading
21 |
22 | ) : (
23 | <>
24 |
Posts
25 |
26 | {postsQuery.data.map((post) => (
27 |
28 | {post.title}
29 |
30 | ))}
31 |
32 |
33 | >
34 | )}
35 |
36 |
37 |
38 |
39 |
Create New Post
40 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/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/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":"boop corgi party 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":"waddup chat a seventh time!","body":"this is the best"},{"id":"pr_Wb0BEf","title":"this is an edited post","body":"how does anything work?"},{"id":"JxyuNPBau","title":"a new post","body":"test"},{"id":"PGKhPsxHG","title":"the new new new","body":"test again again"}]}
2 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | export function sleep(time) {
2 | return new Promise((resolve) => setTimeout(resolve, time))
3 | }
4 |
--------------------------------------------------------------------------------