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

@@ -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>