Matchcenter: Spielplan, Ergebnisse & Tabellen (eigene Seite statt BFV-Widget)
Neue Unterseite /matchcenter ersetzt die alten BFV-Widget-Embeds (verstoßen
gegen CSP) durch eine self-hostete Lösung: Cron-Scraper holt die Daten aus der
öffentlichen BFV-Widget-JSON-API, schreibt data/matchcenter.json und lädt die
Vereinswappen lokal — Besucher kontaktieren nur unsere Domain.
- bin/matchcenter-sync.php: CLI/Cron-Scraper (Muster wie instagram-sync), JSON-API
team/{id}/matches + competition/{compoundId}/table, Wappen via getLogo lokal,
atomarer Write, Fail-safe. Auto-Saison: Tabellen-compoundId wird aus der
Matches-API abgeleitet (team_id ist saison-stabil). Per-Team-Cache-Fallback bei
Fetch-Fehlern, damit Sektionen nie leeren.
- Seite app/pages/matchcenter.php + Route + Nav-Eintrag (unter Fußball).
- Komponenten: match-slider (nächste Spiele), match-card, match-row, league-table
(mit Auf-/Abstiegszonen), matchcenter-section (kicker-Layout: Tabelle 2/3 +
Spiele 1/3), matchcenter-empty, crest (weißes Chip + Initialen-Fallback).
- carousel.js: banner.js zum generischen [data-carousel]-Antrieb verallgemeinert
(Banner- und Match-Slider teilen ihn); crest.js: Logo-Fallback im Browser.
- helpers.php: sportsevent_nodes() für SportsEvent-JSON-LD.
- Icons calendar-event; config.example + .gitignore + CLAUDE.md ergänzt.
- Responsive geprüft (kein Seiten-Overflow 360–1280, Tabelle scrollt in der Karte).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192vTfGmGpoWeyUse4TuQFj
This commit is contained in:
@@ -2903,3 +2903,613 @@
|
||||
.facility__actions {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
/* === matchcenter === */
|
||||
/* Vereinswappen — gemeinsam für Karten und Tabelle; fehlt das Bild, rendert die
|
||||
Komponente einen Initialen-Kreis (.crest--initials) statt eines kaputten <img>. */
|
||||
/* Weißes Chip hält auch helle/weiße Logos auf dunklem Grund sichtbar. */
|
||||
.crest {
|
||||
box-sizing: border-box;
|
||||
flex: none;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 3px;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.crest--initials {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
font-family: var(--ff-body);
|
||||
font-size: var(--fs-300);
|
||||
font-weight: var(--fw-bold);
|
||||
line-height: 1;
|
||||
color: var(--clr-text-muted);
|
||||
background: var(--clr-glass-bg);
|
||||
border: 1px solid var(--clr-glass-border);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Einzelne Begegnung. */
|
||||
.match-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
padding: clamp(1rem, 0.8rem + 0.8vw, 1.4rem);
|
||||
background: var(--clr-glass-bg);
|
||||
border: 1px solid var(--clr-glass-border);
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
/* Datum oben mittig (kicker-Stil). */
|
||||
.match-card__date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4em;
|
||||
font-size: var(--fs-300);
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.match-card__date-icon {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.match-card__fixture {
|
||||
display: grid;
|
||||
/* minmax(0, …) statt 1fr: erlaubt den Spalten, schmaler als ihr Inhalt zu
|
||||
werden (sonst überläuft die Karte auf sehr schmalen Screens). */
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.match-card__side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0; /* verhindert Grid-Blowout der 1fr-Spalten bei langen Namen */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.match-card__club {
|
||||
font-size: var(--fs-400);
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere; /* lange Namen brechen um statt zu überlaufen */
|
||||
}
|
||||
|
||||
.match-card__side.is-own .match-card__club {
|
||||
font-weight: var(--fw-bold);
|
||||
color: var(--clr-text);
|
||||
}
|
||||
|
||||
.match-card__center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 3.25rem;
|
||||
}
|
||||
|
||||
.match-card__score {
|
||||
font-family: var(--ff-heading);
|
||||
font-size: var(--fs-700);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.match-card__colon {
|
||||
margin: 0 0.12em;
|
||||
}
|
||||
|
||||
.match-card__vs {
|
||||
font-family: var(--ff-heading);
|
||||
font-size: var(--fs-650);
|
||||
color: var(--clr-text-faint);
|
||||
}
|
||||
|
||||
/* Zeit mittig unter dem Ergebnis/Anstoß (kicker-Stil). */
|
||||
.match-card__time {
|
||||
margin-top: 0.4rem;
|
||||
font-size: var(--fs-400);
|
||||
color: var(--clr-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Hero-Variante (im Slider): größer und ruhiger. */
|
||||
.match-card--hero {
|
||||
padding: clamp(1.5rem, 1rem + 2vw, 2.5rem);
|
||||
}
|
||||
|
||||
.match-card--hero .match-card__score {
|
||||
font-size: var(--fs-800);
|
||||
}
|
||||
|
||||
.match-card--hero .crest,
|
||||
.match-card--hero .crest--initials {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.match-card--hero .match-card__club {
|
||||
font-size: var(--fs-500);
|
||||
}
|
||||
|
||||
/* „Nächste Spiele"-Slider — Mechanik wie .ad-banner (gemeinsamer Antrieb
|
||||
carousel.js, gemeinsame Keyframe ad-progress). */
|
||||
.match-slider {
|
||||
--slider-interval: 6s;
|
||||
}
|
||||
|
||||
.match-slider > .container > h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.match-slider__track {
|
||||
list-style: none;
|
||||
margin: 2rem auto 0;
|
||||
padding: 0;
|
||||
max-width: 620px;
|
||||
}
|
||||
|
||||
.match-slider__slide[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__team {
|
||||
margin-bottom: 0.85rem;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: var(--fs-300);
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
/* Crossfade nur mit JS: Slides im selben Grid-Feld stapeln (konstante Höhe). */
|
||||
.match-slider.is-enhanced .match-slider__track {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr); /* gestapelte Slides nie breiter als der Track */
|
||||
}
|
||||
|
||||
.match-slider.is-enhanced .match-slider__slide {
|
||||
grid-area: 1 / 1;
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.55s ease, transform 0.55s ease, visibility 0s linear 0.55s;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.match-slider.is-enhanced .match-slider__slide.is-active {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
pointer-events: auto;
|
||||
visibility: visible;
|
||||
transition: opacity 0.55s ease, transform 0.55s ease, visibility 0s;
|
||||
}
|
||||
|
||||
.match-slider__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.match-slider__controls[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__toggle {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0;
|
||||
color: var(--clr-text);
|
||||
background: var(--clr-glass-bg);
|
||||
border: 1px solid var(--clr-glass-border);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.match-slider__toggle[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__toggle:hover {
|
||||
background: rgb(255 255 255 / 0.2);
|
||||
}
|
||||
|
||||
.match-slider__toggle:focus-visible {
|
||||
outline: 2px solid var(--clr-text);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.match-slider__toggle-icon {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.match-slider__toggle .match-slider__toggle-icon--play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__toggle[aria-pressed="true"] .match-slider__toggle-icon--pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__toggle[aria-pressed="true"] .match-slider__toggle-icon--play {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.match-slider__dots {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.match-slider__dot {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 6px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--clr-glass-border);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.match-slider__dot:hover {
|
||||
background: rgb(255 255 255 / 0.35);
|
||||
}
|
||||
|
||||
.match-slider__dot:focus-visible {
|
||||
outline: 2px solid var(--clr-text);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.match-slider__progress {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--clr-accent);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.match-slider__dot.is-active .match-slider__progress {
|
||||
animation: ad-progress var(--slider-interval) linear forwards;
|
||||
}
|
||||
|
||||
.match-slider.is-paused .match-slider__dot.is-active .match-slider__progress {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.match-slider__dot.is-active .match-slider__progress {
|
||||
animation: none;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pro-Mannschaft-Sektion: Kopf mit Name/Liga + Link zur Mannschaftsseite. */
|
||||
.matchcenter-team__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.matchcenter-team__league {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
margin-top: 0.35rem;
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
/* kicker-Layout: Tabelle 2/3 links, Spiele 1/3 rechts; gestapelt auf Mobil.
|
||||
align-items:start + 0-Margins der Karten → beide Spalten starten oben bündig. */
|
||||
.matchcenter-team__grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: clamp(1.5rem, 1rem + 2vw, 2.75rem);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.matchcenter-team__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* min-width:0 erlaubt der Tabellenspalte, schmaler als die min-width:520 der
|
||||
Tabelle zu werden — sonst schiebt die Tabelle die ganze Seite in den
|
||||
horizontalen Scroll, statt selbst in ihrer Karte zu scrollen. */
|
||||
.matchcenter-team__grid > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Mehrere Listen (Nächste Spiele / Letzte Ergebnisse) gleichmäßig gestapelt.
|
||||
max-width hält die kompakten Zeilen gestapelt (≤992px) lesbar schmal, statt sie
|
||||
über die volle Breite zu ziehen; im 2-spaltigen Desktop greift es nicht (die
|
||||
1fr-Spalte ist ohnehin schmaler). */
|
||||
.matchcenter-team__aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
/* Kompakte Spielliste (Seitenspalte) — gleiche Karten-Optik wie die Tabelle. */
|
||||
.match-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: var(--clr-glass-bg);
|
||||
border: 1px solid var(--clr-glass-border);
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.match-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
padding: 0.7rem 0.9rem;
|
||||
border-bottom: 1px solid var(--clr-glass-border);
|
||||
}
|
||||
|
||||
.match-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.match-row__date {
|
||||
grid-column: 1 / -1;
|
||||
margin-bottom: 0.45rem;
|
||||
font-size: var(--fs-300);
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.match-row__teams {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
min-width: 0; /* erlaubt Ellipsis in den Namen */
|
||||
}
|
||||
|
||||
.match-row__team {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.match-row__team .crest,
|
||||
.match-row__team .crest--initials {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.match-row__name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--fs-400);
|
||||
}
|
||||
|
||||
.match-row__team.is-own .match-row__name {
|
||||
font-weight: var(--fw-bold);
|
||||
color: var(--clr-text);
|
||||
}
|
||||
|
||||
.match-row__outcome {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.match-row__score {
|
||||
font-family: var(--ff-heading);
|
||||
font-size: var(--fs-650);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.match-row__time {
|
||||
font-size: var(--fs-400);
|
||||
color: var(--clr-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Liga-Tabelle: horizontal scrollbar (fokussierbare Region) auf schmalen Screens. */
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border: 1px solid var(--clr-glass-border);
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.table-scroll:focus-visible {
|
||||
outline: 2px solid var(--clr-text);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.league-table {
|
||||
width: 100%;
|
||||
min-width: 520px;
|
||||
border-collapse: collapse;
|
||||
background: var(--clr-glass-bg);
|
||||
font-size: var(--fs-400);
|
||||
}
|
||||
|
||||
.league-table th,
|
||||
.league-table td {
|
||||
padding: 0.6rem 0.5rem;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--clr-glass-border);
|
||||
}
|
||||
|
||||
.league-table tbody tr:last-child th,
|
||||
.league-table tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* Spaltenköpfe in Abel (Coolvetica wäre hier zu klein, < 28px). */
|
||||
.league-table thead th {
|
||||
font-family: var(--ff-body);
|
||||
font-weight: var(--fw-bold);
|
||||
font-size: var(--fs-300);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--clr-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.league-table thead th abbr {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.league-table__rank {
|
||||
width: 2.75rem;
|
||||
border-left: 3px solid transparent;
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
/* Vereinsspalte (Kopf + Zellen) linksbündig — höhere Spezifität als .league-table th. */
|
||||
.league-table .league-table__club {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.league-table tbody th.league-table__club {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
min-width: 0; /* erlaubt Ellipsis im Namen */
|
||||
font-weight: var(--fw-regular);
|
||||
}
|
||||
|
||||
.league-table tbody .crest,
|
||||
.league-table tbody .crest--initials {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Lange Vereinsnamen sauber kürzen (voller Name im title-Attribut). */
|
||||
.league-table__name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.league-table__pts {
|
||||
font-weight: var(--fw-bold);
|
||||
color: var(--clr-text);
|
||||
}
|
||||
|
||||
/* Eigene Vereinszeile hervorheben (rote Tönung hinter weißem Text). */
|
||||
.league-table tbody tr.is-own {
|
||||
background: rgb(var(--clr-accent-rgb) / 0.16);
|
||||
}
|
||||
|
||||
.league-table tbody tr.is-own th,
|
||||
.league-table tbody tr.is-own td {
|
||||
font-weight: var(--fw-bold);
|
||||
color: var(--clr-text);
|
||||
}
|
||||
|
||||
/* Auf-/Abstiegszonen: farbiger linker Rand am Rang. */
|
||||
.league-table tbody tr.zone-promotion .league-table__rank {
|
||||
border-left-color: var(--clr-success);
|
||||
}
|
||||
|
||||
.league-table tbody tr.zone-promotion-playoff .league-table__rank {
|
||||
border-left-color: color-mix(in srgb, var(--clr-success) 60%, var(--clr-text-faint));
|
||||
}
|
||||
|
||||
.league-table tbody tr.zone-relegation-playoff .league-table__rank {
|
||||
border-left-color: color-mix(in srgb, var(--clr-error) 60%, var(--clr-text-faint));
|
||||
}
|
||||
|
||||
.league-table tbody tr.zone-relegation .league-table__rank {
|
||||
border-left-color: var(--clr-error);
|
||||
}
|
||||
|
||||
.league-table__legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 1.25rem;
|
||||
margin: 0.85rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: var(--fs-300);
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.league-table__legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45em;
|
||||
}
|
||||
|
||||
.league-table__legend-item::before {
|
||||
content: "";
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
border-radius: 3px;
|
||||
background: var(--clr-text-faint);
|
||||
}
|
||||
|
||||
.league-table__legend-item.zone-promotion::before {
|
||||
background: var(--clr-success);
|
||||
}
|
||||
|
||||
.league-table__legend-item.zone-promotion-playoff::before {
|
||||
background: color-mix(in srgb, var(--clr-success) 60%, var(--clr-text-faint));
|
||||
}
|
||||
|
||||
.league-table__legend-item.zone-relegation-playoff::before {
|
||||
background: color-mix(in srgb, var(--clr-error) 60%, var(--clr-text-faint));
|
||||
}
|
||||
|
||||
.league-table__legend-item.zone-relegation::before {
|
||||
background: var(--clr-error);
|
||||
}
|
||||
|
||||
/* Fallback ohne Cache. */
|
||||
.matchcenter-empty {
|
||||
max-width: 540px;
|
||||
margin-inline: auto;
|
||||
padding: clamp(1.5rem, 1rem + 2vw, 2.5rem);
|
||||
text-align: center;
|
||||
background: var(--clr-glass-bg);
|
||||
border: 1px solid var(--clr-glass-border);
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.matchcenter-empty__icon {
|
||||
font-size: 2.5rem;
|
||||
color: var(--clr-accent);
|
||||
}
|
||||
|
||||
.matchcenter-empty p {
|
||||
margin-top: 0.75rem;
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.matchcenter-empty__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user