feat(community): Datenschicht für Galerie, API und Veröffentlichen

- gallery.ts: Galerie aus Appwrite lesen (Abfragen für Liste, Suche, Cursor, Admin-Filter),
  getGalleryCard (404 und ungültige ID ergeben null), eigene Veröffentlichungen, Meldungen, Bilder
- api.ts: Aufrufe von aircard-api mit JWT (Cache je Nutzer, 10 Minuten, geleert bei Auth-Änderung,
  genau eine Wiederholung bei 401), ApiError mit code, Netz-/Proxy-Fehler als 503
- publish.ts: rendern, zwei Uploads in gallery-images, API; bei Fehler genau die zwei Dateien
  dieses Versuchs löschen; normalizeTags
- cards.ts: eigenständiges fetchFile(bucketId, fileId); CardsApi.fetchFile bleibt Hülle
- appwrite.ts: IDS für gallery, reports, gallery-images

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 14:39:30 +00:00
1 parent 7bbe0a9fe6
commit 89c86fd578
9 files changed
+1576 -7

No files matched your search

+8 -5
View File
@@ -47,7 +47,7 @@ export interface SaveInput {
export const DEFAULT_CARD_NAME = 'Meine Karte';
const NAME_MAX = 80;
/** Zeilen- und Datei-IDs von Appwrite: höchstens 36 Zeichen, kein Sonderzeichen am Anfang. */
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/;
export const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/;
/** Trimmt, fasst Leerraum zusammen und kürzt auf 80 Zeichen (Codepoints); leer ergibt „Meine Karte“. */
export function normalizeName(name: string): string {
@@ -193,11 +193,13 @@ export function createCardsService(api: CardsApi, userId: () => string) {
export type CardsService = ReturnType<typeof createCardsService>;
/** Lädt eine Bilddatei mit Projekt-Header und Cookie (siehe `client.prepareRequest`). */
async function fetchFile(fileId: string): Promise<Blob> {
/** Lädt eine Bilddatei aus einem Bucket mit Projekt-Header und Cookie (siehe `client.prepareRequest`). */
export async function fetchFile(bucketId: string, fileId: string): Promise<Blob> {
// Bewusst kein storage.getFileView(): das hängt das Projekt als Query-Parameter an die URL und schickt keinen
// Header; so trifft die Anfrage die Traefik-Route für /v1 mit Header nicht. prepareRequest setzt Header und Cookie.
const url = new URL(`${APPWRITE_ENDPOINT}/storage/buckets/${IDS.bucket}/files/${encodeURIComponent(fileId)}/view`);
const url = new URL(
`${APPWRITE_ENDPOINT}/storage/buckets/${encodeURIComponent(bucketId)}/files/${encodeURIComponent(fileId)}/view`,
);
const { uri, options } = client.prepareRequest('GET', url);
const res = await fetch(uri, options);
if (!res.ok) {
@@ -231,7 +233,8 @@ export function appwriteCardsApi(): CardsApi {
deleteFile: async (fileId) => {
await storage.deleteFile({ bucketId: IDS.bucket, fileId });
},
fetchFile,
// Die privaten Karten liegen im Karten-Bucket; die Galerie holt ihre Bilder über `fetchFile` mit eigenem Bucket.
fetchFile: (fileId) => fetchFile(IDS.bucket, fileId),
createRow: (data, permissions) =>
tables.createRow<TableRow>({ ...target, rowId: ID.unique(), data, permissions }),
// Beim Ändern bleiben die Rechte der Zeile unangetastet.