Compare commits
3 Commits
main
...
backend-ov
| 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);
|
||||||
|
});
|
||||||
|
|
||||||
exports.register = async (req, res) => {
|
/**
|
||||||
const { name, email, password } = req.body;
|
* Benutzeranmeldung
|
||||||
if (!name || !email || !password) {
|
*/
|
||||||
return res.status(400).json({ message: 'Alle Felder sind erforderlich' });
|
static login = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
}
|
|
||||||
try {
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
|
||||||
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;
|
const { email, password } = req.body;
|
||||||
if (!email || !password) {
|
const result = await AuthService.login(email, password);
|
||||||
return res.status(400).json({ message: 'Email und Passwort sind erforderlich' });
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
}
|
|
||||||
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}`;
|
* Passwort vergessen
|
||||||
|
*/
|
||||||
// E-Mail versenden
|
static forgotPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const emailTemplate = getPasswordResetEmailTemplate(resetUrl, user.name);
|
const result = await AuthService.forgotPassword(req.body.email);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
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 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);
|
||||||
|
});
|
||||||
|
|
||||||
// Passwort hashen und aktualisieren
|
/**
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
* Benutzerabmeldung
|
||||||
await User.update(
|
*/
|
||||||
{ password: hashedPassword },
|
static logout = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
{ 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');
|
const authHeader = req.header('Authorization');
|
||||||
if (!authHeader) {
|
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
|
||||||
return res.status(400).json({ message: 'Kein Token bereitgestellt' });
|
const result = await AuthService.logout(token);
|
||||||
}
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
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' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export der statischen Methoden für die Routen
|
||||||
|
module.exports = {
|
||||||
|
register: AuthController.register,
|
||||||
|
login: AuthController.login,
|
||||||
|
forgotPassword: AuthController.forgotPassword,
|
||||||
|
resetPassword: AuthController.resetPassword,
|
||||||
|
logout: AuthController.logout
|
||||||
};
|
};
|
||||||
@@ -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) => {
|
|
||||||
try {
|
|
||||||
const includePast = String(req.query?.includePast || '').toLowerCase();
|
|
||||||
const wantsPast = includePast === '1' || includePast === 'true' || includePast === 'yes';
|
|
||||||
|
|
||||||
const where = wantsPast
|
|
||||||
? undefined
|
|
||||||
: buildUpcomingWhere();
|
|
||||||
|
|
||||||
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: [
|
|
||||||
['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) => {
|
|
||||||
try {
|
|
||||||
const request = req.body;
|
|
||||||
const where = buildUpcomingWhere();
|
|
||||||
const order = [
|
|
||||||
['date', 'ASC'],
|
|
||||||
['time', 'ASC']
|
|
||||||
];
|
|
||||||
|
|
||||||
if (request.id === 'all') {
|
|
||||||
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,
|
|
||||||
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 });
|
exports.getEventById = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
} catch (error) {
|
const event = await EventService.getEventById(req.params.id);
|
||||||
res.status(500).json({ error: 'Failed to filter events' });
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich abgerufen');
|
||||||
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({
|
exports.filterEvents = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
where: { id: id },
|
const result = await EventService.filterEvents(req.body);
|
||||||
transaction,
|
ErrorHandler.successResponse(res, result, 'Events erfolgreich gefiltert');
|
||||||
});
|
});
|
||||||
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 = {
|
exports.createEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
getAllEvents,
|
const event = await EventService.createEvent(req.body);
|
||||||
createEvent,
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich erstellt', 201);
|
||||||
updateEvent,
|
});
|
||||||
deleteEvent,
|
|
||||||
filterEvents
|
exports.updateEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
};
|
const event = await EventService.updateEvent(req.params.id, req.body);
|
||||||
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich aktualisiert');
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.deleteEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const result = await EventService.deleteEvent(req.params.id);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
@@ -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)
|
exports.updateUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const safeUser = {
|
UserValidator.validateId(req.params.id);
|
||||||
id: user.id,
|
UserValidator.validateUpdateUser(req.body);
|
||||||
name: user.name,
|
const user = await UserService.updateUser(req.params.id, req.body);
|
||||||
email: user.email,
|
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich aktualisiert');
|
||||||
active: user.active,
|
});
|
||||||
created_at: user.created_at
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(201).json(safeUser);
|
exports.deleteUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
} catch (error) {
|
UserValidator.validateId(req.params.id);
|
||||||
console.error('Error creating user:', error);
|
await UserService.deleteUser(req.params.id);
|
||||||
res.status(500).json({ message: 'Error creating user' });
|
ErrorHandler.successResponse(res, null, 'Benutzer erfolgreich gelöscht');
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
exports.updateUser = async (req, res) => {
|
// Neue Route für Passwort-Änderung
|
||||||
try {
|
exports.changePassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const user = await User.findByPk(req.params.id);
|
const { currentPassword, newPassword } = req.body;
|
||||||
if (user) {
|
UserValidator.validateId(req.params.id);
|
||||||
// Erstelle eine Kopie der Request-Daten ohne sensible Felder
|
UserValidator.validatePasswordChange(currentPassword, newPassword);
|
||||||
const updateData = { ...req.body };
|
await UserService.changePassword(req.params.id, currentPassword, newPassword);
|
||||||
|
ErrorHandler.successResponse(res, null, 'Passwort erfolgreich geändert');
|
||||||
// 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) => {
|
|
||||||
try {
|
|
||||||
const user = await User.findByPk(req.params.id);
|
|
||||||
if (user) {
|
|
||||||
await user.destroy();
|
|
||||||
res.status(200).json({ message: 'User deleted successfully' });
|
|
||||||
} else {
|
|
||||||
res.status(404).json({ message: 'User not found' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting user:', error);
|
|
||||||
res.status(500).json({ message: 'Error deleting user' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
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 &&
|
||||||
|
|||||||
32781
package-lock.json
generated
32781
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
78
package.json
78
package.json
@@ -3,81 +3,81 @@
|
|||||||
"version": "1.1.0",
|
"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": {}
|
||||||
},
|
},
|
||||||
|
|||||||
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}
|
||||||
1
public/css/79.eca6f984.css
Normal file
1
public/css/79.eca6f984.css
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user