Multiple changes

This commit is contained in:
Torsten Schulz
2024-06-16 23:03:44 +02:00
parent 4e371f88b1
commit 4f1390b794
17 changed files with 613 additions and 31 deletions

2
.env
View File

@@ -1 +1 @@
VUE_APP_BACKEND_URL= VUE_APP_BACKEND_URL=http://localhost:3000/api

View File

@@ -1,5 +1,4 @@
// controllers/menuDataController.js const { MenuItem } = require('../models'); // Stellen Sie sicher, dass das Modell korrekt importiert wird
const fetchMenuData = require('../utils/fetchMenuData'); const fetchMenuData = require('../utils/fetchMenuData');
exports.getMenuData = async (req, res) => { exports.getMenuData = async (req, res) => {
@@ -15,9 +14,10 @@ exports.saveMenuData = async (req, res) => {
try { try {
const menuData = req.body; const menuData = req.body;
await MenuItem.destroy({ where: {} }); await MenuItem.destroy({ where: {} });
await MenuItem.bulkCreate(menuData); await MenuItem.bulkCreate(menuData, { include: [{ model: MenuItem, as: 'submenu' }] });
res.status(200).send('Menü-Daten erfolgreich gespeichert'); res.status(200).send('Menü-Daten erfolgreich gespeichert');
} catch (error) { } catch (error) {
console.error('Fehler beim Speichern der Menü-Daten:', error);
res.status(500).send('Fehler beim Speichern der Menü-Daten'); res.status(500).send('Fehler beim Speichern der Menü-Daten');
} }
}; };

View File

@@ -0,0 +1,15 @@
// migrations/[timestamp]-add-page-title-to-menu-items.js
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('menu_items', 'page_title', {
type: Sequelize.STRING,
allowNull: true
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('menu_items', 'page_title');
}
};

View File

@@ -1,3 +1,4 @@
// models/MenuItem.js
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
@@ -32,6 +33,10 @@ module.exports = (sequelize) => {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
defaultValue: 0 defaultValue: 0
},
page_title: { // Neuer Eintrag
type: DataTypes.STRING,
allowNull: true
} }
}, { }, {
tableName: 'menu_items', tableName: 'menu_items',

View File

@@ -3,7 +3,7 @@ const router = express.Router();
const pageController = require('../controllers/pageController'); const pageController = require('../controllers/pageController');
const authMiddleware = require('../middleware/authMiddleware') const authMiddleware = require('../middleware/authMiddleware')
router.get('/', authMiddleware, pageController.getPageContent); router.get('/', pageController.getPageContent);
router.post('/', authMiddleware, pageController.savePageContent); router.post('/', authMiddleware, pageController.savePageContent);
module.exports = router; module.exports = router;

View File

@@ -0,0 +1,73 @@
<template>
<div>
<h1>{{ title }}</h1>
<div v-html="renderedContent"></div>
</div>
</template>
<script>
import axios from '@/axios';
import { mapState, mapGetters } from 'vuex';
import { render } from '@/utils/render';
export default {
name: 'ContentComponent',
props: {
link: {
type: String,
required: true,
},
},
data() {
return {
content: '',
title: '',
};
},
computed: {
...mapState(['menuData']),
...mapGetters(['getMenuData']),
renderedContent() {
return render(this.content);
}
},
watch: {
link: {
immediate: true,
handler(newLink) {
this.fetchContent(newLink);
this.setTitle(newLink);
},
},
},
methods: {
async fetchContent(link) {
try {
const response = await axios.get(`/page-content?link=${link}`);
this.content = response.data.content;
} catch (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) {
const found = findTitle(item.submenu, link);
if (found) {
return `${found}`;
}
}
}
return '';
};
this.title = findTitle(this.menuData, link);
},
},
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,91 @@
<template>
<div>
<div v-if="isOpen" class="dialog-overlay">
<div class="dialog-content">
<h3>Gottesdienst-Konfiguration</h3>
<div>
<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>
</div>
<div>
<button @click="confirmWorshipConfiguration">Bestätigen</button>
<button @click="closeWorshipDialog">Schließen</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import axios from '@/axios';
export default {
name: 'WorshipDialog',
emits: ['confirm'],
setup(props, { emit }) {
const isOpen = ref(false);
const locations = ref([]);
const selectedLocation = ref(-1);
const openWorshipDialog = () => {
isOpen.value = true;
fetchLocations();
};
const closeWorshipDialog = () => {
isOpen.value = false;
};
const fetchLocations = async () => {
try {
const response = await axios.get('/event-places');
locations.value = response.data;
} catch (error) {
console.error('Fehler beim Laden der Orte:', error);
}
};
const confirmWorshipConfiguration = () => {
emit('confirm', selectedLocation.value);
closeWorshipDialog();
};
return {
isOpen,
locations,
selectedLocation,
openWorshipDialog,
closeWorshipDialog,
confirmWorshipConfiguration,
};
},
};
</script>
<style scoped>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@@ -4,7 +4,7 @@
<div> <div>
<label for="page-select">Wähle eine Seite:</label> <label for="page-select">Wähle eine Seite:</label>
<select id="page-select" v-model="selectedPage" @change="loadPageContent"> <select id="page-select" v-model="selectedPage" @change="loadPageContent">
<option v-for="page in pages" :key="page.link" :value="page.link">{{ page.name }}</option> <option v-for="page in sortedPages" :key="page.link" :value="page.link">{{ page.name }}</option>
</select> </select>
</div> </div>
<div class="toolbar"> <div class="toolbar">
@@ -15,8 +15,7 @@
<button @click="editor.chain().focus().toggleItalic().run()">Kursiv</button> <button @click="editor.chain().focus().toggleItalic().run()">Kursiv</button>
<button @click="editor.chain().focus().toggleUnderline().run()">Unterstrichen</button> <button @click="editor.chain().focus().toggleUnderline().run()">Unterstrichen</button>
<button @click="editor.chain().focus().toggleStrike().run()">Durchgestrichen</button> <button @click="editor.chain().focus().toggleStrike().run()">Durchgestrichen</button>
<button <button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">Tabelle</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().toggleBulletList().run()">Liste</button>
<button @click="editor.chain().focus().toggleOrderedList().run()">Nummerierte Liste</button> <button @click="editor.chain().focus().toggleOrderedList().run()">Nummerierte Liste</button>
</div> </div>
@@ -35,12 +34,14 @@
<button>Events</button> <button>Events</button>
<button>Kontaktpersonen</button> <button>Kontaktpersonen</button>
<button>Institutionen</button> <button>Institutionen</button>
<button>Gottesdienste</button> <button @click="openWorshipDialog">Gottesdienste</button>
</div> </div>
<div :class="['htmleditor']"> <div :class="['htmleditor']">
<EditorContent :editor="editor" /> <EditorContent :editor="editor" />
</div> </div>
<button @click="savePageContent">Speichern</button> <button @click="savePageContent">Speichern</button>
<WorshipDialog ref="worshipDialog" @confirm="insertWorshipList" />
</div> </div>
</template> </template>
@@ -59,18 +60,21 @@ import Strike from '@tiptap/extension-strike';
import BulletList from '@tiptap/extension-bullet-list'; import BulletList from '@tiptap/extension-bullet-list';
import OrderedList from '@tiptap/extension-ordered-list'; import OrderedList from '@tiptap/extension-ordered-list';
import Heading from '@tiptap/extension-heading'; import Heading from '@tiptap/extension-heading';
import { CustomTableCell, CustomTableHeader } from '../../extensions/CustomTableCell'; // Importiere die angepasste Erweiterung import { CustomTableCell, CustomTableHeader } from '../../extensions/CustomTableCell';
import WorshipDialog from '@/components/WorshipDialog.vue';
export default { export default {
name: 'EditPagesComponent', name: 'EditPagesComponent',
components: { components: {
EditorContent, EditorContent,
WorshipDialog,
}, },
setup() { setup() {
const store = useStore(); const store = useStore();
const pages = ref([]); const pages = ref([]);
const selectedPage = ref(''); const selectedPage = ref('');
const pageHtmlContent = computed(() => store.state.pageContent); const pageHtmlContent = computed(() => store.state.pageContent);
const worshipDialog = ref(null);
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
@@ -97,10 +101,23 @@ export default {
}, },
}); });
const flattenPages = (pages, allPages, parentName = '') => {
pages.forEach(page => {
const pageName = parentName ? `${parentName} -> ${page.name}` : page.name;
allPages.push({ ...page, name: pageName });
if (page.submenu && page.submenu.length) {
flattenPages(page.submenu, allPages, pageName);
}
});
};
const fetchPages = async () => { const fetchPages = async () => {
try { try {
const response = await axios.get('/menu-data'); const response = await axios.get('/menu-data');
pages.value = response.data; const data = response.data;
const allPages = [];
flattenPages(data, allPages);
pages.value = allPages.sort((a, b) => a.name.localeCompare(b.name));
} catch (error) { } catch (error) {
console.error('Fehler beim Abrufen der Seiten:', error); console.error('Fehler beim Abrufen der Seiten:', error);
} }
@@ -115,7 +132,7 @@ export default {
if (editor.value && editor.value.commands) { if (editor.value && editor.value.commands) {
editor.value.commands.setContent(content, false); editor.value.commands.setContent(content, false);
} else { } else {
setTimeout(setEditorContent, 100); // Try again after 100ms if not ready setTimeout(setEditorContent, 100);
} }
}; };
@@ -146,13 +163,31 @@ export default {
onMounted(fetchPages); onMounted(fetchPages);
const sortedPages = computed(() => {
return pages.value;
});
const openWorshipDialog = () => {
worshipDialog.value.openWorshipDialog();
};
const insertWorshipList = (configuration) => {
if (editor.value) {
editor.value.chain().focus().insertContent(`{{ worshipslist:location=${configuration},order:"date asc" }}`).run();
}
};
return { return {
pages, pages,
sortedPages,
selectedPage, selectedPage,
editor, editor,
loadPageContent, loadPageContent,
savePageContent, savePageContent,
pageHtmlContent, pageHtmlContent,
openWorshipDialog,
insertWorshipList,
worshipDialog,
}; };
}, },
}; };

