inital commit

This commit is contained in:
Torsten Schulz
2024-06-15 23:01:46 +02:00
parent 1b7fefe381
commit 61653ff407
105 changed files with 7805 additions and 524 deletions

23
config/config.json Normal file
View File

@@ -0,0 +1,23 @@
{
"development": {
"username": "miriam_user",
"password": "qTCTTWwpEwy3vPDU",
"database": "miriamgemeinde",
"host": "tsschulz.de",
"dialect": "mysql"
},
"test": {
"username": "miriam_user",
"password": "qTCTTWwpEwy3vPDU",
"database": "miriamgemeinde",
"host": "tsschulz.de",
"dialect": "mysql"
},
"production": {
"username": "miriam_user",
"password": "qTCTTWwpEwy3vPDU",
"database": "miriamgemeinde",
"host": "tsschulz.de",
"dialect": "mysql"
}
}

8
config/database.js Normal file
View File

@@ -0,0 +1,8 @@
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vPDU', {
host: 'tsschulz.de',
dialect: 'mysql'
});
module.exports = sequelize;

79
config/menuData.js Normal file
View File

@@ -0,0 +1,79 @@
export const menuData = [
{ name: 'Aktuelles', link: '/', component: 'HomeContent', showInMenu: true, image: 'homepage1.png' },
{
name: 'Gottesdienste',
link: '/worship-services',
component: 'WorshipServicesContent',
showInMenu: true,
submenu: [
{ name: 'Bonames', link: '/worship-services/bonames', component: 'worship/BonamesWorshipContent', showInMenu: true, image: 'homepage3.png' },
{ name: 'Kalbach', link: '/worship-services/kalbach', component: 'worship/KalbachWorshipContent', showInMenu: true },
{ name: 'Am Bügel', link: '/worship-services/am-buegel', component: 'worship/AmBuegelWorshipContent', showInMenu: true }
]
},
{
name: 'Kindertagesstätten',
link: '/daycare-centers',
component: 'DaycareCentersContent',
showInMenu: true,
submenu: [
{ name: 'Kita Sternenzelt', link: '/daycare-centers/sternenzelt', component: 'SternenzeltContent', showInMenu: true },
{ name: 'Kita Kramambuli', link: '/daycare-centers/kramambuli', component: 'KramambuliContent', showInMenu: true }
]
},
{ name: 'Miriams Wunderkiste', link: '/miriams-wonderbox', component: 'MiriamsWonderboxContent', showInMenu: true },
{
name: 'Treffpunkt',
link: '/meeting-point',
component: 'MeetingPointContent',
showInMenu: true,
submenu: [
{ name: 'Kinder', link: '/meeting-point/children', component: 'ChildrenContent', showInMenu: true, image: 'homepage2.png' },
{ name: 'KonfirmandInnen', link: '/meeting-point/confirmands', component: 'ConfirmandsContent', showInMenu: true },
{ name: 'Jugend', link: '/meeting-point/youth', component: 'YouthContent', showInMenu: true },
{ name: 'SeniorInnen', link: '/meeting-point/seniors', component: 'SeniorsContent', showInMenu: true },
{ name: 'Miriamtreff', link: '/meeting-point/miriam-meeting', component: 'MiriamMeetingContent', showInMenu: true },
{ name: 'Nachbarschaftsraum', link: '/meeting-point/neighborhood-room', component: 'NeighborhoodRoomContent', showInMenu: true },
{ name: 'Angebote für Männer und Frauen', link: '/meeting-point/offers', component: 'OffersContent', showInMenu: true }
]
},
{
name: 'Musik',
link: '/music',
component: 'MusicContent',
showInMenu: true,
submenu: [
{ name: 'Miriamkantorei', link: '/music/miriamkantorei', component: 'MiriamChoirContent', showInMenu: true },
{ name: 'Flötenkinder', link: '/music/flute-children', component: 'FluteChildrenContent', showInMenu: true },
{ name: 'Platzhalter', link: '/music/placeholder', component: 'PlaceholderContent', showInMenu: true }
]
},
{
name: 'Unsere Kirchen',
link: '/our-churches',
component: 'OurChurchesContent',
showInMenu: true,
submenu: [
{ name: 'Bonames', link: '/our-churches/bonames', component: 'BonamesChurchContent', showInMenu: true },
{ name: 'Kalbach', link: '/our-churches/kalbach', component: 'KalbachChurchContent', showInMenu: true },
{ name: 'Am Bügel', link: '/our-churches/am-buegel', component: 'AmBuegelChurchContent', showInMenu: true }
]
},
{ name: 'Ansprechpartner und Adressen', link: '/contacts', component: 'ContactsContent', showInMenu: true },
{
name: 'Admin',
link: '/admin',
requiresAuth: true,
showInMenu: true,
submenu: [
{ name: 'Seiten bearbeiten', link: '/admin/edit-pages', component: 'admin/EditPages', requiresAuth: true, showInMenu: true },
{ name: 'Events eingeben', link: '/admin/enter-events', component: 'admin/EnterEvents', requiresAuth: true, showInMenu: true },
{ name: 'Gottesdienste', link: '/admin/worship-services', component: 'admin/AdminWorshipServices', requiresAuth: true, showInMenu: true }
]
},
{ name: 'Login', link: '/login', component: 'authentication/LoginComponent', showInMenu: false },
{ name: 'Passwort vergessen', link: '/forgot-password', component: 'authentication/ForgotPassword', showInMenu: false },
{ name: 'Registrieren', link: '/register', component: 'authentication/RegisterComponent', showInMenu: false },
{ name: 'Terms and Conditions', link: '/terms', component: 'disclaimers/TermsComponent', showInMenu: false },
{ name: 'Privacy Policy', link: '/privacy-policy', component: 'disclaimers/PrivacyPolicyComponent', showInMenu: false }
];

View File

@@ -0,0 +1,55 @@
const bcrypt = require('bcryptjs');
const { User } = require('../models');
const jwt = require('jsonwebtoken');
exports.register = async (req, res) => {
const { name, email, password } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ message: 'Alle Felder sind erforderlich' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({ name, email, password: hashedPassword, active: true });
res.status(201).json({ message: 'Benutzer erfolgreich registriert', user });
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(400).json({ message: 'Email-Adresse bereits in Verwendung' });
}
res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
}
};
exports.login = async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: 'Email und Passwort sind erforderlich' });
}
try {
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({ message: 'Ungültige Anmeldedaten' });
}
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ message: 'Ungültige Anmeldedaten' });
}
if (!user.active) {
return res.status(403).json({ message: 'Benutzerkonto ist nicht aktiv' });
}
const token = jwt.sign({ id: user.id, name: user.name, email: user.email }, 'zTxVgptmPl9!_dr%xxx9999(dd)', { expiresIn: '1h' });
res.status(200).json({ message: 'Login erfolgreich', token, 'user': user });
} catch (error) {
console.log(error);
res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
}
};

View File

