Enhance project structure and styling: Update .gitignore to exclude build artifacts and uploads, modify package.json to streamline the build process, and refactor HTML and Vue components for improved layout and accessibility. Add new CSS styles for better presentation in ContactRender, EventRender, WorshipRender, and ImageContent components.

This commit is contained in:
Torsten Schulz (local)
2026-04-08 08:54:31 +02:00
parent 7e4f2935a3
commit 99ec18c8f7
9 changed files with 332 additions and 163 deletions

8
.gitignore vendored
View File

@@ -27,5 +27,13 @@ server.key
server.cert
public/images/uploads/1ba24ea7-f52c-4179-896f-1909269cab58.jpg
# Vue Build-Artefakte (werden beim Deploy generiert)
public/js/
public/css/
public/**/*.map
# Uploads/Runtime-Dateien nicht versionieren
public/images/uploads/
actualize.sh
files/uploads/GD 24.08.2025-04.01.2026 Stand 12.08.2025.docx

View File

@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build && npm run copy-dist",
"build": "vue-cli-service build",
"copy-dist": "cp -r dist/* public/",
"lint": "vue-cli-service lint"
},

View File

@@ -1 +1,19 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>miriamgemeinde</title><script defer="defer" src="/js/chunk-vendors.a58901d9.js"></script><script defer="defer" src="/js/app.2b3ac443.js"></script><link href="/css/app.c2c4030a.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors.a58901d9.js"></script><script defer="defer" src="/js/app.62331f73.js"></script><link href="/css/app.c2c4030a.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors.a58901d9.js"></script><script defer="defer" src="/js/app.f7f58406.js"></script><link href="/css/app.c2c4030a.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors.b7e76d39.js"></script><script defer="defer" src="/js/app.c50b5429.js"></script><link href="/css/app.3e68accd.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors.b7e76d39.js"></script><script defer="defer" src="/js/app.53c460b9.js"></script><link href="/css/app.43dcf86b.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors.b7e76d39.js"></script><script defer="defer" src="/js/app.e71748c3.js"></script><link href="/css/app.43dcf86b.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors.2da008aa.js"></script><script defer="defer" src="/js/app.e009cb77.js"></script><link href="/css/app.674aab9c.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors.2da008aa.js"></script><script defer="defer" src="/js/app.f2049431.js"></script><link href="/css/app.105a92a3.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but miriamgemeinde doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>
Diese Website funktioniert leider nicht richtig, wenn JavaScript deaktiviert ist. Bitte aktivieren Sie
JavaScript, um fortzufahren.
</strong>
</noscript>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1,89 @@
/**
* Phase 3 Inhaltsmodule als Karten (ruhig, seriös, wiederverwendbar).
*/
.mg-stack {
display: grid;
gap: var(--space-4);
}
.mg-card {
background: var(--color-bg-page);
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
padding: var(--space-4);
}
.mg-card--highlight {
border-color: var(--color-brand-primary);
box-shadow: 0 0 0 1px rgba(148, 0, 255, 0.18);
}
.mg-card__grid {
display: grid;
grid-template-columns: 160px 1fr;
gap: var(--space-4);
align-items: start;
}
@media (max-width: 640px) {
.mg-card__grid {
grid-template-columns: 1fr;
}
}
.mg-media {
width: 100%;
aspect-ratio: 4 / 3;
border-radius: 6px;
overflow: hidden;
background: var(--color-bg-subtle);
border: 1px solid rgba(0, 0, 0, 0.08);
}
.mg-media > img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.mg-title {
margin: 0 0 var(--space-2) 0;
font-weight: 600;
}
.mg-meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-2) var(--space-4);
margin: 0 0 var(--space-2) 0;
color: var(--color-text-muted);
font-size: 0.95rem;
}
.mg-meta strong {
color: var(--color-text);
font-weight: 600;
}
.mg-text {
margin: 0;
}
.mg-badge {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 2px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(0, 0, 0, 0.1);
color: var(--color-text);
font-size: 0.85rem;
}
.mg-accent-left {
border-left: 6px solid rgba(0, 0, 0, 0.12);
padding-left: var(--space-3);
}

View File

