Improve CMS media uploads and admin styling
This commit is contained in:
@@ -24,6 +24,7 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0
|
||||
import { CalendarNavLink as CalendarNavLink_4d06fac03127b339306d5f5ecb073251 } from '../../../components/admin/CalendarNavLink'
|
||||
import { AnalyticsNavLink as AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962 } from '../../../components/admin/AnalyticsNavLink'
|
||||
import { ConversationsNavLink as ConversationsNavLink_67a2594cf28806bc76971e67212a6181 } from '../../../components/admin/ConversationsNavLink'
|
||||
import { UploadLimitGuard as UploadLimitGuard_1a82b0e63592dbfb4e9d3c5c946debf3 } from '../../../components/admin/UploadLimitGuard'
|
||||
import { AdminWelcome as AdminWelcome_b3c24bec21c663ad093f131aaae3d0a6 } from '../../../components/admin/AdminWelcome'
|
||||
import { AdminCalendarView as AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a } from '../../../components/admin/AdminCalendarView'
|
||||
import { AdminAnalyticsView as AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0 } from '../../../components/admin/AdminAnalyticsView'
|
||||
@@ -58,6 +59,7 @@ export const importMap = {
|
||||
"/components/admin/CalendarNavLink#CalendarNavLink": CalendarNavLink_4d06fac03127b339306d5f5ecb073251,
|
||||
"/components/admin/AnalyticsNavLink#AnalyticsNavLink": AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962,
|
||||
"/components/admin/ConversationsNavLink#ConversationsNavLink": ConversationsNavLink_67a2594cf28806bc76971e67212a6181,
|
||||
"/components/admin/UploadLimitGuard#UploadLimitGuard": UploadLimitGuard_1a82b0e63592dbfb4e9d3c5c946debf3,
|
||||
"/components/admin/AdminWelcome#AdminWelcome": AdminWelcome_b3c24bec21c663ad093f131aaae3d0a6,
|
||||
"/components/admin/AdminCalendarView#AdminCalendarView": AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a,
|
||||
"/components/admin/AdminAnalyticsView#AdminAnalyticsView": AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0,
|
||||
|
||||
+128
-9
@@ -18,6 +18,36 @@
|
||||
--font-mono: "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
/* Keep Payload's own color tokens aligned with the light admin surface. The
|
||||
previous theme could leave the content area pale while retaining white text
|
||||
from dark mode, which made headings and descriptions effectively invisible. */
|
||||
html,
|
||||
html[data-theme="light"],
|
||||
html[data-theme="dark"] {
|
||||
--theme-bg: var(--anouma-cream);
|
||||
--theme-input-bg: var(--anouma-paper);
|
||||
--theme-text: var(--anouma-ink);
|
||||
--theme-elevation-0: var(--anouma-paper);
|
||||
--theme-elevation-50: #fbf7f4;
|
||||
--theme-elevation-100: #f1e9e4;
|
||||
--theme-elevation-150: var(--anouma-line);
|
||||
--theme-elevation-200: #d7c9c1;
|
||||
--theme-elevation-300: #b9aaa8;
|
||||
--theme-elevation-400: #8b7b81;
|
||||
--theme-elevation-500: #776870;
|
||||
--theme-elevation-600: #66505f;
|
||||
--theme-elevation-700: #51404b;
|
||||
--theme-elevation-800: var(--anouma-ink);
|
||||
--theme-elevation-900: #211a1e;
|
||||
--theme-elevation-950: #171216;
|
||||
--theme-elevation-1000: #100c0f;
|
||||
--font-body: "Anouma Admin", Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: "SFMono-Regular", Consolas, monospace;
|
||||
background: var(--anouma-cream);
|
||||
color: var(--anouma-ink);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: var(--anouma-cream);
|
||||
@@ -33,6 +63,18 @@ select {
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
body,
|
||||
#app,
|
||||
.template-default,
|
||||
.template-default__wrap,
|
||||
.collection-list,
|
||||
.collection-list__wrap,
|
||||
.collection-edit,
|
||||
.dashboard,
|
||||
.dashboard__wrap {
|
||||
color: var(--anouma-ink);
|
||||
}
|
||||
|
||||
/* The left navigation is intentionally dark and quiet. Content stays light. */
|
||||
.nav {
|
||||
background: var(--anouma-ink);
|
||||
@@ -138,13 +180,56 @@ select {
|
||||
|
||||
.dashboard__header h1,
|
||||
.collection-list__header h1,
|
||||
.edit-view__header h1 {
|
||||
.edit-view__header h1,
|
||||
.list-header__title,
|
||||
.list-header__title h1,
|
||||
.document-fields__label {
|
||||
color: var(--anouma-ink);
|
||||
font-size: clamp(26px, 3vw, 38px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.list-header,
|
||||
.list-header__content,
|
||||
.collection-list__sub-header,
|
||||
.list-controls,
|
||||
.no-results,
|
||||
.no-results h3,
|
||||
.no-results p,
|
||||
.collection-list__table-wrap,
|
||||
.document-fields,
|
||||
.document-fields__main,
|
||||
.field-type,
|
||||
.field-type__wrap,
|
||||
.label-wrapper,
|
||||
.field-label,
|
||||
.field-label label,
|
||||
.field-description,
|
||||
.description {
|
||||
color: var(--anouma-ink);
|
||||
}
|
||||
|
||||
.list-header__title,
|
||||
.list-header__title a,
|
||||
.list-header__title button,
|
||||
.list-header__actions,
|
||||
.list-controls button,
|
||||
.list-controls a {
|
||||
color: var(--anouma-ink);
|
||||
}
|
||||
|
||||
.list-header__title-actions .btn--style-secondary,
|
||||
.list-header__actions .btn--style-secondary,
|
||||
.list-controls .btn--style-subtle {
|
||||
--color: var(--anouma-ink);
|
||||
--hover-color: var(--anouma-ink);
|
||||
--bg-color: var(--anouma-paper);
|
||||
--hover-bg: #f1e9e4;
|
||||
--btn-border: 1px solid var(--anouma-line);
|
||||
--hover-btn-border: 1px solid var(--anouma-mauve);
|
||||
}
|
||||
|
||||
.card,
|
||||
.dashboard__card,
|
||||
.table-wrap,
|
||||
@@ -167,6 +252,7 @@ select {
|
||||
}
|
||||
|
||||
.btn--style-primary {
|
||||
--color: #fff;
|
||||
--bg-color: var(--anouma-mauve);
|
||||
--hover-bg: var(--anouma-plum);
|
||||
}
|
||||
@@ -175,8 +261,10 @@ input,
|
||||
textarea,
|
||||
select,
|
||||
.rs__control {
|
||||
background: var(--anouma-paper) !important;
|
||||
border-color: #d7c9c1;
|
||||
border-radius: 4px;
|
||||
color: var(--anouma-ink) !important;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
@@ -198,6 +286,45 @@ select:focus,
|
||||
color: #7b6b72;
|
||||
}
|
||||
|
||||
.upload,
|
||||
.file-field,
|
||||
.upload-field-card,
|
||||
.upload__dropzoneContent,
|
||||
.dropzone,
|
||||
.bulk-upload--add-files,
|
||||
.bulk-upload--file-manager {
|
||||
color: var(--anouma-ink);
|
||||
}
|
||||
|
||||
.upload,
|
||||
.file-field,
|
||||
.bulk-upload--add-files,
|
||||
.bulk-upload--file-manager,
|
||||
.file-field__filename,
|
||||
.file-field__remote-file,
|
||||
.upload-field-card {
|
||||
background: var(--anouma-paper);
|
||||
border-color: var(--anouma-line);
|
||||
}
|
||||
|
||||
.upload__dragAndDropText,
|
||||
.upload__orText,
|
||||
.bulk-upload--add-files__dragAndDropText,
|
||||
.file-field__filename,
|
||||
.file-field__remote-file,
|
||||
.upload-field-card__filename,
|
||||
.upload-field-card__meta {
|
||||
color: var(--anouma-plum);
|
||||
}
|
||||
|
||||
.upload a,
|
||||
.upload button,
|
||||
.file-field button,
|
||||
.bulk-upload--add-files button,
|
||||
.bulk-upload--file-manager button {
|
||||
color: var(--anouma-ink);
|
||||
}
|
||||
|
||||
.table thead th,
|
||||
.table th {
|
||||
background: #f1e9e4;
|
||||
@@ -253,14 +380,6 @@ select:focus,
|
||||
color: var(--anouma-mauve);
|
||||
}
|
||||
|
||||
textarea,
|
||||
input,
|
||||
select,
|
||||
.rs__control {
|
||||
background: var(--anouma-paper) !important;
|
||||
color: var(--anouma-ink) !important;
|
||||
}
|
||||
|
||||
textarea::placeholder,
|
||||
input::placeholder {
|
||||
color: #8b7b81;
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Media: CollectionConfig = {
|
||||
admin: {
|
||||
group: "Inhalte",
|
||||
useAsTitle: "alt",
|
||||
description: "Bilder für die Website - Alt-Text ist für Barrierefreiheit und SEO Pflicht.",
|
||||
description: "Bilder für die Website. Alt-Text und Bildbeschreibung sind optional. Bis zu 100 Dateien pro Upload.",
|
||||
},
|
||||
access: {
|
||||
// Images are public so the website can display them without a login.
|
||||
@@ -25,6 +25,7 @@ export const Media: CollectionConfig = {
|
||||
},
|
||||
upload: {
|
||||
staticDir: path.resolve(dirname, "..", "media"),
|
||||
bulkUpload: true,
|
||||
// SVG deliberately excluded: it can carry inline <script>/event-handler
|
||||
// payloads that execute if the raw file is ever opened directly (not via
|
||||
// <img>) - not currently needed by any part of the site.
|
||||
@@ -42,9 +43,9 @@ export const Media: CollectionConfig = {
|
||||
name: "alt",
|
||||
type: "text",
|
||||
label: "Alt-Text",
|
||||
required: true,
|
||||
required: false,
|
||||
admin: {
|
||||
description: "Kurze Beschreibung des Bildes für Screenreader und Suchmaschinen.",
|
||||
description: "Optional. Kann später für Barrierefreiheit und SEO ergänzt werden.",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
const MAX_FILES_PER_UPLOAD = 100;
|
||||
|
||||
/** Keeps Payload's native multi-file picker from accepting an unbounded batch. */
|
||||
export function UploadLimitGuard() {
|
||||
useEffect(() => {
|
||||
const attached = new WeakSet<HTMLInputElement>();
|
||||
|
||||
const limitedTransfer = (files: FileList) => {
|
||||
const transfer = new DataTransfer();
|
||||
Array.from(files)
|
||||
.slice(0, MAX_FILES_PER_UPLOAD)
|
||||
.forEach((file) => transfer.items.add(file));
|
||||
return transfer;
|
||||
};
|
||||
|
||||
const attachToFileInputs = () => {
|
||||
document.querySelectorAll<HTMLInputElement>('input[type="file"][multiple]').forEach((input) => {
|
||||
if (attached.has(input)) return;
|
||||
attached.add(input);
|
||||
input.addEventListener("change", () => {
|
||||
if (!input.files || input.files.length <= MAX_FILES_PER_UPLOAD) return;
|
||||
|
||||
try {
|
||||
input.files = limitedTransfer(input.files).files;
|
||||
} catch {
|
||||
// Older browsers can expose a read-only FileList. Payload still
|
||||
// receives the original selection there and can process it.
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const limitDroppedFiles = (event: DragEvent) => {
|
||||
const files = event.dataTransfer?.files;
|
||||
if (!files || files.length <= MAX_FILES_PER_UPLOAD || !(event.target instanceof Element)) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
event.target.dispatchEvent(
|
||||
new DragEvent("drop", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer: limitedTransfer(files),
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// If a browser cannot construct a replacement drag event, the native
|
||||
// picker still enforces the same limit for normal file selection.
|
||||
}
|
||||
};
|
||||
|
||||
attachToFileInputs();
|
||||
document.addEventListener("drop", limitDroppedFiles, true);
|
||||
const observer = new MutationObserver(attachToFileInputs);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
return () => {
|
||||
document.removeEventListener("drop", limitDroppedFiles, true);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -17,5 +17,5 @@ export function mediaUrl(
|
||||
|
||||
export function mediaAlt(value: Media | number | null | undefined): string | undefined {
|
||||
if (!value || typeof value === "number") return undefined;
|
||||
return value.alt;
|
||||
return value.alt ?? undefined;
|
||||
}
|
||||
|
||||
+10
-10
@@ -199,7 +199,7 @@ export interface User {
|
||||
name: string;
|
||||
role: 'admin';
|
||||
/**
|
||||
* Steuert, ob dieser Zugang das Analytics-Dashboard im Admin-Bereich sehen kann.
|
||||
* Steuert, ob dieser Zugang die Auswertung im Admin-Bereich sehen kann.
|
||||
*/
|
||||
canViewAnalytics?: boolean | null;
|
||||
calendarFeedTokenHash?: string | null;
|
||||
@@ -261,7 +261,7 @@ export interface Customer {
|
||||
collection: 'customers';
|
||||
}
|
||||
/**
|
||||
* Bilder für die Website - Alt-Text ist für Barrierefreiheit und SEO Pflicht.
|
||||
* Bilder für die Website. Alt-Text und Bildbeschreibung können später ergänzt werden.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
@@ -269,9 +269,9 @@ export interface Customer {
|
||||
export interface Media {
|
||||
id: number;
|
||||
/**
|
||||
* Kurze Beschreibung des Bildes für Screenreader und Suchmaschinen.
|
||||
* Optional. Kann später für Barrierefreiheit und SEO ergänzt werden.
|
||||
*/
|
||||
alt: string;
|
||||
alt?: string | null;
|
||||
/**
|
||||
* Optionale, längere Beschreibung (z. B. für eine Bildunterschrift).
|
||||
*/
|
||||
@@ -652,7 +652,7 @@ export interface Conversation {
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Einzelne Nachrichten innerhalb einer Anfrage. Antworten aus dem CMS werden automatisch per E-Mail verschickt.
|
||||
* Einzelne Nachrichten innerhalb einer Anfrage. Antworten aus der Verwaltung werden automatisch per E-Mail verschickt.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "conversation-messages".
|
||||
@@ -661,7 +661,7 @@ export interface ConversationMessage {
|
||||
id: number;
|
||||
conversation: number | Conversation;
|
||||
/**
|
||||
* Für Antworten im CMS „Anouma“ auswählen. Eingehende E-Mails werden automatisch als Anfragende Person gespeichert.
|
||||
* Für Antworten aus der Verwaltung „Anouma“ auswählen. Eingehende E-Mails werden automatisch als anfragende Person gespeichert.
|
||||
*/
|
||||
senderType: 'visitor' | 'admin';
|
||||
senderName?: string | null;
|
||||
@@ -1624,7 +1624,7 @@ export interface SeoSetting {
|
||||
*/
|
||||
defaultDescription?: string | null;
|
||||
/**
|
||||
* Wird verwendet, wenn eine Seite kein eigenes Open-Graph-Bild hat.
|
||||
* Wird verwendet, wenn eine Seite kein eigenes Bild für soziale Netzwerke hat.
|
||||
*/
|
||||
defaultOgImage?: (number | null) | Media;
|
||||
/**
|
||||
@@ -1690,7 +1690,7 @@ export interface CookieSetting {
|
||||
label: string;
|
||||
description: string;
|
||||
/**
|
||||
* Globaler Schalter für das gesamte Analytics-System. Wenn deaktiviert, wird diese Kategorie im Banner nicht mehr angeboten und Analytics-Daten werden serverseitig abgelehnt - auch bei einer noch gespeicherten alten Zustimmung.
|
||||
* Globaler Schalter für die gesamte Reichweitenmessung. Wenn deaktiviert, wird diese Kategorie im Banner nicht mehr angeboten und Messdaten werden serverseitig abgelehnt - auch bei einer noch gespeicherten alten Zustimmung.
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
};
|
||||
@@ -1698,7 +1698,7 @@ export interface CookieSetting {
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* Aufbewahrungsdauer für Analytics-Rohdaten und aggregierte Tagesstatistiken.
|
||||
* Aufbewahrungsdauer für Rohdaten und zusammengefasste Tagesstatistiken.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "analytics-settings".
|
||||
@@ -1706,7 +1706,7 @@ export interface CookieSetting {
|
||||
export interface AnalyticsSetting {
|
||||
id: number;
|
||||
/**
|
||||
* Wie lange einzelne Analytics-Events gespeichert bleiben. Begrenzt auch den wählbaren Zeitraum im Analytics-Dashboard.
|
||||
* Wie lange einzelne Messwerte gespeichert bleiben. Begrenzt auch den wählbaren Zeitraum in der Auswertung.
|
||||
*/
|
||||
rawRetentionDays: number;
|
||||
/**
|
||||
|
||||
@@ -56,6 +56,7 @@ export default buildConfig({
|
||||
"/components/admin/CalendarNavLink#CalendarNavLink",
|
||||
"/components/admin/AnalyticsNavLink#AnalyticsNavLink",
|
||||
"/components/admin/ConversationsNavLink#ConversationsNavLink",
|
||||
"/components/admin/UploadLimitGuard#UploadLimitGuard",
|
||||
],
|
||||
views: {
|
||||
kalender: {
|
||||
|
||||
Reference in New Issue
Block a user