diff --git a/package.json b/package.json index bcf6a48..859e21b 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,52 @@ -{ - "name": "frontend", - "version": "0.1.0", - "private": true, - "dependencies": { - "@types/node": "^18.15.11", - "@types/react": "^18.0.37", - "@types/react-dom": "^18.0.11", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router": "^6.4.3", - "react-router-dom": "^6.4.3", - "react-scripts": "5.0.1", - "react-top-loading-bar": "^2.3.1", - "typescript": "^5.0.4", - "web-vitals": "^2.1.4" - }, - "devDependencies": { - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "@types/jest": "^29.5.1", - "eslint": "^8.27.0", - "eslint-plugin-react": "^7.31.10", - "http-proxy-middleware": "^2.0.6" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "dependencies": { + "@types/node": "^18.15.11", + "@types/react": "^18.0.37", + "@types/react-dom": "^18.0.11", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.4.3", + "react-router-dom": "^6.4.3", + "react-scripts": "5.0.1", + "react-top-loading-bar": "^2.3.1", + "typescript": "^5.0.4", + "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^29.5.1", + "eslint": "^8.27.0", + "eslint-plugin-react": "^7.31.10", + "http-proxy-middleware": "^2.0.6" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "lint": "eslint src/ --fix" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/src/App.tsx b/src/App.tsx index ca3341e..9389a60 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,119 +1,125 @@ -import React, { useEffect, useState } from 'react'; -import { Navigate, Route, Routes, useNavigate} from 'react-router-dom'; - -import './css/App.css'; -import Home from './pages/Home'; -import ErrorBoundary from './util/ErrorBoundary'; -import Sidebar, { SidebarMenu } from './components/Sidebar'; -import UserControls from './components/UserControls'; -import { useLoginContext } from './structures/UserContext'; -import Login from './pages/Login'; -import { get, setSession, setSettings } from './util/Util'; -import { PrivateRoute } from './structures/PrivateRoute'; -import { UnauthedRoute } from './structures/UnauthedRoute'; -import Admin from './pages/Admin'; -import TitledPage from './components/TitledPage'; -import Register from './pages/Register'; -import { ClientSettings } from './@types/Other'; -import { User } from './@types/ApiStructures'; - -function App() { - - const [user, updateUser] = useLoginContext(); - const [loading, setLoading] = useState(true); - const navigate = useNavigate(); - - useEffect(() => { - (async () => { - - const settings = await get('/api/settings'); - setSettings(settings.data as ClientSettings); - - const result = await get('/api/user'); - if (result.status === 200) { - setSession(result.data as User); - updateUser(); - } - setLoading(false); - if (result.data?.twoFactor) return navigate('/login/verify'); - })(); - }, []); - - const menuItems = [ - { - to: '/home', label: 'Home', items: [ - { to: '/profile', label: 'Profile', relative: true }, - { to: '/applications', label: 'Applications', relative: true } - ] - }, - { - to: '/admin', label: 'Admin', items: [ - { to: '/users', label: 'Users', relative: true }, - { to: '/roles', label: 'Roles', relative: true }, - { to: '/flags', label: 'Flags', relative: true } - ] - } - ]; - - if (loading) return null; - - return ( -
- -
- - {user ? -
-
- -
- - - -
- : null} - -
- - - - - - - - - - } /> - - - - - - } /> - - - - } /> - - - - } /> - - } /> - - - - -
-

Made with ❤️ by Navy.gif  |  Front-end magic by D3vision

-
-
- -
- - -
- ); -} - -export default App; +import React, { useEffect, useState } from 'react'; +import { Navigate, Route, Routes, useNavigate} from 'react-router-dom'; + +import './css/App.css'; +import Home from './pages/Home'; +import ErrorBoundary from './util/ErrorBoundary'; +import Sidebar, { SidebarMenu } from './components/Sidebar'; +import UserControls from './components/UserControls'; +import { useLoginContext } from './structures/UserContext'; +import Login from './pages/Login'; +import { get, setSession, setSettings } from './util/Util'; +import { PrivateRoute } from './structures/PrivateRoute'; +import { UnauthedRoute } from './structures/UnauthedRoute'; +import Admin from './pages/Admin'; +import TitledPage from './components/TitledPage'; +import Register from './pages/Register'; +import { ClientSettings } from './@types/Other'; +import { User } from './@types/ApiStructures'; + +function App() +{ + + const [user, updateUser] = useLoginContext(); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + + useEffect(() => + { + (async () => + { + + const settings = await get('/api/settings'); + setSettings(settings.data as ClientSettings); + + const result = await get('/api/user'); + if (result.status === 200) + { + setSession(result.data as User); + updateUser(); + } + setLoading(false); + if (result.data?.twoFactor) + return navigate('/login/verify'); + })(); + }, []); + + const menuItems = [ + { + to: '/home', label: 'Home', items: [ + { to: '/profile', label: 'Profile', relative: true }, + { to: '/applications', label: 'Applications', relative: true } + ] + }, + { + to: '/admin', label: 'Admin', items: [ + { to: '/users', label: 'Users', relative: true }, + { to: '/roles', label: 'Roles', relative: true }, + { to: '/flags', label: 'Flags', relative: true } + ] + } + ]; + + if (loading) + return null; + + return ( +
+ +
+ + {user ? +
+
+ +
+ + + +
+ : null} + +
+ + + + + + + + + + } /> + + + + + + } /> + + + + } /> + + + + } /> + + } /> + + + + +
+

Made with ❤️ by Navy.gif  |  Front-end magic by D3vision

