Skip to content

Commit

Permalink
Add prettier
Browse files Browse the repository at this point in the history
Add favorites page
  • Loading branch information
Mark Chigrin authored Feb 7, 2022
1 parent 43b3c5e commit a807849
Show file tree
Hide file tree
Showing 19 changed files with 222 additions and 107 deletions.
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"prettier": "^2.5.1"
}
}
5 changes: 5 additions & 0 deletions src/app/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
10 changes: 5 additions & 5 deletions src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'

import { Pages } from "pages";
import { Pages } from 'pages'

import { store } from "./store";
import { store } from './store'

export const App: React.VFC = () => (
<Provider store={store}>
<BrowserRouter>
<Pages />
</BrowserRouter>
</Provider>
);
)
16 changes: 9 additions & 7 deletions src/app/store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { configureStore } from "@reduxjs/toolkit";
import { configureStore } from '@reduxjs/toolkit'

import { api } from "services/api";
import { api } from 'services/api'
import { favoriteJokes } from 'services/favoriteJokes'

export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
[favoriteJokes.name]: favoriteJokes.reducer,
},
middleware: (getDefaultMiddleware) => [
...getDefaultMiddleware(),
api.middleware,
],
});
middleware: (getDefaultMiddleware) => [...getDefaultMiddleware(), api.middleware],
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
39 changes: 20 additions & 19 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import clsx from "clsx";
import clsx from 'clsx'
import React from 'react'
import { Link, LinkProps } from 'react-router-dom'

const variants = {
primary: "is-primary",
success: "is-success",
warning: "is-warning",
error: "is-error",
} as const;
primary: 'is-primary',
success: 'is-success',
warning: 'is-warning',
error: 'is-error',
} as const

export const Button: React.FC<
React.ComponentPropsWithRef<"button"> & { variant?: keyof typeof variants }
> = ({ variant, ...props }) => (
<button
{...props}
className={clsx(
props.className,
"nes-btn",
props.disabled && "is-disabled",
variant && variants[variant]
)}
/>
);
type ButtonProps = React.ComponentPropsWithRef<'button'> & {
variant?: keyof typeof variants
}

const composeClassName = ({ className, disabled, variant }: Pick<ButtonProps, 'className' | 'disabled' | 'variant'>) =>
clsx(className, 'nes-btn', disabled && 'is-disabled', variant && variants[variant])

export const Button: React.FC<ButtonProps> = (props) => <button {...props} className={composeClassName(props)} />

export const ButtonLink: React.FC<LinkProps & Pick<ButtonProps, 'variant'>> = (props) => (
<Link {...props} className={composeClassName(props)} />
)
9 changes: 9 additions & 0 deletions src/components/Column.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import styled from '@emotion/styled'

export const Column = styled.div<{ gap?: number | string }>`
display: flex;
flex-direction: column;
justify-content: center;
gap: ${({ gap = '2rem' }) => gap};
text-align: center;
`
10 changes: 10 additions & 0 deletions src/components/Container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from '@emotion/styled'

export const Container = styled.div`
display: grid;
place-items: center;
height: 100%;
margin: 0 auto;
max-width: 30rem;
padding: 0 1rem;
`
39 changes: 39 additions & 0 deletions src/components/Joke.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'

import { useAppDispatch, useAppSelector } from 'app/hooks'
import { useGetJokeQuery } from 'services/api'
import { Id } from 'types'
import { Like } from 'components/Like'
import { favoriteJokes } from 'services/favoriteJokes'

interface JokeProps {
id?: Id
}

export const Joke: React.VFC<JokeProps> = ({ id }) => {
const dispatch = useAppDispatch()
const { data: joke, isLoading } = useGetJokeQuery(id)
const liked = useAppSelector((state) =>
state.favoriteJokes.some((favoriteId) => joke != null && favoriteId === joke.id)
)

return (
<div>
{isLoading && 'Loading...'}
{joke != null && (
<>
{joke.value}
<div
css={css`
display: flex;
justify-content: end;
`}
>
<Like liked={liked} onClick={() => dispatch(favoriteJokes.actions.updateJoke(joke.id))} />
</div>
</>
)}
</div>
)
}
8 changes: 8 additions & 0 deletions src/components/Like.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import clsx from 'clsx'

interface LikeProps extends React.ComponentPropsWithRef<'i'> {
liked: boolean
}
export const Like: React.VFC<LikeProps> = ({ liked, ...props }) => (
<i {...props} className={clsx('nes-icon', 'star', !liked && 'is-empty', props.className)} />
)
2 changes: 1 addition & 1 deletion src/index.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#root {
font-family: "Press Start 2P", cursive;
font-family: 'Press Start 2P', cursive;
font-size: 16px;
height: 100vh;
}
16 changes: 8 additions & 8 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from "react";
import ReactDOM from "react-dom";
import "modern-normalize/modern-normalize.css";
import "@fontsource/press-start-2p";
import React from 'react'
import ReactDOM from 'react-dom'
import 'modern-normalize/modern-normalize.css'
import '@fontsource/press-start-2p'

import { App } from "app";
import { App } from 'app'

