Added multiple features

This commit is contained in:
Torsten Schulz
2024-06-17 23:34:31 +02:00
parent 48a54ecdbb
commit 8c54988023
38 changed files with 1006 additions and 145 deletions

View File

@@ -0,0 +1,95 @@
const { Image } = require('../models');
const { v4: uuidv4 } = require('uuid');
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'public/images/uploads');
},
filename: (req, file, cb) => {
const uniqueSuffix = `${uuidv4()}${path.extname(file.originalname)}`;
cb(null, uniqueSuffix);
}
});
const upload = multer({ storage });
exports.uploadImage = upload.single('image');
exports.saveImageDetails = async (req, res) => {
try {
const { title, description, pageId } = req.body;
const filename = req.file.filename;
const newImage = await Image.create({
id: uuidv4(),
filename,
title,
description,
pageId: pageId || null
});
res.status(201).json(newImage);
} catch (error) {
console.error('Fehler beim Speichern des Bildes:', error);
res.status(500).send('Fehler beim Speichern des Bildes');
}
};
exports.getImages = async (req, res) => {
try {
const images = await Image.findAll();
res.status(200).json(images);
} catch (error) {
console.error('Fehler beim Abrufen der Bilder:', error);
res.status(500).send('Fehler beim Abrufen der Bilder');
}
};
exports.getImagesByPage = async (req, res) => {
try {
const { pageId } = req.params;
const images = await Image.findAll({ where: { pageId } });
res.status(200).json(images);
} catch (error) {
console.error('Fehler beim Abrufen der Bilder:', error);
res.status(500).send('Fehler beim Abrufen der Bilder');
}
};
exports.getImageById = async (req, res) => {
try {
const { id } = req.params;
const image = await Image.findByPk(id);
if (image) {
res.status(200).json(image);
} else {
res.status(404).send('Bild nicht gefunden');
}
} catch (error) {
console.error('Fehler beim Abrufen des Bildes:', error);
res.status(500).send('Fehler beim Abrufen des Bildes');
}
};
exports.updateImage = async (req, res) => {
try {
const { id } = req.params;
const { title, description } = req.body;
const image = await Image.findByPk(id);
if (!image) {
return res.status(404).json({ error: 'Bild nicht gefunden' });
}
image.title = title;
image.description = description;
await image.save();
res.json(image);
} catch (error) {
console.error('Fehler beim Aktualisieren des Bildes:', error);
res.status(500).json({ error: 'Fehler beim Aktualisieren des Bildes' });
}
};

View File

@@ -1,4 +1,4 @@
const { MenuItem } = require('../models'); // Stellen Sie sicher, dass das Modell korrekt importiert wird
const { MenuItem } = require('../models');
const fetchMenuData = require('../utils/fetchMenuData');
exports.getMenuData = async (req, res) => {
@@ -13,8 +13,15 @@ exports.getMenuData = async (req, res) => {
exports.saveMenuData = async (req, res) => {
try {
const menuData = req.body;
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: {} });
await MenuItem.bulkCreate(menuData, { include: [{ model: MenuItem, as: 'submenu' }] });
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);

View File

@@ -0,0 +1,60 @@
const { User } = require('../models');
exports.getAllUsers = async (req, res) => {
try {
const users = await User.findAll();
res.status(200).json(users);
} catch (error) {
res.status(500).json({ message: 'Error fetching users' });
}
};
exports.getUserById = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (user) {
res.status(200).json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (error) {
res.status(500).json({ message: 'Error fetching user' });
}
};
exports.createUser = async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (error) {
res.status(500).json({ message: 'Error creating user' });
}
};
exports.updateUser = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (user) {
await user.update(req.body);
res.status(200).json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (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) {
res.status(500).json({ message: 'Error deleting user' });
}
};

View File

@@ -1,4 +1,4 @@
const { Worship, EventPlace } = require('../models');
const { Worship, EventPlace, Sequelize } = require('../models');
const { Op } = require('sequelize'); // Importieren Sie die Operatoren von Sequelize
exports.getAllWorships = async (req, res) => {
@@ -53,7 +53,14 @@ exports.getFilteredWorships = async (req, res) => {
const where = {};
if (location && location !== '-1') {
where.eventPlaceId = location;
if (location.includes('|')) {
const locationsArray = location.split('|');
where.eventPlaceId = {
[Sequelize.Op.in]: locationsArray
}
} else {
where.eventPlaceId = location;
}
}
where.date = {

View File

@@ -0,0 +1,35 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('images', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4
},
filename: {
type: Sequelize.STRING,
allowNull: false
},
title: {
type: Sequelize.STRING,
allowNull: true
},
description: {
type: Sequelize.TEXT,
allowNull: true
},
uploadDate: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.NOW
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('images');
}
};

View File

@@ -0,0 +1,15 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.addColumn('images', 'pageId', {
type: Sequelize.INTEGER,
allowNull: true,
});
},
async down (queryInterface, Sequelize) {
await queryInterface.removeColumn('images', 'pageId');
}
};