+
+
+ +
+ + +
+ ); +} + +export default App; diff --git a/src/components/InputElements.tsx b/src/components/InputElements.tsx index a679a87..0e58ff9 100644 --- a/src/components/InputElements.tsx +++ b/src/components/InputElements.tsx @@ -1,217 +1,245 @@ -import React, { Children, useRef, useState } from "react"; -import ClickDetector from "../util/ClickDetector"; -import { DropdownBaseProps, DropdownItemProps } from "../@types/Components"; -import '../css/components/InputElements.css'; - -export const FileSelector = ({ cb }: { cb: (file: File) => void }) => { - - if (!cb) throw new Error('Missing callback'); - const [file, setFile] = useState(null); - - const onDragOver: React.MouseEventHandler = (event) => { - event.stopPropagation(); - event.preventDefault(); - }; - - const onDrop: React.DragEventHandler = (event) => { - event.preventDefault(); - const { dataTransfer } = event; - if (!dataTransfer.files.length) return; - - const [file] = dataTransfer.files; - setFile(file); - cb(file); - }; - - return ; -}; - -export type InputElementProperties = { - value?: T, - inputRef?: React.RefObject, - onChange?: React.ChangeEventHandler, - children?: React.ReactNode, //JSX.Element | JSX.Element[] | string, - placeholder?: string -} - -export const ToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties) => { - return ; -}; - -export const VerticalToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties) => { - return ; -}; - -type ManualInputProperties = { - onBlur?: React.FocusEventHandler, - onKeyUp?: React.KeyboardEventHandler -} & InputElementProperties; - -type StringInputProperties = { - maxLength?: number, - minLength?: number -} & ManualInputProperties -export const StringInput = ({ value, onChange, inputRef, children, placeholder, onBlur, onKeyUp, minLength, maxLength }: StringInputProperties) => { - - const input = ; - if (children) - return ; - return input; -}; - -export type NumberInputProperties = { - min?: number, - max?: number, - type?: 'float' | 'int', - step?: number -} & ManualInputProperties - -export const NumberInput = ({ children, placeholder, inputRef, onChange, value, min, max, type, step }: NumberInputProperties) => { - if (typeof step === 'undefined') { - if (type === 'float') step = 0.1; - else if (type === 'int') step = 1; - else step = 1; - } - - const input = ; - - if (children) return ; - - return input; -}; - -export const ClickToEdit = ({ value, onUpdate, inputElement }: - { value: string, onUpdate?: (value: string) => void, inputElement?: React.ReactElement }) => { - const [editing, setEditing] = useState(false); - const ref = useRef(null); - const onClick = () => { - setEditing(false); - if (ref.current && onUpdate) - onUpdate(ref.current.value); - }; - - const input = inputElement ? inputElement : ; - if (editing) return - {input} - - ; - return setEditing(true)} className='mt-0 mb-1 clickable'>{value}; -}; - -const DropdownHeader = ({ children, className = '' }: DropdownBaseProps) => { - return - {children} - ; -}; - -const DropdownItem = ({ children, type, selected, onClick }: DropdownItemProps) => { - let InnerElement = null; - if (type === 'multi-select') - InnerElement = - {children as string} - ; - else InnerElement =
{ - event.preventDefault(); - if (onClick) onClick(event); - }}> - {children} -
; - return
- {InnerElement} -
; -}; - -const DropdownItemList = ({ children, className = '' }: DropdownBaseProps) => { - return
- {children} -
; -}; - -type DropdownProps = { - name?: string, - multi?: boolean, - selection?: [], - children: React.ReactNode[], - className?: string -} - -const DropdownComp = ({ children, className = '' }: DropdownProps) => { - - if (!children) - throw new Error('Missing children'); - - if (!children.some(element => element && (element as React.ReactElement).type === DropdownHeader)) - throw new Error('Missing a header'); - - const detailsRef = useRef(null); - - return { - if (detailsRef.current) detailsRef.current.open = false; - }}> -
- {children} -
-
; - -}; - -const Dropdown = Object.assign(DropdownComp, { - Header: DropdownHeader, - ItemList: DropdownItemList, - Item: DropdownItem -}); - -export type InputElementType = - | ((props: InputElementProperties) => React.ReactElement) - | ((props: InputElementProperties) => React.ReactElement) - | ((props: NumberInputProperties) => React.ReactElement); - +import React, { Children, useRef, useState } from "react"; +import ClickDetector from "../util/ClickDetector"; +import { DropdownBaseProps, DropdownItemProps } from "../@types/Components"; +import '../css/components/InputElements.css'; + +export const FileSelector = ({ cb }: { cb: (file: File) => void }) => +{ + + if (!cb) + throw new Error('Missing callback'); + const [file, setFile] = useState(null); + + const onDragOver: React.MouseEventHandler = (event) => + { + event.stopPropagation(); + event.preventDefault(); + }; + + const onDrop: React.DragEventHandler = (event) => + { + event.preventDefault(); + const { dataTransfer } = event; + if (!dataTransfer.files.length) + return; + + const [file] = dataTransfer.files; + setFile(file); + cb(file); + }; + + return ; +}; + +export type InputElementProperties = { + value?: T, + inputRef?: React.RefObject, + onChange?: React.ChangeEventHandler, + children?: React.ReactNode, //JSX.Element | JSX.Element[] | string, + placeholder?: string +} + +export const ToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties) => +{ + return ; +}; + +export const VerticalToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties) => +{ + return ; +}; + +type ManualInputProperties = { + onBlur?: React.FocusEventHandler, + onKeyUp?: React.KeyboardEventHandler +} & InputElementProperties; + +type StringInputProperties = { + maxLength?: number, + minLength?: number +} & ManualInputProperties +export const StringInput = ({ value, onChange, inputRef, children, placeholder, onBlur, onKeyUp, minLength, maxLength }: StringInputProperties) => +{ + + const input = ; + if (children) + return ; + return input; +}; + +export type NumberInputProperties = { + min?: number, + max?: number, + type?: 'float' | 'int', + step?: number +} & ManualInputProperties + +export const NumberInput = ({ children, placeholder, inputRef, onChange, value, min, max, type, step }: NumberInputProperties) => +{ + if (typeof step === 'undefined') + { + if (type === 'float') + step = 0.1; + else if (type === 'int') + step = 1; + else + step = 1; + } + + const input = ; + + if (children) + return ; + + return input; +}; + +export const ClickToEdit = ({ value, onUpdate, inputElement }: + { value: string, onUpdate?: (value: string) => void, inputElement?: React.ReactElement }) => +{ + const [editing, setEditing] = useState(false); + const ref = useRef(null); + const onClick = () => + { + setEditing(false); + if (ref.current && onUpdate) + onUpdate(ref.current.value); + }; + + const input = inputElement ? inputElement : ; + if (editing) + return + {input} + + ; + return setEditing(true)} className='mt-0 mb-1 clickable'>{value}; +}; + +const DropdownHeader = ({ children, className = '' }: DropdownBaseProps) => +{ + return + {children} + ; +}; + +const DropdownItem = ({ children, type, selected, onClick }: DropdownItemProps) => +{ + let InnerElement = null; + if (type === 'multi-select') + InnerElement = + {children as string} + ; + else + InnerElement =
+ { + event.preventDefault(); + if (onClick) + onClick(event); + }}> + {children} +
; + return
+ {InnerElement} +
; +}; + +const DropdownItemList = ({ children, className = '' }: DropdownBaseProps) => +{ + return
+ {children} +
; +}; + +type DropdownProps = { + name?: string, + multi?: boolean, + selection?: [], + children: React.ReactNode[], + className?: string +} + +const DropdownComp = ({ children, className = '' }: DropdownProps) => +{ + + if (!children) + throw new Error('Missing children'); + + if (!children.some(element => element && (element as React.ReactElement).type === DropdownHeader)) + throw new Error('Missing a header'); + + const detailsRef = useRef(null); + + return + { + if (detailsRef.current) + detailsRef.current.open = false; + }}> +
+ {children} +
+
; + +}; + +const Dropdown = Object.assign(DropdownComp, { + Header: DropdownHeader, + ItemList: DropdownItemList, + Item: DropdownItem +}); + +export type InputElementType = + | ((props: InputElementProperties) => React.ReactElement) + | ((props: InputElementProperties) => React.ReactElement) + | ((props: NumberInputProperties) => React.ReactElement); + export { Dropdown }; \ No newline at end of file diff --git a/src/components/PageControls.tsx b/src/components/PageControls.tsx index 64cbbe9..052363f 100644 --- a/src/components/PageControls.tsx +++ b/src/components/PageControls.tsx @@ -7,22 +7,27 @@ type PageButtonProps = { pages: number } -export const PageButtons = ({ setPage, page, pages }: PageButtonProps) => { +export const PageButtons = ({ setPage, page, pages }: PageButtonProps) => +{ return
-

Page: {page} / {pages}

-
; }; -export const BackButton = () => { +export const BackButton = () => +{ const navigate = useNavigate(); - return ; }; \ No newline at end of file diff --git a/src/components/PageElements.tsx b/src/components/PageElements.tsx index 1123782..248e2c1 100644 --- a/src/components/PageElements.tsx +++ b/src/components/PageElements.tsx @@ -1,8 +1,10 @@ import React from "react"; import '../css/components/PageElements.css'; -export const Popup = ({ display = false, children }: { display: boolean, children: React.ReactElement }) => { - if (!display) return null; +export const Popup = ({ display = false, children }: { display: boolean, children: React.ReactElement }) => +{ + if (!display) + return null; return
{children}
; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d0a43b4..a39a06b 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,82 +1,95 @@ -import React, {} from 'react'; -import '../css/components/Sidebar.css'; -import NavLink from '../structures/NavLink'; -import logoImg from '../img/logo.png'; -import { useNavigate } from "react-router"; -import { useLoginContext } from "../structures/UserContext"; -import { clearSession, post } from "../util/Util"; -import { SidebarMenuItem } from '../@types/Other'; - -const Sidebar = ({children}: {children?: React.ReactNode}) => { - return
- {children} -
; -}; - -const toggleMenu: React.MouseEventHandler = (event) => { - event.preventDefault(); - (event.target as HTMLElement).parentElement?.classList.toggle("open"); -}; - -const expandMobileMenu: React.MouseEventHandler = (event) => { - event.preventDefault(); - (event.target as HTMLElement).parentElement?.parentElement?.classList.toggle("show-menu"); -}; - -const closeMobileMenu: React.MouseEventHandler = (event) => { - const element = event.target as HTMLElement; - if(element.classList.contains("sidebar-menu-item-carrot")) return; - (element.getRootNode() as ParentNode).querySelector(".sidebar")?.classList.remove("show-menu"); -}; - -// Nav menu -const SidebarMenu = ({menuItems = [], children}: {menuItems: SidebarMenuItem[], children?: React.ReactNode}) => { - const [user, updateUser] = useLoginContext(); - const navigate = useNavigate(); - - if (!user) return null; - - const logOut = async () => { - const response = await post('/api/logout'); - if (response.status === 200) { - clearSession(); - updateUser(); - navigate('/login'); - } - }; - - const elements = []; - for (const menuItem of menuItems) { - const { label, items: subItems = [], ...rest } = menuItem; - - let subElements = null; - if (subItems.length) subElements = subItems.map(({ label, to, relative = true }) => { - if(relative) to = `${menuItem.to}${to}`; - return {label}; - }); - - elements.push(
- - {label}{subElements && } - - {subElements &&
- {subElements} -
} -
); - } - - return
- - - {elements} - - {children} -
- Log Out -
-
; - -}; - -export {SidebarMenu, Sidebar}; +import React, {} from 'react'; +import '../css/components/Sidebar.css'; +import NavLink from '../structures/NavLink'; +import logoImg from '../img/logo.png'; +import { useNavigate } from "react-router"; +import { useLoginContext } from "../structures/UserContext"; +import { clearSession, post } from "../util/Util"; +import { SidebarMenuItem } from '../@types/Other'; + +const Sidebar = ({children}: {children?: React.ReactNode}) => +{ + return
+ {children} +
; +}; + +const toggleMenu: React.MouseEventHandler = (event) => +{ + event.preventDefault(); + (event.target as HTMLElement).parentElement?.classList.toggle("open"); +}; + +const expandMobileMenu: React.MouseEventHandler = (event) => +{ + event.preventDefault(); + (event.target as HTMLElement).parentElement?.parentElement?.classList.toggle("show-menu"); +}; + +const closeMobileMenu: React.MouseEventHandler = (event) => +{ + const element = event.target as HTMLElement; + if(element.classList.contains("sidebar-menu-item-carrot")) + return; + (element.getRootNode() as ParentNode).querySelector(".sidebar")?.classList.remove("show-menu"); +}; + +// Nav menu +const SidebarMenu = ({menuItems = [], children}: {menuItems: SidebarMenuItem[], children?: React.ReactNode}) => +{ + const [user, updateUser] = useLoginContext(); + const navigate = useNavigate(); + + if (!user) + return null; + + const logOut = async () => + { + const response = await post('/api/logout'); + if (response.status === 200) + { + clearSession(); + updateUser(); + navigate('/login'); + } + }; + + const elements = []; + for (const menuItem of menuItems) + { + const { label, items: subItems = [], ...rest } = menuItem; + + let subElements = null; + if (subItems.length) + subElements = subItems.map(({ label, to, relative = true }) => + { + if(relative) + to = `${menuItem.to}${to}`; + return {label}; + }); + + elements.push(
+ + {label}{subElements && } + + {subElements &&
+ {subElements} +
} +
); + } + + return
+ + + {elements} + + {children} +
+ Log Out +
+
; + +}; + +export {SidebarMenu, Sidebar}; export default Sidebar; \ No newline at end of file diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 93d9abf..7589194 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,45 +1,47 @@ -import React from "react"; - -type Item = { - [key: string]: unknown -} - -type TableEntry = { - onClick?: () => void, - item: Item, - itemKeys: string[] -} - -type TableProps = { - headerItems: string[], - children?: React.ReactNode, - items?: Item[], - itemKeys?: string[] -} - -export const TableListEntry = ({onClick, item, itemKeys}: TableEntry) => { - - return - {itemKeys.map(key => {item[key] as string})} - ; -}; - -export const Table = ({headerItems, children, items, itemKeys}: TableProps) => { - - if (!headerItems) - throw new Error(`Missing table headers`); - if(!children && !items) - throw new Error('Missing items or children'); - let i = 0; - return - - - {headerItems.map(item => )} - - - - {children ? children : items?.map(item => )} - -
{item}
; - +import React from "react"; + +type Item = { + [key: string]: unknown +} + +type TableEntry = { + onClick?: () => void, + item: Item, + itemKeys: string[] +} + +type TableProps = { + headerItems: string[], + children?: React.ReactNode, + items?: Item[], + itemKeys?: string[] +} + +export const TableListEntry = ({onClick, item, itemKeys}: TableEntry) => +{ + + return + {itemKeys.map(key => {item[key] as string})} + ; +}; + +export const Table = ({headerItems, children, items, itemKeys}: TableProps) => +{ + + if (!headerItems) + throw new Error(`Missing table headers`); + if(!children && !items) + throw new Error('Missing items or children'); + let i = 0; + return + + + {headerItems.map(item => )} + + + + {children ? children : items?.map(item => )} + +
{item}
; + }; \ No newline at end of file diff --git a/src/components/TitledPage.tsx b/src/components/TitledPage.tsx index 581281b..d6e8b5a 100644 --- a/src/components/TitledPage.tsx +++ b/src/components/TitledPage.tsx @@ -1,14 +1,15 @@ -import React from "react"; -import '../css/pages/Empty.css'; - -const TitledPage = ({title, children}: {title: string, children: React.ReactNode}) => { - - return
-

{title}

-
- {children} -
-
; -}; - +import React from "react"; +import '../css/pages/Empty.css'; + +const TitledPage = ({title, children}: {title: string, children: React.ReactNode}) => +{ + + return
+

{title}

+
+ {children} +
+
; +}; + export default TitledPage; \ No newline at end of file diff --git a/src/components/UserControls.tsx b/src/components/UserControls.tsx index 188a653..cc00e4e 100644 --- a/src/components/UserControls.tsx +++ b/src/components/UserControls.tsx @@ -1,45 +1,51 @@ -import React, { useRef } from "react"; -import { useNavigate } from "react-router"; -import '../css/components/UserControls.css'; -import { useLoginContext } from "../structures/UserContext"; -import ClickDetector from "../util/ClickDetector"; -import { clearSession, post } from "../util/Util"; - -const UserControls = () => { - - const [user, updateUser] = useLoginContext(); - const detailsRef = useRef(null); - const navigate = useNavigate(); - - if (!user) return null; - - const logOut = async () => { - const response = await post('/api/logout'); - if (response.status === 200) { - clearSession(); - updateUser(); - navigate('/login'); - } - }; - - return { - if (detailsRef.current) detailsRef.current.removeAttribute('open'); - }}> -
- - Hello {user.displayName || user.name} - - - -
-

Profile

-
-

Logout

-
- -
-
; - -}; - +import React, { useRef } from "react"; +import { useNavigate } from "react-router"; +import '../css/components/UserControls.css'; +import { useLoginContext } from "../structures/UserContext"; +import ClickDetector from "../util/ClickDetector"; +import { clearSession, post } from "../util/Util"; + +const UserControls = () => +{ + + const [user, updateUser] = useLoginContext(); + const detailsRef = useRef(null); + const navigate = useNavigate(); + + if (!user) + return null; + + const logOut = async () => + { + const response = await post('/api/logout'); + if (response.status === 200) + { + clearSession(); + updateUser(); + navigate('/login'); + } + }; + + return + { + if (detailsRef.current) + detailsRef.current.removeAttribute('open'); + }}> +
+ + Hello {user.displayName || user.name} + + + +
+

