committed by
GitHub
40 changed files with 7075 additions and 2766 deletions
@ -0,0 +1 @@ |
|||
VITE_API_URL=http://localhost |
File diff suppressed because it is too large
@ -0,0 +1,22 @@ |
|||
import { Button, Container, Text } from '@chakra-ui/react'; |
|||
import { Link } from '@tanstack/react-router'; |
|||
|
|||
const NotFound: React.FC = () => { |
|||
|
|||
return ( |
|||
<> |
|||
<Container h='100vh' |
|||
alignItems='stretch' |
|||
justifyContent='center' textAlign='center' maxW='sm' centerContent> |
|||
<Text fontSize='8xl' color='ui.main' fontWeight='bold' lineHeight='1' mb={4}>404</Text> |
|||
<Text fontSize='md'>Oops!</Text> |
|||
<Text fontSize='md'>Page not found.</Text> |
|||
<Button as={Link} to='/' color='ui.main' borderColor='ui.main' variant='outline' mt={4}>Go back</Button> |
|||
</Container> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
export default NotFound; |
|||
|
|||
|
@ -0,0 +1,33 @@ |
|||
import { useQuery } from 'react-query'; |
|||
import { useNavigate } from '@tanstack/react-router'; |
|||
|
|||
import { Body_login_login_access_token as AccessToken, LoginService, UserOut, UsersService } from '../client'; |
|||
|
|||
const isLoggedIn = () => { |
|||
return localStorage.getItem('access_token') !== null; |
|||
}; |
|||
|
|||
const useAuth = () => { |
|||
const navigate = useNavigate(); |
|||
const { data: user, isLoading } = useQuery<UserOut | null, Error>('currentUser', UsersService.readUserMe, { |
|||
enabled: isLoggedIn(), |
|||
}); |
|||
|
|||
const login = async (data: AccessToken) => { |
|||
const response = await LoginService.loginAccessToken({ |
|||
formData: data, |
|||
}); |
|||
localStorage.setItem('access_token', response.access_token); |
|||
navigate({ to: '/' }); |
|||
}; |
|||
|
|||
const logout = () => { |
|||
localStorage.removeItem('access_token'); |
|||
navigate({ to: '/login' }); |
|||
}; |
|||
|
|||
return { login, logout, user, isLoading }; |
|||
} |
|||
|
|||
export { isLoggedIn }; |
|||
export default useAuth; |
@ -1,38 +0,0 @@ |
|||
import { useUserStore } from '../store/user-store'; |
|||
import { Body_login_login_access_token as AccessToken, LoginService } from '../client'; |
|||
import { useUsersStore } from '../store/users-store'; |
|||
import { useItemsStore } from '../store/items-store'; |
|||
import { useNavigate } from 'react-router-dom'; |
|||
|
|||
const isLoggedIn = () => { |
|||
return localStorage.getItem('access_token') !== null; |
|||
}; |
|||
|
|||
const useAuth = () => { |
|||
const { getUser, resetUser } = useUserStore(); |
|||
const { resetUsers } = useUsersStore(); |
|||
const { resetItems } = useItemsStore(); |
|||
const navigate = useNavigate(); |
|||
|
|||
const login = async (data: AccessToken) => { |
|||
const response = await LoginService.loginAccessToken({ |
|||
formData: data, |
|||
}); |
|||
localStorage.setItem('access_token', response.access_token); |
|||
await getUser(); |
|||
navigate('/'); |
|||
}; |
|||
|
|||
const logout = () => { |
|||
localStorage.removeItem('access_token'); |
|||
resetUser(); |
|||
resetUsers(); |
|||
resetItems(); |
|||
navigate('/login'); |
|||
}; |
|||
|
|||
return { login, logout }; |
|||
} |
|||
|
|||
export { isLoggedIn }; |
|||
export default useAuth; |
@ -1,35 +1,33 @@ |
|||
import React from 'react'; |
|||
import ReactDOM from 'react-dom/client'; |
|||
|
|||
import { ChakraProvider } from '@chakra-ui/provider'; |
|||
import { createStandaloneToast } from '@chakra-ui/toast'; |
|||
import { RouterProvider, createBrowserRouter } from 'react-router-dom'; |
|||
import { ChakraProvider } from '@chakra-ui/react'; |
|||
import { QueryClient, QueryClientProvider } from 'react-query'; |
|||
import { RouterProvider, createRouter } from '@tanstack/react-router' |
|||
import { routeTree } from './routeTree.gen' |
|||
|
|||
import { OpenAPI } from './client'; |
|||
import { isLoggedIn } from './hooks/useAuth'; |
|||
import privateRoutes from './routes/private_route'; |
|||
import publicRoutes from './routes/public_route'; |
|||
import theme from './theme'; |
|||
|
|||
import { StrictMode } from 'react'; |
|||
|
|||
OpenAPI.BASE = import.meta.env.VITE_API_URL; |
|||
OpenAPI.TOKEN = async () => { |
|||
return localStorage.getItem('access_token') || ''; |
|||
} |
|||
|
|||
const router = createBrowserRouter([ |
|||
isLoggedIn() ? privateRoutes() : {}, |
|||
...publicRoutes(), |
|||
]); |
|||
const queryClient = new QueryClient(); |
|||
|
|||
const { ToastContainer } = createStandaloneToast(); |
|||
const router = createRouter({ routeTree }) |
|||
declare module '@tanstack/react-router' { |
|||
interface Register { |
|||
router: typeof router |
|||
} |
|||
} |
|||
|
|||
ReactDOM.createRoot(document.getElementById('root')!).render( |
|||
<React.StrictMode> |
|||
<StrictMode> |
|||
<ChakraProvider theme={theme}> |
|||
<RouterProvider router={router} /> |
|||
<ToastContainer /> |
|||
<QueryClientProvider client={queryClient}> |
|||
<RouterProvider router={router} /> |
|||
</QueryClientProvider> |
|||
</ChakraProvider> |
|||
</React.StrictMode>, |
|||
) |
|||
|
|||
</StrictMode> |
|||
); |
@ -1,22 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import { Container, Text } from '@chakra-ui/react'; |
|||
|
|||
import { useUserStore } from '../store/user-store'; |
|||
|
|||
|
|||
const Dashboard: React.FC = () => { |
|||
const { user } = useUserStore(); |
|||
|
|||
return ( |
|||
<> |
|||
<Container maxW='full' pt={12}> |
|||
<Text fontSize='2xl'>Hi, {user?.full_name || user?.email} 👋🏼</Text> |
|||
<Text>Welcome back, nice to see you again!</Text> |
|||
</Container> |
|||
</> |
|||
|
|||
) |
|||
} |
|||
|
|||
export default Dashboard; |
@ -1,25 +0,0 @@ |
|||
import { Button, Container, Text } from '@chakra-ui/react'; |
|||
import { Link, useRouteError } from 'react-router-dom'; |
|||
|
|||
const ErrorPage: React.FC = () => { |
|||
const error = useRouteError(); |
|||
console.log(error); |
|||
|
|||
return ( |
|||
<> |
|||
<Container h='100vh' |
|||
alignItems='stretch' |
|||
justifyContent='center' textAlign='center' maxW='xs' centerContent> |
|||
<Text fontSize='8xl' color='ui.main' fontWeight='bold' lineHeight='1' mb={4}>Oops!</Text> |
|||
<Text fontSize='md'>Houston, we have a problem.</Text> |
|||
<Text fontSize='md'>An unexpected error has occurred.</Text> |
|||
{/* <Text color='ui.danger'><i>{error.statusText || error.message}</i></Text> */} |
|||
<Button as={Link} to='/' color='ui.main' borderColor='ui.main' variant='outline' mt={4}>Go back to Home</Button> |
|||
</Container> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
export default ErrorPage; |
|||
|
|||
|
@ -1,32 +0,0 @@ |
|||
import React, { useEffect } from 'react'; |
|||
|
|||
import { Flex } from '@chakra-ui/react'; |
|||
import { Outlet } from 'react-router-dom'; |
|||
|
|||
import Sidebar from '../components/Common/Sidebar'; |
|||
import UserMenu from '../components/Common/UserMenu'; |
|||
import { useUserStore } from '../store/user-store'; |
|||
import { isLoggedIn } from '../hooks/useAuth'; |
|||
|
|||
const Layout: React.FC = () => { |
|||
const { getUser } = useUserStore(); |
|||
|
|||
useEffect(() => { |
|||
const fetchUser = async () => { |
|||
if (isLoggedIn()) { |
|||
await getUser(); |
|||
} |
|||
}; |
|||
fetchUser(); |
|||
}, []); |
|||
|
|||
return ( |
|||
<Flex maxW='large' h='auto' position='relative'> |
|||
<Sidebar /> |
|||
<Outlet /> |
|||
<UserMenu /> |
|||
</Flex> |
|||
); |
|||
}; |
|||
|
|||
export default Layout; |
@ -0,0 +1,118 @@ |
|||
/* prettier-ignore-start */ |
|||
|
|||
/* eslint-disable */ |
|||
|
|||
// @ts-nocheck
|
|||
|
|||
// noinspection JSUnusedGlobalSymbols
|
|||
|
|||
// This file is auto-generated by TanStack Router
|
|||
|
|||
// Import Routes
|
|||
|
|||
import { Route as rootRoute } from './routes/__root' |
|||
import { Route as ResetPasswordImport } from './routes/reset-password' |
|||
import { Route as RecoverPasswordImport } from './routes/recover-password' |
|||
import { Route as LoginImport } from './routes/login' |
|||
import { Route as LayoutImport } from './routes/_layout' |
|||
import { Route as LayoutIndexImport } from './routes/_layout/index' |
|||
import { Route as LayoutSettingsImport } from './routes/_layout/settings' |
|||
import { Route as LayoutItemsImport } from './routes/_layout/items' |
|||
import { Route as LayoutAdminImport } from './routes/_layout/admin' |
|||
|
|||
// Create/Update Routes
|
|||
|
|||
const ResetPasswordRoute = ResetPasswordImport.update({ |
|||
path: '/reset-password', |
|||
getParentRoute: () => rootRoute, |
|||
} as any) |
|||
|
|||
const RecoverPasswordRoute = RecoverPasswordImport.update({ |
|||
path: '/recover-password', |
|||
getParentRoute: () => rootRoute, |
|||
} as any) |
|||
|
|||
const LoginRoute = LoginImport.update({ |
|||
path: '/login', |
|||
getParentRoute: () => rootRoute, |
|||
} as any) |
|||
|
|||
const LayoutRoute = LayoutImport.update({ |
|||
id: '/_layout', |
|||
getParentRoute: () => rootRoute, |
|||
} as any) |
|||
|
|||
const LayoutIndexRoute = LayoutIndexImport.update({ |
|||
path: '/', |
|||
getParentRoute: () => LayoutRoute, |
|||
} as any) |
|||
|
|||
const LayoutSettingsRoute = LayoutSettingsImport.update({ |
|||
path: '/settings', |
|||
getParentRoute: () => LayoutRoute, |
|||
} as any) |
|||
|
|||
const LayoutItemsRoute = LayoutItemsImport.update({ |
|||
path: '/items', |
|||
getParentRoute: () => LayoutRoute, |
|||
} as any) |
|||
|
|||
const LayoutAdminRoute = LayoutAdminImport.update({ |
|||
path: '/admin', |
|||
getParentRoute: () => LayoutRoute, |
|||
} as any) |
|||
|
|||
// Populate the FileRoutesByPath interface
|
|||
|
|||
declare module '@tanstack/react-router' { |
|||
interface FileRoutesByPath { |
|||
'/_layout': { |
|||
preLoaderRoute: typeof LayoutImport |
|||
parentRoute: typeof rootRoute |
|||
} |
|||
'/login': { |
|||
preLoaderRoute: typeof LoginImport |
|||
parentRoute: typeof rootRoute |
|||
} |
|||
'/recover-password': { |
|||
preLoaderRoute: typeof RecoverPasswordImport |
|||
parentRoute: typeof rootRoute |
|||
} |
|||
'/reset-password': { |
|||
preLoaderRoute: typeof ResetPasswordImport |
|||
parentRoute: typeof rootRoute |
|||
} |
|||
'/_layout/admin': { |
|||
preLoaderRoute: typeof LayoutAdminImport |
|||
parentRoute: typeof LayoutImport |
|||
} |
|||
'/_layout/items': { |
|||
preLoaderRoute: typeof LayoutItemsImport |
|||
parentRoute: typeof LayoutImport |
|||
} |
|||
'/_layout/settings': { |
|||
preLoaderRoute: typeof LayoutSettingsImport |
|||
parentRoute: typeof LayoutImport |
|||
} |
|||
'/_layout/': { |
|||
preLoaderRoute: typeof LayoutIndexImport |
|||
parentRoute: typeof LayoutImport |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Create and export the route tree
|
|||
|
|||
export const routeTree = rootRoute.addChildren([ |
|||
LayoutRoute.addChildren([ |
|||
LayoutAdminRoute, |
|||
LayoutItemsRoute, |
|||
LayoutSettingsRoute, |
|||
LayoutIndexRoute, |
|||
]), |
|||
LoginRoute, |
|||
RecoverPasswordRoute, |
|||
ResetPasswordRoute, |
|||
]) |
|||
|
|||
/* prettier-ignore-end */ |
@ -0,0 +1,13 @@ |
|||
import { createRootRoute, Outlet } from '@tanstack/react-router' |
|||
import { TanStackRouterDevtools } from '@tanstack/router-devtools' |
|||
import NotFound from '../components/Common/NotFound' |
|||
|
|||
export const Route = createRootRoute({ |
|||
component: () => ( |
|||
<> |
|||
<Outlet /> |
|||
<TanStackRouterDevtools /> |
|||
</> |
|||
), |
|||
notFoundComponent: () => <NotFound />, |
|||
}) |
@ -0,0 +1,38 @@ |
|||
import { Flex, Spinner } from '@chakra-ui/react'; |
|||
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'; |
|||
|
|||
import Sidebar from '../components/Common/Sidebar'; |
|||
import UserMenu from '../components/Common/UserMenu'; |
|||
import useAuth, { isLoggedIn } from '../hooks/useAuth'; |
|||
|
|||
|
|||
export const Route = createFileRoute('/_layout')({ |
|||
component: Layout, |
|||
beforeLoad: async () => { |
|||
if (!isLoggedIn()) { |
|||
throw redirect({ |
|||
to: '/login', |
|||
}) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
function Layout() { |
|||
const { isLoading } = useAuth(); |
|||
|
|||
return ( |
|||
<Flex maxW='large' h='auto' position='relative'> |
|||
<Sidebar /> |
|||
{isLoading ? ( |
|||
<Flex justify='center' align='center' height='100vh' width='full'> |
|||
<Spinner size='xl' color='ui.main' /> |
|||
</Flex> |
|||
) : ( |
|||
<Outlet /> |
|||
)} |
|||
<UserMenu /> |
|||
</Flex> |
|||
); |
|||
}; |
|||
|
|||
export default Layout; |
@ -0,0 +1,27 @@ |
|||
|
|||
import { Container, Text } from '@chakra-ui/react'; |
|||
import { useQueryClient } from 'react-query'; |
|||
import { createFileRoute } from '@tanstack/react-router'; |
|||
|
|||
import { UserOut } from '../../client'; |
|||
|
|||
export const Route = createFileRoute('/_layout/')({ |
|||
component: Dashboard, |
|||
}) |
|||
|
|||
function Dashboard() { |
|||
const queryClient = useQueryClient(); |
|||
|
|||
const currentUser = queryClient.getQueryData<UserOut>('currentUser'); |
|||
|
|||
return ( |
|||
<> |
|||
<Container maxW='full' pt={12}> |
|||
<Text fontSize='2xl'>Hi, {currentUser?.full_name || currentUser?.email} 👋🏼</Text> |
|||
<Text>Welcome back, nice to see you again!</Text> |
|||
</Container> |
|||
</> |
|||
) |
|||
} |
|||
|
|||
export default Dashboard; |
@ -1,21 +0,0 @@ |
|||
import Admin from '../pages/Admin'; |
|||
import Dashboard from '../pages/Dashboard'; |
|||
import ErrorPage from '../pages/ErrorPage'; |
|||
import Items from '../pages/Items'; |
|||
import Layout from '../pages/Layout'; |
|||
import UserSettings from '../pages/UserSettings'; |
|||
|
|||
export default function privateRoutes() { |
|||
|
|||
return { |
|||
path: '/', |
|||
element: <Layout />, |
|||
errorElement: <ErrorPage />, |
|||
children: [ |
|||
{ path: '/', element: <Dashboard /> }, |
|||
{ path: 'items', element: <Items /> }, |
|||
{ path: 'admin', element: <Admin /> }, |
|||
{ path: 'settings', element: <UserSettings /> }, |
|||
], |
|||
}; |
|||
} |
@ -1,15 +0,0 @@ |
|||
import ErrorPage from '../pages/ErrorPage'; |
|||
import Login from '../pages/Login'; |
|||
import RecoverPassword from '../pages/RecoverPassword'; |
|||
import ResetPassword from '../pages/ResetPassword'; |
|||
|
|||
export default function publicRoutes() { |
|||
return [ |
|||
{ path: '/login', element: <Login />, errorElement: <ErrorPage /> }, |
|||
{ path: 'recover-password', element: <RecoverPassword />, errorElement: <ErrorPage /> }, |
|||
{ path: 'reset-password', element: <ResetPassword />, errorElement: <ErrorPage /> }, |
|||
// TODO: complete this
|
|||
// { path: '*', element: <Navigate to='/login' replace /> }
|
|||
]; |
|||
} |
|||
|
@ -1,36 +0,0 @@ |
|||
import { create } from 'zustand'; |
|||
import { ItemCreate, ItemOut, ItemUpdate, ItemsService } from '../client'; |
|||
|
|||
interface ItemsStore { |
|||
items: ItemOut[]; |
|||
getItems: () => Promise<void>; |
|||
addItem: (item: ItemCreate) => Promise<void>; |
|||
editItem: (id: number, item: ItemUpdate) => Promise<void>; |
|||
deleteItem: (id: number) => Promise<void>; |
|||
resetItems: () => void; |
|||
} |
|||
|
|||
export const useItemsStore = create<ItemsStore>((set) => ({ |
|||
items: [], |
|||
getItems: async () => { |
|||
const itemsResponse = await ItemsService.readItems({ skip: 0, limit: 10 }); |
|||
set({ items: itemsResponse.data }); |
|||
}, |
|||
addItem: async (item: ItemCreate) => { |
|||
const itemResponse = await ItemsService.createItem({ requestBody: item }); |
|||
set((state) => ({ items: [...state.items, itemResponse] })); |
|||
}, |
|||
editItem: async (id: number, item: ItemUpdate) => { |
|||
const itemResponse = await ItemsService.updateItem({ id: id, requestBody: item }); |
|||
set((state) => ({ |
|||
items: state.items.map((item) => (item.id === id ? itemResponse : item)) |
|||
})); |
|||
}, |
|||
deleteItem: async (id: number) => { |
|||
await ItemsService.deleteItem({ id }); |
|||
set((state) => ({ items: state.items.filter((item) => item.id !== id) })); |
|||
}, |
|||
resetItems: () => { |
|||
set({ items: [] }); |
|||
} |
|||
})); |
@ -1,28 +0,0 @@ |
|||
import { create } from 'zustand'; |
|||
import { UpdatePassword, UserOut, UserUpdateMe, UsersService } from '../client'; |
|||
|
|||
interface UserStore { |
|||
user: UserOut | null; |
|||
getUser: () => Promise<void>; |
|||
editUser: (user: UserUpdateMe) => Promise<void>; |
|||
editPassword: (password: UpdatePassword) => Promise<void>; |
|||
resetUser: () => void; |
|||
} |
|||
|
|||
export const useUserStore = create<UserStore>((set) => ({ |
|||
user: null, |
|||
getUser: async () => { |
|||
const user = await UsersService.readUserMe(); |
|||
set({ user }); |
|||
}, |
|||
editUser: async (user: UserUpdateMe) => { |
|||
const updatedUser = await UsersService.updateUserMe({ requestBody: user }); |
|||
set((state) => ({ user: { ...state.user, ...updatedUser } })); |
|||
}, |
|||
editPassword: async (password: UpdatePassword) => { |
|||
await UsersService.updatePasswordMe({ requestBody: password }); |
|||
}, |
|||
resetUser: () => { |
|||
set({ user: null }); |
|||
} |
|||
})); |
@ -1,36 +0,0 @@ |
|||
import { create } from "zustand"; |
|||
import { UserCreate, UserOut, UserUpdate, UsersService } from "../client"; |
|||
|
|||
interface UsersStore { |
|||
users: UserOut[]; |
|||
getUsers: () => Promise<void>; |
|||
addUser: (user: UserCreate) => Promise<void>; |
|||
editUser: (id: number, user: UserUpdate) => Promise<void>; |
|||
deleteUser: (id: number) => Promise<void>; |
|||
resetUsers: () => void; |
|||
} |
|||
|
|||
export const useUsersStore = create<UsersStore>((set) => ({ |
|||
users: [], |
|||
getUsers: async () => { |
|||
const usersResponse = await UsersService.readUsers({ skip: 0, limit: 10 }); |
|||
set({ users: usersResponse.data }); |
|||
}, |
|||
addUser: async (user: UserCreate) => { |
|||
const userResponse = await UsersService.createUser({ requestBody: user }); |
|||
set((state) => ({ users: [...state.users, userResponse] })); |
|||
}, |
|||
editUser: async (id: number, user: UserUpdate) => { |
|||
const userResponse = await UsersService.updateUser({ userId: id, requestBody: user }); |
|||
set((state) => ({ |
|||
users: state.users.map((user) => (user.id === id ? userResponse : user)) |
|||
})); |
|||
}, |
|||
deleteUser: async (id: number) => { |
|||
await UsersService.deleteUser({ userId: id }); |
|||
set((state) => ({ users: state.users.filter((user) => user.id !== id) })); |
|||
}, |
|||
resetUsers: () => { |
|||
set({ users: [] }); |
|||
} |
|||
})) |
@ -1,7 +1,8 @@ |
|||
import { defineConfig } from 'vite' |
|||
import react from '@vitejs/plugin-react-swc' |
|||
import { TanStackRouterVite } from '@tanstack/router-vite-plugin' |
|||
import { defineConfig } from 'vite' |
|||
|
|||
// https://vitejs.dev/config/
|
|||
export default defineConfig({ |
|||
plugins: [react()], |
|||
plugins: [react(), TanStackRouterVite()], |
|||
}) |
|||
|
Loading…
Reference in new issue