feat: V2 with TypeScript

This commit is contained in:
Ben Elferink
2024-12-14 16:14:14 +02:00
parent f6c0a52ab5
commit a41cab872f
63 changed files with 13390 additions and 35925 deletions

21
client/.gitignore vendored
View File

@@ -1,21 +0,0 @@
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.eslintcache
npm-debug.log*
yarn-debug.log*
yarn-error.log*

41424
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,16 @@
{
"name": "client",
"name": "client-v2",
"version": "0.1.0",
"private": true,
"main": "index.js",
"license": "ISC",
"scripts": {
"start": "react-scripts start",
"dev": "react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject"
},
"dependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@fontsource/roboto": "^4.5.1",
"@mui/material": "^5.0.6",
"axios": "^0.24.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "^4.0.3"
"start": "serve -s build"
},
"eslintConfig": {
"extends": [
"react-app"
"react-app",
"react-app/jest"
]
},
"browserslist": {
@@ -35,5 +24,20 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.2.0",
"axios": "^1.7.9",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-scripts": "5.0.1",
"zustand": "^5.0.2"
},
"devDependencies": {
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"serve": "^14.2.4"
}
}

View File

@@ -6,11 +6,15 @@
<!-- <meta name="author" content="" /> -->
<!-- <meta name="description" content="" /> -->
<!-- <meta name="keywords" content="" /> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="icon" type="image/x-icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Web App</title>
</head>
<body>

View File

