more pages

This commit is contained in:
2022-12-11 15:16:21 -06:00
parent 67233cb37d
commit 8b06077359
12 changed files with 278 additions and 23 deletions

View File

@@ -1,20 +1,29 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { MissingPage } from "./pages/404";
import { Bot } from "./pages/Bot";
import { Bots } from "./pages/Bots";
import { Dashboard } from "./pages/Dashboard";
import { Layout } from "./pages/Layout";
import { Login } from "./pages/Login";
import { Settings } from "./pages/Settings";
import { Token } from "./pages/Token";
import { Tokens } from "./pages/Tokens";
function App() {
return (
<div>
<BrowserRouter>
<Routes>
<Route path="/logout" element={<Login />} />
<Route path="/login" element={<Login />} />
<Route path="/" element={<Layout />}> {/* collapsible sidebar with links */}
{/* <Route index element={<Dashboard />} />
<Route path="/" element={<Layout />}>
<Route index element={<Dashboard />} />
<Route path="bots" element={<Bots />} />
<Route path="bot/:id" element={<Bot />} />
<Route path="tokens" element={<Tokens />} />
<Route path="token/:id" element={<Token />} />
<Route path="*" element={<MissingPage />} /> */}
<Route path="settings" element={<Settings />} />
<Route path="*" element={<MissingPage />} />
</Route>
</Routes>
</BrowserRouter>

View File

@@ -1,25 +1,58 @@
import { useEffect, useState } from 'react'
export function getTokenFromStorage() {
return localStorage.getItem('token')
}
export enum LoggedIn {
export function clearTokenFromStorage() {
localStorage.removeItem('token')
}
export enum LoginState {
No,
Yes,
Invalid,
}
export function isLoggedIn(): LoggedIn {
export function isLoggedIn(): LoginState {
const token = getTokenFromStorage()
if (token) {
return LoggedIn.Yes
return LoginState.Yes
}
return LoggedIn.No
return LoginState.No
}
export function useClearToken() {
const [loggedOut, setLoggedOut] = useState(false)
useEffect(() => {
clearTokenFromStorage()
setLoggedOut(true)
}, [])
return loggedOut
}
export function usePersistentState<T>(
key: string,
defaultData: T
): [T, (data: T) => void] {
const [data, setData] = useState(defaultData)
useEffect(() => {
const data = localStorage.getItem(key)
if (data) {
setData(JSON.parse(data))
}
}, [])
const setPersistentData = (data: T) => {
localStorage.setItem(key, JSON.stringify(data))
setData(data)
}
return [data, setPersistentData]
}
export function authorizeLogin(
username: string,
password: string,
setLoggedIn: (loggedIn: LoggedIn) => void
setLoggedIn: (loggedIn: LoginState) => void
) {
// TODO why is ENV undefined?
console.log(import.meta.env.API)
@@ -40,9 +73,50 @@ export function authorizeLogin(
.then(data => {
if (data.token) {
localStorage.setItem('token', data.token)
setLoggedIn(LoggedIn.Yes)
setLoggedIn(LoginState.Yes)
} else {
setLoggedIn(LoggedIn.Invalid)
setLoggedIn(LoginState.Invalid)
}
})
.catch(err => {
console.error(err)
setLoggedIn(LoginState.No)
})
}
export function useAuthFetch<T = any>(
endpoint: string,
method: 'GET' | 'POST' | 'DELETE' | 'PATCH',
body?: T
): T | null {
const [data, setData] = useState<T | null>(null)
useEffect(() => {
const token = getTokenFromStorage()
if (token) {
fetch('http://localhost:8080' + endpoint, {
method: method,
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token,
},
body: JSON.stringify(body),
})
.then(response => {
if (response.status === 401) {
clearTokenFromStorage()
return null
} else {
return response.json() as T
}
})
.then(setData)
.catch(err => {
console.error(err)
})
}
}, [])
return data
}

View File

@@ -5,6 +5,7 @@ body {
#root {
--bg-color-1: #262626;
--bg-color-2: #363636;
--bg-color-3-5: #3d3d3d;
--bg-color-3: #464646;
--text-color-1: #d6d6d6;
--text-color-2: #a6a6a6;
@@ -46,7 +47,8 @@ body {
}
}
button {
button, .button {
display: inline-block;
border: none;
outline: none;
background-color: var(--bg-color-2);
@@ -77,6 +79,10 @@ body {
fill: var(--text-color-1);
}
}
&:active {
background-color: var(--bg-color-3-5);
}
}
#login {
@@ -92,4 +98,76 @@ body {
margin-bottom: 5px;
}
}
#sidebar {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 200px;
& + #content {
margin-left: 200px;
}
&.collapse {
width: 50px;
& + #content {
margin-left: 50px;
}
}
background-color: var(--bg-color-2);
color: var(--text-color-2);
#collapse {
cursor: pointer;
input {
display: none;
}
}
.menu {
display: flex;
flex-direction: column;
align-items: center;
font-weight: bold;
width: 100%;
height: calc(100% - 50px);
a {
text-decoration: none;
color: inherit;
font-weight: inherit;
width: 100%;
}
.item {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 50px;
font-size: 16px;
&:hover,
&:focus {
background-color: var(--bg-color-3);
color: var(--text-color-1);
&.important {
background-color: var(--color-error);
}
}
}
}
}
#content {
overflow-x: hidden;
}
}

