From 568b3f3abbb57cc3f87d43c13190106bc0fdead1 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sat, 28 Feb 2026 15:15:37 +0100 Subject: [PATCH] feat: merge Like+Favourite into single heart button, add Report modal with required reason & proof, fix favourite 422 (user_favorites -> artwork_favourites) --- .../Api/ArtworkInteractionController.php | 19 +- .../components/artwork/ArtworkActionBar.jsx | 237 +++++++++++++----- 2 files changed, 185 insertions(+), 71 deletions(-) diff --git a/app/Http/Controllers/Api/ArtworkInteractionController.php b/app/Http/Controllers/Api/ArtworkInteractionController.php index bc8a6d26..d063df88 100644 --- a/app/Http/Controllers/Api/ArtworkInteractionController.php +++ b/app/Http/Controllers/Api/ArtworkInteractionController.php @@ -20,11 +20,11 @@ final class ArtworkInteractionController extends Controller $this->toggleSimple( request: $request, - table: 'user_favorites', + table: 'artwork_favourites', keyColumns: ['user_id', 'artwork_id'], keyValues: ['user_id' => (int) $request->user()->id, 'artwork_id' => $artworkId], - insertPayload: ['created_at' => now()], - requiredTable: 'user_favorites' + insertPayload: ['created_at' => now(), 'updated_at' => now()], + requiredTable: 'artwork_favourites' ); $this->syncArtworkStats($artworkId); @@ -154,8 +154,8 @@ final class ArtworkInteractionController extends Controller return; } - $favorites = Schema::hasTable('user_favorites') - ? (int) DB::table('user_favorites')->where('artwork_id', $artworkId)->count() + $favorites = Schema::hasTable('artwork_favourites') + ? (int) DB::table('artwork_favourites')->where('artwork_id', $artworkId)->count() : 0; $likes = Schema::hasTable('artwork_likes') @@ -167,23 +167,22 @@ final class ArtworkInteractionController extends Controller [ 'favorites' => $favorites, 'rating_count' => $likes, - 'updated_at' => now(), ] ); } private function statusPayload(int $viewerId, int $artworkId): array { - $isFavorited = Schema::hasTable('user_favorites') - ? DB::table('user_favorites')->where('user_id', $viewerId)->where('artwork_id', $artworkId)->exists() + $isFavorited = Schema::hasTable('artwork_favourites') + ? DB::table('artwork_favourites')->where('user_id', $viewerId)->where('artwork_id', $artworkId)->exists() : false; $isLiked = Schema::hasTable('artwork_likes') ? DB::table('artwork_likes')->where('user_id', $viewerId)->where('artwork_id', $artworkId)->exists() : false; - $favorites = Schema::hasTable('user_favorites') - ? (int) DB::table('user_favorites')->where('artwork_id', $artworkId)->count() + $favorites = Schema::hasTable('artwork_favourites') + ? (int) DB::table('artwork_favourites')->where('artwork_id', $artworkId)->count() : 0; $likes = Schema::hasTable('artwork_likes') diff --git a/resources/js/components/artwork/ArtworkActionBar.jsx b/resources/js/components/artwork/ArtworkActionBar.jsx index b961d720..98a9ad87 100644 --- a/resources/js/components/artwork/ArtworkActionBar.jsx +++ b/resources/js/components/artwork/ArtworkActionBar.jsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' function formatCount(value) { const n = Number(value || 0) @@ -64,17 +65,147 @@ function FlagIcon() { ) } +/* ── Report Modal ──────────────────────────────────────────────────────────── */ +const REPORT_REASONS = [ + 'Inappropriate content', + 'Copyright violation', + 'Spam or misleading', + 'Offensive or abusive', +] + +function ReportModal({ open, onClose, onSubmit, submitting }) { + const [selected, setSelected] = useState('') + const [details, setDetails] = useState('') + const backdropRef = useRef(null) + const inputRef = useRef(null) + + // Reset & focus when opening + useEffect(() => { + if (open) { + setSelected('') + setDetails('') + const t = setTimeout(() => inputRef.current?.focus(), 80) + return () => clearTimeout(t) + } + }, [open]) + + // Close on Escape + useEffect(() => { + if (!open) return + const handler = (e) => { if (e.key === 'Escape') onClose() } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [open, onClose]) + + if (!open) return null + + const trimmedDetails = details.trim() + const canSubmit = selected.length > 0 && trimmedDetails.length >= 10 && !submitting + const fullReason = `${selected}: ${trimmedDetails}` + + return createPortal( +
{ if (e.target === backdropRef.current) onClose() }} + className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4" + > +
+ {/* Header */} +
+

Report Artwork

+ +
+ + {/* Body */} +
+ {/* Step 1 — pick a reason */} +
+ +
+ {REPORT_REASONS.map((r) => ( + + ))} +
+
+ + {/* Step 2 — describe & prove */} +
+ +