Added Worship page rendering

This commit is contained in:
Torsten Schulz
2024-06-17 13:36:15 +02:00
parent 4f1390b794
commit 48a54ecdbb
17 changed files with 637 additions and 563 deletions

View File

@@ -1,4 +1,5 @@
const { Worship } = require('../models'); const { Worship, EventPlace } = require('../models');
const { Op } = require('sequelize'); // Importieren Sie die Operatoren von Sequelize
exports.getAllWorships = async (req, res) => { exports.getAllWorships = async (req, res) => {
try { try {
@@ -14,6 +15,7 @@ exports.createWorship = async (req, res) => {
const worship = await Worship.create(req.body); const worship = await Worship.create(req.body);
res.status(201).json(worship); res.status(201).json(worship);
} catch (error) { } catch (error) {
console.log(error);
res.status(500).json({ message: 'Fehler beim Erstellen des Gottesdienstes' }); res.status(500).json({ message: 'Fehler beim Erstellen des Gottesdienstes' });
} }
}; };
@@ -45,3 +47,31 @@ exports.deleteWorship = async (req, res) => {
res.status(500).json({ message: 'Fehler beim Löschen des Gottesdienstes' }); res.status(500).json({ message: 'Fehler beim Löschen des Gottesdienstes' });
} }
}; };
exports.getFilteredWorships = async (req, res) => {
const { location, orderBy } = req.query;
const where = {};
if (location && location !== '-1') {
where.eventPlaceId = location;
}
where.date = {
[Op.gte]: new Date(), // Only include events from today onwards
};
try {
const worships = await Worship.findAll({
where,
include: {
model: EventPlace,
as: 'eventPlace',
},
order: [orderBy.split(' ')],
});
res.status(200).json(worships);
} catch (error) {
console.log(error);
res.status(500).json({ message: 'Fehler beim Abrufen der gefilterten Gottesdienste' });
}
};

View File

@@ -0,0 +1,16 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('worships', 'day_name', {
type: Sequelize.STRING,
allowNull: false,
default: ''
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('worships', 'day_name');
}
};

View File

@@ -34,7 +34,7 @@ module.exports = (sequelize) => {
allowNull: false, allowNull: false,
defaultValue: 0 defaultValue: 0
}, },
page_title: { // Neuer Eintrag page_title: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
} }

View File

@@ -50,6 +50,11 @@ module.exports = (sequelize) => {
introLine: { introLine: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
},
dayName: {
type: DataTypes.STRING,
defaultValue: '',
allowNull: false
} }
}, { }, {
tableName: 'worships', tableName: 'worships',

10
package-lock.json generated
View File

@@ -28,6 +28,7 @@
"core-js": "^3.8.3", "core-js": "^3.8.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"date-fns": "^3.6.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@@ -5708,6 +5709,15 @@
"node": ">=0.12" "node": ">=0.12"
} }
}, },
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debounce": { "node_modules/debounce": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",

View File

@@ -28,6 +28,7 @@
"core-js": "^3.8.3", "core-js": "^3.8.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"date-fns": "^3.6.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",

View File

@@ -1,11 +1,12 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { getAllWorships, createWorship, updateWorship, deleteWorship } = require('../controllers/worshipController'); const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships } = require('../controllers/worshipController');
const authMiddleware = require('../middleware/authMiddleware'); const authMiddleware = require('../middleware/authMiddleware');
router.get('/', authMiddleware, getAllWorships); router.get('/', getAllWorships);
router.post('/', authMiddleware, createWorship); router.post('/', authMiddleware, createWorship);
router.put('/:id', authMiddleware, updateWorship); router.put('/:id', authMiddleware, updateWorship);
router.delete('/:id', authMiddleware, deleteWorship); router.delete('/:id', authMiddleware, deleteWorship);
router.get('/filtered', getFilteredWorships);
module.exports = router; module.exports = router;

View File