7
src/pages/404.tsx Normal file
View File

@@ -0,0 +1,7 @@
export function MissingPage() {
return (
<div>
404
</div>
)
}

12
src/pages/Bot.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { useParams } from "react-router-dom"
import { useAuthFetch } from "../authorization"
export function Bot() {
const { id } = useParams()
var data = useAuthFetch("/bot/" + (id || ""), "GET")
return (
<div>
{JSON.stringify(data)}
</div>
)
}

10
src/pages/Bots.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { useAuthFetch } from "../authorization"
export function Bots() {
var data = useAuthFetch("/bots", "GET")
return (
<div>
{JSON.stringify(data)}
</div>
)
}

7
src/pages/Dashboard.tsx Normal file
View File

@@ -0,0 +1,7 @@
export function Dashboard() {
return (
<div>
dashboard
</div>
)
}

View File

@@ -1,14 +1,32 @@
import { Outlet, Navigate } from 'react-router-dom'
import { isLoggedIn, LoggedIn } from '../authorization'
import { Outlet, Navigate, NavLink } from 'react-router-dom'
import { isLoggedIn, LoginState, usePersistentState } from '../authorization'
const activeClassName: ({ isActive }: { isActive: boolean }) => string = ({ isActive }) => isActive ? "active" : ""
export function Layout() {
const [collapsed, setCollapsed] = usePersistentState("collapsed", false)
return (
<div>
{
isLoggedIn() === LoggedIn.No && <Navigate to="/login" replace={true} />
isLoggedIn() === LoginState.No && <Navigate to="/login" replace={true} />
}
<span>Layout</span>
<Outlet />
<div id="sidebar" className={collapsed ? "collapse" : ""}>
<label id="collapse">
<input type="checkbox" checked={collapsed} onChange={e => setCollapsed(e.target.checked)} />
<div className="button">yo</div>
</label>
<div className="menu">
<NavLink className={activeClassName} to="/"><span className="item">Dashboard</span></NavLink>
<NavLink className={activeClassName} to="/bots"><span className="item">Bots</span></NavLink>
<NavLink className={activeClassName} to="/tokens"><span className="item">Tokens</span></NavLink>
<NavLink className={activeClassName} to="/settings"><span className="item">Settings</span></NavLink>
<NavLink className={activeClassName} to="/logout"><span className="item important">Logout</span></NavLink>
<NavLink className={activeClassName} to="/404"><span className="item">(temp) 404</span></NavLink>
</div>
</div>
<div id="content">
<Outlet />
</div>
</div>
)
}

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from "react"
import { Navigate } from "react-router-dom"
import { authorizeLogin, isLoggedIn, LoggedIn } from "../authorization"
import { authorizeLogin, isLoggedIn, LoginState, useClearToken } from "../authorization"
export function Login() {
var [username, setUsername] = useState("")
@@ -9,23 +9,23 @@ export function Login() {
var login = () => {
if (username.length === 0 || password.length === 0) {
setLoggedIn(LoggedIn.Invalid)
setLoggedIn(LoginState.Invalid)
return
}
authorizeLogin(username, password, setLoggedIn)
}
useMemo(() => {
if (loggedIn === LoggedIn.Invalid) {
setLoggedIn(LoggedIn.No)
if (loggedIn === LoginState.Invalid) {
setLoggedIn(LoginState.No)
}
}, [username, password])
return (
<div id="login">
<div className="form">
<input type="text" className={loggedIn === LoggedIn.Invalid ? "error" : ""} onChange={e => setUsername(e.target.value)} />
<input type="password" className={loggedIn === LoggedIn.Invalid ? "error" : ""} onChange={e => setPassword(e.target.value)} />
<input type="text" className={loggedIn === LoginState.Invalid ? "error" : ""} onChange={e => setUsername(e.target.value)} />
<input type="password" className={loggedIn === LoginState.Invalid ? "error" : ""} onChange={e => setPassword(e.target.value)} />
</div>
<div>
<button className="pad svg" onClick={login}>
@@ -35,8 +35,19 @@ export function Login() {
</button>
</div>
{
loggedIn === LoggedIn.Yes && <Navigate to="/" replace={true} />
loggedIn === LoginState.Yes && <Navigate to="/" replace={true} />
}
</div>
)
}
export function Logout() {
const loggedOut = useClearToken()
return (
<>
{
loggedOut && <Navigate to="/login" replace={true} />
}
</>
)
}

7
src/pages/Settings.tsx Normal file
View File

@@ -0,0 +1,7 @@
export function Settings() {
return (
<div>
settings
</div>
)
}

12
src/pages/Token.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { useParams } from "react-router-dom"
import { useAuthFetch } from "../authorization"
export function Token() {
const { id } = useParams()
var data = useAuthFetch("/token/" + (id || ""), "GET")
return (
<div>
{JSON.stringify(data)}
</div>
)
}

10
src/pages/Tokens.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { useAuthFetch } from "../authorization"
export function Tokens() {
var data = useAuthFetch("/tokens", "GET")
return (
<div>
{JSON.stringify(data)}
</div>
)
}