Compare commits
3 Commits
main
...
68760ef22f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68760ef22f | ||
|
|
7861b9cffb | ||
|
|
77e3dbde82 |
@@ -1,4 +0,0 @@
|
|||||||
dist/
|
|
||||||
public/
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
name: Deploy miriamgemeinde
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
SSH_HOST: ${{ vars.PROD_HOST }}
|
|
||||||
SSH_PORT: ${{ vars.PROD_PORT }}
|
|
||||||
SSH_USER: ${{ vars.PROD_USER }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Show resolved non-secret config
|
|
||||||
run: |
|
|
||||||
echo "SSH_HOST=$SSH_HOST"
|
|
||||||
echo "SSH_PORT=$SSH_PORT"
|
|
||||||
echo "SSH_USER=$SSH_USER"
|
|
||||||
|
|
||||||
- name: Prepare SSH
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
printf '%s' "${{ secrets.PROD_SSH_KEY_B64 }}" | base64 -d > ~/.ssh/id_deploy
|
|
||||||
chmod 600 ~/.ssh/id_deploy
|
|
||||||
ssh-keygen -l -f ~/.ssh/id_deploy
|
|
||||||
ssh-keyscan -p "$SSH_PORT" "$SSH_HOST" >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
- name: Test SSH connection
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
ssh -i ~/.ssh/id_deploy \
|
|
||||||
-o StrictHostKeyChecking=no \
|
|
||||||
-o BatchMode=yes \
|
|
||||||
-o ConnectTimeout=10 \
|
|
||||||
-p "$SSH_PORT" \
|
|
||||||
"$SSH_USER@$SSH_HOST" \
|
|
||||||
"echo SSH OK"
|
|
||||||
|
|
||||||
# If you need server-side preparation (e.g. ensure /var/... exists/permissions),
|
|
||||||
# add it in the remote command before running the update script.
|
|
||||||
- name: Run deployment script
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
ssh -i ~/.ssh/id_deploy \
|
|
||||||
-o StrictHostKeyChecking=no \
|
|
||||||
-o BatchMode=yes \
|
|
||||||
-o ConnectTimeout=10 \
|
|
||||||
-p "$SSH_PORT" \
|
|
||||||
"$SSH_USER@$SSH_HOST" \
|
|
||||||
"bash -lc 'set -euo pipefail; TS=\$(date +\"%Y-%m-%d_%H%M%S\"); SRC=\"/var/www/miriamgemeinde/public/images\"; DEST_BASE=\"/home/torsten/miriamgemeinde/backup/\$TS\"; mkdir -p \"\$DEST_BASE\"; if [ -d \"\$SRC\" ]; then cp -a \"\$SRC\" \"\$DEST_BASE/\"; echo \"Backed up \$SRC -> \$DEST_BASE/images\"; else echo \"WARN: \$SRC does not exist; skipping backup\"; fi; if [ -f /home/torsten/update-miriamgemeinde.sh ]; then bash /home/torsten/update-miriamgemeinde.sh; else echo \"ERROR: /home/torsten/update-miriamgemeinde.sh not found\"; exit 127; fi'"
|
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -27,16 +27,3 @@ server.key
|
|||||||
server.cert
|
server.cert
|
||||||
|
|
||||||
public/images/uploads/1ba24ea7-f52c-4179-896f-1909269cab58.jpg
|
public/images/uploads/1ba24ea7-f52c-4179-896f-1909269cab58.jpg
|
||||||
|
|
||||||
# Vue Build-Artefakte (werden beim Deploy generiert)
|
|
||||||
/public/index.html
|
|
||||||
/public/assets/
|
|
||||||
public/js/
|
|
||||||
public/css/
|
|
||||||
public/**/*.map
|
|
||||||
|
|
||||||
# Uploads/Runtime-Dateien nicht versionieren
|
|
||||||
public/images/uploads/
|
|
||||||
actualize.sh
|
|
||||||
files/uploads/GD 24.08.2025-04.01.2026 Stand 12.08.2025.docx
|
|
||||||
.codex
|
|
||||||
|
|||||||
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"development": {
|
"development": {
|
||||||
"username": "miriam_user",
|
"username": "miriam_user",
|
||||||
"password": "hitomisan",
|
"password": "qTCTTWwpEwy3vPDU",
|
||||||
"database": "miriamgemeinde",
|
"database": "miriamgemeinde",
|
||||||
"host": "tsschulz.de",
|
"host": "tsschulz.de",
|
||||||
"dialect": "mysql"
|
"dialect": "mysql"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"username": "miriam_user",
|
"username": "miriam_user",
|
||||||
"password": "hitomisan",
|
"password": "qTCTTWwpEwy3vPDU",
|
||||||
"database": "miriamgemeinde",
|
"database": "miriamgemeinde",
|
||||||
"host": "tsschulz.de",
|
"host": "tsschulz.de",
|
||||||
"dialect": "mysql"
|
"dialect": "mysql"
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"username": "miriam_user",
|
"username": "miriam_user",
|
||||||
"password": "hitomisan",
|
"password": "qTCTTWwpEwy3vPDU",
|
||||||
"database": "miriamgemeinde",
|
"database": "miriamgemeinde",
|
||||||
"host": "tsschulz.de",
|
"host": "tsschulz.de",
|
||||||
"dialect": "mysql"
|
"dialect": "mysql"
|
||||||
|
|||||||
@@ -1,25 +1,8 @@
|
|||||||
const { Sequelize } = require('sequelize');
|
const { Sequelize } = require('sequelize');
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
const envName = process.env.NODE_ENV || 'development';
|
const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vPDU', {
|
||||||
const fileConfig = require('./config.json')[envName];
|
host: 'tsschulz.de',
|
||||||
|
dialect: 'mysql',
|
||||||
if (!fileConfig) {
|
|
||||||
throw new Error(
|
|
||||||
`[DB] Kein Eintrag in config/config.json für NODE_ENV="${envName}".`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const database = process.env.DB_NAME || fileConfig.database;
|
|
||||||
const username = process.env.DB_USER || fileConfig.username;
|
|
||||||
const password =
|
|
||||||
process.env.DB_PASSWORD === undefined ? fileConfig.password : process.env.DB_PASSWORD;
|
|
||||||
const host = process.env.DB_HOST || fileConfig.host;
|
|
||||||
|
|
||||||
const sequelizeOptions = {
|
|
||||||
host,
|
|
||||||
dialect: fileConfig.dialect || 'mysql',
|
|
||||||
dialectOptions: fileConfig.dialectOptions,
|
|
||||||
retry: {
|
retry: {
|
||||||
match: [
|
match: [
|
||||||
/ConnectionError/,
|
/ConnectionError/,
|
||||||
@@ -28,38 +11,24 @@ const sequelizeOptions = {
|
|||||||
/SequelizeHostNotFoundError/,
|
/SequelizeHostNotFoundError/,
|
||||||
/SequelizeHostNotReachableError/,
|
/SequelizeHostNotReachableError/,
|
||||||
/SequelizeInvalidConnectionError/,
|
/SequelizeInvalidConnectionError/,
|
||||||
/SequelizeConnectionTimedOutError/,
|
/SequelizeConnectionTimedOutError/
|
||||||
],
|
],
|
||||||
max: 5,
|
max: 5
|
||||||
},
|
},
|
||||||
pool: {
|
pool: {
|
||||||
max: 5,
|
max: 5,
|
||||||
min: 0,
|
min: 0,
|
||||||
acquire: 30000,
|
acquire: 30000,
|
||||||
idle: 10000,
|
idle: 10000
|
||||||
},
|
}
|
||||||
logging: process.env.DB_LOGGING === '1' ? console.log : false,
|
});
|
||||||
};
|
|
||||||
|
|
||||||
if (process.env.DB_PORT) {
|
|
||||||
sequelizeOptions.port = parseInt(process.env.DB_PORT, 10);
|
|
||||||
} else if (fileConfig.port) {
|
|
||||||
sequelizeOptions.port = fileConfig.port;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sequelize = new Sequelize(database, username, password, sequelizeOptions);
|
|
||||||
|
|
||||||
async function connectWithRetry() {
|
async function connectWithRetry() {
|
||||||
try {
|
try {
|
||||||
await sequelize.authenticate();
|
await sequelize.authenticate();
|
||||||
console.log(
|
console.log('Connection has been established successfully.');
|
||||||
`[DB] Verbindung OK — host=${host} database=${database} user=${username} (NODE_ENV=${envName})`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[DB] Verbindung fehlgeschlagen:', error.message);
|
console.error('Unable to connect to the database:', error);
|
||||||
console.error(
|
|
||||||
`[DB] Erwartete Quelle: config/config.json → "${envName}" oder Umgebungsvariablen DB_HOST, DB_USER, DB_PASSWORD, DB_NAME`
|
|
||||||
);
|
|
||||||
setTimeout(connectWithRetry, 5000);
|
setTimeout(connectWithRetry, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,209 +1,59 @@
|
|||||||
const bcrypt = require('bcryptjs');
|
const AuthService = require('../services/AuthService');
|
||||||
const { User, PasswordResetToken } = require('../models');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const { addTokenToBlacklist } = require('../utils/blacklist');
|
|
||||||
const { transporter, getPasswordResetEmailTemplate } = require('../config/email');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
function delay(ms) {
|
/**
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
* Controller für Authentifizierungsendpunkte
|
||||||
|
*/
|
||||||
|
class AuthController {
|
||||||
|
/**
|
||||||
|
* Benutzerregistrierung
|
||||||
|
*/
|
||||||
|
static register = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const result = await AuthService.register(req.body);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message, 201);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Benutzeranmeldung
|
||||||
|
*/
|
||||||
|
static login = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
const result = await AuthService.login(email, password);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passwort vergessen
|
||||||
|
*/
|
||||||
|
static forgotPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const result = await AuthService.forgotPassword(req.body.email);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passwort zurücksetzen
|
||||||
|
*/
|
||||||
|
static resetPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const result = await AuthService.resetPassword(req.body.token, req.body.password);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Benutzerabmeldung
|
||||||
|
*/
|
||||||
|
static logout = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const authHeader = req.header('Authorization');
|
||||||
|
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
|
||||||
|
const result = await AuthService.logout(token);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.register = async (req, res) => {
|
// Export der statischen Methoden für die Routen
|
||||||
const { name, email, password } = req.body;
|
module.exports = {
|
||||||
if (!name || !email || !password) {
|
register: AuthController.register,
|
||||||
return res.status(400).json({ message: 'Alle Felder sind erforderlich' });
|
login: AuthController.login,
|
||||||
}
|
forgotPassword: AuthController.forgotPassword,
|
||||||
try {
|
resetPassword: AuthController.resetPassword,
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
logout: AuthController.logout
|
||||||
console.log('Register: creating user', { email });
|
};
|
||||||
|
|
||||||
const maxAttempts = 3;
|
|
||||||
let attempt = 0;
|
|
||||||
let createdUser = null;
|
|
||||||
let lastError = null;
|
|
||||||
|
|
||||||
while (attempt < maxAttempts && !createdUser) {
|
|
||||||
try {
|
|
||||||
createdUser = await User.create({ name, email, password: hashedPassword, active: false });
|
|
||||||
} catch (err) {
|
|
||||||
lastError = err;
|
|
||||||
// Spezifisch auf Lock-Timeout reagieren und erneut versuchen
|
|
||||||
if ((err.code === 'ER_LOCK_WAIT_TIMEOUT' || err?.parent?.code === 'ER_LOCK_WAIT_TIMEOUT') && attempt < maxAttempts - 1) {
|
|
||||||
const backoffMs = 300 * (attempt + 1);
|
|
||||||
console.warn(`Register: ER_LOCK_WAIT_TIMEOUT, retry in ${backoffMs}ms (attempt ${attempt + 1}/${maxAttempts})`);
|
|
||||||
await delay(backoffMs);
|
|
||||||
attempt++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!createdUser && lastError) {
|
|
||||||
console.error('Register error (after retries):', lastError);
|
|
||||||
return res.status(503).json({ message: 'Zeitüberschreitung beim Zugriff auf die Datenbank. Bitte erneut versuchen.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Register: user created', { id: createdUser.id });
|
|
||||||
|
|
||||||
const safeUser = {
|
|
||||||
id: createdUser.id,
|
|
||||||
name: createdUser.name,
|
|
||||||
email: createdUser.email,
|
|
||||||
active: createdUser.active,
|
|
||||||
created_at: createdUser.created_at
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.status(201).json({ message: 'Benutzer erfolgreich registriert', user: safeUser });
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === 'SequelizeUniqueConstraintError') {
|
|
||||||
return res.status(400).json({ message: 'Email-Adresse bereits in Verwendung' });
|
|
||||||
}
|
|
||||||
console.error('Register error:', error);
|
|
||||||
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten', error: error.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.login = async (req, res) => {
|
|
||||||
const { email, password } = req.body;
|
|
||||||
if (!email || !password) {
|
|
||||||
return res.status(400).json({ message: 'Email und Passwort sind erforderlich' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const user = await User.findOne({ where: { email } });
|
|
||||||
if (!user) {
|
|
||||||
return res.status(401).json({ message: 'Ungültige Anmeldedaten' });
|
|
||||||
}
|
|
||||||
const validPassword = await bcrypt.compare(password, user.password);
|
|
||||||
if (!validPassword) {
|
|
||||||
return res.status(401).json({ message: 'Ungültige Anmeldedaten' });
|
|
||||||
}
|
|
||||||
if (!user.active) {
|
|
||||||
return res.status(403).json({ message: 'Benutzerkonto ist nicht aktiv' });
|
|
||||||
}
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ id: user.id, name: user.name, email: user.email },
|
|
||||||
'zTxVgptmPl9!_dr%xxx9999(dd)',
|
|
||||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '12h' }
|
|
||||||
);
|
|
||||||
return res.status(200).json({ message: 'Login erfolgreich', token, 'user': user });
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.forgotPassword = async (req, res) => {
|
|
||||||
const { email } = req.body;
|
|
||||||
if (!email) {
|
|
||||||
return res.status(400).json({ message: 'E-Mail-Adresse ist erforderlich' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const user = await User.findOne({ where: { email } });
|
|
||||||
if (!user) {
|
|
||||||
// Aus Sicherheitsgründen immer Erfolg melden, auch wenn E-Mail nicht existiert
|
|
||||||
return res.status(200).json({ message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alte Reset-Tokens für diesen User löschen
|
|
||||||
await PasswordResetToken.destroy({ where: { userId: user.id } });
|
|
||||||
|
|
||||||
// Neuen Reset-Token generieren
|
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
|
||||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 Stunde
|
|
||||||
|
|
||||||
await PasswordResetToken.create({
|
|
||||||
userId: user.id,
|
|
||||||
token,
|
|
||||||
expiresAt
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset-URL generieren
|
|
||||||
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:8080'}/reset-password?token=${token}`;
|
|
||||||
|
|
||||||
// E-Mail versenden
|
|
||||||
const emailTemplate = getPasswordResetEmailTemplate(resetUrl, user.name);
|
|
||||||
|
|
||||||
const mailOptions = {
|
|
||||||
from: process.env.SMTP_FROM || 'noreply@miriamgemeinde.de',
|
|
||||||
to: email,
|
|
||||||
subject: emailTemplate.subject,
|
|
||||||
html: emailTemplate.html,
|
|
||||||
text: emailTemplate.text
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('=== EMAIL SENDING DEBUG ===');
|
|
||||||
console.log('From:', mailOptions.from);
|
|
||||||
console.log('To:', mailOptions.to);
|
|
||||||
console.log('Subject:', mailOptions.subject);
|
|
||||||
console.log('Reset URL:', resetUrl);
|
|
||||||
console.log('===========================');
|
|
||||||
|
|
||||||
await transporter.sendMail(mailOptions);
|
|
||||||
|
|
||||||
console.log('Password reset email sent to:', email);
|
|
||||||
return res.status(200).json({ message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Forgot password error:', error);
|
|
||||||
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.resetPassword = async (req, res) => {
|
|
||||||
const { token, password } = req.body;
|
|
||||||
if (!token || !password) {
|
|
||||||
return res.status(400).json({ message: 'Token und neues Passwort sind erforderlich' });
|
|
||||||
}
|
|
||||||
if (password.length < 6) {
|
|
||||||
return res.status(400).json({ message: 'Passwort muss mindestens 6 Zeichen lang sein' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Token validieren
|
|
||||||
const resetToken = await PasswordResetToken.findOne({
|
|
||||||
where: {
|
|
||||||
token,
|
|
||||||
used: false,
|
|
||||||
expiresAt: {
|
|
||||||
[require('sequelize').Op.gt]: new Date()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
include: [{ model: User, as: 'user' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resetToken) {
|
|
||||||
return res.status(400).json({ message: 'Ungültiger oder abgelaufener Token' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Passwort hashen und aktualisieren
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
|
||||||
await User.update(
|
|
||||||
{ password: hashedPassword },
|
|
||||||
{ where: { id: resetToken.userId } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Token als verwendet markieren
|
|
||||||
await resetToken.update({ used: true });
|
|
||||||
|
|
||||||
console.log('Password reset successful for user:', resetToken.userId);
|
|
||||||
return res.status(200).json({ message: 'Passwort erfolgreich zurückgesetzt' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Reset password error:', error);
|
|
||||||
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.logout = async (req, res) => {
|
|
||||||
const authHeader = req.header('Authorization');
|
|
||||||
if (!authHeader) {
|
|
||||||
return res.status(400).json({ message: 'Kein Token bereitgestellt' });
|
|
||||||
}
|
|
||||||
const token = authHeader.replace('Bearer ', '');
|
|
||||||
try {
|
|
||||||
addTokenToBlacklist(token);
|
|
||||||
return res.status(200).json({ message: 'Logout erfolgreich' });
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return res.status(500).json({ message: 'Ein Fehler ist beim Logout aufgetreten' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -3,16 +3,7 @@ const { Op } = require('sequelize');
|
|||||||
|
|
||||||
const getAllContactPersons = async (req, res) => {
|
const getAllContactPersons = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
const contactPersons = await ContactPerson.findAll({
|
const contactPersons = await ContactPerson.findAll({
|
||||||
where: {
|
|
||||||
[Op.or]: [
|
|
||||||
{ expiryDate: null },
|
|
||||||
{ expiryDate: { [Op.gte]: today } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Position,
|
model: Position,
|
||||||
@@ -88,14 +79,6 @@ const filterContactPersons = async (req, res) => {
|
|||||||
const where = {};
|
const where = {};
|
||||||
const having = [];
|
const having = [];
|
||||||
|
|
||||||
// Filter für nicht abgelaufene Kontaktpersonen
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
where[Op.or] = [
|
|
||||||
{ expiryDate: null },
|
|
||||||
{ expiryDate: { [Op.gte]: today } }
|
|
||||||
];
|
|
||||||
|
|
||||||
if (config.selection.id && config.selection.id === 'all') {
|
if (config.selection.id && config.selection.id === 'all') {
|
||||||
// No additional filter needed for "all"
|
// No additional filter needed for "all"
|
||||||
} else if (config.selection.id) {
|
} else if (config.selection.id) {
|
||||||
|
|||||||
@@ -1,208 +1,32 @@
|
|||||||
const { Event, Institution, EventPlace, ContactPerson, EventType, EventContactPerson, sequelize } = require('../models');
|
const EventService = require('../services/EventService');
|
||||||
const { Op, fn, col, where: sequelizeWhere } = require('sequelize');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
|
|
||||||
function buildUpcomingWhere() {
|
exports.getAllEvents = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
return {
|
const events = await EventService.getAllEvents();
|
||||||
[Op.or]: [
|
ErrorHandler.successResponse(res, events, 'Events erfolgreich abgerufen');
|
||||||
{ date: { [Op.eq]: null } },
|
});
|
||||||
// Fixed-date events: only today or future (date-only compare).
|
|
||||||
sequelizeWhere(fn('DATE', col('date')), { [Op.gte]: fn('CURDATE') }),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllEvents = async (req, res) => {
|
exports.getEventById = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const event = await EventService.getEventById(req.params.id);
|
||||||
const includePast = String(req.query?.includePast || '').toLowerCase();
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich abgerufen');
|
||||||
const wantsPast = includePast === '1' || includePast === 'true' || includePast === 'yes';
|
});
|
||||||
|
|
||||||
const where = wantsPast
|
exports.filterEvents = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
? undefined
|
const result = await EventService.filterEvents(req.body);
|
||||||
: buildUpcomingWhere();
|
ErrorHandler.successResponse(res, result, 'Events erfolgreich gefiltert');
|
||||||
|
});
|
||||||
|
|
||||||
const events = await Event.findAll({
|
exports.createEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
where,
|
const event = await EventService.createEvent(req.body);
|
||||||
include: [
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich erstellt', 201);
|
||||||
{ model: Institution, as: 'institution' },
|
});
|
||||||
{ model: EventPlace, as: 'eventPlace' },
|
|
||||||
{ model: EventType, as: 'eventType' },
|
|
||||||
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
|
|
||||||
],
|
|
||||||
order: [
|
|
||||||
['date', 'ASC'],
|
|
||||||
['time', 'ASC'],
|
|
||||||
['name', 'ASC'],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
res.json(events);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Failed to fetch events' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterEvents = async (req, res) => {
|
exports.updateEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const event = await EventService.updateEvent(req.params.id, req.body);
|
||||||
const request = req.body;
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich aktualisiert');
|
||||||
const where = buildUpcomingWhere();
|
});
|
||||||
const order = [
|
|
||||||
['date', 'ASC'],
|
|
||||||
['time', 'ASC']
|
|
||||||
];
|
|
||||||
|
|
||||||
if (request.id === 'all') {
|
exports.deleteEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const events = await Event.findAll({
|
const result = await EventService.deleteEvent(req.params.id);
|
||||||
where,
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
include: [
|
});
|
||||||
{ model: Institution, as: 'institution' },
|
|
||||||
{ model: EventPlace, as: 'eventPlace' },
|
|
||||||
{ model: EventType, as: 'eventType' },
|
|
||||||
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
|
|
||||||
],
|
|
||||||
order: order,
|
|
||||||
logging: console.log // Log the generated SQL query
|
|
||||||
});
|
|
||||||
return res.json({ events });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.id === 'home') {
|
|
||||||
const events = await Event.findAll({
|
|
||||||
where: {
|
|
||||||
alsoOnHomepage: 1,
|
|
||||||
...buildUpcomingWhere(),
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{ model: Institution, as: 'institution' },
|
|
||||||
{ model: EventPlace, as: 'eventPlace' },
|
|
||||||
{ model: EventType, as: 'eventType' },
|
|
||||||
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } },
|
|
||||||
],
|
|
||||||
order: order,
|
|
||||||
});
|
|
||||||
return res.json({ events });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!request.id && !request.places && !request.types) {
|
|
||||||
return res.json({ events: [], places: [], types: [], contactPersons: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.id) {
|
|
||||||
where.id = request.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.places && request.places.length > 0) {
|
|
||||||
where.event_place_id = {
|
|
||||||
[Op.in]: request.places.map(id => parseInt(id))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.types && request.types.length > 0) {
|
|
||||||
where.eventTypeId = {
|
|
||||||
[Op.in]: request.types.map(id => parseInt(id))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = await Event.findAll({
|
|
||||||
where,
|
|
||||||
include: [
|
|
||||||
{ model: Institution, as: 'institution' },
|
|
||||||
{ model: EventPlace, as: 'eventPlace' },
|
|
||||||
{ model: EventType, as: 'eventType' },
|
|
||||||
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
|
|
||||||
],
|
|
||||||
order: order,
|
|
||||||
});
|
|
||||||
const displayFields = request.display ? request.display : [];
|
|
||||||
|
|
||||||
const filteredEvents = events.map(event => {
|
|
||||||
const filteredEvent = { ...event.toJSON() };
|
|
||||||
|
|
||||||
if (!displayFields.includes('name')) delete filteredEvent.name;
|
|
||||||
if (!displayFields.includes('type')) delete filteredEvent.eventType;
|
|
||||||
if (!displayFields.includes('place')) delete filteredEvent.eventPlace;
|
|
||||||
if (!displayFields.includes('description')) delete filteredEvent.description;
|
|
||||||
if (!displayFields.includes('time')) delete filteredEvent.time;
|
|
||||||
if (!displayFields.includes('time')) delete filteredEvent.endTime;
|
|
||||||
if (!displayFields.includes('contactPerson')) delete filteredEvent.contactPersons;
|
|
||||||
if (!displayFields.includes('day')) delete filteredEvent.dayOfWeek;
|
|
||||||
if (!displayFields.includes('institution')) delete filteredEvent.institution;
|
|
||||||
|
|
||||||
return filteredEvent;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ events: filteredEvents });
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Failed to filter events' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createEvent = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { contactPersonIds, ...eventData } = req.body;
|
|
||||||
eventData.alsoOnHomepage = eventData.alsoOnHomepage ?? 0;
|
|
||||||
const event = await Event.create(eventData);
|
|
||||||
if (contactPersonIds) {
|
|
||||||
await event.setContactPersons(contactPersonIds);
|
|
||||||
}
|
|
||||||
res.status(201).json(event);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Failed to create event' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateEvent = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { contactPersonIds, ...eventData } = req.body;
|
|
||||||
const event = await Event.findByPk(id);
|
|
||||||
if (!event) {
|
|
||||||
return res.status(404).json({ error: 'Event not found' });
|
|
||||||
}
|
|
||||||
await event.update(eventData);
|
|
||||||
if (contactPersonIds) {
|
|
||||||
await event.setContactPersons(contactPersonIds);
|
|
||||||
}
|
|
||||||
res.status(200).json(event);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Failed to update event' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteEvent = async (req, res) => {
|
|
||||||
const transaction = await sequelize.transaction();
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
// Erst Zuordnungen in der Join-Tabelle löschen, damit FK-Constraints erfüllt sind.
|
|
||||||
await EventContactPerson.destroy({
|
|
||||||
where: { event_id: id },
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleted = await Event.destroy({
|
|
||||||
where: { id: id },
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
if (deleted) {
|
|
||||||
await transaction.commit();
|
|
||||||
res.status(204).json();
|
|
||||||
} else {
|
|
||||||
await transaction.rollback();
|
|
||||||
res.status(404).json({ error: 'Event not found' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
res.status(500).json({ error: 'Failed to delete event' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getAllEvents,
|
|
||||||
createEvent,
|
|
||||||
updateEvent,
|
|
||||||
deleteEvent,
|
|
||||||
filterEvents
|
|
||||||
};
|
|
||||||
@@ -16,7 +16,6 @@ const storage = multer.diskStorage({
|
|||||||
const upload = multer({ storage });
|
const upload = multer({ storage });
|
||||||
|
|
||||||
exports.uploadImage = upload.single('image');
|
exports.uploadImage = upload.single('image');
|
||||||
exports.uploadImages = upload.array('images');
|
|
||||||
|
|
||||||
exports.getAllPages = async (req, res) => {
|
exports.getAllPages = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -52,51 +51,6 @@ exports.saveImageDetails = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.saveImageDetailsBulk = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { baseTitle, description, page, startNumber } = req.body;
|
|
||||||
const files = req.files || [];
|
|
||||||
|
|
||||||
if (!baseTitle || !String(baseTitle).trim()) {
|
|
||||||
return res.status(400).json({ error: 'Bitte einen Basis-Titel angeben.' });
|
|
||||||
}
|
|
||||||
if (files.length === 0) {
|
|
||||||
return res.status(400).json({ error: 'Bitte mindestens ein Bild auswählen.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageItem = page ? await Page.findAll({ where: { link: page } }) : [];
|
|
||||||
const pageId = pageItem && pageItem[0] ? pageItem[0].id : null;
|
|
||||||
const firstNumber = Number.parseInt(startNumber, 10);
|
|
||||||
const startAt = Number.isFinite(firstNumber) ? firstNumber : 1;
|
|
||||||
|
|
||||||
const createdImages = [];
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const file = files[i];
|
|
||||||
const runningNumber = startAt + i;
|
|
||||||
const title = `${String(baseTitle).trim()} ${runningNumber}`;
|
|
||||||
const newImage = await Image.create({
|
|
||||||
id: uuidv4(),
|
|
||||||
filename: file.filename,
|
|
||||||
title,
|
|
||||||
description: description || '',
|
|
||||||
pageId,
|
|
||||||
});
|
|
||||||
createdImages.push(newImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Menübild nur dann setzen, wenn genau ein Bild hochgeladen wurde.
|
|
||||||
if (page && createdImages.length === 1) {
|
|
||||||
const imageUrl = `/uploads/${createdImages[0].filename}`;
|
|
||||||
await MenuItem.update({ image: imageUrl }, { where: { link: page } });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json(createdImages);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Bulk-Upload der Bilder:', error);
|
|
||||||
res.status(500).json({ error: 'Fehler beim Bulk-Upload der Bilder' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getImages = async (req, res) => {
|
exports.getImages = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const images = await Image.findAll({ order: [['title', 'ASC']] });
|
const images = await Image.findAll({ order: [['title', 'ASC']] });
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
const { LiturgicalDay } = require('../models');
|
|
||||||
const { Op } = require('sequelize');
|
|
||||||
const axios = require('axios');
|
|
||||||
|
|
||||||
// Alle liturgischen Tage abrufen
|
|
||||||
const getAllLiturgicalDays = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const days = await LiturgicalDay.findAll({
|
|
||||||
order: [['date', 'ASC']]
|
|
||||||
});
|
|
||||||
res.status(200).json(days);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
res.status(500).json({ message: 'Fehler beim Abrufen der liturgischen Tage' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Eindeutige Namen für Multiselect abrufen
|
|
||||||
const getLiturgicalDayNames = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const days = await LiturgicalDay.findAll({
|
|
||||||
attributes: ['dayName'],
|
|
||||||
group: ['dayName'],
|
|
||||||
order: [['dayName', 'ASC']]
|
|
||||||
});
|
|
||||||
const names = days.map(day => day.dayName);
|
|
||||||
res.status(200).json(names);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
res.status(500).json({ message: 'Fehler beim Abrufen der Tag-Namen' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// HTML von liturgischem Kalender parsen und in DB speichern
|
|
||||||
const loadLiturgicalYear = async (req, res) => {
|
|
||||||
const { year } = req.body;
|
|
||||||
|
|
||||||
if (!year) {
|
|
||||||
return res.status(400).json({ message: 'Jahr ist erforderlich' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
if (year < currentYear || year > currentYear + 2) {
|
|
||||||
return res.status(400).json({ message: 'Jahr muss zwischen aktuellem Jahr und 2 Jahren in der Zukunft liegen' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = `https://www.eike-fleer.de/liturgischer-kalender/${year}.htm`;
|
|
||||||
const response = await axios.get(url, {
|
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const html = response.data;
|
|
||||||
|
|
||||||
// Parse HTML - suche nach Tabellenzeilen mit Datum und Name
|
|
||||||
// Format: "DD.MM.YYYY DayName"
|
|
||||||
const regex = /(\d{2}\.\d{2}\.\d{4})\s*(?: |\s)+(.+?)(?:<\/|$)/gi;
|
|
||||||
const matches = [...html.matchAll(regex)];
|
|
||||||
|
|
||||||
const liturgicalDays = [];
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
const dateStr = match[1]; // DD.MM.YYYY
|
|
||||||
let dayName = match[2];
|
|
||||||
|
|
||||||
// Bereinige den Tag-Namen von HTML-Tags und Entities
|
|
||||||
dayName = dayName
|
|
||||||
.replace(/<[^>]*>/g, '') // Entferne HTML-Tags
|
|
||||||
.replace(/ /g, ' ') // Ersetze
|
|
||||||
.replace(/ä/g, 'ä')
|
|
||||||
.replace(/ö/g, 'ö')
|
|
||||||
.replace(/ü/g, 'ü')
|
|
||||||
.replace(/Ä/g, 'Ä')
|
|
||||||
.replace(/Ö/g, 'Ö')
|
|
||||||
.replace(/Ü/g, 'Ü')
|
|
||||||
.replace(/ß/g, 'ß')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
// Konvertiere Datum von DD.MM.YYYY zu YYYY-MM-DD
|
|
||||||
const [day, month, yearPart] = dateStr.split('.');
|
|
||||||
const isoDate = `${yearPart}-${month}-${day}`;
|
|
||||||
|
|
||||||
if (dayName && dayName.length > 0) {
|
|
||||||
liturgicalDays.push({
|
|
||||||
date: isoDate,
|
|
||||||
dayName: dayName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (liturgicalDays.length === 0) {
|
|
||||||
return res.status(500).json({ message: 'Keine liturgischen Tage gefunden. Möglicherweise hat sich das HTML-Format geändert.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speichere oder aktualisiere die Einträge
|
|
||||||
for (const day of liturgicalDays) {
|
|
||||||
await LiturgicalDay.upsert({
|
|
||||||
date: day.date,
|
|
||||||
dayName: day.dayName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
message: `${liturgicalDays.length} liturgische Tage für ${year} erfolgreich geladen`,
|
|
||||||
count: liturgicalDays.length
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden der liturgischen Tage:', error);
|
|
||||||
if (error.response && error.response.status === 404) {
|
|
||||||
return res.status(404).json({ message: `Liturgischer Kalender für ${year} nicht gefunden` });
|
|
||||||
}
|
|
||||||
res.status(500).json({ message: 'Fehler beim Laden der liturgischen Tage', error: error.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Einzelnen Tag erstellen
|
|
||||||
const createLiturgicalDay = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const day = await LiturgicalDay.create(req.body);
|
|
||||||
res.status(201).json(day);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
res.status(500).json({ message: 'Fehler beim Erstellen des liturgischen Tags' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tag löschen
|
|
||||||
const deleteLiturgicalDay = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const deleted = await LiturgicalDay.destroy({
|
|
||||||
where: { id }
|
|
||||||
});
|
|
||||||
if (deleted) {
|
|
||||||
res.status(200).json({ message: 'Liturgischer Tag erfolgreich gelöscht' });
|
|
||||||
} else {
|
|
||||||
res.status(404).json({ message: 'Liturgischer Tag nicht gefunden' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
res.status(500).json({ message: 'Fehler beim Löschen des liturgischen Tags' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getAllLiturgicalDays,
|
|
||||||
getLiturgicalDayNames,
|
|
||||||
loadLiturgicalYear,
|
|
||||||
createLiturgicalDay,
|
|
||||||
deleteLiturgicalDay
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,31 +1,12 @@
|
|||||||
const { MenuItem } = require('../models');
|
const MenuDataService = require('../services/MenuDataService');
|
||||||
const fetchMenuData = require('../utils/fetchMenuData');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
|
|
||||||
exports.getMenuData = async (req, res) => {
|
exports.getMenuData = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const menuData = await MenuDataService.getMenuData();
|
||||||
const menuData = await fetchMenuData();
|
ErrorHandler.successResponse(res, menuData, 'Menü-Daten erfolgreich abgerufen');
|
||||||
res.json(menuData);
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error('getMenuData:', error);
|
|
||||||
res.status(500).json({ error: 'Menü konnte nicht geladen werden (Datenbank nicht erreichbar).' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.saveMenuData = async (req, res) => {
|
exports.saveMenuData = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const result = await MenuDataService.saveMenuData(req.body);
|
||||||
const menuData = req.body;
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
const adjustedMenuData = menuData.map(item => {
|
});
|
||||||
item.parent_id = item.parent_id < 0 ? null : item.parent_id;
|
|
||||||
return item;
|
|
||||||
})
|
|
||||||
.sort((a, b) => (a.parent_id === null ? -1 : 1) - (b.parent_id === null ? -1 : 1));
|
|
||||||
await MenuItem.destroy({ where: {} });
|
|
||||||
for (const item of adjustedMenuData) {
|
|
||||||
await MenuItem.create(item);
|
|
||||||
}
|
|
||||||
res.status(200).send('Menü-Daten erfolgreich gespeichert');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Speichern der Menü-Daten:', error);
|
|
||||||
res.status(500).send('Fehler beim Speichern der Menü-Daten');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,48 +1,27 @@
|
|||||||
// controllers/pageController.js
|
const PageService = require('../services/PageService');
|
||||||
const { Page } = require('../models');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
|
|
||||||
exports.getMenuData = async (req, res) => {
|
exports.getMenuData = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const pages = await PageService.getAllPages();
|
||||||
const pages = await Page.findAll({
|
ErrorHandler.successResponse(res, pages, 'Seiten erfolgreich abgerufen');
|
||||||
attributes: ['link', 'name']
|
});
|
||||||
});
|
|
||||||
res.json(pages);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Abrufen der Seiten:', error);
|
|
||||||
res.status(500).json({ message: 'Fehler beim Abrufen der Seiten' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getPageContent = async (req, res) => {
|
exports.getPageContent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const result = await PageService.getPageContent(req.query.link);
|
||||||
const page = await Page.findOne({
|
ErrorHandler.successResponse(res, result, 'Seiteninhalt erfolgreich abgerufen');
|
||||||
where: { link: req.query.link }
|
});
|
||||||
});
|
|
||||||
if (page) {
|
|
||||||
res.json({ content: page.content });
|
|
||||||
} else {
|
|
||||||
res.json({ content: "" });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Laden des Seiteninhalts:', error);
|
|
||||||
res.status(500).json({ message: 'Fehler beim Laden des Seiteninhalts' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.savePageContent = async (req, res) => {
|
exports.savePageContent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const result = await PageService.savePageContent(req.body);
|
||||||
const { link, name, content } = req.body;
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
let page = await Page.findOne({ where: { link } });
|
});
|
||||||
if (page) {
|
|
||||||
page.content = content;
|
exports.getPageById = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
page.name = name;
|
const page = await PageService.getPageById(req.params.id);
|
||||||
} else {
|
ErrorHandler.successResponse(res, page, 'Seite erfolgreich abgerufen');
|
||||||
page = await Page.create({ link, name, content });
|
});
|
||||||
}
|
|
||||||
await page.save();
|
exports.deletePage = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
res.json({ message: 'Seiteninhalt gespeichert', page });
|
const result = await PageService.deletePage(req.params.id);
|
||||||
} catch (error) {
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
console.error('Fehler beim Speichern des Seiteninhalts:', error);
|
});
|
||||||
res.status(500).json({ message: 'Fehler beim Speichern des Seiteninhalts' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,104 +1,42 @@
|
|||||||
const { User } = require('../models');
|
const UserService = require('../services/UserService');
|
||||||
|
const UserValidator = require('../validators/UserValidator');
|
||||||
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
|
|
||||||
exports.getAllUsers = async (req, res) => {
|
exports.getAllUsers = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const users = await UserService.getAllUsers();
|
||||||
const users = await User.findAll({
|
ErrorHandler.successResponse(res, users, 'Benutzer erfolgreich abgerufen');
|
||||||
order: [['name', 'ASC']],
|
});
|
||||||
attributes: ['id', 'name', 'email', 'active', 'created_at'] // Passwort ausschließen
|
|
||||||
});
|
|
||||||
res.status(200).json(users);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching users:', error);
|
|
||||||
res.status(500).json({ message: 'Error fetching users' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getUserById = async (req, res) => {
|
exports.getUserById = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
UserValidator.validateId(req.params.id);
|
||||||
const user = await User.findByPk(req.params.id, {
|
const user = await UserService.getUserById(req.params.id);
|
||||||
attributes: ['id', 'name', 'email', 'active', 'created_at'] // Passwort ausschließen
|
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich abgerufen');
|
||||||
});
|
});
|
||||||
if (user) {
|
|
||||||
res.status(200).json(user);
|
|
||||||
} else {
|
|
||||||
res.status(404).json({ message: 'User not found' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching user:', error);
|
|
||||||
res.status(500).json({ message: 'Error fetching user' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createUser = async (req, res) => {
|
exports.createUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
UserValidator.validateCreateUser(req.body);
|
||||||
const user = await User.create(req.body);
|
const user = await UserService.createUser(req.body);
|
||||||
|
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich erstellt', 201);
|
||||||
// Sichere User-Daten zurückgeben (ohne Passwort)
|
});
|
||||||
const safeUser = {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
active: user.active,
|
|
||||||
created_at: user.created_at
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(201).json(safeUser);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating user:', error);
|
|
||||||
res.status(500).json({ message: 'Error creating user' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.updateUser = async (req, res) => {
|
exports.updateUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
UserValidator.validateId(req.params.id);
|
||||||
const user = await User.findByPk(req.params.id);
|
UserValidator.validateUpdateUser(req.body);
|
||||||
if (user) {
|
const user = await UserService.updateUser(req.params.id, req.body);
|
||||||
// Erstelle eine Kopie der Request-Daten ohne sensible Felder
|
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich aktualisiert');
|
||||||
const updateData = { ...req.body };
|
});
|
||||||
|
|
||||||
// Entferne sensible Felder, die niemals über diese Route geändert werden dürfen
|
|
||||||
delete updateData.password;
|
|
||||||
delete updateData.id;
|
|
||||||
delete updateData.created_at;
|
|
||||||
|
|
||||||
// Setze updated_at auf aktuelle Zeit
|
|
||||||
updateData.updated_at = new Date();
|
|
||||||
|
|
||||||
// Logging für Debugging
|
|
||||||
console.log('Updating user:', req.params.id, 'with data:', updateData);
|
|
||||||
|
|
||||||
await user.update(updateData);
|
|
||||||
|
|
||||||
// Sichere User-Daten zurückgeben (ohne Passwort)
|
|
||||||
const safeUser = {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
active: user.active,
|
|
||||||
created_at: user.created_at
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(safeUser);
|
|
||||||
} else {
|
|
||||||
res.status(404).json({ message: 'User not found' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating user:', error);
|
|
||||||
res.status(500).json({ message: 'Error updating user' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.deleteUser = async (req, res) => {
|
exports.deleteUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
UserValidator.validateId(req.params.id);
|
||||||
const user = await User.findByPk(req.params.id);
|
await UserService.deleteUser(req.params.id);
|
||||||
if (user) {
|
ErrorHandler.successResponse(res, null, 'Benutzer erfolgreich gelöscht');
|
||||||
await user.destroy();
|
});
|
||||||
res.status(200).json({ message: 'User deleted successfully' });
|
|
||||||
} else {
|
// Neue Route für Passwort-Änderung
|
||||||
res.status(404).json({ message: 'User not found' });
|
exports.changePassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
}
|
const { currentPassword, newPassword } = req.body;
|
||||||
} catch (error) {
|
UserValidator.validateId(req.params.id);
|
||||||
console.error('Error deleting user:', error);
|
UserValidator.validatePasswordChange(currentPassword, newPassword);
|
||||||
res.status(500).json({ message: 'Error deleting user' });
|
await UserService.changePassword(req.params.id, currentPassword, newPassword);
|
||||||
}
|
ErrorHandler.successResponse(res, null, 'Passwort erfolgreich geändert');
|
||||||
};
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
|||||||
const { WorshipLeader } = require('../models');
|
|
||||||
const { Op } = require('sequelize');
|
|
||||||
|
|
||||||
function normalizeLeaderPayload(body) {
|
|
||||||
const code = String(body.code || '').trim();
|
|
||||||
const name = String(body.name || '').trim();
|
|
||||||
const aliases = String(body.aliases || '').trim();
|
|
||||||
const active = body.active === undefined ? true : !!body.active;
|
|
||||||
return { code, name, aliases, active };
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getAllWorshipLeaders = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const includeInactive = String(req.query?.includeInactive || '').toLowerCase();
|
|
||||||
const wantsInactive = includeInactive === '1' || includeInactive === 'true' || includeInactive === 'yes';
|
|
||||||
|
|
||||||
const where = wantsInactive ? undefined : { active: true };
|
|
||||||
const leaders = await WorshipLeader.findAll({
|
|
||||||
where,
|
|
||||||
order: [['code', 'ASC']],
|
|
||||||
});
|
|
||||||
res.json(leaders);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('getAllWorshipLeaders:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to fetch worship leaders' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createWorshipLeader = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const payload = normalizeLeaderPayload(req.body || {});
|
|
||||||
if (!payload.code || !payload.name) {
|
|
||||||
return res.status(400).json({ message: 'code und name sind Pflichtfelder.' });
|
|
||||||
}
|
|
||||||
const existing = await WorshipLeader.findOne({ where: { code: payload.code } });
|
|
||||||
if (existing) {
|
|
||||||
return res.status(409).json({ message: `Kürzel "${payload.code}" existiert bereits.` });
|
|
||||||
}
|
|
||||||
const created = await WorshipLeader.create(payload);
|
|
||||||
res.status(201).json(created);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('createWorshipLeader:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to create worship leader' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.updateWorshipLeader = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const leader = await WorshipLeader.findByPk(id);
|
|
||||||
if (!leader) {
|
|
||||||
return res.status(404).json({ message: 'Worship leader not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = normalizeLeaderPayload(req.body || {});
|
|
||||||
if (!payload.code || !payload.name) {
|
|
||||||
return res.status(400).json({ message: 'code und name sind Pflichtfelder.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeClash = await WorshipLeader.findOne({
|
|
||||||
where: { code: payload.code, id: { [Op.ne]: id } },
|
|
||||||
});
|
|
||||||
if (codeClash) {
|
|
||||||
return res.status(409).json({ message: `Kürzel "${payload.code}" existiert bereits.` });
|
|
||||||
}
|
|
||||||
|
|
||||||
await leader.update(payload);
|
|
||||||
res.json(leader);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('updateWorshipLeader:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to update worship leader' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.deleteWorshipLeader = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const deleted = await WorshipLeader.destroy({ where: { id } });
|
|
||||||
if (!deleted) {
|
|
||||||
return res.status(404).json({ message: 'Worship leader not found' });
|
|
||||||
}
|
|
||||||
res.status(204).json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('deleteWorshipLeader:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to delete worship leader' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
40
deploy.sh
40
deploy.sh
@@ -9,36 +9,9 @@ NC="\033[0m"
|
|||||||
log() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"; }
|
log() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"; }
|
||||||
err() { echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}" 1>&2; }
|
err() { echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}" 1>&2; }
|
||||||
|
|
||||||
ensure_command() {
|
|
||||||
local cmd="$1"
|
|
||||||
command -v "$cmd" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrap_node() {
|
|
||||||
# In non-interactive SSH sessions, node/npm might not be on PATH (e.g. nvm in .bashrc).
|
|
||||||
if ensure_command npm && ensure_command node; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "$HOME/.nvm/nvm.sh"
|
|
||||||
# Prefer default alias if configured, otherwise keep current.
|
|
||||||
nvm use --silent default >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrap_node
|
|
||||||
if ! ensure_command git; then err "git not found in PATH"; exit 127; fi
|
|
||||||
if ! ensure_command npm; then err "npm not found in PATH"; exit 127; fi
|
|
||||||
|
|
||||||
log "Fetching latest changes..."
|
log "Fetching latest changes..."
|
||||||
git fetch --all --prune || { err "git fetch failed"; exit 1; }
|
git fetch --all --prune || { err "git fetch failed"; exit 1; }
|
||||||
|
|
||||||
log "Cleaning generated frontend artifacts..."
|
|
||||||
git restore -- package-lock.json package.json 2>/dev/null || true
|
|
||||||
git restore -- public/index.html 2>/dev/null || true
|
|
||||||
git clean -fd -- dist public/assets || { err "cleanup failed"; exit 1; }
|
|
||||||
|
|
||||||
log "Pulling latest changes..."
|
log "Pulling latest changes..."
|
||||||
git pull --ff-only || { err "git pull failed"; exit 1; }
|
git pull --ff-only || { err "git pull failed"; exit 1; }
|
||||||
|
|
||||||
@@ -53,17 +26,6 @@ mkdir -p public || true
|
|||||||
cp -R dist/* public/ || { err "copy dist failed"; exit 1; }
|
cp -R dist/* public/ || { err "copy dist failed"; exit 1; }
|
||||||
|
|
||||||
log "Restarting service miriamgemeinde..."
|
log "Restarting service miriamgemeinde..."
|
||||||
if [ "${EUID:-$(id -u)}" -eq 0 ]; then
|
sudo systemctl restart miriamgemeinde || { err "service restart failed"; exit 1; }
|
||||||
systemctl restart miriamgemeinde || { err "service restart failed"; exit 1; }
|
|
||||||
else
|
|
||||||
# Non-interactive deploys (CI) must not prompt for a sudo password.
|
|
||||||
# Configure on the server (recommended):
|
|
||||||
# sudo visudo -f /etc/sudoers.d/miriamgemeinde
|
|
||||||
# torsten ALL=NOPASSWD:/bin/systemctl restart miriamgemeinde,/bin/systemctl status miriamgemeinde
|
|
||||||
sudo -n systemctl restart miriamgemeinde || {
|
|
||||||
err "service restart failed (no password prompt). Configure passwordless sudo for 'systemctl restart miriamgemeinde' or run deploy as root."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Deployment completed successfully."
|
log "Deployment completed successfully."
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
# Konzept: Modernisierung von Design und Bedienbarkeit
|
|
||||||
|
|
||||||
**Projekt:** Evangelische Miriamgemeinde Frankfurt (Vue.js-Webauftritt)
|
|
||||||
**Stand:** April 2026
|
|
||||||
**Ziel:** Zeitgemäße, klare Oberfläche mit hoher Vertrauenswürdigkeit; kirchlich-seriös, ohne „Startup-Optik“.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Zielbild und Leitlinien
|
|
||||||
|
|
||||||
### 1.1 Positionierung
|
|
||||||
|
|
||||||
Die Website ist **Informations- und Gemeinschaftsangebot** einer evangelischen Gemeinde. Sie soll:
|
|
||||||
|
|
||||||
- **verlässlich und ruhig** wirken (kein visuelles „Rauschen“),
|
|
||||||
- **inhaltlich im Vordergrund** stehen (Typografie, Lesbarkeit, klare Hierarchie),
|
|
||||||
- **digital souverän** wirken (gute Struktur, schnelle Orientierung, respektvolle Hilfen für alle Nutzergruppen).
|
|
||||||
|
|
||||||
### 1.2 Nicht verhandelbar: EKHN-Violett
|
|
||||||
|
|
||||||
Die **Grundfarbe EKHN-Violett** bleibt die primäre Markenfarbe. Im Code aktuell u. a. als `#9400ff` mit Hover `#7a00d1` genutzt (Navigation). Diese Farbe wird **nicht ersetzt oder „neu interpretiert“**.
|
|
||||||
|
|
||||||
- Sie wird als **CSS-Design-Token** zentral definiert (z. B. `--color-ekhn-violet`, `--color-ekhn-violet-hover`), damit alle Komponenten konsistent darauf zugreifen.
|
|
||||||
- **Abstufungen** (heller für Hintergründe, transparenter für Overlays) sind **zulässig**, solange die wahrgenommene Marke **dieselbe Violett-Identität** bleibt.
|
|
||||||
- Kontrast zu Text und Icons muss **WCAG-konform** sein (siehe Abschnitt 7).
|
|
||||||
|
|
||||||
### 1.3 Seriosität vs. Modernität
|
|
||||||
|
|
||||||
| Modern (gewünscht) | Vermeiden (für kirchlichen Kontext) |
|
|
||||||
|--------------------|-------------------------------------|
|
|
||||||
| Klares Raster, viel Weißraum | Neon-Verläufe, Spielereien |
|
|
||||||
| Ruhige, lesbare Schrift | Display-Fonts, übertriebene Größen |
|
|
||||||
| Deutliche Fokuszustände (Tastatur) | Aggressive Animationen |
|
|
||||||
| Einheitliche Komponenten | Zufällige Abstände und Stile pro Seite |
|
|
||||||
| Verständliche Navigation | „Experimentelle“ Menüs ohne klare Labels |
|
|
||||||
|
|
||||||
**Leitmotiv:** *Ruhige Sachlichkeit mit warmer, einladender Sprache in der UI (Beschriftungen, Fehlermeldungen, leere Zustände).*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Kurze Ist-Analyse (Ausgangslage)
|
|
||||||
|
|
||||||
Aus dem aktuellen Aufbau (u. a. `AppComponent.vue`, `NavbarComponent.vue`, `HeaderComponent.vue`, `FooterComponent.vue`):
|
|
||||||
|
|
||||||
- **Typografie:** durchgängig `Arial, sans-serif` – funktional, aber wenig Profil; keine skalierbare Typo-Skala.
|
|
||||||
- **Layout:** starre `min-width: 1000px` in der Hauptspalte begünstigt horizontales Scrollen auf Tablets/kleineren Viewports; Zwei-Spalten-Logik mit Breakpoints ist vorhanden, sollte aber **inhaltlich und technisch** weiterentwickelt werden.
|
|
||||||
- **Farben:** Violett in der Navigation; Footer dunkelblau (`#0b1735`); rechte Spalte hellblau (`#d9e2f3`); Header mit Schlagschatten in Lavendeltönen – teils **uneinheitlich** zur Markenfarbe.
|
|
||||||
- **Navigation:** Hamburger/Menü-Button unter 768px; Dropdowns mit Hover – auf Touch-Geräten und für Tastaturnutzer ist hier **Verbesserungspotenzial** (Fokus, ARIA, Touch-Targets).
|
|
||||||
- **Footer:** Login-Link in Grau auf dunklem Grund – **Kontrast** prüfen und ggf. anpassen (ohne Marke zu verändern).
|
|
||||||
|
|
||||||
Diese Punkte fließen als konkrete Maßnahmen in die Phasenplanung (Abschnitt 9) ein.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Design-System: Farben
|
|
||||||
|
|
||||||
### 3.1 Primär (unverändert)
|
|
||||||
|
|
||||||
| Token (Vorschlag) | Verwendung | Hex (Ist) |
|
|
||||||
|-------------------|------------|-----------|
|
|
||||||
| `--color-brand-primary` | Navigationsleiste, primäre Buttons, aktive Zustände | `#9400ff` |
|
|
||||||
| `--color-brand-primary-hover` | Hover, aktive Menüpunkte | `#7a00d1` |
|
|
||||||
|
|
||||||
*Hinweis:* Falls das offizielle EKHN-Handbuch eine minimal abweichende Hex-Angabe vorsieht, **eine** kanonische Quelle festlegen und nur diese verwenden – weiterhin **kein** Wechsel zu einer anderen Farbfamilie.
|
|
||||||
|
|
||||||
### 3.2 Neutrale Flächen (ergänzend, nicht markenersetzend)
|
|
||||||
|
|
||||||
- **Hintergrund Seite:** `#ffffff` oder sehr helles Neutral (`#f8f9fb`), konsistent.
|
|
||||||
- **Sekundärflächen** (Karten, rechte Spalte, Infoboxen): dezentes Grau oder ein **sehr zurückhaltendes Violett-Grau** (z. B. Mischung aus Weiß mit 3–6 % Primärfarbe), damit die Seite **ruhig** bleibt und nicht „bunt“ wirkt.
|
|
||||||
- **Text:** nahezu schwarz für Fließtext (`#1a1a1a` bis `#222`), sekundäre Texte etwas heller – immer mit Kontrastprüfung.
|
|
||||||
|
|
||||||
### 3.3 Akzent (optional, sparsam)
|
|
||||||
|
|
||||||
- **Links** im Fließtext: z. B. unterstrichen oder klar farbig abgesetzt; Primärviolett oder eine **eine** abgestimmte dunklere Violett-Nuance für Lesbarkeit auf Weiß.
|
|
||||||
- **Erfolg/Warnung/Fehler:** Standard-Semantik (Grün/Gelb/Rot) nur für Status – **nicht** als neue Hauptfarbe neben dem Violett.
|
|
||||||
|
|
||||||
### 3.4 Footer
|
|
||||||
|
|
||||||
- Dunkler Footer kann bleiben; **Links und Fokus** müssen gut lesbar sein. Primärviolett für Hover/Fokus auf dunklem Grund nur, wenn der Kontrast stimmt – sonch neutrale helle Linkfarbe + sichtbarer Fokusring.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Typografie
|
|
||||||
|
|
||||||
### 4.1 Schriftwahl
|
|
||||||
|
|
||||||
- **Primärschrift:** eine gut lesbare **System- oder Webschrift** mit neutral-seriösem Charakter, z. B.:
|
|
||||||
- *Source Sans 3*, *Inter*, *Open Sans* oder **beibehaltene Arial** nach einheitlicher Skala – Entscheidung in Phase 1 an **Performance** und **Corporate-Vorgaben** binden.
|
|
||||||
- **Überschriften:** dieselbe Familie mit klarer Gewichtsstaffelung (z. B. 600/700), keine verspielten Display-Schnitte.
|
|
||||||
|
|
||||||
### 4.2 Skala (Beispiel)
|
|
||||||
|
|
||||||
| Stufe | Verwendung | Größe (Orientierung, rem) |
|
|
||||||
|-------|------------|---------------------------|
|
|
||||||
| H1 | Seitentitel (nicht auf jeder Unterseite doppelt mit Logo-Text kollidieren) | `1.75–2rem` |
|
|
||||||
| H2 | Abschnitte | `1.35–1.5rem` |
|
|
||||||
| H3 | Unterabschnitte | `1.15–1.25rem` |
|
|
||||||
| Body | Fließtext | `1rem`, Zeilenlänge max. ca. 65–75 Zeichen |
|
|
||||||
| Klein | Meta, Fußnoten | `0.875rem` mit ausreichend Kontrast |
|
|
||||||
|
|
||||||
### 4.3 Regeln
|
|
||||||
|
|
||||||
- **Keine** reine Großschreibung für lange Menütexte.
|
|
||||||
- **Zeilenabstand** für Fließtext mindestens ca. 1,5.
|
|
||||||
- **Kontrast** von Überschriften und Text zu Hintergrund einhalten (WCAG 2.1 AA).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Layout und Raster
|
|
||||||
|
|
||||||
### 5.1 Container
|
|
||||||
|
|
||||||
- Maximalbreite für Lesbarkeit (z. B. `min(100%, 72rem)`) mit **symmetrischem Innenabstand**.
|
|
||||||
- **Keine** feste `min-width` im vierstelligen Pixelbereich ohne Scroll-Alternative; stattdessen **flexibles Grid** + sinnvolle Mindestbreiten nur dort, wo nötig (Tabellen).
|
|
||||||
|
|
||||||
### 5.2 Breakpoints (Orientierung)
|
|
||||||
|
|
||||||
- **Mobil:** < 640px – eine Spalte, Navigation als klares Overlay oder ausklappbare Liste mit großen Touch-Zielen.
|
|
||||||
- **Tablet:** 640–1024px – ggf. weiterhin eine Spalte oder kompakte Sidebar.
|
|
||||||
- **Desktop:** > 1024px – Zwei-Spalten-Layout optional; rechte Spalte für Bilder/Termine **nicht** zwingend über volle Höhe „eingesperrt“, wenn das inhaltlich sinnvoller ist.
|
|
||||||
|
|
||||||
### 5.3 Weißraum
|
|
||||||
|
|
||||||
- Einheitliches Spacing-System (z. B. Vielfache von 4px oder 0,25rem): Abstände zwischen Blöcken, in Karten, zwischen Formularfeldern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Navigation und Bedienung
|
|
||||||
|
|
||||||
### 6.1 Hauptnavigation
|
|
||||||
|
|
||||||
- **Desktop:** horizontale Leiste mit Vollfarbe EKHN-Violett; aktiver Eintrag **deutlich** (Unterstreichung, Hintergrund oder starker Kontrast – weiterhin im Violett-System).
|
|
||||||
- **Touch:** Menüpunkte mindestens ca. **44×44 px** Klickfläche.
|
|
||||||
- **Untermenüs:** Hover für Maus; für Tastatur **Escape** schließt; **Fokus** sichtbar im gesamten Menübaum.
|
|
||||||
- **Mobile:** „Menü“-Button durch **Icon + Text** oder klares Label; Animationen **kurz** (< 200ms).
|
|
||||||
|
|
||||||
### 6.2 Orientierung
|
|
||||||
|
|
||||||
- Optional: **Brotkrumen** bei tieferen Seiten (Gemeindeleben → Gruppen → …), dezent unterhalb des Headers.
|
|
||||||
- Seitentitel konsistent: eine H1 pro Seite, semantisch korrekt.
|
|
||||||
|
|
||||||
### 6.3 Footer
|
|
||||||
|
|
||||||
- Impressum/Datenschutz bleiben gut auffindbar; gleiche visuelle Gewichtung wie bisher, mit verbessertem Kontrast und Fokus.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Barrierefreiheit (WCAG 2.1 Level AA als Ziel)
|
|
||||||
|
|
||||||
- **Kontrast:** Text auf Violett/Weiß/Grau messen (Tools: axe, Lighthouse, WebAIM Contrast Checker).
|
|
||||||
- **Tastatur:** alle interaktiven Elemente erreichbar; sichtbarer `:focus-visible`.
|
|
||||||
- **Screenreader:** Landmarks (`header`, `nav`, `main`, `footer`), Überschriftenhierarchie, `aria-expanded` für Untermenüs, sinnvolle `alt`-Texte bei Bildern.
|
|
||||||
- **Bewegung:** `prefers-reduced-motion` respektieren (Animationen abschwächen oder deaktivieren).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Komponentenbibliothek (schrittweise)
|
|
||||||
|
|
||||||
Einheitliche Bausteine reduzieren Streuung und erleichtern Wartung:
|
|
||||||
|
|
||||||
| Komponente | Anforderungen |
|
|
||||||
|------------|----------------|
|
|
||||||
| **Primärbutton** | Violett-Hintergrund, weißer Text, Hover, disabled-Zustand, Fokusring |
|
|
||||||
| **Sekundärbutton** | Outline in Violett oder neutral, gleiche Höhe wie Primär |
|
|
||||||
| **Karte** (Termine, News) | Klarer Titel, Datum, Link „weiterlesen“, ruhiger Schatten oder Rahmen |
|
|
||||||
| **Formularfelder** | Labels sichtbar, Fehler näch am Feld, keine rein farblichen Fehlerhinweise |
|
|
||||||
| **Tabellen** (Admin) | Zeilenwechsel, ausreichend Zellpadding, horizontales Scrollen auf schmalen Screens |
|
|
||||||
|
|
||||||
Technisch: zentrale **CSS-Variablen** + ggf. wiederverwendbare Vue-Komponenten oder Utility-Klassen – ohne das Projekt unnötig zu überfrachten.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Umsetzung in Phasen
|
|
||||||
|
|
||||||
### Phase 1 – Fundament (geringes Risiko, hoher Nutzen)
|
|
||||||
|
|
||||||
- Design-Tokens (Farben, Abstände, Schriftgrößen) in einer **globalen** Styleschicht.
|
|
||||||
- Entfernen/Ersetzen problematischer Layout-Regeln (`min-width` Hauptspalte), **Responsive** testen.
|
|
||||||
- Typografie-Skala und Basisabstände vereinheitlichen.
|
|
||||||
- Kontrast Footer/Links prüfen und anpassen.
|
|
||||||
|
|
||||||
### Phase 2 – Navigation & Chrome
|
|
||||||
|
|
||||||
- Navbar optisch verfeinern (Padding, aktive Zustände, Touch) bei **unverändertem** `#9400ff` / Hover.
|
|
||||||
- Header (Titelzeile): Schlagschatten/Lavendel **an Markensystem anbinden** oder reduzieren für seriösere Wirkung.
|
|
||||||
- Optional Brotkrumen für tiefe Seiten.
|
|
||||||
|
|
||||||
### Phase 3 – Inhaltsmodule
|
|
||||||
|
|
||||||
- Termine, Gottesdienste, Kontakt: Kartenlayout, konsistente Datumdarstellung (`date-fns` ist bereits im Projekt).
|
|
||||||
- Bilder: feste Aspect-Ratios oder `object-fit`, damit keine Layout-Sprünge entstehen.
|
|
||||||
|
|
||||||
### Phase 4 – Feinschliff
|
|
||||||
|
|
||||||
- Mikro-Interaktionen (Hover, Fokus) konsistent.
|
|
||||||
- Performance: Schriftarten, Bildgrößen, Lazy Loading wo sinnvoll.
|
|
||||||
- Finale Accessibility-Prüfung.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Erfolgskriterien (messbar / reviewbar)
|
|
||||||
|
|
||||||
- Keine horizontale Scrollbarkeit bei Standard-Viewports durch feste Mindestbreiten.
|
|
||||||
- Lighthouse-Accessibility-Score deutlich verbessert (Ziel: **≥ 90**, wo technisch möglich).
|
|
||||||
- **Manuelle** Tastatur- und Screenreader-Stichprobe auf Startseite, Navigation, einem Formular.
|
|
||||||
- **Visuelles Review** mit 2–3 Stakeholdern: „wirkt seriös, klar, kirchlich passend“ – **ohne** neue Hauptfarbe neben EKHN-Violett.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Explizite Nicht-Ziele
|
|
||||||
|
|
||||||
- Kein Rebranding und kein Ersatz der Primärfarbe.
|
|
||||||
- Kein „Gamification“-Design oder verspielte Illustrationen als Hauptstil.
|
|
||||||
- Keine Einführung schwerer UI-Frameworks nur wegen Optik, wenn das Projekt schlank bleiben soll (abwägen mit Wartbarkeit).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Nächster Schritt
|
|
||||||
|
|
||||||
Umsetzung beginnt mit **Phase 1**: globale Tokens + Layout-Fixes + Kontrast. Dieses Dokument dient als **Referenz** für alle weiteren UI-Änderungen und sollte bei größeren Designentscheidungen aktualisiert werden.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Dokument erstellt als Arbeitsgrundlage für die Modernisierung; bei Abweichungen von offiziellen EKHN-CD-Vorgaben immer die gültige kirchliche Markenrichtlinie Vorrang haben.*
|
|
||||||
20
index.html
20
index.html
@@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<title>Miriamgemeinde</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>
|
|
||||||
Diese Website funktioniert leider nicht richtig, wenn JavaScript deaktiviert ist. Bitte aktivieren Sie
|
|
||||||
JavaScript, um fortzufahren.
|
|
||||||
</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up (queryInterface, Sequelize) {
|
|
||||||
await queryInterface.createTable('liturgical_days', {
|
|
||||||
id: {
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: Sequelize.DATEONLY,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
dayName: {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async down (queryInterface, Sequelize) {
|
|
||||||
await queryInterface.dropTable('liturgical_days');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
up: async (queryInterface, Sequelize) => {
|
|
||||||
await queryInterface.addColumn('worships', 'organ_playing', {
|
|
||||||
type: Sequelize.STRING,
|
|
||||||
allowNull: true
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (queryInterface, Sequelize) => {
|
|
||||||
await queryInterface.removeColumn('worships', 'organ_playing');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
up: async (queryInterface, Sequelize) => {
|
|
||||||
await queryInterface.addColumn('worships', 'approved', {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (queryInterface, Sequelize) => {
|
|
||||||
await queryInterface.removeColumn('worships', 'approved');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.createTable('worship_leaders', {
|
|
||||||
id: {
|
|
||||||
allowNull: false,
|
|
||||||
autoIncrement: true,
|
|
||||||
primaryKey: true,
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
type: Sequelize.STRING(32),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: Sequelize.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
type: Sequelize.STRING(512),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
allowNull: false,
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
allowNull: false,
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.dropTable('worship_leaders');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -25,10 +25,6 @@ module.exports = (sequelize) => {
|
|||||||
email: {
|
email: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
|
||||||
expiryDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'contact_persons',
|
tableName: 'contact_persons',
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
module.exports = (sequelize, DataTypes) => {
|
|
||||||
const LiturgicalDay = sequelize.define('LiturgicalDay', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true
|
|
||||||
},
|
|
||||||
dayName: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'liturgical_days',
|
|
||||||
timestamps: false
|
|
||||||
});
|
|
||||||
|
|
||||||
return LiturgicalDay;
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -61,17 +61,6 @@ module.exports = (sequelize) => {
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'sacristan_service'
|
field: 'sacristan_service'
|
||||||
},
|
},
|
||||||
organPlaying: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'organ_playing'
|
|
||||||
},
|
|
||||||
approved: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'approved'
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
tableName: 'worships',
|
tableName: 'worships',
|
||||||
timestamps: true
|
timestamps: true
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
|
||||||
|
|
||||||
module.exports = (sequelize) => {
|
|
||||||
const WorshipLeader = sequelize.define('WorshipLeader', {
|
|
||||||
code: {
|
|
||||||
type: DataTypes.STRING(32),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
// Comma-separated list of alternative codes (kept simple to avoid join tables).
|
|
||||||
type: DataTypes.STRING(512),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
tableName: 'worship_leaders',
|
|
||||||
timestamps: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return WorshipLeader;
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -3,11 +3,20 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const Sequelize = require('sequelize');
|
const Sequelize = require('sequelize');
|
||||||
const sequelize = require('../config/database');
|
|
||||||
const basename = path.basename(__filename);
|
const basename = path.basename(__filename);
|
||||||
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
const config = require(__dirname + '/../config/config.json')[env];
|
||||||
const db = {};
|
const db = {};
|
||||||
|
|
||||||
fs.readdirSync(__dirname)
|
let sequelize;
|
||||||
|
if (config.use_env_variable) {
|
||||||
|
sequelize = new Sequelize(process.env[config.use_env_variable], config);
|
||||||
|
} else {
|
||||||
|
sequelize = new Sequelize(config.database, config.username, config.password, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs
|
||||||
|
.readdirSync(__dirname)
|
||||||
.filter(file => {
|
.filter(file => {
|
||||||
return (
|
return (
|
||||||
file.indexOf('.') !== 0 &&
|
file.indexOf('.') !== 0 &&
|
||||||
|
|||||||
32750
package-lock.json
generated
32750
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@@ -1,83 +1,83 @@
|
|||||||
{
|
{
|
||||||
"name": "miriamgemeinde",
|
"name": "miriamgemeinde",
|
||||||
"version": "1.1.1",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vite build && npm run copy-dist",
|
"build": "vue-cli-service build && npm run copy-dist",
|
||||||
"preview": "vite preview",
|
"copy-dist": "rm -rf public/* && cp -r dist/* public/",
|
||||||
"copy-dist": "cp -r dist/* public/",
|
"lint": "vue-cli-service lint"
|
||||||
"lint": "eslint src --ext .js,.vue",
|
|
||||||
"lint:all": "eslint . --ext .js,.vue"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconoir/vue": "^7.7.0",
|
"@iconoir/vue": "^7.7.0",
|
||||||
"@tiptap/extension-bold": "^3.22.2",
|
"@tiptap/extension-bold": "^2.4.0",
|
||||||
"@tiptap/extension-bullet-list": "^3.22.2",
|
"@tiptap/extension-bullet-list": "^2.4.0",
|
||||||
"@tiptap/extension-color": "^3.22.2",
|
"@tiptap/extension-color": "^2.4.0",
|
||||||
"@tiptap/extension-heading": "^3.22.2",
|
"@tiptap/extension-heading": "^2.4.0",
|
||||||
"@tiptap/extension-italic": "^3.22.2",
|
"@tiptap/extension-italic": "^2.4.0",
|
||||||
"@tiptap/extension-link": "^3.22.2",
|
"@tiptap/extension-link": "^2.4.0",
|
||||||
"@tiptap/extension-ordered-list": "^3.22.2",
|
"@tiptap/extension-ordered-list": "^2.4.0",
|
||||||
"@tiptap/extension-strike": "^3.22.2",
|
"@tiptap/extension-strike": "^2.4.0",
|
||||||
"@tiptap/extension-table": "^3.22.2",
|
"@tiptap/extension-table": "^2.4.0",
|
||||||
"@tiptap/extension-table-cell": "^3.22.2",
|
"@tiptap/extension-table-cell": "^2.4.0",
|
||||||
"@tiptap/extension-table-header": "^3.22.2",
|
"@tiptap/extension-table-header": "^2.4.0",
|
||||||
"@tiptap/extension-table-row": "^3.22.2",
|
"@tiptap/extension-table-row": "^2.4.0",
|
||||||
"@tiptap/extension-text-style": "^3.22.2",
|
"@tiptap/extension-text-style": "^2.4.0",
|
||||||
"@tiptap/extension-underline": "^3.22.2",
|
"@tiptap/extension-underline": "^2.4.0",
|
||||||
"@tiptap/starter-kit": "^3.22.2",
|
"@tiptap/starter-kit": "^2.4.0",
|
||||||
"@tiptap/vue-3": "^3.22.2",
|
"@tiptap/vue-3": "^2.4.0",
|
||||||
"@xmldom/xmldom": "^0.8.12",
|
"@vue/cli": "^5.0.8",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"docx": "^9.5.1",
|
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^5.2.1",
|
"express": "^4.19.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jszip": "^3.10.1",
|
"moment": "^2.30.1",
|
||||||
"mammoth": "^1.11.0",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.10.1",
|
"mysql2": "^3.10.1",
|
||||||
"nodemailer": "^7.0.6",
|
"nodemailer": "^7.0.6",
|
||||||
"pdf-parse": "^1.1.1",
|
"nodemon": "^3.1.3",
|
||||||
"sequelize": "^6.37.3",
|
"sequelize": "^6.37.3",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vm-browserify": "^1.1.2",
|
"vm-browserify": "^1.1.2",
|
||||||
"vue": "^3.4.30",
|
"vue": "^3.4.30",
|
||||||
"vue-multiselect": "^3.0.0",
|
"vue-multiselect": "^3.0.0",
|
||||||
|
"vue-quill-editor": "^3.0.6",
|
||||||
"vue-router": "^4.3.3",
|
"vue-router": "^4.3.3",
|
||||||
"vuex": "^4.0.2"
|
"vuex": "^4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.5",
|
"@babel/core": "^7.12.16",
|
||||||
"eslint": "^8.57.0",
|
"@babel/eslint-parser": "^7.12.16",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"@vue/cli-plugin-babel": "~5.0.0",
|
||||||
"nodemon": "^3.1.3",
|
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||||
"vite": "^8.0.7",
|
"@vue/cli-service": "~5.0.0",
|
||||||
"vue-eslint-parser": "^9.4.3"
|
"crypto-browserify": "^3.12.0",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
|
"os-browserify": "^0.3.0",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"webpack": "^5.92.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
|
||||||
"es2021": true,
|
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:vue/vue3-essential",
|
"plugin:vue/vue3-essential",
|
||||||
"eslint:recommended"
|
"eslint:recommended"
|
||||||
],
|
],
|
||||||
"parser": "vue-eslint-parser",
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2022,
|
"parser": "@babel/eslint-parser"
|
||||||
"sourceType": "module"
|
|
||||||
},
|
},
|
||||||
"rules": {}
|
"rules": {}
|
||||||
},
|
},
|
||||||
@@ -86,8 +86,5 @@
|
|||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not dead",
|
"not dead",
|
||||||
"not ie 11"
|
"not ie 11"
|
||||||
],
|
]
|
||||||
"engines": {
|
|
||||||
"node": "^22.23.4 || ^24.22.1 || ^26.12.1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
1
public/css/1099.a6cdcdf4.css
Normal file
1
public/css/1099.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/123.c09125f7.css
Normal file
1
public/css/123.c09125f7.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.privacy-policy[data-v-91660a08]{max-width:800px;margin:auto;padding:20px}h1[data-v-91660a08],h2[data-v-91660a08],h3[data-v-91660a08],h4[data-v-91660a08],h5[data-v-91660a08]{margin-top:20px;color:#333}p[data-v-91660a08]{line-height:1.6}ul[data-v-91660a08]{margin:10px 0;padding-left:20px}ul li[data-v-91660a08]{list-style-type:disc}a[data-v-91660a08]{color:#007bff;text-decoration:none}a[data-v-91660a08]:hover{text-decoration:underline}
|
||||||
1
public/css/1258.13c489b0.css
Normal file
1
public/css/1258.13c489b0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.register[data-v-4256bbfa]{max-width:400px;margin:auto}form[data-v-4256bbfa]{display:flex;flex-direction:column}label[data-v-4256bbfa]{margin-top:10px}button[data-v-4256bbfa]{margin-top:20px}
|
||||||
1
public/css/150.9419ef08.css
Normal file
1
public/css/150.9419ef08.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-6bd31626]{border-collapse:collapse;width:100%}table.worships td[data-v-6bd31626]{border:1px solid #000;text-align:center}h3[data-v-6bd31626]{margin:0}table.worships td div[data-v-6bd31626]{margin:5px}.highlight-time[data-v-6bd31626]{text-decoration:underline}.neighborhood-invitation[data-v-6bd31626]{font-weight:700;color:#0020e0}a[data-v-6bd31626]{color:#0020e0}.internal-information[data-v-6bd31626]{color:#e45;font-style:italic}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-708e6f45]{font-weight:700}.event-table[data-v-708e6f45]{border-collapse:collapse}.event-table td[data-v-708e6f45]{border:1px solid #000}.homepage[data-v-708e6f45]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-708e6f45]{padding:.5em 0}.event-image>img[data-v-708e6f45]{max-width:12em;max-height:12em}.contact-box p[data-v-0cc91918]{margin:0}.bottom-margin[data-v-0cc91918]{margin-bottom:1rem}span[data-v-2bbf7aa9]{cursor:pointer;color:blue;text-decoration:underline}.previewinfo[data-v-9a71cbf6]{background-color:#000;color:#d00000;position:absolute;top:93px;left:0;padding:2px 10px;font-weight:700}
|
||||||
1
public/css/1565.5391dc21.css
Normal file
1
public/css/1565.5391dc21.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.privacy-policy[data-v-0c3320ea]{max-width:800px;margin:auto;padding:20px}h1[data-v-0c3320ea],h2[data-v-0c3320ea],h3[data-v-0c3320ea],h4[data-v-0c3320ea],h5[data-v-0c3320ea]{margin-top:20px;color:#333}p[data-v-0c3320ea]{line-height:1.6}ul[data-v-0c3320ea]{margin:10px 0;padding-left:20px}ul li[data-v-0c3320ea]{list-style-type:disc}a[data-v-0c3320ea]{color:#007bff;text-decoration:none}a[data-v-0c3320ea]:hover{text-decoration:underline}
|
||||||
1
public/css/183.43cc4f81.css
Normal file
1
public/css/183.43cc4f81.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.user-administration[data-v-a495c756]{padding:20px}.user-administration h1[data-v-a495c756],.user-administration h2[data-v-a495c756]{margin-bottom:20px}.user-administration form[data-v-a495c756]{display:flex;flex-direction:column;margin-bottom:20px}.user-administration label[data-v-a495c756]{margin-top:10px}.user-administration input[type=email][data-v-a495c756],.user-administration input[type=password][data-v-a495c756],.user-administration input[type=text][data-v-a495c756]{padding:5px;font-size:16px}.user-administration ul[data-v-a495c756]{list-style-type:none;padding:0}.user-administration li[data-v-a495c756]{padding:10px;border-bottom:1px solid #ddd;cursor:pointer}.user-administration li[data-v-a495c756]:hover{background-color:#f0f0f0}
|
||||||
1
public/css/187.f4e467b4.css
Normal file
1
public/css/187.f4e467b4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.event-places-management[data-v-4e6631f7]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-4e6631f7]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-4e6631f7]{margin-top:10px}input[data-v-4e6631f7]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-4e6631f7]{margin-top:10px;padding:10px}table[data-v-4e6631f7]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-4e6631f7],th[data-v-4e6631f7]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-4e6631f7]{background-color:#f4f4f4}
|
||||||
1
public/css/1969.a6cdcdf4.css
Normal file
1
public/css/1969.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/2177.54e852c0.css
Normal file
1
public/css/2177.54e852c0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}.previewinfo[data-v-9a71cbf6]{background-color:#000;color:#d00000;position:absolute;top:93px;left:0;padding:2px 10px;font-weight:700}
|
||||||
1
public/css/23.78894bf8.css
Normal file
1
public/css/23.78894bf8.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
div[data-v-68b32234]{padding:20px}ul[data-v-68b32234]{list-style:none;padding:0;margin:0}li[data-v-68b32234]{padding:0;margin:0}
|
||||||
1
public/css/2353.9504c97b.css
Normal file
1
public/css/2353.9504c97b.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.upload-files[data-v-f2694614]{width:100%;margin:auto}.upload-files div[data-v-f2694614]{margin-bottom:10px}.file-list[data-v-f2694614]{list-style-type:none;padding:0;margin-top:20px}.file-list li[data-v-f2694614]{border-bottom:1px solid #ddd;padding:10px 0}.file-info[data-v-f2694614]{display:flex;justify-content:space-between;cursor:pointer}.file-title[data-v-f2694614]{font-weight:700}.file-name[data-v-f2694614]{color:#555}.file-date[data-v-f2694614]{color:#888}
|
||||||
1
public/css/246.1e896a7d.css
Normal file
1
public/css/246.1e896a7d.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/2463.a6cdcdf4.css
Normal file
1
public/css/2463.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/260.38c6ce8f.css
Normal file
1
public/css/260.38c6ce8f.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.dialog-overlay[data-v-64c2e06a]{top:calc(50% - 25em);left:5%;width:90%;height:50em;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;overflow:auto}.dialog[data-v-64c2e06a]{background:#fff;padding:20px;border-radius:5px;max-width:400px;width:100%;text-align:center}button[data-v-64c2e06a]{margin-top:20px}
|
||||||
1
public/css/2607.a6cdcdf4.css
Normal file
1
public/css/2607.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/2809.b682d961.css
Normal file
1
public/css/2809.b682d961.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.right-column[data-v-12df4c86]{background-color:#d9e2f3}.right-column h2[data-v-12df4c86]{text-align:center;color:#000}.right-column img[data-v-12df4c86]{display:block;margin:0 auto;max-width:100%;height:auto}
|
||||||
1
public/css/281.2d192723.css
Normal file
1
public/css/281.2d192723.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.forgot-password[data-v-c694cf4e]{max-width:400px;margin:auto}form[data-v-c694cf4e]{display:flex;flex-direction:column}label[data-v-c694cf4e]{margin-top:10px}button[data-v-c694cf4e]{margin-top:20px}.dialog[data-v-c694cf4e]{position:fixed;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}.dialog-content[data-v-c694cf4e]{background:#fff;padding:16px;border-radius:4px;max-width:420px;width:90%}
|
||||||
1
public/css/289.56e284e6.css
Normal file
1
public/css/289.56e284e6.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.right-column h2[data-v-d1b58e08]{text-align:center;color:#000}.right-column img[data-v-d1b58e08]{display:block;margin:0 auto;max-width:100%;height:auto}
|
||||||
1
public/css/299.5760daa0.css
Normal file
1
public/css/299.5760daa0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.menu-management[data-v-0e6a0522]{width:100%;margin:auto}.button-container[data-v-0e6a0522]{display:inline-flex;gap:10px;margin-bottom:20px}.tree-view[data-v-0e6a0522]{margin-top:20px}.tree-view ul[data-v-0e6a0522]{list-style-type:none;padding:0}.tree-view li[data-v-0e6a0522]{margin-bottom:5px;padding-left:20px}.tree-view .menu-item[data-v-0e6a0522]{display:inline-flex;width:100%;justify-content:space-between;align-items:center}.tree-view span[data-v-0e6a0522]{cursor:pointer;color:#000}.tree-view button[data-v-0e6a0522]{border:none;height:1.6em;padding:0 .5em;margin:1px;border-radius:5px}.tree-view span[data-v-0e6a0522]:hover{text-decoration:underline}.edit-form[data-v-0e6a0522]{margin-top:20px}.edit-form label[data-v-0e6a0522]{display:block;margin-bottom:5px;font-weight:700}.edit-form input[data-v-0e6a0522]:not([type=checkbox]){display:block;margin-bottom:10px}.edit-form .checkbox-container[data-v-0e6a0522]{display:flex;flex-direction:column;margin-right:10px}.edit-form .order-id[data-v-0e6a0522]{width:50px}.edit-form button[data-v-0e6a0522]{margin-top:5px}
|
||||||
1
public/css/3214.a6cdcdf4.css
Normal file
1
public/css/3214.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/324.2d192723.css
Normal file
1
public/css/324.2d192723.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.forgot-password[data-v-c694cf4e]{max-width:400px;margin:auto}form[data-v-c694cf4e]{display:flex;flex-direction:column}label[data-v-c694cf4e]{margin-top:10px}button[data-v-c694cf4e]{margin-top:20px}.dialog[data-v-c694cf4e]{position:fixed;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}.dialog-content[data-v-c694cf4e]{background:#fff;padding:16px;border-radius:4px;max-width:420px;width:90%}
|
||||||
1
public/css/3299.5760daa0.css
Normal file
1
public/css/3299.5760daa0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.menu-management[data-v-0e6a0522]{width:100%;margin:auto}.button-container[data-v-0e6a0522]{display:inline-flex;gap:10px;margin-bottom:20px}.tree-view[data-v-0e6a0522]{margin-top:20px}.tree-view ul[data-v-0e6a0522]{list-style-type:none;padding:0}.tree-view li[data-v-0e6a0522]{margin-bottom:5px;padding-left:20px}.tree-view .menu-item[data-v-0e6a0522]{display:inline-flex;width:100%;justify-content:space-between;align-items:center}.tree-view span[data-v-0e6a0522]{cursor:pointer;color:#000}.tree-view button[data-v-0e6a0522]{border:none;height:1.6em;padding:0 .5em;margin:1px;border-radius:5px}.tree-view span[data-v-0e6a0522]:hover{text-decoration:underline}.edit-form[data-v-0e6a0522]{margin-top:20px}.edit-form label[data-v-0e6a0522]{display:block;margin-bottom:5px;font-weight:700}.edit-form input[data-v-0e6a0522]:not([type=checkbox]){display:block;margin-bottom:10px}.edit-form .checkbox-container[data-v-0e6a0522]{display:flex;flex-direction:column;margin-right:10px}.edit-form .order-id[data-v-0e6a0522]{width:50px}.edit-form button[data-v-0e6a0522]{margin-top:5px}
|
||||||
1
public/css/331.65e45809.css
Normal file
1
public/css/331.65e45809.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.register[data-v-63b3c0a3]{max-width:400px;margin:auto}form[data-v-63b3c0a3]{display:flex;flex-direction:column}label[data-v-63b3c0a3]{margin-top:10px}button[data-v-63b3c0a3]{margin-top:20px}.dialog[data-v-63b3c0a3]{position:fixed;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}.dialog-content[data-v-63b3c0a3]{background:#fff;padding:16px;border-radius:4px;max-width:420px;width:90%}
|
||||||
1
public/css/3353.bdb3d500.css
Normal file
1
public/css/3353.bdb3d500.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.position-management[data-v-1684a375]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-1684a375]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-1684a375]{margin-top:10px}input[data-v-1684a375]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-1684a375]{margin-top:10px;padding:10px}table[data-v-1684a375]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-1684a375],th[data-v-1684a375]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-1684a375]{background-color:#f4f4f4}
|
||||||
1
public/css/3459.f3d026c5.css
Normal file
1
public/css/3459.f3d026c5.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/3484.32da9cb7.css
Normal file
1
public/css/3484.32da9cb7.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.dialog-overlay[data-v-21ade8c0]{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center}.dialog[data-v-21ade8c0]{background:#fff;padding:20px;border-radius:5px;max-width:400px;width:100%;text-align:center}button[data-v-21ade8c0]{margin-top:20px}.login[data-v-40a158c0]{max-width:400px;margin:auto}form[data-v-40a158c0]{display:flex;flex-direction:column}label[data-v-40a158c0]{margin-top:10px}button[data-v-40a158c0]{margin-top:20px}
|
||||||
1
public/css/353.9504c97b.css
Normal file
1
public/css/353.9504c97b.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.upload-files[data-v-f2694614]{width:100%;margin:auto}.upload-files div[data-v-f2694614]{margin-bottom:10px}.file-list[data-v-f2694614]{list-style-type:none;padding:0;margin-top:20px}.file-list li[data-v-f2694614]{border-bottom:1px solid #ddd;padding:10px 0}.file-info[data-v-f2694614]{display:flex;justify-content:space-between;cursor:pointer}.file-title[data-v-f2694614]{font-weight:700}.file-name[data-v-f2694614]{color:#555}.file-date[data-v-f2694614]{color:#888}
|
||||||
1
public/css/355.c09125f7.css
Normal file
1
public/css/355.c09125f7.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.privacy-policy[data-v-91660a08]{max-width:800px;margin:auto;padding:20px}h1[data-v-91660a08],h2[data-v-91660a08],h3[data-v-91660a08],h4[data-v-91660a08],h5[data-v-91660a08]{margin-top:20px;color:#333}p[data-v-91660a08]{line-height:1.6}ul[data-v-91660a08]{margin:10px 0;padding-left:20px}ul li[data-v-91660a08]{list-style-type:disc}a[data-v-91660a08]{color:#007bff;text-decoration:none}a[data-v-91660a08]:hover{text-decoration:underline}
|
||||||
1
public/css/3550.a6cdcdf4.css
Normal file
1
public/css/3550.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/362.f4e467b4.css
Normal file
1
public/css/362.f4e467b4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.event-places-management[data-v-4e6631f7]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-4e6631f7]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-4e6631f7]{margin-top:10px}input[data-v-4e6631f7]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-4e6631f7]{margin-top:10px;padding:10px}table[data-v-4e6631f7]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-4e6631f7],th[data-v-4e6631f7]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-4e6631f7]{background-color:#f4f4f4}
|
||||||
1
public/css/3715.a6cdcdf4.css
Normal file
1
public/css/3715.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/39.be11324e.css
Normal file
1
public/css/39.be11324e.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
div[data-v-334e7b82]{padding:20px}
|
||||||
1
public/css/398.63d77ea0.css
Normal file
1
public/css/398.63d77ea0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.reset-password[data-v-e49a033c]{max-width:400px;margin:auto}form[data-v-e49a033c]{display:flex;flex-direction:column}label[data-v-e49a033c]{margin-top:10px}input[data-v-e49a033c]{margin-top:5px;padding:8px;border:1px solid #ddd;border-radius:4px}button[data-v-e49a033c]{margin-top:20px;padding:10px;background-color:#007bff;color:#fff;border:none;border-radius:4px;cursor:pointer}button[data-v-e49a033c]:disabled{background-color:#ccc;cursor:not-allowed}.dialog[data-v-e49a033c]{position:fixed;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}.dialog-content[data-v-e49a033c]{background:#fff;padding:16px;border-radius:4px;max-width:420px;width:90%}
|
||||||
1
public/css/4039.be11324e.css
Normal file
1
public/css/4039.be11324e.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
div[data-v-334e7b82]{padding:20px}
|
||||||
1
public/css/404.4bd58cd7.css
Normal file
1
public/css/404.4bd58cd7.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.institution-management[data-v-ff992c44]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-ff992c44]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-ff992c44]{margin-top:10px}input[data-v-ff992c44]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-ff992c44]{margin-top:10px;padding:10px}table[data-v-ff992c44]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-ff992c44],th[data-v-ff992c44]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-ff992c44]{background-color:#f4f4f4}
|
||||||
1
public/css/408.38c6ce8f.css
Normal file
1
public/css/408.38c6ce8f.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.dialog-overlay[data-v-64c2e06a]{top:calc(50% - 25em);left:5%;width:90%;height:50em;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;overflow:auto}.dialog[data-v-64c2e06a]{background:#fff;padding:20px;border-radius:5px;max-width:400px;width:100%;text-align:center}button[data-v-64c2e06a]{margin-top:20px}
|
||||||
1
public/css/423.63d77ea0.css
Normal file
1
public/css/423.63d77ea0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.reset-password[data-v-e49a033c]{max-width:400px;margin:auto}form[data-v-e49a033c]{display:flex;flex-direction:column}label[data-v-e49a033c]{margin-top:10px}input[data-v-e49a033c]{margin-top:5px;padding:8px;border:1px solid #ddd;border-radius:4px}button[data-v-e49a033c]{margin-top:20px;padding:10px;background-color:#007bff;color:#fff;border:none;border-radius:4px;cursor:pointer}button[data-v-e49a033c]:disabled{background-color:#ccc;cursor:not-allowed}.dialog[data-v-e49a033c]{position:fixed;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}.dialog-content[data-v-e49a033c]{background:#fff;padding:16px;border-radius:4px;max-width:420px;width:90%}
|
||||||
1
public/css/441.bdb3d500.css
Normal file
1
public/css/441.bdb3d500.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.position-management[data-v-1684a375]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-1684a375]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-1684a375]{margin-top:10px}input[data-v-1684a375]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-1684a375]{margin-top:10px;padding:10px}table[data-v-1684a375]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-1684a375],th[data-v-1684a375]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-1684a375]{background-color:#f4f4f4}
|
||||||
1
public/css/446.9504c97b.css
Normal file
1
public/css/446.9504c97b.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.upload-files[data-v-f2694614]{width:100%;margin:auto}.upload-files div[data-v-f2694614]{margin-bottom:10px}.file-list[data-v-f2694614]{list-style-type:none;padding:0;margin-top:20px}.file-list li[data-v-f2694614]{border-bottom:1px solid #ddd;padding:10px 0}.file-info[data-v-f2694614]{display:flex;justify-content:space-between;cursor:pointer}.file-title[data-v-f2694614]{font-weight:700}.file-name[data-v-f2694614]{color:#555}.file-date[data-v-f2694614]{color:#888}
|
||||||
1
public/css/466.a6cdcdf4.css
Normal file
1
public/css/466.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/468.43cc4f81.css
Normal file
1
public/css/468.43cc4f81.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.user-administration[data-v-a495c756]{padding:20px}.user-administration h1[data-v-a495c756],.user-administration h2[data-v-a495c756]{margin-bottom:20px}.user-administration form[data-v-a495c756]{display:flex;flex-direction:column;margin-bottom:20px}.user-administration label[data-v-a495c756]{margin-top:10px}.user-administration input[type=email][data-v-a495c756],.user-administration input[type=password][data-v-a495c756],.user-administration input[type=text][data-v-a495c756]{padding:5px;font-size:16px}.user-administration ul[data-v-a495c756]{list-style-type:none;padding:0}.user-administration li[data-v-a495c756]{padding:10px;border-bottom:1px solid #ddd;cursor:pointer}.user-administration li[data-v-a495c756]:hover{background-color:#f0f0f0}
|
||||||
1
public/css/4765.4bd58cd7.css
Normal file
1
public/css/4765.4bd58cd7.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.institution-management[data-v-ff992c44]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-ff992c44]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-ff992c44]{margin-top:10px}input[data-v-ff992c44]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-ff992c44]{margin-top:10px;padding:10px}table[data-v-ff992c44]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-ff992c44],th[data-v-ff992c44]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-ff992c44]{background-color:#f4f4f4}
|
||||||
1
public/css/480.78894bf8.css
Normal file
1
public/css/480.78894bf8.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
div[data-v-68b32234]{padding:20px}ul[data-v-68b32234]{list-style:none;padding:0;margin:0}li[data-v-68b32234]{padding:0;margin:0}
|
||||||
1
public/css/484.f2269286.css
Normal file
1
public/css/484.f2269286.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.dialog-overlay[data-v-64c2e06a]{top:calc(50% - 25em);left:5%;width:90%;height:50em;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;overflow:auto}.dialog[data-v-64c2e06a]{background:#fff;padding:20px;border-radius:5px;max-width:400px;width:100%;text-align:center}button[data-v-64c2e06a]{margin-top:20px}.login[data-v-40a158c0]{max-width:400px;margin:auto}form[data-v-40a158c0]{display:flex;flex-direction:column}label[data-v-40a158c0]{margin-top:10px}button[data-v-40a158c0]{margin-top:20px}
|
||||||
1
public/css/4908.1e896a7d.css
Normal file
1
public/css/4908.1e896a7d.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/493.65e45809.css
Normal file
1
public/css/493.65e45809.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.register[data-v-63b3c0a3]{max-width:400px;margin:auto}form[data-v-63b3c0a3]{display:flex;flex-direction:column}label[data-v-63b3c0a3]{margin-top:10px}button[data-v-63b3c0a3]{margin-top:20px}.dialog[data-v-63b3c0a3]{position:fixed;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}.dialog-content[data-v-63b3c0a3]{background:#fff;padding:16px;border-radius:4px;max-width:420px;width:90%}
|
||||||
1
public/css/5108.a6cdcdf4.css
Normal file
1
public/css/5108.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/527.596e7cea.css
Normal file
1
public/css/527.596e7cea.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.impressum[data-v-612786fa]{max-width:800px;margin:auto;padding:20px}h1[data-v-612786fa],h2[data-v-612786fa],h3[data-v-612786fa],h4[data-v-612786fa]{margin-top:20px;color:#333}p[data-v-612786fa]{line-height:1.6}a[data-v-612786fa]{color:#007bff;text-decoration:none}a[data-v-612786fa]:hover{text-decoration:underline}
|
||||||
1
public/css/535.9e55a8df.css
Normal file
1
public/css/535.9e55a8df.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/5625.7ef4d708.css
Normal file
1
public/css/5625.7ef4d708.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.register[data-v-182f8976]{max-width:400px;margin:auto}form[data-v-182f8976]{display:flex;flex-direction:column}label[data-v-182f8976]{margin-top:10px}button[data-v-182f8976]{margin-top:20px}
|
||||||
1
public/css/5697.a6cdcdf4.css
Normal file
1
public/css/5697.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/6007.9e55a8df.css
Normal file
1
public/css/6007.9e55a8df.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/6107.a6cdcdf4.css
Normal file
1
public/css/6107.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/636.7f8b0e61.css
Normal file
1
public/css/636.7f8b0e61.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
form div[data-v-403a0e0b]{margin-bottom:10px}.uploaded-image[data-v-403a0e0b]{display:inline-block;margin:0 0 .5em .5em;border:1px solid #e0e0e0;padding:10px}.uploaded-image input[data-v-403a0e0b],.uploaded-image textarea[data-v-403a0e0b]{width:100%;margin:5px 0}
|
||||||
1
public/css/662.ee8ad276.css
Normal file
1
public/css/662.ee8ad276.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/6959.d2acc39d.css
Normal file
1
public/css/6959.d2acc39d.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/7.9e55a8df.css
Normal file
1
public/css/7.9e55a8df.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/702.f2269286.css
Normal file
1
public/css/702.f2269286.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.dialog-overlay[data-v-64c2e06a]{top:calc(50% - 25em);left:5%;width:90%;height:50em;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;overflow:auto}.dialog[data-v-64c2e06a]{background:#fff;padding:20px;border-radius:5px;max-width:400px;width:100%;text-align:center}button[data-v-64c2e06a]{margin-top:20px}.login[data-v-40a158c0]{max-width:400px;margin:auto}form[data-v-40a158c0]{display:flex;flex-direction:column}label[data-v-40a158c0]{margin-top:10px}button[data-v-40a158c0]{margin-top:20px}
|
||||||
1
public/css/703.5760daa0.css
Normal file
1
public/css/703.5760daa0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.menu-management[data-v-0e6a0522]{width:100%;margin:auto}.button-container[data-v-0e6a0522]{display:inline-flex;gap:10px;margin-bottom:20px}.tree-view[data-v-0e6a0522]{margin-top:20px}.tree-view ul[data-v-0e6a0522]{list-style-type:none;padding:0}.tree-view li[data-v-0e6a0522]{margin-bottom:5px;padding-left:20px}.tree-view .menu-item[data-v-0e6a0522]{display:inline-flex;width:100%;justify-content:space-between;align-items:center}.tree-view span[data-v-0e6a0522]{cursor:pointer;color:#000}.tree-view button[data-v-0e6a0522]{border:none;height:1.6em;padding:0 .5em;margin:1px;border-radius:5px}.tree-view span[data-v-0e6a0522]:hover{text-decoration:underline}.edit-form[data-v-0e6a0522]{margin-top:20px}.edit-form label[data-v-0e6a0522]{display:block;margin-bottom:5px;font-weight:700}.edit-form input[data-v-0e6a0522]:not([type=checkbox]){display:block;margin-bottom:10px}.edit-form .checkbox-container[data-v-0e6a0522]{display:flex;flex-direction:column;margin-right:10px}.edit-form .order-id[data-v-0e6a0522]{width:50px}.edit-form button[data-v-0e6a0522]{margin-top:5px}
|
||||||
1
public/css/7158.a6cdcdf4.css
Normal file
1
public/css/7158.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/734.bdb3d500.css
Normal file
1
public/css/734.bdb3d500.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.position-management[data-v-1684a375]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-1684a375]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-1684a375]{margin-top:10px}input[data-v-1684a375]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-1684a375]{margin-top:10px;padding:10px}table[data-v-1684a375]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-1684a375],th[data-v-1684a375]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-1684a375]{background-color:#f4f4f4}
|
||||||
1
public/css/7361.a6cdcdf4.css
Normal file
1
public/css/7361.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/7382.49bff9d4.css
Normal file
1
public/css/7382.49bff9d4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.dialog-overlay[data-v-21ade8c0]{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center}.dialog[data-v-21ade8c0]{background:#fff;padding:20px;border-radius:5px;max-width:400px;width:100%;text-align:center}button[data-v-21ade8c0]{margin-top:20px}
|
||||||
1
public/css/757.5e87db7f.css
Normal file
1
public/css/757.5e87db7f.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
form div[data-v-454efcad]{margin-bottom:10px}.uploaded-image[data-v-454efcad]{display:inline-block;margin:0 0 .5em .5em;border:1px solid #e0e0e0;padding:10px}.uploaded-image input[data-v-454efcad],.uploaded-image textarea[data-v-454efcad]{width:100%;margin:5px 0}
|
||||||
1
public/css/763.4687d764.css
Normal file
1
public/css/763.4687d764.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/765.4bd58cd7.css
Normal file
1
public/css/765.4bd58cd7.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.institution-management[data-v-ff992c44]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-ff992c44]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-ff992c44]{margin-top:10px}input[data-v-ff992c44]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-ff992c44]{margin-top:10px;padding:10px}table[data-v-ff992c44]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-ff992c44],th[data-v-ff992c44]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-ff992c44]{background-color:#f4f4f4}
|
||||||
1
public/css/781.9419ef08.css
Normal file
1
public/css/781.9419ef08.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-6bd31626]{border-collapse:collapse;width:100%}table.worships td[data-v-6bd31626]{border:1px solid #000;text-align:center}h3[data-v-6bd31626]{margin:0}table.worships td div[data-v-6bd31626]{margin:5px}.highlight-time[data-v-6bd31626]{text-decoration:underline}.neighborhood-invitation[data-v-6bd31626]{font-weight:700;color:#0020e0}a[data-v-6bd31626]{color:#0020e0}.internal-information[data-v-6bd31626]{color:#e45;font-style:italic}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-708e6f45]{font-weight:700}.event-table[data-v-708e6f45]{border-collapse:collapse}.event-table td[data-v-708e6f45]{border:1px solid #000}.homepage[data-v-708e6f45]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-708e6f45]{padding:.5em 0}.event-image>img[data-v-708e6f45]{max-width:12em;max-height:12em}.contact-box p[data-v-0cc91918]{margin:0}.bottom-margin[data-v-0cc91918]{margin-bottom:1rem}span[data-v-2bbf7aa9]{cursor:pointer;color:blue;text-decoration:underline}.previewinfo[data-v-9a71cbf6]{background-color:#000;color:#d00000;position:absolute;top:93px;left:0;padding:2px 10px;font-weight:700}
|
||||||
1
public/css/782.8fe0947f.css
Normal file
1
public/css/782.8fe0947f.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-6bd31626]{border-collapse:collapse;width:100%}table.worships td[data-v-6bd31626]{border:1px solid #000;text-align:center}h3[data-v-6bd31626]{margin:0}table.worships td div[data-v-6bd31626]{margin:5px}.highlight-time[data-v-6bd31626]{text-decoration:underline}.neighborhood-invitation[data-v-6bd31626]{font-weight:700;color:#0020e0}a[data-v-6bd31626]{color:#0020e0}.internal-information[data-v-6bd31626]{color:#e45;font-style:italic}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-708e6f45]{font-weight:700}.event-table[data-v-708e6f45]{border-collapse:collapse}.event-table td[data-v-708e6f45]{border:1px solid #000}.homepage[data-v-708e6f45]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-708e6f45]{padding:.5em 0}.event-image>img[data-v-708e6f45]{max-width:12em;max-height:12em}.contact-box p[data-v-0cc91918]{margin:0}.bottom-margin[data-v-0cc91918]{margin-bottom:1rem}span[data-v-2bbf7aa9]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/7876.5b86c09c.css
Normal file
1
public/css/7876.5b86c09c.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.forgot-password[data-v-017249f5]{max-width:400px;margin:auto}form[data-v-017249f5]{display:flex;flex-direction:column}label[data-v-017249f5]{margin-top:10px}button[data-v-017249f5]{margin-top:20px}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user