This commit is contained in:
Klaas 2024-05-23 18:49:34 +00:00
commit 973fefd24f
68 changed files with 82886 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.env
node_modules/

0
README.md Normal file
View File

501
app.js Normal file
View File

@ -0,0 +1,501 @@
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
const { Pool } = require('pg');
const path = require('path');
const moment=require('moment')
require('dotenv').config();
const app = express();
const port = 2000;
// Middleware
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: false }));
// Session-Konfiguration
app.use(session({
secret: 'your_secret_key',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 1800000 }
}));
// Authentifizierungs-Middleware
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.redirect('/login');
}
next();
};
const requireAdmin = (req, res, next) => {
if (req.session.role !== 'admin') {
return res.status(403).send('Access denied');
}
next();
};
// Email-Konfiguration
const transporter = nodemailer.createTransport({
host: "mail.boergmann.it",
port: 465,
secure: true,
auth: {
user: 'klaas@boergmann.it',
pass: 'XDsEXTdAUVkH5=V'
}
});
// Datenbankverbindung
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
// Altersberechnung
const calculateAge = (birthdate) => {
const today = new Date();
const birthDate = new Date(birthdate);
let age = today.getFullYear() - birthDate.getFullYear();
const m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
};
// Datumsformatierung
const formatDate = (date) => {
const d = new Date(date);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}.${month}.${year}`;
};
// Funktion zum Laden des aktuellen oder letzten Trainings
const getTraining = async (selectedDate) => {
const today = selectedDate ? new Date(selectedDate) : new Date();
const dayOfWeek = today.getDay();
// Wenn heute Donnerstag ist
if (dayOfWeek === 4) {
const result = await pool.query(`
SELECT tr.*, g1.name AS geraet_riege_1_name, g2.name AS geraet_riege_2_name,
g3.name AS geraet_riege_3_name, g4.name AS geraet_riege_4_name,
g5.name AS geraet_riege_5_name,
t1.name AS aufwaermleiter_name, t2.name AS spielleiter_name,
sp1.name AS aufwaermen_name, sp2.name AS spiel_name
FROM trainings tr
LEFT JOIN geraete g1 ON tr.geraet_riege_1 = g1.id
LEFT JOIN geraete g2 ON tr.geraet_riege_2 = g2.id
LEFT JOIN geraete g3 ON tr.geraet_riege_3 = g3.id
LEFT JOIN geraete g4 ON tr.geraet_riege_4 = g4.id
LEFT JOIN geraete g5 ON tr.geraet_riege_5 = g5.id
LEFT JOIN teilnehmende t1 ON tr.aufwaermleiter = t1.id
LEFT JOIN teilnehmende t2 ON tr.spielleiter = t2.id
LEFT JOIN spiele sp1 ON tr.aufwaermen = sp1.id
LEFT JOIN spiele sp2 ON tr.spiel = sp2.id
WHERE tr.datum = $1
LIMIT 1
`, [today]);
return result.rows[0];
} else {
const result = await pool.query(`
SELECT tr.*, g1.name AS geraet_riege_1_name, g2.name AS geraet_riege_2_name,
g3.name AS geraet_riege_3_name, g4.name AS geraet_riege_4_name,
g5.name AS geraet_riege_5_name,
t1.name AS aufwaermleiter_name, t2.name AS spielleiter_name,
sp1.name AS aufwaermen_name, sp2.name AS spiel_name
FROM trainings tr
LEFT JOIN geraete g1 ON tr.geraet_riege_1 = g1.id
LEFT JOIN geraete g2 ON tr.geraet_riege_2 = g2.id
LEFT JOIN geraete g3 ON tr.geraet_riege_3 = g3.id
LEFT JOIN geraete g4 ON tr.geraet_riege_4 = g4.id
LEFT JOIN geraete g5 ON tr.geraet_riege_5 = g5.id
LEFT JOIN teilnehmende t1 ON tr.aufwaermleiter = t1.id
LEFT JOIN teilnehmende t2 ON tr.spielleiter = t2.id
LEFT JOIN spiele sp1 ON tr.aufwaermen = sp1.id
LEFT JOIN spiele sp2 ON tr.spiel = sp2.id
WHERE tr.datum <= $1
ORDER BY tr.datum DESC
LIMIT 1
`, [today]);
return result.rows[0];
}
};
// Funktion zum Laden aller Spiele
const getAllSpiele = async () => {
const result = await pool.query(`
SELECT s.id, s.name,
COALESCE(EXTRACT(EPOCH FROM (NOW() - MAX(tr.datum))) / 604800, EXTRACT(EPOCH FROM (NOW() - '1970-01-01'::date)) / 604800) AS weeks_since_last
FROM spiele s
LEFT JOIN trainings tr ON s.id = tr.spiel
GROUP BY s.id
ORDER BY weeks_since_last ASC;
`);
return result.rows;
};
// Funktion zum Hinzufügen eines neuen Spiels, falls es noch nicht existiert
const addNewSpiel = async (spielName) => {
try {
const result = await pool.query('INSERT INTO spiele (name) VALUES ($1) RETURNING id', [spielName]);
return result.rows[0].id;
} catch (error) {
console.error('Error adding new spiel:', error);
throw error;
}
};
// Funktion zum Laden der vier Leute, die am längsten nicht Aufwärmen bzw. Spiel geleitet haben
// Funktion zum Laden der vier Leute, die am längsten nicht Aufwärmen geleitet haben
const getCandidatesForAufwaermleiter = async () => {
const result = await pool.query(`
SELECT t.id, t.name,
COALESCE(EXTRACT(EPOCH FROM (NOW() - MAX(tr.datum))) / 604800, EXTRACT(EPOCH FROM (NOW() - '1970-01-01'::date)) / 604800) AS weeks_since_last
FROM teilnehmende t
LEFT JOIN trainings tr ON t.id = tr.aufwaermleiter
WHERE t.helfer = true
GROUP BY t.id
ORDER BY weeks_since_last DESC
LIMIT 4;
`);
return result.rows;
};
// Funktion zum Laden der vier Leute, die am längsten nicht Spiel geleitet haben
const getCandidatesForSpielleiter = async () => {
const result = await pool.query(`
SELECT t.id, t.name,
COALESCE(EXTRACT(EPOCH FROM (NOW() - MAX(tr.datum))) / 604800, EXTRACT(EPOCH FROM (NOW() - '1970-01-01'::date)) / 604800) AS weeks_since_last
FROM teilnehmende t
LEFT JOIN trainings tr ON t.id = tr.spielleiter
WHERE t.helfer = true
GROUP BY t.id
ORDER BY weeks_since_last DESC
LIMIT 4
`);
return result.rows;
};
// Funktion zum Laden der Riegenteilnehmer absteigend nach Alter sortiert
const getRiegenMitgliederSortedByAge = async () => {
const result = await pool.query(`
SELECT r.riegennummer, t.name, t.alter
FROM riegen r
JOIN teilnehmende t ON r.fremdid_teilnehmende = t.id
ORDER BY r.riegennummer, t.alter DESC
`);
return result.rows;
};
// Registrierung
app.post('/register', async (req, res) => {
const { username, password } = req.body;
try {
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query('INSERT INTO users (username, password) VALUES ($1, $2)', [username, hashedPassword]);
res.redirect('/login');
} catch (error) {
console.error('Error registering user:', error);
res.status(500).send('Internal Server Error');
}
});
// Login
app.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
const userResult = await pool.query('SELECT * FROM users WHERE username = $1', [username]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
const match = await bcrypt.compare(password, user.password);
if (match) {
req.session.userId = user.id;
req.session.role=user.role;
res.redirect('/');
} else {
res.redirect('/login');
}
} else {
res.redirect('/login');
}
} catch (error) {
console.error('Error logging in:', error);
res.status(500).send('Internal Server Error');
}
});
// Logout
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).send('Internal Server Error');
}
res.redirect('/login');
});
});
// Benutzer freischalten (nur Admin)
app.post('/activate', requireAuth, requireAdmin, async (req, res) => {
const { userId } = req.body;
try {
await pool.query('UPDATE users SET is_active = TRUE WHERE id = $1', [userId]);
res.redirect('/admin');
} catch (error) {
console.error('Error activating user:', error);
res.status(500).send('Internal Server Error');
}
});
// Passwort-Zurücksetzung anfordern
app.post('/forgot-password', async (req, res) => {
const { email } = req.body;
console.log ( email );
try {
const userResult = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
const token = crypto.randomBytes(20).toString('hex');
const resetLink = `http://tkd.boergmann.it/reset-password/${token}`;
console.log(resetLink);
await pool.query('UPDATE users SET reset_password_token = $1, reset_password_expires = $2 WHERE id = $3', [token, Date.now() + 3600, user.id]);
const mailOptions = {
to: user.email,
from: 'admin@boergmann.it',
subject: 'Password Reset',
text: `Click the following link to reset your password: ${resetLink}`
};
transporter.sendMail(mailOptions, (err) => {
if (err) {
console.error('Error sending email:', err);
res.status(500).send('Error sending email');
} else {
res.send('Password reset link sent');
}
});
} else {
res.status(400).send('Email not found');
}
} catch (error) {
console.error('Error in forgot-password:', error);
res.status(500).send('Internal Server Error');
}
});
app.get('/forgot-password', async (req, res) => {
res.render('forgot-password', {session: req.session, token : '123'})
})
// Passwort zurücksetzen
app.get('/reset-password/:token', async (req, res) => {
const { token } = req.params;
try {
const userResult = await pool.query('SELECT * FROM users WHERE reset_password_token = $1 AND reset_password_expires > $2', [token, Date.now()]);
if (userResult.rows.length > 0) {
res.render('reset-password', { token }); // Stelle sicher, dass es eine reset-password.ejs gibt
} else {
res.status(400).send('Password reset token is invalid or has expired');
}
} catch (error) {
console.error('Error in reset-password:', error);
res.status(500).send('Internal Server Error');
}
});
app.post('/reset-password/:token', async (req, res) => {
const { token } = req.params;
const { password } = req.body;
try {
const userResult = await pool.query('SELECT * FROM users WHERE reset_password_token = $1 AND reset_password_expires > $2', [token, Date.now()]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query('UPDATE users SET password = $1, reset_password_token = NULL, reset_password_expires = NULL WHERE id = $2', [hashedPassword, user.id]);
res.redirect('/login');
} else {
res.status(400).send('Password reset token is invalid or has expired');
}
} catch (error) {
console.error('Error in reset-password:', error);
res.status(500).send('Internal Server Error');
}
});
// Profilseite
app.get('/profile', requireAuth, (req, res) => {
res.render('profile', { session: req.session}); // Stelle sicher, dass es eine profile.ejs gibt
});
app.post('/profile', requireAuth, async (req, res) => {
const { email, password } = req.body;
try {
if (email) {
await pool.query('UPDATE users SET email = $1 WHERE id = $2', [email, req.session.userId]);
}
if (password) {
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query('UPDATE users SET password = $1 WHERE id = $2', [hashedPassword, req.session.userId]);
}
res.redirect('/profile');
} catch (error) {
console.error('Error updating profile:', error);
res.status(500).send('Internal Server Error');
}
});
app.post('/update-leader', async (req, res) => {
const { trainingId, type, leaderId } = req.body;
try {
if (type === 'aufwaermleiter') {
await pool.query('UPDATE trainings SET aufwaermleiter = $1 WHERE id = $2', [leaderId, trainingId]);
} else if (type === 'spielleiter') {
await pool.query('UPDATE trainings SET spielleiter = $1 WHERE id = $2', [leaderId, trainingId]);
}
res.redirect('/');
} catch (err) {
console.error(err);
res.send("Error " + err);
}
});
// Admin-Seite
app.get('/admin', requireAuth, requireAdmin, async (req, res) => {
const usersResult = await pool.query('SELECT * FROM users WHERE is_active = FALSE');
res.render('admin', { users: usersResult.rows, session: req.session }); // Stelle sicher, dass es eine admin.ejs gibt
});
// Teilnehmer_innen
app.get('/', requireAuth, async (req, res) => {
try {
const selectedDate = req.query.date;
const training = await getTraining(selectedDate);
const trainingsResult = await pool.query('SELECT datum FROM trainings ORDER BY datum ASC');
const trainingsDates = trainingsResult.rows.map(tr => ({
datum: formatDate(tr.datum),
rawDatum: tr.datum
}));
if (training) {
training.datum = formatDate(training.datum);
}
const aufwaermleiterCandidates = await getCandidatesForAufwaermleiter();
const spielleiterCandidates = await getCandidatesForSpielleiter();
const spielCandidates = await getAllSpiele();
const aufwaermenCandidates = await getAllSpiele();
res.render('trainings', {
training,
trainingsDates,
selectedDate: training.datum,
aufwaermleiterCandidates,
spielleiterCandidates,
aufwaermenCandidates, // Übergeben der Kandidaten für Aufwärmleiter
spielCandidates, // Übergeben der Spiele
session: req.session
});
} catch (err) {
console.error(err);
res.send("Error " + err);
}
});
app.get('/riege', requireAuth, async (req, res) => {
try {
// Abrufen der Riegendaten einschließlich der Teilnehmer und deren Altersberechnung
const result = await pool.query(`
SELECT r.riegennummer, t.id, t.name, t.geburtsdatum, r.helfer
FROM riegen r
JOIN teilnehmende t ON r.fremdID_Teilnehmende = t.id
ORDER BY r.riegennummer, t.geburtsdatum ASC
`);
// Gruppieren der Riegenteilnehmer nach Riegennummer
const riegen = {};
result.rows.forEach(row => {
const age = calculateAge(row.geburtsdatum);
if (!riegen[row.riegennummer]) {
riegen[row.riegennummer] = [];
}
riegen[row.riegennummer].push({
id: row.id,
name: row.name,
age: age,
helfer: row.helfer,
});
});
res.render('riegen', { riegen: riegen, session: req.session });
} catch (error) {
console.error('Error fetching riegen:', error);
res.status(500).send('Internal Server Error');
}
});
app.get('/teilnehmer', requireAuth, async (req, res) => {
try {
const teilnehmendeResult = await pool.query('SELECT * FROM teilnehmende');
const teilnehmende = teilnehmendeResult.rows.map(t => ({
...t,
age: calculateAge(t.geburtsdatum)
})).sort((a, b) => b.age - a.age);
res.render('teilnehmer', { teilnehmende, session: req.session });
} catch (err) {
console.error(err);
res.send("Error " + err);
}
});
app.get('/mitglied/:id', requireAuth, async (req, res) => {
const { id } = req.params;
try {
const userResult = await pool.query('SELECT * FROM teilnehmende WHERE id = $1', [id]);
if (userResult.rows.length > 0) {
const mitglied = userResult.rows.map(t => ({
...t,
age: calculateAge(t.geburtsdatum)
}))
res.render('mitglied', {
id,
mitglied: mitglied[0],
session: req.session
});
} else {
res.status(400).send('Mitglied existiert nicht');
}
} catch (error) {
console.error('Error in Mitglied:', error);
res.status(500).send('Internal Server Error');
}
});
// Login und Registrierung anzeigen
app.get('/login', (req, res) => {
res.render('login', {session: req.session}); // Stelle sicher, dass es eine login.ejs gibt
});
app.get('/register', (req, res) => {
res.render('register'); // Stelle sicher, dass es eine register.ejs gibt
});
const server = app.listen(port, '0.0.0.0', () => {
console.log(`Server is running on http://localhost:${port}/`);
});

