changed project struture.

Also addded a functional JWT token service.

Also added user react frontend
This commit is contained in:
2025-07-23 11:59:59 +02:00
parent 2b4b554c24
commit d552f40c2d
52 changed files with 3807 additions and 31 deletions

24
frontend_admin/.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?

12
frontend_admin/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5001
CMD ["npm", "start"]

69
frontend_admin/README.md Normal file
View File

@@ -0,0 +1,69 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

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
frontend_admin/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/svg+xml" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bikelane - Web</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,67 @@
import React, { useState } from "react";
import "./App.css";
import Header from "./Header";
function App() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await fetch("http://localhost:5002/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
password,
}),
});
if (response.ok) {
console.log("Login successful");
// Handle successful login here
} else {
console.log("Login failed");
// Handle login error here
}
} catch (error) {
console.error("Login error:", error);
}
};
return (
<>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Login</button>
</form>
</>
);
}
export default App;

View File

@@ -0,0 +1,30 @@
import React, { useState } from "react";
import Header from "./components/Header";
import LoginCard from "./components/LoginCard";
function App() {
const [showLogin, setShowLogin] = useState(false);
const handleLoginClick = () => setShowLogin(true);
const handleCloseLogin = () => setShowLogin(false);
const handleLoginSubmit = (username: string, password: string) => {
// Hier kannst du fetch einbauen
alert(`Login: ${username} / ${password}`);
setShowLogin(false);
};
return (
<div className="min-h-screen bg-white">
<Header onLoginClick={handleLoginClick} />
<main className="pt-20 w-full h-full flex flex-col items-center justify-start">
{/* Hier kommt später der Seiteninhalt */}
{showLogin && (
<LoginCard onClose={handleCloseLogin} onSubmit={handleLoginSubmit} />
)}
</main>
</div>
);
}
export default App;

View File

@@ -0,0 +1,29 @@
import React from "react";
import bike from "../../src/assets/bicycle-solid.svg";
import user from "../../src/assets/circle-user-solid.svg";
type HeaderProps = {
onLoginClick: () => void;
};
const Header: React.FC<HeaderProps> = ({ onLoginClick }) => (
<header className="fixed top-0 left-0 w-full h-20 bg-[#101c5e] flex items-center justify-between px-10 z-50 shadow">
<div className="flex items-center gap-3">
<img src={bike} alt="Bike Logo" className="h-10 w-10" />
<span className="text-white text-2xl font-semibold select-none">
Bikelane <b>Web</b>
</span>
</div>
<div className="flex items-center gap-4">
<img src={user} alt="User Icon" className="h-8 w-8" />
<button
onClick={onLoginClick}
className="bg-white text-[#101c5e] font-bold px-5 py-1 rounded-lg text-lg border-2 border-white hover:bg-gray-200 transition"
>
Login
</button>
</div>
</header>
);
export default Header;

View File

@@ -0,0 +1,72 @@
import React, { useRef, useEffect } from "react";
type LoginCardProps = {
onClose: () => void;
onSubmit: (username: string, password: string) => void;
};
const LoginCard: React.FC<LoginCardProps> = ({ onClose, onSubmit }) => {
const [username, setUsername] = React.useState("");
const [password, setPassword] = React.useState("");
const cardRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (cardRef.current && !cardRef.current.contains(e.target as Node)) {
onClose();
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [onClose]);
return (
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-40">
<div
ref={cardRef}
className="bg-gray-200 rounded-xl shadow-lg w-full max-w-md flex flex-col"
>
<div className="bg-[#101c5e] text-white rounded-t-xl px-8 py-4 text-center text-2xl font-bold">
Login
</div>
<form
className="flex flex-col gap-5 px-8 py-6"
onSubmit={(e) => {
e.preventDefault();
onSubmit(username, password);
}}
>
<label className="font-bold">
username
<input
type="text"
className="mt-2 w-full rounded-full px-5 py-2 bg-gray-400 outline-none"
value={username}
onChange={(e) => setUsername(e.target.value)}
autoFocus
required
/>
</label>
<label className="font-bold">
password
<input
type="password"
className="mt-2 w-full rounded-full px-5 py-2 bg-gray-400 outline-none"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</label>
<button
type="submit"
className="bg-[#101c5e] text-white font-bold rounded-lg py-3 mt-6 text-xl hover:bg-[#203080] transition"
>
Login
</button>
</form>
</div>
</div>
);
};
export default LoginCard;

