-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(auth): init auth feature and global router
- Loading branch information
1 parent
e8b0eac
commit 13c8d07
Showing
15 changed files
with
741 additions
and
407 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import network from "@/utils/network"; | ||
import { AuthRequest, AuthResponse } from "./auth.types"; | ||
import { UserInfo } from "@/stores/auh.types"; | ||
|
||
const getUserInfo = async (): Promise<UserInfo> => network.get('/users/info') | ||
const loginUser = async (request: AuthRequest): Promise<AuthResponse> => network.post('/users/auth', JSON.stringify(request)) | ||
const registerUser = async (request: AuthRequest): Promise<AuthResponse> => network.post('/users', JSON.stringify(request)) | ||
|
||
export default { | ||
getUserInfo, | ||
loginUser, | ||
registerUser, | ||
|
||
QUERY_KEY_GET_USER_INFO: "getUserInfo" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export type AuthRequest = { | ||
email: string | ||
password: string | ||
name: string | ||
} | ||
|
||
export type AuthResponse = { | ||
token: string | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,125 +1,84 @@ | ||
import { useEffect, useLayoutEffect, useState } from 'react'; | ||
import ListItemDecorator from '@mui/joy/ListItemDecorator'; | ||
import Tabs from '@mui/joy/Tabs'; | ||
import TabList from '@mui/joy/TabList'; | ||
import Tab, { tabClasses } from '@mui/joy/Tab'; | ||
import HomeRoundedIcon from '@mui/icons-material/HomeRounded'; | ||
import ScheduleIcon from '@mui/icons-material/Schedule'; | ||
import SettingIcon from '@mui/icons-material/Settings'; | ||
import { Box, LinearProgress } from '@mui/joy'; | ||
import loadable from '@loadable/component' | ||
import { Box, LinearProgress, Typography } from '@mui/joy'; | ||
import './App.css'; | ||
|
||
import useTabStore from '@/stores/tab'; | ||
import { Outlet, RouterProvider, createRootRoute, createRoute, createRouter, redirect } from '@tanstack/react-router'; | ||
import useAuthStore from '@/stores/auth'; | ||
|
||
const colors = ['primary', 'danger', 'success', 'warning'] as const; | ||
const pages = [ | ||
loadable(() => import('./home')), | ||
loadable(() => import('./schedule')), | ||
loadable(() => import('./setting')) | ||
] | ||
// Root route section | ||
const rootRoute = createRootRoute({ | ||
component: () => ( | ||
<Box sx={{ minHeight: '100vh' }}> | ||
<Outlet /> | ||
</Box> | ||
), | ||
notFoundComponent: () => <Typography>Halaman Tidak Ditemukan, Hubungi Admin</Typography> | ||
}) | ||
|
||
function App() { | ||
const [windowReady, setWindowReady] = useState<boolean>(false) | ||
const {tab, setTab} = useTabStore() | ||
// Home route section | ||
const homeRoute = createRoute({ | ||
beforeLoad: ({ location }) => { | ||
const accessToken = useAuthStore.getState().accessToken | ||
if (location.pathname == '/') { | ||
throw redirect({ | ||
to: "/dashboard", | ||
replace: true | ||
}) | ||
} | ||
|
||
useLayoutEffect(() => { | ||
const queryString = window.location.search | ||
const urlParams = new URLSearchParams(queryString); | ||
if (urlParams.has('t')) { | ||
const t = Number(urlParams.get('t')) | ||
if (t < 1 || t > pages.length) return | ||
setTab(t) | ||
if (!accessToken) { | ||
throw redirect({ | ||
to: "/auth", | ||
replace: true | ||
}) | ||
} | ||
setWindowReady(true) | ||
}, [setTab]) | ||
}, | ||
getParentRoute: () => rootRoute, | ||
path: '/', | ||
}).lazy(() => import('./home').then(d => d.HomeRoute)) | ||
|
||
const dashboardRoute = createRoute({ | ||
getParentRoute: () => homeRoute, | ||
path: '/dashboard', | ||
loader: () => <></> | ||
}).lazy(() => import('./home/dashboard').then(d => d.DashboardRoute)) | ||
|
||
const scheduleRoute = createRoute({ | ||
getParentRoute: () => homeRoute, | ||
path: '/schedule', | ||
loader: () => <></> | ||
}).lazy(() => import('./home/schedule').then(d => d.ScheduleRoute)) | ||
|
||
const settingRoute = createRoute({ | ||
getParentRoute: () => homeRoute, | ||
path: '/setting', | ||
loader: () => <></> | ||
}).lazy(() => import('./home/setting').then(d => d.SettingRoute)) | ||
|
||
useEffect(() => { | ||
if (!windowReady) return | ||
window.history.replaceState({}, '', `?t=${tab}`) | ||
}, [tab, windowReady]) | ||
|
||
return ( | ||
<> | ||
<Page index={tab - 1} /> | ||
<BottomNavigation currentIndex={tab - 1} onIndexChange={(index: number) => setTab(index + 1)} /> | ||
</> | ||
) | ||
} | ||
// Auth route section | ||
const authRoute = createRoute({ | ||
getParentRoute: () => rootRoute, | ||
path: '/auth', | ||
}).lazy(() => import('./auth').then(d => d.AuthRoute)) | ||
|
||
const Page = ({ index }: { index: number }) => { | ||
const Page = pages[index] | ||
return <Box key={index} sx={{ pb: 2 }} className='fade'> | ||
<Page fallback={<LinearProgress />} /> | ||
</Box> | ||
} | ||
// Construct the route tree | ||
const routeTree = rootRoute.addChildren([ | ||
authRoute, | ||
homeRoute.addChildren([ | ||
dashboardRoute, | ||
scheduleRoute, | ||
settingRoute | ||
]) | ||
]) | ||
|
||
const BottomNavigation = ({ currentIndex, onIndexChange }: BottomNavigationProps) => { | ||
return <Tabs | ||
size="lg" | ||
aria-label="Bottom Navigation" | ||
value={currentIndex} | ||
onChange={(_, value) => onIndexChange(value as number)} | ||
sx={(theme) => ({ | ||
mt: 'auto', | ||
borderRadius: 16, | ||
position: 'sticky', | ||
bottom: 0, | ||
left: 0, | ||
right: 0, | ||
boxShadow: theme.shadow.sm, | ||
'--joy-shadowChannel': theme.vars.palette[colors[currentIndex]].darkChannel, | ||
[`& .${tabClasses.root}`]: { | ||
py: 1, | ||
flex: 1, | ||
transition: '0.3s', | ||
fontWeight: 'md', | ||
fontSize: 'md', | ||
[`&:not(.${tabClasses.selected}):not(:hover)`]: { | ||
opacity: 0.7, | ||
}, | ||
}, | ||
})} | ||
> | ||
<TabList | ||
variant='soft' | ||
size="sm" | ||
disableUnderline | ||
> | ||
<Tab | ||
orientation="vertical" | ||
{...(currentIndex === 0 && { color: colors[0] })} | ||
> | ||
<ListItemDecorator> | ||
<HomeRoundedIcon /> | ||
</ListItemDecorator> | ||
Dashboard | ||
</Tab> | ||
<Tab | ||
orientation="vertical" | ||
{...(currentIndex === 1 && { color: colors[1] })} | ||
> | ||
<ListItemDecorator> | ||
<ScheduleIcon /> | ||
</ListItemDecorator> | ||
Penjadwalan | ||
</Tab> | ||
<Tab | ||
orientation="vertical" | ||
{...(currentIndex === 2 && { color: colors[2] })} | ||
> | ||
<ListItemDecorator> | ||
<SettingIcon /> | ||
</ListItemDecorator> | ||
Pengaturan | ||
</Tab> | ||
</TabList> | ||
</Tabs> | ||
} | ||
const router = createRouter({ | ||
routeTree, | ||
defaultPendingComponent: () => <Box><LinearProgress /></Box>, | ||
defaultErrorComponent: () => <Typography>Terjadi Kesalahan, Segera Hubungi Admin</Typography>, | ||
defaultPendingMs: 0, | ||
defaultPendingMinMs: 0 | ||
}) | ||
|
||
type BottomNavigationProps = { | ||
currentIndex: number | ||
onIndexChange: (index: number) => void | ||
} | ||
const App = () => <RouterProvider router={router} /> | ||
|
||
export default App |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { Button, CircularProgress, Link, Typography } from "@mui/joy" | ||
import { Grid, TextField } from "@mui/material" | ||
import { createLazyRoute, useNavigate } from "@tanstack/react-router" | ||
|
||
import icon from '@/assets/icon.png' | ||
import { useEffect, useState } from "react" | ||
import authAPI from '@/apis/auth' | ||
import { useMutation, useQuery } from "@tanstack/react-query" | ||
import { AuthRequest } from "@/apis/auth.types" | ||
import useAuthStore from "@/stores/auth" | ||
|
||
const Auth = () => { | ||
const [isLoginState, setIsLoginState] = useState(true) | ||
const [name, setName] = useState("") | ||
const [email, setEmail] = useState("") | ||
const [password, setPassword] = useState("") | ||
const [error, setError] = useState({ | ||
"name": false, | ||
"password": false, | ||
"email": false, | ||
}) | ||
const auth = useAuthStore() | ||
const navigate = useNavigate() | ||
|
||
const authMutation = useMutation({ | ||
mutationFn: (request: AuthRequest) => isLoginState ? authAPI.loginUser(request) : authAPI.registerUser(request), | ||
onSuccess: (data) => { | ||
auth.setAuth(data.token) | ||
} | ||
}) | ||
|
||
const userInfoQuery = useQuery({ | ||
enabled: auth.accessToken != "", | ||
queryFn: authAPI.getUserInfo, | ||
queryKey: [authAPI.QUERY_KEY_GET_USER_INFO] | ||
}) | ||
|
||
const handleAuthSubmit = () => { | ||
if (!validateAuth()) return | ||
const payload = { email, password, name } | ||
authMutation.mutate(payload) | ||
} | ||
|
||
const handleChangeAuthMode = () => { | ||
setIsLoginState(prev => !prev) | ||
setName("") | ||
setEmail("") | ||
setPassword("") | ||
setError({ | ||
"name": false, | ||
"password": false, | ||
"email": false, | ||
}) | ||
} | ||
|
||
const validateAuth = (): boolean => { | ||
setError({ | ||
name: name == "", | ||
email: email == "", | ||
password: password == "", | ||
}) | ||
if (!isLoginState) { | ||
return name !== "" && email !== "" && password !== ""; | ||
} | ||
|
||
return email !== "" && password !== ""; | ||
} | ||
|
||
console.log(userInfoQuery.data) | ||
useEffect(() => { | ||
if (userInfoQuery.isLoading) return | ||
if (userInfoQuery.data?.is_active) { | ||
navigate({ | ||
to: '/dashboard', | ||
replace: true, | ||
}) | ||
} | ||
}, [userInfoQuery.data?.is_active, userInfoQuery.isLoading, navigate]) | ||
|
||
return ( | ||
<Grid container flexDirection='column' gap={2} sx={{ p: 2, height: '100vh' }} alignItems='center' justifyContent='center' className="fade"> | ||
<img src={icon} width={120} height={120} /> | ||
<Typography level="h2" fontWeight='500' sx={{ mb: 1 }}>ARA Farm IoT Panel</Typography> | ||
{auth.accessToken != "" ? ( | ||
<> | ||
{userInfoQuery.isLoading ? <CircularProgress /> : ( | ||
<Typography textAlign='center'>Akun belum aktif. Silahkan hubungi admin untuk aktivasi</Typography> | ||
)} | ||
</> | ||
) : ( | ||
<> | ||
{!isLoginState && ( | ||
<TextField | ||
error={!!error["name"]} | ||
helperText={error["name"] ? "Nama Tidak Boleh Kosong" : ""} | ||
sx={{ width: '80%' }} | ||
label="Nama" | ||
type="text" | ||
value={name} | ||
onChange={e => setName(e.target.value)} /> | ||
)} | ||
<TextField | ||
error={!!error["email"]} | ||
helperText={error["email"] ? "Email Tidak Boleh Kosong" : ""} | ||
sx={{ width: '80%' }} | ||
label="Email" | ||
type="email" | ||
value={email} | ||
onChange={e => setEmail(e.target.value)} /> | ||
<TextField | ||
error={!!error["password"]} | ||
helperText={error["password"] ? "Password Tidak Boleh Kosong" : ""} | ||
sx={{ width: '80%' }} | ||
label="Password" | ||
type="password" | ||
value={password} | ||
onChange={e => setPassword(e.target.value)} /> | ||
<Button size='lg' sx={{ width: '80%' }} onClick={handleAuthSubmit} loading={authMutation.isPending}>{!isLoginState ? "Daftar" : "Masuk"}</Button> | ||
<Typography> | ||
{isLoginState ? "Belum" : "Sudah"} Punya Akun? <Link fontWeight='bold' onClick={handleChangeAuthMode}>{isLoginState ? "Daftar" : "Masuk"}</Link> | ||
</Typography> | ||
</> | ||
)} | ||
</Grid> | ||
) | ||
} | ||
|
||
export const AuthRoute = createLazyRoute('/auth')({ | ||
component: Auth | ||
}) |
Oops, something went wrong.