Profile

+
+

Logout

+
+ +
+
; + +}; + export default UserControls; \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 2dcb30b..7722ce3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,11 +8,11 @@ import { BrowserRouter } from 'react-router-dom'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( - - - - - + + + + + ); // root.render( // diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 6c5584f..d8d6f32 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -1,51 +1,54 @@ -import React, { useState } from "react"; -import { Route, Routes } from "react-router"; -import { FileSelector } from "../components/InputElements"; -import '../css/pages/Admin.css'; -import ErrorBoundary from "../util/ErrorBoundary"; -import Roles from "./admin/Roles"; -import Users from "./admin/Users"; -import Flags from "./admin/Flags"; - - -const Main = () => { - const [file, setFile] = useState(null); - - const submit: React.MouseEventHandler = (event) => { - event.preventDefault(); - console.log(file); - }; - - return
- -
-

Thematic customisation

-
- - - - - -
- -
-

Dingus

- -
- -
; -}; - -const Admin = () => { - - return - - } /> - } /> - } /> - } /> - - ; -}; - +import React, { useState } from "react"; +import { Route, Routes } from "react-router"; +import { FileSelector } from "../components/InputElements"; +import '../css/pages/Admin.css'; +import ErrorBoundary from "../util/ErrorBoundary"; +import Roles from "./admin/Roles"; +import Users from "./admin/Users"; +import Flags from "./admin/Flags"; + + +const Main = () => +{ + const [file, setFile] = useState(null); + + const submit: React.MouseEventHandler = (event) => + { + event.preventDefault(); + console.log(file); + }; + + return
+ +
+

Thematic customisation

+
+ + + + + +
+ +
+

Dingus

+ +
+ +
; +}; + +const Admin = () => +{ + + return + + } /> + } /> + } /> + } /> + + ; +}; + export default Admin; \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 7cda0bf..80a4d7b 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,25 +1,27 @@ -import React from "react"; -import { Route, Routes } from "react-router"; -import '../css/pages/Home.css'; -import ErrorBoundary from "../util/ErrorBoundary"; -import Applications from "./home/Applications"; -import Profile from "./home/Profile"; - -const Main = () => { - - return
- What to put here? hmmm -
; -}; - -const Home = () => { - - return - - } /> - } /> - } /> - - ; -}; +import React from "react"; +import { Route, Routes } from "react-router"; +import '../css/pages/Home.css'; +import ErrorBoundary from "../util/ErrorBoundary"; +import Applications from "./home/Applications"; +import Profile from "./home/Profile"; + +const Main = () => +{ + + return
+ What to put here? hmmm +
; +}; + +const Home = () => +{ + + return + + } /> + } /> + } /> + + ; +}; export default Home; \ No newline at end of file diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index f9aff1e..9c2b3d1 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,146 +1,159 @@ -import React, { useState, useRef } from "react"; -import { Route, Routes, useNavigate } from "react-router"; -import '../css/pages/Login.css'; -import logoImg from '../img/logo.png'; -import { useLoginContext } from "../structures/UserContext"; -import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar'; -import { fetchUser, getSettings, post } from "../util/Util"; -import { OAuthProvider } from "../@types/Other"; - -const CredentialFields = () => { - - const [, setUser] = useLoginContext(); - const [fail, setFail] = useState(false); - const navigate = useNavigate(); - const ref = useRef(null); - const usernameRef = useRef(null); - const passwordRef = useRef(null); - - const loginMethods = getSettings()?.OAuthProviders || []; - - const loginClick: React.MouseEventHandler = async (event: React.MouseEvent) => { - ref.current?.continuousStart(); - event.preventDefault(); - // const username = (document.getElementById('username') as HTMLInputElement)?.value; - // const password = (document.getElementById('password') as HTMLInputElement)?.value; - const username = usernameRef.current?.value; - const password = passwordRef.current?.value; - if (!username?.length || !password?.length) return; - - const result = await post('/api/login', { username, password }); - - if (![200, 302].includes(result.status)) { - ref.current?.complete(); - setFail(true); - return; - } - - if (!result.data?.twoFactor) { - setUser(await fetchUser()); - ref.current?.complete(); - return navigate('/home'); - } - ref.current?.complete(); - return navigate('verify'); - - }; - - return
- -
- -
- -
-