36
models/Image.js Normal file
View File

@@ -0,0 +1,36 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Image = sequelize.define('Image', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
filename: {
type: DataTypes.STRING,
allowNull: false
},
title: {
type: DataTypes.STRING,
allowNull: true
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
uploadDate: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
pageId: {
type: DataTypes.UUID,
allowNull: true
}
}, {
tableName: 'images',
timestamps: false
});
return Image;
};

238
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "miriamgemeinde",
"version": "0.1.0",
"dependencies": {
"@iconoir/vue": "^7.7.0",
"@tiptap/extension-bold": "^2.4.0",
"@tiptap/extension-bullet-list": "^2.4.0",
"@tiptap/extension-heading": "^2.4.0",
@@ -21,7 +22,6 @@
"@tiptap/extension-underline": "^2.4.0",
"@tiptap/starter-kit": "^2.4.0",
"@tiptap/vue-3": "^2.4.0",
"@vueup/vue-quill": "^1.2.0",
"axios": "^1.7.2",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
@@ -32,10 +32,12 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.10.1",
"nodemon": "^3.1.3",
"sequelize": "^6.37.3",
"sequelize-cli": "^6.6.2",
"uuid": "^10.0.0",
"vm-browserify": "^1.1.2",
"vue": "^3.2.13",
"vue-multiselect": "^3.0.0",
@@ -1958,6 +1960,52 @@
"deprecated": "Use @eslint/object-schema instead",
"dev": true
},
"node_modules/@iconoir/vue": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@iconoir/vue/-/vue-7.7.0.tgz",
"integrity": "sha512-EvJK80DUGpCFd2MZsC5K6duv/h4zafruBSQYV9F5b+kJOrdWNCHxEHpwbUwjsI35GpSzMD0N2jAR86s3YTsMmw==",
"dependencies": {
"vue-demi": "^0.14.6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/iconoir"
},
"peerDependencies": {
"@vue/composition-api": ">=1.0.0-rc.1",
"vue": "^2.6.11 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@iconoir/vue/node_modules/vue-demi": {
"version": "0.14.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
"integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -3741,18 +3789,6 @@
"integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==",
"dev": true
},
"node_modules/@vueup/vue-quill": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@vueup/vue-quill/-/vue-quill-1.2.0.tgz",
"integrity": "sha512-kd5QPSHMDpycklojPXno2Kw2JSiKMYduKYQckTm1RJoVDA557MnyUXgcuuDpry4HY/Rny9nGNcK+m3AHk94wag==",
"dependencies": {
"quill": "^1.3.7",
"quill-delta": "^4.2.2"
},
"peerDependencies": {
"vue": "^3.2.41"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
@@ -4114,6 +4150,11 @@
"node": ">= 8"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
},
"node_modules/arch": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
@@ -4657,8 +4698,7 @@
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"node_modules/buffer-xor": {
"version": "1.0.3",
@@ -4666,6 +4706,17 @@
"integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
"dev": true
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -5143,6 +5194,47 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"engines": [
"node >= 0.8"
],
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concat-stream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/concat-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/concat-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
@@ -5280,8 +5372,7 @@
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cors": {
"version": "2.8.5",
@@ -7158,11 +7249,6 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w=="
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -8373,8 +8459,7 @@
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/isexe": {
"version": "2.0.0",
@@ -8822,11 +8907,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -8849,11 +8929,6 @@
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@@ -9463,7 +9538,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -9490,7 +9564,6 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -9537,6 +9610,23 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/multer": {
"version": "1.4.5-lts.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/multicast-dns": {
"version": "7.2.5",
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
@@ -10995,8 +11085,7 @@
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/progress": {
"version": "2.0.3",
@@ -11358,16 +11447,6 @@
"quill-delta": "^3.6.2"
}
},
"node_modules/quill-delta": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-4.2.2.tgz",
"integrity": "sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==",
"dependencies": {
"fast-diff": "1.2.0",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0"
}
},
"node_modules/quill/node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
@@ -11972,6 +12051,14 @@
"node": ">=10"
}
},
"node_modules/sequelize/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@@ -12290,6 +12377,15 @@
"websocket-driver": "^0.7.4"
}
},
"node_modules/sockjs/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -12436,6 +12532,14 @@
"readable-stream": "^3.5.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -12939,6 +13043,11 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
@@ -13064,8 +13173,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/utila": {
"version": "0.4.0",
@@ -13082,9 +13190,13 @@
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -13752,9 +13864,9 @@
}
},
"node_modules/webpack-dev-server/node_modules/ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"dev": true,
"engines": {
"node": ">=10.0.0"
@@ -13995,9 +14107,9 @@
"dev": true
},
"node_modules/ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"dev": true,
"engines": {
"node": ">=8.3.0"
@@ -14015,6 +14127,14 @@
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@@ -8,6 +8,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@iconoir/vue": "^7.7.0",
"@tiptap/extension-bold": "^2.4.0",
"@tiptap/extension-bullet-list": "^2.4.0",
"@tiptap/extension-heading": "^2.4.0",
@@ -21,7 +22,6 @@
"@tiptap/extension-underline": "^2.4.0",
"@tiptap/starter-kit": "^2.4.0",
"@tiptap/vue-3": "^2.4.0",
"@vueup/vue-quill": "^1.2.0",
"axios": "^1.7.2",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
@@ -32,10 +32,12 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.10.1",
"nodemon": "^3.1.3",
"sequelize": "^6.37.3",
"sequelize-cli": "^6.6.2",
"uuid": "^10.0.0",
"vm-browserify": "^1.1.2",
"vue": "^3.2.13",
"vue-multiselect": "^3.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

12
routes/image.js Normal file
View File

@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const { uploadImage, saveImageDetails, getImages, getImagesByPage, getImageById, updateImage } = require('../controllers/imageController');
const authMiddleware = require('../middleware/authMiddleware')
router.post('/', authMiddleware, uploadImage, saveImageDetails);
router.get('/', authMiddleware, getImages);
router.get('/page/:pageId', getImagesByPage);
router.get('/:id', getImageById);
router.put('/:id', authMiddleware, updateImage);
module.exports = router;

12
routes/users.js Normal file
View File

@@ -0,0 +1,12 @@
const express = require('express');
const router = express.Router();
const { getAllUsers, createUser, updateUser, deleteUser, getUserById } = require('../controllers/userController');
const authMiddleware = require('../middleware/authMiddleware');
router.get('/', authMiddleware, getAllUsers);
router.get('/:id', authMiddleware, getUserById);
router.post('/', authMiddleware, createUser);
router.put('/:id', authMiddleware, updateUser);
router.delete('/:id', authMiddleware, deleteUser);
module.exports = router;

View File

@@ -12,6 +12,8 @@ const eventRouter = require('./routes/event');
const menuDataRouter = require('./routes/menuData');
const worshipRouter = require('./routes/worships');
const pageRoutes = require('./routes/pages');
const userRoutes = require('./routes/users');
const imageRoutes = require('./routes/image');
const app = express();
const PORT = 3000;
@@ -29,7 +31,8 @@ app.use('/api/events', eventRouter);
app.use('/api/menu-data', menuDataRouter);
app.use('/api/worships', worshipRouter);
app.use('/api/page-content', pageRoutes);
app.use('/api/users', userRoutes);
app.use('/api/image', imageRoutes);
sequelize.sync().then(() => {
app.listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/icons/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/icons/table.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,7 @@
<template>
<footer class="footer">
<div class="left-links">
<router-link class="login-link" to="/login" v-if="!isLoggedIn">Login</router-link>
<router-link class="login-link" to="/auth/login" v-if="!isLoggedIn">Login</router-link>
<a v-if="isLoggedIn" @click="logout" class="logout-link">Logout</a>
</div>
<div class="right-links">

View File

@@ -11,6 +11,7 @@
<script>
import NavbarComponent from './NavbarComponent.vue';
import { mapActions } from 'vuex';
import router from '@/router'; // Importieren Sie den Router
export default {
name: 'HeaderComponent',
@@ -19,8 +20,16 @@ export default {
},
methods: {
...mapActions(['loadMenuData']),
reloadMenu() {
this.loadMenuData();
async reloadMenu() {
await this.loadMenuData();
this.$router.push({ path: '/' }); // Zurück zur Startseite oder eine andere Seite, um sicherzustellen, dass der Router neu geladen wird
const routes = this.$store.state.menuData.map(item => {
return {
path: item.link,
component: () => import(`../components/${item.component}.vue`)
};
});
routes.forEach(route => router.addRoute(route)); // Neue Routen hinzufügen
}
}
};

View File

@@ -7,12 +7,19 @@
<label for="location-select">Bitte wählen Sie den Ort für den Gottestdienst aus:</label>
</div>
<div>
<select id="location-select" v-model="selectedLocation">
<option :value="-1">Alle Orte</option>
<option v-for="location in locations" :key="location.id" :value="location.id">
{{ location.name }}
</option>
</select>
<multiselect
v-model="selectedLocations"
:options="locations"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
:preselect-first="false"
label="name"
track-by="id"
placeholder="Orte auswählen"
>
</multiselect>
</div>
<div>
<button @click="confirmWorshipConfiguration">Bestätigen</button>
@@ -26,14 +33,19 @@
<script>
import { ref } from 'vue';
import axios from '@/axios';
import Multiselect from 'vue-multiselect';
import 'vue-multiselect/dist/vue-multiselect.css';
export default {
name: 'WorshipDialog',
components: {
Multiselect
},
emits: ['confirm'],
setup(props, { emit }) {
const isOpen = ref(false);
const locations = ref([]);
const selectedLocation = ref(-1);
const selectedLocations = ref([]);
const openWorshipDialog = () => {
isOpen.value = true;
@@ -54,14 +66,15 @@ export default {
};
const confirmWorshipConfiguration = () => {
emit('confirm', selectedLocation.value);
const selectedLocationIds = selectedLocations.value.map(location => location.id).join('|');
emit('confirm', selectedLocationIds);
closeWorshipDialog();
};
return {
isOpen,
locations,
selectedLocation,
selectedLocations,
openWorshipDialog,
closeWorshipDialog,
confirmWorshipConfiguration,
@@ -88,4 +101,8 @@ export default {
padding: 20px;
border-radius: 8px;
}
</style>
.multiselect {
width: 100%;
}
</style>

View File

@@ -10,7 +10,7 @@
<div v-if="worship.neighborInvitation" class="neighborhood-invitation">Einladung zum Gottesdienst im Nachbarschaftsraum:</div>
<h3>
<span :class="worship.highlightTime ? 'highlight-time' : ''">{{ formatTime(worship.time) }}</span>&nbsp;-&nbsp;
{{ !worship.neighborInvitation ? worship.title : '' }}
{{ !worship.neighborInvitation ? worship.title : `Gottesdienst in ${worship.eventPlace.name}` }}
</h3>
<div v-if="worship.organizer">Gestaltung: {{ worship.organizer }}</div>
<div v-if="worship.collection">Kollekte: {{ worship.collection }}</div>

View File

@@ -1,12 +0,0 @@
<template>
<div>
<h2></h2>
</div>
</template>
<script>
export default {
name: 'AdminWorshipService',
};
</script>

View File

@@ -11,11 +11,18 @@
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">H1</button>
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()">H2</button>
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()">H3</button>
<button @click="editor.chain().focus().toggleBold().run()">Fett</button>
<button @click="editor.chain().focus().toggleItalic().run()">Kursiv</button>
<button @click="editor.chain().focus().toggleUnderline().run()">Unterstrichen</button>
<button @click="editor.chain().focus().toggleBold().run()" width="24" height="24">
<BoldIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().toggleItalic().run()">
<ItalicIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().toggleUnderline().run()">
<UnderlineIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().toggleStrike().run()">Durchgestrichen</button>
<button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">Tabelle</button>
<button
@click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">Tabelle</button>
<button @click="editor.chain().focus().toggleBulletList().run()">Liste</button>
<button @click="editor.chain().focus().toggleOrderedList().run()">Nummerierte Liste</button>
</div>
@@ -62,12 +69,16 @@ import OrderedList from '@tiptap/extension-ordered-list';
import Heading from '@tiptap/extension-heading';
import { CustomTableCell, CustomTableHeader } from '../../extensions/CustomTableCell';
import WorshipDialog from '@/components/WorshipDialog.vue';
import { BoldIcon, ItalicIcon, UnderlineIcon } from '@/icons';
export default {
name: 'EditPagesComponent',
components: {
EditorContent,
WorshipDialog,
BoldIcon,
ItalicIcon,
UnderlineIcon
},
setup() {
const store = useStore();
@@ -110,7 +121,7 @@ export default {
}
});
};
const fetchPages = async () => {
try {
const response = await axios.get('/menu-data');
@@ -179,9 +190,10 @@ export default {
worshipDialog.value.openWorshipDialog();
};
const insertWorshipList = (configuration) => {
const insertWorshipList = (selectedLocations) => {
if (editor.value) {
editor.value.chain().focus().insertContent(`{{ worshipslist:location=${configuration},order:"date asc" }}`).run();
const configuration = `location=${selectedLocations},order:"date asc"`;
editor.value.chain().focus().insertContent(`{{ worshipslist:${configuration} }}`).run();
}
};
@@ -215,7 +227,7 @@ export default {
margin-bottom: 10px;
}
.toolbar button {
.toolbar button {
margin-right: 5px;
}
@@ -242,4 +254,19 @@ export default {
.ql-editor {
background-color: #fff !important;
}
.edit-pages div>button {
border: none;
padding: 0.25em;
margin: 2px;
}
.button-icon {
background-image: url(@/assets/icons/Bold_Italic_Underline.png);
background-position: left top;
background-size: 10px 24px;
background-repeat: no-repeat;
width: 24px;
height: 24px;
}
</style>

View File

@@ -0,0 +1,134 @@
<template>
<div>
<h1>Bild hochladen</h1>
<form @submit.prevent="uploadImage">
<div>
<label for="title">Titel</label>
<input type="text" id="title" v-model="title" />
</div>
<div>
<label for="description">Beschreibung</label>
<textarea id="description" v-model="description"></textarea>
</div>
<div>
<label for="image">Bild</label>
<input type="file" id="image" @change="onFileChange" />
</div>
<div>
<label for="page">Seite</label>
<select id="page" v-model="selectedPage">
<option value="">Keine Seite</option>
<option v-for="page in pages" :key="page.id" :value="page.id">{{ page.title }}</option>
</select>
</div>
<button type="submit">Hochladen</button>
</form>
<div v-if="images.length">
<h2>Hochgeladene Bilder</h2>
<div v-for="image in images" :key="image.id" class="uploaded-image">
<img :src="`/images/uploads/${image.filename}`" :alt="image.title" width="100" />
<input type="text" v-model="image.title" @change="updateImage(image)" />
<textarea v-model="image.description" @change="updateImage(image)"></textarea>
<p>{{ formatDate(image.uploadDate) }} {{ formatTimeFromDate(image.uploadDate) }}</p>
</div>
</div>
</div>
</template>
<script>
import axios from '../../axios';
import { formatDate, formatTimeFromDate } from '@/utils/strings'
export default {
name: 'ImageUpload',
data() {
return {
title: '',
description: '',
image: null,
selectedPage: '',
pages: [],
images: []
};
},
methods: {
formatDate,
formatTimeFromDate,
onFileChange(e) {
this.image = e.target.files[0];
},
async uploadImage() {
const formData = new FormData();
formData.append('title', this.title);
formData.append('description', this.description);
formData.append('image', this.image);
formData.append('pageId', this.selectedPage);
try {
await axios.post('/image/', formData);
this.fetchImages();
this.resetForm();
} catch (error) {
console.error('Fehler beim Hochladen des Bildes:', error);
}
},
async fetchImages() {
try {
const response = await axios.get('/image');
this.images = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Bilder:', error);
}
},
async fetchPages() {
try {
const response = await axios.get('/image/pages');
this.pages = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Seiten:', error);
}
},
async updateImage(image) {
try {
await axios.put(`/image/${image.id}`, {
title: image.title,
description: image.description
});
this.fetchImages();
} catch (error) {
console.error('Fehler beim Aktualisieren des Bildes:', error);
}
},
resetForm() {
this.title = '';
this.description = '';
this.image = null;
this.selectedPage = '';
document.getElementById('image').value = null;
}
},
mounted() {
this.fetchImages();
this.fetchPages();
}
};
</script>
<style scoped>
form div {
margin-bottom: 10px;
}
.uploaded-image {
display: inline-block;
margin: 0 0 0.5em 0.5em;
border: 1px solid #e0e0e0;
padding: 10px;
}
.uploaded-image input,
.uploaded-image textarea {
width: 100%;
margin: 5px 0;
}
</style>

View File

@@ -1,19 +1,63 @@
<template>
<div>
<h1>Administration</h1>
<p>Hier kommt eine Navigation hin.</p>
</div>
</template>
<script>
export default {
name: 'DefaultComponent'
};
</script>
<style scoped>
div {
padding: 20px;
<div>
<h1>Seitenpflege</h1>
<p>Herzlich Willkommen. Auf diesen Seiten können Sie die Inhalte der Webseiten pflegen.</p>
<ul>
<li v-for="item in adminSubmenu" :key="item.id">
<router-link :to="item.link">{{ item.name }}</router-link>
</li>
</ul>
</div>
</template>
<script>
import axios from "../../axios";
import { ref, onMounted } from 'vue';
export default {
name: 'DefaultComponent',
setup() {
const adminSubmenu = ref([]);
const fetchMenuData = async () => {
try {
const response = await axios.get('/menu-data');
const menuData = response.data;
// Suche nach dem Admin-Submenü
const adminMenu = menuData.find(item => item.name === 'Admin');
if (adminMenu) {
adminSubmenu.value = adminMenu.submenu;
}
} catch (error) {
console.error('Fehler beim Abrufen der Menü-Daten:', error);
}
};
onMounted(() => {
fetchMenuData();
});
return {
adminSubmenu
};
}
</style>
};
</script>
<style scoped>
div {
padding: 20px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
padding: 0;
margin: 0;
}
</style>

View File

@@ -6,7 +6,7 @@
<button @click="saveMenuData">Speichern</button>
</div>
<div v-if="selectedMenuItem" class="edit-form">
<h2>Menüpunkt bearbeiten</h2>
<h2>{{ isEditMode ? 'Menüpunkt bearbeiten' : 'Menüpunkt erstellen' }}</h2>
<form @submit.prevent="saveMenuData">
<label for="name">Name</label>
<input id="name" v-model="selectedMenuItem.name" placeholder="Name" />
@@ -18,8 +18,7 @@
<input id="page-title" v-model="selectedMenuItem.pageTitle" placeholder="Seitenname" />
<label for="order-id">Order ID</label>
<input id="order-id" v-model.number="selectedMenuItem.orderId" placeholder="Order ID" type="number"
class="order-id" />
<input id="order-id" v-model.number="selectedMenuItem.order_id" placeholder="Order ID" type="number" class="order-id" />
<div class="checkbox-container">
<label>
@@ -33,8 +32,8 @@
</div>
<label for="parent-id">Elternelement</label>
<select id="parent-id" v-model="selectedMenuItem.parent_id">
<option value="">Ohne Elternelement</option>
<select id="parent-id" v-model.number="selectedMenuItem.parent_id">
<option value="-1">Ohne Elternelement</option>
<option v-for="item in flattenedMenuData" :key="item.id" :value="item.id">
<span v-html="getIndentedName(item)"></span>
</option>
@@ -42,6 +41,9 @@
<label for="component">Vue-Komponente</label>
<input id="component" v-model="selectedMenuItem.component" placeholder="Vue-Komponente" />
<button type="button" @click="resetForm">Neuen Menüpunkt erstellen</button>
<button type="submit">Speichern</button>
</form>
</div>
<div class="tree-view">
@@ -49,7 +51,7 @@
<li v-for="menuItem in sortedMenuData" :key="menuItem.id">
<div class="menu-item">
<span @click="selectMenuItem(menuItem)">
{{ menuItem.name }} (ID: {{ menuItem.orderId }})
{{ menuItem.name }} (ID: {{ menuItem.order_id }})
</span>
<div class="action-buttons">
<button @click="addSubmenu(menuItem)" class="action-button">Untermenü hinzufügen</button>
@@ -60,10 +62,10 @@
<li v-for="submenuItem in sortedSubmenu(menuItem)" :key="submenuItem.id">
<div class="menu-item">
<span @click="selectMenuItem(submenuItem)">
{{ submenuItem.name }} (ID: {{ submenuItem.orderId }})
{{ submenuItem.name }} (ID: {{ submenuItem.order_id }})
</span>
<div class="action-buttons">
<button @click="addSubmenu(menuItem)" class="action-button">Untermenü hinzufügen</button>
<button @click="addSubmenu(submenuItem)" class="action-button">Untermenü hinzufügen</button>
<button @click="removeSubmenu(menuItem, submenuItem)" class="action-button">Löschen</button>
</div>
</div>
@@ -78,12 +80,15 @@
<script>
import { ref, onMounted, computed } from 'vue';
import axios from '../../axios';
import { useStore } from 'vuex';
export default {
name: 'MenuManagement',
setup() {
const store = useStore();
const menuData = ref([]);
const selectedMenuItem = ref(null);
const isEditMode = ref(false);
const fetchMenuData = async () => {
try {
@@ -104,9 +109,14 @@ export default {
}
};
const flattenMenuData = (data, parentId = null) => {
const flattenMenuData = (data) => {
return data.reduce((acc, item) => {
const newItem = { ...item, parent_id: parentId, page_title: item.pageTitle };
const newItem = {
...item,
page_title: item.pageTitle,
show_in_menu: item.showInMenu,
requires_auth: item.requiresAuth,
};
const { submenu, ...rest } = newItem;
acc.push(rest);
if (submenu && submenu.length) {
@@ -116,6 +126,17 @@ export default {
}, []);
};
const flattenMenuStructure = (menuItems, indent = 0) => {
return menuItems.reduce((acc, item) => {
acc.push({ ...item, indent });
if (item.submenu && item.submenu.length) {
acc.push(...flattenMenuStructure(item.submenu, item.id, indent + 1));
}
console.log
return acc;
}, []);
};
const addMenuItem = () => {
const newItem = {
name: '',
@@ -124,12 +145,13 @@ export default {
pageTitle: '',
showInMenu: true,
requiresAuth: false,
orderId: 0,
order_id: 0,
submenu: [],
parent_id: null,
parent_id: 0,
};
menuData.value.push(newItem);
selectMenuItem(newItem);
isEditMode.value = false;
};
const addSubmenu = (menuItem) => {
@@ -140,11 +162,11 @@ export default {
pageTitle: '',
showInMenu: true,
requiresAuth: false,
orderId: 0,
parent_id: menuItem.id,
order_id: 0,
};
menuItem.submenu.push(newSubItem);
selectMenuItem(newSubItem);
isEditMode.value = false;
};
const removeMenuItem = (menuItem) => {
@@ -165,21 +187,35 @@ export default {
const selectMenuItem = (menuItem) => {
selectedMenuItem.value = menuItem;
isEditMode.value = true;
};
const resetForm = () => {
selectedMenuItem.value = null;
isEditMode.value = false;
};
const sortedMenuData = computed(() => {
return [...menuData.value].sort((a, b) => a.orderId - b.orderId);
return [...menuData.value].sort((a, b) => a.order_id - b.order_id);
});
const sortedSubmenu = (menuItem) => {
return menuItem.submenu.slice().sort((a, b) => a.orderId - b.orderId);
return menuItem.submenu.slice().sort((a, b) => a.order_id - b.order_id);
};
const getIndentedName = (item) => {
return '&nbsp;'.repeat(item.indent * 2) + item.name;
};
onMounted(fetchMenuData);
const flattenedMenuData = computed(() => {
const menuStruct = flattenMenuStructure(store.state.menuData);
console.log(menuStruct);
return menuStruct;
});
onMounted(() => {
fetchMenuData();
});
return {
menuData,
@@ -194,6 +230,9 @@ export default {
removeSubmenu,
selectMenuItem,
getIndentedName,
isEditMode,
resetForm,
flattenedMenuData,
};
},
};
@@ -277,4 +316,4 @@ export default {
.edit-form button {
margin-top: 5px;
}
</style>
</style>

View File

@@ -0,0 +1,153 @@
<template>
<div class="user-administration">
<h1>Benutzerverwaltung</h1>
<h2>{{ formTitle }}</h2>
<form @submit.prevent="saveUser">
<label for="name">Name:</label>
<input id="name" v-model="currentUser.name" required />
<label for="email">Email:</label>
<input id="email" v-model="currentUser.email" type="email" required />
<label for="password">Passwort:</label>
<input id="password" v-model="currentUser.password" type="password" :required="isCreating" />
<div>
<label for="active">Aktiv:</label>
<input id="active" v-model="currentUser.active" type="checkbox" />
</div>
<button type="submit">{{ isCreating ? 'Erstellen' : 'Aktualisieren' }}</button>
</form>
<button v-if="!isCreating" @click="resetForm">Zurück zu Benutzer erstellen</button>
<div v-if="users.length">
<h2>Vorhandene Benutzer</h2>
<ul>
<li v-for="user in users" :key="user.id" @click="editUser(user)">
{{ user.name }} ({{ user.email }})
</li>
</ul>
</div>
</div>
</template>
<script>
import axios from '@/axios';
export default {
name: 'UserAdministration',
data() {
return {
users: [],
currentUser: {
name: '',
email: '',
password: '',
active: false
},
isCreating: true
};
},
computed: {
formTitle() {
return this.isCreating ? 'Benutzer erstellen' : 'Benutzer bearbeiten';
}
},
methods: {
async fetchUsers() {
try {
const response = await axios.get('/users');
this.users = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Benutzer:', error);
}
},
async saveUser() {
if (this.isCreating) {
await this.createUser();
} else {
await this.updateUser();
}
this.resetForm();
this.fetchUsers();
},
async createUser() {
try {
await axios.post('/users', this.currentUser);
} catch (error) {
console.error('Fehler beim Erstellen des Benutzers:', error);
}
},
async updateUser() {
try {
await axios.put(`/users/${this.currentUser.id}`, this.currentUser);
} catch (error) {
console.error('Fehler beim Aktualisieren des Benutzers:', error);
}
},
editUser(user) {
this.currentUser = { ...user, password: '' };
this.isCreating = false;
},
resetForm() {
this.currentUser = {
name: '',
email: '',
password: '',
active: false
};
this.isCreating = true;
}
},
mounted() {
this.fetchUsers();
}
};
</script>
<style scoped>
.user-administration {
padding: 20px;
}
.user-administration h1,
.user-administration h2 {
margin-bottom: 20px;
}
.user-administration form {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
.user-administration label {
margin-top: 10px;
}
.user-administration input[type="text"],
.user-administration input[type="email"],
.user-administration input[type="password"] {
padding: 5px;
font-size: 16px;
}
.user-administration ul {
list-style-type: none;
padding: 0;
}
.user-administration li {
padding: 10px;
border-bottom: 1px solid #ddd;
cursor: pointer;
}
.user-administration li:hover {
background-color: #f0f0f0;
}
</style>

View File

@@ -57,7 +57,7 @@ export default {
localStorage.setItem('token', token);
this.login(data.user);
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
this.$router.push('/admin');
this.$router.push('/admin/index');
} catch (error) {
if (error.response) {
this.showDialog('Fehler', error.response.data.message);

View File

@@ -0,0 +1,24 @@
<template>
<div class="some-page">
<ContentComponent :link="currentLink" />
</div>
</template>
<script>
import ContentComponent from '@/components/ContentComponent.vue';
export default {
name: 'SomePage',
components: {
ContentComponent,
},
computed: {
currentLink() {
return this.$route.path;
},
},
};
</script>
<style scoped>
</style>

8
src/icons.js Normal file
View File

@@ -0,0 +1,8 @@
// src/icons.js
import { Bold, Italic, Underline } from '@iconoir/vue';
export {
Bold as BoldIcon,
Italic as ItalicIcon,
Underline as UnderlineIcon,
};

View File

@@ -16,20 +16,26 @@ function generateRoutesFromMenu(menu) {
if (item.link === '/admin/edit-pages') {
return;
}
let route = {
path: item.link,
meta: { requiresAuth: item.requiresAuth || false },
components: {
default: loadComponent(item.component),
rightColumn: loadComponent('ImageContent')
}
};
let route = null;
if (item.link && item.link !== '') {
route = {
path: item.link,
meta: { requiresAuth: item.requiresAuth || false },
components: {
default: loadComponent(item.component),
rightColumn: loadComponent('ImageContent')
}
};
}
if (item.submenu && item.submenu.length > 0) {
let children = generateRoutesFromMenu(item.submenu);
routes.push(...children);
}
routes.push(route);
if (route) {
routes.push(route);
}
});
return routes;
}

View File

@@ -9,3 +9,11 @@ export function formatDate(date) {
const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
return new Date(date).toLocaleDateString('de-DE', options);
}
export function formatTimeFromDate(dateString) {
const date = new Date(dateString);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}

View File

@@ -33,7 +33,7 @@ function buildMenuStructure(menuItems) {
component: item.component,
showInMenu: item.show_in_menu,
requiresAuth: item.requires_auth,
orderId: item.order_id,
order_id: item.order_id,
pageTitle: item.page_title,
submenu: []
};