@@ -1,73 +1,74 @@
<template> <template>
<div> <div>
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
<div v-html="renderedContent"></div> <RenderContentComponent :content="content" />
</div> </div>
</template> </template>
<script> <script>
import axios from '@/axios'; import axios from '@/axios';
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { render } from '@/utils/render'; import RenderContentComponent from '@/components/RenderContentComponent.vue';
export default { export default {
name: 'ContentComponent', name: 'ContentComponent',
components: {
RenderContentComponent
},
props: { props: {
link: { link: {
type: String, type: String,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
content: '', content: '',
title: '', title: '',
}; };
}, },
computed: { computed: {
...mapState(['menuData']), ...mapState(['menuData']),
...mapGetters(['getMenuData']), ...mapGetters(['getMenuData']),
renderedContent() {
return render(this.content);
}
}, },
watch: { watch: {
link: { link: {
immediate: true, immediate: true,
handler(newLink) { handler(newLink) {
this.fetchContent(newLink); this.fetchContent(newLink);
this.setTitle(newLink); this.setTitle(newLink);
},
}, },
},
}, },
methods: { methods: {
async fetchContent(link) { async fetchContent(link) {
try { try {
const response = await axios.get(`/page-content?link=${link}`); const response = await axios.get(`/page-content?link=${link}`);
this.content = response.data.content; this.content = response.data.content;
} catch (error) { } catch (error) {
console.error('Fehler beim Abrufen des Inhalts:', error); console.error('Fehler beim Abrufen des Inhalts:', error);
}
},
setTitle(link) {
const findTitle = (menuItems, link) => {
for (const item of menuItems) {
if (item.link === link) {
return item.pageTitle || item.name;
} }
}, if (item.submenu && item.submenu.length > 0) {
setTitle(link) { const found = findTitle(item.submenu, link);
const findTitle = (menuItems, link) => { if (found) {
for (const item of menuItems) { return `${found}`;
if (item.link === link) { }
return item.pageTitle || item.name; }
} }
if (item.submenu && item.submenu.length > 0) { return '';
const found = findTitle(item.submenu, link); };
if (found) { this.title = findTitle(this.menuData, link);
return `${found}`; },
}
}
}
return '';
};
this.title = findTitle(this.menuData, link);
},
}, },
}; };
</script> </script>
<style scoped></style>
<style scoped></style>

View File

@@ -0,0 +1,74 @@
<template>
<div>
<component :is="dynamicComponent" v-bind="componentProps"></component>
</div>
</template>
<script>
import { createApp, h } from 'vue';
import WorshipRender from './WorshipRender.vue';
export default {
name: 'RenderContentComponent',
props: {
content: {
type: String,
required: true
}
},
data() {
return {
dynamicComponent: null,
componentProps: {}
};
},
watch: {
content: {
immediate: true,
handler(newContent) {
this.renderContent(newContent);
}
}
},
methods: {
renderContent(content) {
const worshipsPattern = /{{ worshipslist:(.*?) }}/g;
content.replace(worshipsPattern, (match, config) => {
const props = this.parseConfig(config);
this.dynamicComponent = WorshipRender;
this.componentProps = props;
return '<div id="worship-render-placeholder"></div>';
});
this.$nextTick(() => {
if (this.dynamicComponent) {
const placeholder = document.getElementById('worship-render-placeholder');
if (placeholder) {
const app = createApp({
render() {
return h(this.dynamicComponent, this.componentProps);
}
});
app.mount(placeholder);
}
}
});
},
parseConfig(configString) {
const config = {};
const configArray = configString.split(',');
configArray.forEach(item => {
const [key, value] = item.split('=');
if (key && value !== undefined) {
config[key.trim()] = isNaN(value) ? value.trim() : Number(value);
}
});
return config;
}
}
};
</script>
<style scoped>
/* Add styles if needed */
</style>

View File

