Main project structure #3

Closed
Beesquit wants to merge 17 commits from dev into main
27 changed files with 4131 additions and 0 deletions
Showing only changes of commit 96cc825985 - Show all commits

32
package-lock.json generated
View File

@ -8,9 +8,11 @@
"name": "picrinth-admin", "name": "picrinth-admin",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@material/material-color-utilities": "^0.3.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.7.1" "react-router-dom": "^7.7.1",
"react-toastify": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.32.0", "@eslint/js": "^9.32.0",
@ -1023,6 +1025,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@material/material-color-utilities": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.3.0.tgz",
"integrity": "sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==",
"license": "Apache-2.0"
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1894,6 +1902,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -2947,6 +2964,19 @@
"react-dom": ">=18" "react-dom": ">=18"
} }
}, },
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",

View File

@ -10,9 +10,11 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@material/material-color-utilities": "^0.3.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.7.1" "react-router-dom": "^7.7.1",
"react-toastify": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.32.0", "@eslint/js": "^9.32.0",

BIN
public/test.mp4 Normal file

Binary file not shown.

View File

@ -1,7 +1,14 @@
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet, useLocation } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate, Outlet, useLocation } from 'react-router-dom';
import {useContext, useEffect, useState } from "react"; import {useEffect, useState } from "react";
import AuthContext, { AuthProvider } from "./auth/auth-provider" import { ToastContainer } from "react-toastify";
import {
argbFromHex,
themeFromSourceColor,
applyTheme,
} from "@material/material-color-utilities";
import { AuthProvider } from "./auth/auth-provider"
import ping from "./auth/ping"; import ping from "./auth/ping";
import Login from "./pages/login/login" import Login from "./pages/login/login"
@ -14,8 +21,22 @@ import useAuth from './auth/auth';
function App() { function App() {
const theme = themeFromSourceColor(argbFromHex("ffddaf"));
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
applyTheme(theme, { target: document.body, dark: systemDark });
return ( return (
<AuthProvider> <AuthProvider>
<>
<ToastContainer
position="top-right"
autoClose={2000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick={false}
rtl={false}
theme={"light"}
// transition={Bounce}
/>
<Router> <Router>
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
@ -30,13 +51,14 @@ function App() {
<Route path="*" element={<div>404</div>} /> <Route path="*" element={<div>404</div>} />
</Routes> </Routes>
</Router> </Router>
</>
</AuthProvider> </AuthProvider>
); );
} }
const PrivateRoute = () => { const PrivateRoute = () => {
const { token } = useAuth(); const { token, loading } = useAuth();
const location = useLocation(); const location = useLocation();
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
const [isValid, setIsValid] = useState(false); const [isValid, setIsValid] = useState(false);
@ -49,14 +71,15 @@ const PrivateRoute = () => {
return; return;
} }
const result = await ping(token); const result = await ping(token);
console.log(result)
setIsValid(result); setIsValid(result);
setChecked(true); setChecked(true);
}; };
checkAuth(); if (!loading) {
}, [token]); checkAuth();
}
}, [token, loading]);
if (!checked) { if (loading || !checked) {
return <div>Checking server availability...</div>; return <div>Checking server availability...</div>;
} }

View File

@ -4,40 +4,40 @@ import { type JSX, useEffect, useState, createContext } from "react"
type AuthContextType = { type AuthContextType = {
token: string | null; token: string | null;
setToken: (token: string | null) => void; setToken: (token: string | null) => void;
loading: boolean;
}; };
const AuthContext = createContext<AuthContextType>({ const AuthContext = createContext<AuthContextType>({
token: null, token: null,
setToken: () => {}, setToken: () => {},
loading: true,
}); });
export const AuthProvider = ({ children }: { children: JSX.Element }) => { export const AuthProvider = ({ children }: { children: JSX.Element }) => {
const [token, setTokenState] = useState<string | null>(null); const [token, setTokenState] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const savedToken = localStorage.getItem("token"); const savedToken = localStorage.getItem("token");
if (savedToken) { if (savedToken) {
setTokenState(savedToken); setTokenState(savedToken);
console.log("meow")
} }
console.log(savedToken) setLoading(false)
}, []); }, []);
const setToken = (newToken: string | null) => { const setToken = (newToken: string | null) => {
setTokenState(newToken); setTokenState(newToken);
if (newToken) { if (newToken) {
localStorage.setItem("token", newToken); localStorage.setItem("token", newToken);
console.log("saved")
} else { } else {
localStorage.removeItem("token"); localStorage.removeItem("token");
console.log("removed")
} }
}; };
return ( return (
<AuthContext.Provider value={{ token, setToken }}> <AuthContext.Provider value={{ token, setToken, loading }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

View File

@ -1,5 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import { toast } from "react-toastify"
import useAuth from "../../auth/auth"; import useAuth from "../../auth/auth";
@ -12,36 +15,52 @@ const Login = () => {
const from = location.state?.from?.pathname || "/"; const from = location.state?.from?.pathname || "/";
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = () => {
e.preventDefault();
// TODO: request to API // TODO: request to API
if (username === "admin" && password === "1234") { if ((username === "admin" && password === "1234") || (username === "a" && password === "a")) {
const token = "todo.jwt.token"; const token = "todo.jwt.token";
setToken(token); setToken(token);
navigate(from, { replace: true }); navigate(from, { replace: true });
} else { } else {
alert("Wrong login or password"); toast.error("Wrong login or password");
} }
}; };
return ( return (
<div style={{ padding: "20px" }}> <div style={{
display: "flex",
justifyContent: "right",
alignItems: "center",
minHeight: "100vh",
flexDirection: "column",
gap: "15px",
margin: "0 15px"
}}>
<h2>Login</h2> <h2>Login</h2>
<form onSubmit={handleSubmit}> <form onSubmit={(e) => {e.preventDefault(); handleSubmit()}}
id="form-id"
style={{
flexDirection: "column",
display: "flex",
gap: "5px",
width: "100%",
maxWidth: "400px",
alignItems: "center",
}}
>
<div> <div>
<label>Username:</label>
<input <input
type="text" type="text"
placeholder=" Username"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
required required
/> />
</div> </div>
<div> <div>
<label>Password:</label>
<input <input
type="password" type="password"
placeholder=" Password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required