4543
frontend_admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"start": "vite --host 0.0.0.0 --port 5001",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"js-cookie": "^3.0.5",
"lucide-react": "^0.525.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.5"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/js-cookie": "^3.0.6",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4",
"vite-plugin-svgr": "^4.3.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -0,0 +1,58 @@
import "./App.css";
import Layout from "./layout/Layout";
import { useUsers } from "./utils/useUsers";
function App() {
const users = useUsers();
return (
<Layout>
<table className="min-w-full divide-y divide-gray-200 shadow rounded-lg overflow-hidden">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
#
</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Username
</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
First name
</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Last name
</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Email
</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Password
</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Created
</th>
<th className="px-4 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Role
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100">
{users.map((user: any, idx: number) => (
<tr key={user.id} className="hover:bg-blue-50 transition">
<td className="px-4 py-2 whitespace-nowrap">{idx + 1}</td>
<td className="px-4 py-2 whitespace-nowrap">{user.username}</td>
<td className="px-4 py-2 whitespace-nowrap">{user.first_name}</td>
<td className="px-4 py-2 whitespace-nowrap">{user.last_name}</td>
<td className="px-4 py-2 whitespace-nowrap">{user.email}</td>
<td className="px-4 py-2 whitespace-nowrap">{user.password}</td>
<td className="px-4 py-2 whitespace-nowrap">{user.created}</td>
<td className="px-4 py-2 whitespace-nowrap">{user.role}</td>
</tr>
))}
</tbody>
</table>
</Layout>
);
}
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="10" width="10" viewBox="0 0 640 640"><!--!Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M331.7 107.3C336 100.3 343.7 96 352 96L456 96C469.3 96 480 106.7 480 120C480 133.3 469.3 144 456 144L390.4 144L462.6 292.4C473.3 289.5 484.5 288 496 288C566.7 288 624 345.3 624 416C624 486.7 566.7 544 496 544C425.3 544 368 486.7 368 416C368 374 388.2 336.8 419.4 313.4L399 271.5L325.5 418.5C323.2 423.3 319.2 427.3 314.1 429.7C313.5 430 312.9 430.2 312.3 430.4C309.4 431.5 306.4 432 303.4 431.9L271 432C263.1 495.1 209.3 544 144 544C73.3 544 16 486.7 16 416C16 345.3 73.3 288 144 288C154.8 288 165.2 289.3 175.2 291.8L203.7 234.9L192.2 208L152 208C138.7 208 128 197.3 128 184C128 170.7 138.7 160 152 160L208 160C217.6 160 226.3 165.7 230.1 174.5L244.4 208L368.1 208L330.4 130.5C326.8 123.1 327.2 114.3 331.6 107.3zM228.5 292.7L182.9 384L267.7 384L228.6 292.7zM305.7 351L353.2 256L265 256L305.7 351zM474.4 426.5L444.7 365.5C431.9 378.5 424 396.3 424 416C424 455.8 456.2 488 496 488C535.8 488 568 455.8 568 416C568 376.2 535.8 344 496 344C493.3 344 490.5 344.2 487.9 344.5L517.6 405.5C523.4 417.4 518.4 431.8 506.5 437.6C494.6 443.4 480.2 438.4 474.4 426.5zM149.2 432C129 432 115.8 410.7 124.9 392.6L149.1 344.1C147.4 344 145.7 343.9 144 343.9C104.2 343.9 72 376.1 72 415.9C72 455.7 104.2 487.9 144 487.9C178.3 487.9 206.9 464 214.2 431.9L149.2 431.9z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#ffffff" d="M312 32c-13.3 0-24 10.7-24 24s10.7 24 24 24l25.7 0 34.6 64-149.4 0-27.4-38C191 99.7 183.7 96 176 96l-56 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l43.7 0 22.1 30.7-26.6 53.1c-10-2.5-20.5-3.8-31.2-3.8C57.3 224 0 281.3 0 352s57.3 128 128 128c65.3 0 119.1-48.9 127-112l49 0c8.5 0 16.3-4.5 20.7-11.8l84.8-143.5 21.7 40.1C402.4 276.3 384 312 384 352c0 70.7 57.3 128 128 128s128-57.3 128-128s-57.3-128-128-128c-13.5 0-26.5 2.1-38.7 6L375.4 48.8C369.8 38.4 359 32 347.2 32L312 32zM458.6 303.7l32.3 59.7c6.3 11.7 20.9 16 32.5 9.7s16-20.9 9.7-32.5l-32.3-59.7c3.6-.6 7.4-.9 11.2-.9c39.8 0 72 32.2 72 72s-32.2 72-72 72s-72-32.2-72-72c0-18.6 7-35.5 18.6-48.3zM133.2 368l65 0c-7.3 32.1-36 56-70.2 56c-39.8 0-72-32.2-72-72s32.2-72 72-72c1.7 0 3.4 .1 5.1 .2l-24.2 48.5c-9 18.1 4.1 39.4 24.3 39.4zm33.7-48l50.7-101.3 72.9 101.2-.1 .1-123.5 0zm90.6-128l108.5 0L317 274.8 257.4 192z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M463 448.2C440.9 409.8 399.4 384 352 384L288 384C240.6 384 199.1 409.8 177 448.2C212.2 487.4 263.2 512 320 512C376.8 512 427.8 487.3 463 448.2zM64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576C178.6 576 64 461.4 64 320zM320 336C359.8 336 392 303.8 392 264C392 224.2 359.8 192 320 192C280.2 192 248 224.2 248 264C248 303.8 280.2 336 320 336z"/></svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#ffffff" d="M399 384.2C376.9 345.8 335.4 320 288 320l-64 0c-47.4 0-88.9 25.8-111 64.2c35.2 39.2 86.2 63.8 143 63.8s107.8-24.7 143-63.8zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm256 16a72 72 0 1 0 0-144 72 72 0 1 0 0 144z"/></svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -0,0 +1,41 @@
import { useState } from "react";
import React from "react";
import LoginCard from "./LoginCard";
import { greeting } from "../utils/functions";
const Header: React.FC = () => {
const [loginCardVisible, setLoginCardVisible] = useState(false);
const closeLoginCard = () => {
setLoginCardVisible(false);
};
let loginBtnVal: string = "Hello, " + greeting() + "!";
return (
<header className="bg-blue-600 text-white p-4 shadow-md">
<div className="container mx-auto flex justify-between items-center">
<h1 className="text-xl font-bold">
Bikelane <strong>Admin Panel</strong>
</h1>
<nav>
<ul className="flex space-x-4">
<li>
<a className="hover:underline">
<button
onClick={() => setLoginCardVisible(true)}
className="bg-blue-700 shadow-md hover:bg-blue-800 transition padding px-4 py-2 rounded-md text-white font-semibold"
>
{loginBtnVal ?? "Login"}
</button>
</a>
</li>
</ul>
</nav>
</div>
<div>{loginCardVisible && <LoginCard onClose={closeLoginCard} />}</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,119 @@
import React from "react";
import Cookies from "js-cookie";
import { logout } from "../utils/functions";
type LoginCardProps = {
onClose: () => void;
};
const LoginCard: React.FC<LoginCardProps> = ({ onClose }) => {
return (
<div className="fixed inset-0 flex items-center justify-center bg-black/35">
<div className="max-w-sm bg-white rounded-xl shadow-md p-8 relative">
<button
className="absolute top-4 right-4 bg-red-500 text-white w-8 h-8 rounded-full flex items-center justify-center font-bold shadow hover:bg-red-600 transition focus:outline-none focus:ring-2 focus:ring-red-400"
onClick={onClose}
aria-label="Close"
>
&times;
</button>
<h2 className="text-black text-2xl font-bold mb-6 text-center">
Login
</h2>
<form
onSubmit={async (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const username = formData.get("username");
const password = formData.get("password");
// Example: send login request
await fetch("http://localhost:5002/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
})
.then(async (response) => {
if (response.ok) {
const data = await response.json();
Cookies.set("token", data.token, { expires: 7 });
onClose();
Cookies.set("name", data.user.first_name, { expires: 7 });
await fetch("http://localhost:5002/api/getAllUsers", {
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
})
.then((res) => res.json())
.then((users) => {
localStorage.setItem("users", JSON.stringify(users));
});
document.location.reload();
} else if (response.status === 401) {
alert("Invalid credentials");
}
})
.catch((error) => {
console.log("Login failed: ", error);
});
}}
className="space-y-4 text-black"
>
<div>
<label
htmlFor="username"
className="block text-sm font-medium text-gray-700 mb-1"
>
Username
</label>
<input
type="text"
name="username"
id="username"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 mb-1"
>
Password
</label>
<input
type="password"
name="password"
id="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{Cookies.get("name") ? (
<p></p>
) : (
<input
type="submit"
value="Login"
className="w-full bg-blue-600 text-white font-semibold py-2 rounded-md hover:bg-blue-700 transition"
/>
)}
</form>
{Cookies.get("name") ? (
<button
className="w-full bg-black text-white font-semibold py-2 rounded-md hover:bg-green-400 transition"
onClick={logout}
>
Logout
</button>
) : (
<p className="text-center text-gray-500 mt-4">
Don't have an account?{" "}
<a href="/register" className="text-blue-600 hover:underline">
Register here
</a>
</p>
)}
</div>
</div>
);
};
export default LoginCard;