1602
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "kinderturnen-gpt",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-session": "^1.18.0",
"moment": "^2.30.1",
"nodemailer": "^6.9.13",
"pg": "^8.11.5"
}
}

4085
public/bootstrap/css/bootstrap-grid.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12057
public/bootstrap/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12030
public/bootstrap/css/bootstrap.rtl.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6314
public/bootstrap/js/bootstrap.bundle.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4447
public/bootstrap/js/bootstrap.esm.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4494
public/bootstrap/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
public/bootstrap/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1315
public/jquery-ui/jquery-ui.css vendored Normal file

File diff suppressed because it is too large Load Diff

19070
public/jquery-ui/jquery-ui.js vendored Normal file

File diff suppressed because it is too large Load Diff

2
public/jquery/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

302
routes.js Normal file
View File

@ -0,0 +1,302 @@
const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
const { Pool } = require('pg');
const path = require('path');
require('dotenv').config();
const router = express.Router();
// Session-Konfiguration
app.use(session({
secret: 'your_secret_key',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 60000 }
}));
// Authentifizierungs-Middleware
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.redirect('/login');
}
next();
};
const requireAdmin = (req, res, next) => {
if (req.session.role !== 'admin') {
return res.status(403).send('Access denied');
}
next();
};
// Registrierung
router.post('/register', async (req, res) => {
const { username, password } = req.body;
try {
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query('INSERT INTO users (username, password) VALUES ($1, $2)', [username, hashedPassword]);
res.redirect('/login');
} catch (error) {
console.error('Error registering user:', error);
res.status(500).send('Internal Server Error');
}
});
// Login
router.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
const userResult = await pool.query('SELECT * FROM users WHERE username = $1', [username]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
const match = await bcrypt.compare(password, user.password);
if (match) {
req.session.userId = user.id;
req.session.role=user.role;
res.redirect('/');
} else {
res.redirect('/login');
}
} else {
res.redirect('/login');
}
} catch (error) {
console.error('Error logging in:', error);
res.status(500).send('Internal Server Error');
}
});
// Logout
router.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).send('Internal Server Error');
}
res.redirect('/login');
});
});
// Benutzer freischalten (nur Admin)
router.post('/activate', requireAuth, requireAdmin, async (req, res) => {
const { userId } = req.body;
try {
await pool.query('UPDATE users SET is_active = TRUE WHERE id = $1', [userId]);
res.redirect('/admin');
} catch (error) {
console.error('Error activating user:', error);
res.status(500).send('Internal Server Error');
}
});
// Passwort-Zurücksetzung anfordern
router.post('/forgot-password', async (req, res) => {
const { email } = req.body;
try {
const userResult = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
const token = crypto.randomBytes(20).toString('hex');
const resetLink = `http://tkd.boergmann.it/reset-password/${token}`;
await pool.query('UPDATE users SET reset_password_token = $1, reset_password_expires = $2 WHERE id = $3', [token, Date.now() + 3600000, user.id]);
const mailOptions = {
to: user.email,
from: 'admin@boergmann.it',
subject: 'Password Reset',
text: `Click the following link to reset your password: ${resetLink}`
};
transporter.sendMail(mailOptions, (err) => {
if (err) {
console.error('Error sending email:', err);
res.status(500).send('Error sending email');
} else {
res.send('Password reset link sent');
}
});
} else {
res.status(400).send('Email not found');
}
} catch (error) {
console.error('Error in forgot-password:', error);
res.status(500).send('Internal Server Error');
}
});
// Passwort zurücksetzen
router.get('/reset-password/:token', async (req, res) => {
const { token } = req.params;
try {
const userResult = await pool.query('SELECT * FROM users WHERE reset_password_token = $1 AND reset_password_expires > $2', [token, Date.now()]);
if (userResult.rows.length > 0) {
res.render('reset-password', { token }); // Stelle sicher, dass es eine reset-password.ejs gibt
} else {
res.status(400).send('Password reset token is invalid or has expired');
}
} catch (error) {
console.error('Error in reset-password:', error);
res.status(500).send('Internal Server Error');
}
});
router.post('/reset-password/:token', async (req, res) => {
const { token } = req.params;
const { password } = req.body;
try {
const userResult = await pool.query('SELECT * FROM users WHERE reset_password_token = $1 AND reset_password_expires > $2', [token, Date.now()]);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query('UPDATE users SET password = $1, reset_password_token = NULL, reset_password_expires = NULL WHERE id = $2', [hashedPassword, user.id]);
res.redirect('/login');
} else {
res.status(400).send('Password reset token is invalid or has expired');
}
} catch (error) {
console.error('Error in reset-password:', error);
res.status(500).send('Internal Server Error');
}
});
// Profilseite
router.get('/profile', requireAuth, (req, res) => {
res.render('profile', { session: req.session}); // Stelle sicher, dass es eine profile.ejs gibt
});
router.post('/profile', requireAuth, async (req, res) => {
const { email, password } = req.body;
try {
if (email) {
await pool.query('UPDATE users SET email = $1 WHERE id = $2', [email, req.session.userId]);
}
if (password) {
const hashedPassword = await bcrypt.hash(password, 10);
await pool.query('UPDATE users SET password = $1 WHERE id = $2', [hashedPassword, req.session.userId]);
}
res.redirect('/profile');
} catch (error) {
console.error('Error updating profile:', error);
res.status(500).send('Internal Server Error');
}
});
router.post('/update-leader', async (req, res) => {
const { trainingId, type, leaderId } = req.body;
try {
if (type === 'aufwaermleiter') {
await pool.query('UPDATE trainings SET aufwaermleiter = $1 WHERE id = $2', [leaderId, trainingId]);
} else if (type === 'spielleiter') {
await pool.query('UPDATE trainings SET spielleiter = $1 WHERE id = $2', [leaderId, trainingId]);
}
res.redirect('/');
} catch (err) {
console.error(err);
res.send("Error " + err);
}
});
// Admin-Seite
router.get('/admin', requireAuth, requireAdmin, async (req, res) => {
const usersResult = await pool.query('SELECT * FROM users WHERE is_active = FALSE');
res.render('admin', { users: usersResult.rows, session: req.session }); // Stelle sicher, dass es eine admin.ejs gibt
});
// Teilnehmer_innen
router.get('/', requireAuth, async (req, res) => {
try {
const selectedDate = req.query.date;
const training = await getTraining(selectedDate);
const trainingsResult = await pool.query('SELECT datum FROM trainings ORDER BY datum ASC');
const trainingsDates = trainingsResult.rows.map(tr => ({
datum: formatDate(tr.datum),
rawDatum: tr.datum
}));
if (training) {
training.datum = formatDate(training.datum);
}
const aufwaermleiterCandidates = await getCandidatesForAufwaermleiter();
const spielleiterCandidates = await getCandidatesForSpielleiter();
const spielCandidates = await getAllSpiele();
const aufwaermenCandidates = await getAllSpiele();
res.render('trainings', {
training,
trainingsDates,
selectedDate: formatDate(selectedDate),
aufwaermleiterCandidates,
spielleiterCandidates,
aufwaermenCandidates, // Übergeben der Kandidaten für Aufwärmleiter
spielCandidates, // Übergeben der Spiele
session: req.session
});
} catch (err) {
console.error(err);
res.send("Error " + err);
}
});
router.get('/riege', requireAuth, async (req, res) => {
try {
// Abrufen der Riegendaten einschließlich der Teilnehmer und deren Altersberechnung
const result = await pool.query(`
SELECT r.riegennummer, t.id, t.name, t.geburtsdatum, r.helfer
FROM riegen r
JOIN teilnehmende t ON r.fremdID_Teilnehmende = t.id
ORDER BY r.riegennummer, t.geburtsdatum ASC
`);
// Gruppieren der Riegenteilnehmer nach Riegennummer
const riegen = {};
result.rows.forEach(row => {
const age = calculateAge(row.geburtsdatum);
if (!riegen[row.riegennummer]) {
riegen[row.riegennummer] = [];
}
riegen[row.riegennummer].push({
id: row.id,
name: row.name,
age: age,
helfer: row.helfer,
});
});
res.render('riegen', { riegen: riegen, session: req.session });
} catch (error) {
console.error('Error fetching riegen:', error);
res.status(500).send('Internal Server Error');
}
});
router.get('/teilnehmer', requireAuth, async (req, res) => {
try {
const teilnehmendeResult = await pool.query('SELECT * FROM teilnehmende');
const teilnehmende = teilnehmendeResult.rows.map(t => ({
...t,
age: calculateAge(t.geburtsdatum)
})).sort((a, b) => b.age - a.age);
res.render('teilnehmer', { teilnehmende, session: req.session });
} catch (err) {
console.error(err);
res.send("Error " + err);
}
});
// Login und Registrierung anzeigen
router.get('/login', (req, res) => {
res.render('login', {session: req.session}); // Stelle sicher, dass es eine login.ejs gibt
});
router.get('/register', (req, res) => {
res.render('register'); // Stelle sicher, dass es eine register.ejs gibt
});

