Compare commits

1 Commits
dev ... main

Author SHA1 Message Date
b6c0fec2da Base project structure 2025-09-23 10:55:46 +03:00
44 changed files with 4765 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

23
eslint.config.js Normal file
View File

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/myautag.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Picrinth [#]</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3459
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "picrinth-admin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@material/material-color-utilities": "^0.3.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.7.1",
"react-toastify": "^11.0.5"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^4.7.0",
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.0",
"vite": "^7.1.0"
}
}

BIN
public/login-video.mp4 Normal file

Binary file not shown.

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 MiB

106
src/App.tsx Normal file
View File

@ -0,0 +1,106 @@
import { BrowserRouter as Router, Routes, Route, Outlet, useLocation, useNavigate } from "react-router-dom";
import {useEffect, useState } from "react";
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 useAuth from "./auth/auth";
import Login from "./pages/login/login"
import Register from "./pages/register/register"
import Home from "./pages/home/home"
import Dashboard from "./pages/dashboard/dashboard"
import Users from "./pages/users/users";
import Groups from "./pages/groups/groups";
import Feeds from "./pages/feeds/feeds";
import Pictures from "./pages/pictures/pictures";
import Account from "./pages/acccount/account";
import Settings from "./pages/settings/settings"
const PrivateRoute = () => {
const { token, loading } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [checked, setChecked] = useState(false);
const [isValid, setIsValid] = useState(false);
useEffect(() => {
const checkAuth = async () => {
if (!token) {
setIsValid(false);
setChecked(true);
return;
}
const result = await ping(token);
setIsValid(result);
setChecked(true);
};
if (!loading) {
checkAuth();
}
}, [token, loading]);
if (loading || !checked) {
return <div>Checking server availability...</div>;
}
if (!isValid) {
navigate(`/login?to=${location.pathname}`);
}
else {
return <Outlet />
}
}
function App() {
const theme = themeFromSourceColor(argbFromHex("ffddaf"));
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
applyTheme(theme, { target: document.body, dark: systemDark });
return (
<AuthProvider>
<>
<ToastContainer
position="top-right"
autoClose={2000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick={false}
rtl={false}
theme={systemDark ? "light" : "dark"}
/>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route element={<PrivateRoute />}>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/users" element={<Users />} />
<Route path="/groups" element={<Groups />} />
<Route path="/feeds" element={<Feeds />} />
<Route path="/pictures" element={<Pictures />} />
<Route path="/account" element={<Account />} />
<Route path="/settings" element={<Settings />} />
</Route>
<Route path="*" element={<h1 className="not-found">404 [#]<br/>Not Found</h1>} />
</Routes>
</Router>
</>
</AuthProvider>
);
}
export default App;

View File

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

9
src/auth/auth.tsx Normal file
View File

@ -0,0 +1,9 @@
import { useContext } from "react"
import AuthContext from "./auth-provider"
function useAuth() {
return useContext(AuthContext);
}
export default useAuth;

15
src/auth/logout.tsx Normal file
View File

@ -0,0 +1,15 @@
import { useNavigate } from "react-router-dom";
import useAuth from "./auth";
const useLogout = () => {
const { setToken } = useAuth();
const navigate = useNavigate();
return () => {
setToken(null);
navigate("/login");
};
}
export default useLogout;

10
src/auth/ping.tsx Normal file
View File

@ -0,0 +1,10 @@
const ping = async (token: string | null): Promise<boolean> => {
// TODO: request to API - ping
return new Promise((resolve) => {
setTimeout(() => {
resolve(!!token);
}, 300);
});
};
export default ping;

View File

@ -0,0 +1,53 @@
.form-field {
position: relative;
margin-bottom: 14px;
width: 100%;
}
.form-field input {
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-field input:focus {
outline: none;
border-color: var(--accent-color-lilac);
}
.form-field input.error {
border-color: var(--accent-color-alternative);
}
.error-message {
position: absolute;
bottom: -20px;
left: 0;
color: var(--accent-color-alternative);
font-size: 12px;
opacity: 0;
transform: translateY(10px);
animation: slideUp 0.3s ease forwards;
}
.field-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
color: #a0aec0;
}
.form-field:has(.field-icon) input {
padding-left: 40px;
}
@keyframes slideUp {
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -0,0 +1,99 @@
import React, { useState } from "react";
import "./FormField.css";
interface FormFieldProperties {
type?: "text" | "email" | "password" | "number" | "tel" | "url" | "search";
placeholder?: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
required?: boolean;
errorMessage?: string;
showError?: boolean;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
disabled?: boolean;
autoComplete?: string;
customValidator?: (value: string) => string | null;
showErrorInitially?: boolean;
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}
const FormField: React.FC<FormFieldProperties> = ({
type = "text",
placeholder,
value,
onChange,
required = false,
errorMessage = "Please fill out this field",
showError = false,
onBlur,
onFocus,
className = "",
disabled = false,
autoComplete = "off",
customValidator,
showErrorInitially = false,
icon: Icon,
}) => {
const [touched, setTouched] = useState<boolean>(showErrorInitially);
const [customError, setCustomError] = useState<string>("");
const validateField = (value: string): string => {
if (required && !value) {
return errorMessage;
}
if (customValidator) {
const customValidation = customValidator(value);
if (customValidation) {
return customValidation;
}
}
return "";
};
const shouldShowError = showError || (touched && validateField(value));
const handleBlur = (e: React.FocusEvent<HTMLInputElement>): void => {
setTouched(true);
const error = validateField(value);
setCustomError(error);
onBlur?.(e);
};
const handleFocus = (e: React.FocusEvent<HTMLInputElement>): void => {
setCustomError("");
onFocus?.(e);
};
return (
<div className={`form-field ${className}`}>
{Icon && (
<div className="field-icon">
<Icon />
</div>
)}
<input
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={handleBlur}
onFocus={handleFocus}
required={required}
disabled={disabled}
autoComplete={autoComplete}
className={shouldShowError ? "error" : ""}
/>
{shouldShowError && (
<div className="error-message">
{customError || errorMessage}
</div>
)}
</div>
);
};
export default FormField;

View File

@ -0,0 +1,78 @@
.sidebar {
display: flex;
top: 0;
left: 0;
width: 280px;
height: 100%;
background-color: var(--background-dark);
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.3);
border-right: 1px solid #404040;
flex-direction: column;
}
.sidebar-content {
flex: 1;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid #404040;
background-color: #333333;
}
.sidebar-title {
margin: 0;
font-size: 1.25rem;
color: var(--text-dark);
}
.sidebar-nav {
padding: 1rem 0;
}
.sidebar-bottom {
padding: 0;
border-top: 1px solid #404040;
margin-top: auto;
}
.sidebar-link {
display: block;
padding: 0.75rem 1.5rem;
color: var(--text-dark);
text-decoration: none;
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.sidebar-link:hover {
background-color: #333333;
border-left-color: var(--links-main-color);
color: var(--links-main-color);
}
.logout-button {
display: block;
width: 100%;
padding: 0.75rem 1.5rem;
background: none;
border: none;
text-align: left;
font-family: inherit;
font-size: inherit;
color: var(--text-dark);
text-decoration: none;
cursor: pointer;
transition: color 0.2s ease;
border-left: 3px solid transparent;
}
.logout-button:hover {
color: var(--accent-color-alternative);
background-color: #333333;
border-left-color: var(--accent-color-alternative);
}

View File

@ -0,0 +1,35 @@
import React from "react";
import useLogout from "../auth/logout"
import "./Sidebar.css";
const Sidebar: React.FC = () => {
return (
<div className="sidebar">
<div className="sidebar-content">
<div className="sidebar-header">
<h2 className="sidebar-title">Picrinth [#]</h2>
</div>
<nav className="sidebar-nav">
<a href="/" className="sidebar-link">Home</a>
<a href="/dashboard" className="sidebar-link">Dashboard</a>
<a href="/users" className="sidebar-link">Users</a>
<a href="/groups" className="sidebar-link">Groups</a>
<a href="/feeds" className="sidebar-link">Feeds</a>
<a href="/pictures" className="sidebar-link">Pictures</a>
</nav>
</div>
<div className="sidebar-bottom">
<button onClick={useLogout()} className="sidebar-link logout-button">
Logout
</button>
<a href="/account" className="sidebar-link">Account</a>
<a href="/settings" className="sidebar-link">Settings</a>
</div>
</div>
);
};
export default Sidebar;

56
src/index.css Normal file
View File

@ -0,0 +1,56 @@
:root {
--background-dark: #242424;
--text-dark: rgba(255, 255, 255, 0.87);
--links-main-color: #cc3f69;
--links-hover-color: #ab3659;
--buttons-main-color: #a80032;
--buttons-hover-color: #92002c;
--buttons-press-color: #63001e;
--accent-color-alternative: #ff6b6b;
--accent-color-green: #52b852;
--accent-color-seagreen-bright: #3cb371;
--accent-color-seagreen-dark: #2e8b57;
--accent-color-blue-bright: #4169e1;
--accent-color-blue-dark: #191970;
--accent-color-yellow: #ffd700;
--accent-color-golden: #daa520;
--accent-color-turquoise: #48d1cc;
--accent-color-lilac: #9370db;
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: var(--text-dark);
background-color: var(--background-dark);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
max-width: 100%;
max-height: 100%;
}
.not-found {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

10
src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import "./index.css"
import App from "./App.tsx"
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1,14 @@
.account {
display: flex;
width: 100vw;
height: 100vh;
max-width: 100%;
}
.account-content {
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,15 @@
import "./account.css"
import Sidebar from "../../components/Sidebar";
const Account = () => {
return (
<div className="account">
<Sidebar/>
<div className="account-content">
<h2>Account</h2>
</div>
</div>
)
}
export default Account;

View File

@ -0,0 +1,22 @@
.dashboard {
display: flex;
width: 100vw;
height: 100vh;
max-width: 100%;
}
.dashboard-content {
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}
.dashboard-text {
display: flex;
max-width: 40vw;
text-align: center;
text-wrap: pretty;
outline: 1px solid thistle;
}

View File

@ -0,0 +1,17 @@
import Sidebar from "../../components/Sidebar";
import "./dashboard.css"
const Dashboard = () => {
return (
<div className="dashboard">
<Sidebar/>
<div className="dashboard-content">
<div className="dashboard-text">
<h2>Dashboard Content<br/>WIP</h2>
</div>
</div>
</div>
)
}
export default Dashboard;

14
src/pages/feeds/feeds.css Normal file
View File

@ -0,0 +1,14 @@
.feeds {
display: flex;
width: 100vw;
height: 100vh;
max-width: 100%;
}
.feeds-content {
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}

15
src/pages/feeds/feeds.tsx Normal file
View File

@ -0,0 +1,15 @@
import "./feeds.css"
import Sidebar from "../../components/Sidebar";
const Feeds = () => {
return (
<div className="feeds">
<Sidebar/>
<div className="feeds-content">
<h2>Feeds</h2>
</div>
</div>
)
}
export default Feeds;

View File

@ -0,0 +1,14 @@
.groups {
display: flex;
width: 100vw;
height: 100vh;
max-width: 100%;
}
.groups-content {
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,15 @@
import "./groups.css"
import Sidebar from "../../components/Sidebar";
const Groups = () => {
return (
<div className="groups">
<Sidebar/>
<div className="groups-content">
<h2>Groups</h2>
</div>
</div>
)
}
export default Groups;

14
src/pages/home/home.css Normal file
View File

@ -0,0 +1,14 @@
.home {
display: flex;
width: 100vw;
height: 100vh;
max-width: 100%;
}
.home-content {
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}

33
src/pages/home/home.tsx Normal file
View File

@ -0,0 +1,33 @@
import { Link } from "react-router-dom";
import Sidebar from "../../components/Sidebar";
import "./home.css"
const Home = () => {
return (
<div className="home">
<Sidebar/>
<div className="home-content">
<h2>Home</h2>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/register">Register</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
</ul>
</nav>
</div>
</div>
)
}
export default Home;

106
src/pages/login/login.css Normal file
View File

@ -0,0 +1,106 @@
.login {
display: flex;
width: 100vw;
height: 100vh;
max-width:100%;
justify-content: space-between;
}
.login-left {
overflow: hidden;
display: flex;
background-color: black;
height: 100%;
justify-content: center;
margin-right: auto;
}
.login-right {
display: flex;
height: 100%;
flex-direction: column;
margin-right: 5vw;
margin-left: 5vw;
justify-content: inherit;
}
.login-logo-block {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.login-login-block {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.login-links-block {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
margin-bottom: 2vh;
font-size: xx-small;
}
.login-overlay-text {
position: absolute;
margin-left: 0;
top: 5%;
left: 5%;
z-index: 1;
text-wrap: balance;
max-width: 50vw;
text-shadow: 0px 0px 8px #fff;
}
.login-video {
display: flex;
}
.login-logo {
width: 200px;
height: 200px;
}
.login-button {
margin-top: "10px";
padding: 16px 32px;
font-size: 16px;
border-radius: 12px;
background-color: var(--buttons-main-color);
color: var(--text_dark);
border: none;
cursor: pointer;
text-align: center;
text-decoration: none;
}
.login-button:hover {
background-color: var(--buttons-hover-color);
color: var(--text_dark);
}
.login-button:active {
background-color: var(--buttons-press-color);
color: var(--text_dark);
}
.login-link {
color: var(--links-main-color);
text-decoration: none;
}
.login-link:hover {
color: var(--links-hover-color);
text-decoration: none;
}

116
src/pages/login/login.tsx Normal file
View File

@ -0,0 +1,116 @@
import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { toast } from "react-toastify"
import useAuth from "../../auth/auth";
import FormField from "../../components/FormField"
import "./login.css"
const Login = () => {
const { setToken } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const location = useLocation();
const searchPath = new URLSearchParams(location.search);
const redirectPath = searchPath.get("to") || "/";
const [usernameEmpty, setUsernameEmpty] = useState(false);
const [passwordEmpty, setPasswordEmpty] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setUsernameEmpty(!username);
setPasswordEmpty(!password);
if (!username || !password) {
return;
}
// TODO: request to API - login
if (username === "a" && password === "a") {
const token = "todo.jwt.token";
setToken(token);
navigate(redirectPath, { replace: true });
} else {
toast.error("Wrong login or password");
}
};
return (
<div className="login">
<div className="login-overlay-text">
<h3>
Welcome to Picrinth Web-Admin!<br/>
Picrinth is an open source app for scrolling synchronized<br/>
feeds of pictures from different sources together with your friends<br/>
Enjoy [#]
</h3>
</div>
<div className="login-left">
<video className="login-video" autoPlay loop muted>
<source src={"./login-video.mp4"} type="video/mp4" />
</video>
</div>
<div className="login-right">
<h2 className="login-logo-block">
<img className="login-logo" src="./logo.png" alt="Picrinth logo" />
</h2>
<div className="login-login-block">
<form onSubmit={handleSubmit}
style={{
flexDirection: "column",
display: "flex",
gap: "10px",
alignItems: "center",
}}
noValidate
>
<FormField
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
showError={usernameEmpty}
errorMessage="Please enter your username"
/>
<FormField
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
showError={passwordEmpty}
errorMessage="Please enter your password"
/>
<button type="submit" className="login-button">
Login
</button>
</form>
<h3 style={{textAlign: "center"}}>
<a href={`/register?to=${redirectPath}`} className="login-link">Create account</a>
</h3>
</div>
<div className="login-links-block">
<h2>
<a href="https://git.frik.su/Beesquit/picrinth-admin" className="login-link">Frontend</a>
</h2>
<h2>
<a href="https://git.frik.su/Beesquit/picrinth-server" className="login-link">Backend</a>
</h2>
<h2>
<a href="https://git.frik.su/Beesquit" className="login-link">Author</a>
</h2>
</div>
</div>
</div>
);
};
export default Login;

View File

@ -0,0 +1,14 @@
.pictures {
display: flex;
width: 100vw;
height: 100vh;
max-width: 100%;
}
.pictures-content {
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,15 @@
import "./pictures.css"
import Sidebar from "../../components/Sidebar";
const Pictures = () => {
return (
<div className="pictures">
<Sidebar/>
<div className="pictures-content">
<h2>Pictures</h2>
</div>
</div>
)
}
export default Pictures;

View File

@ -0,0 +1,60 @@
.register {
display: flex;
width: 100vw;
height: 100vh;
max-width:100%;
}
.register-background {
opacity: 0.6;
position: absolute;
z-index: -1;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.register-block {
display: flex;
margin: auto;
flex-direction: column;
justify-content: center;
align-items: center;
}
.register-button {
margin-top: "10px";
padding: 16px 32px;
font-size: 16px;
border-radius: 12px;
background-color: var(--buttons-main-color);
color: var(--text_dark);
border: none;
cursor: pointer;
text-align: center;
text-decoration: none;
}
.register-button:hover {
background-color: var(--buttons-hover-color);
color: var(--text_dark);
}
.register-button:active {
background-color: var(--buttons-press-color);
color: var(--text_dark);
}
.register-link {
color: var(--links-main-color);
text-decoration: none;
}
.register-link:hover {
color: var(--links-hover-color);
text-decoration: none;
}

View File

@ -0,0 +1,88 @@
import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { toast } from "react-toastify"
import useAuth from "../../auth/auth";
import FormField from "../../components/FormField"
import "./register.css"
const Register = () => {
const { setToken } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const location = useLocation();
const searchPath = new URLSearchParams(location.search);
const redirectPath = searchPath.get("to") || "/";
const [usernameEmpty, setUsernameEmpty] = useState(false);
const [passwordEmpty, setPasswordEmpty] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setUsernameEmpty(!username);
setPasswordEmpty(!password);
if (!username || !password) {
return;
}
// TODO: request to API - register
if (username === "user" && password === "user") {
const token = "newusertodo.jwt.token";
setToken(token);
navigate(redirectPath, { replace: true });
} else {
toast.error("Wrong login or password");
}
};
return (
<div className="register">
<img className="register-background" src="register-background.jpg"></img>
<div className="register-block">
<form onSubmit={handleSubmit}
style={{
flexDirection: "column",
display: "flex",
gap: "10px",
alignItems: "center",
}}
noValidate
>
<FormField
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
showError={usernameEmpty}
errorMessage="Please enter the username you want"
/>
<FormField
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
showError={passwordEmpty}
errorMessage="Please enter the password you want"
/>
<button type="submit" className="register-button">
Create account
</button>
<h4>
<a href={`/login?to=${redirectPath}`} className="register-link">Back to login</a>
</h4>
</form>
</div>
</div>
)
}
export default Register;

View File

@ -0,0 +1,14 @@
.settings {
display: flex;
width: 100vw;
height: 100vh;
max-width: 100%;
}
.settings-content {
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,15 @@
import "./settings.css"
import Sidebar from "../../components/Sidebar";
const Settings = () => {
return (
<div className="settings">
<Sidebar/>
<div className="settings-content">
<h2>Settings</h2>
</div>
</div>
)
}
export default Settings;

14
src/pages/users/users.css Normal file
View File

@ -0,0 +1,14 @@
.users {
display: flex;
width: 100vw;
height: 100vh;
max-width: 100%;
}
.users-content {
display: flex;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}

15
src/pages/users/users.tsx Normal file
View File

@ -0,0 +1,15 @@
import "./users.css"
import Sidebar from "../../components/Sidebar";
const Users = () => {
return (
<div className="users">
<Sidebar/>
<div className="users-content">
<h2>Users</h2>
</div>
</div>
)
}
export default Users;

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

27
tsconfig.app.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

25
tsconfig.node.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})