Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,42 @@
import { Routes } from '@angular/router'
import { LoginService } from './services/login.service'
import { metaGuard } from './services/seo.service'

export const routes: Routes = [
{
path: '',
loadComponent: () => import('./pages/home/home.component').then((m) => m.HomeComponent),
title: 'Startseite',
data: { meta: { title: 'Startseite', description: 'Entdecke Veranstaltungen zum Jubiläum 1200 Jahre Radolfzell.' } },
canActivate: [metaGuard],
},
{
path: 'favourites',
loadComponent: () => import('./pages/favourites/favourites.component').then((m) => m.FavouritesComponent),
title: 'Favoriten',
data: { meta: { title: 'Favoriten', description: 'Deine gemerkten Veranstaltungen im Überblick.' } },
canActivate: [metaGuard],
},
{
path: 'about',
loadComponent: () => import('./pages/about/about.component').then((m) => m.AboutComponent),
title: 'Über uns',
data: { meta: { title: 'Über uns', description: 'Informationen zum Jubiläumsprojekt 1200 Jahre Radolfzell.' } },
canActivate: [metaGuard],
},
{
path: 'more',
loadComponent: () => import('./pages/more/more.component').then((m) => m.MoreComponent),
title: 'Mehr',
data: { meta: { title: 'Mehr', description: 'Weitere Inhalte und Informationen rund um das Jubiläum.' } },
canActivate: [metaGuard],
},
{
path: 'team',
loadComponent: () => import('./pages/devs/devs.component').then((m) => m.DevsComponent),
title: 'Team',
data: { meta: { title: 'Team', description: 'Lerne das Team hinter 1200 Jahre Radolfzell kennen.' } },
canActivate: [metaGuard],
},
{
path: 'event/:id',
Expand All @@ -36,54 +47,64 @@ export const routes: Routes = [
path: 'login',
loadComponent: () => import('./pages/login/login.component').then((m) => m.LoginPageComponent),
title: 'Login',
data: { meta: { title: 'Login', description: 'Melde dich an, um Veranstaltungen zu verwalten.' } },
canActivate: [metaGuard],
},
{
path: 'kategorie/:slug',
loadComponent: () => import('./pages/kategorie/kategorie.component').then((m) => m.KategorieComponent),
title: 'Kategorie',
data: { meta: { title: 'Kategorien', description: 'Entdecke Veranstaltungen nach Kategorien.' } },
canActivate: [metaGuard],
},
{
path: 'all-events',
path: 'event',
loadComponent: () => import('./pages/kategorie/kategorie.component').then((m) => m.KategorieComponent),
title: 'Alle Veranstaltungen',
},
{
path: 'admin',
loadComponent: () =>
import('./pages/admin-event-overview/admin-event-overview.component').then((m) => m.AdminEventOverviewComponent),
canActivate: [LoginService],
canActivate: [LoginService, metaGuard],
title: 'Admin: Veranstaltungen',
data: { meta: { title: 'Admin: Veranstaltungen', description: 'Veranstaltungen verwalten.' } },
},
{
path: 'admin/locations',
loadComponent: () =>
import('./pages/admin-location-overview/admin-location-overview.component').then((m) => m.AdminLocationOverviewComponent),
canActivate: [LoginService],
canActivate: [LoginService, metaGuard],
title: 'Admin: Orte',
data: { meta: { title: 'Admin: Orte', description: 'Veranstaltungsorte verwalten.' } },
},
{
path: 'admin/location/create',
loadComponent: () => import('./pages/location-edit/location-edit.component').then((m) => m.LocationEditComponent),
canActivate: [LoginService],
canActivate: [LoginService, metaGuard],
title: 'Ort erstellen',
data: { meta: { title: 'Ort erstellen', description: 'Neuen Veranstaltungsort anlegen.' } },
},
{
path: 'admin/location/:id',
loadComponent: () => import('./pages/location-edit/location-edit.component').then((m) => m.LocationEditComponent),
canActivate: [LoginService],
canActivate: [LoginService, metaGuard],
title: 'Ort bearbeiten',
data: { meta: { title: 'Ort bearbeiten', description: 'Veranstaltungsort bearbeiten.' } },
},
{
path: 'admin/create',
loadComponent: () => import('./pages/event-create/event-create.component').then((m) => m.EventCreateComponent),
canActivate: [LoginService],
canActivate: [LoginService, metaGuard],
title: 'Veranstaltung erstellen',
data: { meta: { title: 'Veranstaltung erstellen', description: 'Neue Veranstaltung anlegen.' } },
},
{
path: 'admin/event/:id',
loadComponent: () => import('./pages/event-create/event-create.component').then((m) => m.EventCreateComponent),
canActivate: [LoginService],
canActivate: [LoginService, metaGuard],
title: 'Veranstaltung bearbeiten',
data: { meta: { title: 'Veranstaltung erstellen', description: 'Neue oder bestehende Veranstaltung bearbeiten.' } },
},
{
path: 'offline',
Expand All @@ -96,20 +117,23 @@ export const routes: Routes = [
import('./pages/admin-organizer-overview/admin-organizer-overview.component').then(
(m) => m.AdminOrganizerOverviewComponent,
),
canActivate: [LoginService],
canActivate: [LoginService, metaGuard],
title: 'Admin: Veranstalter',
data: { meta: { title: 'Admin: Veranstaltungen', description: 'Veranstaltungen verwalten.' } },
},
{
path: 'admin/organizer/create',
loadComponent: () => import('./pages/organizer-edit/organizer-edit.component').then((m) => m.OrganizerEditComponent),
canActivate: [LoginService],
canActivate: [LoginService, metaGuard],
title: 'Veranstalter erstellen',
data: { meta: { title: 'Veranstaltung erstellen', description: 'Neue Veranstaltung anlegen.' } },
},
{
path: 'admin/organizer/:id',
loadComponent: () => import('./pages/organizer-edit/organizer-edit.component').then((m) => m.OrganizerEditComponent),
canActivate: [LoginService],
canActivate: [LoginService, metaGuard],
title: 'Veranstalter bearbeiten',
data: { meta: { title: 'Veranstaltung erstellen', description: 'Neue oder bestehende Veranstaltung bearbeiten.' } },
},
{
path: '404',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="h-18 w-90 md:w-3xl">
<a [routerLink]="['/all-events']" class="justify-top card" tabindex="0">
<a [routerLink]="['/event']" class="justify-top card" tabindex="0">
<div class="flex h-full w-full flex-col items-center justify-center p-6">
<span class="text-lg font-semibold text-gray-800"> {{ 'event-card.show-more' | translate }}</span>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/component/bottom-nav/bottom-nav.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

<span class="flex-1">
<a
routerLink="/all-events"
routerLink="/event"
routerLinkActive="text-[#c70d56] bg-[#ffffff40] rounded-xl scale-110"
class="relative flex transform flex-col items-center justify-center p-0.5 p-1 pt-2 text-[#3b4ea3] transition hover:text-[#c70d56]"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class GoBackComponent {
goBack(): void {
if (this.goBackParams) {
console.log('Navigating back with filterQuery:', this.goBackParams)
this.router.navigate(['/all-events'], { queryParams: { filterQuery: this.goBackParams } })
this.router.navigate(['/event'], { queryParams: { filterQuery: this.goBackParams } })
return
}

Expand Down
158 changes: 152 additions & 6 deletions src/app/pages/event-detail/event-detail.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Event } from '../../models/event.interface'
import { Location } from '../../models/location.interface'
import { ActivatedRoute, Router } from '@angular/router'
import { EventService } from '../../services/event.service'
import { CommonModule } from '@angular/common'
import { CommonModule, DOCUMENT } from '@angular/common'
import { Organizer } from '../../models/organizer.interface'
import { LocationService } from '../../services/location.service'
import { OrganizerService } from '../../services/organizer.service'
Expand All @@ -28,6 +28,7 @@ import { GoBackComponent } from '@app/component/go-back-button/go-back-button.co
import { EventTypePillComponent } from '@app/component/event-type-pill/event-type-pill.component'
import { EventTopicPillListComponent } from '@app/component/event-topic-pill-list/event-topic-pill-list.component'
import { EventCardListComponent } from '@app/component/event-card-list/event-card-list.component'
import { SeoService } from '@app/services/seo.service'

@Component({
selector: 'app-event-detail-page',
Expand Down Expand Up @@ -77,6 +78,8 @@ export class EventDetailPageComponent implements OnInit, OnDestroy {
private readonly loginservice = inject(LoginService)
private readonly mediaService = inject(MediaService)
private readonly markForCheck = injectMarkForCheck()
private readonly seo = inject(SeoService)
private readonly document = inject(DOCUMENT)
readonly sharedStateService = inject(SharedStateService)

ngOnInit(): void {
Expand Down Expand Up @@ -224,7 +227,13 @@ export class EventDetailPageComponent implements OnInit, OnDestroy {

// Batch-Update für weniger Change Detection Zyklen
requestAnimationFrame(async () => {
const mediaResults = await mediaPromise
const [mediaResults, location, organizer, type] = await Promise.all([
mediaPromise,
locationPromise,
organizerPromise,
typePromise,
])

if (mediaResults.length > 0) {
this.mediaList = mediaResults.map((m) => ({
url: m.url,
Expand All @@ -233,11 +242,31 @@ export class EventDetailPageComponent implements OnInit, OnDestroy {
}))
}

locationPromise.then((location) => (this.location = location))
organizerPromise.then((organizer) => (this.organizer = organizer))
typePromise.then((type) => (this.type = type))
this.location = location
this.organizer = organizer
this.type = type

const url = this.getEventUrl()
const toDate = (d?: Date) => (d ? new Date(d) : null)
const startDate = toDate(this.event!.date_start)
const endDate = toDate(this.event!.date_end)
const locale = navigator.language || 'de-DE'
let dateText = ''
if (startDate) {
const opts: Intl.DateTimeFormatOptions = { dateStyle: 'full', timeStyle: 'short' }
dateText = new Intl.DateTimeFormat(locale, opts).format(startDate)
if (endDate && endDate.getTime() !== startDate.getTime()) {
const endText = new Intl.DateTimeFormat(locale, opts).format(endDate)
dateText = `${dateText} – ${endText}`
}
}
const descriptionParts: string[] = []
if (dateText) descriptionParts.push(dateText)
if (this.location?.name) descriptionParts.push(this.location.name)
const description = descriptionParts.join(' • ') || this.event!.description || this.event!.name

document.title = `${this.event!.name} - 1200 Jahre Radolfzell`
this.seo.setSocialMeta(this.event!.name, description, url, this.mediaList[0]?.url, 'website')
this.setStructuredData(this.event!, this.mediaList[0]?.url)
this.markForCheck()
})
} catch (err) {
Expand Down Expand Up @@ -270,4 +299,121 @@ export class EventDetailPageComponent implements OnInit, OnDestroy {
const baseUrl = window.location.origin
return `${baseUrl}/event/${id}`
}


/**
* Fügt strukturierte Daten (Schema.org/JSON-LD) für Events hinzu,
* damit Suchmaschinen und Social Crawler die Veranstaltung besser verstehen.
*/
private setStructuredData(event: Event, imageUrl?: string): void {
const scriptId = 'ld-json-event'
const existing = this.document.getElementById(scriptId)
if (existing?.parentNode) {
existing.parentNode.removeChild(existing)
}

const url = this.getEventUrl()
const startDate = event.date_start ? new Date(event.date_start) : null
const endDate = event.date_end ? new Date(event.date_end) : null

const data: {
'@context': 'https://schema.org'
'@type': 'Event'
name: string
url: string
eventStatus: string
eventAttendanceMode: string
description?: string
startDate?: string
endDate?: string
image?: string[]
location?: {
'@type': 'Place'
name: string
address: {
'@type': 'PostalAddress'
streetAddress?: string
postalCode?: string
addressLocality: string
addressCountry: string
}
geo?: {
'@type': 'GeoCoordinates'
latitude: number
longitude: number
}
}
organizer?: {
'@type': 'Organization'
name: string
email?: string
telephone?: string
}
offers?: {
'@type': 'Offer'
price: string
priceCurrency: string
availability: string
}
} = {
'@context': 'https://schema.org',
'@type': 'Event',
name: event.name,
url,
eventStatus: 'https://schema.org/EventScheduled',
eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode',
}

if (event.description) data.description = event.description
if (startDate) data.startDate = startDate.toISOString()
if (endDate) data.endDate = endDate.toISOString()
if (imageUrl) data.image = [imageUrl]

if (this.location) {
const latitude = this.location.geo_point?.coordinates?.[1]
const longitude = this.location.geo_point?.coordinates?.[0]
data.location = {
'@type': 'Place',
name: this.location.name,
address: {
'@type': 'PostalAddress',
streetAddress: this.location.street || undefined,
postalCode: this.location.zip_code || undefined,
addressLocality: this.location.city || 'Radolfzell',
addressCountry: 'DE',
},
}
if (latitude != null && longitude != null) {
data.location.geo = {
'@type': 'GeoCoordinates',
latitude,
longitude,
}
}
}

if (this.organizer) {
data.organizer = {
'@type': 'Organization',
name: this.organizer.name,
email: this.organizer.email || undefined,
telephone: this.organizer.phonenumber || undefined,
}
}

if (event.price != null) {
data.offers = {
'@type': 'Offer',
price: String(event.price),
priceCurrency: 'EUR',
availability: 'https://schema.org/InStock',
}
}

const script = this.document.createElement('script')
script.type = 'application/ld+json'
script.id = scriptId
script.text = JSON.stringify(data)
this.document.head.appendChild(script)
}
}
Loading
Loading