Skip to content

Commit

Permalink
feat: add mockend for data fetching
Browse files Browse the repository at this point in the history
* feat: use css to switch dark buttons

* chore: remove types for classnames, remove console

* feat: add default style for button group, add i18n in not found, add html bg

* feat: init fetch

* chore: add git attr for text eol

* chore: add spelling and type check in husky

* chore: add missing yarn cmd

* chore: fix spell

* chore: fix spell

* chore: downgrade husky to ~3.1
  • Loading branch information
wwwenjie committed Apr 23, 2021
1 parent bcb82f1 commit 61b63f8
Show file tree
Hide file tree
Showing 31 changed files with 376 additions and 42 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"words": [
"vite",
"testid",
"svgr"
"svgr",
"deduping"
],
"flagWords": [],
"ignorePaths": [
Expand Down
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text eol=lf
2 changes: 1 addition & 1 deletion .huskyrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"hooks": {
"pre-commit": "echo \"git commit trigger husky pre-commit hook\" && lint-staged",
"pre-commit": "echo \"git commit trigger husky pre-commit hook\" && lint-staged && yarn test:spelling && tsc",
"post-commit": "git update-index --again"
}
}
8 changes: 8 additions & 0 deletions .mockend.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Post": {
"title": "string",
"views": "int",
"published": "boolean",
"createdAt": "date"
}
}
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,20 @@
"fix:lint": "eslint src --ext .ts,.tsx --fix"
},
"dependencies": {
"axios": "^0.21.1",
"classnames": "^2.3.1",
"i18next": "^20.2.1",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-i18next": "^11.8.13",
"react-icons": "^4.2.0",
"react-router-dom": "^5.2.0",
"react-use": "^17.2.3"
"react-use": "^17.2.3",
"swr": "^0.5.5"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@types/classnames": "^2.2.11",
"@types/jest": "^26.0.22",
"@types/node": "^14.14.31",
"@types/react": "^17.0.0",
Expand All @@ -56,7 +57,7 @@
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^5.1.3",
"husky": "~3.1.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
"lint-staged": "^10.5.4",
Expand Down
19 changes: 14 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import React, { FC } from 'react'
import AppRouter from '@AppRouter'

console.log('invoke CI')
import { SWRConfig as SWRConfigProvider } from 'swr'
import { get } from '@data'

const App: FC = () => (
<div className="app-body">
<AppRouter />
</div>
<SWRConfigProvider
// https://swr.vercel.app/docs/global-configuration
value={{
fetcher: get,
shouldRetryOnError: false,
revalidateOnFocus: false,
}}
>
<div className="app-body">
<AppRouter />
</div>
</SWRConfigProvider>
)

export default App
4 changes: 3 additions & 1 deletion src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-dom'
import { Landing, NotFound } from '@pages'
import { Landing, NotFound, Post } from '@pages'

const AppRouter: React.FC = () => (
<Router>
Expand All @@ -9,6 +9,8 @@ const AppRouter: React.FC = () => (

<Route exact path="/landing" render={() => <Landing />} />

<Route exact path="/post" render={() => <Post />} />

<Route path="*" render={() => <NotFound />} />
</Switch>
</Router>
Expand Down
18 changes: 14 additions & 4 deletions src/components/atoms/FooterButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@ import { IoLanguage, IoLogoGithub } from 'react-icons/io5'
import { useDark } from '@hooks'
import { useTranslation } from 'react-i18next'
import './FooterButtons.css'
import classNames from 'classnames'

export const DarkModeButton: FC = () => {
const { isDark, toggleDark } = useDark()

return isDark ? (
<BiMoon data-testid="dark-button" onClick={toggleDark} className="footer-button" />
) : (
<BiSun data-testid="dark-button" onClick={toggleDark} className="footer-button" />
return (
<>
<BiMoon
data-testid="moon-button"
onClick={toggleDark}
className={classNames('footer-button', { hidden: !isDark })}
/>
<BiSun
data-testid="sun-button"
onClick={toggleDark}
className={classNames('footer-button', { hidden: isDark })}
/>
</>
)
}

Expand Down
89 changes: 81 additions & 8 deletions src/components/atoms/__tests__/FooterButtons.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,95 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { DarkModeButton, GitHubButton, LanguageButton } from '@components/atoms'
import i18n from '@locales/i18n'
import { handleMatchMedia } from '@utils'
import { ColorScheme } from '@hooks'

describe('<FooterButtons />', () => {
it('<DarkModeButton /> should change color scheme when click', () => {
beforeEach(() => {
handleMatchMedia()
})

afterEach(() => {
localStorage.clear()
})

const expectColorScheme = (colorScheme: ColorScheme) => {
expect(JSON.parse(localStorage.getItem('color-scheme') || '')).toStrictEqual(colorScheme)
}

it('<DarkModeButton /> should change color scheme when click', () => {
render(<DarkModeButton />)

const moonButton = screen.getByTestId('moon-button')

const sunButton = screen.getByTestId('sun-button')

expectColorScheme('auto')

fireEvent.click(sunButton)
expectColorScheme('dark')

fireEvent.click(moonButton)
expectColorScheme('light')
})

it('<DarkModeButton /> should hide moon button when color scheme is auto and light', () => {
render(<DarkModeButton />)

const moonButton = screen.getByTestId('moon-button')

expectColorScheme('auto')
expect(moonButton).toHaveClass('hidden')

fireEvent.click(moonButton)
expectColorScheme('dark')

fireEvent.click(moonButton)
expectColorScheme('light')

expect(moonButton).toHaveClass('hidden')
})

it('<DarkModeButton /> should hide sun button when color scheme is dark', () => {
render(<DarkModeButton />)

const sunButton = screen.getByTestId('sun-button')

expectColorScheme('auto')

fireEvent.click(sunButton)
expectColorScheme('dark')

expect(sunButton).toHaveClass('hidden')
})

it('<DarkModeButton /> should show sun button when color scheme is auto and light', () => {
render(<DarkModeButton />)

const sunButton = screen.getByTestId('sun-button')

expectColorScheme('auto')
expect(sunButton.hidden).toBeFalsy()

fireEvent.click(sunButton)
expectColorScheme('dark')

fireEvent.click(sunButton)
expectColorScheme('light')

expect(sunButton.hidden).toBeFalsy()
})

it('<DarkModeButton /> should show moon button when color scheme is dark', () => {
render(<DarkModeButton />)

// button will change due to color-scheme change
const button = () => screen.getByTestId('dark-button')
const moonButton = screen.getByTestId('moon-button')

expect(localStorage.getItem('color-scheme')).toStrictEqual('"auto"')
expectColorScheme('auto')

fireEvent.click(button())
expect(localStorage.getItem('color-scheme')).toStrictEqual('"dark"')
fireEvent.click(moonButton)
expectColorScheme('dark')

fireEvent.click(button())
expect(localStorage.getItem('color-scheme')).toStrictEqual('"light"')
expect(moonButton.hidden).toBeFalsy()
})

it('<LanguageButton /> should change language when click', () => {
Expand Down
8 changes: 6 additions & 2 deletions src/components/molecules/FooterButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import React, { FC } from 'react'
import { DarkModeButton, GitHubButton, LanguageButton } from '@components/atoms'
import classNames from 'classnames'

export const FooterButtonGroup: FC<React.HTMLAttributes<HTMLDivElement>> = ({ ...rest }) => (
<div {...rest}>
export const FooterButtonGroup: FC<React.HTMLAttributes<HTMLDivElement>> = ({
className,
...rest
}) => (
<div className={classNames('my-2 flex flex-row', className)} {...rest}>
<DarkModeButton />
<LanguageButton />
<GitHubButton />
Expand Down
3 changes: 3 additions & 0 deletions src/components/molecules/Home/Post.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React, { FC } from 'react'

export const Post: FC = () => <div>Hello, World!</div>
1 change: 1 addition & 0 deletions src/components/molecules/Home/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Post } from './Post'
1 change: 1 addition & 0 deletions src/components/molecules/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Home'
export * from './Landing'
export { FooterButtonGroup } from './FooterButtonGroup'
15 changes: 15 additions & 0 deletions src/components/organisms/Home/Post.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { FC } from 'react'
import { usePost } from '@data'

export const Post: FC = () => {
const { data: post, loading, error } = usePost(1)

return (
<div className="h-screen flex flex-col items-center justify-center">
{loading && <div>Loading...</div>}
{/* usually error could be handle in axios like show a message, but feel free if you want to add error display when request error */}
{error && <div>Error</div>}
<div>{post?.title}</div>
</div>
)
}
1 change: 1 addition & 0 deletions src/components/organisms/Home/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Post as PostOrganisms } from './Post'
2 changes: 1 addition & 1 deletion src/components/organisms/Landing/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const Landing: FC = () => (
<div className="h-screen flex flex-col items-center justify-center">
<Logo />
<Description />
<FooterButtonGroup className="my-2 flex flex-row" />
<FooterButtonGroup />
<LinkGroup />
</div>
)
2 changes: 1 addition & 1 deletion src/components/organisms/Landing/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { Landing } from './Landing'
export { Landing as LandingOrganisms } from './Landing'
1 change: 1 addition & 0 deletions src/components/organisms/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Home'
export * from './Landing'
Empty file removed src/data/.gitkeep
Empty file.
54 changes: 54 additions & 0 deletions src/data/Fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import axios from 'axios'

export const fetcher = axios.create({
// baseURL: 'https://mockend.com/wwwenjie/react-starter/',
baseURL: 'https://jsonplaceholder.typicode.com/',
headers: {
Accept: 'application/json',
'Content-type': 'application/json',
},
})

const onRequestError = async (error: any) => {
// logic when request error
}

const onResponseError = async (error: any) => {
if (error?.response?.status) {
await onResponseStatus(error.response.status)
}

// logic when response error
}

const onResponseStatus = async (status: number) => {
const statusMapper = new Map<number, () => void>()
.set(401, () => {
// redirect to login or anything you want
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.set(404, () => {})

const func = statusMapper.get(status)
if (func) {
await func()
}
}

fetcher.interceptors.request.use(
(config) => config,
async (error) => {
await onRequestError(error)
return Promise.reject(error)
},
)

fetcher.interceptors.response.use(
async (response) => response.data,
async (error) => {
await onResponseError(error)
return Promise.reject(error)
},
)

export const { get, post, patch, delete: deleteFetch } = fetcher
30 changes: 30 additions & 0 deletions src/data/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import useSWR from 'swr'

export type Post = {
readonly title: string
readonly views: number
readonly published: boolean
readonly createdAt: Date
}

export const usePosts = () => {
const { data, error } = useSWR<readonly Post[]>('posts')

return {
data,
loading: !error && !data,
error,
}
}

export const usePost = (postId: number) => {
const { data, error } = useSWR<Post>(`posts/${postId}`, {
dedupingInterval: 600,
})

return {
data,
loading: !error && !data,
error,
}
}
2 changes: 2 additions & 0 deletions src/data/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Fetcher'
export * from './Post'
3 changes: 2 additions & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"desc": "Opinionated React Starter Template",
"learn": "Learn",
"docs": "Docs"
"docs": "Docs",
"notFound": "Sorry, the page you visited does not exist."
}

1 comment on commit 61b63f8

@vercel
Copy link

@vercel vercel bot commented on 61b63f8 Apr 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.