View File

@@ -0,0 +1,272 @@
<template>
<div class="menu-management">
<h1>Menüverwaltung</h1>
<div class="button-container">
<button @click="addMenuItem">Hauptmenü hinzufügen</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>
</template>
<script>
import { ref, onMounted, computed } from 'vue';
import axios from '../../axios';
export default {
name: 'MenuManagement',
setup() {
const menuData = ref([]);
const selectedMenuItem = ref(null);
const fetchMenuData = async () => {
try {
const response = await axios.get('/menu-data');
menuData.value = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Menü-Daten:', error);
}
};
const saveMenuData = async () => {
try {
const flatMenuData = flattenMenuData(menuData.value);
await axios.post('/menu-data', flatMenuData);
alert('Menü-Daten erfolgreich gespeichert');
} catch (error) {
console.error('Fehler beim Speichern der Menü-Daten:', error);
}
};
const flattenMenuData = (data, parentId = null) => {
return data.reduce((acc, item) => {
const newItem = { ...item, parent_id: parentId };
const { submenu, ...rest } = newItem;
acc.push(rest);
if (submenu && submenu.length) {
acc.push(...flattenMenuData(submenu, newItem.id));
}
return acc;
}, []);
};
const addMenuItem = () => {
const newItem = {
name: '',
link: '',
component: '',
showInMenu: true,
requiresAuth: false,
orderId: 0,
submenu: [],
parent_id: null,
};
menuData.value.push(newItem);
selectMenuItem(newItem);
};
const addSubmenu = (menuItem) => {
const newSubItem = {
name: '',
link: '',
component: '',
showInMenu: true,
requiresAuth: false,
orderId: 0,
parent_id: menuItem.id,
};
menuItem.submenu.push(newSubItem);
selectMenuItem(newSubItem);
};
const removeMenuItem = (menuItem) => {
const index = menuData.value.indexOf(menuItem);
if (index > -1) {
menuData.value.splice(index, 1);
}
selectedMenuItem.value = null;
};
const removeSubmenu = (menuItem, submenuItem) => {
const index = menuItem.submenu.indexOf(submenuItem);
if (index > -1) {
menuItem.submenu.splice(index, 1);
}
selectedMenuItem.value = null;
};
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>
.menu-management {
width: 100%;
margin: auto;
}
.button-container {
display: inline-flex;
gap: 10px;
margin-bottom: 20px;
}
.tree-view {
margin-top: 20px;
}
.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,22 +1,24 @@
<template> <template>
<div> <div>
<div class="previewinfo">Dies ist eine Vorschau.</div> <div class="previewinfo">Dies ist eine Vorschau.</div>
<div v-html="content"></div> <div v-html="renderedContent"></div>
</div> </div>
</template> </template>
<script> <script>
import { computed } from 'vue'; import { computed } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import { render } from '@/utils/render';
export default { export default {
name: 'PagePreview', name: 'PagePreview',
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));
return { return {
content, renderedContent,
}; };
}, },
}; };

View File

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

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>

View File

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

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>

View File

@@ -48,9 +48,8 @@ export default createStore({
actions: { actions: {
async loadMenuData({ commit }) { async loadMenuData({ commit }) {
try { try {
const response = await fetch('/menu-data'); const response = await axios.get('/menu-data');
const menuData = await response.json(); commit('setMenuData', response.data);
commit('setMenuData', menuData);
} catch (error) { } catch (error) {
console.error('Fehler beim Laden der Menü-Daten:', error); console.error('Fehler beim Laden der Menü-Daten:', error);
} }

15
src/utils/render.js Normal file
View File

@@ -0,0 +1,15 @@
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>`;
}

View File

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