Log in

- {fail ?

Invalid credentials

: null} - -
- - - -
- -
- Don't have an account?
navigate('/register')} className="clickable">Register instead! -
- -
- -
- Alternate login methods -
- {loginMethods.map((method: OAuthProvider) =>
{ window.location.pathname = `/api/login/${method.name}`; }}> - {method.name} -

{method.name}

-
)} -
-
-
; -}; - -const TwoFactor = () => { - - const [fail, setFail] = useState(false); - const [, setUser] = useLoginContext(); - const navigate = useNavigate(); - const mfaCode = useRef(null); - const ref = useRef(null); - - const twoFactorClick: React.MouseEventHandler = async (event) => { - event.preventDefault(); - // const code = document.getElementById('2faCode').value; - const code = mfaCode.current?.value; - if (!code) return; - ref.current?.continuousStart(); - const result = await post('/api/login/verify', { code }); - if (result.status === 200) { - setUser(await fetchUser()); - ref.current?.complete(); - return navigate('/home'); - } - ref.current?.complete(); - setFail(true); - }; - - return
- -
- -
- -
-

Verification Code

- {fail ?

Invalid code

: null} -
- - -
-
-
; -}; - -const Login = () => { - - return
-
- - - } /> - } /> - - -
-
; - -}; - +import React, { useState, useRef } from "react"; +import { Route, Routes, useNavigate } from "react-router"; +import '../css/pages/Login.css'; +import logoImg from '../img/logo.png'; +import { useLoginContext } from "../structures/UserContext"; +import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar'; +import { fetchUser, getSettings, post } from "../util/Util"; +import { OAuthProvider } from "../@types/Other"; + +const CredentialFields = () => +{ + + const [, setUser] = useLoginContext(); + const [fail, setFail] = useState(false); + const navigate = useNavigate(); + const ref = useRef(null); + const usernameRef = useRef(null); + const passwordRef = useRef(null); + + const loginMethods = getSettings()?.OAuthProviders || []; + + const loginClick: React.MouseEventHandler = async (event: React.MouseEvent) => + { + ref.current?.continuousStart(); + event.preventDefault(); + // const username = (document.getElementById('username') as HTMLInputElement)?.value; + // const password = (document.getElementById('password') as HTMLInputElement)?.value; + const username = usernameRef.current?.value; + const password = passwordRef.current?.value; + if (!username?.length || !password?.length) + return; + + const result = await post('/api/login', { username, password }); + + if (![200, 302].includes(result.status)) + { + ref.current?.complete(); + setFail(true); + return; + } + + if (!result.data?.twoFactor) + { + setUser(await fetchUser()); + ref.current?.complete(); + return navigate('/home'); + } + ref.current?.complete(); + return navigate('verify'); + + }; + + return
+ +
+ +
+ +
+