@@ -0,0 +1,78 @@
const { ContactPerson, Position } = require('../models');
const getAllContactPersons = async (req, res) => {
try {
const contactPersons = await ContactPerson.findAll({
include: [
{
model: Position,
as: 'positions',
through: { attributes: [] }
}
]
});
res.json(contactPersons);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch contact persons' });
console.error(error);
}
};
const createContactPerson = async (req, res) => {
try {
const { positions, ...contactPersonData } = req.body;
const contactPerson = await ContactPerson.create(contactPersonData);
if (positions && positions.length > 0) {
const positionIds = positions.map(position => position.id);
await contactPerson.setPositions(positionIds);
}
res.status(201).json(contactPerson);
} catch (error) {
res.status(500).json({ error: 'Failed to create contact person' });
console.error(error);
}
};
const updateContactPerson = async (req, res) => {
try {
const { id } = req.params;
const { positions, ...contactPersonData } = req.body;
const contactPerson = await ContactPerson.findByPk(id);
if (!contactPerson) {
return res.status(404).json({ error: 'Contact person not found' });
}
await contactPerson.update(contactPersonData);
if (positions && positions.length > 0) {
const positionIds = positions.map(position => position.id);
await contactPerson.setPositions(positionIds);
}
res.status(200).json(contactPerson);
} catch (error) {
res.status(500).json({ error: 'Failed to update contact person' });
console.error(error);
}
};
const deleteContactPerson = async (req, res) => {
try {
const { id } = req.params;
const deleted = await ContactPerson.destroy({
where: { id: id }
});
if (deleted) {
res.status(204).json();
} else {
res.status(404).json({ error: 'Contact person not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to delete contact person' });
console.error(error);
}
};
module.exports = {
getAllContactPersons,
createContactPerson,
updateContactPerson,
deleteContactPerson
};

View File

@@ -0,0 +1,74 @@
const { Event, Institution, EventPlace, ContactPerson } = require('../models');
const getAllEvents = async (req, res) => {
try {
const events = await Event.findAll({
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
]
});
res.json(events);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch events' });
console.error(error);
}
};
const createEvent = async (req, res) => {
try {
const { contactPersonIds, ...eventData } = req.body;
const event = await Event.create(eventData);
if (contactPersonIds) {
await event.setContactPersons(contactPersonIds);
}
res.status(201).json(event);
} catch (error) {
res.status(500).json({ error: 'Failed to create event' });
console.error(error);
}
};
const updateEvent = async (req, res) => {
try {
const { id } = req.params;
const { contactPersonIds, ...eventData } = req.body;
const event = await Event.findByPk(id);
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
await event.update(eventData);
if (contactPersonIds) {
await event.setContactPersons(contactPersonIds);
}
res.status(200).json(event);
} catch (error) {
res.status(500).json({ error: 'Failed to update event' });
console.error(error);
}
};
const deleteEvent = async (req, res) => {
try {
const { id } = req.params;
const deleted = await Event.destroy({
where: { id: id }
});
if (deleted) {
res.status(204).json();
} else {
res.status(404).json({ error: 'Event not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to delete event' });
console.error(error);
}
};
module.exports = {
getAllEvents,
createEvent,
updateEvent,
deleteEvent
};

View File

@@ -0,0 +1,61 @@
const { EventPlace } = require('../models');
const getAllEventPlaces = async (req, res) => {
try {
const eventPlaces = await EventPlace.findAll();
res.json(eventPlaces);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch event places' });
}
};
const createEventPlace = async (req, res) => {
try {
const eventPlace = await EventPlace.create(req.body);
res.status(201).json(eventPlace);
} catch (error) {
res.status(500).json({ error: 'Failed to create event place' });
console.log(req.body);
console.log(error);
}
};
const updateEventPlace = async (req, res) => {
try {
const { id } = req.params;
const [updated] = await EventPlace.update(req.body, {
where: { id: id }
});
if (updated) {
const updatedEventPlace = await EventPlace.findOne({ where: { id: id } });
res.status(200).json(updatedEventPlace);
} else {
res.status(404).json({ error: 'Event place not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to update event place' });
}
};
const deleteEventPlace = async (req, res) => {
try {
const { id } = req.params;
const deleted = await EventPlace.destroy({
where: { id: id }
});
if (deleted) {
res.status(204).json();
} else {
res.status(404).json({ error: 'Event place not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to delete event place' });
}
};
module.exports = {
getAllEventPlaces,
createEventPlace,
updateEventPlace,
deleteEventPlace
};

View File

@@ -0,0 +1,59 @@
const { EventType } = require('../models');
const getAllEventTypes = async (req, res) => {
try {
const eventTypes = await EventType.findAll();
res.json(eventTypes);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch event types' });
}
};
const createEventType = async (req, res) => {
try {
const eventType = await EventType.create(req.body);
res.status(201).json(eventType);
} catch (error) {
res.status(500).json({ error: 'Failed to create event type' });
}
};
const updateEventType = async (req, res) => {
try {
const { id } = req.params;
const [updated] = await EventType.update(req.body, {
where: { id: id }
});
if (updated) {
const updatedEventType = await EventType.findOne({ where: { id: id } });
res.status(200).json(updatedEventType);
} else {
res.status(404).json({ error: 'Event type not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to update event type' });
}
};
const deleteEventType = async (req, res) => {
try {
const { id } = req.params;
const deleted = await EventType.destroy({
where: { id: id }
});
if (deleted) {
res.status(204).json();
} else {
res.status(404).json({ error: 'Event type not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to delete event type' });
}
};
module.exports = {
getAllEventTypes,
createEventType,
updateEventType,
deleteEventType
};

View File

@@ -0,0 +1,76 @@
const { Institution, ContactPerson } = require('../models');
const getAllInstitutions = async (req, res) => {
try {
const institutions = await Institution.findAll({
include: [
{
model: ContactPerson,
as: 'contactPersons',
through: { attributes: [] }
}
]
});
res.json(institutions);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch institutions' });
console.error(error);
}
};
const createInstitution = async (req, res) => {
try {
const { contactPersonIds, ...institutionData } = req.body;
const institution = await Institution.create(institutionData);
if (contactPersonIds) {
await institution.setContactPersons(contactPersonIds);
}
res.status(201).json(institution);
} catch (error) {
res.status(500).json({ error: 'Failed to create institution' });
console.error(error);
}
};
const updateInstitution = async (req, res) => {
try {
const { id } = req.params;
const { contactPersonIds, ...institutionData } = req.body;
const institution = await Institution.findByPk(id);
if (!institution) {
return res.status(404).json({ error: 'Institution not found' });
}
await institution.update(institutionData);
if (contactPersonIds) {
await institution.setContactPersons(contactPersonIds);
}
res.status(200).json(institution);
} catch (error) {
res.status(500).json({ error: 'Failed to update institution' });
console.error(error);
}
};
const deleteInstitution = async (req, res) => {
try {
const { id } = req.params;
const deleted = await Institution.destroy({
where: { id: id }
});
if (deleted) {
res.status(204).json();
} else {
res.status(404).json({ error: 'Institution not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to delete institution' });
console.error(error);
}
};
module.exports = {
getAllInstitutions,
createInstitution,
updateInstitution,
deleteInstitution
};

View File

@@ -0,0 +1,23 @@
// controllers/menuDataController.js
const fetchMenuData = require('../utils/fetchMenuData');
exports.getMenuData = async (req, res) => {
try {
const menuData = await fetchMenuData();
res.json(menuData);
} catch (error) {
res.status(500).send('Error fetching menu data');
}
};
exports.saveMenuData = async (req, res) => {
try {
const menuData = req.body;
await MenuItem.destroy({ where: {} });
await MenuItem.bulkCreate(menuData);
res.status(200).send('Menü-Daten erfolgreich gespeichert');
} catch (error) {
res.status(500).send('Fehler beim Speichern der Menü-Daten');
}
};

View File

@@ -0,0 +1,48 @@
// controllers/pageController.js
const { Page } = require('../models');
exports.getMenuData = async (req, res) => {
try {
const pages = await Page.findAll({
attributes: ['link', 'name']
});
res.json(pages);
} catch (error) {
console.error('Fehler beim Abrufen der Seiten:', error);
res.status(500).json({ message: 'Fehler beim Abrufen der Seiten' });
}
};
exports.getPageContent = async (req, res) => {
try {
const page = await Page.findOne({
where: { link: req.query.link }
});
if (page) {
res.json({ content: page.content });
} else {
res.json({ content: "" });
}
} catch (error) {
console.error('Fehler beim Laden des Seiteninhalts:', error);
res.status(500).json({ message: 'Fehler beim Laden des Seiteninhalts' });
}
};
exports.savePageContent = async (req, res) => {
try {
const { link, name, content } = req.body;
let page = await Page.findOne({ where: { link } });
if (page) {
page.content = content;
page.name = name;
} else {
page = await Page.create({ link, name, content });
}
await page.save();
res.json({ message: 'Seiteninhalt gespeichert', page });
} catch (error) {
console.error('Fehler beim Speichern des Seiteninhalts:', error);
res.status(500).json({ message: 'Fehler beim Speichern des Seiteninhalts' });
}
};

View File

@@ -0,0 +1,59 @@
const { Position } = require('../models');
const getAllPositions = async (req, res) => {
try {
const positions = await Position.findAll();
res.json(positions);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch positions' });
}
};
const createPosition = async (req, res) => {
try {
const position = await Position.create(req.body);
res.status(201).json(position);
} catch (error) {
res.status(500).json({ error: 'Failed to create position' });
}
};
const updatePosition = async (req, res) => {
try {
const { id } = req.params;
const [updated] = await Position.update(req.body, {
where: { id: id }
});
if (updated) {
const updatedPosition = await Position.findOne({ where: { id: id } });
res.status(200).json(updatedPosition);
} else {
res.status(404).json({ error: 'Position not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to update position' });
}
};
const deletePosition = async (req, res) => {
try {
const { id } = req.params;
const deleted = await Position.destroy({
where: { id: id }
});
if (deleted) {
res.status(204).json();
} else {
res.status(404).json({ error: 'Position not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to delete position' });
}
};
module.exports = {
getAllPositions,
createPosition,
updatePosition,
deletePosition
};

View File

@@ -0,0 +1,47 @@
const { Worship } = require('../models');
exports.getAllWorships = async (req, res) => {
try {
const worships = await Worship.findAll();
res.status(200).json(worships);
} catch (error) {
res.status(500).json({ message: 'Fehler beim Abrufen der Gottesdienste' });
}
};
exports.createWorship = async (req, res) => {
try {
const worship = await Worship.create(req.body);
res.status(201).json(worship);
} catch (error) {
res.status(500).json({ message: 'Fehler beim Erstellen des Gottesdienstes' });
}
};
exports.updateWorship = async (req, res) => {
try {
const worship = await Worship.findByPk(req.params.id);
if (worship) {
await worship.update(req.body);
res.status(200).json(worship);
} else {
res.status(404).json({ message: 'Gottesdienst nicht gefunden' });
}
} catch (error) {
res.status(500).json({ message: 'Fehler beim Aktualisieren des Gottesdienstes' });
}
};
exports.deleteWorship = async (req, res) => {
try {
const worship = await Worship.findByPk(req.params.id);
if (worship) {
await worship.destroy();
res.status(200).json({ message: 'Gottesdienst erfolgreich gelöscht' });
} else {
res.status(404).json({ message: 'Gottesdienst nicht gefunden' });
}
} catch (error) {
res.status(500).json({ message: 'Fehler beim Löschen des Gottesdienstes' });
}
};

View File

@@ -0,0 +1,19 @@
const jwt = require('jsonwebtoken');
const authMiddleware = (req, res, next) => {
const authHeader = req.header('Authorization');
if (!authHeader) {
return res.status(401).json({ message: 'Zugriff verweigert. Kein Token vorhanden.' });
}
const token = authHeader.replace('Bearer ', '');
try {
const decoded = jwt.verify(token, 'zTxVgptmPl9!_dr%xxx9999(dd)');
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ message: 'Ungültiges Token.' });
}
};
module.exports = authMiddleware;

View File

@@ -0,0 +1,43 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('events', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
uniqueId: {
type: Sequelize.STRING
},
eventType: {
type: Sequelize.STRING
},
name: {
type: Sequelize.STRING
},
date: {
type: Sequelize.DATE
},
time: {
type: Sequelize.TIME
},
dayOfWeek: {
type: Sequelize.INTEGER
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Events');
}
};

View File

@@ -0,0 +1,40 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('event_places', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
street: {
type: Sequelize.STRING
},
zipcode: {
type: Sequelize.STRING
},
city: {
type: Sequelize.STRING
},
state: {
type: Sequelize.STRING
},
country: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('event_places');
}
};

View File

@@ -0,0 +1,15 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('menu_items', 'order_id', {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0 // Standardwert, falls benötigt
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('menu_items', 'order_id');
}
};

View File

@@ -0,0 +1,68 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('institutions', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING,
allowNull: false
},
street: {
type: Sequelize.STRING,
allowNull: true
},
zipcode: {
type: Sequelize.STRING,
allowNull: true
},
city: {
type: Sequelize.STRING,
allowNull: true
},
phone: {
type: Sequelize.STRING,
allowNull: true
},
fax: {
type: Sequelize.STRING,
allowNull: true
},
email: {
type: Sequelize.STRING,
allowNull: true
}
});
await queryInterface.createTable('InstitutionContactPerson', {
institution_id: {
type: Sequelize.INTEGER,
references: {
model: 'institutions',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
contact_person_id: {
type: Sequelize.INTEGER,
references: {
model: 'contact_persons',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('InstitutionContactPerson');
await queryInterface.dropTable('institutions');
}
};

View File

@@ -0,0 +1,51 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Events', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
title: {
type: Sequelize.STRING,
allowNull: false
},
description: {
type: Sequelize.TEXT
},
date: {
type: Sequelize.DATE,
allowNull: false
},
repeat: {
type: Sequelize.STRING
},
interval: {
type: Sequelize.INTEGER
},
institution_id: {
type: Sequelize.INTEGER,
references: {
model: 'institutions',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
},
event_place_id: {
type: Sequelize.INTEGER,
references: {
model: 'event_places',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('Events');
}
};

View File

@@ -0,0 +1,28 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('EventContactPersons', {
event_id: {
type: Sequelize.INTEGER,
references: {
model: 'Events',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
contact_person_id: {
type: Sequelize.INTEGER,
references: {
model: 'contact_persons',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('EventContactPersons');
}
};

View File

@@ -0,0 +1,20 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('events', 'institution_id', {
type: Sequelize.INTEGER,
references: {
model: 'institutions',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
allowNull: true
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('events', 'institution_id');
}
};

View File

@@ -0,0 +1,20 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('events', 'event_place_id', {
type: Sequelize.INTEGER,
references: {
model: 'event_places',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
allowNull: true
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('events', 'event_place_id');
}
};

View File

@@ -0,0 +1,14 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('events', 'endTime', {
type: Sequelize.TIME,
allowNull: true,
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('events', 'endTime');
}
};

View File

@@ -0,0 +1,15 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('worships', 'date', {
type: Sequelize.DATE,
allowNull: false,
defaultValue: '2024-01-01'
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('worships', 'date');
}
};

View File

@@ -0,0 +1,41 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('pages', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
link: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
name: {
type: Sequelize.STRING,
allowNull: false
},
content: {
type: Sequelize.TEXT,
allowNull: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now')
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now')
}
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('pages');
}
};

56
models/ContactPerson.js Normal file
View File

@@ -0,0 +1,56 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const ContactPerson = sequelize.define('ContactPerson', {
name: {
type: DataTypes.STRING,
allowNull: false
},
phone: {
type: DataTypes.STRING,
allowNull: true
},
street: {
type: DataTypes.STRING,
allowNull: true
},
zipcode: {
type: DataTypes.STRING,
allowNull: true
},
city: {
type: DataTypes.STRING,
allowNull: true
},
email: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'contact_persons',
timestamps: false
});
ContactPerson.associate = function(models) {
ContactPerson.belongsToMany(models.Event, {
through: models.EventContactPerson,
foreignKey: 'contact_person_id',
otherKey: 'event_id',
as: 'events'
});
ContactPerson.belongsToMany(models.Position, {
through: models.ContactPersonPosition,
foreignKey: 'contact_person_id',
otherKey: 'position_id',
as: 'positions'
});
ContactPerson.belongsToMany(models.Institution, {
through: models.InstitutionContactPerson,
foreignKey: 'contact_person_id',
otherKey: 'institution_id',
as: 'institutions'
});
};
return ContactPerson;
};

View File

@@ -0,0 +1,29 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const ContactPersonPosition = sequelize.define('ContactPersonPosition', {
contact_person_id: {
type: DataTypes.INTEGER,
references: {
model: 'ContactPerson',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
position_id: {
type: DataTypes.INTEGER,
references: {
model: 'Position',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
}
}, {
tableName: 'contact_person_positions',
timestamps: false
});
return ContactPersonPosition;
};

37
models/EventPlace.js Normal file
View File

@@ -0,0 +1,37 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const EventPlace = sequelize.define('EventPlace', {
name: {
type: DataTypes.STRING,
allowNull: false
},
street: {
type: DataTypes.STRING,
allowNull: false
},
zipcode: {
type: DataTypes.STRING,
allowNull: false
},
city: {
type: DataTypes.STRING,
allowNull: false
},
backgroundColor: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'event_places'
});
EventPlace.associate = function(models) {
EventPlace.hasMany(models.Event, {
foreignKey: 'event_place_id',
as: 'events'
});
};
return EventPlace;
};

22
models/EventType.js Normal file
View File

@@ -0,0 +1,22 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const EventType = sequelize.define('EventType', {
caption: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'event_types',
timestamps: false
});
EventType.associate = function(models) {
EventType.hasMany(models.Event, {
foreignKey: 'eventTypeId',
as: 'events'
});
};
return EventType;
};

52
models/Institution.js Normal file
View File

@@ -0,0 +1,52 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Institution = sequelize.define('Institution', {
name: {
type: DataTypes.STRING,
allowNull: false
},
street: {
type: DataTypes.STRING,
allowNull: true
},
zipcode: {
type: DataTypes.STRING,
allowNull: true
},
city: {
type: DataTypes.STRING,
allowNull: true
},
phone: {
type: DataTypes.STRING,
allowNull: true
},
fax: {
type: DataTypes.STRING,
allowNull: true
},
email: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'institutions',
timestamps: false
});
Institution.associate = function(models) {
Institution.belongsToMany(models.ContactPerson, {
through: models.InstitutionContactPerson,
foreignKey: 'institution_id',
otherKey: 'contact_person_id',
as: 'contactPersons'
});
Institution.hasMany(models.Event, {
foreignKey: 'institution_id',
as: 'events'
});
};
return Institution;
};

View File

@@ -0,0 +1,29 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const InstitutionContactPerson = sequelize.define('InstitutionContactPerson', {
institution_id: {
type: DataTypes.INTEGER,
references: {
model: 'Institution',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
contact_person_id: {
type: DataTypes.INTEGER,
references: {
model: 'ContactPerson',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
}
}, {
tableName: 'InstitutionContactPerson',
timestamps: false
});
return InstitutionContactPerson;
};

49
models/MenuItem.js Normal file
View File

@@ -0,0 +1,49 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const MenuItem = sequelize.define('MenuItem', {
name: {
type: DataTypes.STRING,
allowNull: false
},
link: {
type: DataTypes.STRING,
allowNull: false
},
component: {
type: DataTypes.STRING,
allowNull: false
},
show_in_menu: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
},
requires_auth: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
parent_id: {
type: DataTypes.INTEGER,
allowNull: true
},
order_id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
}
}, {
tableName: 'menu_items',
timestamps: false
});
MenuItem.associate = function(models) {
MenuItem.hasMany(models.MenuItem, {
foreignKey: 'parent_id',
as: 'submenu'
});
};
return MenuItem;
};

25
models/Page.js Normal file
View File

@@ -0,0 +1,25 @@
// models/Page.js
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Page = sequelize.define('Page', {
link: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
content: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'pages',
timestamps: true
});
return Page;
};

24
models/Position.js Normal file
View File

@@ -0,0 +1,24 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Position = sequelize.define('Position', {
caption: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'positions',
timestamps: false
});
Position.associate = function(models) {
Position.belongsToMany(models.ContactPerson, {
through: models.ContactPersonPosition,
foreignKey: 'position_id',
otherKey: 'contact_person_id',
as: 'contactPersons'
});
};
return Position;
};

29
models/User.js Normal file
View File

@@ -0,0 +1,29 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const User = sequelize.define('User', {
name: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
password: {
type: DataTypes.STRING,
allowNull: false
},
active: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
}, {
timestamps: true,
createdAt: 'created_at',
updatedAt: false
});
return User;
};

67
models/Worship.js Normal file
View File

@@ -0,0 +1,67 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Worship = sequelize.define('Worship', {
eventPlaceId: {
type: DataTypes.INTEGER,
references: {
model: 'EventPlace',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
},
date: {
type: DataTypes.DATE,
allowNull: false
},
time: {
type: DataTypes.TIME,
allowNull: false
},
title: {
type: DataTypes.STRING,
allowNull: false
},
organizer: {
type: DataTypes.STRING,
allowNull: true
},
collection: {
type: DataTypes.STRING,
allowNull: true
},
address: {
type: DataTypes.STRING,
allowNull: true
},
selfInformation: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
highlightTime: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
neighborInvitation: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
introLine: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'worships',
timestamps: true
});
Worship.associate = function(models) {
Worship.belongsTo(models.EventPlace, {
foreignKey: 'eventPlaceId',
as: 'eventPlace'
});
};
return Worship;
};

64
models/event.js Normal file
View File

@@ -0,0 +1,64 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const Event = sequelize.define('Event', {
uniqueId: {
type: DataTypes.STRING,
allowNull: true
},
eventTypeId: {
type: DataTypes.INTEGER,
allowNull: true
},
eventPlaceId: {
type: DataTypes.INTEGER,
allowNull: true
},
name: {
type: DataTypes.STRING,
allowNull: true
},
date: {
type: DataTypes.DATE,
allowNull: true
},
time: {
type: DataTypes.TIME,
allowNull: true
},
endTime: {
type: DataTypes.TIME,
allowNull: true
},
dayOfWeek: {
type: DataTypes.INTEGER,
allowNull: true
},
description: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'events',
timestamps: true
});
Event.associate = function(models) {
Event.belongsTo(models.Institution, {
foreignKey: 'institution_id',
as: 'institution'
});
Event.belongsTo(models.EventPlace, {
foreignKey: 'event_place_id',
as: 'eventPlace'
});
Event.belongsToMany(models.ContactPerson, {
through: 'EventContactPerson',
foreignKey: 'event_id',
otherKey: 'contact_person_id',
as: 'contactPersons'
});
};
return Event;
};

View File

@@ -0,0 +1,29 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
const EventContactPerson = sequelize.define('EventContactPerson', {
event_id: {
type: DataTypes.INTEGER,
references: {
model: 'Event',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
contact_person_id: {
type: DataTypes.INTEGER,
references: {
model: 'ContactPerson',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
}
}, {
tableName: 'EventContactPersons',
timestamps: false
});
return EventContactPerson;
};

42
models/index.js Normal file
View File

@@ -0,0 +1,42 @@
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf('.') !== 0 &&
file !== basename &&
file.slice(-3) === '.js' &&
file.indexOf('.test.js') === -1
);
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

2849
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,36 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@tiptap/extension-bold": "^2.4.0",
"@tiptap/extension-bullet-list": "^2.4.0",
"@tiptap/extension-heading": "^2.4.0",
"@tiptap/extension-italic": "^2.4.0",
"@tiptap/extension-ordered-list": "^2.4.0",
"@tiptap/extension-strike": "^2.4.0",
"@tiptap/extension-table": "^2.4.0",
"@tiptap/extension-table-cell": "^2.4.0",
"@tiptap/extension-table-header": "^2.4.0",
"@tiptap/extension-table-row": "^2.4.0",
"@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",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"vue": "^3.2.13" "cors": "^2.8.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.10.1",
"nodemon": "^3.1.3",
"sequelize": "^6.37.3",
"sequelize-cli": "^6.6.2",
"vue": "^3.2.13",
"vue-multiselect": "^3.0.0",
"vue-quill-editor": "^3.0.6",
"vue-router": "^4.3.3",
"vuex": "^4.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.16", "@babel/core": "^7.12.16",

BIN
public/images/homepage1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 KiB

BIN
public/images/homepage2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/images/homepage3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

8
routes/auth.js Normal file
View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
router.post('/register', authController.register);
router.post('/login', authController.login);
module.exports = router;

11
routes/contactPerson.js Normal file
View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const { getAllContactPersons, createContactPerson, updateContactPerson, deleteContactPerson } = require('../controllers/contactPersonController');
const authMiddleware = require('../middleware/authMiddleware')
router.get('/', authMiddleware, getAllContactPersons);
router.post('/', authMiddleware, createContactPerson);
router.put('/:id', authMiddleware, updateContactPerson);
router.delete('/:id', authMiddleware, deleteContactPerson);
module.exports = router;

11
routes/event.js Normal file
View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const { getAllEvents, createEvent, updateEvent, deleteEvent } = require('../controllers/eventController.js');
const authMiddleware = require('../middleware/authMiddleware')
router.get('/', authMiddleware, getAllEvents);
router.post('/', authMiddleware, createEvent);
router.put('/:id', authMiddleware, updateEvent);
router.delete('/:id', authMiddleware, deleteEvent);
module.exports = router;

11
routes/eventPlaces.js Normal file
View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const { getAllEventPlaces, createEventPlace, updateEventPlace, deleteEventPlace } = require('../controllers/eventPlaceController.js');
const authMiddleware = require('../middleware/authMiddleware')
router.get('/', authMiddleware, getAllEventPlaces);
router.post('/', authMiddleware, createEventPlace);
router.put('/:id', authMiddleware, updateEventPlace);
router.delete('/:id', authMiddleware, deleteEventPlace);
module.exports = router;

11
routes/eventtypes.js Normal file
View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const { getAllEventTypes, createEventType, updateEventType, deleteEventType } = require('../controllers/eventtypecontroller');
const authMiddleware = require('../middleware/authMiddleware')
router.get('/', authMiddleware, getAllEventTypes);
router.post('/', authMiddleware, createEventType);
router.put('/:id', authMiddleware, updateEventType);
router.delete('/:id', authMiddleware, deleteEventType);
module.exports = router;

11
routes/institutions.js Normal file
View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const institutionController = require('../controllers/institutionController');
const authMiddleware = require('../middleware/authMiddleware')
router.get('/', authMiddleware, institutionController.getAllInstitutions);
router.post('/', authMiddleware, institutionController.createInstitution);
router.put('/:id', authMiddleware, institutionController.updateInstitution);
router.delete('/:id', authMiddleware, institutionController.deleteInstitution);
module.exports = router;

8
routes/menuData.js Normal file
View File

@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
const menuDataController = require('../controllers/menuDataController');
const authMiddleware = require('../middleware/authMiddleware')
router.get('/', menuDataController.getMenuData);
router.post('/', authMiddleware, menuDataController.saveMenuData);
module.exports = router;

9
routes/pages.js Normal file
View File

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

11
routes/positions.js Normal file
View File

@@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
const { getAllPositions, createPosition, updatePosition, deletePosition } = require('../controllers/positionController');
const authMiddleware = require('../middleware/authMiddleware')
router.get('/', authMiddleware, getAllPositions);
router.post('/', authMiddleware, createPosition);
router.put('/:id', authMiddleware, updatePosition);
router.delete('/:id', authMiddleware, deletePosition);
module.exports = router;

11
routes/worships.js Normal file
View File

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

37
server.js Normal file
View File

@@ -0,0 +1,37 @@
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const sequelize = require('./config/database');
const authRoutes = require('./routes/auth');
const eventTypesRouter = require('./routes/eventtypes');
const eventPlacesRouter = require('./routes/eventPlaces');
const contactPersonsRouter = require('./routes/contactPerson');
const positionsRouter = require('./routes/positions');
const institutionRoutes = require('./routes/institutions');
const eventRouter = require('./routes/event');
const menuDataRouter = require('./routes/menuData');
const worshipRouter = require('./routes/worships');
const pageRoutes = require('./routes/pages');
const app = express();
const PORT = 3000;
app.use(cors());
app.use(bodyParser.json());
app.use('/api/auth', authRoutes);
app.use('/api/event-types', eventTypesRouter);
app.use('/api/event-places', eventPlacesRouter);
app.use('/api/contact-persons', contactPersonsRouter);
app.use('/api/positions', positionsRouter);
app.use('/api/institutions', institutionRoutes);
app.use('/api/events', eventRouter);
app.use('/api/menu-data', menuDataRouter);
app.use('/api/worships', worshipRouter);
app.use('/api/page-content', pageRoutes);
sequelize.sync().then(() => {
app.listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);
});
});

View File

@@ -1,26 +0,0 @@
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

88
src/AppComponent.vue Normal file
View File

@@ -0,0 +1,88 @@
<template>
<div id="app">
<HeaderComponent />
<main class="content-section">
<div class="left-column">
<router-view />
</div>
<div class="right-column">
<router-view name="rightColumn" />
</div>
</main>
<FooterComponent />
</div>
</template>
<script>
import HeaderComponent from './common/components/HeaderComponent.vue';
import FooterComponent from './common/components/FooterComponent.vue';
export default {
name: 'AppComponent',
components: {
HeaderComponent,
FooterComponent,
}
};
</script>
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
background-color: #ffffff;
font-family: Arial, sans-serif;
width: 100%;
overflow: hidden;
}
#app {
display: flex;
flex-direction: column;
height: 100%;
}
.content-section {
flex: 1;
display: flex;
color: #000;
overflow: hidden;
}
.left-column, .right-column {
flex: 1;
overflow-y: auto;
}
.left-column {
margin: 0.5em 0 0.5em 0.5em;
background-color: #ffffff;
}
.right-column {
background-color: #d9e2f3;
}
.right-column h2 {
text-align: center;
color: #000;
}
.right-column img {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
}
@media (max-width: 768px) {
.content-section {
flex-direction: column;
}
.left-column, .right-column {
padding: 10px;
}
}
</style>

21
src/assets/css/editor.css Normal file
View File

@@ -0,0 +1,21 @@
.htmleditor {
background-color: #fff;
width: calc(100% - 26px);
height: 31em;
border: 1px solid black;
margin: 7px;
padding: 5px;
}
.htmleditor table {
border: 1px solid #e0e0e0;
border-collapse: collapse;
}
.htmleditor th {
border: 1px solid #e0e0e0;
}
.htmleditor td {
border: 1px solid #e0e0e0;
}

33
src/axios.js Normal file
View File

@@ -0,0 +1,33 @@
import axios from 'axios';
import store from './store';
import router from './router';
axios.defaults.baseURL = 'http://localhost:3000/api';
axios.interceptors.request.use(
config => {
const token = store.state.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
response => {
return response;
},
error => {
if (error.response && error.response.status === 401) {
store.commit('logout');
router.push('/');
}
return Promise.reject(error);
}
);
export default axios;

View File

@@ -0,0 +1,63 @@
<template>
<div v-if="modelValue" class="dialog-overlay">
<div class="dialog">
<h2>{{ title }}</h2>
<p>{{ message }}</p>
<button @click="closeDialog">OK</button>
</div>
</div>
</template>
<script>
export default {
name: 'DialogComponent',
props: {
title: {
type: String,
required: true
},
message: {
type: String,
required: true
},
modelValue: {
type: Boolean,
default: false
}
},
methods: {
closeDialog() {
this.$emit('update:modelValue', false);
this.$emit('close');
}
}
};
</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 {
background: white;
padding: 20px;
border-radius: 5px;
max-width: 400px;
width: 100%;
text-align: center;
}
button {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<footer class="footer">
<div class="left-links">
<router-link class="login-link" to="/login" v-if="!isLoggedIn">Login</router-link>
<a v-if="isLoggedIn" @click="logout" class="logout-link">Logout</a>
</div>
<div class="right-links">
<router-link to="/terms">Impressum</router-link>
<router-link to="/privacy-policy">Datenschutzerklärung</router-link>
</div>
</footer>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
name: 'FooterComponent',
computed: {
...mapGetters(['isLoggedIn'])
},
methods: {
...mapActions(['logout']),
navigateToLogin() {
this.$router.push('/login');
}
}
};
</script>
<style scoped>
.footer {
background-color: #0b1735;
bottom: 0;
left: 0;
width: 100%;
padding: 7px;
display: flex;
justify-content: space-between;
align-items: center;
}
.left-links {
display: flex;
align-items: center;
}
.right-links {
display: flex;
align-items: center;
}
.footer a {
color: #fff;
padding-right: 20px;
text-decoration: none;
}
.footer a.login-link {
color: #444;
}
.footer a.logout-link {
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<header>
<div class="header-title">
<h1>Evangelische Miriamgemeinde Frankfurt am Main</h1>
<span class="reload-icon" @click="reloadMenu">&#x27F3;</span>
</div>
<NavbarComponent />
</header>
</template>
<script>
import NavbarComponent from './NavbarComponent.vue';
import { mapActions } from 'vuex';
export default {
name: 'HeaderComponent',
components: {
NavbarComponent
},
methods: {
...mapActions(['loadMenuData']),
reloadMenu() {
this.loadMenuData();
}
}
};
</script>
<style scoped>
header {
display: flex;
flex-direction: column;
width: 100%;
background-color: #e0bfff;
}
.header-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.3em 0.5em;
}
header h1 {
margin: 0;
}
.reload-icon {
font-size: 16px;
cursor: pointer;
margin-left: 10px;
background-color: #e0bfff;
color: white;
padding: 5px;
border-radius: 50%;
}
.reload-icon:hover {
color: #7a00d1;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<nav class="navbar">
<ul>
<li v-for="item in menu" :key="item.name">
<router-link :to="item.link" v-if="item.link">
{{ item.name }}
</router-link>
<span v-if="!item.link">
{{ item.name }}
</span>
<transition name="fade">
<div v-if="item.submenu && item.submenu.length" class="dropdown-content">
<router-link v-for="subitem in item.submenu" :key="subitem.name" :to="subitem.link">
{{ subitem.name }}
</router-link>
</div>
</transition>
</li>
</ul>
</nav>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'NavbarComponent',
computed: {
...mapState(['menuData']),
menu() {
return this.menuData.filter(item => {
if (!item.showInMenu) {
return false;
}
if (item.requiresAuth && !this.isLoggedIn) {
return false;
}
if (item.submenu) {
item.submenu = item.submenu.filter(subitem => subitem.showInMenu && (!subitem.requiresAuth || this.isLoggedIn));
}
return true;
});
},
isLoggedIn() {
return this.$store.getters.isLoggedIn;
}
}
};
</script>
<style scoped>
.navbar {
background-color: #9400ff;
overflow: visible;
height: 47px;
display: flex;
width: 100%;
}
.navbar ul {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
}
.navbar li {
position: relative;
}
.navbar a,
.navbar li > span {
display: block;
color: white;
text-align: center;
padding: 14px 20px;
text-decoration: none;
font-weight: bold;
}
.navbar a:hover {
background-color: #7a00d1;
}
.menu-icon {
width: 20px;
height: 20px;
margin-right: 5px;
}
.dropdown-content {
position: absolute;
background-color: #9400ff;
min-width: 200px;
z-index: 1;
top: 100%;
left: 0;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
box-shadow: 2px 2px 4px #666;
}
.dropdown-content a {
color: white;
padding: 12px 16px;
text-decoration: none;
display: block;
text-align: left;
}
.dropdown-content a:hover {
background-color: #7a00d1;
}
.navbar li:hover .dropdown-content {
opacity: 1;
visibility: visible;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
}
.fade-enter, .fade-leave-to /* .fade-leave-active in <2.1.8 */ {
opacity: 0;
visibility: hidden;
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div class="contact-person-form">
<h2>Kontaktperson Formular</h2>
<form @submit.prevent="saveContactPerson">
<label for="name">Name:</label>
<input type="text" id="name" v-model="localContactPerson.name" required>
<label for="phone">Telefon:</label>
<input type="text" id="phone" v-model="localContactPerson.phone">
<label for="street">Straße:</label>
<input type="text" id="street" v-model="localContactPerson.street">
<label for="zipcode">PLZ:</label>
<input type="text" id="zipcode" v-model="localContactPerson.zipcode">
<label for="city">Ort:</label>
<input type="text" id="city" v-model="localContactPerson.city">
<label for="email">Email:</label>
<input type="email" id="email" v-model="localContactPerson.email">
<label for="positions">Positionen:</label>
<multiselect
v-model="selectedPositions"
:options="positions"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
placeholder="Wähle einige"
label="caption"
track-by="id"
:preselect-first="false"
></multiselect>
<button type="submit">Speichern</button>
<button type="button" @click="resetForm">Neue Kontaktperson</button>
</form>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect';
import axios from 'axios';
export default {
name: 'ContactPersonForm',
components: { Multiselect },
props: {
contactPerson: {
type: Object,
default: () => ({
name: '',
phone: '',
street: '',
zipcode: '',
city: '',
email: '',
positions: []
})
},
positions: {
type: Array,
required: true
}
},
data() {
return {
localContactPerson: { ...this.contactPerson },
selectedPositions: this.contactPerson.positions || []
};
},
watch: {
contactPerson: {
handler(newVal) {
this.localContactPerson = { ...newVal };
this.selectedPositions = newVal.positions || [];
},
deep: true,
immediate: true
},
selectedPositions(newVal) {
this.localContactPerson.positions = newVal;
}
},
methods: {
async saveContactPerson() {
try {
const positionIds = this.selectedPositions.map(position => position.id);
const payload = {
...this.localContactPerson,
positionIds: positionIds
};
if (this.localContactPerson.id) {
await axios.put(`http://localhost:3000/api/contact-persons/${this.localContactPerson.id}`, payload);
} else {
await axios.post('http://localhost:3000/api/contact-persons', payload);
}
this.$emit('contactPersonSaved');
} catch (error) {
console.error('Fehler beim Speichern der Kontaktperson:', error);
}
},
resetForm() {
this.localContactPerson = {
name: '',
phone: '',
street: '',
zipcode: '',
city: '',
email: '',
positions: []
};
this.selectedPositions = [];
}
}
};
</script>
<style scoped>
@import '~vue-multiselect/dist/vue-multiselect.css';
.contact-person-form {
max-width: 400px;
margin: auto;
}
form {
display: flex;
flex-direction: column;
}
label {
margin-top: 10px;
}
button {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<div class="event-form">
<h2>Veranstaltung Formular</h2>
<form @submit.prevent="saveEvent">
<table>
<tr>
<td><label for="name">Name:</label></td>
<td><input type="text" id="name" v-model="eventData.name" required></td>
</tr>
<tr>
<td><label for="eventType">Typ:</label></td>
<td>
<multiselect
v-model="selectedEventType"
:options="eventTypes"
label="caption"
track-by="id"
placeholder="Typ wählen"
></multiselect>
</td>
</tr>
<tr>
<td><label for="dateMode">Datum-Modus:</label></td>
<td>
<select v-model="dateMode">
<option value="date">Datum</option>
<option value="weekday">Wochentag</option>
<option value="interval">Intervall</option>
</select>
</td>
</tr>
<tr v-if="dateMode === 'date' || dateMode === 'interval'">
<td><label for="date">Datum:</label></td>
<td><input type="date" id="date" v-model="eventData.date"></td>
</tr>
<tr v-if="dateMode === 'weekday' || dateMode === 'interval'">
<td><label for="dayOfWeek">Wochentag:</label></td>
<td>
<multiselect
v-model="eventData.dayOfWeek"
:options="weekdays"
label="name"
track-by="value"
placeholder="Wochentag wählen"
></multiselect>
</td>
</tr>
<tr>
<td><label for="time">Uhrzeit:</label></td>
<td><input type="time" id="time" v-model="eventData.time"></td>
</tr>
<tr>
<td><label for="endTime">Ende-Uhrzeit:</label></td>
<td><input type="time" id="endTime" v-model="eventData.endTime"></td>
</tr>
<tr>
<td><label for="description">Beschreibung:</label></td>
<td><textarea id="description" v-model="eventData.description" class="descriptionedit"></textarea></td>
</tr>
<tr>
<td><label for="institution">Institution:</label></td>
<td>
<multiselect
v-model="selectedInstitution"
:options="localInstitutions"
label="name"
track-by="id"
placeholder="Institution wählen"
></multiselect>
</td>
</tr>
<tr>
<td><label for="eventPlace">Veranstaltungsort:</label></td>
<td>
<multiselect
v-model="selectedEventPlace"
:options="localEventPlaces"
label="name"
track-by="id"
placeholder="Veranstaltungsort wählen"
></multiselect>
</td>
</tr>
<tr>
<td><label for="contactPersons">Kontaktpersonen:</label></td>
<td>
<multiselect
v-model="selectedContactPersons"
:options="localContactPersons"
:multiple="true"
label="name"
track-by="id"
placeholder="Kontaktpersonen wählen"
></multiselect>
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">Speichern</button></td>
</tr>
</table>
</form>
</div>
</template>
<script>
import axios from 'axios';
import Multiselect from 'vue-multiselect';
export default {
name: 'EventForm',
components: { Multiselect },
props: {
event: {
type: Object,
required: true,
default: () => ({})
},
institutions: {
type: Array,
required: true,
default: () => []
},
eventPlaces: {
type: Array,
required: true,
default: () => []
},
contactPersons: {
type: Array,
required: true,
default: () => []
}
},
data() {
return {
eventData: { ...this.event },
selectedEventType: null,
selectedInstitution: this.event.institution || null,
selectedEventPlace: this.event.eventPlace || null,
selectedContactPersons: this.event.contactPersons || [],
eventTypes: [],
dateMode: 'date',
weekdays: [
{ name: 'Montag', value: 1 },
{ name: 'Dienstag', value: 2 },
{ name: 'Mittwoch', value: 3 },
{ name: 'Donnerstag', value: 4 },
{ name: 'Freitag', value: 5 },
{ name: 'Samstag', value: 6 },
{ name: 'Sonntag', value: 7 },
],
localInstitutions: [...this.institutions],
localEventPlaces: [...this.eventPlaces],
localContactPersons: [...this.contactPersons]
};
},
watch: {
event(newVal) {
this.eventData = { ...newVal };
this.selectedEventType = this.eventTypes.find(type => type.id === newVal.eventTypeId) || null;
this.selectedInstitution = newVal.institution || null;
this.selectedEventPlace = newVal.eventPlace || null;
this.selectedContactPersons = newVal.contactPersons || [];
this.determineDateMode();
},
institutions(newVal) {
this.localInstitutions = [...newVal];
},
eventPlaces(newVal) {
this.localEventPlaces = [...newVal];
},
contactPersons(newVal) {
this.localContactPersons = [...newVal];
}
},
async created() {
try {
const eventTypeResponse = await axios.get('http://localhost:3000/api/event-types');
this.eventTypes = eventTypeResponse.data;
this.selectedEventType = this.eventTypes.find(type => type.id === this.event.eventTypeId) || null;
} catch (error) {
console.error('Failed to fetch event types:', error);
}
this.determineDateMode();
},
methods: {
async saveEvent() {
try {
const payload = {
...this.eventData,
eventTypeId: this.selectedEventType ? this.selectedEventType.id : null,
institution_id: this.selectedInstitution ? this.selectedInstitution.id : null,
event_place_id: this.selectedEventPlace ? this.selectedEventPlace.id : null,
contactPersonIds: this.selectedContactPersons.map(person => person.id)
};
payload.dayOfWeek = payload.dayOfWeek.value;
let response;
if (this.eventData.id) {
response = await axios.put(`http://localhost:3000/api/events/${this.eventData.id}`, payload);
} else {
response = await axios.post('http://localhost:3000/api/events', payload);
}
this.$emit('saved', response.data);
} catch (error) {
console.error('Failed to save event:', error);
}
},
determineDateMode() {
if (this.eventData.date && this.eventData.dayOfWeek) {
this.dateMode = 'interval';
} else if (this.eventData.date) {
this.dateMode = 'date';
} else if (this.eventData.dayOfWeek) {
this.dateMode = 'weekday';
} else {
this.dateMode = 'date';
}
}
}
};
</script>
<style scoped>
@import 'vue-multiselect/dist/vue-multiselect.css';
.event-form {
max-width: 600px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
table {
width: 100%;
border-collapse: collapse;
}
td {
padding: 8px;
vertical-align: top;
}
label {
display: block;
}
button {
margin-top: 20px;
}
.descriptionedit {
width: 100%;
height: 10em;
}
</style>

View File

@@ -1,58 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -0,0 +1,124 @@
<template>
<div class="institution-form">
<h2>{{ institution && institution.id ? 'Institution bearbeiten' : 'Institution erstellen' }}</h2>
<form @submit.prevent="saveInstitution">
<label for="name">Name:</label>
<input type="text" id="name" v-model="localInstitution.name" required>
<label for="street">Straße:</label>
<input type="text" id="street" v-model="localInstitution.street">
<label for="zipcode">PLZ:</label>
<input type="text" id="zipcode" v-model="localInstitution.zipcode">
<label for="city">Ort:</label>
<input type="text" id="city" v-model="localInstitution.city">
<label for="phone">Telefon:</label>
<input type="text" id="phone" v-model="localInstitution.phone">
<label for="fax">Fax:</label>
<input type="text" id="fax" v-model="localInstitution.fax">
<label for="email">E-Mail:</label>
<input type="email" id="email" v-model="localInstitution.email">
<label for="contactPersons">Kontaktpersonen:</label>
<multiselect
v-model="selectedContactPersons"
:options="contactPersons"
:multiple="true"
placeholder="Kontaktpersonen auswählen"
label="name"
track-by="id"
></multiselect>
<button type="submit">Speichern</button>
<button type="button" @click="$emit('cancelled')">Abbrechen</button>
</form>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect';
import axios from 'axios';
export default {
name: 'InstitutionForm',
components: { Multiselect },
props: {
institution: {
type: Object,
default: () => null
},
contactPersons: {
type: Array,
required: true
}
},
data() {
return {
localInstitution: {
name: '',
street: '',
zipcode: '',
city: '',
phone: '',
fax: '',
email: '',
contactPersons: []
},
selectedContactPersons: []
};
},
watch: {
institution: {
immediate: true,
handler(newVal) {
if (newVal) {
this.localInstitution = { ...newVal };
this.selectedContactPersons = newVal.contactPersons || [];
}
}
},
selectedContactPersons(newVal) {
this.localInstitution.contactPersons = newVal;
}
},
methods: {
async saveInstitution() {
try {
if (this.localInstitution.id) {
await axios.put(`http://localhost:3000/api/institutions/${this.localInstitution.id}`, this.localInstitution);
} else {
await axios.post('http://localhost:3000/api/institutions', this.localInstitution);
}
this.$emit('saved');
this.$emit('cancelled');
} catch (error) {
console.error('Fehler beim Speichern der Institution:', error);
}
}
}
};
</script>
<style scoped>
@import '~vue-multiselect/dist/vue-multiselect.css';
.institution-form {
max-width: 400px;
margin: auto;
}
form {
display: flex;
flex-direction: column;
}
label {
margin-top: 10px;
}
button {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div>
<multiselect
v-model="selectedPositions"
:options="options"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
placeholder="Positionen auswählen"
label="caption"
track-by="id"
></multiselect>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect';
import axios from 'axios';
export default {
name: 'PositionSelect',
components: { Multiselect },
props: {
value: {
type: Array,
default: () => []
},
options: {
type: Array,
default: () => []
}
},
data() {
return {
selectedPositions: this.value
};
},
watch: {
value(newVal) {
this.selectedPositions = newVal;
console.log('PositionSelect - value watch - newVal:', newVal);
},
selectedPositions(newVal) {
console.log('PositionSelect - selectedPositions watch - newVal:', newVal);
this.$emit('input', newVal);
}
},
created() {
this.fetchPositions();
},
methods: {
async fetchPositions() {
try {
const response = await axios.get('http://localhost:3000/api/positions');
console.log('PositionSelect - fetchPositions - response.data:', response.data);
this.$emit('update:options', response.data);
} catch (error) {
console.error('Fehler beim Abrufen der Positionen:', error);
}
}
}
};
</script>
<style scoped>
@import '~vue-multiselect/dist/vue-multiselect.css';
</style>

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
<template>
<div>
<h1>Seite existiert nicht</h1>
<p>Leider existiert die aufgerufene Seite nicht.</p>
</div>
</template>
<script>
export default {
name: 'DefaultComponent'
};
</script>
<style scoped>
div {
padding: 20px;
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div class="left-column">
<h2>Aktuelles</h2>
<p><a href="https://miriamgemeinde.de/wp-content/uploads/2024/04/GB.INT_.pdf"><strong>Hier finden Sie den aktuellen Gemeindebrief</strong></a></p>
<h3>Sommerfest</h3>
<p>Am Sonntag, den 09.06.2024 findet das ökumenische Sommerfest in Bonames statt.</p>
<h4>10:30 Uhr Musikalischer Gottestdienst</h4>
<p>mit der Kantorei St. Bonifatius, der Miriamkantorei und dem Gospelchor Chariots in der Katholischen Pfarrkirche St. Bonifatius, Oberer Kalbacher Weg 9</p>
<h4>Ab 12:00 Uhr Gemeinsames Fest</h4>
<p>am Evangelischen Gemeindezentrum Bonames, Kirchhofsweg 11<br>Begegnungen, Gespräche und Angebote für Kinder<br>Gegrilltes und Getränke, Kaffee und Kuchen<br>Bis 16:00 Uhr</p>
<h3>Ökumenisches Friedensgebet</h3>
<p>Jeden letzten Freitag im Monat um 18:00 Uhr<br>Evangelische Friedenskirche in Harheim</p>
<hr>
<h3>Miriamgemeinde auf <a href="https://www.youtube.com/channel/UCFWJZuXPKuTef9-NIo-vdPg">Youtube</a></h3>
<p>Gottesdienst-Aufzeichnungen von unseren Gottesdienstorten.</p>
<p>Bei Präsenzgottesdiensten finden Sie die Aufzeichnung erst am Sonntagnachmittag!</p>
<hr>
<h3>Youtube-Kanäle unserer Kindertagesstätten</h3>
<p><a href="https://www.youtube.com/channel/UCwDe0xnydn9xV-4MHjGhjWQ">KiTa Krambambuli</a></p>
<p><a href="https://www.youtube.com/channel/UC_Sj_yC__i91czlJUW4ylcg">KiTa Sternenzelt</a></p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
<p>lorem ipsum...</p>
</div>
</template>
<script>
export default {
name: 'HomeContent'
};
</script>

View File

@@ -0,0 +1,70 @@
<template>
<div class="right-column">
<img :src="currentImage" alt="Cross" />
</div>
</template>
<script>
import { menuData } from '../../config/menuData';
export default {
name: 'ImageContent',
data() {
return {
defaultImage: '/images/homepage1.png',
currentImage: '/images/homepage1.png'
};
},
watch: {
$route: {
immediate: true,
handler() {
this.updateImage();
}
}
},
methods: {
updateImage() {
const routePath = this.$route.path;
const menuItem = this.findMenuItemByPath(menuData, routePath);
if (menuItem && menuItem.image) {
this.currentImage = `/images/${menuItem.image}`;
} else {
this.currentImage = this.defaultImage;
}
},
findMenuItemByPath(menu, path) {
for (let item of menu) {
if (item.link === path) {
return item;
}
if (item.submenu) {
const subItem = this.findMenuItemByPath(item.submenu, path);
if (subItem) {
return subItem;
}
}
}
return null;
}
}
};
</script>
<style scoped>
.right-column {
background-color: #d9e2f3;
}
.right-column h2 {
text-align: center;
color: #000;
}
.right-column img {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
}
</style>

View File

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

View File

@@ -0,0 +1,11 @@
<template>
<div>
<h2>Miriams Wunderkiste</h2>
</div>
</template>
<script>
export default {
name: 'MiriamsWonderboxContent',
};
</script>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,207 @@
<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('http://localhost:3000/api/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

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

View File

@@ -0,0 +1,83 @@
<template>
<div>
<h1>Kontaktpersonen Verwaltung</h1>
<ContactPersonForm
:contactPerson="selectedContactPerson"
:positions="positions"
@contactPersonSaved="fetchContactPersons"
/>
<ul>
<li v-for="contactPerson in contactPersons" :key="contactPerson.id" @click="selectContactPerson(contactPerson)">
{{ contactPerson.name }}
</li>
</ul>
</div>
</template>
<script>
import axios from 'axios';
import ContactPersonForm from '../../components/ContactPersonForm.vue';
export default {
name: 'ContactPersonManagement',
components: {
ContactPersonForm
},
data() {
return {
contactPersons: [],
selectedContactPerson: {
name: '',
phone: '',
street: '',
zipcode: '',
city: '',
email: '',
positions: []
},
positions: []
};
},
created() {
this.fetchContactPersons();
this.fetchPositions();
},
methods: {
async fetchContactPersons() {
try {
const response = await axios.get('http://localhost:3000/api/contact-persons');
this.contactPersons = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Kontaktpersonen:', error);
}
},
async fetchPositions() {
try {
const response = await axios.get('http://localhost:3000/api/positions');
this.positions = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Positionen:', error);
}
},
selectContactPerson(contactPerson) {
this.selectedContactPerson = contactPerson;
}
}
};
</script>
<style scoped>
ul {
list-style-type: none;
padding: 0;
}
li {
cursor: pointer;
padding: 5px;
margin: 5px 0;
background-color: #f0f0f0;
}
li:hover {
background-color: #e0e0e0;
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<div class="edit-pages">
<h2>Webseiten bearbeiten</h2>
<div>
<label for="page-select">Wähle eine Seite:</label>
<select id="page-select" v-model="selectedPage" @change="loadPageContent">
<option v-for="page in pages" :key="page.link" :value="page.link">{{ page.name }}</option>
</select>
</div>
<div class="toolbar">
<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().toggleStrike().run()">Durchgestrichen</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>
<div class="table-toolbar">
<button @click="editor.chain().focus().addColumnBefore().run()">Spalte davor einfügen</button>
<button @click="editor.chain().focus().addColumnAfter().run()">Spalte danach einfügen</button>
<button @click="editor.chain().focus().addRowBefore().run()">Zeile davor einfügen</button>
<button @click="editor.chain().focus().addRowAfter().run()">Zeile danach einfügen</button>
<button @click="editor.chain().focus().deleteColumn().run()">Spalte löschen</button>
<button @click="editor.chain().focus().deleteRow().run()">Zeile löschen</button>
<button @click="editor.chain().focus().toggleHeaderColumn().run()">Header-Spalte umschalten</button>
<button @click="editor.chain().focus().toggleHeaderRow().run()">Header-Zeile umschalten</button>
<button @click="editor.chain().focus().toggleHeaderCell().run()">Header-Zelle umschalten</button>
</div>
<div class="additional-toolbar">
<button>Events</button>
<button>Kontaktpersonen</button>
<button>Institutionen</button>
<button>Gottesdienste</button>
</div>
<div :class="['htmleditor']">
<EditorContent :editor="editor" />
</div>
<button @click="savePageContent">Speichern</button>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import { useStore } from 'vuex';
import axios from '../../axios';
import { EditorContent, useEditor } from '@tiptap/vue-3';
import StarterKit from '@tiptap/starter-kit';
import Table from '@tiptap/extension-table';
import TableRow from '@tiptap/extension-table-row';
import Bold from '@tiptap/extension-bold';
import Italic from '@tiptap/extension-italic';
import Underline from '@tiptap/extension-underline';
import Strike from '@tiptap/extension-strike';
import BulletList from '@tiptap/extension-bullet-list';
import OrderedList from '@tiptap/extension-ordered-list';
import Heading from '@tiptap/extension-heading';
import { CustomTableCell, CustomTableHeader } from '../../extensions/CustomTableCell'; // Importiere die angepasste Erweiterung
export default {
name: 'EditPagesComponent',
components: {
EditorContent,
},
setup() {
const store = useStore();
const pages = ref([]);
const selectedPage = ref('');
const pageHtmlContent = computed(() => store.state.pageContent);
const editor = useEditor({
extensions: [
StarterKit,
Table.configure({
resizable: true,
}),
TableRow,
CustomTableCell,
CustomTableHeader,
Bold,
Italic,
Underline,
Strike,
BulletList,
OrderedList,
Heading.configure({
levels: [1, 2, 3],
}),
],
content: '',
onUpdate: ({ editor }) => {
store.commit('UPDATE_PAGE_CONTENT', editor.getHTML());
},
});
const fetchPages = async () => {
try {
const response = await axios.get('http://localhost:3000/api/menu-data');
pages.value = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Seiten:', error);
}
};
const loadPageContent = async () => {
try {
await store.dispatch('loadPageContent', selectedPage.value);
const content = store.getters.pageContent;
const setEditorContent = () => {
if (editor.value && editor.value.commands) {
editor.value.commands.setContent(content, false);
} else {
setTimeout(setEditorContent, 100); // Try again after 100ms if not ready
}
};
setEditorContent();
} catch (error) {
console.error('Fehler beim Laden des Seiteninhalts:', error);
}
};
const savePageContent = async () => {
try {
const selectedPageName = pages.value.find(page => page.link === selectedPage.value)?.name || '';
if (!selectedPageName) {
return;
}
const contentToSave = editor.value.getHTML();
store.commit('SET_PAGE_CONTENT', contentToSave);
await store.dispatch('savePageContent', {
link: selectedPage.value,
name: selectedPageName,
});
} catch (error) {
console.error('Fehler beim Speichern des Seiteninhalts:', error);
}
};
onMounted(fetchPages);
return {
pages,
selectedPage,
editor,
loadPageContent,
savePageContent,
pageHtmlContent,
};
},
};
</script>
<style scoped>
.edit-pages {
width: 100%;
margin: auto;
}
#page-select {
margin-bottom: 20px;
}
.toolbar {
margin-bottom: 10px;
}
.toolbar button {
margin-right: 5px;
}
.table-toolbar {
margin-bottom: 10px;
}
.table-toolbar button {
margin-right: 5px;
}
.additional-toolbar {
margin-bottom: 10px;
}
.additional-toolbar button {
margin-right: 5px;
}
.ql-container {
background-color: #fff !important;
}
.ql-editor {
background-color: #fff !important;
}
</style>

View File

@@ -0,0 +1,144 @@
<template>
<div class="event-management">
<h2>Veranstaltungen</h2>
<button @click="createEvent">Neue Veranstaltung</button>
<EventForm v-if="showForm"
:event="selectedEvent"
:institutions="institutions"
:eventPlaces="eventPlaces"
:contactPersons="contactPersons"
@saved="handleEventSaved"
@cancelled="handleEventCancelled" />
<table>
<thead>
<tr>
<th>Name</th>
<th>Typ</th>
<th>Datum</th>
<th>Uhrzeit</th>
<th>Wochentag</th>
<th>Beschreibung</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<tr v-for="event in events" :key="event.id">
<td>{{ event.name }}</td>
<td>{{ getEventTypeCaption(event.eventTypeId) }}</td>
<td>{{ event.date }}</td>
<td>{{ formatTime(event.time) }}<span v-if="event.endTime"> - {{ formatTime(event.endTime) }}</span></td>
<td>{{ getWeekdayName(event.dayOfWeek) }}</td>
<td>{{ event.description }}</td>
<td>
<button @click="editEvent(event)">Bearbeiten</button>
<button @click="deleteEvent(event.id)">Löschen</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import axios from 'axios';
import EventForm from '@/components/EventForm.vue';
import { formatTime } from '../../utils/strings';
export default {
components: { EventForm },
data() {
return {
events: [],
institutions: [],
eventPlaces: [],
contactPersons: [],
eventTypes: [],
selectedEvent: null,
showForm: false,
};
},
async created() {
await this.fetchData();
},
methods: {
formatTime,
async fetchData() {
try {
const [eventResponse, institutionResponse, eventPlaceResponse, contactPersonResponse, eventTypeResponse] = await Promise.all([
axios.get('http://localhost:3000/api/events'),
axios.get('http://localhost:3000/api/institutions'),
axios.get('http://localhost:3000/api/event-places'),
axios.get('http://localhost:3000/api/contact-persons'),
axios.get('http://localhost:3000/api/event-types')
]);
this.events = eventResponse.data;
this.institutions = institutionResponse.data;
this.eventPlaces = eventPlaceResponse.data;
this.contactPersons = contactPersonResponse.data;
this.eventTypes = eventTypeResponse.data;
} catch (error) {
console.error('Fehler beim Abrufen der Daten:', error);
}
},
createEvent() {
this.selectedEvent = {};
this.showForm = true;
},
editEvent(event) {
this.selectedEvent = { ...event };
this.showForm = true;
},
async deleteEvent(id) {
try {
await axios.delete(`http://localhost:3000/api/events/${id}`);
this.fetchData();
} catch (error) {
console.error('Fehler beim Löschen der Veranstaltung:', error);
}
},
handleEventSaved() {
this.showForm = false;
this.fetchData();
},
handleEventCancelled() {
this.showForm = false;
},
getEventTypeCaption(eventTypeId) {
const eventType = this.eventTypes.find(type => type.id === eventTypeId);
return eventType ? eventType.caption : 'Unbekannt';
},
getWeekdayName(dayOfWeek) {
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
return weekdays[dayOfWeek - 1];
},
}
};
</script>
<style scoped>
.event-management {
max-width: 1200px;
margin: 0 auto;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
}
th {
background-color: #f2f2f2;
}
button {
margin: 5px;
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<div class="event-places-management">
<h2>Veranstaltungsorte verwalten</h2>
<form @submit.prevent="addEventPlace">
<label for="name">Name:</label>
<input type="text" id="name" v-model="newEventPlace.name" placeholder="Name" required>
<label for="street">Straße:</label>
<input type="text" id="street" v-model="newEventPlace.street" placeholder="Straße" required>
<label for="zipcode">PLZ:</label>
<input type="text" id="zipcode" v-model="newEventPlace.zipcode" placeholder="PLZ" required>
<label for="city">Stadt:</label>
<input type="text" id="city" v-model="newEventPlace.city" placeholder="Stadt" required>
<label for="backgroundColor">Hintergrundfarbe:</label>
<input type="color" id="backgroundColor" v-model="newEventPlace.backgroundColor">
<button type="submit">Speichern</button>
<button type="button" v-if="editMode" @click="resetForm">Neuen Veranstaltungsort erstellen</button>
</form>
<table>
<thead>
<tr>
<th>Name</th>
<th>Bearbeiten</th>
<th>Löschen</th>
</tr>
</thead>
<tbody>
<tr v-for="eventPlace in eventPlaces" :key="eventPlace.id">
<td>{{ eventPlace.name }}</td>
<td><button @click="editEventPlace(eventPlace)">Bearbeiten</button></td>
<td><button @click="deleteEventPlace(eventPlace.id)">Löschen</button></td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
eventPlaces: [],
newEventPlace: {
name: '',
street: '',
zipcode: '',
city: '',
backgroundColor: '#ffffff'
},
editMode: false,
editId: null
};
},
methods: {
async fetchEventPlaces() {
const response = await axios.get('http://localhost:3000/api/event-places');
this.eventPlaces = response.data;
},
async addEventPlace() {
if (this.editMode) {
await axios.put(`http://localhost:3000/api/event-places/${this.editId}`, this.newEventPlace);
} else {
const response = await axios.post('http://localhost:3000/api/event-places', this.newEventPlace);
this.eventPlaces.push(response.data);
}
this.resetForm();
await this.fetchEventPlaces();
},
async updateEventPlace(eventPlace) {
await axios.put(`http://localhost:3000/api/event-places/${eventPlace.id}`, eventPlace);
this.fetchEventPlaces(); // Refresh the list
},
async deleteEventPlace(id) {
await axios.delete(`http://localhost:3000/api/event-places/${id}`);
this.fetchEventPlaces(); // Refresh the list
},
editEventPlace(eventPlace) {
this.newEventPlace = { ...eventPlace };
this.editMode = true;
this.editId = eventPlace.id;
},
resetForm() {
this.newEventPlace = {
name: '',
street: '',
zipcode: '',
city: '',
backgroundColor: '#ffffff'
};
this.editMode = false;
this.editId = null;
}
},
created() {
this.fetchEventPlaces();
}
};
</script>
<style scoped>
.event-places-management {
max-width: 600px;
margin: auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
form {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
label {
margin-top: 10px;
}
input {
margin-top: 5px;
margin-bottom: 10px;
padding: 8px;
}
button {
margin-top: 10px;
padding: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
th {
background-color: #f4f4f4;
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<div class="event-types-management">
<h2>Event-Typen Verwaltung</h2>
<form @submit.prevent="saveEventType">
<label for="newEventType">Event-Typ:</label>
<input type="text" id="newEventType" v-model="eventTypeData.caption" placeholder="Event-Typ" required>
<button type="submit">{{ editMode ? 'Aktualisieren' : 'Hinzufügen' }}</button>
<button type="button" v-if="editMode" @click="resetForm">Abbrechen</button>
</form>
<table>
<tr v-for="eventType in eventTypes" :key="eventType.id">
<td>{{ eventType.caption }}</td>
<td><button @click="editEventType(eventType)">Bearbeiten</button></td>
<td><button @click="deleteEventType(eventType.id)">Löschen</button></td>
</tr>
</table>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
eventTypes: [],
eventTypeData: {
caption: ''
},
editMode: false,
editId: null
};
},
methods: {
async fetchEventTypes() {
try {
const response = await axios.get('http://localhost:3000/api/event-types');
this.eventTypes = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Event-Typen:', error);
}
},
async saveEventType() {
try {
if (this.editMode) {
await axios.put(`http://localhost:3000/api/event-types/${this.editId}`, this.eventTypeData);
} else {
const response = await axios.post('http://localhost:3000/api/event-types', this.eventTypeData);
this.eventTypes.push(response.data);
}
this.resetForm();
await this.fetchEventTypes();
} catch (error) {
console.error('Fehler beim Speichern des Event-Typs:', error);
}
},
editEventType(eventType) {
this.eventTypeData = { ...eventType };
this.editMode = true;
this.editId = eventType.id;
},
async deleteEventType(id) {
try {
await axios.delete(`http://localhost:3000/api/event-types/${id}`);
await this.fetchEventTypes();
} catch (error) {
console.error('Fehler beim Löschen des Event-Typs:', error);
}
},
resetForm() {
this.eventTypeData = {
caption: ''
};
this.editMode = false;
this.editId = null;
}
},
async created() {
await this.fetchEventTypes();
}
};
</script>
<style scoped>
@import 'vue-multiselect/dist/vue-multiselect.css';
.event-types-management {
max-width: 600px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
form {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
label {
margin-top: 10px;
}
input {
margin-top: 10px;
padding: 5px;
}
button {
margin-top: 20px;
padding: 10px;
}
ul {
margin-top: 20px;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid rgba(224, 224, 224, 0.9);
position: relative;
}
button {
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,19 @@
<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;
}
</style>

View File

@@ -0,0 +1,189 @@
<template>
<div class="institution-management">
<h2>Institutionenverwaltung</h2>
<form @submit.prevent="saveInstitution">
<label for="name">Name:</label>
<input type="text" id="name" v-model="institutionData.name" required>
<label for="street">Straße:</label>
<input type="text" id="street" v-model="institutionData.street">
<label for="zipcode">PLZ:</label>
<input type="text" id="zipcode" v-model="institutionData.zipcode">
<label for="city">Stadt:</label>
<input type="text" id="city" v-model="institutionData.city">
<label for="phone">Telefon:</label>
<input type="text" id="phone" v-model="institutionData.phone">
<label for="fax">Fax:</label>
<input type="text" id="fax" v-model="institutionData.fax">
<label for="email">Email:</label>
<input type="email" id="email" v-model="institutionData.email">
<button type="submit">Speichern</button>
<button type="button" @click="resetForm" v-if="editMode">Neue Institution erstellen</button>
</form>
<table>
<thead>
<tr>
<th>Name</th>
<th>Bearbeiten</th>
<th>Löschen</th>
</tr>
</thead>
<tbody>
<tr v-for="institution in institutions" :key="institution.id">
<td>{{ institution.name }}</td>
<td><button @click="editInstitution(institution)">Bearbeiten</button></td>
<td><button @click="deleteInstitution(institution.id)">Löschen</button></td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'InstitutionManagement',
data() {
return {
institutions: [],
contactPersons: [],
institutionData: {
name: '',
street: '',
zipcode: '',
city: '',
phone: '',
fax: '',
email: ''
},
selectedInstitution: null,
showForm: false,
editMode: false,
editId: null
};
},
created() {
this.fetchInstitutions();
this.fetchContactPersons();
},
methods: {
async fetchInstitutions() {
try {
const response = await axios.get('http://localhost:3000/api/institutions');
this.institutions = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Institutionen:', error);
}
},
async fetchContactPersons() {
try {
const response = await axios.get('http://localhost:3000/api/contact-persons');
this.contactPersons = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Kontaktpersonen:', error);
}
},
async saveInstitution() {
try {
if (this.editMode) {
await axios.put(`http://localhost:3000/api/institutions/${this.editId}`, this.institutionData);
} else {
const response = await axios.post('http://localhost:3000/api/institutions', this.institutionData);
this.institutions.push(response.data);
}
this.resetForm();
await this.fetchInstitutions();
} catch (error) {
console.error('Fehler beim Speichern der Institution:', error);
}
},
editInstitution(institution) {
this.institutionData = { ...institution };
this.editMode = true;
this.editId = institution.id;
this.showForm = true;
},
async deleteInstitution(id) {
try {
await axios.delete(`http://localhost:3000/api/institutions/${id}`);
this.fetchInstitutions();
} catch (error) {
console.error('Fehler beim Löschen der Institution:', error);
}
},
resetForm() {
this.institutionData = {
name: '',
street: '',
zipcode: '',
city: '',
phone: '',
fax: '',
email: ''
};
this.editMode = false;
this.editId = null;
this.showForm = false;
},
showCreateForm() {
this.resetForm();
this.showForm = true;
}
}
};
</script>
<style scoped>
.institution-management {
max-width: 600px;
margin: auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
form {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
label {
margin-top: 10px;
}
input {
margin-top: 5px;
margin-bottom: 10px;
padding: 8px;
}
button {
margin-top: 10px;
padding: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
th {
background-color: #f4f4f4;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div>
<div class="previewinfo">Dies ist eine Vorschau.</div>
<div v-html="content"></div>
</div>
</template>
<script>
import { computed } from 'vue';
import { useStore } from 'vuex';
export default {
name: 'PagePreview',
setup() {
const store = useStore();
const content = computed(() => store.state.pageContent);
return {
content,
};
},
};
</script>
<style scoped>
.previewinfo {
background-color: black;
color: #d00000;
position: absolute;
top: 93px;
left: 0;
padding: 2px 10px;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<div class="position-management">
<h2>Verwalten der Rollen</h2>
<form @submit.prevent="addPosition">
<label for="caption">Rollenbezeichnung:</label>
<input type="text" id="caption" v-model="newPosition.caption" placeholder="Rollenbezeichnung" required>
<button type="submit">Speichern</button>
<button type="button" v-if="editMode" @click="resetForm">Neue Rolle erstellen</button>
</form>
<table>
<thead>
<tr>
<th>Rollenbezeichnung</th>
<th>Bearbeiten</th>
<th>Löschen</th>
</tr>
</thead>
<tbody>
<tr v-for="position in positions" :key="position.id">
<td>{{ position.caption }}</td>
<td><button @click="editPosition(position)">Bearbeiten</button></td>
<td><button @click="deletePosition(position.id)">Löschen</button></td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
positions: [],
newPosition: {
caption: ''
},
editMode: false,
editId: null
};
},
methods: {
async fetchPositions() {
const response = await axios.get('http://localhost:3000/api/positions');
this.positions = response.data;
},
async addPosition() {
if (this.editMode) {
await axios.put(`http://localhost:3000/api/positions/${this.editId}`, this.newPosition);
} else {
const response = await axios.post('http://localhost:3000/api/positions', this.newPosition);
this.positions.push(response.data);
}
this.resetForm();
await this.fetchPositions();
},
async updatePosition(position) {
await axios.put(`http://localhost:3000/api/positions/${position.id}`, position);
this.fetchPositions(); // Refresh the list
},
async deletePosition(id) {
await axios.delete(`http://localhost:3000/api/positions/${id}`);
this.fetchPositions(); // Refresh the list
},
editPosition(position) {
this.newPosition = { ...position };
this.editMode = true;
this.editId = position.id;
},
resetForm() {
this.newPosition = {
caption: ''
};
this.editMode = false;
this.editId = null;
}
},
created() {
this.fetchPositions();
}
};
</script>
<style scoped>
.position-management {
max-width: 600px;
margin: auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
form {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
label {
margin-top: 10px;
}
input {
margin-top: 5px;
margin-bottom: 10px;
padding: 8px;
}
button {
margin-top: 10px;
padding: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ccc;
padding: 10px;
text-align: left;
}
th {
background-color: #f4f4f4;
}
</style>

View File

@@ -0,0 +1,228 @@
<template>
<div class="worship-management">
<h2>Gottesdienst Verwaltung</h2>
<form @submit.prevent="saveWorship">
<label for="eventPlaceId">Veranstaltungsort:</label>
<multiselect v-model="selectedEventPlace" :options="eventPlaces" label="name" track-by="id"
placeholder="Veranstaltungsort wählen"></multiselect>
<label for="date">Datum:</label>
<input type="date" id="date" v-model="worshipData.date" required>
<label for="time">Uhrzeit:</label>
<input type="time" id="time" v-model="worshipData.time" required>
<label for="title">Titel:</label>
<input type="text" id="title" v-model="worshipData.title" required>
<label for="organizer">Gestalter:</label>
<input type="text" id="organizer" v-model="worshipData.organizer">
<label for="collection">Kollekte:</label>
<input type="text" id="collection" v-model="worshipData.collection">
<label for="address">Adresse:</label>
<input type="text" id="address" v-model="worshipData.address">
<label for="selfInformation">Selbstinformation:</label>
<input type="checkbox" id="selfInformation" v-model="worshipData.selfInformation">
<label for="highlightTime">Uhrzeit hervorheben:</label>
<input type="checkbox" id="highlightTime" v-model="worshipData.highlightTime">
<label for="neighborInvitation">Einladung zum Nachbarschaftsraum:</label>
<input type="checkbox" id="neighborInvitation" v-model="worshipData.neighborInvitation">
<label for="introLine">Einleitungszeile:</label>
<input type="text" id="introLine" v-model="worshipData.introLine">
<button type="submit">Speichern</button>
<button type="button" @click="resetForm">Neuer Gottesdienst</button>
</form>
<ul>
<li v-for="worship in worships" :key="worship.id">
<span>{{ worship.title }} - {{ formatDate(worship.date) }}, {{ formatTime(worship.time) }}</span>
<button @click="editWorship(worship)">Bearbeiten</button>
<button @click="deleteWorship(worship.id)">Löschen</button>
<div class="tooltip">{{ getEventPlaceName(worship.eventPlaceId) }}</div>
</li>
</ul>
</div>
</template>
<script>
import axios from 'axios';
import Multiselect from 'vue-multiselect';
import { formatTime, formatDate } from '../../utils/strings'; // Importieren der Methode
export default {
name: 'WorshipManagement',
components: { Multiselect },
data() {
return {
worships: [],
eventPlaces: [],
worshipData: {
eventPlaceId: null,
date: '',
time: '',
title: '',
organizer: '',
collection: '',
address: '',
selfInformation: false,
highlightTime: false,
neighborInvitation: false,
introLine: ''
},
selectedEventPlace: null,
editMode: false,
editId: null
};
},
async created() {
await this.fetchEventPlaces();
await this.fetchWorships();
},
methods: {
formatTime,
formatDate,
async fetchWorships() {
try {
const response = await axios.get('http://localhost:3000/api/worships');
this.worships = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Gottesdienste:', error);
}
},
async fetchEventPlaces() {
try {
const response = await axios.get('http://localhost:3000/api/event-places');
this.eventPlaces = response.data;
} catch (error) {
console.error('Fehler beim Abrufen der Veranstaltungsorte:', error);
}
},
async saveWorship() {
try {
const payload = {
...this.worshipData,
eventPlaceId: this.selectedEventPlace ? this.selectedEventPlace.id : null
};
if (this.editMode) {
await axios.put(`http://localhost:3000/api/worships/${this.editId}`, payload);
} else {
await axios.post('http://localhost:3000/api/worships', payload);
}
this.resetForm();
await this.fetchWorships();
} catch (error) {
console.error('Fehler beim Speichern des Gottesdienstes:', error);
}
},
editWorship(worship) {
this.worshipData = { ...worship };
this.selectedEventPlace = this.eventPlaces.find(ep => ep.id === worship.eventPlaceId);
this.editMode = true;
this.editId = worship.id;
},
async deleteWorship(id) {
try {
await axios.delete(`http://localhost:3000/api/worships/${id}`);
await this.fetchWorships();
} catch (error) {
console.error('Fehler beim Löschen des Gottesdienstes:', error);
}
},
resetForm() {
this.worshipData = {
eventPlaceId: null,
date: '',
time: '',
title: '',
organizer: '',
collection: '',
address: '',
selfInformation: false,
highlightTime: false,
neighborInvitation: false,
introLine: ''
};
this.selectedEventPlace = null;
this.editMode = false;
this.editId = null;
},
getEventPlaceName(eventPlaceId) {
const place = this.eventPlaces.find(place => place.id === eventPlaceId);
return place ? place.name : 'Unbekannter Ort';
}
}
};
</script>
<style scoped>
@import 'vue-multiselect/dist/vue-multiselect.css';
.worship-management {
max-width: 600px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
form {
display: flex;
flex-direction: column;
}
label {
margin-top: 10px;
}
button {
margin-top: 20px;
}
ul {
margin-top: 20px;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid rgba(224, 224, 224, 0.9);
position: relative;
}
button {
margin-left: 10px;
}
.tooltip {
visibility: hidden;
width: auto;
background-color: rgba(224, 224, 224, 0.6);
color: #000;
text-align: center;
padding: 5px 0;
position: absolute;
z-index: 1;
bottom: 75%;
left: 50%;
margin-left: -100px;
padding: 5px;
border: 1px solid #000;
opacity: 0;
transition: opacity 0.2s;
}
li:hover .tooltip {
visibility: visible;
opacity: 1;
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<div class="forgot-password">
<h2>Passwort vergessen</h2>
<form>
<label for="email">Email-Adresse:</label>
<input type="email" id="email" required>
<button type="submit">Link zum Zurücksetzen senden</button>
</form>
<p>
<router-link to="/login">Login</router-link>
</p>
<p>
<router-link to="/register">Registrieren</router-link>
</p>
</div>
</template>
<script>
export default {
name: 'ForgotPassword'
};
</script>
<style scoped>
.forgot-password {
max-width: 400px;
margin: auto;
}
form {
display: flex;
flex-direction: column;
}
label {
margin-top: 10px;
}
button {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<div class="login">
<h2>Login</h2>
<form @submit.prevent="runLogin">
<label for="email">Email-Adresse:</label>
<input type="email" id="email" v-model="email" required>
<label for="password">Passwort:</label>
<input type="password" id="password" v-model="password" required>
<button type="submit">Login</button>
</form>
<p>
<router-link to="/register">Registrieren</router-link>
</p>
<p>
<router-link to="/forgot-password">Passwort vergessen?</router-link>
</p>
<DialogComponent
:title="dialogTitle"
:message="dialogMessage"
v-model="dialogVisible"
@close="closeDialog"
/>
</div>
</template>
<script>
import axios from 'axios';
import DialogComponent from '../../common/components/DialogComponent.vue';
import { mapActions } from 'vuex';
export default {
name: 'LoginComponent',
components: {
DialogComponent
},
data() {
return {
email: '',
password: '',
dialogTitle: '',
dialogMessage: '',
dialogVisible: false
};
},
methods: {
...mapActions(['login']),
async runLogin() {
try {
const response = await axios.post('http://localhost:3000/api/auth/login', {
email: this.email,
password: this.password
});
console.log(1);
const token = response.data.token;
console.log(2);
const data = response.data;
console.log(3);
localStorage.setItem('token', token);
console.log(4);
console.log(data);
this.login(data.user);
console.log(5);
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
console.log(6);
this.$router.push('/admin');
} catch (error) {
if (error.response) {
this.showDialog('Fehler', error.response.data.message);
} else {
this.showDialog('Ein Fehler ist aufgetreten', error.message);
}
}
},
showDialog(title, message) {
this.dialogTitle = title;
this.dialogMessage = message;
this.dialogVisible = true;
},
closeDialog() {
this.dialogVisible = false;
}
}
};
</script>
<style scoped>
.login {
max-width: 400px;
margin: auto;
}
form {
display: flex;
flex-direction: column;
}
label {
margin-top: 10px;
}
button {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div class="register">
<h2>Registrieren</h2>
<form @submit.prevent="register">
<label for="name">Name:</label>
<input type="text" id="name" v-model="name" required>
<label for="email">Email-Adresse:</label>
<input type="email" id="email" v-model="email" required>
<label for="password">Passwort:</label>
<input type="password" id="password" v-model="password" required>
<button type="submit">Registrieren</button>
</form>
<p>
<router-link to="/login">Login</router-link>
</p>
<p>
<router-link to="/forgot-password">Passwort vergessen?</router-link>
</p>
</div>
</template>
<script>
export default {
name: 'RegisterComponent',
components: {
},
data() {
return {
name: '',
email: '',
password: '',
dialogTitle: '',
dialogMessage: '',
dialogVisible: false
};
},
methods: {
async register() {
try {
const response = await fetch('http://localhost:3000/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: this.name,
email: this.email,
password: this.password
})
});
if (response.ok) {
await response.json();
this.showDialog('Registrierung erfolgreich', 'Ihr Konto wurde erfolgreich erstellt.');
} else {
const error = await response.json();
this.showDialog('Fehler', error.message);
}
} catch (err) {
this.showDialog('Ein Fehler ist aufgetreten', err.message);
}
},
showDialog(title, message) {
this.dialogTitle = title;
this.dialogMessage = message;
this.dialogVisible = true;
}
}
};
</script>
<style scoped>
.register {
max-width: 400px;
margin: auto;
}
form {
display: flex;
flex-direction: column;
}
label {
margin-top: 10px;
}
button {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<div class="privacy-policy">
<h1>Datenschutzerklärung der Miriamgemeinde Frankfurt am Main</h1>
<p>
Die Miriamgemeinde Frankfurt am Main nimmt den Schutz Ihrer persönlichen Daten sehr ernst und behandelt Ihre personenbezogenen Daten vertraulich und entsprechend der kirchlichen Datenschutzgesetze sowie dieser Datenschutzerklärung. Die Sicherheit Ihrer Daten steht für uns an erster Stelle.
</p>
<h2>Anbieter:</h2>
<p>
Miriamgemeinde Frankfurt am Main, Gemeindebüro Bonames<br />
Kirchhofsweg 5, 60437 Frankfurt, Tel.: 50 14 17, Fax: 50 93 0148,<br />
Email: <a href="mailto:Ev.Kirche-Bonames@t-online.de">Ev.Kirche-Bonames@t-online.de</a><br />
Inhaltlich Verantwortlicher gemäß § 6 MDStV: Sabine Mentrup
</p>
<p>
Die Nutzung der Webseite der Miriamgemeinde Frankfurt am Main ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben. Die nachfolgende Erklärung gibt Ihnen einen Überblick darüber, wie dieser Schutz gewährleistet werden soll und welche Art von Daten zu welchem Zweck von Ihnen erhoben werden.
</p>
<p>
Die Miriamgemeinde Frankfurt am Main weist darauf hin, dass die Datenübertragung im Internet (z.B. bei der Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht möglich.
</p>
<h2>Datenverarbeitung auf dieser Internetseite</h2>
<h3>Server-Log-Files</h3>
<p>
Im Allgemeinen ist es für die Nutzung der Internetseite Miriamgemeinde Frankfurt am Main nicht erforderlich, dass Sie personenbezogene Daten angeben. Um die Zugriffs-Statistik auf die Internetseite zu erheben, werden folgende Daten automatisch in Log-Files des Servers erhoben und 60 Tage lang gespeichert, die der Browser übermittelt. Diese sind:
</p>
<ul>
<li>IP-Adresse (anonymisiert)</li>
<li>Betriebssystem</li>
<li>Browser-Typ / -Version / -Sprache</li>
<li>Datum und Uhrzeit der Server-Anfrage mit Angabe der Zeitzone</li>
<li>Sofern Sie Seiten mit Passwortschutz besuchen: verwendeter Benutzername</li>
<li>Webseiten, die Sie bei uns besuchen.</li>
</ul>
<p>
Diese Daten sind nicht bestimmten Personen zuordenbar. Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen.
</p>
<h3>Verwendung von Cookies</h3>
<p>
Die Internetseiten der Miriamgemeinde Frankfurt am Main verwenden teilweise sogenannte Cookies. Cookies richten auf Ihrem Rechner keinen Schaden an und enthalten keine Viren. Cookies dienen dazu, unser Angebot nutzerfreundlicher, effektiver und sicherer zu machen. Cookies sind kleine Textdateien, die auf Ihrem Rechner abgelegt werden und die Ihr Browser speichert.
</p>
<p>
Die meisten der von uns verwendeten Cookies sind sogenannte Session-Cookies. Sie werden nach Ende Ihres Besuchs automatisch gelöscht. Andere Cookies bleiben auf Ihrem Endgerät gespeichert, bis Sie diese löschen. Diese Cookies ermöglichen es uns, Ihren Browser beim nächsten Besuch wiederzuerkennen.
</p>
<p>
Sie können Ihren Browser so einstellen, dass Sie über das Setzen von Cookies informiert werden und Cookies nur im Einzelfall erlauben, die Annahme von Cookies für bestimmte Fälle oder generell ausschließen sowie das automatische Löschen der Cookies beim Schließen des Browsers aktivieren. Bei der Deaktivierung von Cookies kann die Funktionalität dieser Website eingeschränkt sein.
</p>
<h3>Kommentarfunktion</h3>
<p>
Die Kommentarfunktion auf dieser Webseite ist aktuell nicht aktiviert. Bei aktivierter Kommentarfunktion gilt: Wenn Sie einen Kommentar auf unserer Website schreiben, kann das eine Einwilligung sein, Ihren Namen, E-Mail-Adresse und Website in Cookies zu speichern. Dies ist eine Komfortfunktion, damit Sie nicht, wenn Sie einen weiteren Kommentar schreiben, all diese Daten erneut eingeben müssen. Diese Cookies werden ein Jahr lang gespeichert.
</p>
<h3>Angemeldete Nutzer</h3>
<p>
Falls Sie ein Konto haben und sich auf dieser Website anmelden, werden wir ein temporäres Cookie setzen, um festzustellen, ob Ihr Browser Cookies akzeptiert. Dieses Cookie enthält keine personenbezogenen Daten und wird verworfen, wenn Sie den Browser schließen.
</p>
<p>
Wenn Sie sich anmelden, werden wir einige Cookies einrichten, um Ihre Anmeldeinformationen und Anzeigeoptionen zu speichern. Anmelde-Cookies verfallen nach zwei Tagen und Cookies für die Anzeigeoptionen nach einem Jahr. Falls Sie bei der Anmeldung Angemeldet bleiben auswählen, wird Ihre Anmeldung zwei Wochen lang aufrechterhalten. Mit der Abmeldung aus Ihrem Konto werden die Anmelde-Cookies gelöscht.
</p>
<p>
Wenn Sie einen Artikel bearbeiten oder veröffentlichen, wird ein zusätzlicher Cookie in Ihrem Browser gespeichert. Dieser Cookie enthält keine personenbezogenen Daten und verweist nur auf die Beitrags-ID des Artikels, den Sie gerade bearbeitet haben. Der Cookie verfällt nach einem Tag.
</p>
<h3>Links zu Webseiten anderer Anbieter</h3>
<p>
Unser Online-Angebot enthält Links zu Webseiten anderer Anbieter. Wir haben keinen Einfluss darauf, dass diese Anbieter die Datenschutzbestimmungen einhalten.
</p>
<h3>Ihr Recht auf Auskunft, Löschung, Sperrung</h3>
<p>
Sie haben als Nutzer das Recht, Auskunft darüber zu verlangen, welche Daten über Sie bei uns gespeichert sind und zu welchem Zweck diese Speicherung erfolgt. Darüber hinaus können Sie unrichtige Daten berichtigen oder solche Daten löschen lassen, deren Speicherung unzulässig oder nicht mehr erforderlich ist. Sie haben die Rechte auf Datenübertragbarkeit, Einschränkung der Verarbeitung und Widerspruch. Außerdem haben Sie das Recht, sich bei der Aufsichtsbehörde über die stattfindende Datenverarbeitung zu beschweren. Zuständige Aufsichtsbehörde ist Der Beauftragte für den Datenschutz der EKD Adresse siehe unten.
</p>
<p>
Sie erhalten jederzeit ohne Angabe von Gründen kostenfrei Auskunft über Ihre bei uns gespeicherten Daten. Sie können jederzeit Ihre bei uns erhobenen Daten sperren, berichtigen oder löschen lassen. Auch können Sie jederzeit die uns erteilte Einwilligung zur Datenerhebung und Verwendung ohne Angaben von Gründen widerrufen. Wenden Sie sich hierzu bitte an die auf dieser Seite angegebene Kontaktadresse des Datenschutzbeauftragten. Wir stehen Ihnen jederzeit gern für weitergehende Fragen zu unserem Hinweisen zum Datenschutz und zur Verarbeitung Ihrer persönlichen Daten zur Verfügung.
</p>
<h3>Der Datenschutzbeauftragte für den Datenschutz der Evangelischen Kirchen in Deutschland</h3>
<p>
Die Aufsicht über die Einhaltung der Vorschriften zum Datenschutz obliegt im kirchlichen Bereich dem Beauftragten für den Datenschutz der EKD. Für den Bereich der Evangelischen Kirche in Hessen und Nassau (EKHN) ist zuständig die Außenstelle Dortmund für die Datenschutzregion Mitte-West
</p>
<p>
Friedhof 4<br />
44135 Dortmund<br />
Tel: 0231 / 533827-0<br />
Fax: 0231 / 533827-20<br />
E-Mail: <a href="mailto:mitte-west@datenschutz.ekd.de">mitte-west@datenschutz.ekd.de</a>
</p>
<h3>E-Mails / Formulare</h3>
<p>
Aus technischen oder betrieblichen Gründen kann der Empfang von E-Mail-Kommunikation gestört sein und / oder nicht rechtzeitig den Empfänger erreichen. Daher hat die Versendung von E-Mails an uns keine fristwahrende Wirkung und kann Fristen nicht rechtsverbindlich setzen. Wir empfehlen, zeitkritische oder eilige Nachrichten zusätzlich per Post, Kurier oder Telefax zu übersenden.
</p>
<p>
Falls Sie sicher sein wollen, dass Ihre E-Mail ordnungsgemäß empfangen worden ist, fordern Sie bitte von dem Empfänger eine schriftliche Empfangsbestätigung an. Wir unternehmen alle vernünftigerweise zu erwartenden Vorsichtsmaßnahmen, um das Risiko einer Übertragung von Computerviren zu verhindern. Wir sind jedoch nicht haftbar für Schäden, die durch Computerviren entstehen.
</p>
<p>
Bitte führen Sie selber Überprüfungen auf Computerviren durch, bevor Sie E-Mails lesen, insbesondere bevor Sie Anhänge zu E-Mails öffnen. Die Kommunikation per E-Mail ist unsicher, da grundsätzlich die Möglichkeit der Kenntnisnahme und Manipulation durch Dritte besteht. Wir empfehlen, keine vertraulichen Daten unverschlüsselt per E-Mail zu versenden.
</p>
<h3>Änderungen</h3>
<p>
Wir behalten uns vor, die Datenschutzerklärung zu ändern, um sie an geänderte Rechtslagen, oder bei Änderungen des Dienstes sowie der Datenverarbeitung anzupassen. Dies gilt jedoch nur im Hinblick auf Erklärungen zur Datenverarbeitung. Sofern Einwilligungen der Nutzer erforderlich sind oder Bestandteile der Datenschutzerklärung Regelungen des Vertragsverhältnisses mit den Nutzern enthalten, erfolgen die Änderungen nur mit Zustimmung der Nutzer.
</p>
<p>
Die Nutzer werden gebeten, sich regelmäßig über den Inhalt der Datenschutzerklärung zu informieren.
</p>
<p>Stand: 24. Mai 2018</p>
</div>
</template>
<script>
export default {
name: 'PrivacyPolicyComponent'
};
</script>
<style scoped>
.privacy-policy {
max-width: 800px;
margin: auto;
padding: 20px;
}
h1, h2, h3, h4, h5 {
margin-top: 20px;
color: #333;
}
p {
line-height: 1.6;
}
ul {
margin: 10px 0;
padding-left: 20px;
}
ul li {
list-style-type: disc;
}
a {
color: #007BFF;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<div class="impressum">
<h1>Evangelische Miriamgemeinde Frankfurt</h1>
<section>
<h2>Gemeindebüro Bonames</h2>
<p>Kirchhofsweg 5<br />
60437 Frankfurt<br />
Tel.: 50 14 17, Fax: 50 93 0148<br />
Gemeindesekretärin: Angela Kehl<br />
Bürozeiten:<br />
Di. 9.00 - 12.00 Uhr<br />
Email: <a href="mailto:Ev.Kirche-Bonames@t-online.de">Ev.Kirche-Bonames@t-online.de</a>
</p>
</section>
<section>
<h2>Gemeindebüro Kalbach</h2>
<p>An der Grünhohl 9<br />
60437 Frankfurt<br />
Tel.: 50 25 78, Fax: 50 49 39<br />
Gemeindesekretärin: Angela Kehl<br />
Bürozeiten:<br />
Mo. 9.00 - 12.00 Uhr<br />
Email: <a href="mailto:ev-kirchengemeinde-kalbach@t-online.de">ev-kirchengemeinde-kalbach@t-online.de</a>
</p>
</section>
<section>
<h3>Inhaltlich Verantwortlicher gemäß § 6 MDStV:</h3>
<p>Torsten Schulz</p>
</section>
<section>
<h3>Haftungsausschluss</h3>
<h4>1. Inhalt des Onlineangebotes</h4>
<p>
Der Autor übernimmt keinerlei Gewähr für die Aktualität, Korrektheit, Vollständigkeit oder Qualität der bereitgestellten Informationen. Haftungsansprüche gegen den Autor, welche sich auf Schäden materieller oder ideeller Art beziehen, die durch die Nutzung oder Nichtnutzung der dargebotenen Informationen bzw. durch die Nutzung fehlerhafter und unvollständiger Informationen verursacht wurden, sind grundsätzlich ausgeschlossen, sofern seitens des Autors kein nachweislich vorsätzliches oder grob fahrlässiges Verschulden vorliegt.
Alle Angebote sind freibleibend und unverbindlich. Der Autor behält es sich ausdrücklich vor, Teile der Seiten oder das gesamte Angebot ohne gesonderte Ankündigung zu verändern, zu ergänzen, zu löschen oder die Veröffentlichung zeitweise oder endgültig einzustellen.
</p>
<h4>2. Verweise und Links</h4>
<p>
Bei direkten oder indirekten Verweisen auf fremde Webseiten ("Hyperlinks"), die außerhalb des Verantwortungsbereiches des Autors liegen, würde eine Haftungsverpflichtung ausschließlich in dem Fall in Kraft treten, in dem der Autor von den Inhalten Kenntnis hat und es ihm technisch möglich und zumutbar wäre, die Nutzung im Falle rechtswidriger Inhalte zu verhindern.
Der Autor erklärt hiermit ausdrücklich, dass zum Zeitpunkt der Linksetzung keine illegalen Inhalte auf den zu verlinkenden Seiten erkennbar waren. Auf die aktuelle und zukünftige Gestaltung, die Inhalte oder die Urheberschaft der verlinkten/verknüpften Seiten hat der Autor keinerlei Einfluss. Deshalb distanziert er sich hiermit ausdrücklich von allen Inhalten aller verlinkten/verknüpften Seiten, die nach der Linksetzung verändert wurden. Diese Feststellung gilt für alle innerhalb des eigenen Internetangebotes gesetzten Links und Verweise sowie für Fremdeinträge in vom Autor eingerichteten Gästebüchern, Diskussionsforen, Linkverzeichnissen, Mailinglisten und in allen anderen Formen von Datenbanken, auf deren Inhalt externe Schreibzugriffe möglich sind. Für illegale, fehlerhafte oder unvollständige Inhalte und insbesondere für Schäden, die aus der Nutzung oder Nichtnutzung solcherart dargebotener Informationen entstehen, haftet allein der Anbieter der Seite, auf welche verwiesen wurde, nicht derjenige, der über Links auf die jeweilige Veröffentlichung lediglich verweist.
</p>
<h4>3. Urheber- und Kennzeichenrecht</h4>
<p>
Der Autor ist bestrebt, in allen Publikationen die Urheberrechte der verwendeten Grafiken, Tondokumente, Videosequenzen und Texte zu beachten, von ihm selbst erstellte Grafiken, Tondokumente, Videosequenzen und Texte zu nutzen oder auf lizenzfreie Grafiken, Tondokumente, Videosequenzen und Texte zurückzugreifen.
Alle innerhalb des Internetangebotes genannten und ggf. durch Dritte geschützten Marken- und Warenzeichen unterliegen uneingeschränkt den Bestimmungen des jeweils gültigen Kennzeichenrechts und den Besitzrechten der jeweiligen eingetragenen Eigentümer. Allein aufgrund der bloßen Nennung ist nicht der Schluss zu ziehen, dass Markenzeichen nicht durch Rechte Dritter geschützt sind!
Das Copyright für veröffentlichte, vom Autor selbst erstellte Objekte bleibt allein beim Autor der Seiten. Eine Vervielfältigung oder Verwendung solcher Grafiken, Tondokumente, Videosequenzen und Texte in anderen elektronischen oder gedruckten Publikationen ist ohne ausdrückliche Zustimmung des Autors nicht gestattet.
</p>
<h4>4. Datenschutz</h4>
<p>
Sofern innerhalb des Internetangebotes die Möglichkeit zur Eingabe persönlicher oder geschäftlicher Daten (Emailadressen, Namen, Anschriften) besteht, so erfolgt die Preisgabe dieser Daten seitens des Nutzers auf ausdrücklich freiwilliger Basis. Die Inanspruchnahme und Bezahlung aller angebotenen Dienste ist - soweit technisch möglich und zumutbar - auch ohne Angabe solcher Daten bzw. unter Angabe anonymisierter Daten oder eines Pseudonyms gestattet. Die Nutzung der im Rahmen des Impressums oder vergleichbarer Angaben veröffentlichten Kontaktdaten wie Postanschriften, Telefon- und Faxnummern sowie Emailadressen durch Dritte zur Übersendung von nicht ausdrücklich angeforderten Informationen ist nicht gestattet. Rechtliche Schritte gegen die Versender von sogenannten Spam-Mails bei Verstößen gegen dieses Verbot sind ausdrücklich vorbehalten.
Bitte beachten Sie unsere <router-link to="/privacy-policy">Datenschutzerklärung</router-link>.
</p>
<h4>5. Rechtswirksamkeit dieses Haftungsausschlusses</h4>
<p>
Dieser Haftungsausschluss ist als Teil des Internetangebotes zu betrachten, von dem aus auf diese Seite verwiesen wurde. Sofern Teile oder einzelne Formulierungen dieses Textes der geltenden Rechtslage nicht, nicht mehr oder nicht vollständig entsprechen sollten, bleiben die übrigen Teile des Dokumentes in ihrem Inhalt und ihrer Gültigkeit davon unberührt.
</p>
<h4>6. Schlussbestimmungen</h4>
<p>
Es gilt das Recht der Bundesrepublik Deutschland.
Ausschließlicher Gerichtsstand für Rechtsstreitigkeiten aus dem Nutzungsverhältnis ist Frankfurt.
Änderungen der Nutzungsbedingungen bedürfen der Schriftform (kein E-Mail). Dies gilt auch für die Aufhebung oder Änderung dieser Schriftklausel.
</p>
</section>
</div>
</template>
<script>
export default {
name: 'ImpressumComponent'
};
</script>
<style scoped>
.impressum {
max-width: 800px;
margin: auto;
padding: 20px;
}
h1, h2, h3, h4 {
margin-top: 20px;
color: #333;
}
p {
line-height: 1.6;
}
a {
color: #007BFF;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,11 @@
<template>
<div>
<h2>Gottesdienste in unserer Gemeinde</h2>
</div>
</template>
<script>
export default {
name: 'AllWorshipsContent',
};
</script>

View File

@@ -0,0 +1,11 @@
<template>
<div>
<h2>Gottesdienste in Bonames</h2>
</div>
</template>
<script>
export default {
name: 'BonamesContent',
};
</script>

View File

@@ -0,0 +1,27 @@
import { TableCell as TableCellOriginal } from '@tiptap/extension-table-cell';
export const CustomTableCell = TableCellOriginal.extend({
content: 'inline*',
parseHTML() {
return [
{ tag: 'td' },
];
},
renderHTML({ HTMLAttributes }) {
return ['td', HTMLAttributes, 0];
},
});
import { TableHeader as TableHeaderOriginal } from '@tiptap/extension-table-header';
export const CustomTableHeader = TableHeaderOriginal.extend({
content: 'inline*',
parseHTML() {
return [
{ tag: 'th' },
];
},
renderHTML({ HTMLAttributes }) {
return ['th', HTMLAttributes, 0];
},
});

View File

@@ -1,4 +1,21 @@
import { createApp } from 'vue' import { createApp } from 'vue';
import App from './App.vue' import AppComponent from './AppComponent.vue';
import router from './router';
import store from './store';
import axios from './axios'; // Korrigieren Sie den Import-Pfad hier
import './assets/css/editor.css'; // Hier deine CSS-Datei einbinden
createApp(App).mount('#app') async function fetchMenuData() {
const response = await fetch('http://localhost:3000/api/menu-data');
return await response.json();
}
fetchMenuData().then(menuData => {
store.commit('setMenuData', menuData);
});
const app = createApp(AppComponent);
app.use(router);
app.use(store);
app.config.globalProperties.$axios = axios;
app.mount('#app');

82
src/router.js Normal file
View File

@@ -0,0 +1,82 @@
import { createRouter, createWebHistory } from 'vue-router';
import store from './store';
function loadComponent(componentName) {
return () => {
if (!componentName) {
return import('./content/DefaultComponent.vue');
}
return import(`./content/${componentName}.vue`);
};
}
function generateRoutesFromMenu(menu) {
let routes = [];
menu.forEach(item => {
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')
}
};
if (item.submenu && item.submenu.length > 0) {
let children = generateRoutesFromMenu(item.submenu);
routes.push(...children);
}
routes.push(route);
});
return routes;
}
const router = createRouter({
history: createWebHistory(),
routes: []
});
router.beforeEach(async (to, from, next) => {
if (!store.state.menuData.length) {
await store.dispatch('loadMenuData');
const routes = generateRoutesFromMenu(store.state.menuData);
routes.forEach(route => router.addRoute(route));
addEditPagesRoute();
router.addRoute({
path: '/:pathMatch(.*)*',
components: {
default: loadComponent('DefaultComponent'),
rightColumn: loadComponent('ImageContent')
}
});
next({ ...to, replace: true });
} else {
if (to.matched.some(record => record.meta.requiresAuth) && !store.getters.isLoggedIn) {
next('/login');
} else {
next();
}
}
});
function addEditPagesRoute() {
if (router.hasRoute('/admin/edit-pages')) {
router.removeRoute('/admin/edit-pages');
}
router.addRoute({
path: '/admin/edit-pages',
components: {
default: loadComponent('admin/PagePreviewComponent'),
rightColumn: loadComponent('admin/EditPagesComponent')
},
name: 'admin-edit-pages'
});
}
addEditPagesRoute();
export default router;

Some files were not shown because too many files have changed in this diff Show More