MediaGalleryModal.jsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import React, { useState, useEffect } from "react";
  2. import { API_BASE } from "../config";
  3. function MediaGalleryModal({ isOpen, onClose, onSelect, slug }) {
  4. const [images, setImages] = useState([]);
  5. const [loading, setLoading] = useState(false);
  6. const [uploading, setUploading] = useState(false);
  7. const [error, setError] = useState(null);
  8. useEffect(() => {
  9. if (isOpen) {
  10. fetchMedia();
  11. }
  12. }, [isOpen, slug]);
  13. const fetchMedia = async () => {
  14. try {
  15. setLoading(true);
  16. const url = slug
  17. ? `${API_BASE}/media?slug=${encodeURIComponent(slug)}`
  18. : `${API_BASE}/media`;
  19. const response = await fetch(url, { credentials: "include" });
  20. if (!response.ok) throw new Error("Failed to load media");
  21. const data = await response.json();
  22. setImages(data);
  23. } catch (err) {
  24. console.error("Fetch media error:", err);
  25. setError("Could not load images.");
  26. } finally {
  27. setLoading(false);
  28. }
  29. };
  30. const handleUpload = async (e) => {
  31. const file = e.target.files[0];
  32. if (!file) return;
  33. try {
  34. setUploading(true);
  35. const formData = new FormData();
  36. formData.append("file", file);
  37. const uploadUrl = slug
  38. ? `${API_BASE}/upload?slug=${encodeURIComponent(slug)}`
  39. : `${API_BASE}/upload`;
  40. const response = await fetch(uploadUrl, {
  41. method: "POST",
  42. body: formData,
  43. credentials: "include",
  44. });
  45. if (!response.ok) throw new Error("Upload failed");
  46. await fetchMedia(); // Refresh list
  47. } catch (err) {
  48. setError(err.message);
  49. } finally {
  50. setUploading(false);
  51. }
  52. };
  53. const handleDelete = async (path, e) => {
  54. e.stopPropagation();
  55. if (!window.confirm("Delete this image?")) return;
  56. try {
  57. const response = await fetch(`${API_BASE}/media`, {
  58. method: "DELETE",
  59. headers: { "Content-Type": "application/json" },
  60. body: JSON.stringify({ path }),
  61. credentials: "include",
  62. });
  63. if (!response.ok) throw new Error("Delete failed");
  64. await fetchMedia();
  65. } catch (err) {
  66. alert(err.message);
  67. }
  68. };
  69. if (!isOpen) return null;
  70. return (
  71. <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
  72. <div className="theme-surface rounded-xl shadow-2xl w-full max-w-4xl max-h-[80vh] flex flex-col overflow-hidden theme-border border">
  73. {/* Header */}
  74. <div className="px-6 py-4 border-b theme-border flex justify-between items-center bg-gray-50/50">
  75. <h3 className="text-lg font-bold theme-text">Media Gallery</h3>
  76. <div className="flex items-center space-x-4">
  77. <label className="cursor-pointer btn-theme-primary text-white px-4 py-2 rounded-lg transition-colors text-sm font-medium">
  78. {uploading ? "Uploading..." : "Upload Image"}
  79. <input
  80. type="file"
  81. className="hidden"
  82. accept="image/*"
  83. onChange={handleUpload}
  84. disabled={uploading}
  85. />
  86. </label>
  87. <button
  88. onClick={onClose}
  89. className="text-gray-500 hover:text-gray-700 theme-text-secondary hover:theme-text"
  90. >
  91. <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
  92. </button>
  93. </div>
  94. </div>
  95. {/* Content */}
  96. <div className="p-6 overflow-y-auto flex-grow theme-bg">
  97. {error && (
  98. <div className="mb-4 p-3 bg-red-100 text-red-700 rounded-lg">
  99. {error}
  100. </div>
  101. )}
  102. {loading ? (
  103. <div className="flex justify-center py-12">
  104. <div className="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"></div>
  105. </div>
  106. ) : images.length === 0 ? (
  107. <div className="text-center py-12 theme-text-secondary">
  108. No images found in this folder.
  109. </div>
  110. ) : (
  111. <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
  112. {images.map((img) => (
  113. <div
  114. key={img.name}
  115. className="group relative theme-surface rounded-lg shadow-sm border theme-border overflow-hidden hover:shadow-md transition-shadow cursor-pointer aspect-square"
  116. onClick={() => onSelect(`${API_BASE}${img.url}`, img.name)}
  117. >
  118. <img
  119. src={`${API_BASE}${img.url}`}
  120. alt={img.name}
  121. className="w-full h-full object-cover"
  122. loading="lazy"
  123. />
  124. <div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors" />
  125. {/* Delete Button */}
  126. <button
  127. onClick={(e) => handleDelete(img.url, e)}
  128. className="absolute top-2 right-2 p-1.5 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-600"
  129. title="Delete"
  130. >
  131. <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
  132. </button>
  133. {/* Name Label */}
  134. <div className="absolute bottom-0 left-0 right-0 bg-black/60 text-white text-xs p-2 truncate">
  135. {img.name}
  136. </div>
  137. </div>
  138. ))}
  139. </div>
  140. )}
  141. </div>
  142. <div className="p-4 bg-gray-50/50 border-t theme-border text-sm theme-text-secondary text-center">
  143. Click an image to insert it into the editor.
  144. </div>
  145. </div>
  146. </div>
  147. );
  148. }
  149. export default MediaGalleryModal;