Log in

+ {fail ?

Invalid credentials

: null} + +
+ + + +
+ +
+ Don't have an account?
navigate('/register')} className="clickable">Register instead! +
+ +
+ +
+ Alternate login methods +
+ {loginMethods.map((method: OAuthProvider) =>
+ { + window.location.pathname = `/api/login/${method.name}`; + }}> + {method.name} +

{method.name}

+
)} +
+
+
; +}; + +const TwoFactor = () => +{ + + const [fail, setFail] = useState(false); + const [, setUser] = useLoginContext(); + const navigate = useNavigate(); + const mfaCode = useRef(null); + const ref = useRef(null); + + const twoFactorClick: React.MouseEventHandler = async (event) => + { + event.preventDefault(); + // const code = document.getElementById('2faCode').value; + const code = mfaCode.current?.value; + if (!code) + return; + ref.current?.continuousStart(); + const result = await post('/api/login/verify', { code }); + if (result.status === 200) + { + setUser(await fetchUser()); + ref.current?.complete(); + return navigate('/home'); + } + ref.current?.complete(); + setFail(true); + }; + + return
+ +
+ +
+ +
+

Verification Code

+ {fail ?

Invalid code

: null} +
+ + +
+
+
; +}; + +const Login = () => +{ + + return
+
+ + + } /> + } /> + + +
+
; + +}; + export default Login; \ No newline at end of file diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index 16e59e4..bb5e88f 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -1,66 +1,70 @@ -import React, { useState, useRef } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { post } from "../util/Util"; -import '../css/pages/Register.css'; -import logoImg from '../img/logo.png'; -import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar'; - -const Register = () => { - - const [error, setError] = useState(); - const [params] = useSearchParams(); - const navigate = useNavigate(); - const ref = useRef(null); - const usernameRef = useRef(null); - const passwordRef = useRef(null); - - document.body.classList.add('bg-triangles'); - const code = params.get('code'); - - const submit: React.MouseEventHandler = async (event) => { - ref.current?.continuousStart(); - event.preventDefault(); - const username = usernameRef.current?.value; - const password = passwordRef.current?.value; - if (!username?.length || !password?.length) { - ref.current?.complete(); - return; - } - const response = await post('/api/register', { username, password, code }); - if (response.status !== 200) { - ref.current?.complete(); - return setError(response.message as string || 'unknown error'); - } - ref.current?.complete(); - navigate('/login'); - }; - - return
- -
-
-
- -
- -
-