@@ -9,14 +9,14 @@
"type": "image/x-icon"
},
{
"src":"/android-chrome-192x192.png",
"sizes":"192x192",
"type":"image/png"
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src":"/android-chrome-512x512.png",
"sizes":"512x512",
"type":"image/png"
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": ".",

View File

@@ -0,0 +1,10 @@
export interface Account {
username: string
password: string
role: 'user' | 'admin'
}
export interface FormData {
username: Account['username']
password: Account['password']
}

View File

@@ -1,24 +0,0 @@
import {useAuth} from './contexts/AuthContext'
import Header from './components/Header'
export default function App() {
const {isLoggedIn} = useAuth()
return (
<div className='App'>
<Header />
{isLoggedIn ? <LoggedInText /> : <LoggedOutText />}
</div>
)
}
const LoggedInText = () => {
const {account} = useAuth()
return <p>Hey, {account.username}! I'm happy to let you know: you are authenticated!</p>
}
const LoggedOutText = () => (
<p>Don't forget to start your backend server, then authenticate yourself.</p>
)

43
client/src/App.tsx Normal file
View File

@@ -0,0 +1,43 @@
import React, { Fragment } from 'react'
import { useAuth } from 'contexts/AuthContext'
import AuthModal from 'components/AuthModal'
import Header from 'components/Header'
import logo from 'assets/react.svg'
import 'styles/ReactWelcome.css'
const App = () => {
return (
<div className='App'>
<Header />
<ReactWelcome />
<LoggedInStatus />
<AuthModal />
</div>
)
}
const ReactWelcome = () => {
return (
<Fragment>
<img src={logo} className='ReactWelcome-logo' alt='logo' />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a className='ReactWelcome-link' href='https://reactjs.org' target='_blank' rel='noopener noreferrer'>
Learn React
</a>
</Fragment>
)
}
const LoggedInStatus = () => {
const { isLoggedIn, account } = useAuth()
if (isLoggedIn && !!account) {
return <p>Hey, {account.username}! I'm happy to let you know: you are authenticated!</p>
}
return <p>Don't forget to start your backend server, and then authenticate yourself.</p>
}
export default App

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,121 +0,0 @@
import {Fragment, useState} from 'react'
import {Dialog, DialogTitle, TextField, Button, CircularProgress} from '@mui/material'
import {useAuth} from '../contexts/AuthContext'
const textFieldSx = {mx: 2, my: 0.5}
export default function AuthModal({open, close, isRegisterMode, toggleRegister}) {
const {login, register} = useAuth()
const [formData, setFormData] = useState({})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleChange = (e) => {
const {name, value} = e.target
setFormData((prev) => ({...prev, [name]: value}))
}
const clickSubmit = async () => {
setLoading(true)
setError('')
try {
isRegisterMode ? await register(formData) : await login(formData)
close()
} catch (error) {
setError(error)
}
setLoading(false)
}
const disabledLoginButton = !formData['username'] || !formData['password']
const disabledRegisterButton = !formData['username'] || !formData['password']
return (
<Dialog open={open} onClose={close}>
{isRegisterMode ? (
<RegisterForm formData={formData} handleChange={handleChange} />
) : (
<LoginForm formData={formData} handleChange={handleChange} />
)}
{error && <span className='error'>{error}</span>}
{loading ? (
<center>
<CircularProgress color='inherit' />
</center>
) : (
<Button
onClick={clickSubmit}
disabled={isRegisterMode ? disabledRegisterButton : disabledLoginButton}>
{isRegisterMode ? 'Register' : 'Login'}
</Button>
)}
<Button onClick={toggleRegister}>
{isRegisterMode ? 'I already have an account' : "I don't have an account"}
</Button>
</Dialog>
)
}
function LoginForm({formData, handleChange}) {
return (
<Fragment>
<DialogTitle>Login to your account</DialogTitle>
<TextField
label='Username'
name='username'
type='text'
value={formData['username'] || ''}
onChange={handleChange}
variant='filled'
sx={textFieldSx}
required
/>
<TextField
label='Password'
name='password'
type='password'
value={formData['password'] || ''}
onChange={handleChange}
variant='filled'
sx={textFieldSx}
required
/>
</Fragment>
)
}
function RegisterForm({formData, handleChange}) {
return (
<Fragment>
<DialogTitle>Create a new account</DialogTitle>
<TextField
label='Username'
name='username'
type='text'
value={formData['username'] || ''}
onChange={handleChange}
variant='filled'
sx={textFieldSx}
required
/>
<TextField
label='Password'
name='password'
type='password'
value={formData['password'] || ''}
onChange={handleChange}
variant='filled'
sx={textFieldSx}
required
/>
</Fragment>
)
}

View File

@@ -0,0 +1,92 @@
import React, { type ChangeEventHandler, Fragment, useState } from 'react'
import { useModalStore } from 'store/useModalStore'
import { useAuth } from 'contexts/AuthContext'
import { Dialog, DialogTitle, TextField, Button, CircularProgress } from '@mui/material'
import { type FormData } from '@types'
interface Props {}
const AuthModal: React.FC<Props> = () => {
const { login, register } = useAuth()
const { currentModal, setCurrentModal } = useModalStore()
const isRegisterMode = currentModal === 'REGISTER'
const isOpen = ['AUTH', 'LOGIN', 'REGISTER'].includes(currentModal)
const onClose = () => setCurrentModal('')
const [formData, setFormData] = useState<FormData>({ username: '', password: '' })
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
}
const clickSubmit = async () => {
setLoading(true)
setError('')
try {
isRegisterMode ? await register(formData) : await login(formData)
onClose()
} catch (error: any) {
setError(typeof error === 'string' ? error : JSON.stringify(error))
}
setLoading(false)
}
const isSubmitButtonDisabled = !formData['username'] || !formData['password']
return (
<Dialog open={isOpen} onClose={onClose}>
{isRegisterMode ? <DialogTitle>Create a new account</DialogTitle> : <DialogTitle>Login to your account</DialogTitle>}
<TextField
label='Username'
name='username'
type='text'
value={formData['username']}
onChange={handleChange}
variant='filled'
sx={{ mx: 2, my: 0.5 }}
required
/>
<TextField
label='Password'
name='password'
type='password'
value={formData['password']}
onChange={handleChange}
variant='filled'
sx={{ mx: 2, my: 0.5 }}
required
/>
{error && <span className='error'>{error}</span>}
{loading ? (
<center>
<CircularProgress color='inherit' />
</center>
) : isRegisterMode ? (
<Fragment>
<Button onClick={clickSubmit} disabled={isSubmitButtonDisabled}>
Register
</Button>
<Button onClick={() => setCurrentModal('LOGIN')}>I already have an account</Button>
</Fragment>
) : (
<Fragment>
<Button onClick={clickSubmit} disabled={isSubmitButtonDisabled}>
Login
</Button>
<Button onClick={() => setCurrentModal('REGISTER')}>I don't have an account</Button>
</Fragment>
)}
</Dialog>
)
}
export default AuthModal

