Files
Spanglish/frontend/src/components/MediaPicker.tsx
T
root 47ba754f05 Add full SEO optimization for Spanglish social and language events
- Add comprehensive metadata to root layout with Open Graph, Twitter cards
- Create dynamic sitemap.ts for all pages and events
- Create robots.ts with proper allow/disallow rules
- Add JSON-LD Event structured data to event detail pages
- Add page-specific metadata to events, community, contact, FAQ pages
- Add FAQ structured data schema
- Update footer with local SEO text for Asunción, Paraguay
- Add web manifest for mobile SEO
- Create 404 page with proper noindex
- Optimize image alt text and add lazy loading
- Add NEXT_PUBLIC_SITE_URL env variable
- Add about/ folder to gitignore
2026-01-30 21:05:25 +00:00

278 lines
10 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { mediaApi, Media } from '@/lib/api';
import Button from '@/components/ui/Button';
import {
PhotoIcon,
ArrowUpTrayIcon,
XMarkIcon,
CheckIcon,
FolderOpenIcon,
} from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
interface MediaPickerProps {
value?: string;
onChange: (url: string) => void;
relatedId?: string;
relatedType?: string;
}
export default function MediaPicker({ value, onChange, relatedId, relatedType = 'event' }: MediaPickerProps) {
const [showModal, setShowModal] = useState(false);
const [activeTab, setActiveTab] = useState<'upload' | 'library'>('upload');
const [media, setMedia] = useState<Media[]>([]);
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
const [selectedMedia, setSelectedMedia] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (showModal && activeTab === 'library') {
loadMedia();
}
}, [showModal, activeTab]);
const loadMedia = async () => {
setLoading(true);
try {
const { media } = await mediaApi.getAll();
setMedia(media);
} catch (error) {
toast.error('Failed to load media library');
} finally {
setLoading(false);
}
};
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const result = await mediaApi.upload(file, relatedId, relatedType);
onChange(result.url);
toast.success('Image uploaded successfully');
setShowModal(false);
} catch (error: any) {
toast.error(error.message || 'Failed to upload image');
} finally {
setUploading(false);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const handleSelectFromLibrary = () => {
if (selectedMedia) {
onChange(selectedMedia);
setShowModal(false);
setSelectedMedia(null);
}
};
const handleRemove = () => {
onChange('');
};
return (
<div>
<label className="block text-sm font-medium mb-1">Event Banner Image</label>
<div className="mt-2">
{value ? (
<div className="relative">
<img
src={value}
alt="Event banner"
className="w-full h-40 object-cover rounded-btn"
/>
<button
type="button"
onClick={handleRemove}
className="absolute top-2 right-2 bg-red-500 text-white p-1 rounded-full hover:bg-red-600"
>
<XMarkIcon className="w-4 h-4" />
</button>
</div>
) : (
<div className="flex gap-3">
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="flex-1 border-2 border-dashed border-secondary-light-gray rounded-btn p-6 text-center cursor-pointer hover:border-primary-yellow transition-colors"
>
{uploading ? (
<div className="flex flex-col items-center">
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full" />
<p className="mt-2 text-sm text-gray-500">Uploading...</p>
</div>
) : (
<div className="flex flex-col items-center">
<ArrowUpTrayIcon className="w-10 h-10 text-gray-400" />
<p className="mt-2 text-sm text-gray-600">Upload New</p>
<p className="text-xs text-gray-400">JPEG, PNG, GIF, WebP</p>
</div>
)}
</button>
<button
type="button"
onClick={() => { setShowModal(true); setActiveTab('library'); }}
className="flex-1 border-2 border-dashed border-secondary-light-gray rounded-btn p-6 text-center cursor-pointer hover:border-primary-yellow transition-colors"
>
<div className="flex flex-col items-center">
<FolderOpenIcon className="w-10 h-10 text-gray-400" />
<p className="mt-2 text-sm text-gray-600">Choose from Library</p>
<p className="text-xs text-gray-400">Select existing media</p>
</div>
</button>
</div>
)}
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp,image/avif"
onChange={handleUpload}
className="hidden"
/>
</div>
{/* Media Library Modal */}
{showModal && (
<div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4">
<div className="bg-white rounded-btn w-full max-w-4xl max-h-[80vh] flex flex-col">
{/* Modal Header */}
<div className="flex items-center justify-between p-4 border-b">
<h3 className="text-lg font-semibold">Select Image</h3>
<button
type="button"
onClick={() => { setShowModal(false); setSelectedMedia(null); }}
className="p-1 hover:bg-gray-100 rounded"
>
<XMarkIcon className="w-5 h-5" />
</button>
</div>
{/* Tabs */}
<div className="flex border-b">
<button
type="button"
onClick={() => setActiveTab('upload')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'upload'
? 'border-primary-yellow text-primary-dark'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
<ArrowUpTrayIcon className="w-4 h-4 inline-block mr-2" />
Upload New
</button>
<button
type="button"
onClick={() => setActiveTab('library')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'library'
? 'border-primary-yellow text-primary-dark'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
<FolderOpenIcon className="w-4 h-4 inline-block mr-2" />
Media Library
</button>
</div>
{/* Tab Content */}
<div className="flex-1 overflow-y-auto p-4">
{activeTab === 'upload' && (
<div
onClick={() => fileInputRef.current?.click()}
className="border-2 border-dashed border-secondary-light-gray rounded-btn p-12 text-center cursor-pointer hover:border-primary-yellow transition-colors"
>
{uploading ? (
<div className="flex flex-col items-center">
<div className="animate-spin w-12 h-12 border-4 border-primary-yellow border-t-transparent rounded-full" />
<p className="mt-4 text-gray-500">Uploading...</p>
</div>
) : (
<div className="flex flex-col items-center">
<PhotoIcon className="w-16 h-16 text-gray-400" />
<p className="mt-4 text-lg text-gray-600">Click to upload an image</p>
<p className="text-sm text-gray-400 mt-1">JPEG, PNG, GIF, WebP (max 10MB)</p>
</div>
)}
</div>
)}
{activeTab === 'library' && (
<>
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full" />
</div>
) : media.length === 0 ? (
<div className="text-center py-12 text-gray-500">
<PhotoIcon className="w-12 h-12 mx-auto text-gray-400" />
<p className="mt-2">No images in library</p>
<p className="text-sm">Upload an image to get started</p>
</div>
) : (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3">
{media.map((item) => (
<button
key={item.id}
type="button"
onClick={() => setSelectedMedia(item.fileUrl)}
className={`relative aspect-square rounded-btn overflow-hidden border-2 transition-all ${
selectedMedia === item.fileUrl
? 'border-primary-yellow ring-2 ring-primary-yellow/30'
: 'border-transparent hover:border-gray-300'
}`}
>
<img
src={item.fileUrl}
alt=""
className="w-full h-full object-cover"
/>
{selectedMedia === item.fileUrl && (
<div className="absolute inset-0 bg-primary-yellow/20 flex items-center justify-center">
<div className="bg-primary-yellow rounded-full p-1">
<CheckIcon className="w-5 h-5 text-white" />
</div>
</div>
)}
</button>
))}
</div>
)}
</>
)}
</div>
{/* Modal Footer */}
{activeTab === 'library' && (
<div className="flex justify-end gap-3 p-4 border-t">
<Button
type="button"
variant="outline"
onClick={() => { setShowModal(false); setSelectedMedia(null); }}
>
Cancel
</Button>
<Button
type="button"
onClick={handleSelectFromLibrary}
disabled={!selectedMedia}
>
Select Image
</Button>
</div>
)}
</div>
</div>
)}
</div>
);
}