@@ -0,0 +1,94 @@
<template>
<div>
<table v-if="worships.length" class="worships">
<tr v-for="worship in worships" :key="worship.id" :style="worship.eventPlace && worship.eventPlace.backgroundColor ? `background-color:${worship.eventPlace.backgroundColor}` : ''">
<td>
<div>{{ formatDate(worship.date) }}</div>
<div>{{ worship.dayName }}</div>
</td>
<td>
<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 : '' }}
</h3>
<div v-if="worship.organizer">Gestaltung: {{ worship.organizer }}</div>
<div v-if="worship.collection">Kollekte: {{ worship.collection }}</div>
<div v-if="worship.eventPlace.id && worship.eventPlace.id">
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{ worship.eventPlace.city }}
</div>
<div v-if="worship.selfInformation" class="selfinformation">Bitte informieren Sie sich auch auf den Internetseiten dieser Gemeinde!</div>
</td>
</tr>
</table>
<p v-else>Keine Gottesdienste verfügbar.</p>
</div>
</template>
<script>
import axios from '@/axios';
import { formatTime, formatDate } from '@/utils/strings';
export default {
name: 'WorshipRender',
props: {
location: {
type: Number,
required: true
},
orderBy: {
type: String,
default: 'date ASC'
}
},
data() {
return {
worships: []
};
},
async created() {
await this.fetchWorships();
},
methods: {
formatTime,
formatDate,
async fetchWorships() {
try {
const response = await axios.get('/worships/filtered', {
params: {
location: this.location,
orderBy: this.orderBy
}
});
this.worships = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Gottesdienste:', error);
}
}
}
};
</script>
<style scoped>
table.worships {
border-collapse: collapse;
width: 100%;
}
table.worships td {
border: 1px solid #000;
text-align: center;
}
h3 {
margin: 0;
}
table.worships td div{
margin: 5px;
}
.highlight-time {
text-decoration: underline;
}
.neighborhood-invitation {
font-weight: bold;
color: #0020e0;
}
</style>

View File

