Skip to content

Commit

Permalink
feat(auth): init auth feature and global router
Browse files Browse the repository at this point in the history
  • Loading branch information
Ibrahimsyah committed Mar 31, 2024
1 parent e8b0eac commit 13c8d07
Show file tree
Hide file tree
Showing 15 changed files with 741 additions and 407 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@fontsource/inter": "^5.0.16",
"@loadable/component": "^5.16.3",
"@mui/icons-material": "^5.15.6",
"@mui/joy": "^5.0.0-beta.24",
"@mui/material": "^5.15.6",
"@mui/x-charts": "^6.19.5",
"@mui/x-date-pickers": "^6.19.3",
"@tanstack/react-query": "^5.17.19",
"@tanstack/react-query-devtools": "^5.18.0",
"@tanstack/react-router": "^1.26.1",
"dayjs": "^1.11.10",
"firebase": "^10.7.2",
"react": "^18.2.0",
Expand Down
15 changes: 15 additions & 0 deletions src/apis/auth.ts
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"
}
9 changes: 9 additions & 0 deletions src/apis/auth.types.ts
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
}
2 changes: 1 addition & 1 deletion src/pages/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
max-width: 500px;
min-height: 100vh;
margin: 0 auto;
display: flex;
/* display: flex; */
flex-direction: column;
background-color: white;
box-shadow: 0 0 48px 0 rgba(0,0,0,.2);
Expand Down
183 changes: 71 additions & 112 deletions src/pages/App.tsx
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
130 changes: 130 additions & 0 deletions src/pages/auth/index.tsx
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
})
Loading

0 comments on commit 13c8d07

Please sign in to comment.