16
views/admin.ejs Normal file
View File

@ -0,0 +1,16 @@
<%- include('partials/header') %>
<h1>Admin Panel</h1>
<ul>
<% users.forEach(user => { %>
<li>
<%= user.username %> - <%= user.email %>
<form action="/activate" method="post" style="display: inline;">
<input type="hidden" name="userId" value="<%= user.id %>">
<button type="submit" class="btn btn-success">Activate</button>
</form>
</li>
<% }) %>
</ul>
<%- include('partials/footer') %>

13
views/forgot-password.ejs Normal file
View File

@ -0,0 +1,13 @@
<%- include('partials/header') %>
<h1>Forgot Password</h1>
<form action="/forgot-password" method="post">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<button type="submit" class="btn btn-primary">Send Reset Link</button>
</form>
<a href="/login">Back to Login</a>
<%- include('partials/footer') %>

47
views/index.ejs Normal file
View File

@ -0,0 +1,47 @@
<%- include('partials/header') %>
<h1>Turnstunden Organisation</h1>
<h2>Teilnehmende</h2>
<ul>
<% teilnehmende.forEach(teilnehmer => { %>
<li>
<% if (teilnehmer.helfer) { %>
<strong><%= teilnehmer.name %></strong> (<%= teilnehmer.age %> Jahre)
<% } else { %>
<%= teilnehmer.name %> (<%= teilnehmer.age %> Jahre)
<% } %>
</li>
<% }) %>
</ul>
<h2>Riegen</h2>
<ul>
<% riegen.forEach(riege => { %>
<li>
Riege <%= riege.riegennummer %> - <%= riege.name %> (<%= riege.age %> Jahre)
</li>
<% }) %>
</ul>
<h2>Trainings</h2>
<ul>
<% trainings.forEach(training => { %>
<li>
Datum: <%= training.datum %> -
Gerät Riege 1: <%= training.geraet_riege_1_name %> -
Gerät Riege 2: <%= training.geraet_riege_2_name %> -
Gerät Riege 3: <%= training.geraet_riege_3_name %> -
Gerät Riege 4: <%= training.geraet_riege_4_name %> -
Gerät Riege 5: <%= training.geraet_riege_5_name %> -
Aufwärmleiter: <%= training.aufwaermleiter_name %> -
Spielleiter: <%= training.spielleiter_name %> -
Aufwärmen: <%= training.aufwaermen_name %> -
Spiel: <%= training.spiel_name %>
</li>
<% }) %>
</ul>
</body>
</html>
<%- include('partials/footer') %>