View File

@@ -0,0 +1,37 @@
import React from "react";
import userIcon from "../assets/circle-user-solid-full.svg";
import logoIcon from "../assets/bicycle-solid-full.svg";
const Sidebar: React.FC = () => (
<aside className="w-72 bg-white/90 shadow-2xl rounded-r-3xl p-8 flex flex-col gap-10 border-r border-blue-100">
<div className="flex items-center gap-4 mb-10">
<img
src={logoIcon}
alt="Bikelane Logo"
className="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center shadow-lg"
/>
<span className="font-extrabold text-2xl text-blue-700 tracking-wide drop-shadow">
Bikelanes
</span>
</div>
<nav>
<ul className="space-y-6">
<li>
<a
href="/#"
className="flex items-center gap-3 px-4 py-3 rounded-xl text-blue-700 font-medium bg-blue-50 hover:bg-blue-200 hover:text-blue-900 transition-all shadow-sm"
>
<img src={userIcon} alt="Users" className="w-6 h-6" />
<span className="text-lg">Users</span>
</a>
</li>
{/* Add more links as needed */}
</ul>
</nav>
<div className="mt-auto text-xs text-blue-300 text-center pt-6 border-t border-blue-100">
<span>Bikelane pre-0.1</span>
</div>
</aside>
);
export default Sidebar;

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -0,0 +1,53 @@
import React from "react";
import Header from "../components/Header";
import userIcon from "../assets/circle-user-solid-full.svg";
import logoIcon from "../assets/bicycle-solid-full.svg";
import Cookies from "js-cookie"; // Add this import
import Sidebar from "../components/Sidebar";
type LayoutProps = {
children: React.ReactNode;
};
const Layout: React.FC<LayoutProps> = ({ children }) => {
const isLoggedIn = !!Cookies.get("name"); // Check login status
return (
<div className="flex flex-col min-h-screen bg-gradient-to-br from-blue-50 via-blue-100 to-blue-200">
<Header />
<div className="flex flex-1">
{isLoggedIn && (
<>
{/* Sidebar */}
<Sidebar />
{/* Main content */}
<main className="flex-1 p-10 bg-white/80 rounded-l-3xl shadow-2xl m-6 overflow-auto">
{children}
</main>
</>
)}
</div>
<footer className="bg-gradient-to-r from-blue-800 via-blue-900 to-blue-800 text-blue-100 py-6 px-5 text-center rounded-t-3xl shadow-xl mt-8 tracking-wide border-t border-blue-700">
<div className="flex flex-col items-center gap-2">
<span className="font-bold text-lg tracking-widest drop-shadow">
Bikelane Web
</span>
<span className="text-xs text-blue-200">
&copy; {new Date().getFullYear()}
<a
href="https://git.the1s.de/theis.gaedigk/bikelane"
className="underline hover:text-blue-300 transition"
target="_blank"
rel="noopener noreferrer"
>
Gitea Repository
</a>
<span className="ml-1">| Fork me please 🚲</span>
</span>
</div>
</footer>
</div>
);
};
export default Layout;

View File

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

View File

@@ -0,0 +1,12 @@
import Cookies from "js-cookie";
export const greeting = () => {
return Cookies.get("name") ?? "Login";
};
export const logout = () => {
Cookies.remove("name");
Cookies.remove("token");
localStorage.removeItem("users");
window.location.reload();
};

View File

@@ -0,0 +1,29 @@
import { useState, useEffect } from "react";
export interface User {
id: number;
username: string;
first_name: string;
last_name: string;
email: string;
password: string;
created: string;
}
export function useUsers(): User[] {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
const data = localStorage.getItem("users");
if (data) {
try {
const parsed = JSON.parse(data);
setUsers(parsed.result || []);
} catch {
setUsers([]);
}
}
}, []);
return users;
}

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

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

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", "other/components", "other/App.tsx"]
}

View File

@@ -0,0 +1,14 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}

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"]
}

View File

@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), svgr(), tailwindcss()],
server: {
host: "0.0.0.0",
port: 5001,
watch: {
usePolling: true,
},
},
});