View File

@@ -1,85 +0,0 @@
import {Fragment, useState} from 'react'
import {
AppBar,
IconButton,
Avatar,
Popover,
List,
ListSubheader,
ListItemButton,
} from '@mui/material'
import OnlineIndicator from './OnlineIndicator'
import AuthModal from './AuthModal'
import {useAuth} from '../contexts/AuthContext'
export default function Header() {
const {isLoggedIn, account, logout} = useAuth()
const [anchorEl, setAnchorEl] = useState(null)
const [popover, setPopover] = useState(false)
const [authModal, setAuthModal] = useState(false)
const [register, setRegister] = useState(false)
const openPopover = (e) => {
setPopover(true)
setAnchorEl(e.currentTarget)
}
const closePopover = () => {
setPopover(false)
setAnchorEl(null)
}
const clickLogin = () => {
setRegister(false)
setAuthModal(true)
closePopover()
}
const clickRegister = () => {
setRegister(true)
setAuthModal(true)
closePopover()
}
return (
<AppBar className='header' position='static'>
<h1>Web App</h1>
<IconButton onClick={openPopover}>
<OnlineIndicator online={isLoggedIn}>
<Avatar src={account?.username || ''} alt={account?.username || ''} />
</OnlineIndicator>
</IconButton>
<Popover
anchorEl={anchorEl}
open={popover}
onClose={closePopover}
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
transformOrigin={{vertical: 'top', horizontal: 'right'}}>
<List style={{minWidth: '100px'}}>
<ListSubheader style={{textAlign: 'center'}}>
Hello, {isLoggedIn ? account.username : 'Guest'}
</ListSubheader>
{isLoggedIn ? (
<ListItemButton onClick={logout}>Logout</ListItemButton>
) : (
<Fragment>
<ListItemButton onClick={clickLogin}>Login</ListItemButton>
<ListItemButton onClick={clickRegister}>Reigster</ListItemButton>
</Fragment>
)}
</List>
</Popover>
<AuthModal
open={authModal}
close={() => setAuthModal(false)}
isRegisterMode={register}
toggleRegister={() => setRegister((prev) => !prev)}
/>
</AppBar>
)
}

View File

@@ -0,0 +1,70 @@
import React, { Fragment, type MouseEventHandler, useState } from 'react'
import { useModalStore } from 'store/useModalStore'
import { useAuth } from 'contexts/AuthContext'
import OnlineIndicator from 'components/OnlineIndicator'
import { AppBar, IconButton, Avatar, Popover, List, ListSubheader, ListItemButton } from '@mui/material'
interface Props {}
const Header: React.FC<Props> = () => {
const { isLoggedIn, account, logout } = useAuth()
const { setCurrentModal } = useModalStore()
const [anchorEl, setAnchorEl] = useState<(EventTarget & HTMLButtonElement) | null>(null)
const [popover, setPopover] = useState(false)
const openPopover: MouseEventHandler<HTMLButtonElement> = (e) => {
setPopover(true)
setAnchorEl(e.currentTarget)
}
const closePopover = () => {
setPopover(false)
setAnchorEl(null)
}
const clickLogin = () => {
setCurrentModal('LOGIN')
closePopover()
}
const clickRegister = () => {
setCurrentModal('REGISTER')
closePopover()
}
return (
<AppBar className='header' position='static'>
<h1>Web App</h1>
<IconButton onClick={openPopover}>
<OnlineIndicator online={isLoggedIn}>
<Avatar src={account?.username || ''} alt={account?.username || 'Guest'} />
</OnlineIndicator>
</IconButton>
<Popover
anchorEl={anchorEl}
open={popover}
onClose={closePopover}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<List style={{ minWidth: '100px' }}>
<ListSubheader style={{ textAlign: 'center' }}>Hello, {account?.username || 'Guest'}</ListSubheader>
{isLoggedIn ? (
<ListItemButton onClick={logout}>Logout</ListItemButton>
) : (
<Fragment>
<ListItemButton onClick={clickLogin}>Login</ListItemButton>
<ListItemButton onClick={clickRegister}>Register</ListItemButton>
</Fragment>
)}
</List>
</Popover>
</AppBar>
)
}
export default Header