@@ -1,207 +0,0 @@
<template>
<div class="admin-menu-management">
<h2>Menüverwaltung</h2>
<div>
<h3>Neuen Menüpunkt hinzufügen</h3>
<form @submit.prevent="addMenuItem">
<label for="name">Name:</label>
<input type="text" v-model="newItem.name" required />
<label for="link">Link:</label>
<input type="text" v-model="newItem.link" required />
<label for="component">Component:</label>
<input type="text" v-model="newItem.component" required />
<label for="showInMenu">Im Menü anzeigen:</label>
<input type="checkbox" v-model="newItem.showInMenu" />
<label for="requiresAuth">Authentifizierung erforderlich:</label>
<input type="checkbox" v-model="newItem.requiresAuth" />
<label for="image">Bild:</label>
<input type="text" v-model="newItem.image" />
<label for="parentId">Eltern-Menüpunkt:</label>
<select v-model="newItem.parentId">
<option :value="null">Kein Eltern-Menüpunkt</option>
<option v-for="item in menuItems" :key="item.id" :value="item.id">
{{ item.name }}
</option>
</select>
<button type="submit">Hinzufügen</button>
</form>
</div>
<div>
<h3>Menüpunkte bearbeiten</h3>
<ul>
<li v-for="item in menuItems" :key="item.id">
{{ item.name }}
<button @click="editMenuItem(item)">Bearbeiten</button>
<button @click="deleteMenuItem(item.id)">Löschen</button>
</li>
</ul>
</div>
<div v-if="editingItem">
<h3>Menüpunkt bearbeiten</h3>
<form @submit.prevent="updateMenuItem">
<label for="editName">Name:</label>
<input type="text" v-model="editingItem.name" required />
<label for="editLink">Link:</label>
<input type="text" v-model="editingItem.link" required />
<label for="editComponent">Component:</label>
<input type="text" v-model="editingItem.component" required />
<label for="editShowInMenu">Im Menü anzeigen:</label>
<input type="checkbox" v-model="editingItem.showInMenu" />
<label for="editRequiresAuth">Authentifizierung erforderlich:</label>
<input type="checkbox" v-model="editingItem.requiresAuth" />
<label for="editImage">Bild:</label>
<input type="text" v-model="editingItem.image" />
<label for="editParentId">Eltern-Menüpunkt:</label>
<select v-model="editingItem.parentId">
<option :value="null">Kein Eltern-Menüpunkt</option>
<option v-for="item in menuItems" :key="item.id" :value="item.id">
{{ item.name }}
</option>
</select>
<button type="submit">Aktualisieren</button>
<button @click="cancelEdit">Abbrechen</button>
</form>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
name: 'AdminMenuManagement',
data() {
return {
newItem: {
name: '',
link: '',
component: '',
showInMenu: false,
requiresAuth: false,
image: '',
parentId: null
},
editingItem: null
};
},
computed: {
...mapState(['menuData']),
menuItems() {
return this.menuData;
}
},
methods: {
...mapActions(['loadMenuData']),
async addMenuItem() {
this.menuData.push(this.newItem);
await this.saveMenuData();
this.resetNewItem();
},
editMenuItem(item) {
this.editingItem = { ...item };
},
async updateMenuItem() {
const index = this.menuData.findIndex(item => item.id === this.editingItem.id);
if (index !== -1) {
this.menuData.splice(index, 1, this.editingItem);
await this.saveMenuData();
this.cancelEdit();
}
},
async deleteMenuItem(id) {
this.menuData = this.menuData.filter(item => item.id !== id);
await this.saveMenuData();
},
cancelEdit() {
this.editingItem = null;
},
resetNewItem() {
this.newItem = {
name: '',
link: '',
component: '',
showInMenu: false,
requiresAuth: false,
image: '',
parentId: null
};
},
async saveMenuData() {
try {
await fetch('/menu-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.menuData)
});
await this.loadMenuData();
} catch (error) {
console.error('Fehler beim Speichern der Menü-Daten:', error);
}
}
},
created() {
this.loadMenuData();
}
};
</script>
<style scoped>
.admin-menu-management {
padding: 20px;
}
form {
margin-bottom: 20px;
}
form label {
display: block;
margin-top: 10px;
}
form input,
form select {
width: 100%;
padding: 5px;
margin-top: 5px;
}
button {
margin-top: 10px;
padding: 5px 10px;
cursor: pointer;
}
ul {
list-style-type: none;
padding: 0;
}
li {
margin: 10px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
li button {
margin-left: 10px;
}
</style>

View File

@@ -46,7 +46,7 @@
</template> </template>
<script> <script>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted, watch } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import axios from '../../axios'; import axios from '../../axios';
import { EditorContent, useEditor } from '@tiptap/vue-3'; import { EditorContent, useEditor } from '@tiptap/vue-3';
@@ -97,7 +97,7 @@ export default {
], ],
content: '', content: '',
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {
store.commit('UPDATE_PAGE_CONTENT', editor.getHTML()); store.commit('SET_PAGE_CONTENT', editor.getHTML());
}, },
}); });
@@ -118,6 +118,7 @@ export default {
const allPages = []; const allPages = [];
flattenPages(data, allPages); flattenPages(data, allPages);
pages.value = allPages.sort((a, b) => a.name.localeCompare(b.name)); pages.value = allPages.sort((a, b) => a.name.localeCompare(b.name));
store.commit('setMenuData', data);
} catch (error) { } catch (error) {
console.error('Fehler beim Abrufen der Seiten:', error); console.error('Fehler beim Abrufen der Seiten:', error);
} }
@@ -127,7 +128,6 @@ export default {
try { try {
await store.dispatch('loadPageContent', selectedPage.value); await store.dispatch('loadPageContent', selectedPage.value);
const content = store.getters.pageContent; const content = store.getters.pageContent;
const setEditorContent = () => { const setEditorContent = () => {
if (editor.value && editor.value.commands) { if (editor.value && editor.value.commands) {
editor.value.commands.setContent(content, false); editor.value.commands.setContent(content, false);
@@ -135,7 +135,6 @@ export default {
setTimeout(setEditorContent, 100); setTimeout(setEditorContent, 100);
} }
}; };
setEditorContent(); setEditorContent();
} catch (error) { } catch (error) {
console.error('Fehler beim Laden des Seiteninhalts:', error); console.error('Fehler beim Laden des Seiteninhalts:', error);
@@ -167,6 +166,15 @@ export default {
return pages.value; return pages.value;
}); });
watch(selectedPage, (newPage) => {
store.dispatch('setSelectedPage', newPage);
const page = pages.value.find(page => page.link === newPage);
if (page) {
store.dispatch('setPageTitle', page.name);
}
loadPageContent();
});
const openWorshipDialog = () => { const openWorshipDialog = () => {
worshipDialog.value.openWorshipDialog(); worshipDialog.value.openWorshipDialog();
}; };

View File

@@ -1,272 +1,280 @@
<template> <template>
<div class="menu-management"> <div class="menu-management">
<h1>Menüverwaltung</h1> <h1>Menüverwaltung</h1>
<div class="button-container"> <div class="button-container">
<button @click="addMenuItem">Hauptmenü hinzufügen</button> <button @click="addMenuItem">Hauptmenü hinzufügen</button>
<button @click="saveMenuData">Speichern</button> <button @click="saveMenuData">Speichern</button>
</div>
<div v-if="selectedMenuItem" class="edit-form">
<h2>Menüpunkt bearbeiten</h2>
<form @submit.prevent="saveMenuData">
<label for="name">Name</label>
<input id="name" v-model="selectedMenuItem.name" placeholder="Name" />
<label for="link">Link</label>
<input id="link" v-model="selectedMenuItem.link" placeholder="Link" />
<label for="order-id">Order ID</label>
<input id="order-id" v-model.number="selectedMenuItem.orderId" placeholder="Order ID" type="number" class="order-id" />
<div class="checkbox-container">
<label>
<input type="checkbox" v-model="selectedMenuItem.showInMenu" />
Im Menü anzeigen
</label>
<label>
<input type="checkbox" v-model="selectedMenuItem.requiresAuth" />
Authentifizierung erforderlich
</label>
</div>
<label for="parent-id">Elternelement</label>
<select id="parent-id" v-model="selectedMenuItem.parent_id">
<option value="">Ohne Elternelement</option>
<option v-for="item in flattenedMenuData" :key="item.id" :value="item.id">
<span v-html="getIndentedName(item)"></span>
</option>
</select>
</form>
</div>
<div class="tree-view">
<ul>
<li v-for="menuItem in sortedMenuData" :key="menuItem.id">
<div class="menu-item">
<span @click="selectMenuItem(menuItem)">
{{ menuItem.name }} (ID: {{ menuItem.orderId }})
</span>
<div class="action-buttons">
<button @click="addSubmenu(menuItem)" class="action-button">Untermenü hinzufügen</button>
<button @click="removeMenuItem(menuItem)" class="action-button">Löschen</button>
</div>
</div>
<ul v-if="menuItem.submenu.length">
<li v-for="submenuItem in sortedSubmenu(menuItem)" :key="submenuItem.id">
<div class="menu-item">
<span @click="selectMenuItem(submenuItem)">
{{ submenuItem.name }} (ID: {{ submenuItem.orderId }})
</span>
<div class="action-buttons">
<button @click="addSubmenu(menuItem)" class="action-button">Untermenü hinzufügen</button>
<button @click="removeSubmenu(menuItem, submenuItem)" class="action-button">Löschen</button>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div> </div>
</template> <div v-if="selectedMenuItem" class="edit-form">
<h2>Menüpunkt bearbeiten</h2>
<form @submit.prevent="saveMenuData">
<label for="name">Name</label>
<input id="name" v-model="selectedMenuItem.name" placeholder="Name" />
<script> <label for="link">Link</label>
import { ref, onMounted, computed } from 'vue'; <input id="link" v-model="selectedMenuItem.link" placeholder="Link" />
import axios from '../../axios';
export default { <label for="page-title">Seitenname</label>
name: 'MenuManagement', <input id="page-title" v-model="selectedMenuItem.pageTitle" placeholder="Seitenname" />
setup() {
const menuData = ref([]);
const selectedMenuItem = ref(null);
const fetchMenuData = async () => { <label for="order-id">Order ID</label>
try { <input id="order-id" v-model.number="selectedMenuItem.orderId" placeholder="Order ID" type="number"
const response = await axios.get('/menu-data'); class="order-id" />
menuData.value = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Menü-Daten:', error);
}
};
const saveMenuData = async () => { <div class="checkbox-container">
try { <label>
const flatMenuData = flattenMenuData(menuData.value); <input type="checkbox" v-model="selectedMenuItem.showInMenu" />
await axios.post('/menu-data', flatMenuData); Im Menü anzeigen
alert('Menü-Daten erfolgreich gespeichert'); </label>
} catch (error) { <label>
console.error('Fehler beim Speichern der Menü-Daten:', error); <input type="checkbox" v-model="selectedMenuItem.requiresAuth" />
} Authentifizierung erforderlich
}; </label>
</div>
const flattenMenuData = (data, parentId = null) => { <label for="parent-id">Elternelement</label>
return data.reduce((acc, item) => { <select id="parent-id" v-model="selectedMenuItem.parent_id">
const newItem = { ...item, parent_id: parentId }; <option value="">Ohne Elternelement</option>
const { submenu, ...rest } = newItem; <option v-for="item in flattenedMenuData" :key="item.id" :value="item.id">
acc.push(rest); <span v-html="getIndentedName(item)"></span>
if (submenu && submenu.length) { </option>
acc.push(...flattenMenuData(submenu, newItem.id)); </select>
}
return acc;
}, []);
};
const addMenuItem = () => { <label for="component">Vue-Komponente</label>
const newItem = { <input id="component" v-model="selectedMenuItem.component" placeholder="Vue-Komponente" />
name: '', </form>
link: '', </div>
component: '', <div class="tree-view">
showInMenu: true, <ul>
requiresAuth: false, <li v-for="menuItem in sortedMenuData" :key="menuItem.id">
orderId: 0, <div class="menu-item">
submenu: [], <span @click="selectMenuItem(menuItem)">
parent_id: null, {{ menuItem.name }} (ID: {{ menuItem.orderId }})
}; </span>
menuData.value.push(newItem); <div class="action-buttons">
selectMenuItem(newItem); <button @click="addSubmenu(menuItem)" class="action-button">Untermenü hinzufügen</button>
}; <button @click="removeMenuItem(menuItem)" class="action-button">Löschen</button>
</div>
</div>
<ul v-if="menuItem.submenu.length">
<li v-for="submenuItem in sortedSubmenu(menuItem)" :key="submenuItem.id">
<div class="menu-item">
<span @click="selectMenuItem(submenuItem)">
{{ submenuItem.name }} (ID: {{ submenuItem.orderId }})
</span>
<div class="action-buttons">
<button @click="addSubmenu(menuItem)" class="action-button">Untermenü hinzufügen</button>
<button @click="removeSubmenu(menuItem, submenuItem)" class="action-button">Löschen</button>
</div>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
const addSubmenu = (menuItem) => { <script>
const newSubItem = { import { ref, onMounted, computed } from 'vue';
name: '', import axios from '../../axios';
link: '',
component: '',
showInMenu: true,
requiresAuth: false,
orderId: 0,
parent_id: menuItem.id,
};
menuItem.submenu.push(newSubItem);
selectMenuItem(newSubItem);
};
const removeMenuItem = (menuItem) => { export default {
const index = menuData.value.indexOf(menuItem); name: 'MenuManagement',
if (index > -1) { setup() {
menuData.value.splice(index, 1); const menuData = ref([]);
} const selectedMenuItem = ref(null);
selectedMenuItem.value = null;
};
const removeSubmenu = (menuItem, submenuItem) => { const fetchMenuData = async () => {
const index = menuItem.submenu.indexOf(submenuItem); try {
if (index > -1) { const response = await axios.get('/menu-data');
menuItem.submenu.splice(index, 1); menuData.value = response.data;
} } catch (error) {
selectedMenuItem.value = null; console.error('Fehler beim Abrufen der Menü-Daten:', error);
}; }
const selectMenuItem = (menuItem) => {
selectedMenuItem.value = menuItem;
};
const sortedMenuData = computed(() => {
return [...menuData.value].sort((a, b) => a.orderId - b.orderId);
});
const sortedSubmenu = (menuItem) => {
return menuItem.submenu.slice().sort((a, b) => a.orderId - b.orderId);
};
const getIndentedName = (item) => {
return '&nbsp;'.repeat(item.indent * 2) + item.name;
};
onMounted(fetchMenuData);
return {
menuData,
sortedMenuData,
sortedSubmenu,
selectedMenuItem,
fetchMenuData,
saveMenuData,
addMenuItem,
addSubmenu,
removeMenuItem,
removeSubmenu,
selectMenuItem,
getIndentedName,
};
},
}; };
</script>
<style scoped> const saveMenuData = async () => {
.menu-management { try {
width: 100%; const flatMenuData = flattenMenuData(menuData.value);
margin: auto; await axios.post('/menu-data', flatMenuData);
} alert('Menü-Daten erfolgreich gespeichert');
} catch (error) {
console.error('Fehler beim Speichern der Menü-Daten:', error);
}
};
.button-container { const flattenMenuData = (data, parentId = null) => {
display: inline-flex; return data.reduce((acc, item) => {
gap: 10px; const newItem = { ...item, parent_id: parentId, page_title: item.pageTitle };
margin-bottom: 20px; const { submenu, ...rest } = newItem;
} acc.push(rest);
if (submenu && submenu.length) {
acc.push(...flattenMenuData(submenu, newItem.id));
}
return acc;
}, []);
};
.tree-view { const addMenuItem = () => {
margin-top: 20px; const newItem = {
} name: '',
link: '',
component: '',
pageTitle: '',
showInMenu: true,
requiresAuth: false,
orderId: 0,
submenu: [],
parent_id: null,
};
menuData.value.push(newItem);
selectMenuItem(newItem);
};
.tree-view ul { const addSubmenu = (menuItem) => {
list-style-type: none; const newSubItem = {
padding: 0; name: '',
} link: '',
component: '',
pageTitle: '',
showInMenu: true,
requiresAuth: false,
orderId: 0,
parent_id: menuItem.id,
};
menuItem.submenu.push(newSubItem);
selectMenuItem(newSubItem);
};
.tree-view li { const removeMenuItem = (menuItem) => {
margin-bottom: 5px; const index = menuData.value.indexOf(menuItem);
padding-left: 20px; if (index > -1) {
} menuData.value.splice(index, 1);
}
selectedMenuItem.value = null;
};
.tree-view .menu-item { const removeSubmenu = (menuItem, submenuItem) => {
display: inline-flex; const index = menuItem.submenu.indexOf(submenuItem);
width: 100%; if (index > -1) {
justify-content: space-between; menuItem.submenu.splice(index, 1);
align-items: center; }
} selectedMenuItem.value = null;
};
.tree-view span { const selectMenuItem = (menuItem) => {
cursor: pointer; selectedMenuItem.value = menuItem;
color: black; };
}
.tree-view button { const sortedMenuData = computed(() => {
border: none; return [...menuData.value].sort((a, b) => a.orderId - b.orderId);
height: 1.6em; });
padding: 0 0.5em;
margin: 1px;
border-radius: 5px;
}
.tree-view span:hover { const sortedSubmenu = (menuItem) => {
text-decoration: underline; return menuItem.submenu.slice().sort((a, b) => a.orderId - b.orderId);
} };
.edit-form { const getIndentedName = (item) => {
margin-top: 20px; return '&nbsp;'.repeat(item.indent * 2) + item.name;
} };
.edit-form label { onMounted(fetchMenuData);
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.edit-form input:not([type="checkbox"]) { return {
display: block; menuData,
margin-bottom: 10px; sortedMenuData,
} sortedSubmenu,
selectedMenuItem,
fetchMenuData,
saveMenuData,
addMenuItem,
addSubmenu,
removeMenuItem,
removeSubmenu,
selectMenuItem,
getIndentedName,
};
},
};
</script>
.edit-form .checkbox-container { <style scoped>
display: flex; .menu-management {
flex-direction: column; width: 100%;
margin-right: 10px; margin: auto;
} }
.edit-form .order-id { .button-container {
width: 50px; display: inline-flex;
} gap: 10px;
margin-bottom: 20px;
}
.edit-form button { .tree-view {
margin-top: 5px; margin-top: 20px;
} }
</style>
.tree-view ul {
list-style-type: none;
padding: 0;
}
.tree-view li {
margin-bottom: 5px;
padding-left: 20px;
}
.tree-view .menu-item {
display: inline-flex;
width: 100%;
justify-content: space-between;
align-items: center;
}
.tree-view span {
cursor: pointer;
color: black;
}
.tree-view button {
border: none;
height: 1.6em;
padding: 0 0.5em;
margin: 1px;
border-radius: 5px;
}
.tree-view span:hover {
text-decoration: underline;
}
.edit-form {
margin-top: 20px;
}
.edit-form label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.edit-form input:not([type="checkbox"]) {
display: block;
margin-bottom: 10px;
}
.edit-form .checkbox-container {
display: flex;
flex-direction: column;
margin-right: 10px;
}
.edit-form .order-id {
width: 50px;
}
.edit-form button {
margin-top: 5px;
}
</style>

View File

@@ -1,26 +1,55 @@
<template> <template>
<div> <div>
<div class="previewinfo">Dies ist eine Vorschau.</div> <div class="previewinfo">Dies ist eine Vorschau.</div>
<div v-html="renderedContent"></div> <h1>{{ title }}</h1>
<RenderContentComponent :content="content" />
</div> </div>
</template> </template>
<script> <script>
import { computed } from 'vue'; import { computed, watchEffect } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import { render } from '@/utils/render'; import RenderContentComponent from '@/components/RenderContentComponent.vue';
export default { export default {
name: 'PagePreview', name: 'PagePreview',
components: {
RenderContentComponent
},
setup() { setup() {
const store = useStore(); const store = useStore();
const content = computed(() => store.state.pageContent); const content = computed(() => store.state.pageContent);
const renderedContent = computed(() => render(content.value)); const selectedPage = computed(() => store.state.selectedPage);
const menuData = computed(() => store.state.menuData);
const title = computed(() => store.state.pageTitle);
const setTitle = (link) => {
const findTitle = (menuItems, link) => {
for (const item of menuItems) {
if (item.link === link) {
return item.pageTitle || item.name;
}
if (item.submenu && item.submenu.length > 0) {
const found = findTitle(item.submenu, link);
if (found) {
return found;
}
}
}
return '';
};
store.dispatch('setPageTitle', findTitle(menuData.value, link));
};
watchEffect(() => {
setTitle(selectedPage.value);
});
return { return {
renderedContent, content,
title
}; };
}, }
}; };
</script> </script>

View File

@@ -9,6 +9,9 @@
<label for="date">Datum:</label> <label for="date">Datum:</label>
<input type="date" id="date" v-model="worshipData.date" required> <input type="date" id="date" v-model="worshipData.date" required>
<label for="dayName">Name des Tags:</label>
<input type="text" id="dayName" v-model="worshipData.dayName" required>
<label for="time">Uhrzeit:</label> <label for="time">Uhrzeit:</label>
<input type="time" id="time" v-model="worshipData.time" required> <input type="time" id="time" v-model="worshipData.time" required>

View File

@@ -1,6 +1,6 @@
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import axios from 'axios'; import axios from 'axios';
import router from '../router'; // Importieren des Routers import router from '../router';
let user = []; let user = [];
try { try {
@@ -16,6 +16,8 @@ export default createStore({
token: localStorage.getItem('token') || '', token: localStorage.getItem('token') || '',
menuData: [], menuData: [],
pageContent: '', pageContent: '',
pageTitle: '',
selectedPage: '',
}, },
mutations: { mutations: {
setLogin(state, { user, token }) { setLogin(state, { user, token }) {
@@ -44,6 +46,12 @@ export default createStore({
UPDATE_PAGE_CONTENT(state, content) { UPDATE_PAGE_CONTENT(state, content) {
state.pageContent = content; state.pageContent = content;
}, },
setPageTitle(state, title) {
state.pageTitle = title;
},
setSelectedPage(state, page) {
state.selectedPage = page;
},
}, },
actions: { actions: {
async loadMenuData({ commit }) { async loadMenuData({ commit }) {
@@ -77,6 +85,12 @@ export default createStore({
console.error('Fehler beim Speichern des Seiteninhalts:', error); console.error('Fehler beim Speichern des Seiteninhalts:', error);
} }
}, },
setPageTitle({ commit }, title) {
commit('setPageTitle', title);
},
setSelectedPage({ commit }, page) {
commit('setSelectedPage', page);
},
login({ commit }, { user, token }) { login({ commit }, { user, token }) {
commit('setLogin', { user, token }); commit('setLogin', { user, token });
}, },
@@ -89,5 +103,7 @@ export default createStore({
user: state => state.user, user: state => state.user,
menuData: state => state.menuData, menuData: state => state.menuData,
pageContent: state => state.pageContent, pageContent: state => state.pageContent,
pageTitle: state => state.pageTitle,
selectedPage: state => state.selectedPage,
} }
}); });

View File

@@ -1,15 +0,0 @@
export function render(content) {
console.log('do render', content);
const worshipsPattern = /{{ worshipslist:(.*?) }}/g;
const renderedContent = content.replace(worshipsPattern, (match, config) => {
return renderWorships(config);
});
return renderedContent;
}
function renderWorships(config) {
console.log('render worships', config);
return `<div class="worships-list">Worships at location ${location}</div>`;
}