first
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -30,3 +30,5 @@ yarn-error.log*
|
|||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
|
/db.sqlite
|
||||||
|
|||||||
82
libs/db.js
Normal file
82
libs/db.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
const db = new Database("db.sqlite");
|
||||||
|
|
||||||
|
// init DB
|
||||||
|
db.prepare(
|
||||||
|
`CREATE TABLE IF NOT EXISTS time_ranges (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL DEFAULT '',
|
||||||
|
range TEXT NOT NULL DEFAULT '',
|
||||||
|
username TEXT NOT NULL DEFAULT ''
|
||||||
|
)`
|
||||||
|
).run();
|
||||||
|
db.prepare(
|
||||||
|
`CREATE TABLE IF NOT EXISTS configs (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
)`
|
||||||
|
).run();
|
||||||
|
db.prepare(
|
||||||
|
`INSERT OR IGNORE INTO configs (name, value) VALUES ('limit', '1')`
|
||||||
|
).run();
|
||||||
|
db.prepare(
|
||||||
|
`INSERT OR IGNORE INTO configs (name, value) VALUES ('token', 'woshimima')`
|
||||||
|
).run();
|
||||||
|
|
||||||
|
// prepare statements
|
||||||
|
const insertTimeRange = db.prepare(
|
||||||
|
`INSERT INTO time_ranges (name, range) VALUES (?, ?)`
|
||||||
|
);
|
||||||
|
const getTimeRanges = db.prepare(`SELECT * FROM time_ranges`);
|
||||||
|
const deleteTimeRange = db.prepare(`DELETE FROM time_ranges WHERE id = ?`);
|
||||||
|
const updateUsername = db.prepare(
|
||||||
|
`UPDATE time_ranges SET username = ? WHERE id = ?`
|
||||||
|
);
|
||||||
|
const countUser = db.prepare(
|
||||||
|
`SELECT COUNT(*) as count FROM time_ranges WHERE username = ?`
|
||||||
|
);
|
||||||
|
const getUsername = db.prepare(`SELECT username FROM time_ranges WHERE id = ?`);
|
||||||
|
const updateUsernameWithLimit = db.transaction((username, id, limit) => {
|
||||||
|
const count = countUser.get(username).count;
|
||||||
|
const existingUsername = getUsername.get(id).username;
|
||||||
|
if (existingUsername !== "") {
|
||||||
|
throw new Error("Username already exists");
|
||||||
|
}
|
||||||
|
if (count >= limit) {
|
||||||
|
throw new Error("Limit reached");
|
||||||
|
}
|
||||||
|
updateUsername.run(username, id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getConfigStmt = db.prepare(`SELECT value FROM configs WHERE name = ?`);
|
||||||
|
const setConfigStmt = db.prepare(
|
||||||
|
`UPDATE configs SET value = ? WHERE name = ?`
|
||||||
|
);
|
||||||
|
const getLimit = () => {
|
||||||
|
const limit = getConfigStmt.get("limit").value;
|
||||||
|
return parseInt(limit);
|
||||||
|
};
|
||||||
|
const setLimit = (limit) => {
|
||||||
|
setConfigStmt.run(limit, "limit");
|
||||||
|
};
|
||||||
|
const getToken = () => {
|
||||||
|
const token = getConfigStmt.get("token").value;
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const authenticate = (token) => {
|
||||||
|
const tokenFromDB = getToken();
|
||||||
|
return token === tokenFromDB;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
insertTimeRange,
|
||||||
|
getTimeRanges,
|
||||||
|
deleteTimeRange,
|
||||||
|
updateUsername,
|
||||||
|
updateUsernameWithLimit,
|
||||||
|
getLimit,
|
||||||
|
setLimit,
|
||||||
|
authenticate,
|
||||||
|
};
|
||||||
1659
package-lock.json
generated
1659
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
|||||||
"@emotion/styled": "^11.8.1",
|
"@emotion/styled": "^11.8.1",
|
||||||
"@mui/icons-material": "^5.5.1",
|
"@mui/icons-material": "^5.5.1",
|
||||||
"@mui/material": "^5.5.3",
|
"@mui/material": "^5.5.3",
|
||||||
|
"better-sqlite3": "^7.5.0",
|
||||||
"next": "12.1.2",
|
"next": "12.1.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2"
|
||||||
|
|||||||
@@ -1,7 +1,63 @@
|
|||||||
import '../styles/globals.css'
|
import Head from "next/head";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Stack,
|
||||||
|
Box,
|
||||||
|
CssBaseline,
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import "../styles/globals.css";
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }) {
|
function MyApp({ Component, pageProps }) {
|
||||||
return <Component {...pageProps} />
|
const [username, setUsername] = useState("");
|
||||||
|
pageProps = { ...pageProps, username, setUsername };
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>ITSC Tool</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="minimum-scale=1, initial-scale=1, width=device-width"
|
||||||
|
/>
|
||||||
|
<CssBaseline />
|
||||||
|
</Head>
|
||||||
|
<AppBar
|
||||||
|
position="static"
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">
|
||||||
|
<Link href="/">ITSC Tool</Link>
|
||||||
|
</Typography>
|
||||||
|
{username && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
//localStorage.removeItem("username");
|
||||||
|
setUsername("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{username} (Logout)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyApp
|
export default MyApp;
|
||||||
|
|||||||
30
pages/api/time/limit.js
Normal file
30
pages/api/time/limit.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { authenticate, setLimit } from "../../../libs/db";
|
||||||
|
|
||||||
|
export default function handler(req, res) {
|
||||||
|
// put method
|
||||||
|
if (req.method === "PUT") {
|
||||||
|
const { token, limit } = req.body;
|
||||||
|
|
||||||
|
// authenticate
|
||||||
|
if (!authenticate(token)) {
|
||||||
|
res.status(401).json({
|
||||||
|
error: "Unauthorized",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check type is integer
|
||||||
|
const limitInt = parseInt(limit);
|
||||||
|
if (!limitInt) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: "limit must be integer",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLimit(limitInt);
|
||||||
|
res.status(200).send({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(405).send({ error: "method not allowed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
29
pages/api/time/ranges.js
Normal file
29
pages/api/time/ranges.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { authenticate, getTimeRanges, insertTimeRange } from "../../../libs/db";
|
||||||
|
|
||||||
|
export default function handler(req, res) {
|
||||||
|
// get method
|
||||||
|
if (req.method === "GET") {
|
||||||
|
res.setHeader("Cache-Control", "no-cache no-store must-revalidate");
|
||||||
|
res.status(200).json(getTimeRanges.all());
|
||||||
|
return;
|
||||||
|
} else if (req.method === "POST") {
|
||||||
|
// authenticate
|
||||||
|
const { token } = req.body;
|
||||||
|
if (!authenticate(token)) {
|
||||||
|
res.status(401).json({ error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// jsonfiy
|
||||||
|
const { name, range } = req.body;
|
||||||
|
insertTimeRange.run(name, range);
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 500 error
|
||||||
|
res.status(500).json({
|
||||||
|
error: "Method not allowed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
59
pages/api/time/ranges/[id].js
Normal file
59
pages/api/time/ranges/[id].js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { deleteTimeRange, getLimit, authenticate, updateUsernameWithLimit } from "../../../../libs/db";
|
||||||
|
|
||||||
|
export default function handler(req, res) {
|
||||||
|
// check if id is valid
|
||||||
|
const { id } = req.query;
|
||||||
|
if (id === undefined) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: "Missing id",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete method
|
||||||
|
if (req.method === "DELETE") {
|
||||||
|
// authenticate
|
||||||
|
const { token } = req.body;
|
||||||
|
if (!authenticate(token)) {
|
||||||
|
console.log("[DELETE] Authentication failed");
|
||||||
|
res.status(401).json({
|
||||||
|
error: "Unauthenticated",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deleteTimeRange.run(id);
|
||||||
|
|
||||||
|
// update username
|
||||||
|
} else if (req.method === "PUT") {
|
||||||
|
// check if id is valid
|
||||||
|
// check if username is valid
|
||||||
|
const { username } = req.body;
|
||||||
|
if (username === undefined) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: "Missing username",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const limit = getLimit();
|
||||||
|
updateUsernameWithLimit(username, id, limit);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not allow
|
||||||
|
} else {
|
||||||
|
// 500 error
|
||||||
|
res.status(500).json({
|
||||||
|
error: "Method not allowed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
141
pages/index.js
141
pages/index.js
@@ -1,69 +1,82 @@
|
|||||||
import Head from 'next/head'
|
import { useState, useEffect } from "react";
|
||||||
import Image from 'next/image'
|
import { useRouter } from "next/router";
|
||||||
import styles from '../styles/Home.module.css'
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
Stack,
|
||||||
|
InputField,
|
||||||
|
Box,
|
||||||
|
Snackbar,
|
||||||
|
Container,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
export default function Index(props) {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||||
|
|
||||||
|
// get username from localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
const localUsername = localStorage.getItem("username");
|
||||||
|
if (localUsername) {
|
||||||
|
setUsername(localUsername);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
if (!username) {
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// set local storage
|
||||||
|
localStorage.setItem("username", username);
|
||||||
|
props.setUsername(username);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.username) {
|
||||||
|
router.push("/time");
|
||||||
|
}
|
||||||
|
}, [props.username]);
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<Container>
|
||||||
<Head>
|
<Stack direction="row" spacing={2}>
|
||||||
<title>Create Next App</title>
|
<TextField
|
||||||
<meta name="description" content="Generated by create next app" />
|
label="Username"
|
||||||
<link rel="icon" href="/favicon.ico" />
|
value={username}
|
||||||
</Head>
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
onKeyUp={(e) => {
|
||||||
<main className={styles.main}>
|
if (e.key === "Enter") {
|
||||||
<h1 className={styles.title}>
|
login();
|
||||||
Welcome to <a href="https://nextjs.org">Next.js!</a>
|
}
|
||||||
</h1>
|
}}
|
||||||
|
/>
|
||||||
<p className={styles.description}>
|
<Link href="/time" passHref>
|
||||||
Get started by editing{' '}
|
<Button
|
||||||
<code className={styles.code}>pages/index.js</code>
|
variant="contained"
|
||||||
</p>
|
color="primary"
|
||||||
|
onClick={(e) => {
|
||||||
<div className={styles.grid}>
|
e.preventDefault();
|
||||||
<a href="https://nextjs.org/docs" className={styles.card}>
|
login();
|
||||||
<h2>Documentation →</h2>
|
}}
|
||||||
<p>Find in-depth information about Next.js features and API.</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://nextjs.org/learn" className={styles.card}>
|
|
||||||
<h2>Learn →</h2>
|
|
||||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://github.com/vercel/next.js/tree/canary/examples"
|
|
||||||
className={styles.card}
|
|
||||||
>
|
>
|
||||||
<h2>Examples →</h2>
|
Login
|
||||||
<p>Discover and deploy boilerplate example Next.js projects.</p>
|
</Button>
|
||||||
</a>
|
</Link>
|
||||||
|
</Stack>
|
||||||
<a
|
<Snackbar
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
open={snackbarOpen}
|
||||||
className={styles.card}
|
autoHideDuration={1000}
|
||||||
|
onClose={() => setSnackbarOpen(false)}
|
||||||
>
|
>
|
||||||
<h2>Deploy →</h2>
|
<Alert variant="filled" severity="error">
|
||||||
<p>
|
Username can{"'"}t be empty
|
||||||
Instantly deploy your Next.js site to a public URL with Vercel.
|
</Alert>
|
||||||
</p>
|
</Snackbar>
|
||||||
</a>
|
</Container>
|
||||||
</div>
|
);
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer className={styles.footer}>
|
|
||||||
<a
|
|
||||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Powered by{' '}
|
|
||||||
<span className={styles.logo}>
|
|
||||||
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
297
pages/time.js
Normal file
297
pages/time.js
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Box,
|
||||||
|
Alert,
|
||||||
|
Snackbar,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
TableContainer,
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableBody,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
export default function Time(props) {
|
||||||
|
const [ranges, setRanges] = useState([]);
|
||||||
|
const [range, setRange] = useState("");
|
||||||
|
const [newName, setNewName] = useState("");
|
||||||
|
const [snackbarError, setSnackbarError] = useState(false);
|
||||||
|
const [snackbarErrorMessage, setSnackbarErrorMessage] = useState("");
|
||||||
|
const [snackbarSuccess, setSnackbarSuccess] = useState(false);
|
||||||
|
const [limit, setLimit] = useState(1);
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const isAdmin = () => {
|
||||||
|
if (props.username === "admin") {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addRange = () => {
|
||||||
|
fetch("/api/time/ranges", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: newName,
|
||||||
|
range,
|
||||||
|
token,
|
||||||
|
}),
|
||||||
|
}).then((res) =>
|
||||||
|
res.json().then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
setSnackbarError(true);
|
||||||
|
setSnackbarErrorMessage(res.error);
|
||||||
|
} else {
|
||||||
|
setSnackbarSuccess(true);
|
||||||
|
refreshRanges();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshRanges = () => {
|
||||||
|
fetch("/api/time/ranges")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
setSnackbarError(true);
|
||||||
|
setSnackbarErrorMessage(res.error);
|
||||||
|
} else {
|
||||||
|
setSnackbarSuccess(true);
|
||||||
|
setRanges(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRange = (id) => {
|
||||||
|
fetch(`/api/time/ranges/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
setSnackbarError(true);
|
||||||
|
setSnackbarErrorMessage(res.error);
|
||||||
|
} else {
|
||||||
|
setSnackbarSuccess(true);
|
||||||
|
refreshRanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUsername = (id, username) => {
|
||||||
|
fetch(`/api/time/ranges/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
setSnackbarError(true);
|
||||||
|
setSnackbarErrorMessage(res.error);
|
||||||
|
} else {
|
||||||
|
setSnackbarSuccess(true);
|
||||||
|
refreshRanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLimit = (limit) => {
|
||||||
|
fetch("/api/time/limit", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ limit, token }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
setSnackbarError(true);
|
||||||
|
setSnackbarErrorMessage(res.error);
|
||||||
|
} else {
|
||||||
|
setSnackbarSuccess(true);
|
||||||
|
refreshRanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.username) {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
refreshRanges();
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
*/
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshRanges();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{isAdmin() && (
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
my: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
label="Token"
|
||||||
|
value={token}
|
||||||
|
onChange={(e) => setToken(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
my: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
label="Name"
|
||||||
|
value={newName}
|
||||||
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Range"
|
||||||
|
value={range}
|
||||||
|
onChange={(e) => setRange(e.target.value)}
|
||||||
|
placeholder="2022-01-01 00:00:00"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => addRange()}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
my: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
label="Limit"
|
||||||
|
value={limit}
|
||||||
|
onChange={(e) => setLimit(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
updateLimit(limit);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update Limit
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => refreshRanges()}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Name</TableCell>
|
||||||
|
<TableCell>Range</TableCell>
|
||||||
|
<TableCell>Taken</TableCell>
|
||||||
|
<TableCell>Action</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{ranges.map((range) => (
|
||||||
|
<TableRow key={range.id}>
|
||||||
|
<TableCell>{range.name}</TableCell>
|
||||||
|
<TableCell>{range.range}</TableCell>
|
||||||
|
<TableCell>{range.username}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
disabled={range.username !== ""}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => updateUsername(range.id, props.username)}
|
||||||
|
>
|
||||||
|
Take
|
||||||
|
</Button>
|
||||||
|
{isAdmin() && (
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => deleteRange(range.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<Snackbar
|
||||||
|
open={snackbarError}
|
||||||
|
autoHideDuration={1000}
|
||||||
|
onClose={() => setSnackbarError(false)}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
variant="filled"
|
||||||
|
onClose={() => setSnackbarError(false)}
|
||||||
|
severity="error"
|
||||||
|
>
|
||||||
|
{snackbarErrorMessage}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
<Snackbar
|
||||||
|
open={snackbarSuccess}
|
||||||
|
autoHideDuration={1000}
|
||||||
|
onClose={() => setSnackbarSuccess(false)}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
variant="filled"
|
||||||
|
onClose={() => setSnackbarSuccess(false)}
|
||||||
|
severity="success"
|
||||||
|
>
|
||||||
|
Success!
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user