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

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);