import "./index.css";
import './index.css'

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
document.getElementById('root')
)
25 changes: 25 additions & 0 deletions src/pages/Favorites.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useAppDispatch, useAppSelector } from 'app/hooks'
import { Button, ButtonLink } from 'components/Button'
import { Column } from 'components/Column'
import { Joke } from 'components/Joke'
import { favoriteJokes } from 'services/favoriteJokes'

export const Favorites: React.VFC = () => {
const jokes = useAppSelector((state) => state.favoriteJokes)
const dispatch = useAppDispatch()
const isListEmpty = jokes.length === 0
return (
<Column>
{isListEmpty && 'Now your list is empty.\nLike jokes to complete your collection!'}
{jokes.map((id) => (
<Joke id={id} key={id} />
))}
<Column gap="0.5rem">
<Button variant="warning" disabled={isListEmpty} onClick={() => dispatch(favoriteJokes.actions.purgeList())}>
Clear list
</Button>
<ButtonLink to="/">Back</ButtonLink>
</Column>
</Column>
)
}
62 changes: 20 additions & 42 deletions src/pages/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,25 @@
import styled from "@emotion/styled";
import { Button } from "components/Button";
import { useState } from "react";
import { useGetRandomJokeQuery } from "services/api";
import { useState } from 'react'

const Container = styled.div`
display: grid;
place-items: center;
height: 100%;
margin: 0 auto;
max-width: 30rem;
`;

const Column = styled.div<{ gap?: number | string }>`
display: flex;
flex-direction: column;
justify-content: center;
gap: ${({ gap = "2rem" }) => gap};
text-align: center;
margin: 0 1rem;
`;
import { Button, ButtonLink } from 'components/Button'
import { Column } from 'components/Column'
import { Joke } from 'components/Joke'
import { api } from 'services/api'

export const Main: React.VFC = () => {
const [auto, setAuto] = useState<Boolean>(false);
const {
data: joke,
isLoading,
isFetching,
refetch,
} = useGetRandomJokeQuery(undefined, { pollingInterval: auto ? 3000 : 0 });
const [auto, setAuto] = useState<Boolean>(false)
const [refetch, { isFetching }] = api.endpoints.getJoke.useLazyQuery({
pollingInterval: auto ? 3000 : 0,
})
return (
<Container>
<Column>
{isLoading && "Loading..."}
{joke?.value}
<Column gap="0.5rem">
<Button onClick={refetch} disabled={isFetching} variant="primary">
Next joke
</Button>
<Button onClick={() => setAuto((e) => !e)}>
{auto ? "Disable" : "Enable"} auto next
</Button>
</Column>
<Column>
<Joke />
<Column gap="0.5rem">
<Button onClick={() => refetch()} disabled={isFetching} variant="primary">
Next joke
</Button>
<Button onClick={() => setAuto((e) => !e)}>{auto ? 'Disable' : 'Enable'} auto next</Button>
<ButtonLink to="/favorites">Favorites</ButtonLink>
</Column>
</Container>
);
};
</Column>
)
}
18 changes: 12 additions & 6 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { Route, Routes } from "react-router";
import { Route, Routes } from 'react-router'

import { Main } from "./Main";
import { Container } from 'components/Container'

import { Main } from './Main'
import { Favorites } from './Favorites'

export const Pages = () => (
<Routes>
<Route path="/" element={<Main />} />
</Routes>
);
<Container>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/favorites" element={<Favorites />} />
</Routes>
</Container>
)
19 changes: 8 additions & 11 deletions src/services/api.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

import { Joke } from "types";
import { Joke } from 'types'

export const api = createApi({
reducerPath: "chuckNorrisApi",
reducerPath: 'chuckNorrisApi',
baseQuery: fetchBaseQuery({
baseUrl: "https://api.chucknorris.io/jokes/",
baseUrl: 'https://api.chucknorris.io/jokes/',
}),
endpoints: (build) => ({
getRandomJoke: build.query<Joke, void>({
query: () => "random",
}),
getJokeById: build.query<Joke, string>({
query: (id) => id,
getJoke: build.query<Joke, string | void>({
query: (id) => id ?? 'random',
}),
}),
});
})

export const { useGetRandomJokeQuery, useGetJokeByIdQuery } = api;
export const { useGetJokeQuery } = api
24 changes: 24 additions & 0 deletions src/services/favoriteJokes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

import { Id } from 'types'

type JokeAction = PayloadAction<Id>

export const favoriteJokes = createSlice({
name: 'favoriteJokes',
initialState: [] as Id[],
reducers: {
updateJoke(state, { payload }: JokeAction) {
if (state.includes(payload)) {
return state.filter((id) => id !== payload)
}
if (state.length === 10) {
state.splice(0, 1)
}
state.push(payload)
},
purgeList() {
return []
},
},
})
7 changes: 4 additions & 3 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export type Id = string
export interface Joke {
id: string;
url: string;
value: string;
id: Id
url: string
value: string

// created_at: string;
// updated_at: string;
Expand Down
Loading

0 comments on commit a807849

Please sign in to comment.