From 2fd71fd406beac359c695872475b40f23511667d Mon Sep 17 00:00:00 2001 From: Matthias G Date: Sat, 1 Aug 2026 12:25:01 +0200 Subject: [PATCH] Number entries on publish, delete media, read the cover - drafts stay unnumbered, the number is handed out when an entry goes live - DELETE /api/v1/media/:id and a delete control in the media library - cover_media_id in the draft read, coverMediaId and coverUrl in the archive read --- docs/api.md | 31 +- drizzle/0010_steady_wither.sql | 2 + drizzle/meta/0010_snapshot.json | 1899 ++++++++++++++++++++ drizzle/meta/_journal.json | 7 + messages/de.json | 9 +- messages/en.json | 9 +- src/app/api/v1/media/[id]/route.ts | 33 + src/app/api/v1/posts/[slug]/route.ts | 3 +- src/app/api/v1/posts/route.ts | 3 +- src/components/admin/EntryEditor.tsx | 5 +- src/components/admin/MediaCard.tsx | 3 + src/components/admin/MediaDeleteButton.tsx | 53 + src/components/archive/PostDetail.tsx | 4 +- src/components/ui/Plate.tsx | 8 +- src/components/ui/Watermark.tsx | 6 +- src/data/repositories/archive.ts | 2 +- src/data/repositories/entries.ts | 61 +- src/data/repositories/media.ts | 18 +- src/data/repositories/posts.ts | 4 +- src/data/repositories/reads.ts | 2 +- src/data/schema/posts.ts | 2 +- src/lib/api-covers.ts | 22 + src/lib/api-entries.ts | 4 +- src/lib/entry-types.ts | 2 +- src/lib/media-actions.ts | 25 + src/lib/media-delete.ts | 36 + src/lib/openapi.ts | 16 +- tests/api/write.test.ts | 2 +- tests/data/entries.test.ts | 77 +- tests/lib/entries.test.ts | 12 +- tests/scripts/seed.test.ts | 2 +- 31 files changed, 2286 insertions(+), 76 deletions(-) create mode 100644 drizzle/0010_steady_wither.sql create mode 100644 drizzle/meta/0010_snapshot.json create mode 100644 src/app/api/v1/media/[id]/route.ts create mode 100644 src/components/admin/MediaDeleteButton.tsx create mode 100644 src/lib/api-covers.ts create mode 100644 src/lib/media-delete.ts diff --git a/docs/api.md b/docs/api.md index 9cf463e..a6bcb2f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,6 +1,6 @@ # Logbuch API -Stand: 31.07.2026. Diese Datei beschreibt den tatsächlichen Stand. Was noch nicht gebaut ist, steht am Ende unter "Noch nicht vorhanden" und ist dort auch so gekennzeichnet. +Stand: 01.08.2026. Diese Datei beschreibt den tatsächlichen Stand der Schnittstelle. ## Wofür Logbuch da ist @@ -73,7 +73,7 @@ curl -s -H "Authorization: Bearer $TOKEN" \ Antwort: `{ "items": [...], "meta": { "total", "page", "per_page" } }` -Ein Eintrag in der Liste trägt: `id`, `slug`, `title`, `teaser`, `type`, `audience`, `publishAt`, `number`, `projectSlug`, `projectName`, `projectCode`, `projectColor`. +Ein Eintrag in der Liste trägt: `id`, `slug`, `title`, `teaser`, `type`, `audience`, `publishAt`, `number`, `coverMediaId`, `coverUrl`, `projectSlug`, `projectName`, `projectCode`, `projectColor`. ### GET /api/v1/posts/:slug @@ -123,6 +123,15 @@ curl -s -X POST -H "Authorization: Bearer $TOKEN" \ Grenzen: nur Bildformate, höchstens 15 MB. +### DELETE /api/v1/media/:id + +Löscht ein Bild samt seiner Fassungen. Hängt es noch an einem Beitrag, als Aufmacher oder in einem Block, kommt 409 zurück. + +```bash +curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \ + http://localhost:4700/api/v1/media/ +``` + ### GET /api/v1/media/file/... Liefert eine Variante aus. Die Pfade stehen am Medium unter `variants`, je Eintrag mit `width`, `format` und `path`. @@ -153,23 +162,9 @@ Wer über die API schreibt, schreibt für Menschen. Diese Regeln gelten für jed **Wer veröffentlicht.** Nicht der Zugang. Ein Mensch schaut drauf und gibt frei. Ein über die API verfasster Beitrag ist immer ein Vorschlag. -## Noch nicht vorhanden +## Nummern -Ehrlich benannt, damit niemand daran vorbei entwickelt. Diese Endpunkte werden gerade gebaut: - -| Geplant | Zweck | -|---|---| -| `POST /api/v1/posts` | Beitrag als Entwurf anlegen | -| `PATCH /api/v1/posts/:id` | Entwurf ändern | -| `PUT /api/v1/posts/:id/blocks` | Inhaltsblöcke setzen | -| `GET /api/v1/posts/:id/preview` | Entwurf zurücklesen | -| `DELETE /api/v1/posts/:id` | Entwurf löschen | -| `GET /api/v1/post-types` | verwaltbare Beitragsarten | -| `GET /api/v1/style-guide` | dieser Leitfaden, maschinenlesbar | -| `GET /api/v1/openapi.json` | maschinenlesbare Beschreibung | -| `/admin/api` | diese Dokumentation im Adminbereich | - -Solange sie fehlen, kann ein Zugang mit Schreibrecht ausschließlich Bilder hochladen. +Die Nummer eines Eintrags wird beim Veröffentlichen vergeben, nicht beim Anlegen. Ein Entwurf hat `number: null`. Damit bleiben die Nummern im Archiv lückenlos, auch wenn Entwürfe wieder verworfen werden. ## Blocktypen diff --git a/drizzle/0010_steady_wither.sql b/drizzle/0010_steady_wither.sql new file mode 100644 index 0000000..3d1fc57 --- /dev/null +++ b/drizzle/0010_steady_wither.sql @@ -0,0 +1,2 @@ +ALTER TABLE "post" ALTER COLUMN "number" DROP NOT NULL;--> statement-breakpoint +UPDATE "post" SET "number" = NULL WHERE "status" = 'draft'; \ No newline at end of file diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..a591c02 --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,1899 @@ +{ + "id": "560c398a-4ec6-478e-99c6-89374eef5d35", + "prevId": "12dc209e-f668-4e06-85fb-442148784ce7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_type": { + "name": "actor_type", + "type": "audit_actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_label": { + "name": "actor_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity": { + "name": "entity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_created": { + "name": "audit_log_created", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_entity": { + "name": "audit_log_entity", + "columns": [ + { + "expression": "entity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_user": { + "name": "account_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_user": { + "name": "session_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_project_role": { + "name": "user_project_role", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "project_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'moderator'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_project_role_project": { + "name": "user_project_role_project", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_project_role_user_id_user_id_fk": { + "name": "user_project_role_user_id_user_id_fk", + "tableFrom": "user_project_role", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_project_role_project_id_project_id_fk": { + "name": "user_project_role_project_id_project_id_fk", + "tableFrom": "user_project_role", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_project_role_user_id_project_id_pk": { + "name": "user_project_role_user_id_project_id_pk", + "columns": [ + "user_id", + "project_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier": { + "name": "verification_identifier", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.brand": { + "name": "brand", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#191c20'" + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "brand_slug_unique": { + "name": "brand_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "brand_id": { + "name": "brand_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#191c20'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_brand_id_brand_id_fk": { + "name": "project_brand_id_brand_id_fk", + "tableFrom": "project", + "tableTo": "brand", + "columnsFrom": [ + "brand_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_slug_unique": { + "name": "project_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_counter": { + "name": "post_counter", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "post_counter_project_id_project_id_fk": { + "name": "post_counter_project_id_project_id_fk", + "tableFrom": "post_counter", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "media_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'image'" + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime": { + "name": "mime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "variant_error": { + "name": "variant_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_project_created": { + "name": "media_project_created", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_project_id_project_id_fk": { + "name": "media_project_id_project_id_fk", + "tableFrom": "media", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "media_uploaded_by_user_id_fk": { + "name": "media_uploaded_by_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_type": { + "name": "post_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label_de": { + "name": "label_de", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label_en": { + "name": "label_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#191c20'" + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "post_type_sort": { + "name": "post_type_sort", + "columns": [ + { + "expression": "sort", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "post_type_key_unique": { + "name": "post_type_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.block": { + "name": "block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "block_post_sort": { + "name": "block_post_sort", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "block_post_id_post_id_fk": { + "name": "block_post_id_post_id_fk", + "tableFrom": "block", + "tableTo": "post", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue": { + "name": "issue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_from": { + "name": "period_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "period_to": { + "name": "period_to", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "issue_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "publish_at": { + "name": "publish_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "issue_project_id_project_id_fk": { + "name": "issue_project_id_project_id_fk", + "tableFrom": "issue", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_tag": { + "name": "post_tag", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "post_tag_post_id_post_id_fk": { + "name": "post_tag_post_id_post_id_fk", + "tableFrom": "post_tag", + "tableTo": "post", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_tag_tag_id_tag_id_fk": { + "name": "post_tag_tag_id_tag_id_fk", + "tableFrom": "post_tag", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "post_tag_post_id_tag_id_pk": { + "name": "post_tag_post_id_tag_id_pk", + "columns": [ + "post_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post": { + "name": "post", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "teaser": { + "name": "teaser", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type_id": { + "name": "type_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "post_type_default()" + }, + "audience": { + "name": "audience", + "type": "post_audience", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "status": { + "name": "status", + "type": "post_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "locale": { + "name": "locale", + "type": "post_locale", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'de'" + }, + "translation_group": { + "name": "translation_group", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "publish_at": { + "name": "publish_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_media_id": { + "name": "cover_media_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "post_publish_at": { + "name": "post_publish_at", + "columns": [ + { + "expression": "publish_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_project_status": { + "name": "post_project_status", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_project_id_project_id_fk": { + "name": "post_project_id_project_id_fk", + "tableFrom": "post", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_issue_id_issue_id_fk": { + "name": "post_issue_id_issue_id_fk", + "tableFrom": "post", + "tableTo": "issue", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "post_type_id_post_type_id_fk": { + "name": "post_type_id_post_type_id_fk", + "tableFrom": "post", + "tableTo": "post_type", + "columnsFrom": [ + "type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "post_cover_media_id_media_id_fk": { + "name": "post_cover_media_id_media_id_fk", + "tableFrom": "post", + "tableTo": "media", + "columnsFrom": [ + "cover_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "post_project_slug": { + "name": "post_project_slug", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "slug" + ] + }, + "post_project_number": { + "name": "post_project_number", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "tag_project_id_project_id_fk": { + "name": "tag_project_id_project_id_fk", + "tableFrom": "tag", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tag_project_slug": { + "name": "tag_project_slug", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_client": { + "name": "api_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "client_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'read'" + }, + "scope": { + "name": "scope", + "type": "client_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'customer'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "can_publish": { + "name": "can_publish", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_client_project_id_project_id_fk": { + "name": "api_client_project_id_project_id_fk", + "tableFrom": "api_client", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_client_token_hash_unique": { + "name": "api_client_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_read": { + "name": "post_read", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_user_id": { + "name": "external_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "post_read_project_user": { + "name": "post_read_project_user", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_read_post_id_post_id_fk": { + "name": "post_read_post_id_post_id_fk", + "tableFrom": "post_read", + "tableTo": "post", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_read_project_id_project_id_fk": { + "name": "post_read_project_id_project_id_fk", + "tableFrom": "post_read", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "post_read_post_id_external_user_id_pk": { + "name": "post_read_post_id_external_user_id_pk", + "columns": [ + "post_id", + "external_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_idempotency_key": { + "name": "api_idempotency_key", + "schema": "", + "columns": { + "client_id": { + "name": "client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_idempotency_key_client_id_api_client_id_fk": { + "name": "api_idempotency_key_client_id_api_client_id_fk", + "tableFrom": "api_idempotency_key", + "tableTo": "api_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_idempotency_key_post_id_post_id_fk": { + "name": "api_idempotency_key_post_id_post_id_fk", + "tableFrom": "api_idempotency_key", + "tableTo": "post", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_idempotency_key_client_id_key_pk": { + "name": "api_idempotency_key_client_id_key_pk", + "columns": [ + "client_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.audit_actor_type": { + "name": "audit_actor_type", + "schema": "public", + "values": [ + "user", + "client" + ] + }, + "public.project_role": { + "name": "project_role", + "schema": "public", + "values": [ + "moderator" + ] + }, + "public.media_kind": { + "name": "media_kind", + "schema": "public", + "values": [ + "image", + "video" + ] + }, + "public.issue_status": { + "name": "issue_status", + "schema": "public", + "values": [ + "draft", + "scheduled", + "published" + ] + }, + "public.post_audience": { + "name": "post_audience", + "schema": "public", + "values": [ + "internal", + "customer", + "public" + ] + }, + "public.post_locale": { + "name": "post_locale", + "schema": "public", + "values": [ + "de", + "en" + ] + }, + "public.post_status": { + "name": "post_status", + "schema": "public", + "values": [ + "draft", + "review", + "scheduled", + "published", + "archived" + ] + }, + "public.client_mode": { + "name": "client_mode", + "schema": "public", + "values": [ + "read", + "write" + ] + }, + "public.client_scope": { + "name": "client_scope", + "schema": "public", + "values": [ + "internal", + "customer", + "public" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 44430dd..163a177 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1785502444366, "tag": "0009_clean_mac_gargan", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1785579010161, + "tag": "0010_steady_wither", + "breakpoints": true } ] } \ No newline at end of file diff --git a/messages/de.json b/messages/de.json index 770e752..90ff186 100644 --- a/messages/de.json +++ b/messages/de.json @@ -703,7 +703,10 @@ "card": { "meta": "{width} × {height} · {size} KB · {variants, plural, =0 {keine Fassung} one {# Fassung} other {# Fassungen}}", "altMissing": "Ohne Alt-Text. Für öffentliche Beiträge ist er Pflicht.", - "variantError": "Die Fassungen konnten nicht erzeugt werden. Das Original ist gespeichert und nutzbar." + "variantError": "Die Fassungen konnten nicht erzeugt werden. Das Original ist gespeichert und nutzbar.", + "delete": "Bild löschen", + "deleteConfirm": "Wirklich löschen?", + "keep": "Behalten" }, "messages": { "uploaded": "Das Bild ist hochgeladen und die Fassungen sind erzeugt.", @@ -714,7 +717,9 @@ "unreadableImage": "Die Datei lässt sich nicht als Bild lesen. Prüfe, ob sie vollständig ist.", "projectUnknown": "Dieses Projekt gibt es nicht.", "mediaUnknown": "Dieses Bild gibt es nicht mehr.", - "uploadFailed": "Das Hochladen hat gerade nicht geklappt. Versuch es in einem Moment noch einmal." + "uploadFailed": "Das Hochladen hat gerade nicht geklappt. Versuch es in einem Moment noch einmal.", + "deleted": "Das Bild ist gelöscht.", + "mediaInUse": "Das Bild hängt noch an einem Beitrag. Nimm es dort zuerst heraus." } }, "mail": { diff --git a/messages/en.json b/messages/en.json index 2850c01..3fbb885 100644 --- a/messages/en.json +++ b/messages/en.json @@ -703,7 +703,10 @@ "card": { "meta": "{width} × {height} · {size} KB · {variants, plural, =0 {no rendition} one {# rendition} other {# renditions}}", "altMissing": "No alt text. It is mandatory for public entries.", - "variantError": "The renditions could not be created. The original is stored and usable." + "variantError": "The renditions could not be created. The original is stored and usable.", + "delete": "Delete image", + "deleteConfirm": "Really delete?", + "keep": "Keep" }, "messages": { "uploaded": "The image is uploaded and the renditions are created.", @@ -714,7 +717,9 @@ "unreadableImage": "The file cannot be read as an image. Check whether it is complete.", "projectUnknown": "This project does not exist.", "mediaUnknown": "This image no longer exists.", - "uploadFailed": "Uploading did not work just now. Try again in a moment." + "uploadFailed": "Uploading did not work just now. Try again in a moment.", + "deleted": "The image is deleted.", + "mediaInUse": "The image is still used in an entry. Remove it there first." } }, "mail": { diff --git a/src/app/api/v1/media/[id]/route.ts b/src/app/api/v1/media/[id]/route.ts new file mode 100644 index 0000000..5e780ba --- /dev/null +++ b/src/app/api/v1/media/[id]/route.ts @@ -0,0 +1,33 @@ +import { findMedia } from '~/data/repositories/media' +import { resolveClient } from '~/lib/api-auth' +import { removeMedia } from '~/lib/media-delete' +import { problem } from '~/lib/problem' + +export async function DELETE(request: Request, context: { params: Promise<{ id: string }> }) { + const client = await resolveClient(request) + + if (!client) { + return problem(401, 'unauthorized') + } + + if (client.mode !== 'write') { + return problem(403, 'forbidden', 'Dieses Token darf nur lesen.') + } + + const { id } = await context.params + const item = await findMedia(id) + + if (!item || (client.projectId !== null && item.projectId !== client.projectId)) { + return problem(404, 'media_not_found') + } + + const result = await removeMedia(id, item.projectId) + + if (!result.ok) { + return result.reason === 'in_use' + ? problem(409, 'media_in_use', 'Das Bild hängt noch an einem Beitrag.') + : problem(404, 'media_not_found') + } + + return Response.json({ deleted: id }) +} diff --git a/src/app/api/v1/posts/[slug]/route.ts b/src/app/api/v1/posts/[slug]/route.ts index 4274eb8..c6ace22 100644 --- a/src/app/api/v1/posts/[slug]/route.ts +++ b/src/app/api/v1/posts/[slug]/route.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import { findPost, findPostIdBySlug } from '~/data/repositories/posts' import { resolveClient } from '~/lib/api-auth' +import { withCover } from '~/lib/api-covers' import { apiDeleteEntry, apiReadEntry, apiUpdateEntry } from '~/lib/api-entries' import { entryProblem } from '~/lib/api-problem' import { problem } from '~/lib/problem' @@ -34,7 +35,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s const post = await findPost(slug, client.scope, client.projectId ?? undefined) if (post) { - return Response.json(post) + return Response.json(await withCover(post)) } const id = await findPostIdBySlug(slug) diff --git a/src/app/api/v1/posts/route.ts b/src/app/api/v1/posts/route.ts index 2275091..27dee22 100644 --- a/src/app/api/v1/posts/route.ts +++ b/src/app/api/v1/posts/route.ts @@ -4,6 +4,7 @@ import { isPostTypeKey } from '~/domain/post-type' import { apiCreateEntry } from '~/lib/api-entries' import { entryProblem } from '~/lib/api-problem' import { resolveClient } from '~/lib/api-auth' +import { withCovers } from '~/lib/api-covers' import { problem } from '~/lib/problem' const querySchema = z.object({ @@ -42,7 +43,7 @@ export async function GET(request: Request) { }) return Response.json({ - items: result.items, + items: await withCovers(result.items), meta: { total: result.total, page: query.page, per_page: query.per_page }, }) } diff --git a/src/components/admin/EntryEditor.tsx b/src/components/admin/EntryEditor.tsx index d16f0e3..0ef5df7 100644 --- a/src/components/admin/EntryEditor.tsx +++ b/src/components/admin/EntryEditor.tsx @@ -35,6 +35,7 @@ import { textareaClass, } from './styles' import { Select } from './Select' +import { entryMark } from '~/components/ui/Plate' import { blockTypes, emptyBlockData, isBlockEmpty, type BlockType, type BlockValues } from '~/domain/blocks' import { checkPublish } from '~/domain/publish-checks' import { defaultPostTypeColor } from '~/domain/post-type' @@ -64,7 +65,7 @@ export type EntryEditorProject = { export type EntryEditorEntry = { id: string - number: number + number: number | null slug: string status: PostStatus publishAt: string | null @@ -498,7 +499,7 @@ export function EntryEditor({ projects, types, media: initialMedia, accept, defa {status ? ( - {project ? `${project.code}-${String(status.number).padStart(4, '0')}` : status.number} + {entryMark({ code: project?.code ?? null, slug: project?.slug ?? '' }, status.number)} ) : null} diff --git a/src/components/admin/MediaCard.tsx b/src/components/admin/MediaCard.tsx index 5cd11d4..5e16906 100644 --- a/src/components/admin/MediaCard.tsx +++ b/src/components/admin/MediaCard.tsx @@ -1,6 +1,7 @@ import Image from 'next/image' import { getTranslations } from 'next-intl/server' import { TbAlertTriangle } from 'react-icons/tb' +import { MediaDeleteButton } from './MediaDeleteButton' import { MediaMetaForm } from './MediaMetaForm' import { metaClass } from './styles' import { hasAlt, mediaAlt, mediaThumbnail } from '~/lib/media-url' @@ -58,6 +59,8 @@ export async function MediaCard({ item, project }: MediaCardProps) { )} + + ) diff --git a/src/components/admin/MediaDeleteButton.tsx b/src/components/admin/MediaDeleteButton.tsx new file mode 100644 index 0000000..c7d432d --- /dev/null +++ b/src/components/admin/MediaDeleteButton.tsx @@ -0,0 +1,53 @@ +'use client' + +import { useActionState, useState } from 'react' +import { useTranslations } from 'next-intl' +import { TbTrash } from 'react-icons/tb' +import { AdminSubmit } from './AdminSubmit' +import { MediaMessage } from './MediaMessage' +import { deleteMediaAction } from '~/lib/media-actions' +import { emptyAdminFormState, type AdminFormState } from '~/lib/admin-forms' + +export type MediaDeleteButtonProps = { + mediaId: string + project: string +} + +export function MediaDeleteButton({ mediaId, project }: MediaDeleteButtonProps) { + const t = useTranslations('media') + const [asking, setAsking] = useState(false) + const [state, formAction] = useActionState(deleteMediaAction, emptyAdminFormState) + + return ( +
+ + + + + + {asking ? ( + + }> + {t('card.deleteConfirm')} + + + + ) : ( + + )} + + ) +} diff --git a/src/components/archive/PostDetail.tsx b/src/components/archive/PostDetail.tsx index 435799c..b9d3869 100644 --- a/src/components/archive/PostDetail.tsx +++ b/src/components/archive/PostDetail.tsx @@ -132,7 +132,9 @@ export function PostDetail({ detail }: PostDetailProps) { {media.length > 0 ? ( {entry('images', { count: media.length })} ) : null} - {entryNumber(post.number)} + {post.number === null ? null : ( + {entryNumber(post.number)} + )}
= { @@ -51,7 +51,7 @@ export function Plate({ project, color, number, size = 'row', showNumber = true, className={`${projectSurface} ${shells[size]} font-mono font-semibold text-paper ${className ?? ''}`} > {projectCode(project)} - {showNumber ? ( + {showNumber && number !== null ? ( {entryNumber(number)} ) : null} diff --git a/src/components/ui/Watermark.tsx b/src/components/ui/Watermark.tsx index 5263d7b..88a0c00 100644 --- a/src/components/ui/Watermark.tsx +++ b/src/components/ui/Watermark.tsx @@ -1,11 +1,15 @@ import { entryNumber } from './Plate' export type WatermarkProps = { - number: number + number: number | null className?: string } export function Watermark({ number, className }: WatermarkProps) { + if (number === null) { + return null + } + return (