Base project structure
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
23
eslint.config.js
Normal 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
13
index.html
Normal 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
3459
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal 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
BIN
public/login-video.mp4
Normal file
Binary file not shown.
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/register-background.jpg
Normal file
BIN
public/register-background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.7 MiB |
106
src/App.tsx
Normal file
106
src/App.tsx
Normal 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;
|
||||||
46
src/auth/auth-provider.tsx
Normal file
46
src/auth/auth-provider.tsx
Normal 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
9
src/auth/auth.tsx
Normal 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
15
src/auth/logout.tsx
Normal 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
10
src/auth/ping.tsx
Normal 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;
|
||||||
53
src/components/FormField.css
Normal file
53
src/components/FormField.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/components/FormField.tsx
Normal file
99
src/components/FormField.tsx
Normal 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;
|
||||||
78
src/components/Sidebar.css
Normal file
78
src/components/Sidebar.css
Normal 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);
|
||||||
|
}
|
||||||
35
src/components/Sidebar.tsx
Normal file
35
src/components/Sidebar.tsx
Normal 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
56
src/index.css
Normal 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
10
src/main.tsx
Normal 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>,
|
||||||
|
)
|
||||||
14
src/pages/acccount/account.css
Normal file
14
src/pages/acccount/account.css
Normal 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;
|
||||||
|
}
|
||||||
15
src/pages/acccount/account.tsx
Normal file
15
src/pages/acccount/account.tsx
Normal 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;
|
||||||
22
src/pages/dashboard/dashboard.css
Normal file
22
src/pages/dashboard/dashboard.css
Normal 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;
|
||||||
|
}
|
||||||
17
src/pages/dashboard/dashboard.tsx
Normal file
17
src/pages/dashboard/dashboard.tsx
Normal 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
14
src/pages/feeds/feeds.css
Normal 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
15
src/pages/feeds/feeds.tsx
Normal 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;
|
||||||
14
src/pages/groups/groups.css
Normal file
14
src/pages/groups/groups.css
Normal 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;
|
||||||
|
}
|
||||||
15
src/pages/groups/groups.tsx
Normal file
15
src/pages/groups/groups.tsx
Normal 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
14
src/pages/home/home.css
Normal 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
33
src/pages/home/home.tsx
Normal 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
106
src/pages/login/login.css
Normal 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
116
src/pages/login/login.tsx
Normal 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;
|
||||||
14
src/pages/pictures/pictures.css
Normal file
14
src/pages/pictures/pictures.css
Normal 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;
|
||||||
|
}
|
||||||
15
src/pages/pictures/pictures.tsx
Normal file
15
src/pages/pictures/pictures.tsx
Normal 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;
|
||||||
60
src/pages/register/register.css
Normal file
60
src/pages/register/register.css
Normal 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;
|
||||||
|
}
|
||||||
88
src/pages/register/register.tsx
Normal file
88
src/pages/register/register.tsx
Normal 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;
|
||||||
14
src/pages/settings/settings.css
Normal file
14
src/pages/settings/settings.css
Normal 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;
|
||||||
|
}
|
||||||
15
src/pages/settings/settings.tsx
Normal file
15
src/pages/settings/settings.tsx
Normal 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
14
src/pages/users/users.css
Normal 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
15
src/pages/users/users.tsx
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal 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
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal 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
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user