Main project structure #3

Closed
Beesquit wants to merge 17 commits from dev into main
25 changed files with 3995 additions and 0 deletions
Showing only changes of commit 533da219b3 - Show all commits

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>

3429
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"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": {
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.7.1"
},
"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/myautag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

39
src/App.tsx Normal file
View File

@ -0,0 +1,39 @@
import { BrowserRouter as Router, Routes, Route, Navigate, Outlet, useLocation } from 'react-router-dom';
import {useContext } from "react";
import Login from "./pages/login/login"
import Register from "./pages/register/register"
import Home from "./pages/home/home"
import AuthContext, { AuthProvider } from "./auth/auth-provider"
function App() {
return (
<AuthProvider>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route element={<PrivateRoute />}>
<Route path='/' element={<Home />} />
</Route>
</Routes>
</Router>
</AuthProvider>
);
}
const PrivateRoute = () => {
const { isAuthenticated } = useContext(AuthContext);
const location = useLocation();
return (
isAuthenticated === true ?
<Outlet />
:
<Navigate to="/login" state={{ from: location }} replace />
);
}
export default App;

View File

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

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

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

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

@ -0,0 +1,12 @@
import useAuth from "./auth";
const logout = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { setToken: setAuth } = useAuth();
return () => {
setAuth(false);
};
}
export default logout;

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

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

28
src/index.css Normal file
View File

@ -0,0 +1,28 @@
:root {
--background_dark: #242424;
--text_dark: rgba(255, 255, 255, 0.87);
--buttons-main-color: #63001e;
--buttons-press-color: #ff3e78;
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;
display: grid;
place-items: center;
min-width: 320px;
min-height: 100vh;
}

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

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

@ -0,0 +1,31 @@
import { Link } from 'react-router-dom';
import logout from "../../auth/logout"
const Home = () => {
return (
<div>
<h2>Home</h2>
<button onClick={logout()}>
Logout
</button>
<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>
)
}
export default Home;

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

@ -0,0 +1,56 @@
import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import useAuth from "../../auth/auth";
const Login = () => {
const { setToken: setAuth } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || "/home";
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// TODO: request to API
if (username === "admin" && password === "1234") {
setAuth(true);
navigate(from, { replace: true });
} else {
alert("Неверный логин или пароль");
}
};
return (
<div style={{ padding: "20px" }}>
<h2>Login</h2>
<form onSubmit={handleSubmit}>
<div>
<label>Username:</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" style={{ marginTop: "10px" }}>
Login
</button>
</form>
</div>
);
};
export default Login;

View File

@ -0,0 +1,66 @@
div {
margin: auto;
text-align:center;
}
input {
margin: auto;
text-align: center;
}
button {
/* margin: auto; */
background-color: var(--background_dark);
color: var(--text_dark);
border: 2px solid black;
border-color: var(--buttons-main-color);
padding: 16px 32px;
text-align: center;
text-decoration: none;
font-size: 16px;
cursor: pointer;
border-radius: 12px;
transition-duration: 0.4s;
}
button:hover {
background-color: var(--buttons-main-color);
color: var(--text_dark);
}
button:active {
background-color: var(--buttons-press-color);
color: var(--text_dark);
}
button span {
cursor: pointer;
display: inline-block;
position: relative;
transition: 0.5s;
}
button span:after {
/* content: '\00bb'; */
content: '\2317';
position: absolute;
opacity: 0;
top: 0;
right: -20px;
transition: 0.5s;
}
button:hover span {
padding-right: 25px;
}
button:hover span:after {
opacity: 1;
right: 0;
}
.bold-text {
font-weight: bold;
}

View File

@ -0,0 +1,89 @@
import React from "react"
import './login.css'
import { useLocation, useNavigate } from 'react-router-dom';
const Login = () => {
state = {
username: "",
password: "",
}
const { setAuth } = useContext(AuthContext); // используем контекст для получения значений isAuthenticated и setAuth
const navigate = useNavigate(); // используем хук useNavigate для навигации по маршрутам
const location = useLocation(); // используем хук useLocation для получения текущего маршрута
// получаем маршрут, на который нужно перенаправить пользователя после авторизации
const from = location.state?.from?.pathname || '/';
validateString = (text:string) => {
return text ? text : false
}
return (
<div>
<div>
<input
type="text"
name="username"
placeholder="Username"
onChange={(e) => this.setState({username: e.target.value})}
onBlur={(e) => this.setState({username: e.target.value})}
/>
</div>
<div>
<input
type="text"
name="password"
placeholder="Password"
onChange={(e) => this.setState({password: e.target.value})}
onBlur={(e) => this.setState({password: e.target.value})}
color="#FF0000"
/>
</div>
<div className="bold-text">
Your username is: {this.state.username}
</div>
<div>
{
(() => {
const r: string | boolean = this.validateString(this.state.username);
if (r) {
return r
}
return "invalid";
})()
}
</div>
<div className="bold-text">
Your password is: {this.state.password}
</div>
<div>
{
(() => {
const r: string | boolean = this.validateString(this.state.password);
if (r) {
return r
}
return "invalid";
})()
}
</div>
<button
onClick={() =>
this.validateString(this.state.username)
&&
this.validateString(this.state.password)
? false
: alert("Please enter not blank username and password")
}
>
<span>
Login
</span>
</button>
</div>
)
}
export default Login;

View File

@ -0,0 +1,9 @@
const Register = () => {
return (
<div>
<h2>Register</h2>
</div>
)
}
export default Register;

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()],
})