| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- import React, { useState, useEffect } from "react";
- import { API_BASE } from "../config";
- function MediaGalleryModal({ isOpen, onClose, onSelect, slug }) {
- const [images, setImages] = useState([]);
- const [loading, setLoading] = useState(false);
- const [uploading, setUploading] = useState(false);
- const [error, setError] = useState(null);
- useEffect(() => {
- if (isOpen) {
- fetchMedia();
- }
- }, [isOpen, slug]);
- const fetchMedia = async () => {
- try {
- setLoading(true);
- const url = slug
- ? `${API_BASE}/media?slug=${encodeURIComponent(slug)}`
- : `${API_BASE}/media`;
- const response = await fetch(url, { credentials: "include" });
- if (!response.ok) throw new Error("Failed to load media");
- const data = await response.json();
- setImages(data);
- } catch (err) {
- console.error("Fetch media error:", err);
- setError("Could not load images.");
- } finally {
- setLoading(false);
- }
- };
- const handleUpload = async (e) => {
- const file = e.target.files[0];
- if (!file) return;
- try {
- setUploading(true);
- const formData = new FormData();
- formData.append("file", file);
- const uploadUrl = slug
- ? `${API_BASE}/upload?slug=${encodeURIComponent(slug)}`
- : `${API_BASE}/upload`;
- const response = await fetch(uploadUrl, {
- method: "POST",
- body: formData,
- credentials: "include",
- });
- if (!response.ok) throw new Error("Upload failed");
- await fetchMedia(); // Refresh list
- } catch (err) {
- setError(err.message);
- } finally {
- setUploading(false);
- }
- };
- const handleDelete = async (path, e) => {
- e.stopPropagation();
- if (!window.confirm("Delete this image?")) return;
- try {
- const response = await fetch(`${API_BASE}/media`, {
- method: "DELETE",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ path }),
- credentials: "include",
- });
- if (!response.ok) throw new Error("Delete failed");
- await fetchMedia();
- } catch (err) {
- alert(err.message);
- }
- };
- if (!isOpen) return null;
- return (
- <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
- <div className="theme-surface rounded-xl shadow-2xl w-full max-w-4xl max-h-[80vh] flex flex-col overflow-hidden theme-border border">
- {/* Header */}
- <div className="px-6 py-4 border-b theme-border flex justify-between items-center bg-gray-50/50">
- <h3 className="text-lg font-bold theme-text">Media Gallery</h3>
- <div className="flex items-center space-x-4">
- <label className="cursor-pointer btn-theme-primary text-white px-4 py-2 rounded-lg transition-colors text-sm font-medium">
- {uploading ? "Uploading..." : "Upload Image"}
- <input
- type="file"
- className="hidden"
- accept="image/*"
- onChange={handleUpload}
- disabled={uploading}
- />
- </label>
- <button
- onClick={onClose}
- className="text-gray-500 hover:text-gray-700 theme-text-secondary hover:theme-text"
- >
- <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>
- </button>
- </div>
- </div>
- {/* Content */}
- <div className="p-6 overflow-y-auto flex-grow theme-bg">
- {error && (
- <div className="mb-4 p-3 bg-red-100 text-red-700 rounded-lg">
- {error}
- </div>
- )}
- {loading ? (
- <div className="flex justify-center py-12">
- <div className="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"></div>
- </div>
- ) : images.length === 0 ? (
- <div className="text-center py-12 theme-text-secondary">
- No images found in this folder.
- </div>
- ) : (
- <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
- {images.map((img) => (
- <div
- key={img.name}
- className="group relative theme-surface rounded-lg shadow-sm border theme-border overflow-hidden hover:shadow-md transition-shadow cursor-pointer aspect-square"
- onClick={() => onSelect(`${API_BASE}${img.url}`, img.name)}
- >
- <img
- src={`${API_BASE}${img.url}`}
- alt={img.name}
- className="w-full h-full object-cover"
- loading="lazy"
- />
- <div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors" />
- {/* Delete Button */}
- <button
- onClick={(e) => handleDelete(img.url, e)}
- 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"
- title="Delete"
- >
- <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>
- </button>
- {/* Name Label */}
- <div className="absolute bottom-0 left-0 right-0 bg-black/60 text-white text-xs p-2 truncate">
- {img.name}
- </div>
- </div>
- ))}
- </div>
- )}
- </div>
- <div className="p-4 bg-gray-50/50 border-t theme-border text-sm theme-text-secondary text-center">
- Click an image to insert it into the editor.
- </div>
- </div>
- </div>
- );
- }
- export default MediaGalleryModal;
|