Register

- - {error &&

{error}

} - -
- - - -
- - -
-
-
-
; -}; - +import React, { useState, useRef } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { post } from "../util/Util"; +import '../css/pages/Register.css'; +import logoImg from '../img/logo.png'; +import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar'; + +const Register = () => +{ + + const [error, setError] = useState(); + const [params] = useSearchParams(); + const navigate = useNavigate(); + const ref = useRef(null); + const usernameRef = useRef(null); + const passwordRef = useRef(null); + + document.body.classList.add('bg-triangles'); + const code = params.get('code'); + + const submit: React.MouseEventHandler = async (event) => + { + ref.current?.continuousStart(); + event.preventDefault(); + const username = usernameRef.current?.value; + const password = passwordRef.current?.value; + if (!username?.length || !password?.length) + { + ref.current?.complete(); + return; + } + const response = await post('/api/register', { username, password, code }); + if (response.status !== 200) + { + ref.current?.complete(); + return setError(response.message as string || 'unknown error'); + } + ref.current?.complete(); + navigate('/login'); + }; + + return
+ +
+
+
+ +
+ +
+

Register

+ + {error &&

{error}

} + +
+ + + +
+ + +
+
+
+
; +}; + export default Register; \ No newline at end of file diff --git a/src/pages/admin/Roles.tsx b/src/pages/admin/Roles.tsx index c04f2e8..8e83e91 100644 --- a/src/pages/admin/Roles.tsx +++ b/src/pages/admin/Roles.tsx @@ -9,12 +9,14 @@ import { get, patch, post } from "../../util/Util"; import { Permissions as Perms, Role as R } from "../../@types/ApiStructures"; import { ToggleSwitch } from "../../components/InputElements"; -const Role = ({ role }: {role: R}) => { +const Role = ({ role }: {role: R}) => +{ // const perms = { ...role.permissions }; const [perms, updatePerms] = useState(role.permissions); - const commitPerms = async () => { + const commitPerms = async () => + { await patch(`/api/roles/${role.id}`, perms); }; @@ -35,9 +37,9 @@ const Role = ({ role }: {role: R}) => {

Created: {new Date(role.createdTimestamp).toDateString()}

{/*

Disabled: {role.disabled.toString()}

*/} - + Disabled - + @@ -57,21 +59,25 @@ const Role = ({ role }: {role: R}) => { ; }; -const CreateRoleField = ({ numRoles, className, addRole }: { numRoles: number, className?: string, addRole: (role: R) => void }) => { +const CreateRoleField = ({ numRoles, className, addRole }: { numRoles: number, className?: string, addRole: (role: R) => void }) => +{ const [error, setError] = useState(null); const nameRef = useRef(null); const positionRef = useRef(null); - const createRole: React.MouseEventHandler = async (event) => { + const createRole: React.MouseEventHandler = async (event) => + { event.preventDefault(); - if (!nameRef.current || !positionRef.current) return setError('Must supply name and position'); + if (!nameRef.current || !positionRef.current) + return setError('Must supply name and position'); const name = nameRef.current.value; const position = parseInt(positionRef.current.value); const response = await post('/api/roles', { name, position }); - if (!response.success) return setError(response.message || 'Unknown error'); + if (!response.success) + return setError(response.message || 'Unknown error'); addRole(response.data as R); }; @@ -94,7 +100,8 @@ const CreateRoleField = ({ numRoles, className, addRole }: { numRoles: number, c }; -const Roles = () => { +const Roles = () => +{ const [error, setError] = useState(null); const [roles, setRoles] = useState([]); @@ -102,41 +109,50 @@ const Roles = () => { const [pages, setPages] = useState(1); const [loading, setLoading] = useState(true); - useEffect(() => { - (async () => { + useEffect(() => + { + (async () => + { const result = await get('/api/roles', { page }); - if (result.success && result.data) { + if (result.success && result.data) + { setError(null); setRoles(result.data.roles as R[]); setPages(result.data.pages); - } else { + } + else + { setError(result.message || 'Unknown error'); } setLoading(false); })(); }, [page]); - const RoleWrapper = () => { + const RoleWrapper = () => + { const { id } = useParams(); const role = roles.find(r => r.id === id); - if(!role) return

Unknown role

; + if(!role) + return

Unknown role

; return ; }; const navigate = useNavigate(); - const RoleListWrapper = () => { + const RoleListWrapper = () => + { return

All Roles

{roles.map(role => { - navigate(role.id); - }} - key={role.id} - item={role} - itemKeys={['name', 'id']} - />)} + onClick={() => + { + navigate(role.id); + }} + key={role.id} + item={role} + itemKeys={['name', 'id']} + />)}
diff --git a/src/pages/admin/Users.tsx b/src/pages/admin/Users.tsx index 02358e9..c1020a3 100644 --- a/src/pages/admin/Users.tsx +++ b/src/pages/admin/Users.tsx @@ -1,321 +1,359 @@ -import React, { useContext, useEffect, useState } from "react"; -import { Route, Routes, useNavigate, useParams } from "react-router"; -import ErrorBoundary from "../../util/ErrorBoundary"; -import { get, post } from '../../util/Util'; -import '../../css/pages/Users.css'; -import { Table, TableListEntry } from "../../components/Table"; -import { BackButton, PageButtons } from "../../components/PageControls"; -import { Permissions } from "../../views/PermissionsView"; -import { Application as App, Permissions as Perms, User as APIUser, Role } from "../../@types/ApiStructures"; -import { Dropdown, ToggleSwitch } from "../../components/InputElements"; - -type PartialUser = { - apps: App[] -} -const SelectedUserContext = React.createContext<{ - user: PartialUser | null, - updateUser:(user: PartialUser) => void -}>({ - user: null, - updateUser: () => { /** */ } -}); -const Context = ({ children }: { children: React.ReactNode }) => { - const [user, updateUser] = useState(null); - return - {children} - ; -}; - -const ApplicationList = ({ apps }: { apps: App[] }) => { - - const navigate = useNavigate(); - return - {apps.map(app => { - navigate(`applications/${app.id}`); - }} - key={app.id} - item={app} - itemKeys={['name', 'id']} - />)} -
; - -}; - -const Application = ({ app }: { app: App }) => { - - const commitPerms = async () => { - await post(`/api/applications/${app.id}/permissions`, perms); - }; - const [perms, updatePerms] = useState(app.permissions); - const descProps = { defaultValue: app.description || '', placeholder: 'Describe your application' }; - return
-
-
-
- -

Application {app.name} ({app.id})

-
- -

Created: {new Date(app.createdTimestamp).toDateString()}

- - Disabled: - - - Description - -
- -
- - -
- -
-
; -}; - -const RoleComp = ({ role, onClick }: { role: Role, onClick: React.ReactEventHandler }) => { - return
- {role.name} X -
; -}; - -const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => { - - const [unequippedRoles, updateUnequipped] = useState(roles.filter(role => !userRoles.some(r => role.id === r.id))); - const [equippedRoles, updateEquipped] = useState(userRoles); - - const roleSelected = (role: Role) => { - updateEquipped([...equippedRoles, role]); - updateUnequipped(unequippedRoles.filter(r => r.id !== role.id)); - }; - const roleDeselected = (role: Role) => { - updateEquipped(equippedRoles.filter(r => r.id !== role.id)); - updateUnequipped([...unequippedRoles, role]); - }; - return - - - {equippedRoles.map(role => { - event.preventDefault(); - roleDeselected(role); - }} />)} - - - - - {unequippedRoles.map(role => { - roleSelected(role); - }} - > - {role.name} - )} - - - - ; -}; - -const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => { - - // const [apps, updateApps] = useState([]); - const [perms, updatePerms] = useState(user.permissions); - const userContext = useContext(SelectedUserContext); - - useEffect(() => { - (async () => { - const appsResponse = await get(`/api/users/${user.id}/applications`); - if (appsResponse.success) { - const a = appsResponse.data as App[]; - // updateApps(a); - userContext.updateUser({ apps: a }); +import React, { useContext, useEffect, useState } from "react"; +import { Route, Routes, useNavigate, useParams } from "react-router"; +import ErrorBoundary from "../../util/ErrorBoundary"; +import { get, post } from '../../util/Util'; +import '../../css/pages/Users.css'; +import { Table, TableListEntry } from "../../components/Table"; +import { BackButton, PageButtons } from "../../components/PageControls"; +import { Permissions } from "../../views/PermissionsView"; +import { Application as App, Permissions as Perms, User as APIUser, Role } from "../../@types/ApiStructures"; +import { Dropdown, ToggleSwitch } from "../../components/InputElements"; + +type PartialUser = { + apps: App[] +} +const SelectedUserContext = React.createContext<{ + user: PartialUser | null, + updateUser:(user: PartialUser) => void + }>({ + user: null, + updateUser: () => + { /** */ } + }); +const Context = ({ children }: { children: React.ReactNode }) => +{ + const [user, updateUser] = useState(null); + return + {children} + ; +}; + +const ApplicationList = ({ apps }: { apps: App[] }) => +{ + + const navigate = useNavigate(); + return + {apps.map(app => + { + navigate(`applications/${app.id}`); + }} + key={app.id} + item={app} + itemKeys={['name', 'id']} + />)} +
; + +}; + +const Application = ({ app }: { app: App }) => +{ + + const commitPerms = async () => + { + await post(`/api/applications/${app.id}/permissions`, perms); + }; + const [perms, updatePerms] = useState(app.permissions); + const descProps = { defaultValue: app.description || '', placeholder: 'Describe your application' }; + return
+
+
+
+ +

Application {app.name} ({app.id})

+
+ +

Created: {new Date(app.createdTimestamp).toDateString()}

+ + Disabled: + + + Description + +
+ +
+ + +
+ +
+
; +}; + +const RoleComp = ({ role, onClick }: { role: Role, onClick: React.ReactEventHandler }) => +{ + return
+ {role.name} X +
; +}; + +const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => +{ + + const [unequippedRoles, updateUnequipped] = useState(roles.filter(role => !userRoles.some(r => role.id === r.id))); + const [equippedRoles, updateEquipped] = useState(userRoles); + + const roleSelected = (role: Role) => + { + updateEquipped([...equippedRoles, role]); + updateUnequipped(unequippedRoles.filter(r => r.id !== role.id)); + }; + const roleDeselected = (role: Role) => + { + updateEquipped(equippedRoles.filter(r => r.id !== role.id)); + updateUnequipped([...unequippedRoles, role]); + }; + return + + + {equippedRoles.map(role => + { + event.preventDefault(); + roleDeselected(role); + }} />)} + + + + + {unequippedRoles.map(role => + { + roleSelected(role); + }} + > + {role.name} + )} + + + + ; +}; + +const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => +{ + + // const [apps, updateApps] = useState([]); + const [perms, updatePerms] = useState(user.permissions); + const userContext = useContext(SelectedUserContext); + + useEffect(() => + { + (async () => + { + const appsResponse = await get(`/api/users/${user.id}/applications`); + if (appsResponse.success) + { + const a = appsResponse.data as App[]; + // updateApps(a); + userContext.updateUser({ apps: a }); + } + })(); + }, []); + + const commitPerms = async () => + { + await post(`/api/users/${user.id}/permissions`, perms); + }; + + return
+
+
+
+ +

User {user.displayName} ({user.id})

+
+ +

Created: {new Date(user.createdTimestamp).toDateString()}

+

2FA: {(user.twoFactor && ) || 'Disabled'}

+ + Login Disabled: + + + + + + + + + +