@@ -1,28 +1,34 @@
<template>
<div v-if="config && config.style === 'box' && contacts && contacts.length && contacts.length > 0">
<div v-for="contact in contacts" :key="contact.id" class="contact-box bottom-margin">
<p>{{ contact.name }} <span v-if="contact.expiryDate" class="expiry-date">(bis {{ formatDate(contact.expiryDate) }})</span></p>
<p v-if="displayOptions.includes('phone')">Telefon: {{ contact.phone }}</p>
<p v-if="displayOptions.includes('street')">Straße: {{ contact.street }}</p>
<p v-if="displayOptions.includes('zipcode')">Postleitzahl: {{ contact.zipcode }}</p>
<p v-if="displayOptions.includes('city')">Stadt: {{ contact.city }}</p>
<p v-if="displayOptions.includes('email')">E-Mail: {{ contact.email }}</p>
<p v-if="displayOptions.includes('positions')">Positionen: {{ contact.positions.map(pos =>
pos.caption).join(', ') }}</p>
</div>
</div>
<span v-else-if="config.style === 'float' && contacts && contacts.length && contacts.length > 0">
<span v-for="contact in contacts" :key="contact.id" class="bottom-margin">
{{ contact.name }}<span v-if="contact.expiryDate" class="expiry-date"> (bis {{ formatDate(contact.expiryDate) }})</span>
<span v-if="displayOptions.includes('phone')">, Telefon: {{ contact.phone }}</span>
<span v-if="displayOptions.includes('street')">, Straße: {{ contact.street }}</span>
<span v-if="displayOptions.includes('zipcode')">, Postleitzahl: {{ contact.zipcode }}</span>
<span v-if="displayOptions.includes('city')">, Stadt: {{ contact.city }}</span>
<span v-if="displayOptions.includes('email')">, E-Mail: {{ contact.email }}</span>
<span v-if="displayOptions.includes('positions')">, Positionen: {{ contact.positions.map(pos =>
pos.caption).join(', ') }}</span>
</span>
</span>
<div v-if="config && config.style === 'box' && contacts && contacts.length && contacts.length > 0" class="mg-stack">
<article v-for="contact in contacts" :key="contact.id" class="mg-card">
<h3 class="mg-title">
{{ contact.name }}
<span v-if="contact.expiryDate" class="expiry-date">(bis {{ formatDate(contact.expiryDate) }})</span>
</h3>
<p class="mg-meta" v-if="displayOptions.includes('positions') && contact.positions?.length">
<span><strong>Positionen:</strong> {{ contact.positions.map(pos => pos.caption).join(', ') }}</span>
</p>
<div class="mg-stack stack-tight">
<p v-if="displayOptions.includes('phone') && contact.phone" class="mg-text"><strong>Telefon:</strong> {{ contact.phone }}</p>
<p v-if="displayOptions.includes('email') && contact.email" class="mg-text"><strong>E-Mail:</strong> {{ contact.email }}</p>
<p v-if="displayOptions.includes('street') && contact.street" class="mg-text"><strong>Straße:</strong> {{ contact.street }}</p>
<p v-if="displayOptions.includes('zipcode') && contact.zipcode" class="mg-text"><strong>PLZ:</strong> {{ contact.zipcode }}</p>
<p v-if="displayOptions.includes('city') && contact.city" class="mg-text"><strong>Stadt:</strong> {{ contact.city }}</p>
</div>
</article>
</div>
<div v-else-if="config.style === 'float' && contacts && contacts.length && contacts.length > 0" class="float-list">
<p v-for="contact in contacts" :key="contact.id" class="float-item">
<strong>{{ contact.name }}</strong><span v-if="contact.expiryDate" class="expiry-date"> (bis {{ formatDate(contact.expiryDate) }})</span>
<span v-if="displayOptions.includes('phone') && contact.phone">, Telefon: {{ contact.phone }}</span>
<span v-if="displayOptions.includes('street') && contact.street">, Straße: {{ contact.street }}</span>
<span v-if="displayOptions.includes('zipcode') && contact.zipcode">, PLZ: {{ contact.zipcode }}</span>
<span v-if="displayOptions.includes('city') && contact.city">, Stadt: {{ contact.city }}</span>
<span v-if="displayOptions.includes('email') && contact.email">, E-Mail: {{ contact.email }}</span>
<span v-if="displayOptions.includes('positions') && contact.positions?.length">, Positionen: {{ contact.positions.map(pos => pos.caption).join(', ') }}</span>
</p>
</div>
</template>
<script>
@@ -73,15 +79,22 @@ export default {
</script>
<style scoped>
.contact-box p {
margin: 0;
}
.bottom-margin {
margin-bottom: 1rem;
}
.expiry-date {
font-size: 0.9em;
color: #666;
font-style: italic;
font-size: 0.9em;
color: var(--color-text-muted);
font-style: italic;
}
.stack-tight {
gap: var(--space-2);
}
.float-list {
display: grid;
gap: var(--space-3);
}
.float-item {
margin: 0;
}
</style>

View File

@@ -1,39 +1,54 @@
<template>
<div>
<table v-if="events.length > 1" class="event-table">
<tr v-for="event in events" :key="event.id">
<td>
<div v-if="hasImage(event)" class="event-image"><img v-if="imageMap[event.relatedImage]"
:src="imageMap[event.relatedImage]" /></div>
<div v-if="shouldDisplay('name')" class="event-name">{{ event.name }}</div>
<div>{{ formatDateOrDay(event.date, event.dayOfWeek) }}</div>
<div v-if="shouldDisplay('time')">{{ formatTime(event.time) }} <span v-if="event.endTime"> - {{
formatTime(event.endTime) }}</span> Uhr</div>
<div v-if="shouldDisplay('place')">{{ event.eventPlace?.name }}</div>
<div v-if="shouldDisplay('description')" class="description">{{ event.description }}</div>
<div v-if="shouldDisplay('contactPerson')">{{event.contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
</div>
<div v-if="shouldDisplay('institution')">{{ event.institution?.name }}</div>
<div v-if="shouldDisplay('type')">{{ event.eventType?.caption }}</div>
</td>
</tr>
</table>
<div v-else-if="events.length === 1"
:class="events[0].alsoOnHomepage && config.id === 'home' ? 'homepage' : ''">
<div v-if="hasImage(events[0])" class="event-image"><img v-if="imageMap[events[0].relatedImage]"
:src="imageMap[events[0].relatedImage]" /></div>
<div v-if="shouldDisplay('name')" class="event-name">{{ events[0].name }}</div>
<div>{{ formatDateOrDay(events[0].date, events[0].dayOfWeek) }}</div>
<div v-if="shouldDisplay('time')">{{ formatTime(events[0].time) }} <span v-if="events[0].endTime"> - {{
formatTime(events[0].endTime) }}</span> Uhr</div>
<div v-if="shouldDisplay('place')">{{ events[0].eventPlace?.name }}</div>
<div v-if="shouldDisplay('description')" class="description">{{ events[0].description }}</div>
<div v-if="shouldDisplay('contactPerson')">{{events[0].contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
</div>
<div v-if="shouldDisplay('institution')">{{ events[0].institution?.name }}</div>
<div v-if="shouldDisplay('type')">{{ events[0].eventType?.caption }}</div>
<div>
<div v-if="events.length" class="mg-stack">
<article
v-for="event in events"
:key="event.id"
class="mg-card"
:class="event.alsoOnHomepage && config.id === 'home' ? 'mg-card--highlight' : ''"
>
<div class="mg-card__grid">
<div v-if="hasImage(event)" class="mg-media" aria-hidden="true">
<img v-if="imageMap[event.relatedImage]" :src="imageMap[event.relatedImage]" alt="" />
</div>
<div v-else class="mg-media" aria-hidden="true"></div>
<div>
<h3 v-if="shouldDisplay('name')" class="mg-title">{{ event.name }}</h3>
<p class="mg-meta">
<span><strong>Datum:</strong> {{ formatDateOrDay(event.date, event.dayOfWeek) }}</span>
<span v-if="shouldDisplay('time')">
<strong>Uhrzeit:</strong>
{{ formatTime(event.time) }}<span v-if="event.endTime"> {{ formatTime(event.endTime) }}</span> Uhr
</span>
<span v-if="shouldDisplay('place') && event.eventPlace?.name">
<strong>Ort:</strong> {{ event.eventPlace?.name }}
</span>
</p>
<p v-if="shouldDisplay('description') && event.description" class="mg-text mg-accent-left">
{{ event.description }}
</p>
<p v-if="shouldDisplay('contactPerson') && event.contactPersons?.length" class="mg-meta">
<span>
<strong>Kontakt:</strong>
{{ event.contactPersons.map(cp => formatContactPerson(cp)).join(', ') }}
</span>
</p>
<p v-if="shouldDisplay('institution') && event.institution?.name" class="mg-meta">
<span><strong>Institution:</strong> {{ event.institution?.name }}</span>
</p>
<p v-if="shouldDisplay('type') && event.eventType?.caption" class="mg-meta">
<span><strong>Typ:</strong> {{ event.eventType?.caption }}</span>
</p>
</div>
</div>
</article>
</div>
<p v-else>Keine Veranstaltungen verfügbar.</p>
</div>
</template>
<script>
@@ -120,30 +135,7 @@ export default {
</script>
<style scoped>
.event-name {
font-weight: bold;
}
.event-table {
border-collapse: collapse;
}
.event-table td {
border: 1px solid black;
}
.homepage {
border: 1px solid #9400ff;
padding: 0.5em;
text-align: center;
}
.description {
padding: 0.5em 0;
}
.event-image > img {
max-width: 12em;
max-height: 12em;
.mg-title {
margin-top: 0;
}
</style>

View File

@@ -1,51 +1,70 @@
<template>
<div>
<table v-if="worships.length" class="worships">
<tr v-for="worship in worships" :key="worship.id"
:style="worship.eventPlace && worship.eventPlace.backgroundColor ? `background-color:${worship.eventPlace.backgroundColor}` : ''">
<td>
<div>{{ formatDate(worship.date) }}</div>
<div>{{ worship.dayName }}</div>
</td>
<td>
<div v-if="worship.neighborInvitation" class="neighborhood-invitation">Einladung zum Gottesdienst im
Nachbarschaftsraum:</div>
<h3>
<span
:class="worship.highlightTime ? 'highlight-time' : ''"
>{{ formatTime(worship.time) }}</span>&nbsp;-&nbsp;
{{
worship.title
? worship.title
: (worship.eventPlace && worship.eventPlace.name
? `Gottesdienst in ${worship.eventPlace.name}`
: 'Gottesdienst')
}}
</h3>
<div v-if="worship.organizer">Gestaltung: {{ worship.organizer }}</div>
<div v-if="worship.sacristanService" class="internal-information">Küsterdienst: {{ worship.sacristanService }}</div>
<div v-if="worship.collection">Kollekte: {{ worship.collection }}</div>
<div v-if="worship.organPlaying" class="internal-information">Orgelspiel: {{ worship.organPlaying }}</div>
<div v-if="worship.address">{{ worship.address }}</div>
<div
v-if="!worship.address && worship.eventPlace && worship.eventPlace.id"
>
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{ worship.eventPlace.city }}
</div>
<div v-if="worship.selfInformation" class="selfinformation">
Bitte informieren Sie sich auch auf den
<a
v-if="worship.eventPlace && worship.eventPlace.website"
:href="worship.eventPlace.website"
target="_blank"
>Internetseiten dieser Gemeinde!</a>
<span v-else>Internetseiten dieser Gemeinde!</span>
</div>
</td>
</tr>
</table>
<p v-else>Keine Gottesdienste verfügbar.</p>
<div>
<div v-if="worships.length" class="mg-stack">
<article
v-for="worship in worships"
:key="worship.id"
class="mg-card"
>
<div class="worship-card">
<aside
class="worship-card__left"
:style="worship.eventPlace?.backgroundColor ? `background-color:${worship.eventPlace.backgroundColor}` : ''"
>
<div class="worship-card__date">{{ formatDate(worship.date) }}</div>
<div v-if="worship.dayName" class="worship-card__day">{{ worship.dayName }}</div>
<div v-if="worship.eventPlace?.name" class="worship-card__place">{{ worship.eventPlace.name }}</div>
</aside>
<div class="worship-card__right">
<h3 class="mg-title">
<span :class="worship.highlightTime ? 'highlight-time' : ''">{{ formatTime(worship.time) }}</span>
<span aria-hidden="true"> </span>
{{
worship.title
? worship.title
: (worship.eventPlace && worship.eventPlace.name
? `Gottesdienst in ${worship.eventPlace.name}`
: 'Gottesdienst')
}}
</h3>
<p v-if="worship.neighborInvitation" class="neighborhood-invitation mg-accent-left">
Einladung zum Gottesdienst im Nachbarschaftsraum
</p>
<div class="mg-stack stack-tight">
<p v-if="worship.organizer" class="mg-text"><strong>Gestaltung:</strong> {{ worship.organizer }}</p>
<p v-if="worship.collection" class="mg-text"><strong>Kollekte:</strong> {{ worship.collection }}</p>
<p v-if="worship.address" class="mg-text"><strong>Adresse:</strong> {{ worship.address }}</p>
<p v-if="!worship.address && worship.eventPlace && worship.eventPlace.id" class="mg-text">
<strong>Adresse:</strong> {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{ worship.eventPlace.city }}
</p>
<p v-if="worship.sacristanService" class="internal-information mg-text">
<strong>Küsterdienst:</strong> {{ worship.sacristanService }}
</p>
<p v-if="worship.organPlaying" class="internal-information mg-text">
<strong>Orgelspiel:</strong> {{ worship.organPlaying }}
</p>
<p v-if="worship.selfInformation" class="selfinformation mg-text">
Bitte informieren Sie sich auch auf den
<a
v-if="worship.eventPlace && worship.eventPlace.website"
:href="worship.eventPlace.website"
target="_blank"
rel="noopener noreferrer"
>Internetseiten dieser Gemeinde</a>
<span v-else>Internetseiten dieser Gemeinde</span>.
</p>
</div>
</div>
</div>
</article>
</div>
<p v-else>Keine Gottesdienste verfügbar.</p>
</div>
</template>
<script>
@@ -86,32 +105,57 @@ export default {
</script>
<style scoped>
table.worships {
border-collapse: collapse;
width: 100%;
.worship-card {
display: grid;
grid-template-columns: 180px 1fr;
gap: var(--space-4);
align-items: start;
}
table.worships td {
border: 1px solid #000;
text-align: center;
@media (max-width: 640px) {
.worship-card {
grid-template-columns: 1fr;
gap: var(--space-3);
}
}
h3 {
margin: 0;
.worship-card__left {
border-radius: 6px;
padding: var(--space-3);
color: var(--color-text);
background: var(--color-bg-subtle);
border: 1px solid rgba(0, 0, 0, 0.08);
}
table.worships td div{
margin: 5px;
.worship-card__date {
font-weight: 600;
}
.worship-card__day,
.worship-card__place {
margin-top: var(--space-1);
color: rgba(0, 0, 0, 0.75);
}
.worship-card__right {
min-width: 0;
}
.highlight-time {
text-decoration: underline;
text-decoration: underline;
}
.neighborhood-invitation {
font-weight: bold;
color: #0020e0;
font-weight: 600;
}
a {
color: #0020e0;
color: var(--color-brand-primary-hover);
}
.internal-information {
color: #e45;
font-style: italic;
color: #a01935;
font-style: italic;
}
.stack-tight {
gap: var(--space-2);
}
</style>

View File

@@ -1,5 +1,7 @@
<template>
<img :src="currentImage" />
<div class="side-image">
<img :src="currentImage" alt="" />
</div>
</template>
<script>
@@ -54,15 +56,17 @@ export default {
</script>
<style scoped>
.right-column h2 {
text-align: center;
color: #000;
.side-image {
width: 100%;
aspect-ratio: 4 / 3;
background: var(--color-bg-subtle);
overflow: hidden;
}
.right-column img {
.side-image img {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
width: 100%;
height: 100%;
object-fit: cover;
}
</style>

View File

@@ -4,6 +4,7 @@ import router from './router';
import store from './store';
import axios from './axios';
import './assets/css/design-tokens.css';
import './assets/css/content-cards.css';
import './assets/css/editor.css';
const app = createApp(AppComponent);