18
views/login.ejs Normal file
View File

@ -0,0 +1,18 @@
<%- include('partials/header') %>
<h1>Login</h1>
<form action="/login" method="post">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
<a href="/register">Register</a>
<a href="/forgot-password">Forgot Password?</a>
<%- include('partials/footer') %>

5
views/mitglied Normal file
View File

@ -0,0 +1,5 @@
<%- include('partials/header') %>
<h1> <%= userResult.name %> </h1>
<%- include('partials/footer') %>

10
views/mitglied.ejs Normal file
View File

@ -0,0 +1,10 @@
<%- include('partials/header') %>
<% birthday = new Date(mitglied.geburtsdatum) %>
<h1> <%= mitglied.name %> </h1>
<strong> Geburtsdatum </strong> <%= birthday.getDate() %>.<%= birthday.getMonth() + 1 %>.<%= birthday.getFullYear() %> (<%= mitglied.age %>) </br>
<strong> Adresse </strong> <%= mitglied.adresse %>
<%- include('partials/footer') %>

View File

@ -0,0 +1,5 @@
</div>
<script src="/jquery/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.bundle.min.js"></script>
</body>
</html>

41
views/partials/header.ejs Normal file
View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Turnstunden WebApp</title>
<link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">Turnstunden WebApp</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Trainings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/riege">Riegen</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/teilnehmer">Teilnehmende</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/profile">Profil</a>
</li>
<% if (session && session.role === 'admin') { %>
<li class="nav-item"><a class="nav-link" href="/admin">Admin</a></li>
<% } %>
<% if (session && session.userId) { %>
<li><form action="/logout" method="post"><button type="submit">Logout</button></form></li>
<% } else { %>
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li>
<% } %>
</ul>
</div>
</nav>
<div class="container">

