'use client' import React, { useState } from 'react' import { UploadCloud, Info, Plus, X, Video, GripVertical, Image, Layout, Images, StarIcon, ImageIcon } from 'lucide-react' import { useRouter } from 'next/navigation' import { useOrg } from '@components/Contexts/OrgContext' import { useLHSession } from '@components/Contexts/LHSessionContext' import { getOrgLogoMediaDirectory, getOrgPreviewMediaDirectory, getOrgThumbnailMediaDirectory } from '@services/media/media' import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/ui/tabs" import { toast } from 'react-hot-toast' import { constructAcceptValue } from '@/lib/constants' import { uploadOrganizationLogo, uploadOrganizationThumbnail, uploadOrganizationPreview, updateOrganization } from '@services/settings/org' import { cn } from '@/lib/utils' import { Input } from "@components/ui/input" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog" import { Button } from "@components/ui/button" import { Label } from "@components/ui/label" import { SiLoom, SiYoutube } from '@icons-pack/react-simple-icons' import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd' const SUPPORTED_FILES = constructAcceptValue(['png', 'jpg']) type Preview = { id: string; url: string; type: 'image' | 'youtube' | 'loom'; filename?: string; thumbnailUrl?: string; order: number; }; // Update the height constant const PREVIEW_HEIGHT = 'h-28' // Reduced height // Add this type for the video service selection type VideoService = 'youtube' | 'loom' | null; // Add this constant for consistent sizing const DIALOG_ICON_SIZE = 'w-16 h-16' // Add this constant at the top with other constants const ADD_PREVIEW_OPTIONS = [ { id: 'image', title: 'Upload Images', description: 'PNG, JPG (max 5MB)', icon: UploadCloud, color: 'blue', onClick: () => document.getElementById('previewInput')?.click() }, { id: 'youtube', title: 'YouTube', description: 'Add YouTube video', icon: SiYoutube, color: 'red', onClick: (setSelectedService: Function) => setSelectedService('youtube') }, { id: 'loom', title: 'Loom', description: 'Add Loom video', icon: SiLoom, color: 'blue', onClick: (setSelectedService: Function) => setSelectedService('loom') } ] as const; export default function OrgEditImages() { const router = useRouter() const session = useLHSession() as any const access_token = session?.data?.tokens?.access_token const org = useOrg() as any const [localLogo, setLocalLogo] = useState(null) const [localThumbnail, setLocalThumbnail] = useState(null) const [isLogoUploading, setIsLogoUploading] = useState(false) const [isThumbnailUploading, setIsThumbnailUploading] = useState(false) const [previews, setPreviews] = useState(() => { // Initialize with image previews const imagePreviews = (org?.previews?.images || []) .filter((item: any) => item?.filename) // Filter out empty filenames .map((item: any, index: number) => ({ id: item.filename, url: getOrgThumbnailMediaDirectory(org?.org_uuid, item.filename), filename: item.filename, type: 'image' as const, order: item.order ?? index // Use existing order or fallback to index })); // Initialize with video previews const videoPreviews = (org?.previews?.videos || []) .filter((video: any) => video && video.id) .map((video: any, index: number) => ({ id: video.id, url: video.url, type: video.type as 'youtube' | 'loom', thumbnailUrl: video.type === 'youtube' ? `https://img.youtube.com/vi/${video.id}/maxresdefault.jpg` : '', filename: '', order: video.order ?? (imagePreviews.length + index) // Use existing order or fallback to index after images })); const allPreviews = [...imagePreviews, ...videoPreviews]; return allPreviews.sort((a, b) => a.order - b.order); }); const [isPreviewUploading, setIsPreviewUploading] = useState(false) const [videoUrl, setVideoUrl] = useState('') const [videoDialogOpen, setVideoDialogOpen] = useState(false) const [selectedService, setSelectedService] = useState(null) const handleFileChange = async (event: React.ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { const file = event.target.files[0] setLocalLogo(URL.createObjectURL(file)) setIsLogoUploading(true) const loadingToast = toast.loading('Uploading logo...') try { await uploadOrganizationLogo(org.id, file, access_token) await new Promise((r) => setTimeout(r, 1500)) toast.success('Logo Updated', { id: loadingToast }) router.refresh() } catch (err) { toast.error('Failed to upload logo', { id: loadingToast }) } finally { setIsLogoUploading(false) } } } const handleThumbnailChange = async (event: React.ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { const file = event.target.files[0] setLocalThumbnail(URL.createObjectURL(file)) setIsThumbnailUploading(true) const loadingToast = toast.loading('Uploading thumbnail...') try { await uploadOrganizationThumbnail(org.id, file, access_token) await new Promise((r) => setTimeout(r, 1500)) toast.success('Thumbnail Updated', { id: loadingToast }) router.refresh() } catch (err) { toast.error('Failed to upload thumbnail', { id: loadingToast }) } finally { setIsThumbnailUploading(false) } } } const handleImageButtonClick = (inputId: string) => (event: React.MouseEvent) => { event.preventDefault() document.getElementById(inputId)?.click() } const handlePreviewUpload = async (event: React.ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { const files = Array.from(event.target.files) const remainingSlots = 4 - previews.length if (files.length > remainingSlots) { toast.error(`You can only upload ${remainingSlots} more preview${remainingSlots === 1 ? '' : 's'}`) return } setIsPreviewUploading(true) const loadingToast = toast.loading(`Uploading ${files.length} preview${files.length === 1 ? '' : 's'}...`) try { const uploadPromises = files.map(async (file) => { const response = await uploadOrganizationPreview(org.id, file, access_token) return { id: response.name_in_disk, url: URL.createObjectURL(file), filename: response.name_in_disk, type: 'image' as const, order: previews.length // Add new items at the end } }) const newPreviews = await Promise.all(uploadPromises) const updatedPreviews = [...previews, ...newPreviews] await updateOrganization(org.id, { previews: { images: updatedPreviews .filter(p => p.type === 'image') .map(p => ({ filename: p.filename, order: p.order })), videos: updatedPreviews .filter(p => p.type === 'youtube' || p.type === 'loom') .map(p => ({ type: p.type, url: p.url, id: p.id, order: p.order })) } }, access_token) setPreviews(updatedPreviews) toast.success(`${files.length} preview${files.length === 1 ? '' : 's'} added`, { id: loadingToast }) router.refresh() } catch (err) { toast.error('Failed to upload previews', { id: loadingToast }) } finally { setIsPreviewUploading(false) } } } const removePreview = async (id: string) => { const loadingToast = toast.loading('Removing preview...') try { const updatedPreviews = previews.filter(p => p.id !== id) const updatedPreviewFilenames = updatedPreviews.map(p => p.filename) await updateOrganization(org.id, { previews: { images: updatedPreviewFilenames } }, access_token) setPreviews(updatedPreviews) toast.success('Preview removed', { id: loadingToast }) router.refresh() } catch (err) { toast.error('Failed to remove preview', { id: loadingToast }) } } const extractVideoId = (url: string, type: 'youtube' | 'loom'): string | null => { if (type === 'youtube') { const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/ const match = url.match(regex) return match ? match[1] : null } else if (type === 'loom') { const regex = /(?:loom\.com\/(?:share|embed)\/)([a-zA-Z0-9]+)/ const match = url.match(regex) return match ? match[1] : null } return null } const handleVideoSubmit = async (type: 'youtube' | 'loom') => { const videoId = extractVideoId(videoUrl, type); if (!videoId) { toast.error(`Invalid ${type} URL`); return; } // Check if video already exists if (previews.some(preview => preview.id === videoId)) { toast.error('This video has already been added'); return; } const loadingToast = toast.loading('Adding video preview...'); try { const thumbnailUrl = type === 'youtube' ? `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg` : ''; const newPreview: Preview = { id: videoId, url: videoUrl, type, thumbnailUrl, filename: '', order: previews.length // Add new items at the end }; const updatedPreviews = [...previews, newPreview]; await updateOrganization(org.id, { previews: { images: updatedPreviews .filter(p => p.type === 'image') .map(p => ({ filename: p.filename, order: p.order })), videos: updatedPreviews .filter(p => p.type === 'youtube' || p.type === 'loom') .map(p => ({ type: p.type, url: p.url, id: p.id, order: p.order })) } }, access_token); setPreviews(updatedPreviews); setVideoUrl(''); setVideoDialogOpen(false); toast.success('Video preview added', { id: loadingToast }); router.refresh(); } catch (err) { toast.error('Failed to add video preview', { id: loadingToast }); } }; const handleDragEnd = async (result: DropResult) => { if (!result.destination) return; const items = Array.from(previews); const [reorderedItem] = items.splice(result.source.index, 1); items.splice(result.destination.index, 0, reorderedItem); // Update order numbers const reorderedItems = items.map((item, index) => ({ ...item, order: index })); setPreviews(reorderedItems); // Update the order in the backend const loadingToast = toast.loading('Updating preview order...'); try { await updateOrganization(org.id, { previews: { images: reorderedItems .filter(p => p.type === 'image') .map(p => ({ filename: p.filename, order: p.order })), videos: reorderedItems .filter(p => p.type === 'youtube' || p.type === 'loom') .map(p => ({ type: p.type, url: p.url, id: p.id, order: p.order })) } }, access_token); toast.success('Preview order updated', { id: loadingToast }); router.refresh(); } catch (err) { toast.error('Failed to update preview order', { id: loadingToast }); setPreviews(previews); } }; // Add function to reset video dialog state const resetVideoDialog = () => { setSelectedService(null) setVideoUrl('') } return (

Images & Previews

Manage your organization's logo, thumbnail, and preview images

Logo Thumbnail Previews

Accepts PNG, JPG (max 5MB)

Recommended size: 200x100 pixels

Accepts PNG, JPG (max 5MB)

Recommended size: 200x100 pixels

{(provided) => (
{previews.map((preview, index) => ( {(provided, snapshot) => (
{preview.type === 'image' ? (
) : (
{preview.type === 'youtube' ? ( ) : ( )}
)}
)} ))} {provided.placeholder} {previews.length < 4 && (
{ setVideoDialogOpen(open); if (!open) resetVideoDialog(); }}> Add Preview
{!selectedService ? ( <> {ADD_PREVIEW_OPTIONS.map((option) => ( ))} ) : (
{selectedService === 'youtube' ? ( ) : ( )}

{selectedService === 'youtube' ? 'Add YouTube Video' : 'Add Loom Video'}

{selectedService === 'youtube' ? 'Paste your YouTube video URL' : 'Paste your Loom video URL'}

setVideoUrl(e.target.value)} className="w-full" autoFocus />
)}
)}
)}

Drag to reorder • Maximum 4 previews • Supports images & videos

) }