Release - Upload

This commit is contained in:
Js_Sman_Omen
2025-03-24 14:34:56 +01:00
commit 89f35ca466
370 changed files with 32904 additions and 0 deletions
+24
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?
+8
View File
@@ -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
+38
View File
@@ -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 },
],
},
},
]
+34
View File
@@ -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>
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES2022",
"jsx": "react",
"allowImportingTsExtensions": true,
"strictNullChecks": true,
"strictFunctionTypes": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}
+48
View File
@@ -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"
}
}
+1
View File
@@ -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

+58
View File
@@ -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;
}
+31
View File
@@ -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

+59
View File
@@ -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
+34
View File
@@ -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
+106
View File
@@ -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
+78
View File
@@ -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
+121
View File
@@ -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;
+353
View File
@@ -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
+128
View File
@@ -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
+78
View File
@@ -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;
}
+25
View File
@@ -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
+14
View File
@@ -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
+10
View File
@@ -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>,
)
+42
View File
@@ -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};
+125
View File
@@ -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
}
}
+15
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
export const API_BASE_URL = "http://127.0.0.1:8000"
+36
View File
@@ -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
+40
View File
@@ -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
+107
View File
@@ -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
+7
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()],
})
+2822
View File
File diff suppressed because it is too large Load Diff