16
views/profile.ejs Normal file
View File

@ -0,0 +1,16 @@
<%- include('partials/header') %>
<h1>Profile</h1>
<form action="/profile" method="post">
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email">
</div>
<div class="mb-3">
<label for="password" class="form-label">New Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Update Profile</button>
</form>
<%- include('partials/footer') %>

21
views/register.ejs Normal file
View File

@ -0,0 +1,21 @@
<%- include('partials/header') %>
<h1>Register</h1>
<form action="/register" method="post">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
<a href="/login">Login</a>
<%- include('partials/footer') %>

12
views/reset-password.ejs Normal file
View File

@ -0,0 +1,12 @@
<%- include('partials/header') %>
<h1>Reset Password</h1>
<form action="/reset-password/<%= token %>" method="post">
<div class="mb-3">
<label for="password" class="form-label">New Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Reset Password</button>
</form>
<%- include('partials/footer') %>

28
views/riegen.ejs Normal file
View File

@ -0,0 +1,28 @@
<%- include('partials/header') %>
<h1>Riegen</h1>
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="myTab" role="tablist">
<% Object.keys(riegen).forEach((riegennummer, index) => { %>
<li class="nav-item" role="presentation">
<button class="nav-link <%= index === 0 ? 'active' : '' %>" id="tab-<%= riegennummer %>" data-bs-toggle="tab" data-bs-target="#riege-<%= riegennummer %>" type="button" role="tab" aria-controls="riege-<%= riegennummer %>" aria-selected="<%= index === 0 ? 'true' : 'false' %>">Riege <%= riegennummer %></button>
</li>
<% }) %>
</ul>
<!-- Tab panes -->
<div class="tab-content" id="myTabContent">
<% Object.keys(riegen).forEach((riegennummer, index) => { %>
<div class="tab-pane fade <%= index === 0 ? 'show active' : '' %>" id="riege-<%= riegennummer %>" role="tabpanel" aria-labelledby="tab-<%= riegennummer %>">
<h3>
<% riegen[riegennummer].forEach(teilnehmer => { if(teilnehmer.helfer) { %> <a href="/mitglied/<%= teilnehmer.id %>"><strong><%= teilnehmer.name %></strong> (<%= teilnehmer.age %> Jahre)</a> <% }}) %>
</h3>
<ul class="list-group list-group-flush">
<% riegen[riegennummer].forEach(teilnehmer => { if(!teilnehmer.helfer) { %> <li> <a href="/mitglied/<%= teilnehmer.id %>"><strong><%= teilnehmer.name %></strong> (<%= teilnehmer.age %> Jahre)</a> </li> <% }}) %>
</ul>
</div>
<% }) %>
</div>
<%- include('partials/footer') %>

