diff --git a/package-lock.json b/package-lock.json index 3d5bf3090c5d728907c7f5efb4114082fb3c2f4b..5254bb45302fce0a77597033336b26c0f9580076 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10364,6 +10364,15 @@ "object-visit": "^1.0.0" } }, + "match-sorter": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.1.0.tgz", + "integrity": "sha512-sKPMf4kbF7Dm5Crx0bbfLpokK68PUJ/0STUIOPa1ZmTZEA3lCaPK3gapQR573oLmvdkTfGojzySkIwuq6Z6xRQ==", + "requires": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -13021,6 +13030,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==" }, + "react-query": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.6.0.tgz", + "integrity": "sha512-39ptLt4qaKO1DE+ta6SpPutweEgDvUQj/KlebC+okJ9Nxbs5ExxKV8RYlLeop6vdDFyiMmwYrt1POiF8oWGh1A==", + "requires": { + "@babel/runtime": "^7.5.5", + "match-sorter": "^6.0.2" + } + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", @@ -13503,6 +13521,11 @@ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=" + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", diff --git a/package.json b/package.json index 7d149a8a7861b0d39a33446f056751e125252cd6..f0269bcdeaccb8b89b7e645852b2e6ae473d8b70 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "react": "^16.14.0", "react-dom": "^16.14.0", "react-hook-form": "^6.14.2", + "react-query": "^3.6.0", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-scripts": "^3.4.4", diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5279e89bd76752473dd481d4087c9d71a3644806 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { ClientContextProvider } from './context'; +import ThemeProvider from './context/ThemeProvider'; +import { UserStateProvider } from './context/UserContext'; +import Routes from './Routes'; +import { darkTheme } from './theme'; + +function App(): React.ReactElement { + return ( + <ClientContextProvider> + <UserStateProvider> + <ThemeProvider theme={darkTheme}> + <Router> + <Routes /> + </Router> + </ThemeProvider> + </UserStateProvider> + </ClientContextProvider> + ); +} + +export default App; diff --git a/src/core/components/MainRouting.tsx b/src/Routes.tsx similarity index 57% rename from src/core/components/MainRouting.tsx rename to src/Routes.tsx index 9de8dc4fc746809f74605ce33ed7d102431cab33..d11cc5a1c8fa2dfad2f9f9b6dafee1d2d54aae3e 100644 --- a/src/core/components/MainRouting.tsx +++ b/src/Routes.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Route, Switch } from 'react-router'; -import NewsPage from '../../components/NewsPage'; -import ProfileButton from './ProfileButton'; +import ProfileButton from './components/ProfileButton'; +import NewsPage from './pages/NewsPage'; -const MainRouting: React.FC = () => ( +const Routes: React.FC = () => ( <Switch> <Route path="/" exact> <ProfileButton /> @@ -14,4 +14,4 @@ const MainRouting: React.FC = () => ( </Switch> ); -export default MainRouting; +export default Routes; diff --git a/src/client/auth.ts b/src/client/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..81dff37dae8fc798967691078c55f04c1fcbe1ba --- /dev/null +++ b/src/client/auth.ts @@ -0,0 +1,42 @@ +import Axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { EmptyReq, LoginReq, RegisterReq, UserResponse } from './types'; + +type LoginQuery = (data?: LoginReq) => Promise<AxiosResponse<UserResponse>>; +type RegisterQuery = (data?: RegisterReq) => Promise<AxiosResponse<UserResponse>>; +type MeQuery = (data?: EmptyReq, authToken?: string) => Promise<AxiosResponse<UserResponse>>; + +export type AuthClient = { + login: LoginQuery; + me: MeQuery; + register: RegisterQuery; +}; + +export function auth(config: AxiosRequestConfig): AuthClient { + const axios = Axios.create({ + ...config, + baseURL: `${config.baseURL}/api/auth`, + }); + + const login: LoginQuery = (data?: LoginReq): Promise<AxiosResponse<UserResponse>> => { + return axios.post<UserResponse>('/login', data); + }; + + const register: RegisterQuery = (data?: RegisterReq): Promise<AxiosResponse<UserResponse>> => { + return axios.post<UserResponse>('/register', data); + }; + + const me: MeQuery = ( + data?: undefined, + authToken?: string, + ): Promise<AxiosResponse<UserResponse>> => { + return axios.get<UserResponse>('/me', { + headers: { Authorization: `Bearer ${authToken}` }, + }); + }; + + return { + login, + register, + me, + }; +} diff --git a/src/client/client.ts b/src/client/client.ts new file mode 100644 index 0000000000000000000000000000000000000000..062ff8145dd6185b309d2d707084b75fe925bc92 --- /dev/null +++ b/src/client/client.ts @@ -0,0 +1,23 @@ +import { AxiosRequestConfig } from 'axios'; +import { auth, AuthClient } from './auth'; + +const DEFAULT_BASE_URL = ''; + +export function createClient( + baseURL = DEFAULT_BASE_URL, + configOverride?: AxiosRequestConfig, +): Client { + const config: AxiosRequestConfig = { + baseURL, + withCredentials: true, + ...configOverride, + }; + const client = { + auth: auth(config), + }; + return client; +} + +export type Client = { + auth: AuthClient; +}; diff --git a/src/client/types.ts b/src/client/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b50c0f2e3bcf7ee76917538416c9428673b1c0e --- /dev/null +++ b/src/client/types.ts @@ -0,0 +1,15 @@ +import { Profile } from '../types/types'; + +export type EmptyReq = undefined; + +export type LoginReq = { + username: string; + password: string; +}; +export type RegisterReq = { + username: string; + password: string; +}; +export type UserResponse = { + user: Profile; +}; diff --git a/src/core/components/CustomModal.tsx b/src/components/CustomModal.tsx similarity index 100% rename from src/core/components/CustomModal.tsx rename to src/components/CustomModal.tsx diff --git a/src/core/components/Footer.tsx b/src/components/Footer.tsx similarity index 100% rename from src/core/components/Footer.tsx rename to src/components/Footer.tsx diff --git a/src/core/components/Header.tsx b/src/components/Header.tsx similarity index 90% rename from src/core/components/Header.tsx rename to src/components/Header.tsx index e6c3963ea9142a26c02d0d0cff4d2fed04ca67ef..191c5bc6b9fed4543f0ddc9d4ad8bebb29f839fa 100644 --- a/src/core/components/Header.tsx +++ b/src/components/Header.tsx @@ -37,15 +37,16 @@ const MENU_ITEMS: AppBarMenuItem[] = [ const useStyles = makeStyles((theme) => ({ appBar: { - backgroundColor: '#E5BD2F', + backgroundColor: theme.palette.primary.main, padding: theme.spacing(1), }, tab: { + color: theme.palette.secondary.main, fontWeight: 600, padding: '0px', }, indicator: { - backgroundColor: 'white', + backgroundColor: theme.palette.secondary.main, }, navItem: { flex: 1, @@ -94,7 +95,9 @@ const Header: React.FC = () => { <Typography variant="h4">SCH-BODY</Typography> </Grid> <Grid className={classes.navItem} item container justify="flex-end"> - <Button variant="contained">Login</Button> + <Button color="secondary" variant="contained"> + Login + </Button> </Grid> </Grid> </Toolbar> diff --git a/src/components/News.tsx b/src/components/News.tsx index d674e392308f147b57dbfa910004eb7aae255b22..6927dde045399f8b630019ec18dbf41ec072f8cb 100644 --- a/src/components/News.tsx +++ b/src/components/News.tsx @@ -10,8 +10,9 @@ interface NewsProps { const useStyles = makeStyles((theme) => ({ container: { + color: 'white', borderRadius: '8px', - backgroundColor: '#E5E5E5', + backgroundColor: theme.palette.background.paper, margin: theme.spacing(2), padding: theme.spacing(1), }, diff --git a/src/components/NewsContainer.tsx b/src/components/NewsContainer.tsx index eaa3db945382fb25a0e906c6eb209a3d1c01a032..42714e2cc3f9909d5f9ae52a284dc76e142f6249 100644 --- a/src/components/NewsContainer.tsx +++ b/src/components/NewsContainer.tsx @@ -1,30 +1,17 @@ import { CircularProgress, Grid } from '@material-ui/core'; -import React, { useEffect } from 'react'; -import useGetNewsList from '../hooks/useGetNewsList'; +import React from 'react'; import News from './News'; interface Props {} const NewsContainer: React.FC = () => { - const [{ data, isLoading }, getNews] = useGetNewsList(); - - useEffect(() => { - getNews(); - }, [getNews]); + const isLoading = false; return isLoading ? ( <CircularProgress /> ) : ( <Grid container spacing={2}> - {data && - data.map((item) => ( - <News - title={item.title} - author="Anonymus" - content={item.text} - createDate={item.publishedAt} - /> - ))} + <News title="ASd" author="Anonymus" content="ASDADDD" createDate="2021.01.01" /> </Grid> ); }; diff --git a/src/components/NewsPage.tsx b/src/components/NewsPage.tsx deleted file mode 100644 index 82b744c859b836b9467d60e5eab4a9ec08cf1306..0000000000000000000000000000000000000000 --- a/src/components/NewsPage.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container } from '@material-ui/core'; -import React from 'react'; -import NewsContainer from './NewsContainer'; - -const NewsPage: React.FC = () => ( - <Container> - <NewsContainer /> - </Container> -); - -export default NewsPage; diff --git a/src/core/components/ProfileButton.tsx b/src/components/ProfileButton.tsx similarity index 100% rename from src/core/components/ProfileButton.tsx rename to src/components/ProfileButton.tsx diff --git a/src/core/components/ProfileModal.tsx b/src/components/ProfileModal.tsx similarity index 100% rename from src/core/components/ProfileModal.tsx rename to src/components/ProfileModal.tsx diff --git a/src/context/ClientContext.tsx b/src/context/ClientContext.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c1130185348c6ba33d919dec4da70b93a6d0a2b4 --- /dev/null +++ b/src/context/ClientContext.tsx @@ -0,0 +1,29 @@ +import React, { createContext } from 'react'; +import { Client, createClient } from '../client/client'; + +export type ClientContextType = { + client: Client; +}; + +export const ClientContext = createContext<ClientContextType>({ + client: createClient(), +}); + +type ClientContextProviderProps = { + baseUrl?: string; +}; + +export const ClientContextProvider: React.FC<ClientContextProviderProps> = ({ + children, + baseUrl = '', +}) => { + return ( + <ClientContext.Provider + value={{ + client: createClient(baseUrl), + }} + > + {children} + </ClientContext.Provider> + ); +}; diff --git a/src/context/ThemeProvider.tsx b/src/context/ThemeProvider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a32c2249373bd7826f228c0c1251a6631e180453 --- /dev/null +++ b/src/context/ThemeProvider.tsx @@ -0,0 +1,29 @@ +import { + createMuiTheme, + StylesProvider, + Theme, + ThemeProvider as MUIThemeProvider, +} from '@material-ui/core/styles'; +import React, { useMemo } from 'react'; +import { ThemeProvider as SCThemeProvider } from 'styled-components'; +import { theme as defaultTheme } from '../theme'; + +export interface ThemeProviderProps { + theme?: Theme; +} + +const ThemeProvider: React.FC<ThemeProviderProps> = ({ theme: themeProp, children }) => { + const theme = useMemo( + () => (themeProp ? createMuiTheme(defaultTheme, themeProp) : defaultTheme), + [themeProp], + ); + return ( + <StylesProvider injectFirst> + <MUIThemeProvider theme={theme}> + <SCThemeProvider theme={theme}>{children}</SCThemeProvider> + </MUIThemeProvider> + </StylesProvider> + ); +}; + +export default ThemeProvider; diff --git a/src/core/context/UserContext.tsx b/src/context/UserContext.tsx similarity index 93% rename from src/core/context/UserContext.tsx rename to src/context/UserContext.tsx index 0c8730888f2d7442b21a156755c96ab92c60975d..4a9de33994518c115aacba0dce7c0ef423fb07ac 100644 --- a/src/core/context/UserContext.tsx +++ b/src/context/UserContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useState } from 'react'; -import { Profile, Role } from '../types'; +import { Profile, Role } from '../types/types'; interface ContextProps { profile: Profile; diff --git a/src/context/index.tsx b/src/context/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8577543dbc08fe049b8acc239df36a3eb958b549 --- /dev/null +++ b/src/context/index.tsx @@ -0,0 +1,2 @@ +export * from './ClientContext'; +export * from './UserContext'; diff --git a/src/core/App.tsx b/src/core/App.tsx deleted file mode 100644 index 00d70f3d25ef7e15a784a3c7624c98e35d417e5e..0000000000000000000000000000000000000000 --- a/src/core/App.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useEffect } from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import styled from 'styled-components'; -import useAddNews from '../hooks/useAddNews'; -import Footer from './components/Footer'; -import Header from './components/Header'; -import MainRouting from './components/MainRouting'; -import { UserStateProvider } from './context/UserContext'; - -const Container = styled.div` - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; -`; - -const MainContent = styled.div` - height: 100%; -`; - -function App(): React.ReactElement { - const [response, addNews] = useAddNews(); - - useEffect(() => { - addNews({ body: { title: 'Testing', text: 'Test Test TEST' } }); - }, [addNews]); - - useEffect(() => { - if (response.data) { - console.log(response.data.text); - } - }, [response.data]); - - return ( - <UserStateProvider> - <Router> - <Container> - <Header /> - <MainContent> - <MainRouting /> - </MainContent> - <Footer /> - </Container> - </Router> - </UserStateProvider> - ); -} - -export default App; diff --git a/src/hooks/useAddNews.ts b/src/hooks/useAddNews.ts deleted file mode 100644 index 51d2eb199c2749d2cbdf945825a17b80f0de649b..0000000000000000000000000000000000000000 --- a/src/hooks/useAddNews.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { News } from '../core/types'; -import useRequest from './useRequest'; -import useRestQueries from './useRestQueries'; - -const useAddNews = () => { - const { post } = useRestQueries(); - const request = post<News>('/api/v1/news'); - - return useRequest<News>({ request }); -}; - -export default useAddNews; diff --git a/src/hooks/useClientContext.ts b/src/hooks/useClientContext.ts new file mode 100644 index 0000000000000000000000000000000000000000..67f603c15ac1888d7e60ec6cef503ca9f8ba99c7 --- /dev/null +++ b/src/hooks/useClientContext.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react'; +import { ClientContext } from '../context'; +import type { ClientContextType } from '../context'; + +function useClientContext(): ClientContextType { + const clientContext = useContext(ClientContext); + if (!clientContext) { + throw new Error('You must use `ClientContextProvider` to access the `ClientContext`.'); + } + return clientContext; +} + +export default useClientContext; diff --git a/src/hooks/useGetNewsList.ts b/src/hooks/useGetNewsList.ts deleted file mode 100644 index 850572353916fa2cc58ba0ab293c4b32e50c4fc7..0000000000000000000000000000000000000000 --- a/src/hooks/useGetNewsList.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { News } from '../core/types'; -import useRequest from './useRequest'; -import useRestQueries from './useRestQueries'; - -const useGetNewsList = (data?: News[]) => { - const { get } = useRestQueries(); - const request = get<News[]>('/api/v1/news'); - - return useRequest<News[]>({ request, initialData: data }); -}; - -export default useGetNewsList; diff --git a/src/hooks/useLogin.ts b/src/hooks/useLogin.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b7e823ab9ef3c89162c2a1864915f346f79cb1c --- /dev/null +++ b/src/hooks/useLogin.ts @@ -0,0 +1,8 @@ +import { useMutation } from 'react-query'; +import useClientContext from './useClientContext'; + +export default function useLogin() { + const client = useClientContext(); + + return useMutation('login', client.client.auth.login); +} diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts deleted file mode 100644 index 6e41e58f6add946cf3015c953537ab50a9879353..0000000000000000000000000000000000000000 --- a/src/hooks/useRequest.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { Refetch, RequestArgs, ResponseData } from './types'; - -function useRequest<T = any>({ request, initialData }: RequestArgs<T>): [ResponseData<T>, Refetch] { - const [data, setData] = useState(initialData); - const [isLoading, setIsLoading] = useState(false); - const [isError, setIsError] = useState(false); - const [fetching, setFetching] = useState(false); - const [requestParams, setRequestParams] = useState({}); - - const refetch = useCallback((newParams?) => { - setRequestParams(newParams ?? {}); - setFetching(true); - }, []); - - useEffect(() => { - const fetchData = async () => { - setIsError(false); - setIsLoading(true); - - try { - const result = await request(requestParams); - - setData(result.data); - } catch (error) { - setIsError(true); - } - setFetching(false); - setIsLoading(false); - }; - - if (fetching && !isLoading) { - fetchData(); - } - }, [request, fetching, isLoading, requestParams]); - - return [{ data, isLoading, isError }, refetch]; -} - -export default useRequest; diff --git a/src/hooks/useRestQueries.ts b/src/hooks/useRestQueries.ts deleted file mode 100644 index 3b91a068b9f7b4bf32667fbaee378c329c095f8c..0000000000000000000000000000000000000000 --- a/src/hooks/useRestQueries.ts +++ /dev/null @@ -1,20 +0,0 @@ -import axios, { AxiosRequestConfig } from 'axios'; -import { ApiRequest, RequestParams } from './types'; - -function useRestQueries( - // When a request needs more config like CancelToken, etc. - config?: AxiosRequestConfig, -): Record<string, <Data>(path: string) => ApiRequest<Data>> { - return { - get: <Data>(path: string) => (params: RequestParams) => - axios.get<Data>(path, { ...config, ...params }), - post: <Data>(path: string) => (params: RequestParams) => - axios.post<Data>(path, params.body ?? {}, { ...config, ...params }), - put: <Data>(path: string) => (params: RequestParams) => - axios.put<Data>(path, params.body ?? {}, { ...config, ...params }), - delete: (path: string) => (params: RequestParams) => - axios.delete(path, { ...config, ...params }), - }; -} - -export default useRestQueries; diff --git a/src/index.tsx b/src/index.tsx index 65d5ef9d6423481dfe5b8633ff9e592cee535581..57a536ca7c3661c785a033ce7226c025c1bc210c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import App from './core/App'; +import App from './App'; import './index.css'; import * as serviceWorker from './serviceWorker'; diff --git a/src/pages/NewsPage.tsx b/src/pages/NewsPage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..45a629d8cd4d0d4db95402b2540b0001ee88949b --- /dev/null +++ b/src/pages/NewsPage.tsx @@ -0,0 +1,14 @@ +import { Container } from '@material-ui/core'; +import React from 'react'; +import NewsContainer from '../components/NewsContainer'; +import Page from './Page'; + +const NewsPage: React.FC = () => ( + <Page> + <Container> + <NewsContainer /> + </Container> + </Page> +); + +export default NewsPage; diff --git a/src/pages/Page.tsx b/src/pages/Page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d2204f6460d05e80232a5c3bd71f695facb3f575 --- /dev/null +++ b/src/pages/Page.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import styled from 'styled-components'; +import Footer from '../components/Footer'; +import Header from '../components/Header'; + +const Container = styled.div( + (props) => ` + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + + background-color: ${props.theme.palette.background.default}; +`, +); + +const MainContent = styled.div` + height: 100%; +`; + +const Page: React.FC = ({ children }) => { + return ( + <Container> + <Header /> + <MainContent>{children}</MainContent> + <Footer /> + </Container> + ); +}; + +export default Page; diff --git a/src/styled-components.d.ts b/src/styled-components.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..38cc3fd77826cef9386713bf741b22a5323152b0 --- /dev/null +++ b/src/styled-components.d.ts @@ -0,0 +1,7 @@ +import { Theme } from '@material-ui/core/styles'; +import 'styled-components'; + +declare module 'styled-components' { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface DefaultTheme extends Theme {} +} diff --git a/src/theme.ts b/src/theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..b40cff6a6f63846c622d1abc6dc226dde0fb738f --- /dev/null +++ b/src/theme.ts @@ -0,0 +1,50 @@ +import { createMuiTheme } from '@material-ui/core'; + +// eslint-disable-next-line import/prefer-default-export +export const theme = createMuiTheme({ + // TODO: Create default theme + /* palette: { + primary: {}, + secondary: {}, + error: {}, + success: {}, + info: {}, + warning: {}, + grey: {}, + + text: {}, + }, */ + typography: { + fontFamily: 'Roboto', + }, +}); + +export const darkTheme = createMuiTheme({ + palette: { + primary: { + main: '#1A6B97', + contrastText: '#FFFFFF', + }, + secondary: { + main: '#FFD200', + contrastText: '#202020', + }, + error: { + main: '#FF0C3E', + }, + background: { + default: '#2A2A28', + paper: '#202020', + }, + /* success: {}, + info: {}, + warning: {}, */ + + text: { + primary: '#FFFFFF', + }, + }, + typography: { + fontFamily: 'Roboto', + }, +}); diff --git a/src/core/types.ts b/src/types/types.ts similarity index 100% rename from src/core/types.ts rename to src/types/types.ts