diff --git a/src/new-frontend/src/App.tsx b/src/new-frontend/src/App.tsx index 958baf13ca..6bbd728860 100644 --- a/src/new-frontend/src/App.tsx +++ b/src/new-frontend/src/App.tsx @@ -1,19 +1,57 @@ -import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { Route, BrowserRouter as Router, Routes } from 'react-router-dom'; import Layout from './pages/Layout'; import Login from './pages/auth/Login'; import RecoverPassword from './pages/auth/RecoverPassword'; +import Admin from './pages/main/Admin'; +import Dashboard from './pages/main/Dashboard'; +import Items from './pages/main/Items'; +import Profile from './pages/main/Profile'; +import { ChakraProvider, extendTheme } from '@chakra-ui/react'; + +// Theme +const theme = extendTheme({ + colors: { + ui: { + main: "#009688", + secondary: "#EDF2F7", + success: '#48BB78', + danger: '#E53E3E', + } + }, + components: { + Tabs: { + variants: { + enclosed: { + tab: { + _selected: { + color: 'ui.main', + }, + }, + }, + }, + }, + }, +}); function App() { return ( - - - } /> - } /> - }> - - - + <> + + + + } /> + } /> + }> + } /> + } /> + } /> + } /> + + + + + ) } diff --git a/src/new-frontend/src/components/ActionsMenu.tsx b/src/new-frontend/src/components/ActionsMenu.tsx new file mode 100644 index 0000000000..1d14c47b30 --- /dev/null +++ b/src/new-frontend/src/components/ActionsMenu.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { Button, Menu, MenuButton, MenuItem, MenuList, useDisclosure } from '@chakra-ui/react'; +import { BsThreeDotsVertical } from 'react-icons/bs'; +import { FiTrash, FiEdit } from 'react-icons/fi'; + +import Delete from '../pages/modals/DeleteAlert'; +import EditUser from '../pages/modals/EditUser'; +import EditItem from '../pages/modals/EditItem'; + +interface ActionsMenuProps { + type: string; + id: number; +} + +const ActionsMenu: React.FC = ({ type, id }) => { + const editUserModal = useDisclosure(); + const deleteModal = useDisclosure(); + + return ( + <> + + } variant="unstyled"> + + + }>Edit {type} + } color="ui.danger">Delete {type} + + { + type === "User" ? + : + } + + + + ); +}; + +export default ActionsMenu; diff --git a/src/new-frontend/src/components/Navbar.tsx b/src/new-frontend/src/components/Navbar.tsx new file mode 100644 index 0000000000..1e347a9387 --- /dev/null +++ b/src/new-frontend/src/components/Navbar.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import { Button, Flex, Icon, useDisclosure } from '@chakra-ui/react'; +import { FaPlus } from "react-icons/fa"; + +import CreateItem from '../pages/modals/CreateItem'; +import CreateUser from '../pages/modals/CreateUser'; + +interface NavbarProps { + type: string; +} + +const Navbar: React.FC = ({ type }) => { + const createUserModal = useDisclosure(); + const createItemModal = useDisclosure(); + + return ( + <> + + + + + + + ); +}; + +export default Navbar; diff --git a/src/new-frontend/src/components/Sidebar.tsx b/src/new-frontend/src/components/Sidebar.tsx index a67623076f..d483a8c8e8 100644 --- a/src/new-frontend/src/components/Sidebar.tsx +++ b/src/new-frontend/src/components/Sidebar.tsx @@ -17,7 +17,7 @@ const Sidebar: React.FC = () => { } /> - + @@ -33,7 +33,7 @@ const Sidebar: React.FC = () => { {/* Desktop */} - + Logo diff --git a/src/new-frontend/src/components/SidebarItems.tsx b/src/new-frontend/src/components/SidebarItems.tsx index 7790111f80..e0f9281369 100644 --- a/src/new-frontend/src/components/SidebarItems.tsx +++ b/src/new-frontend/src/components/SidebarItems.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { Flex, Icon, Text } from '@chakra-ui/react'; -import { FiBriefcase, FiHome, FiLogOut, FiUser, FiUsers } from 'react-icons/fi'; -import { Link } from 'react-router-dom'; +import { FiBriefcase, FiHome, FiLogOut, FiSettings, FiUsers } from 'react-icons/fi'; +import { Link, useNavigate } from 'react-router-dom'; + const items = [ { icon: FiHome, title: 'Dashboard', path: "/" }, - { icon: FiUser, title: 'Profile', path: "/profile" }, { icon: FiBriefcase, title: 'Items', path: "/items" }, { icon: FiUsers, title: 'Admin', path: "/admin" }, + { icon: FiSettings, title: 'User Settings', path: "/settings" }, { icon: FiLogOut, title: 'Log out' } ]; @@ -17,15 +18,24 @@ interface SidebarItemsProps { } const SidebarItems: React.FC = ({ onClose }) => { + const navigate = useNavigate(); + + const handleLogout = async () => { + localStorage.removeItem("access_token"); + navigate("/login"); + // TODO: reset all Zustand states + }; + const listItems = items.map((item) => ( + }} onClick={item.title === 'Log out' ? handleLogout : onClose}> - - + + {item.title} + diff --git a/src/new-frontend/src/components/UserInfo.tsx b/src/new-frontend/src/components/UserInfo.tsx index 3cf059b3f2..ddea6d002c 100644 --- a/src/new-frontend/src/components/UserInfo.tsx +++ b/src/new-frontend/src/components/UserInfo.tsx @@ -1,33 +1,22 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Avatar, Flex, Skeleton, Text } from '@chakra-ui/react'; import { FaUserAstronaut } from 'react-icons/fa'; -import { UserOut, UsersService } from '../client'; +import { useUserStore } from '../store/user-store'; -const UserInfo: React.FC = () => { - const [userData, setUserData] = useState(); - useEffect(() => { - const fetchUserData = async () => { - try { - const userResponse = await UsersService.readUserMe(); - setUserData(userResponse); - } catch (error) { - // TODO: Handle error to give feedback to the user - console.error(error); - } - }; - fetchUserData(); - }, []); +const UserInfo: React.FC = () => { + const { user } = useUserStore(); + return ( <> - {userData ? ( + {user ? ( - } size='sm' alignSelf="center" /> + } size='sm' alignSelf="center" /> {/* TODO: Conditional tooltip based on email length */} - {userData.email} + {user.email} ) : diff --git a/src/new-frontend/src/pages/auth/Login.tsx b/src/new-frontend/src/pages/auth/Login.tsx index 112ae1cea8..53e01ed224 100644 --- a/src/new-frontend/src/pages/auth/Login.tsx +++ b/src/new-frontend/src/pages/auth/Login.tsx @@ -10,10 +10,9 @@ import { LoginService } from "../../client"; import { Body_login_login_access_token as AccessToken } from "../../client/models/Body_login_login_access_token"; const Login: React.FC = () => { - const [show, setShow] = useBoolean(); +const [show, setShow] = useBoolean(); const navigate = useNavigate(); const { register, handleSubmit } = useForm(); - const onSubmit: SubmitHandler = async (data) => { const response = await LoginService.loginAccessToken({ formData: data, @@ -23,49 +22,51 @@ const Login: React.FC = () => { }; return ( - - FastAPI logo - - - - - - - - - {show ? : } - - - -
- - Forgot password? - -
-
- -
+ <> + + FastAPI logo + + + + + + + + + {show ? : } + + + +
+ + Forgot password? + +
+
+ +
+ ); }; diff --git a/src/new-frontend/src/pages/main/Admin.tsx b/src/new-frontend/src/pages/main/Admin.tsx new file mode 100644 index 0000000000..70fd5eb88c --- /dev/null +++ b/src/new-frontend/src/pages/main/Admin.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useState } from 'react'; + +import { Box, Container, Flex, Heading, Spinner, Table, TableContainer, Tbody, Td, Th, Thead, Tr, useToast } from '@chakra-ui/react'; + +import ActionsMenu from '../../components/ActionsMenu'; +import Navbar from '../../components/Navbar'; +import { useUsersStore } from '../../store/users-store'; + +const Admin: React.FC = () => { + const toast = useToast(); + const [isLoading, setIsLoading] = useState(false); + const { users, getUsers } = useUsersStore(); + + useEffect(() => { + const fetchUsers = async () => { + try { + setIsLoading(true); + await getUsers(); + setIsLoading(false); + } catch (err) { + setIsLoading(false); + toast({ + title: 'Something went wrong.', + description: 'Failed to fetch users. Please try again.', + status: 'error', + isClosable: true, + }); + } + } + fetchUsers(); + }, []) + + return ( + <> + {isLoading ? ( + // TODO: Add skeleton + + + + ) : ( + users && + + + User Management + + + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
Full nameEmailRoleStatus
{user.full_name || "N/A"}{user.email}{user.is_superuser ? "Superuser" : "User"} + + + {user.is_active ? "Active" : "Inactive"} + + + +
+
+
+ )} + + ) +} + +export default Admin; diff --git a/src/new-frontend/src/pages/main/Dashboard.tsx b/src/new-frontend/src/pages/main/Dashboard.tsx new file mode 100644 index 0000000000..3c616a7038 --- /dev/null +++ b/src/new-frontend/src/pages/main/Dashboard.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { Box, Text } from '@chakra-ui/react'; + +import { useUserStore } from '../../store/user-store'; + + +const Dashboard: React.FC = () => { + const { user } = useUserStore(); + + return ( + <> + {user ? ( + + Hi, {user.full_name || user.email} 👋🏼 + Welcome back, nice to see you again! + + ) : null} + + + ) +} + +export default Dashboard; \ No newline at end of file diff --git a/src/new-frontend/src/pages/main/Items.tsx b/src/new-frontend/src/pages/main/Items.tsx new file mode 100644 index 0000000000..7e2a2fb314 --- /dev/null +++ b/src/new-frontend/src/pages/main/Items.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useState } from 'react'; + +import { Container, Flex, Heading, Spinner, Table, TableContainer, Tbody, Td, Th, Thead, Tr, useToast } from '@chakra-ui/react'; + +import ActionsMenu from '../../components/ActionsMenu'; +import Navbar from '../../components/Navbar'; +import { useItemsStore } from '../../store/items-store'; + + +const Items: React.FC = () => { + const toast = useToast(); + const [isLoading, setIsLoading] = useState(false); + const { items, getItems } = useItemsStore(); + + useEffect(() => { + const fetchItems = async () => { + try { + setIsLoading(true); + await getItems(); + setIsLoading(false); + } catch (err) { + setIsLoading(false); + toast({ + title: 'Something went wrong.', + description: 'Failed to fetch items. Please try again.', + status: 'error', + isClosable: true, + }); + } + } + fetchItems(); + }, []) + + + return ( + <> + {isLoading ? ( + // TODO: Add skeleton + + + + ) : ( + items && + + + Items Management + + + + + + + + + + + + + {items.map((item) => ( + + + + + + + ))} + +
IDTitleDescription
{item.id}{item.title}{item.description || "N/A"} + +
+
+
+ )} + + ) +} + +export default Items; \ No newline at end of file diff --git a/src/new-frontend/src/pages/main/Profile.tsx b/src/new-frontend/src/pages/main/Profile.tsx new file mode 100644 index 0000000000..4ef140f868 --- /dev/null +++ b/src/new-frontend/src/pages/main/Profile.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import { Container, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; + +import Appearance from '../panels/Appearance'; +import ChangePassword from '../panels/ChangePassword'; +import DeleteAccount from '../panels/DeleteAccount'; +import UserInformation from '../panels/UserInformation'; + + + +const Profile: React.FC = () => { + + return ( + <> + + + User Settings + + + + Profile + Password + Appearance + Danger zone + + + + + + + + + + + + + + + + + + + + ); +}; + +export default Profile; + diff --git a/src/new-frontend/src/pages/modals/CreateItem.tsx b/src/new-frontend/src/pages/modals/CreateItem.tsx new file mode 100644 index 0000000000..d3574353a0 --- /dev/null +++ b/src/new-frontend/src/pages/modals/CreateItem.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; + +import { Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useToast } from '@chakra-ui/react'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +import { ItemCreate } from '../../client'; +import { useItemsStore } from '../../store/items-store'; + +interface CreateItemProps { + isOpen: boolean; + onClose: () => void; +} + +const CreateItem: React.FC = ({ isOpen, onClose }) => { + const toast = useToast(); + const [isLoading, setIsLoading] = useState(false); + const { register, handleSubmit } = useForm(); + const { addItem } = useItemsStore(); + + const onSubmit: SubmitHandler = async (data) => { + try { + setIsLoading(true); + await addItem(data); + setIsLoading(false); + + toast({ + title: 'Success!', + description: 'Item created successfully.', + status: 'success', + isClosable: true, + }); + onClose(); + } catch (err) { + setIsLoading(false); + toast({ + title: 'Something went wrong.', + description: 'Failed to create item. Please try again.', + status: 'error', + isClosable: true, + }); + } + }; + + return ( + <> + + + + Create Item + + + + Title + + + + Description + + + + + + + + + + + + ); +}; + +export default CreateItem; diff --git a/src/new-frontend/src/pages/modals/CreateUser.tsx b/src/new-frontend/src/pages/modals/CreateUser.tsx new file mode 100644 index 0000000000..951f683438 --- /dev/null +++ b/src/new-frontend/src/pages/modals/CreateUser.tsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; + +import { Box, Button, Checkbox, Flex, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Spinner, useToast } from '@chakra-ui/react'; +import { SubmitHandler, useForm } from 'react-hook-form'; + +import { UserCreate } from '../../client'; +import { useUsersStore } from '../../store/users-store'; + +interface CreateUserProps { + isOpen: boolean; + onClose: () => void; +} + +const CreateUser: React.FC = ({ isOpen, onClose }) => { + const toast = useToast(); + const [isLoading, setIsLoading] = useState(false); + const { register, handleSubmit } = useForm(); + const { addUser } = useUsersStore(); + + const onSubmit: SubmitHandler = async (data) => { + try { + setIsLoading(true); + await addUser(data); + setIsLoading(false); + toast({ + title: 'Success!', + description: 'User created successfully.', + status: 'success', + isClosable: true, + }); + onClose(); + + } catch (err) { + setIsLoading(false); + toast({ + title: 'Something went wrong.', + description: 'Failed to create user. Please try again.', + status: 'error', + isClosable: true, + }); + } + } + + return ( + <> + + + + {/* TODO: Check passwords */} + Create User + + + + Email + + + + Full name + + + + Set Password + + + + Confirm Password + + + + + Is superuser? + + + Is active? + + + + + + + + + + + + ) +} + +export default CreateUser; \ No newline at end of file diff --git a/src/new-frontend/src/pages/modals/DeleteAlert.tsx b/src/new-frontend/src/pages/modals/DeleteAlert.tsx new file mode 100644 index 0000000000..9524ff330e --- /dev/null +++ b/src/new-frontend/src/pages/modals/DeleteAlert.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; + +import { AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, Button } from '@chakra-ui/react'; +import { useForm } from 'react-hook-form'; + +import { useItemsStore } from '../../store/items-store'; + +interface DeleteProps { + toDelete: string; + id: number + isOpen: boolean; + onClose: () => void; +} + +const Delete: React.FC = ({ toDelete, id, isOpen, onClose }) => { + const cancelRef = React.useRef(null); + const [isLoading, setIsLoading] = useState(false); + const { handleSubmit } = useForm(); + const { deleteItem } = useItemsStore(); + + const onSubmit = async () => { + try { + setIsLoading(true); + await deleteItem(id); + setIsLoading(false); + onClose(); + } catch (err) { + setIsLoading(false); + console.error(err); + + } + } + + return ( + <> + + + + + Delete {toDelete} + + + + Are you sure? You will not be able to undo this action. + + + + + + + + + + + ) +} + +export default Delete; \ No newline at end of file diff --git a/src/new-frontend/src/pages/modals/EditItem.tsx b/src/new-frontend/src/pages/modals/EditItem.tsx new file mode 100644 index 0000000000..cbd001f43c --- /dev/null +++ b/src/new-frontend/src/pages/modals/EditItem.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'; + +interface EditItemProps { + isOpen: boolean; + onClose: () => void; +} + +const EditItem: React.FC = ({ isOpen, onClose }) => { + + return ( + <> + + + + Edit Item + + + + Item + + + + + Description + + + + + + + + + + + + ) +} + +export default EditItem; \ No newline at end of file diff --git a/src/new-frontend/src/pages/modals/EditUser.tsx b/src/new-frontend/src/pages/modals/EditUser.tsx new file mode 100644 index 0000000000..0434fe589b --- /dev/null +++ b/src/new-frontend/src/pages/modals/EditUser.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import { Button, Checkbox, Flex, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from '@chakra-ui/react'; + +interface EditUserProps { + isOpen: boolean; + onClose: () => void; +} + +const EditUser: React.FC = ({ isOpen, onClose }) => { + + return ( + <> + + + + Edit User + + + + Email + + + + + Full name + + + + Set Password + + + + + Is superuser? + + + Is active? + + + + + + + + + + + + ) +} + +export default EditUser; \ No newline at end of file diff --git a/src/new-frontend/src/pages/panels/Appearance.tsx b/src/new-frontend/src/pages/panels/Appearance.tsx new file mode 100644 index 0000000000..af16abf6be --- /dev/null +++ b/src/new-frontend/src/pages/panels/Appearance.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import { Button, Container, Heading, Radio, RadioGroup, Stack } from '@chakra-ui/react'; + +const Appearance: React.FC = () => { + const [value, setValue] = React.useState('system'); + + return ( + <> + + + Appearance + + + + + Use system settings (default) + + + Light + + + Dark + + + + + + + ); +} +export default Appearance; \ No newline at end of file diff --git a/src/new-frontend/src/pages/panels/ChangePassword.tsx b/src/new-frontend/src/pages/panels/ChangePassword.tsx new file mode 100644 index 0000000000..f7437bf54f --- /dev/null +++ b/src/new-frontend/src/pages/panels/ChangePassword.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { Box, Button, Container, FormControl, FormLabel, Heading, Input } from '@chakra-ui/react'; + +const ChangePassword: React.FC = () => { + + return ( + <> + + + Change Password + + + + Old password + + + + New password + + + + Confirm new password + + + + + + + ); +} +export default ChangePassword; \ No newline at end of file diff --git a/src/new-frontend/src/pages/panels/DeleteAccount.tsx b/src/new-frontend/src/pages/panels/DeleteAccount.tsx new file mode 100644 index 0000000000..1616b1d984 --- /dev/null +++ b/src/new-frontend/src/pages/panels/DeleteAccount.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { Button, Container, Heading, Text } from '@chakra-ui/react'; + +const DeleteAccount: React.FC = () => { + + return ( + <> + + + Delete Account + + + Are you sure you want to delete your account? This action cannot be undone. + + + + + ); +} +export default DeleteAccount; \ No newline at end of file diff --git a/src/new-frontend/src/pages/panels/UserInformation.tsx b/src/new-frontend/src/pages/panels/UserInformation.tsx new file mode 100644 index 0000000000..f7673ed007 --- /dev/null +++ b/src/new-frontend/src/pages/panels/UserInformation.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; + +import { Button, Container, FormControl, FormLabel, Heading, Input, Text } from '@chakra-ui/react'; + +import { useUserStore } from '../../store/user-store'; + +const UserInformation: React.FC = () => { + const [editMode, setEditMode] = useState(false); + const { user } = useUserStore(); + + + const toggleEditMode = () => { + setEditMode(!editMode); + }; + + return ( + <> + + + User Information + + + Full name + { + editMode ? + : + + {user?.full_name || "N/A"} + + } + + + Email + { + editMode ? + : + + {user?.email || "N/A"} + + } + + + + + ); +} +export default UserInformation; \ No newline at end of file