17
views/teilnehmer.ejs Normal file
View File

@ -0,0 +1,17 @@
<%- include('partials/header') %>
<h1>Teilnehmende</h1>
<ul>
<% teilnehmende.forEach(teilnehmer => { %>
<li>
<% if (teilnehmer.helfer) { %>
<a href="/mitglied/<%= teilnehmer.id %>"><strong><%= teilnehmer.name %></strong> (<%= teilnehmer.age %> Jahre)</a>
<% } else { %>
<a href="/mitglied/<%= teilnehmer.id %>"><%= teilnehmer.name %> (<%= teilnehmer.age %> Jahre)</a>
<% } %>
</li>
<% }) %>
</ul>
</body>
</html>
<%- include('partials/footer') %>

119
views/test.html Normal file
View File

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Turnstunden WebApp</title>
<link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="/jquery-ui/jquery-ui.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">Turnstunden WebApp</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Trainings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/riege">Riegen</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/teilnehmer">Teilnehmende</a>
</li>
</ul>
</div>
</nav>
<div class="container">
<h1>Riegen</h1>
<div id="tabs">
<ul>
<li><a href="#tabs-1">Riege 1</a></li>
<li><a href="#tabs-2">Riege 2</a></li>
<li><a href="#tabs-3">Riege 3</a></li>
<li><a href="#tabs-4">Riege 4</a></li>
<li><a href="#tabs-5">Riege 5</a></li>
</ul>
<div class="riege" id="tabs-1">
<h3> Klaas Börgmann (39) </h3>
<ul>
<li> Pascal Terpitz (32) </li>
<li> Lilly Warg (18) </li>
<li> Louisa Payne (16) </li>
<li> Vivien Horn (16) </li>
<li> Stella Marie Gartmann (14) </li>
<li> Talea Celine Stringa (14) </li>
<li> Helena Markwald (14) </li>
<li> Lilly-Marie Schmidtke (13) </li>
<li> Paula Wewer (11) </li>
<li> Emmi Claeys (11) </li>
</ul>
</div>
<div class="riege" id="tabs-2">
<h3> Rabea Zimmermann (20) </h3>
<ul>
<li> Isabella Esposito (11) </li>
<li> Tessa-May Lucie Groom (11) </li>
<li> Kira Sergeeva (10) </li>
<li> Linn Haustein (10) </li>
<li> Charlotte Maria Kamann (10) </li>
<li> Dariia Sevchuk (54) </li>
<li> Emilia Rack (54) </li>
</ul>
</div>
<div class="riege" id="tabs-3">
<h3> Laura Schwarz (25) </h3>
<ul>
<li> Mathilda Rack (9) </li>
<li> Aylin Schlack (9) </li>
<li> Maja Bonna (9) </li>
<li> Maysaa Fiad (8) </li>
<li> Maya Herrmann (8) </li>
<li> Anna Clara Hombogen (8) </li>
<li> Lena Maria Endemann (8) </li>
</ul>
</div>
<div class="riege" id="tabs-4">
<h3> Carlotta Trojahn (14) Laila Talatou (14) </h3>
<ul>
<li> Malvina Roncato (7) </li>
<li> Frida Kropp (7) </li>
<li> Emelie Dreißen (7) </li>
<li> Carlotta Rütters (6) </li>
<li> Emily-Rose Antonia Groom (6) </li>
<li> Nicole Al-Khonni (54) </li>
<li> Alexa Al-Khonni (54) </li>
</ul>
</div>
<div class="riege" id="tabs-5">
<h3> Simon Pietkiewicz (20) </h3>
<ul>
<li> Hans-Georg Drayß (86) </li>
<li> Marlon Eberhardt (18) </li>
<li> Philipp Ilsen (16) </li>
<li> Joshua Füchtmeier (13) </li>
<li> Aiden-Worn Schmidtke (12) </li>
<li> Sean-Pie Pharrell Jacono (10) </li>
<li> David Schwarz (8) </li>
<li> Engin Özkan (54) </li>
</ul>
</div>
</div>
</div>
<script src="/jquery/jquery.min.js"></script>
<script src="/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="/jquery-ui/jquery-ui.js"></script>
</body>
</html>

