Release - Upload
This commit is contained in:
@@ -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?
|
||||
@@ -0,0 +1,8 @@
|
||||
# React + 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/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
@@ -0,0 +1,38 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import react from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Technische Hochschule Nuernberg - Pruefplan Ansicht </title>
|
||||
<!-- Bootstrap CSS Link -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<!-- Fontawesome and Bootstrap Icons -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"target": "ES2022",
|
||||
"jsx": "react",
|
||||
"allowImportingTsExtensions": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-build-classic": "^40.2.0",
|
||||
"@ckeditor/ckeditor5-react": "^6.2.0",
|
||||
"axios": "^1.6.5",
|
||||
"bootstrap": "^5.3.2",
|
||||
"chart.js": "^4.4.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-photo-album": "^2.3.0",
|
||||
"react-player": "^2.14.1",
|
||||
"react-rater": "^6.0.5",
|
||||
"react-router-dom": "6.10.0",
|
||||
"sweetalert2": "^11.7.32",
|
||||
"yet-another-react-lightbox": "^3.14.0",
|
||||
"zustand": "^4.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"prettier": "^3.0.3",
|
||||
"simple-zustand-devtools": "^1.1.0",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,58 @@
|
||||
/* #root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
} */
|
||||
|
||||
|
||||
.dropdown-card{
|
||||
position: relative;
|
||||
top: -500px;
|
||||
transition: top 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
.dropdown-card.visible{
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.custom-btn-hover:hover {
|
||||
background-color: #c72427cc !important; /* Keep original background */
|
||||
color: white !important;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// import { useState } from 'react'
|
||||
// import reactLogo from './assets/react.svg'
|
||||
// import viteLogo from '/vite.svg'
|
||||
// import './App.css'
|
||||
|
||||
import { Route, Routes, BrowserRouter } from "react-router-dom"
|
||||
import MainWrapper from "./layouts/MainWrapper"
|
||||
import PrivateRoute from "./layouts/PrivateRoute"
|
||||
|
||||
|
||||
import Homepage from "./views/Homepage"
|
||||
import Mainpage from "./views/Mainpage"
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
{/* Everything in here will be routed by the BrowserRouter from react */}
|
||||
<MainWrapper>
|
||||
{/* Everything in here will be wrapped with the MainWrapper and is the children of it */}
|
||||
<Routes>
|
||||
{/* These are the differrent pages to route to -> like path... in django */}
|
||||
<Route path="/" element={<Homepage />}/>
|
||||
<Route path="/mainpage/" element={<Mainpage />}/>
|
||||
</Routes>
|
||||
</MainWrapper>
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default App
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,59 @@
|
||||
import Table from 'react-bootstrap/Table';
|
||||
import { useState,useEffect } from 'react';
|
||||
|
||||
|
||||
|
||||
|
||||
const EventTable = () => {
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Sample data for Testing
|
||||
const sampleData = [
|
||||
{
|
||||
event: "System Update",
|
||||
date: "2025-01-01T12:00:00Z",
|
||||
},
|
||||
{
|
||||
event: "User Login",
|
||||
date: "2025-01-02T09:30:00Z",
|
||||
},
|
||||
{
|
||||
event: "Password Reset",
|
||||
date: "2025-01-03T15:45:00Z",
|
||||
},
|
||||
];
|
||||
setData(sampleData)
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container d-flex flex-column" style={{ marginTop: "10px" }}>
|
||||
<Table responsive>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Datum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length > 0 ? (
|
||||
data.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td>{item.event}</td>
|
||||
<td>{item.date}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" className="text-center">No data available</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default EventTable
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Alert } from 'bootstrap';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import "../App.css"
|
||||
import EventTable from './EventTable';
|
||||
|
||||
|
||||
|
||||
const LogbookCard = ({isVisible}) => {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`dropdown-card ${isVisible ? "visible" : ""} z-2 m-1`} >
|
||||
|
||||
<div className='position-absolute m-1'>
|
||||
<div className='card shadow' style={{ width: "30rm" }}>
|
||||
<h5 className='card-header'> Logbuch </h5>
|
||||
<div className='card-body'>
|
||||
<h6> Uploads & Änderungen </h6>
|
||||
<EventTable/>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogbookCard
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
||||
import {login} from '../utils/auth'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
|
||||
const LoginForm = () => {
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const navigate = useNavigate() // Hook for navigation handling
|
||||
|
||||
const handleSubmit = async (e) =>{
|
||||
e.preventDefault() //placeholder are invalide
|
||||
setIsLoading(true)
|
||||
|
||||
const {error} = await login(username, password)
|
||||
if (error){
|
||||
alert(error)
|
||||
setIsLoading(false)
|
||||
}else{
|
||||
navigate("/mainpage/")
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="container d-flex flex-column" style={{ marginTop: "50px" }}>
|
||||
<div className="row align-items-center justify-content-center g-0 h-lg-100 py-8">
|
||||
<div className="col-lg-5 col-md-8 py-8 py-xl-0">
|
||||
<div className="card shadow">
|
||||
<div className="card-body p-6">
|
||||
<div className="mb-4">
|
||||
<h1 className="mb-1 fw-bold" style={{ color: "#c72426" }}>Anmelden</h1>
|
||||
<p> Als Studierende der Th-Nuernber mit Ihren Rechner-Zugangsdaten</p>
|
||||
</div>
|
||||
{/* Form */}
|
||||
<form className="needs-validation" noValidate="" onSubmit={handleSubmit}>
|
||||
{/* Username */}
|
||||
<div className="mb-3">
|
||||
<label htmlFor="text" className="form-label">
|
||||
Anmeldename
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
className="form-control"
|
||||
name="username"
|
||||
placeholder="mustermannma12345"
|
||||
required=""
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<div className="invalid-feedback">
|
||||
Please enter valid username.
|
||||
</div>
|
||||
</div>
|
||||
{/* Password */}
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="form-label">
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
className="form-control"
|
||||
name="password"
|
||||
placeholder="**************"
|
||||
required=""
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<div className="invalid-feedback">
|
||||
Please enter valid password.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="d-grid">
|
||||
{/* Change the submit button according to the loading state -> depending on the backend authentication */}
|
||||
{isLoading === true && (
|
||||
<button disabled type="submit" className="btn btn-outline-secondary">
|
||||
Proccessing <i className='fas fa-spinner fa-spin'></i>
|
||||
</button>
|
||||
)}
|
||||
{isLoading === false && (
|
||||
<button className="btn btn-outline-secondary custom-btn-hover" type="submit" style={{ color: "#c72426" }}>
|
||||
Login <i className='fas fa-sign-in-alt'></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginForm
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
||||
import {login} from '../utils/auth'
|
||||
import { useState, useEffect } from 'react'
|
||||
import "../App.css"
|
||||
|
||||
|
||||
const MartNrForm = ( {onSubmit} ) => {
|
||||
const [martNr, setMartNr] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async (e) =>{
|
||||
e.preventDefault() //placeholder are invalide
|
||||
setIsLoading(true)
|
||||
|
||||
onSubmit(martNr)
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="container d-flex flex-column" style={{ marginTop: "50px" }}>
|
||||
<div className="row align-items-center justify-content-center g-0 h-lg-100 py-8">
|
||||
<div className="col-lg-5 col-md-8 py-8 py-xl-0">
|
||||
<div className="card shadow" >
|
||||
<div className="card-body p-6">
|
||||
<div className="mb-4">
|
||||
<h1 className="mb-1 fw-bold" style={{ color: "#c72426" }}>Martrikelnummer</h1>
|
||||
<p> Geben Sie Ihre Martirkelnummer ein um nur Ihre Prüfungen zu sehen </p>
|
||||
</div>
|
||||
{/* Form */}
|
||||
<form className="needs-validation" noValidate="" onSubmit={handleSubmit}>
|
||||
{/* Username */}
|
||||
<div className="mb-3">
|
||||
<input
|
||||
type="text"
|
||||
id="martrikelnummer"
|
||||
className="form-control"
|
||||
name="martrikelnummer"
|
||||
placeholder="3634521"
|
||||
required=""
|
||||
onChange={(e) => setMartNr(e.target.value)}
|
||||
/>
|
||||
<div className="invalid-feedback">
|
||||
Please enter valid username.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="d-grid">
|
||||
{/* Change the submit button according to the loading state -> depending on the backend authentication */}
|
||||
{isLoading === true && (
|
||||
<button disabled type="submit" className="btn btn-outline-secondary">
|
||||
Proccessing <i className='fas fa-spinner fa-spin'></i>
|
||||
</button>
|
||||
)}
|
||||
{isLoading === false && (
|
||||
<button className="btn btn-outline-secondary custom-btn-hover" type="submit" style={{ color: "#c72426" }}>
|
||||
Submit <i className='fas fa-sign-in-alt'></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MartNrForm
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Link, useNavigate} from "react-router-dom";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import jwt_decode from "jwt-decode"
|
||||
import Coockie from "js-cookie"
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import logo from '/src/assets/th_logo_big.jpg';
|
||||
import {logout} from '../utils/auth'
|
||||
import PruefPlanTable from "./PruefplanTable";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function Navbar({setSettingsVisibility, setLogbookVisibility}) {
|
||||
|
||||
const isVisible = useAuthStore((state) => state.isLoggedIn());
|
||||
const userName = useAuthStore((state) => state.user().user_name)
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
const handleSettingsButton = () => {
|
||||
setSettingsVisibility((prev) => {
|
||||
const newState = !prev
|
||||
return newState
|
||||
})
|
||||
}
|
||||
const handleLogbookButton = () => {
|
||||
setLogbookVisibility((prev) => {
|
||||
const newState = !prev
|
||||
return newState
|
||||
})
|
||||
}
|
||||
const handleLogout = async (e) =>{
|
||||
e.preventDefault() //placeholder are invalide
|
||||
|
||||
const {error} = await logout()
|
||||
if (error){
|
||||
alert(error)
|
||||
}else{
|
||||
navigate("/")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<nav className="navbar navbar-expand-sm bg-body-tertiary sticky-top">
|
||||
<div className="container-fluid">
|
||||
<a className="navbar-brand" href="https://www.th-nuernberg.de/" target="_blank" rel="noopener noreferrer">
|
||||
<img src={logo} alt="th-nuernberg-logo" style={{ height: 50, width: "auto", objectFit: "contain", borderRadius: 10 }}/>
|
||||
</a>
|
||||
|
||||
{/* Button that appears in collapsed state */}
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
|
||||
{/* Styling for the button -> Burger menu */}
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
|
||||
{/* Section that collapses in the button menu */}
|
||||
<div className="collapse navbar-collapse" id="navbarNav">
|
||||
{
|
||||
isVisible && (
|
||||
<>
|
||||
{/*List items aligned left with the brand */}
|
||||
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li className="nav-item mx-2">
|
||||
<button className="btn btn-outline-secondary" type="submit" onClick={handleSettingsButton}>
|
||||
<i className="fa-solid fa-gear fa-lg me-2"></i>
|
||||
Einstellungen
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav-item mx-2">
|
||||
<button className="btn btn-outline-secondary" type="submit" onClick={handleLogbookButton}>
|
||||
<i className="fa-solid fa-book fa-lg me-2"></i>
|
||||
Logbuch
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
isVisible && (
|
||||
<form className="d-flex" role="button" onSubmit={handleLogout}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Username"
|
||||
value={userName}
|
||||
disabled
|
||||
/>
|
||||
<button className="btn btn-outline-secondary mx-1" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
export default Navbar;
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import apiInstance from "../utils/axios"
|
||||
|
||||
|
||||
const PruefPlanTable = ({ martNr, columnVisibility }) => {
|
||||
|
||||
const [allData, setAllData] = useState([])
|
||||
const [data, setData] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState(null);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [today, setToday] = useState("")
|
||||
const [loading, isLoading] = useState(true);
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const fetchPruefplanData = async () => {
|
||||
isLoading(true)
|
||||
|
||||
try {
|
||||
|
||||
const { data, status } = await apiInstance.get('api/fetchExamData', {})
|
||||
|
||||
const formattedData = data.map(item => ({
|
||||
...item,
|
||||
date: new Date(item.date).toLocaleDateString("de-DE", {
|
||||
year: "2-digit",
|
||||
month: "2-digit",
|
||||
day: "2-digit"
|
||||
})
|
||||
}));
|
||||
|
||||
setAllData(formattedData)
|
||||
|
||||
|
||||
} catch (error) {
|
||||
setError(error.message)
|
||||
}
|
||||
finally {
|
||||
isLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchPruefplanData();
|
||||
}, [])
|
||||
|
||||
|
||||
const handleFilterToday = () => {
|
||||
if (today === "") {
|
||||
setToday(getTodayDate)
|
||||
}
|
||||
else {
|
||||
setToday("")
|
||||
}
|
||||
}
|
||||
|
||||
const makeDataUnique = (dataSet) => {
|
||||
const seen = new Set()
|
||||
return dataSet.filter((data) => {
|
||||
const uniqueKey = `${data.examIdent}`
|
||||
// const uniqueKey = `${data.date}-${data.time}-${data.weekday}-${data.room}-${data.location}-${data.subjectIdent}-${data.examIdent}`
|
||||
if (seen.has(uniqueKey)) {
|
||||
return false
|
||||
}
|
||||
seen.add(uniqueKey)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let filtered = allData;
|
||||
|
||||
if (martNr) {
|
||||
filtered = filtered.filter(item => item.studMartNr === martNr);
|
||||
}
|
||||
|
||||
filtered = makeDataUnique(filtered)
|
||||
|
||||
if (today) {
|
||||
filtered = filtered.filter(item => item.date === today)
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
filtered = filtered.filter(
|
||||
item =>
|
||||
item.date.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.time.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.weekday.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.room.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.location.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.subjectIdent.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.examIdent.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.supervisor.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
item.examStudCount.toLowerCase().includes(filter.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
setData(filtered);
|
||||
}, [martNr, filter, today, allData]);
|
||||
|
||||
// Function to get today's date in "dd.mm.yyyy" format
|
||||
const getTodayDate = () => {
|
||||
const today = new Date();
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const year = today.getFullYear();
|
||||
return `${day}.${month}.${year}`;
|
||||
};
|
||||
|
||||
const parseDateTime = (dateStr, timeStr) => {
|
||||
const [day, month, year] = dateStr.split(".").map(Number);
|
||||
const [hour, minute] = timeStr.split(":").map(Number);
|
||||
return new Date(year, month - 1, day, hour, minute);
|
||||
};
|
||||
|
||||
// Function to handle sorting
|
||||
const sortData = (key) => {
|
||||
let sortedData = [...data];
|
||||
if (key === "date") {
|
||||
sortedData.sort((a, b) => {
|
||||
const dateA = parseDateTime(a.date, "00:00");
|
||||
const dateB = parseDateTime(b.date, "00:00");
|
||||
return sortConfig?.key === key && sortConfig.direction === "asc"
|
||||
? dateB - dateA
|
||||
: dateA - dateB;
|
||||
});
|
||||
} else if (key === "time") {
|
||||
sortedData.sort((a, b) => {
|
||||
const dateA = parseDateTime("01.01.2000", a.time);
|
||||
const dateB = parseDateTime("01.01.2000", b.time);
|
||||
return sortConfig?.key === key && sortConfig.direction === "asc"
|
||||
? dateB - dateA
|
||||
: dateA - dateB;
|
||||
});
|
||||
} else {
|
||||
sortedData.sort((a, b) => {
|
||||
return sortConfig?.key === key && sortConfig.direction === "asc"
|
||||
? b[key].localeCompare(a[key])
|
||||
: a[key].localeCompare(b[key]);
|
||||
});
|
||||
}
|
||||
|
||||
setSortConfig((prev) => ({
|
||||
key,
|
||||
direction: prev?.key === key && prev.direction === "asc" ? "desc" : "asc",
|
||||
}));
|
||||
setData(sortedData);
|
||||
};
|
||||
|
||||
// Function to format date and time to YYYYMMDDTHHMMSS for ICS
|
||||
const formatICSTime = (date, time, addMinutes = 0) => {
|
||||
const [day, month, year] = date.split('.').map(Number);
|
||||
const fullYear = year < 100 ? 2000 + year : year; // Convert YY to YYYY
|
||||
const [hour, minute] = time.split(':').map(Number);
|
||||
|
||||
const eventDate = new Date(fullYear, month - 1, day, hour, minute);
|
||||
eventDate.setMinutes(eventDate.getMinutes() + addMinutes); // Add minutes dynamically
|
||||
|
||||
return eventDate.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
|
||||
};
|
||||
|
||||
// Function to generate and download ICS file
|
||||
const exportToCalendar = () => {
|
||||
let icsContent = `BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Your Company//NONSGML v1.0//EN\n`;
|
||||
|
||||
data.forEach((item) => {
|
||||
const startDate = formatICSTime(item.date, item.time);
|
||||
const endDate = formatICSTime(item.date, item.time, 90); // Default duration: 90 minutes
|
||||
|
||||
icsContent += `BEGIN:VEVENT\n`;
|
||||
icsContent += `UID:${item.date.replace(/\./g, '')}-${item.time.replace(':', '')}-${Math.random()
|
||||
.toString(36).substr(2, 9)}@pruefplan.com\n`;
|
||||
icsContent += `DTSTAMP:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"}\n`;
|
||||
icsContent += `DTSTART:${startDate}\n`;
|
||||
icsContent += `DTEND:${endDate}\n`;
|
||||
icsContent += `SUMMARY:${item.subjectIdent} Prüfung\n`;
|
||||
icsContent += `LOCATION:${item.location}\n`;
|
||||
icsContent += `DESCRIPTION:Prüfung für ${item.subjectIdent} in Raum ${item.room}\n`;
|
||||
icsContent += `END:VEVENT\n`;
|
||||
});
|
||||
|
||||
icsContent += `END:VCALENDAR`;
|
||||
|
||||
const blob = new Blob([icsContent], { type: "text/calendar" });
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = "Pruefungsplan.ics";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suche..."
|
||||
className="form-control"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="table-responsive-lg" style={{ maxHeight: "25em", overflowY: 'auto' }}>
|
||||
|
||||
<table className="table table-light table-striped table-hover">
|
||||
<thead style={{ position: "sticky", top: 0, backgroundColor: "white", zIndex: 2 }}>
|
||||
<tr>
|
||||
|
||||
{columnVisibility.date &&
|
||||
<th scope="col" onClick={() => sortData("date")} style={{ cursor: "pointer" }}>
|
||||
Datum {sortConfig?.key === "date" && (sortConfig.direction === "asc" ? "↑" : "↓")}
|
||||
</th>}
|
||||
|
||||
{columnVisibility.time &&
|
||||
<th scope="col" onClick={() => sortData("time")} style={{ cursor: "pointer" }}>
|
||||
Zeit {sortConfig?.key === "time" && (sortConfig.direction === "asc" ? "↑" : "↓")}
|
||||
</th>}
|
||||
|
||||
{columnVisibility.weekday &&
|
||||
<th scope="col" onClick={() => sortData("weekday")} style={{ cursor: "pointer" }}>
|
||||
Wochentag {sortConfig?.key === "weekday" && (sortConfig.direction === "asc" ? "↑" : "↓")}
|
||||
</th>}
|
||||
|
||||
{columnVisibility.room &&
|
||||
<th scope="col" onClick={() => sortData("room")} style={{ cursor: "pointer" }}>
|
||||
Raum {sortConfig?.key === "room" && (sortConfig.direction === "asc" ? "↑" : "↓")}
|
||||
</th>}
|
||||
|
||||
{columnVisibility.location &&
|
||||
<th scope="col" onClick={() => sortData("location")} style={{ cursor: "pointer" }}>
|
||||
Ort {sortConfig?.key === "location" && (sortConfig.direction === "asc" ? "↑" : "↓")}
|
||||
</th>}
|
||||
|
||||
{columnVisibility.subjectIdent &&
|
||||
<th scope="col" onClick={() => sortData("subjectIdent")} style={{ cursor: "pointer" }}>
|
||||
Fachbezeichnung {sortConfig?.key === "subjectIdent" && (sortConfig.direction === "asc" ? "↑" : "↓")}
|
||||
</th>}
|
||||
|
||||
{columnVisibility.examIdent &&
|
||||
<th scope="col" onClick={() => sortData("examIdent")} style={{ cursor: "pointer" }}>
|
||||
Prüfungsbezeichnung {sortConfig?.key === "examIdent" && (sortConfig.direction === "asc" ? "↑" : "↓")}
|
||||
</th>}
|
||||
|
||||
{columnVisibility.examStudCount &&
|
||||
<th scope="col" onClick={() => sortData("examStudCount")} style={{ cursor: "pointer" }}>
|
||||
Anz. Anmeldung {sortConfig?.key === "examStudCount" && (sortConfig.direction === "asc" ? "↑" : "↓")}
|
||||
</th>}
|
||||
|
||||
{columnVisibility.supervisor &&
|
||||
<th scope="col" onClick={() => sortData("supervisor")} style={{ cursor: "pointer" }}>
|
||||
Betreuer {sortConfig?.key === "supervisor" && (sortConfig.direction === "asc" ? "↑" : "↓")}
|
||||
</th>}
|
||||
|
||||
{/* <th>MartNr</th> */}
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
||||
|
||||
<tbody className="table-group-divider" >
|
||||
{
|
||||
loading ?
|
||||
(
|
||||
<tr>
|
||||
<td colSpan={visibleColumnCount} className="text-center">
|
||||
<div className="text-center my-4">
|
||||
<div className="spinner-border text-primary" role="status" style={{ width: "3rem", height: "3rem" }}>
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p>Loading data...</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : error ? (
|
||||
<tr>
|
||||
<td colSpan={visibleColumnCount}>
|
||||
<div className="alert alert-danger text-center">{error}</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
|
||||
data.length > 0 ? (
|
||||
data.map((item, index) =>
|
||||
<tr key={index}>
|
||||
|
||||
{columnVisibility.date && <td>{item.date}</td>}
|
||||
{columnVisibility.time && <td>{item.time}</td>}
|
||||
{columnVisibility.weekday && <td>{item.weekday}</td>}
|
||||
{columnVisibility.room && <td>{item.room}</td>}
|
||||
{columnVisibility.location && (
|
||||
<td>
|
||||
<a href={item.location} target="_blank" rel="noopener noreferrer">
|
||||
<i class="fa-solid fa-map-location-dot" style={{ color: "#c72426" }}></i>
|
||||
</a>
|
||||
</td>
|
||||
)}
|
||||
{columnVisibility.subjectIdent && <td>{item.subjectIdent}</td>}
|
||||
{columnVisibility.examIdent && <td>{item.examIdent}</td>}
|
||||
{columnVisibility.examStudCount && <td>{item.examStudCount}</td>}
|
||||
{columnVisibility.supervisor && <td>{item.supervisor}</td>}
|
||||
|
||||
{/* <td>{item.studMartNr}</td> */}
|
||||
|
||||
</tr>
|
||||
)
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={visibleColumnCount} className="text-center">No data available</td>
|
||||
</tr>
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div className='my-3'>
|
||||
|
||||
<button className="btn btn-outline-secondary me-2" type="submit" onClick={handleFilterToday} disabled={loading}>
|
||||
{today === "" ? (
|
||||
<>
|
||||
<i className="fa-solid fa-calendar-day fa-lg me-2"></i>
|
||||
Heutige Prüfungen
|
||||
</>) : (
|
||||
|
||||
<>
|
||||
<i className="fa-solid fa-calendar-days fa-lg me-2"></i>
|
||||
Alle Prüfungen
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button className="btn btn-outline-secondary" type="submit" onClick={exportToCalendar} disabled={loading}>
|
||||
<i className="fa-solid fa-file-export fa-lg me-2"></i>
|
||||
<i className="fa-solid fa-file-csv fa-lg me-2"></i>
|
||||
Kalenderexport
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default PruefPlanTable
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Alert } from 'bootstrap';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import "../App.css"
|
||||
|
||||
|
||||
|
||||
const SettingsCard = ({ isVisible, setColumnVisibility }) => {
|
||||
|
||||
const handleCheckboxField = useCallback((e) => {
|
||||
switch (e.target.id) {
|
||||
// case "date":
|
||||
// setColumnVisability(prev => ({ ...prev, date: e.target.checked }))
|
||||
// break;
|
||||
// case "time":
|
||||
// setColumnVisability(prev => ({ ...prev, time: e.target.checked }))
|
||||
// break;
|
||||
case "weekday":
|
||||
setColumnVisibility(prev => ({ ...prev, weekday: e.target.checked }))
|
||||
break;
|
||||
// case "room":
|
||||
// setColumnVisability(prev => ({ ...prev, room: e.target.checked }))
|
||||
// break;
|
||||
case "location":
|
||||
setColumnVisibility(prev => ({ ...prev, location: e.target.checked }))
|
||||
break;
|
||||
case "subjectIdent":
|
||||
setColumnVisibility(prev => ({ ...prev, subjectIdent: e.target.checked }))
|
||||
break;
|
||||
// case "examIdent":
|
||||
// setColumnVisability(prev => ({ ...prev, examIdent: e.target.checked }))
|
||||
// break;
|
||||
case "examStudCount":
|
||||
setColumnVisibility(prev => ({ ...prev, examStudCount: e.target.checked }))
|
||||
break;
|
||||
case "supervisor":
|
||||
setColumnVisibility(prev => ({ ...prev, supervisor: e.target.checked }))
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`dropdown-card ${isVisible ? "visible" : ""} z-2 m-1`} >
|
||||
|
||||
<div className='position-absolute m-1'>
|
||||
<div className='card shadow' style={{ width: "30rm" }}>
|
||||
<h5 className='card-header'> Einstellungen </h5>
|
||||
<div className='card-body'>
|
||||
<h6> Optionale Tabellenspalten - Sichtbarkeit </h6>
|
||||
<table className='table'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div className='input-groub '>
|
||||
<div className='input-group-text'>
|
||||
<input className="form-check-input me-2" type="checkbox" value="" id='weekday' onChange={handleCheckboxField} defaultChecked={false} />
|
||||
Wochentag
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className='input-groub'>
|
||||
<div className='input-group-text'>
|
||||
<input className="form-check-input me-2" type="checkbox" value="" id='supervisor' onChange={handleCheckboxField} defaultChecked={false} />
|
||||
Betreuer
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div className='input-groub '>
|
||||
<div className='input-group-text'>
|
||||
<input className="form-check-input me-2" type="checkbox" value="" id='location' onChange={handleCheckboxField} defaultChecked={true} />
|
||||
Ort
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className='input-groub '>
|
||||
<div className='input-group-text'>
|
||||
<input className="form-check-input me-2" type="checkbox" value="" id='subjectIdent' onChange={handleCheckboxField} defaultChecked={true} />
|
||||
Fachbezeichnung
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div className='input-groub'>
|
||||
<div className='input-group-text'>
|
||||
<input className="form-check-input me-2" type="checkbox" value="" id='examStudCount' onChange={handleCheckboxField} defaultChecked={false} />
|
||||
Anz. Anmeldung
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className='input-groub'>
|
||||
<div className='input-group-text'>
|
||||
<input className="form-check-input me-2" type="checkbox" value="" id='examIdent' onChange={handleCheckboxField} defaultChecked={false} />
|
||||
Prüfungsbezeichnung
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsCard
|
||||
@@ -0,0 +1,78 @@
|
||||
/* :root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
/* Variables */
|
||||
:root {
|
||||
--color-ohm-red: #c72426;
|
||||
--color-ohm-dark-blue: #16283d;
|
||||
--color-ohm-light-purple: rgb(223, 217, 230);
|
||||
--color-light-grey: #f0f0f0;
|
||||
--navbar-links-gap: 15px;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { setUser } from "../utils/auth";
|
||||
|
||||
|
||||
const MainWrapper = ({children}) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = async () =>{
|
||||
setLoading(true)
|
||||
|
||||
await setUser()
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
handler() //Directly call the handler wehn the Func is initialized
|
||||
}, []) //Dependancy Array: [] -> Only execute once on mounting
|
||||
|
||||
|
||||
// Return html conditionaly -> depending on setUser()
|
||||
return <>{loading ? null : children}</>
|
||||
}
|
||||
|
||||
export default MainWrapper
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useEffect } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
|
||||
|
||||
const PrivateRoute = ({ children }) => {
|
||||
const loggedIn = useAuthStore((state) => state.isLoggedIn)() //Using a Hook on the state -> tiggers a rerender when the att has changed
|
||||
|
||||
// Return the content of the PrivateRoute or navigate back to the login page
|
||||
return loggedIn ? <>{children}</> : <Navigate to="/" />
|
||||
}
|
||||
|
||||
export default PrivateRoute
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
// Hier wird die Logic abgebildet die dafür verantwortlich ist den Login-Status eines Users zu managen
|
||||
|
||||
import {create} from "zustand"
|
||||
import {mountStoreDevtool} from "simple-zustand-devtools"
|
||||
|
||||
|
||||
const useAuthStore = create((set, get) => ({
|
||||
allUserData: null, //Default State for all User is null
|
||||
loading: false, //Variable that keeps track of the user state while the login proces
|
||||
loginState: false,
|
||||
|
||||
|
||||
// Func to get the userdata from the allUserData
|
||||
user: () => ({
|
||||
user_name: get().allUserData?.username|| null,
|
||||
// user_password: get().allUserData?.password || null,
|
||||
}),
|
||||
|
||||
// Func to set the allUserData
|
||||
setUser: (user) => set({
|
||||
allUserData: user,
|
||||
}),
|
||||
|
||||
setLoginState: (isLogin) =>set({
|
||||
loginState: isLogin,
|
||||
}),
|
||||
|
||||
// Func to set the loading status
|
||||
setLoading: (loading) => ({loading}),
|
||||
|
||||
// Func to keep track of the login state -> if allUserData is null the user is not loged in
|
||||
isLoggedIn: () => get().loginState,
|
||||
|
||||
}));
|
||||
|
||||
// Extended debug functionality for this Func when in Dev mode
|
||||
if (import.meta.env.DEV){
|
||||
mountStoreDevtool("AuthStore", useAuthStore);
|
||||
}
|
||||
|
||||
|
||||
export {useAuthStore};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useAuthStore } from "../store/auth"
|
||||
import apiInstance from "./axios"
|
||||
import jwt_decode from "jwt-decode"
|
||||
import Cookie from "js-cookie"
|
||||
import Swal from "sweetalert2"
|
||||
|
||||
|
||||
|
||||
// Func to login a user
|
||||
export const login = async (username, password) => {
|
||||
try {
|
||||
const {data, status} = await apiInstance.post('api/user/token', {
|
||||
username,
|
||||
password,
|
||||
})
|
||||
|
||||
|
||||
if(status == 200){
|
||||
setAuthUser(data.access, data.refresh)
|
||||
}
|
||||
|
||||
return {data, error: null}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return {data: null, error: error.response.data?.detail || "Benutzername oder Kennwort falsch."}
|
||||
}
|
||||
}
|
||||
|
||||
// Func to logout a user
|
||||
export const logout = async() =>{
|
||||
|
||||
try {
|
||||
const {data, status} = await apiInstance.post('api/user/logoutUser', {
|
||||
})
|
||||
|
||||
|
||||
if(status == 200){
|
||||
|
||||
// Clearing infromations about the user from the browser
|
||||
Cookie.remove("access_token")
|
||||
Cookie.remove("refresh_token")
|
||||
useAuthStore.getState().setUser(null) // Set current user to null -> mark as logout
|
||||
useAuthStore.getState().setLoginState(false)
|
||||
alert("You have been logged out")
|
||||
}
|
||||
|
||||
return {data, error: null}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return {data: null, error: error.response.data?.detail || "Benutzername oder Kennwort falsch."}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Func to set an AuthUser with Cookie
|
||||
export const setUser = async () => {
|
||||
const access_token = Cookie.get("access_token")
|
||||
const refresh_token = Cookie.get("refresh_token")
|
||||
|
||||
if (!access_token || !refresh_token){
|
||||
console.log("no acces and refresh token available")
|
||||
return
|
||||
}
|
||||
|
||||
if(isAccessTokenExpired(access_token)){
|
||||
// Generate a new Token with access an refresh from the current refresh token
|
||||
const response = getRefreshedToken(refresh_token)
|
||||
setAuthUser(response.access, response.refresh)
|
||||
}else{
|
||||
setAuthUser(access_token, refresh_token)
|
||||
}
|
||||
}
|
||||
|
||||
// Func to set Cookie data and update the AuthStore
|
||||
export const setAuthUser = (access_token, refresh_token) => {
|
||||
// Set Cookie data
|
||||
Cookie.set("access_token", access_token, {
|
||||
expires: 1,
|
||||
secure:true,
|
||||
})
|
||||
Cookie.set("refresh_token", refresh_token, {
|
||||
expires: 3,
|
||||
secure:true,
|
||||
})
|
||||
|
||||
// Get user informations from token
|
||||
const user = jwt_decode(access_token) ?? null
|
||||
|
||||
// Update AuthStore
|
||||
if (user) {
|
||||
console.log("user gets updated",user)
|
||||
useAuthStore.getState().setUser(user)
|
||||
useAuthStore.getState().setLoginState(true)
|
||||
}
|
||||
useAuthStore.getState().setLoading(false)
|
||||
|
||||
}
|
||||
|
||||
// Func to get a refreshed token from Backend
|
||||
export const getRefreshedToken = async () => {
|
||||
// Get refresh token stored in the Cookie
|
||||
const refresh_token = Cookie.get("refresh_token")
|
||||
|
||||
// Call REST API to generate a new token from refresh token
|
||||
const response = await apiInstance.post("api/user/token/refresh/",{
|
||||
refresh: refresh_token,
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
// Func to ceck if a acces token is expired
|
||||
export const isAccessTokenExpired = (access_token) => {
|
||||
try {
|
||||
const decodedToken = jwt_decode(access_token)
|
||||
return decodedToken.exp < Date.now() / 1000
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import axios from "axios"
|
||||
import { API_BASE_URL } from "./constants";
|
||||
|
||||
// Func to specify the API default behaviour
|
||||
const apiInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 100000,
|
||||
headers:{
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default apiInstance
|
||||
@@ -0,0 +1 @@
|
||||
export const API_BASE_URL = "http://127.0.0.1:8000"
|
||||
@@ -0,0 +1,36 @@
|
||||
import axios from "axios"
|
||||
import {getRefreshedToken, isAccessTokenExpired, setAuthUser} from "./auth"
|
||||
import {API_BASE_URL} from "./constants"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
|
||||
// Func to handle API calles
|
||||
const useAxios = () =>{
|
||||
const accessToken = Cookies.get("access_token")
|
||||
const refreshToken = Cookies.get("refresh_token")
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers:{
|
||||
Authorization:"Bearer ${accessToken}"
|
||||
}
|
||||
})
|
||||
|
||||
// Setup a interceptor to run befor every API call using Axios
|
||||
// Func to check if the token from a request is expired bevor letting it through the backend
|
||||
// If a token is expired this func handles the refresh automatically
|
||||
axiosInstance.interceptors.request.use(async (req) => {
|
||||
if(!isAccessTokenExpired(accessToken)){
|
||||
return req
|
||||
}
|
||||
|
||||
const response = await getRefreshedToken(refreshToken)
|
||||
setAuthUser(response.access, response.refresh)
|
||||
req.headers.Authorization = "Bearer ${response.data?.access}"
|
||||
return req
|
||||
})
|
||||
|
||||
return axiosInstance
|
||||
}
|
||||
|
||||
export default useAxios
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
|
||||
import Navbar from '../components/Navbar'
|
||||
import LoginForm from '../components/LoginForm'
|
||||
import EventTable from '../components/EventTable'
|
||||
|
||||
function Homepage() {
|
||||
const date = new Date().getFullYear();
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<Navbar />
|
||||
|
||||
{/* Create a Bootstrap fluid Container for responsive design */}
|
||||
<div className='container-fluid my-5'>
|
||||
|
||||
{/* Adressing row by row with each new div */}
|
||||
<div className='row'>
|
||||
|
||||
<div className="container my-5 text-center">
|
||||
<h1 className="fs-1">
|
||||
Prüfungsplan Ansicht {date}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="container my-5">
|
||||
<LoginForm />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default Homepage
|
||||
@@ -0,0 +1,107 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import Navbar from '../components/Navbar'
|
||||
import PruefPlanTable from '../components/PruefplanTable'
|
||||
import LoginForm from '../components/LoginForm'
|
||||
import EventTable from '../components/EventTable'
|
||||
import PrivateRoute from '../layouts/PrivateRoute'
|
||||
import MartNrForm from '../components/MartNrForm'
|
||||
import SettingsCard from '../components/SettingsCard'
|
||||
import LogbookCard from '../components/LogbookCard'
|
||||
|
||||
|
||||
|
||||
function Mainpage() {
|
||||
|
||||
const [martNr, setMartNr] = useState("")
|
||||
const [settingsIsVisible, setSettingsIsVisible] = useState(false)
|
||||
const [logbookIsVisible, setLogbookIsVisible] = useState(false)
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState({
|
||||
date: true, //not optional
|
||||
time: true, //not optional
|
||||
weekday: false, //default false
|
||||
room: true, //not optional
|
||||
location: true, //default true
|
||||
subjectIdent: true, //default true
|
||||
examIdent: false, //default false
|
||||
supervisor: false, //default false
|
||||
examStudCount: false, //default false
|
||||
})
|
||||
|
||||
const handleMartNrSubmit = (value) => {
|
||||
setMartNr(value)
|
||||
}
|
||||
|
||||
const handleLogbookVisibility = (value) => {
|
||||
setLogbookIsVisible(value)
|
||||
setSettingsIsVisible(!value)
|
||||
}
|
||||
|
||||
const handleSettingsVisibility = (value) => {
|
||||
setSettingsIsVisible(value)
|
||||
setLogbookIsVisible(!value)
|
||||
}
|
||||
|
||||
const handleBackClick = () => {
|
||||
setMartNr("")
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<PrivateRoute>
|
||||
<div>
|
||||
|
||||
<Navbar setSettingsVisibility={handleSettingsVisibility} setLogbookVisibility={handleLogbookVisibility} />
|
||||
|
||||
<SettingsCard isVisible={settingsIsVisible} setColumnVisibility={setColumnVisibility} />
|
||||
|
||||
<LogbookCard isVisible={logbookIsVisible} />
|
||||
|
||||
{/* Create a Bootstrap fluid Container for responsive design */}
|
||||
<div className='container-fluid my-5'>
|
||||
|
||||
{/* Adressing row by row with each new div */}
|
||||
<div className='row'>
|
||||
|
||||
<div className="container my-5 text-center">
|
||||
<h1 className="fs-1">
|
||||
{martNr === "" ?
|
||||
"Prüfungsplan Ansicht - Alle Prüfungen"
|
||||
:
|
||||
`Prüfungsplan Ansicht - Alle Prüfungen von ${martNr}`
|
||||
}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="container my-5">
|
||||
{
|
||||
martNr === "" ? (
|
||||
<>
|
||||
<MartNrForm onSubmit={handleMartNrSubmit} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button className="btn btn-outline-secondary custom-btn-hover" type="submit" style={{ color: "#c72426" }} onClick={handleBackClick}>
|
||||
<i className='fas fa-sign-in-alt fa-flip-horizontal'></i> Zurück zu allen Prüfungen
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<PruefPlanTable martNr={martNr} columnVisibility={columnVisibility} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</PrivateRoute>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default Mainpage
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
+2822
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user