View File

@@ -1,7 +1,8 @@
import {styled} from '@mui/material/styles'
import {Badge, Avatar} from '@mui/material'
import React from 'react'
import { styled } from '@mui/material/styles'
import { Badge, Avatar } from '@mui/material'
const StyledBadge = styled(Badge)(({theme}) => ({
const StyledBadge = styled(Badge)(({ theme }) => ({
'& .MuiBadge-badge': {
backgroundColor: 'black',
color: 'black',
@@ -30,34 +31,34 @@ const StyledBadge = styled(Badge)(({theme}) => ({
},
}))
const OnlineBadge = styled(StyledBadge)(({theme}) => ({
const OnlineBadge = styled(StyledBadge)({
'& .MuiBadge-badge': {
backgroundColor: 'var(--online)',
color: 'var(--online)',
},
}))
})
const OfflineBadge = styled(StyledBadge)(({theme}) => ({
const OfflineBadge = styled(StyledBadge)({
'& .MuiBadge-badge': {
backgroundColor: 'var(--offline)',
color: 'var(--offline)',
},
}))
})
export default function OnlineIndicator({online = false, children = <Avatar src='' alt='' />}) {
return online ? (
<OnlineBadge
variant='dot'
overlap='circular'
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}>
{children}
</OnlineBadge>
) : (
<OfflineBadge
variant='dot'
overlap='circular'
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}>
const OnlineIndicator = ({ online = false, children = <Avatar src='' alt='' /> }) => {
if (online) {
return (
<OnlineBadge variant='dot' overlap='circular' anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}>
{children}
</OnlineBadge>
)
}
return (
<OfflineBadge variant='dot' overlap='circular' anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}>
{children}
</OfflineBadge>
)
}
export default OnlineIndicator

View File

@@ -1,6 +1,4 @@
// api url (where your server is hosted at)
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'http://localhost:8080'
export {
BACKEND_URL,
}
export { BACKEND_URL }

View File

@@ -1,61 +1,69 @@
import { createContext, useContext, useState, useEffect } from 'react'
import axios from '../utils/axios'
import React, { createContext, useContext, useState, useEffect, type PropsWithChildren, useMemo } from 'react'
import axios from 'utils/axios'
import { type FormData, type Account } from '@types'
// init context
const AuthContext = createContext()
// export the consumer
export function useAuth() {
return useContext(AuthContext)
interface Context {
token: string | null
account: Account | null
isLoggedIn: boolean
register: (payload: FormData) => Promise<any>
login: (payload: FormData) => Promise<any>
logout: () => void
}
// export the provider (handle all the logic here)
export function AuthProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [account, setAccount] = useState(null)
const [token, setToken] = useState(localStorage.getItem('token') || null)
const initContext: Context = {
token: null,
account: null,
isLoggedIn: false,
register: async () => {},
login: async () => {},
logout: () => {},
}
const register = (formData = {}) =>
new Promise((resolve, reject) => {
// init context
const AuthContext = createContext(initContext)
const { Provider } = AuthContext
// export the consumer
export const useAuth = () => useContext(AuthContext)
// export the provider
export const AuthProvider = ({ children }: PropsWithChildren) => {
const [token, setToken] = useState(localStorage.getItem('token') || initContext.token)
const [account, setAccount] = useState(initContext.account)
const [isLoggedIn, setIsLoggedIn] = useState(initContext.isLoggedIn)
const register = (formData: FormData) => {
return new Promise((resolve, reject) => {
axios
.post('/auth/register', formData)
.then(({
data: {
data: accountData,
token: accessToken,
},
}) => {
.then(({ data: { data: accountData, token: accessToken } }) => {
setAccount(accountData)
setToken(accessToken)
setIsLoggedIn(true)
resolve(true)
})
.catch((error) => {
console.error(error)
reject(error?.response?.data?.message || error.message)
})
})
}
const login = (formData = {}) =>
new Promise((resolve, reject) => {
const login = (formData: FormData) => {
return new Promise((resolve, reject) => {
axios
.post('/auth/login', formData)
.then(({
data: {
data: accountData,
token: accessToken,
},
}) => {
.then(({ data: { data: accountData, token: accessToken } }) => {
setAccount(accountData)
setToken(accessToken)
setIsLoggedIn(true)
resolve(true)
})
.catch((error) => {
console.error(error)
reject(error?.response?.data?.message || error.message)
})
})
}
const logout = () => {
setIsLoggedIn(false)
@@ -66,10 +74,7 @@ export function AuthProvider({ children }) {
const loginWithToken = async () => {
try {
const {
data: {
data: accountData,
token: accessToken,
},
data: { data: accountData, token: accessToken },
} = await axios.get('/auth/login', {
headers: {
authorization: `Bearer ${token}`,
@@ -79,7 +84,7 @@ export function AuthProvider({ children }) {
setAccount(accountData)
setToken(accessToken)
setIsLoggedIn(true)
} catch (error) {
} catch (error: any) {
console.error(error)
if (error?.response?.statusCode === 401) setToken(null)
}
@@ -102,17 +107,7 @@ export function AuthProvider({ children }) {
if (!isLoggedIn && !account && token) loginWithToken()
}, [isLoggedIn, account, token]) // eslint-disable-line react-hooks/exhaustive-deps
return (
<AuthContext.Provider
value={{
isLoggedIn,
account,
token,
register,
login,
logout,
}}>
{children}
</AuthContext.Provider>
)
const value = useMemo(() => ({ token, account, isLoggedIn, register, login, logout }), [token, account, isLoggedIn])
return <Provider value={value}>{children}</Provider>
}

View File

@@ -0,0 +1,19 @@
const getQueryPayload = (queryStr?: string) => {
if (!queryStr) {
console.warn('query string is not defined')
return {}
}
const queryObj: Record<string, string> = {}
const queryArr = (queryStr[0] === '?' ? queryStr.substring(1, queryStr.length) : queryStr).split('&')
queryArr.forEach((str) => {
const [key, val] = str.split('=')
queryObj[key] = val
})
return queryObj
}
export default getQueryPayload

View File

@@ -1,19 +0,0 @@
export default function getQuery(queryStr = '') {
if (queryStr) {
const queryObj = {}
const queryArr = (
queryStr[0] === '?' ? queryStr.substring(1, queryStr.length) : queryStr
).split('&')
queryArr.forEach((str) => {
const [key, val] = str.split('=')
queryObj[key] = val
})
return queryObj
} else {
console.warn('Query string is not defined')
return {}
}
}

View File

@@ -1,11 +0,0 @@
export default function getTokenPayload(token = '') {
if (token) {
const informativePart = token.split('.')[1]
const payload = JSON.parse(window.atob(informativePart))
return payload
} else {
console.warn('Token is not defined')
return {}
}
}

View File

@@ -0,0 +1,13 @@
const getTokenPayload = (token?: string) => {
if (!token) {
console.warn('token is not defined')
return {}
}
const informativePart = token.split('.')[1]
const payload = JSON.parse(window.atob(informativePart))
return payload
}
export default getTokenPayload

View File

@@ -1,20 +0,0 @@
import {useState, useEffect} from 'react'
/* How to use this hook:
import useLocalStorage from './hooks/useLocalStorage';
function App() {
const [example, setExample] = useLocalStorage('example_key', {example: 'anything as default'});
return ();
}; */
export default function useLocalStorage(key = '', defaultValue = null) {
const [value, setValue] = useState(JSON.parse(localStorage.getItem(key)) || defaultValue)
useEffect(() => {
if (value != null) localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue]
}

View File

@@ -0,0 +1,42 @@
import { useState, useEffect } from 'react'
/*
How to use this hook:
import useLocalStorage from './hooks/useLocalStorage';
function App() {
const [example, setExample] = useLocalStorage('example_key', { example: 'anything as default' });
return ();
};
*/
const getLsVal = (key: string, defaultValue: any) => {
const storedStr = localStorage.getItem(key) || ''
if (!!storedStr) {
return JSON.parse(storedStr)
} else {
return defaultValue
}
}
const setLsVal = (key: string, value: any) => {
if (value !== undefined && value !== null) {
const str = JSON.stringify(value)
localStorage.setItem(key, str)
}
}
const useLocalStorage = (key: string, defaultValue: any = null) => {
const [value, setValue] = useState(getLsVal(key, defaultValue))
useEffect(() => setLsVal(key, value), [key, value])
return [value, setValue]
}
export default useLocalStorage

View File

@@ -1,15 +1,19 @@
import {useState, useEffect} from 'react'
import { useState, useEffect } from 'react'
/* How to use this hook:
/*
How to use this hook:
import useMediaQuery from './hooks/useMediaQuery';
function App() {
const isMobile = useMediaQuery('(max-width: 768px)');
return ();
}; */
};
export default function useMediaQuery(query = '(max-width: 768px)') {
*/
const useMediaQuery = (query: string = '(max-width: 768px)') => {
const [matches, setMatches] = useState(window.matchMedia(query).matches)
useEffect(() => {
@@ -22,3 +26,5 @@ export default function useMediaQuery(query = '(max-width: 768px)') {
return matches
}
export default useMediaQuery

View File

@@ -1,17 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import {AuthProvider} from './contexts/AuthContext'
import CssBaseline from '@mui/material/CssBaseline'
import '@fontsource/roboto'
import './styles/index.css'
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<CssBaseline />
<App />
</AuthProvider>
</React.StrictMode>,
document.getElementById('root'),
)

18
client/src/index.tsx Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import CssBaseline from '@mui/material/CssBaseline'
import { AuthProvider } from 'contexts/AuthContext'
import App from 'App'
import 'styles/index.css'
const element = document.getElementById('root') as HTMLElement
const root = ReactDOM.createRoot(element)
root.render(
<React.StrictMode>
<AuthProvider>
<CssBaseline />
<App />
</AuthProvider>
</React.StrictMode>
)

1
client/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,11 @@
import { create } from 'zustand'
interface StoreState {
currentModal: string
setCurrentModal: (str: string) => void
}
export const useModalStore = create<StoreState>((set) => ({
currentModal: '',
setCurrentModal: (str) => set({ currentModal: str }),
}))

View File

@@ -0,0 +1,23 @@
.ReactWelcome-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.ReactWelcome-logo {
animation: ReactWelcome-logo-spin infinite 20s linear;
}
}
.ReactWelcome-link {
color: #61dafb;
}
@keyframes ReactWelcome-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -4,8 +4,16 @@
}
body {
font-family: 'Roboto';
margin: 0;
font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
.App {
@@ -35,7 +43,7 @@ body {
}
/*
Extra Small Devices, Phones
Extra Small Devices, Phones
@media only screen and (min-width: 480px) {}
*/

View File

@@ -1,7 +0,0 @@
import axios from 'axios'
import { BACKEND_URL } from '../constants'
// axios configuration
export default axios.create({
baseURL: BACKEND_URL,
})

View File

@@ -0,0 +1,8 @@
import Axios from 'axios'
import { BACKEND_URL } from '../constants'
const axios = Axios.create({
baseURL: BACKEND_URL,
})
export default axios

27
client/tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5", // Specify ECMAScript target version
"lib": ["dom", "dom.iterable", "esnext"], // List of library files to be included in the compilation
"allowJs": true, // Allow JavaScript files to be compiled
"skipLibCheck": true, // Skip type checking of all declaration files
"esModuleInterop": true, // Disables namespace imports (import * as fs from "fs") and enables CJS/AMD/UMD style imports (import fs from "fs")
"allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
"strict": true, // Enable all strict type checking options
"forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file.
"module": "esnext", // Specify module code generation
"moduleResolution": "node", // Resolve modules using Node.js style
"isolatedModules": true, // Unconditionally emit imports for unresolved files
"resolveJsonModule": true, // Include modules imported with .json extension
"noEmit": true, // Do not emit output (meaning do not compile code, only perform type checking)
"jsx": "react", // Support JSX in .tsx files
"sourceMap": true, // Generate corrresponding .map file
"declaration": true, // Generate corresponding .d.ts file
"noUnusedLocals": true, // Report errors on unused locals
"noUnusedParameters": true, // Report errors on unused parameters
"incremental": true, // Enable incremental compilation by reading/writing information from prior compilations to a file on disk
"noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases in switch statement
"baseUrl": "src"
},
"include": ["src"],
"exclude": ["node_modules", "build"]
}