112
views/trainings.ejs Normal file
View File

@ -0,0 +1,112 @@
<%- include('partials/header') %>
<h1>Trainings</h1>
<form method="GET" action="/">
<label for="training-date">Wählen Sie ein Datum:</label>
<select id="training-date" name="date" onchange="this.form.submit()">
<% trainingsDates.forEach(date => { %>
<option value="<%= date.rawDatum %>" <%= selectedDate === date.datum ? 'selected' : '' %>>
<%= date.datum %>
</option>
<% }) %>
</select>
</form>
<% if (training) { %>
<ul>
<li>Datum: <%= training.datum %></li>
<li>Gerät Riege 1: <%= training.geraet_riege_1_name %></li>
<li>Gerät Riege 2: <%= training.geraet_riege_2_name %></li>
<li>Gerät Riege 3: <%= training.geraet_riege_3_name %></li>
<li>Gerät Riege 4: <%= training.geraet_riege_4_name %></li>
<li>Gerät Riege 5: <%= training.geraet_riege_5_name %></li>
<li>
Aufwärmleiter:
<% if (training.aufwaermleiter_name) { %>
<%= training.aufwaermleiter_name %>
<% } else { %>
<form method="POST" action="/update-leader">
<input type="hidden" name="trainingId" value="<%= training.id %>">
<input type="hidden" name="type" value="aufwaermleiter">
<select name="leaderId">
<% aufwaermleiterCandidates.forEach(candidate => { %>
<option value="<%= candidate.id %>"><%= candidate.name %> (<%= Math.floor(candidate.weeks_since_last) %> Wochen)</option>
<% }) %>
</select>
<button type="submit">Speichern</button>
</form>
<% } %>
</li>
<li>
Spielleiter:
<% if (training.spielleiter_name) { %>
<%= training.spielleiter_name %>
<% } else { %>
<form method="POST" action="/update-leader">
<input type="hidden" name="trainingId" value="<%= training.id %>">
<input type="hidden" name="type" value="spielleiter">
<select name="leaderId">
<% spielleiterCandidates.forEach(candidate => { %>
<option value="<%= candidate.id %>"><%= candidate.name %> (<%= Math.floor(candidate.weeks_since_last) %> Wochen)</option>
<% }) %>
</select>
<button type="submit">Speichern</button>
</form>
<% } %>
</li>
<li>
Aufwärmen:
<% if (training.aufwaermen_name) { %>
<%= training.aufwaermen_name %>
<% } else { %>
<form method="POST" action="/update-training">
<input type="hidden" name="trainingId" value="<%= training.id %>">
<input type="hidden" name="type" value="aufwaermen">
<select name="aufwaermenId">
<% aufwaermenCandidates.forEach(candidate => { %>
<option value="<%= candidate.id %>"><%= candidate.name %> (<%= Math.floor(candidate.weeks_since_last) %> Wochen)</option>
<% }) %>
</select>
<button type="submit">Speichern</button>
</form>
<% } %>
<form method="POST" action="/add-spiel">
<input type="hidden" name="trainingId" value="<%= training.id %>">
<input type="hidden" name="type" value="aufwaermen">
<input type="text" name="aufwaermenName" placeholder="Neues Aufwärmen">
<button type="submit">Hinzufügen</button>
</form>
</li>
<!-- Für Spiel -->
<li>
Spiel:
<% if (training.spiel_name) { %>
<%= training.spiel_name %>
<% } else { %>
<form method="POST" action="/update-training">
<input type="hidden" name="trainingId" value="<%= training.id %>">
<input type="hidden" name="type" value="spiel">
<select name="spielId">
<% spielCandidates.forEach(candidate => { %>
<option value="<%= candidate.id %>"><%= candidate.name %> (<%= Math.floor(candidate.weeks_since_last) %> Wochen)</option>
<% }) %>
</select>
<button type="submit">Speichern</button>
</form>
<% } %>
<form method="POST" action="/add-spiel">
<input type="hidden" name="trainingId" value="<%= training.id %>">
<input type="hidden" name="type" value="spiel">
<input type="text" name="spielName" placeholder="Neues Spiel">
<button type="submit">Hinzufügen</button>
</form>
</li>
</ul>
<% } else { %>
<p>Kein Training gefunden für das ausgewählte Datum.</p>
<% } %>
</body>
</html>
<%- include('partials/footer') %>