244 lines
9.4 KiB
TypeScript
244 lines
9.4 KiB
TypeScript
'use client';
|
|
import React, { useState, ChangeEvent, FormEvent, useEffect } from 'react';
|
|
import IconTrashLines from '../icon/icon-trash-lines';
|
|
import axios from 'axios';
|
|
import { useRouter } from 'next/navigation';
|
|
import { buildApiUrl } from '@/utils/BaseUrl.utils';
|
|
|
|
interface FormErrors {
|
|
[key: string]: string;
|
|
}
|
|
interface EditEventFormProps {
|
|
eventId: string | null;
|
|
}
|
|
|
|
const extractDriveFileId = (link: string): string | null => {
|
|
const trimmed = link?.trim();
|
|
if (!trimmed) return null;
|
|
|
|
const match = trimmed.match(/\/d\/([a-zA-Z0-9_-]+)/) ||
|
|
trimmed.match(/[?&]id=([a-zA-Z0-9_-]+)/) ||
|
|
trimmed.match(/\/open\?id=([a-zA-Z0-9_-]+)/);
|
|
|
|
return match?.[1] ?? null;
|
|
};
|
|
|
|
const buildDriveViewUrl = (id: string) => `https://drive.google.com/thumbnail?id=${id}&sz=w1000`;
|
|
const buildDriveDownloadUrl = (id: string) => `https://drive.google.com/uc?export=download&id=${id}`;
|
|
const buildDriveThumbnailUrl = (id: string) => `https://drive.google.com/thumbnail?id=${id}&sz=w1000`;
|
|
const buildDriveViewAuthUrl = (id: string) => `https://drive.google.com/uc?export=view&id=${id}&authuser=0`;
|
|
|
|
const normalizeDriveImageUrl = (link: string): string => {
|
|
const trimmed = link?.trim() || '';
|
|
const id = extractDriveFileId(trimmed);
|
|
if (!id) return trimmed;
|
|
return buildDriveViewUrl(id);
|
|
};
|
|
|
|
|
|
const getDriveFallbackUrl = (currentUrl: string): string | null => {
|
|
const id = extractDriveFileId(currentUrl);
|
|
if (!id) return null;
|
|
|
|
if (currentUrl.includes('thumbnail?id=')) {
|
|
return buildDriveViewAuthUrl(id);
|
|
}
|
|
if (currentUrl.includes('export=download')) {
|
|
return buildDriveThumbnailUrl(id);
|
|
}
|
|
if (currentUrl.includes('export=view')) {
|
|
return buildDriveDownloadUrl(id);
|
|
}
|
|
return buildDriveViewAuthUrl(id);
|
|
};
|
|
|
|
const CreateEventGalleryForm: React.FC<EditEventFormProps> = ({ eventId }) => {
|
|
const router = useRouter();
|
|
|
|
const [linkInput, setLinkInput] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [errors, setErrors] = useState<FormErrors>({});
|
|
const [existingImagesCount, setExistingImagesCount] = useState<number>(0);
|
|
const [fetchingImages, setFetchingImages] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (eventId) {
|
|
getEventGallery();
|
|
} else {
|
|
setFetchingImages(false);
|
|
}
|
|
}, [eventId]);
|
|
|
|
const getEventGallery = async () => {
|
|
try {
|
|
const token = localStorage.getItem("token");
|
|
const res = await axios.get(buildApiUrl(`event-images/event/${eventId}`), {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
const images = res.data?.data || [];
|
|
setExistingImagesCount(images.length);
|
|
} catch (error) {
|
|
console.error('error fetching existing images', error);
|
|
} finally {
|
|
setFetchingImages(false);
|
|
}
|
|
};
|
|
|
|
const remainingSlots = Math.max(0, 20 - existingImagesCount);
|
|
|
|
const parsedLinks = React.useMemo(() => {
|
|
return linkInput
|
|
.split(/[\n,]+/)
|
|
.map((link) => link.trim())
|
|
.filter((link) => link !== '')
|
|
.map(normalizeDriveImageUrl);
|
|
}, [linkInput]);
|
|
|
|
const handleLinkInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
const newValue = e.target.value;
|
|
setLinkInput(newValue);
|
|
const currentCount = newValue.split(/[\n,]+/).map(l => l.trim()).filter(l => l !== '').length;
|
|
if (currentCount > remainingSlots) {
|
|
setErrors({ images: `You can only add up to ${remainingSlots} more images (20 total per event).` });
|
|
} else {
|
|
setErrors(prev => ({ ...prev, images: '' }));
|
|
}
|
|
};
|
|
|
|
const handleImageDelete = (index: number) => {
|
|
const lines = linkInput.split(/[\n,]+/).map(l => l.trim()).filter(l => l !== '');
|
|
lines.splice(index, 1);
|
|
setLinkInput(lines.join('\n'));
|
|
};
|
|
|
|
const validateForm = (): boolean => {
|
|
const newErrors: FormErrors = {};
|
|
|
|
if (parsedLinks.length === 0) {
|
|
newErrors.images = 'Please provide at least one image link';
|
|
} else if (parsedLinks.length > remainingSlots) {
|
|
newErrors.images = `Only ${remainingSlots} more images can be added (20 total per event).`;
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault();
|
|
if (!validateForm()) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
// Step 1: Prepare body for bulk save
|
|
const body = {
|
|
eventid: Number(eventId),
|
|
imageurl: parsedLinks
|
|
};
|
|
|
|
console.log("Sending body:", body);
|
|
|
|
// Step 3: Call bulk API
|
|
const token = localStorage.getItem('token');
|
|
await axios.post(
|
|
buildApiUrl('event-images/bulk'),
|
|
body,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
router.push(`/event-gallery?eventid=${eventId}`);
|
|
} catch (error: any) {
|
|
console.error('Upload failed:', error);
|
|
const message = error.response?.data?.message || 'Failed to upload images. Please try again.';
|
|
setErrors({ submit: message });
|
|
alert(message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="max-w-4xl mx-auto p-6 bg-white rounded shadow-md">
|
|
<h2 className="text-xl font-bold mb-4">Upload Gallery Images</h2>
|
|
|
|
{/* Link Input */}
|
|
<div className="mb-4">
|
|
<label htmlFor="gallery" className="block font-medium mb-1">Image Links (Google Drive etc.)</label>
|
|
<textarea
|
|
id="gallery"
|
|
rows={5}
|
|
placeholder="Paste your Google Drive links here, one per line..."
|
|
value={linkInput}
|
|
onChange={handleLinkInputChange}
|
|
disabled={remainingSlots === 0}
|
|
className="w-full border rounded px-3 py-2 disabled:bg-gray-100 disabled:cursor-not-allowed dark:disabled:bg-gray-800"
|
|
/>
|
|
{errors.images && <p className="text-red-500 text-sm">{errors.images}</p>}
|
|
<p className="text-gray-500 text-xs mt-1">
|
|
{fetchingImages
|
|
? 'Checking current image count...'
|
|
: remainingSlots > 0
|
|
? `You can add up to ${remainingSlots} more images (currently ${existingImagesCount}/20).`
|
|
: `You have reached the maximum of 20 images for this event.`}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Previews */}
|
|
{parsedLinks.length > 0 && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
{parsedLinks.map((url, index) => (
|
|
<div key={index} className="relative border rounded shadow overflow-hidden">
|
|
<img
|
|
src={url}
|
|
alt={`Preview ${index + 1}`}
|
|
className="w-full h-40 object-cover"
|
|
onError={(e) => {
|
|
const img = e.currentTarget as HTMLImageElement;
|
|
const fallback = getDriveFallbackUrl(url);
|
|
if (fallback && fallback !== url) {
|
|
img.src = fallback;
|
|
return;
|
|
}
|
|
img.src = "https://placehold.co/150x150?text=Invalid+Image+Link";
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleImageDelete(index)}
|
|
className="absolute top-1 right-1 bg-red-600 text-white rounded-full p-1 hover:bg-red-700"
|
|
>
|
|
<IconTrashLines className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Submit */}
|
|
<div className="mt-6 flex items-center gap-4">
|
|
<button
|
|
type="submit"
|
|
disabled={loading || fetchingImages || remainingSlots === 0}
|
|
className={`bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 ${(loading || fetchingImages || remainingSlots === 0) ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
>
|
|
{loading ? 'Uploading...' : fetchingImages ? 'Checking limits...' : 'Submit'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => router.back()}
|
|
className="bg-gray-200 text-gray-800 px-6 py-2 rounded hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
>
|
|
Back
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
export default CreateEventGalleryForm;
|
|
|