initial
This commit is contained in:
commit
973fefd24f
|
@ -0,0 +1,2 @@
|
|||
.env
|
||||
node_modules/
|
|
@ -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}/`);
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
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
|
@ -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
|
@ -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
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
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
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 it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -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
|
||||
});
|
|
@ -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') %>
|
|
@ -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') %>
|
|
@ -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') %>
|
|
@ -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') %>
|
|
@ -0,0 +1,5 @@
|
|||
<%- include('partials/header') %>
|
||||
|
||||
<h1> <%= userResult.name %> </h1>
|
||||
|
||||
<%- include('partials/footer') %>
|
|
@ -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') %>
|
|
@ -0,0 +1,5 @@
|
|||
</div>
|
||||
<script src="/jquery/jquery.min.js"></script>
|
||||
<script src="/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -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">
|
|
@ -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') %>
|
|
@ -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') %>
|
|
@ -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') %>
|
|
@ -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') %>
|
|
@ -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') %>
|
|
@ -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>
|
|
@ -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') %>
|
Loading…
Reference in New Issue