import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { useDesignTokens } from '@/theme/useDesignTokens'; import { useParams, useNavigate, useLocation, useSearchParams } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { useBoard, useBoardPins, useUserPins, useUserBoards, useUserPinsInBoards, useUpdatePinOrder, useAddPinToBoard, useRemovePinFromBoard, queryKeys, } from '@/hooks/api/useApiQueries'; import { Button } from '@/components/ui/design-system/Button'; import { ButtonGroup, DropdownButton, Dropdown } from '@/components/ui/design-system/patterns'; import { AdaptiveDropdown } from '@/components/ui/design-system/patterns/AdaptiveDropdown'; import { useIsMobile } from '@/components/ui/design-system/patterns/CardActions'; import { Input } from '@/components/ui/design-system/Input'; import { BoardToolbar } from '@/components/ui/BoardToolbar'; import { BoardBulkActionsBar, type BulkStatusOption, type BulkStatusValue, } from '@/components/ui/BoardBulkActionsBar'; import { PlusIcon, ShareIcon, PencilIcon, XMarkIcon, ArrowPathIcon, ChevronDownIcon, MagnifyingGlassIcon, Squares2X2Icon, CloudArrowUpIcon, UserIcon, ListBulletIcon, ArrowsUpDownIcon, RectangleStackIcon, LockClosedIcon, GlobeAltIcon, ClockIcon, FunnelIcon, } from '@heroicons/react/24/outline'; import { pinsService } from '@/services/pinsService'; import { useAuthStore } from '@/store/authStore'; import { LoadingSpinner } from '@/components/ui/ds'; import { PinCard } from '@/components/ui/PinCard'; import { CompactPinGrid } from '@/components/ui/CompactPinGrid'; import { PinBoardViewSkeleton } from './components/PinBoardViewSkeleton'; import { PinDetailModal } from '@/components/ui/PinDetailModal'; import { AddPinFlowModal } from '@/components/modals/AddPinFlowModal'; import { ShareBoardModal } from './components/ShareBoardModal'; import { ZoomableDraggablePinGrid } from './components/ZoomableDraggablePinGrid'; import { EditPinModal } from '@/components/ui/PinDetailModal/EditPinModal'; import { BulkUploadModal } from '@/components/modals/BulkUploadModal'; import { Pin as UIPin, Pin } from '@/types/pin'; import { Pin as DataConnectPin } from '@/services/dataconnect/pinsDataConnectService'; import { shouldShowTradableFilter, allowsManualPinAddition, shouldAppearInBoardSelection, } from '@/utils/boardUtils'; import { LastUpdatedDisplay } from '@/components/ui/LastUpdatedDisplay'; import { PageToolbar } from '@/components/ui/PageToolbar'; import { colorTokens } from '@/components/ui/design-system/foundations'; import { useGlobalDragAndDrop } from '@/hooks/useGlobalDragAndDrop'; import { DragDropOverlay } from '@/components/ui/DragDropOverlay'; import { DndContext, DragEndEvent, DragStartEvent, CollisionDetection, rectIntersection, useSensor, useSensors, MouseSensor, KeyboardSensor, } from '@dnd-kit/core'; import { sortableKeyboardCoordinates } from '@dnd-kit/sortable'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/hooks/useTranslation'; import { makeApiRequest } from '@/config/api'; import { Avatar } from '@/components/ui/Avatar'; import { ProfileHeader } from '@/components/ui/ProfileHeader'; import { FollowButton, FollowStats as FollowStatsComponent } from '@/components/ui/FollowButton'; import { ProfileMenu } from '@/components/ui/ProfileMenu'; import { ChangeProfilePhotoModal } from '@/components/ui/ChangeProfilePhotoModal'; import { usersService } from '@/services/usersService'; import { userNameUtils } from '@/utils/nameUtils'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/design-system/Tabs'; import { useWishlistCount } from '@/hooks/useWishlistCount'; import { useListingsCount } from '@/hooks/useListingsCount'; import { useSafeArea } from '@/components/ui/SafeAreaView'; import { MapPinIcon, StarIcon, ShieldCheckIcon } from '@heroicons/react/24/outline'; import { analyticsService } from '@/services/analyticsService'; import { useNoIndex } from '@/hooks/useNoIndex'; import { FavoritesFilterButton } from '@/components/ui/FavoritesFilterButton'; import { pinFavoritesService } from '@/services/pinFavoritesService'; import { OnboardingTour } from '@/components/tour/OnboardingTour'; import { useTour } from '@/hooks/useTour'; import { TourType } from '@/types/tour'; import { useUserPinOwnership } from '@/hooks/useUserPinOwnership'; import { usePullToRefresh } from '@/hooks/usePullToRefresh'; import { PullToRefreshIndicator } from '@/components/ui/PullToRefreshIndicator'; type PinSortOption = 'custom' | 'name-asc' | 'name-desc' | 'collection' | 'set'; export const PinBoardViewPage: React.FC = () => { const { semantic } = useDesignTokens(); const { pins: pinsTexts, common: commonTexts, messages: messagesTexts } = useTranslation(); const [favoritePinIdsLoading, setFavoritePinIdsLoading] = useState(false); const { boardId } = useParams<{ boardId: string }>(); const navigate = useNavigate(); const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); const { user, isAuthenticated } = useAuthStore(); const queryClient = useQueryClient(); const { showToast, success, error: showError, info } = useToast(); const isMobile = useIsMobile(); const { isInApp, safeAreaInsets } = useSafeArea(); const [isUiChromeHidden, setIsUiChromeHidden] = useState(false); // Ref para evitar logs duplicados de erro const errorLoggedRef = useRef<{ boardError: any; pinsError: any } | null>(null); // Tour functionality const { runTour, completeTour, stopTour, hasSeenTour, markTourAsSeen, isTourRunning, currentTourType, } = useTour(); // Ownership check for visitor (to show owned/ISO badges on other users' boards) const { ownedPinDatabaseIds, refresh: refreshOwnership } = useUserPinOwnership(); // Smart navigation function that uses state to return to correct location const handleBackNavigation = () => { const state = location.state as any; // If we have return state from profile, use it if (state?.returnTo === 'profile-boards' && state?.profileUrl) { navigate(state.profileUrl); return; } // Default fallback to profile boards tab navigate(`/${user?.username || user?.id}?tab=boards`); }; // Profile photo functionality const handleAvatarClick = () => { if (isOwnProfile && boardOwnerData) { setShowPhotoEditor(true); } }; const handleUploadPhoto = async (file: File) => { if (!boardOwnerData || !user || !isOwnProfile) { console.error('Cannot upload photo: insufficient permissions'); return; } try { console.log('🔄 Starting photo upload:', file.name); showToast(commonTexts('upload.uploadingPhoto', { defaultValue: 'Uploading photo...' })); // Import Firebase Storage functions const { ref, uploadBytes, getDownloadURL, deleteObject } = await import('firebase/storage'); const { storage } = await import('@/services/firebase'); const { updateProfile } = await import('firebase/auth'); const { auth } = await import('@/services/firebase'); // Validate & resize image if necessary const maxSizeMb = 5; if (!file.type.startsWith('image/')) { showToast( commonTexts('errors.invalidImageType', { defaultValue: 'Please select a valid image file.', }), 'error' ); return; } if (file.size > maxSizeMb * 1024 * 1024) { showToast( commonTexts('errors.imageTooLarge', { defaultValue: `Image too large (max ${maxSizeMb}MB).`, }), 'error' ); return; } const resizedFile = await resizeImageAndUpload(file, 512, 512); // Delete old photo if it exists and is from Firebase Storage if ( boardOwnerData.avatarUrl && boardOwnerData.avatarUrl.startsWith('https://firebasestorage.googleapis.com') ) { try { const match = boardOwnerData.avatarUrl.match(/\/o\/(.*?)\?/); if (match && match[1]) { const filePath = decodeURIComponent(match[1]); const oldFileRef = ref(storage, filePath); await deleteObject(oldFileRef); console.log(' Old photo deleted from Storage'); } } catch (error) { console.warn('️ Could not delete old photo:', error); } } // Upload new photo to Firebase Storage const storageRef = ref(storage, `avatars/${boardOwnerData.id}/${Date.now()}_${file.name}`); await uploadBytes(storageRef, resizedFile); const downloadURL = await getDownloadURL(storageRef); console.log(' Photo uploaded to Storage:', downloadURL); // Update Firebase Auth profile if (auth.currentUser) { await updateProfile(auth.currentUser, { photoURL: downloadURL }); console.log(' Firebase Auth profile updated'); } // Update user in Firestore via usersService await usersService.update(boardOwnerData.id, { avatarUrl: downloadURL }); console.log(' User profile updated in Firestore'); // Update local boardOwnerData state setBoardOwnerData((prev: any) => (prev ? { ...prev, avatarUrl: downloadURL } : null)); // Update authStore with new avatar URL const { useAuthStore } = await import('@/store/authStore'); const authStore = useAuthStore.getState(); if (authStore.user && authStore.user.id === boardOwnerData.id) { authStore.setUser({ ...authStore.user, avatarUrl: downloadURL, }); console.log(' AuthStore updated with new avatar'); } success( commonTexts('success.photoUploaded', { defaultValue: 'Photo uploaded successfully!' }) ); analyticsService.trackEvent({ event: 'profile_avatar_upload', category: 'profile', action: 'avatar_upload', userId: boardOwnerData.id, metadata: { fileName: file.name, boardId }, }); } catch (error) { console.error(' Error uploading photo:', error); showError( commonTexts('errors.uploadPhotoFailed', { defaultValue: 'Failed to upload photo. Please try again.', }) ); } }; const handleRemovePhoto = async () => { if (!boardOwnerData || !user || !isOwnProfile) { console.error('Cannot remove photo: insufficient permissions'); return; } try { console.log('🔄 Removing photo'); showToast(commonTexts('upload.removingPhoto', { defaultValue: 'Removing photo...' })); // Import Firebase functions const { ref, deleteObject } = await import('firebase/storage'); const { storage } = await import('@/services/firebase'); const { updateProfile } = await import('firebase/auth'); const { auth } = await import('@/services/firebase'); const { DEFAULT_AVATAR } = await import('@/store/authStore'); // Delete photo from Firebase Storage if it exists if ( boardOwnerData.avatarUrl && boardOwnerData.avatarUrl.startsWith('https://firebasestorage.googleapis.com') ) { try { const match = boardOwnerData.avatarUrl.match(/\/o\/(.*?)\?/); if (match && match[1]) { const filePath = decodeURIComponent(match[1]); const fileRef = ref(storage, filePath); await deleteObject(fileRef); console.log(' Photo deleted from Storage'); } } catch (error) { console.warn('️ Could not delete photo from Storage:', error); } } // Update Firebase Auth profile if (auth.currentUser) { await updateProfile(auth.currentUser, { photoURL: DEFAULT_AVATAR }); console.log(' Firebase Auth profile updated'); } // Update user in Firestore via usersService await usersService.update(boardOwnerData.id, { avatarUrl: DEFAULT_AVATAR }); console.log(' User profile updated in Firestore'); // Update local boardOwnerData state setBoardOwnerData((prev: any) => (prev ? { ...prev, avatarUrl: DEFAULT_AVATAR } : null)); // Update authStore with default avatar const { useAuthStore } = await import('@/store/authStore'); const authStore = useAuthStore.getState(); if (authStore.user && authStore.user.id === boardOwnerData.id) { authStore.setUser({ ...authStore.user, avatarUrl: DEFAULT_AVATAR, }); console.log(' AuthStore updated with default avatar'); } success(commonTexts('success.photoRemoved', { defaultValue: 'Photo removed successfully!' })); analyticsService.trackEvent({ event: 'profile_avatar_remove', category: 'profile', action: 'avatar_remove', userId: boardOwnerData.id, metadata: { boardId }, }); } catch (error) { console.error(' Error removing photo:', error); showError( commonTexts('errors.removePhotoFailed', { defaultValue: 'Failed to remove photo. Please try again.', }) ); } }; // Utility function to resize image const resizeImageAndUpload = async ( file: File, maxWidth: number, maxHeight: number ): Promise => { return new Promise((resolve) => { const img = new window.Image(); const url = URL.createObjectURL(file); img.onload = () => { // Clean up the blob URL immediately after loading URL.revokeObjectURL(url); let { width, height } = img; if (width > maxWidth || height > maxHeight) { const scale = Math.min(maxWidth / width, maxHeight / height); width = Math.round(width * scale); height = Math.round(height * scale); } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx?.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => { if (blob) { const resized = new File([blob], file.name, { type: file.type }); resolve(resized); } }, file.type); }; img.onerror = () => { // Clean up the blob URL on error too URL.revokeObjectURL(url); resolve(file); }; img.src = url; }); }; // REMOVIDO: console.log que causava logs múltiplos const [selectedPin, setSelectedPin] = useState< (UIPin & { owner?: { id: string; name: string; avatar?: string } }) | null >(null); // Bulk selection mode (Instagram-like) const [isSelectMode, setIsSelectMode] = useState(false); const [selectedPinIds, setSelectedPinIds] = useState>(new Set()); const [isBulkProcessing, setIsBulkProcessing] = useState(false); const [showAddPinModal, setShowAddPinModal] = useState(false); const [showShareModal, setShowShareModal] = useState(false); const [showBulkUploadModal, setShowBulkUploadModal] = useState(false); const [focusOnComment, setFocusOnComment] = useState(false); const [viewMode, setViewMode] = useState<'compact' | 'grid'>(() => { const saved = localStorage.getItem(`board-${boardId}-view-mode`); // COMPACT VIEW DEFAULT: Usar 'compact' como padrão para melhor UX mobile if (!saved) { localStorage.setItem(`board-${boardId}-view-mode`, 'compact'); return 'compact'; } return (saved as 'compact' | 'grid') || 'compact'; }); // Force compact mode on mobile and keep localStorage in sync useEffect(() => { try { if (isMobile && viewMode !== 'compact') { setViewMode('compact'); if (boardId) localStorage.setItem(`board-${boardId}-view-mode`, 'compact'); } } catch {} }, [isMobile]); const [tradableFilter, setTradableFilter] = useState< 'all' | 'display' | 'trade' | 'sale' | 'sale_trade' | 'iso' | 'placeholder' >(() => { // Primeiro tenta ler da URL const urlFilter = searchParams.get('filter'); if ( urlFilter && ['all', 'display', 'trade', 'sale', 'sale_trade', 'iso', 'placeholder', 'reference'].includes( urlFilter ) ) { const normalized = urlFilter === 'reference' ? 'placeholder' : urlFilter; return normalized as | 'all' | 'display' | 'trade' | 'sale' | 'sale_trade' | 'iso' | 'placeholder'; } // Se não houver na URL, tenta ler do localStorage try { if (boardId) { const saved = localStorage.getItem(`board-${boardId}-tradable-filter`); if ( saved && [ 'all', 'display', 'trade', 'sale', 'sale_trade', 'iso', 'placeholder', 'reference', ].includes(saved) ) { const normalized = saved === 'reference' ? 'placeholder' : saved; return normalized as | 'all' | 'display' | 'trade' | 'sale' | 'sale_trade' | 'iso' | 'placeholder'; } } } catch {} return 'all'; }); // Favorites filter is only shown/used for All Pins when tradableFilter === 'all' const [showFavoritesOnly, setShowFavoritesOnly] = useState(() => { // Primeiro tenta ler da URL const urlFavorites = searchParams.get('favorites'); if (urlFavorites) { return urlFavorites === 'true'; } // Se não houver na URL, tenta ler do localStorage try { if (boardId) { const saved = localStorage.getItem(`board-${boardId}-favorites-only`); return saved === 'true'; } } catch {} return false; }); // ISO visibility toggle for all boards const [showIsoPins, setShowIsoPins] = useState(() => { try { if (!boardId) return false; const saved = localStorage.getItem(`board-${boardId}-show-iso`); return saved === 'true'; } catch { return false; } }); // Placeholder pins visibility toggle for all boards const [showPlaceholderPins, setShowPlaceholderPins] = useState(() => { try { if (!boardId) return true; // Show by default const saved = localStorage.getItem(`board-${boardId}-show-placeholder`); if (saved !== null) return saved !== 'false'; // Default to true unless explicitly false // Backward compatibility: old key used to be "show-reference" const legacy = localStorage.getItem(`board-${boardId}-show-reference`); return legacy !== 'false'; } catch { return true; } }); const [searchQuery, setSearchQuery] = useState(() => { // Primeiro tenta ler da URL const urlSearch = searchParams.get('search'); if (urlSearch) { return urlSearch; } // Se não houver na URL, tenta ler do localStorage try { if (boardId) { const saved = localStorage.getItem(`board-${boardId}-search-query`); return saved || ''; } } catch {} return ''; }); // Removido modo de edição - pins sempre editáveis individualmente // TrashBin removed: permanent delete only const [orderedPins, setOrderedPins] = useState([]); const [filteredPins, setFilteredPins] = useState([]); // Cache of favorite pin IDs for the board owner (used for All Pins favorites filter) const favoritePinIdsSetRef = useRef | null>(null); const [editingPin, setEditingPin] = useState(null); // Estados para ordenação de pins const [pinSortBy, setPinSortBy] = useState(() => { // Primeiro tenta ler da URL const urlSort = searchParams.get('sort'); if ( urlSort && ['custom', 'recent', 'name-asc', 'name-desc', 'liked', 'commented'].includes(urlSort) ) { return urlSort as PinSortOption; } // Se não houver na URL, tenta ler do localStorage try { if (boardId) { const saved = localStorage.getItem(`board-${boardId}-pin-sort-by`); if ( saved && ['custom', 'recent', 'name-asc', 'name-desc', 'liked', 'commented'].includes(saved) ) { return saved as PinSortOption; } } } catch {} return 'custom'; }); // Grouping mode for boards (excluding All Pins) const [groupBy, setGroupBy] = useState<'none' | 'set' | 'collection' | 'status'>(() => { // Primeiro tenta ler da URL const urlGroup = searchParams.get('group'); if (urlGroup && ['none', 'set', 'collection', 'status'].includes(urlGroup)) { return urlGroup as 'none' | 'set' | 'collection' | 'status'; } // Se não houver na URL, tenta ler do localStorage try { if (boardId) { const saved = localStorage.getItem(`board-${boardId}-group-by`); if (saved && ['none', 'set', 'collection', 'status'].includes(saved)) { return saved as 'none' | 'set' | 'collection' | 'status'; } } } catch {} return 'none'; }); // Highlight pin from search results const highlightPinId = searchParams.get('highlightPin'); const [hasScrolledToHighlight, setHasScrolledToHighlight] = useState(false); // Collapsible state for sticky group headers const [collapsedStatusGroups, setCollapsedStatusGroups] = useState>({}); const [collapsedSetGroups, setCollapsedSetGroups] = useState>({}); // Sync filters with URL parameters and localStorage (persist filters on refresh) useEffect(() => { const params = new URLSearchParams(searchParams); // Update URL params and localStorage based on current filter state if (searchQuery) { params.set('search', searchQuery); try { if (boardId) localStorage.setItem(`board-${boardId}-search-query`, searchQuery); } catch {} } else { params.delete('search'); try { if (boardId) localStorage.removeItem(`board-${boardId}-search-query`); } catch {} } if (tradableFilter !== 'all') { params.set('filter', tradableFilter); try { if (boardId) localStorage.setItem(`board-${boardId}-tradable-filter`, tradableFilter); } catch {} } else { params.delete('filter'); try { if (boardId) localStorage.removeItem(`board-${boardId}-tradable-filter`); } catch {} } if (pinSortBy !== 'custom') { params.set('sort', pinSortBy); try { if (boardId) localStorage.setItem(`board-${boardId}-pin-sort-by`, pinSortBy); } catch {} } else { params.delete('sort'); try { if (boardId) localStorage.removeItem(`board-${boardId}-pin-sort-by`); } catch {} } if (groupBy !== 'none') { params.set('group', groupBy); try { if (boardId) localStorage.setItem(`board-${boardId}-group-by`, groupBy); } catch {} } else { params.delete('group'); try { if (boardId) localStorage.removeItem(`board-${boardId}-group-by`); } catch {} } if (showFavoritesOnly) { params.set('favorites', 'true'); try { if (boardId) localStorage.setItem(`board-${boardId}-favorites-only`, 'true'); } catch {} } else { params.delete('favorites'); try { if (boardId) localStorage.removeItem(`board-${boardId}-favorites-only`); } catch {} } // Only update URL if params actually changed (avoid infinite loops) const newParamsString = params.toString(); const currentParamsString = searchParams.toString(); if (newParamsString !== currentParamsString) { setSearchParams(params, { replace: true }); } }, [searchQuery, tradableFilter, pinSortBy, groupBy, showFavoritesOnly, boardId]); // Estados para controle de cliques múltiplos const [lastClickTime, setLastClickTime] = useState(0); const [isModalOpening, setIsModalOpening] = useState(false); // Estados para profile do dono do board (definidos após currentBoard) const [boardOwnerData, setBoardOwnerData] = useState(null); const [profileActiveTab, setProfileActiveTab] = useState<'boards' | 'listings' | 'wishlist'>( 'boards' ); const [showProfileMenu, setShowProfileMenu] = useState(false); const [showPhotoEditor, setShowPhotoEditor] = useState(false); // Check if viewing own profile const isOwnProfile = user && boardOwnerData && boardOwnerData.id === user.id; // Auto-start My Pins tour for new users (only once, on first visit) useEffect(() => { // Only run tour if this is the user's own profile page (My Pins) if (isOwnProfile && user && !hasSeenTour(TourType.MY_PINS)) { // Delay to ensure components are rendered const timer = setTimeout(() => { markTourAsSeen(TourType.MY_PINS); runTour(TourType.MY_PINS); }, 2000); return () => clearTimeout(timer); } }, [isOwnProfile, user?.id]); // Only depend on user ID and isOwnProfile to run once per user const handleTourComplete = () => { completeTour(TourType.MY_PINS); }; // Condições para mostrar a visualização agrupada const isAllPinsBoard = boardId?.startsWith('all-pins-'); const allPinsUserId = isAllPinsBoard ? boardId?.replace('all-pins-', '') : undefined; // Condições para mostrar a visualização agrupada por Status (All Pins) const showGroupedView = groupBy === 'status' && isAllPinsBoard && tradableFilter === 'all' && !showFavoritesOnly && searchQuery.trim() === ''; // Group-by views const showGroupedBySetView = groupBy === 'set'; const showGroupedByCollectionView = groupBy === 'collection'; // Agrupar pins por status const groupedPinsByStatus = useMemo(() => { if (!showGroupedView) return {}; const groups: { [key: string]: UIPin[] } = {}; filteredPins.forEach((pin) => { const status = pin.availability || 'display'; // 'display' as default if (!groups[status]) { groups[status] = []; } groups[status].push(pin); }); return groups; }, [filteredPins, showGroupedView]); // Group pins by Set for normal boards const groupedPinsBySet = useMemo(() => { if (!showGroupedBySetView) return {} as { [key: string]: UIPin[] }; const groups: { [key: string]: UIPin[] } = {}; filteredPins.forEach((pin) => { const pinAny = pin as unknown as Record; const setKey = (pinAny?.set_id as string | undefined) || 'no-set'; if (!groups[setKey]) groups[setKey] = []; groups[setKey].push(pin); }); return groups; }, [filteredPins, showGroupedBySetView]); // Labels for Set groups const setGroupLabels: Record = useMemo(() => { const labels: Record = {}; Object.entries(groupedPinsBySet).forEach(([key, pins]) => { if (key === 'no-set') { labels[key] = pinsTexts('board.group.noSet', { defaultValue: 'No Set' }); return; } const first: any = pins[0] as any; labels[key] = first?.set_name || pinsTexts('board.group.unknownSet', { defaultValue: 'Unknown Set' }); }); return labels; }, [groupedPinsBySet]); // Group pins by Collection const groupedPinsByCollection = useMemo(() => { if (!showGroupedByCollectionView) return {} as { [key: string]: UIPin[] }; const groups: { [key: string]: UIPin[] } = {}; filteredPins.forEach((pin) => { const anyP = pin as unknown as Record; const key = (anyP?.collection_id as string | undefined) || (anyP?.collection_name as string | undefined) || (pin.origin || '' ? `origin:${String(pin.origin)}` : 'no-collection'); if (!groups[key]) groups[key] = []; groups[key].push(pin); }); return groups; }, [filteredPins, showGroupedByCollectionView]); const collectionGroupLabels: Record = useMemo(() => { const labels: Record = {}; Object.entries(groupedPinsByCollection).forEach(([key, pins]) => { if (key === 'no-collection') { labels[key] = pinsTexts('board.group.noCollection', { defaultValue: 'No Collection' }); return; } const first: any = pins[0] as any; const tagBased = Array.isArray(first?.tags) ? (first.tags as string[]).find((t) => /collection/i.test(t)) : ''; labels[key] = first?.collection_name || tagBased || first?.origin || pinsTexts('board.group.unknownCollection', { defaultValue: 'Unknown Collection' }); }); return labels; }, [groupedPinsByCollection]); const statusOrder = ['display', 'sale', 'trade', 'sale_trade', 'iso']; const groupLabels: Record = { display: 'Display Only (NFT/NFS)', sale: 'For Sale', trade: 'For Trade', sale_trade: 'For Sale or Trade', iso: 'In Search Of', }; // View mode options for dropdown const viewModeOptions = [ { value: 'compact', label: pinsTexts('board.viewModes.compact', { defaultValue: 'Compact View' }), icon: , }, { value: 'grid', label: pinsTexts('board.viewModes.grid', { defaultValue: 'Grid View' }), icon: , }, ]; // Status/Filter options for dropdown const statusOptions = [ { value: 'all', label: pinsTexts('board.statusOptions.all', { defaultValue: 'All' }), icon: , }, { value: 'display', label: pinsTexts('board.statusOptions.display', { defaultValue: 'Display Only (NFT/NFS)' }), }, { value: 'trade', label: pinsTexts('board.statusOptions.trade', { defaultValue: 'For Trade Only (FTO)' }), }, { value: 'sale', label: pinsTexts('board.statusOptions.sale', { defaultValue: 'For Sale Only (FSO)' }), }, { value: 'sale_trade', label: pinsTexts('board.statusOptions.sale_trade', { defaultValue: 'For Sale or Trade (FS/FT)', }), }, { value: 'iso', label: pinsTexts('board.statusOptions.iso', { defaultValue: 'In Search Of (ISO)' }), }, ]; // Sort options for dropdown const sortOptionsForToolbar = [ { value: 'name-asc', label: pinsTexts('board.sort.nameAsc', { defaultValue: 'Name (A-Z)' }) }, { value: 'name-desc', label: pinsTexts('board.sort.nameDesc', { defaultValue: 'Name (Z-A)' }) }, { value: 'collection', label: pinsTexts('board.sort.collection', { defaultValue: 'Collection' }), }, { value: 'set', label: pinsTexts('board.sort.set', { defaultValue: 'Set' }) }, ]; // --- Mobile avatar sizing to mirror PublicProfile header --- const headerInfoRef = useRef(null); const [avatarDynamicSize, setAvatarDynamicSize] = useState(null); const [isSmUp, setIsSmUp] = useState(false); useEffect(() => { if (typeof window === 'undefined') return; const mq = window.matchMedia('(min-width: 640px)'); const update = () => setIsSmUp(mq.matches); update(); mq.addEventListener('change', update); return () => mq.removeEventListener('change', update); }, []); // Observar quando o header móvel é ocultado/exibido (atributo em ) useEffect(() => { try { const update = () => { const hidden = document.documentElement.hasAttribute('data-ui-chrome-hidden'); setIsUiChromeHidden(Boolean(hidden)); }; update(); const observer = new MutationObserver(() => update()); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-ui-chrome-hidden'], }); return () => observer.disconnect(); } catch {} }, []); useEffect(() => { if (!headerInfoRef.current) return; const element = headerInfoRef.current; const resize = () => { const height = Math.round(element.getBoundingClientRect().height); const minHeight = 72; setAvatarDynamicSize(Math.max(minHeight, height)); }; resize(); const ro = new ResizeObserver(() => resize()); ro.observe(element); window.addEventListener('resize', resize); return () => { ro.disconnect(); window.removeEventListener('resize', resize); }; }, [headerInfoRef, isSmUp]); // Estado para sistema de undo (Ctrl+Z / Cmd+Z) const [lastDeletedPin, setLastDeletedPin] = useState<{ pin: UIPin; timestamp: number; } | null>(null); // Estado para detectar quando modais estão abertos (desabilita drag) const isAnyModalOpen = selectedPin !== null || showAddPinModal || showShareModal || editingPin !== null || showBulkUploadModal; // Estados para drag and drop const [droppedImageFile, setDroppedImageFile] = useState(null); const [activeId, setActiveId] = useState(null); const mousePositionRef = useRef({ x: 0, y: 0 }); // Check if this is the special "All Pins" virtual board // (Variables already declared above) // ✨ TanStack Query hooks centralizados const { data: board, isLoading: boardLoading, error: boardError, } = useBoard(!isAllPinsBoard ? boardId : undefined); const { data: boardPins = [], isLoading: boardPinsLoading, error: boardPinsError, } = useBoardPins(!isAllPinsBoard ? boardId : undefined, user?.id, { ensurePlaceholders: Boolean(showPlaceholderPins), }); // All Pins virtual board: source pins from user's boards aggregation const { data: allPinsUserPins = [], isLoading: userPinsLoading, error: userPinsError, } = useUserPinsInBoards(isAllPinsBoard ? (allPinsUserId as string) : undefined); // Global owned pins count for the board owner (must match "All Pins" and exclude ISO/REF). // We intentionally fetch "pins in boards" (physical statuses) to keep counting consistent across views. const boardOwnerIdForPinsCount = isAllPinsBoard ? (allPinsUserId as string | undefined) : ((board as any)?.userId ?? (board as any)?.user_id); const { data: boardOwnerPinsInBoards = [] } = useUserPinsInBoards( boardOwnerIdForPinsCount ? String(boardOwnerIdForPinsCount) : undefined ); // Boards list (used for bulk move-to-board) const { data: userBoards = [] } = useUserBoards(user?.id, true); // Use the appropriate pins and loading states based on board type const pins = isAllPinsBoard ? allPinsUserPins : boardPins; const pinsLoading = isAllPinsBoard ? userPinsLoading : boardPinsLoading; const pinsError = isAllPinsBoard ? userPinsError : boardPinsError; // Reset error log ref when boardId changes useEffect(() => { errorLoggedRef.current = null; // Reset bulk selection state when navigating between boards setIsSelectMode(false); setSelectedPinIds(new Set()); }, [boardId]); // Create virtual board object for "All Pins" view const virtualBoard = isAllPinsBoard && allPinsUserId ? { id: boardId!, name: pinsTexts('allPins.title', { defaultValue: 'All Pins' }), description: pinsTexts('allPins.subtitle', { defaultValue: 'All your pins in one place', }), isPrivate: false, isPublic: true, pinCount: pins.length, lastUpdated: new Date().toISOString(), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), userId: allPinsUserId, coverImageUrl: pins.length > 0 ? pins[0]?.imageUrl || pins[0]?.image : undefined, } : null; // Use virtual board if this is All Pins view, otherwise use regular board const currentBoard = virtualBoard || board; // Get board owner ID for hooks const boardOwnerId = currentBoard?.userId || currentBoard?.user_id; // When viewing your own board, the owned check indicator is redundant const isViewingOwnBoard = Boolean(user && boardOwnerId && boardOwnerId === user.id); // Apply noindex for All Pins virtual route (SEO guard) useNoIndex(Boolean(isAllPinsBoard)); // Pull to refresh handler const handlePullRefresh = useCallback(async () => { console.log('🔄 Pull to refresh - reloading board pins'); if (isAllPinsBoard && allPinsUserId) { await queryClient.refetchQueries({ queryKey: queryKeys.pins.byUser(allPinsUserId), }); } else if (boardId && user?.id) { await queryClient.refetchQueries({ // Prefix match to cover both ensurePlaceholders on/off variants queryKey: ['pins', 'board', boardId], }); } refreshOwnership?.(); }, [isAllPinsBoard, allPinsUserId, boardId, user?.id, queryClient, refreshOwnership]); // Pull to refresh hook const { containerRef: pullToRefreshContainerRef, isPulling, isRefreshing, pullDistance, progress, touchHandlers, } = usePullToRefresh({ onRefresh: handlePullRefresh, threshold: 80, disabled: pinsLoading || boardLoading, }); // Load favorite pins for the owner when in All Pins view (once) useEffect(() => { const loadFavoritePins = async () => { try { if (!isAllPinsBoard || !allPinsUserId) return; if (favoritePinIdsSetRef.current) return; // Already loaded setFavoritePinIdsLoading(true); const favoriteIds = await pinFavoritesService.getFavoritePinIds(allPinsUserId); favoritePinIdsSetRef.current = new Set(favoriteIds); } catch (e) { console.warn('Failed to load favorite pins for All Pins view', e); } finally { setFavoritePinIdsLoading(false); } }; loadFavoritePins(); }, [isAllPinsBoard, allPinsUserId]); // Telemetry: profile_all_pins_view useEffect(() => { if (!isAllPinsBoard || !allPinsUserId) return; const state = (location.state as any) || {}; analyticsService.trackEvent({ event: 'profile_all_pins_view', category: 'navigation', action: 'view', metadata: { userId: allPinsUserId, pinCount: Array.isArray(pins) ? pins.length : 0, source: state.returnTo === 'profile-boards' ? 'profile' : 'direct', hasStateReturn: Boolean(state.returnTo), filters: { search: searchQuery, sort: pinSortBy, view: viewMode, status: tradableFilter, }, }, }); }, [ isAllPinsBoard, allPinsUserId, pins, searchQuery, pinSortBy, viewMode, tradableFilter, location.state, ]); // Hook para contadores das abas - sempre chamados no topo com valor consistente const { count: wishlistCount } = useWishlistCount(boardOwnerId || ''); const { count: listingsCount } = useListingsCount(boardOwnerId || ''); // ✨ Mutations com invalidação automática const updatePinOrderMutation = useUpdatePinOrder(); const addPinToBoardMutation = useAddPinToBoard(); const removePinFromBoardMutation = useRemovePinFromBoard(); // Buscar dados do dono do board - moved to top to avoid hooks order issues useEffect(() => { const fetchBoardOwner = async () => { if (!currentBoard?.userId && !currentBoard?.user_id) return; try { const ownerData = await usersService.getById(currentBoard?.userId || currentBoard?.user_id); setBoardOwnerData(ownerData); } catch (error) { console.error('Error fetching board owner:', error); } }; fetchBoardOwner(); }, [currentBoard?.userId, currentBoard?.user_id]); // Hook para gerenciar drag and drop global - moved to top const { isDragging, isOverTarget } = useGlobalDragAndDrop({ onImageDrop: (file: File) => { console.log(' Image dropped on board:', file.name); setDroppedImageFile(file); setShowAddPinModal(true); showToast( pinsTexts('board.dragDrop.imageReady', { name: file.name, defaultValue: 'Image "{{name}}" ready to be added as pin!', }), 'success' ); }, enabled: isAuthenticated && isOwnProfile && !isAllPinsBoard, // Disable drag-drop add on All Pins (view-only) }); // Add drag and drop sensors - LONG PRESS ACTIVATION for mobile UX - moved to top const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { delay: 0, tolerance: 0, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); // Track mouse position during drag useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (activeId) { mousePositionRef.current = { x: e.clientX, y: e.clientY }; } }; if (activeId) { document.addEventListener('mousemove', handleMouseMove); return () => document.removeEventListener('mousemove', handleMouseMove); } }, [activeId]); // Effect for board not found redirect - must be at component level to follow hooks rules useEffect(() => { if (!currentBoard) { const timer = setTimeout(() => { navigate(`/${user?.username || user?.id}`, { replace: true }); }, 3000); return () => clearTimeout(timer); } }, [currentBoard, navigate]); // Apply global no-select when in Custom reorder mode to avoid OS text selection on long-press useEffect(() => { try { if (typeof document !== 'undefined') { if (pinSortBy === 'custom') { document.body.classList.add('no-select'); } else { document.body.classList.remove('no-select'); } } } catch {} return () => { try { if (typeof document !== 'undefined') { document.body.classList.remove('no-select'); } } catch {} }; }, [pinSortBy]); // ✅ FIX: Memoization and optimization of useEffect to avoid infinite loop useEffect(() => { // Evitar re-execução desnecessária se pins não mudaram realmente if (!pins || pins.length === 0) { if (orderedPins.length > 0) { setOrderedPins([]); } return; } // 🎯 FILTER OUT ISO PINS: ISO pins should only appear in Wishlist, not in regular boards let pinsToDisplay = pins; // Ensure uniqueness by id to prevent duplicates from upstream const uniqueById = (arr: UIPin[]) => { const seen = new Set(); const result: UIPin[] = []; for (const p of arr) { const key = String(p?.id ?? ''); if (!key) continue; if (seen.has(key)) continue; seen.add(key); result.push(p); } return result; }; // Default sort: Collection → Set → ISO; if there is no ISO, fallback to alphabetical const sortByCollectionSetIso = (arr: UIPin[]): UIPin[] => { const hasAnyIso = arr.some((p) => p.availability === 'iso'); const getCollection = (p: UIPin): string => { const fromTags = Array.isArray((p as any).tags) ? ((p as any).tags as string[]).find((t) => /collection/i.test(t)) : ''; const origin = (p.origin || '').toString(); return (fromTags || origin || '').toString(); }; const getSet = (p: UIPin): string => { return (p.series || (p as any).set || '').toString(); }; const byNameAsc = (a: UIPin, b: UIPin) => (a.name || '').localeCompare(b.name || ''); return [...arr].sort((a, b) => { // 1) Collection asc, items with collection come before items without const aCol = getCollection(a); const bCol = getCollection(b); if (aCol && bCol) { const cmp = aCol.localeCompare(bCol); if (cmp !== 0) return cmp; } else if (aCol && !bCol) { return -1; } else if (!aCol && bCol) { return 1; } // 2) Set asc, items with set come before items without const aSet = getSet(a); const bSet = getSet(b); if (aSet && bSet) { const cmp = aSet.localeCompare(bSet); if (cmp !== 0) return cmp; } else if (aSet && !bSet) { return -1; } else if (!aSet && bSet) { return 1; } // 3) ISO last (only if there is at least one ISO in the dataset) if (hasAnyIso) { const aIso = a.availability === 'iso'; const bIso = b.availability === 'iso'; if (aIso !== bIso) return aIso ? 1 : -1; // non-ISO first, ISO last // tie-breaker by name return byNameAsc(a, b); } // Fallback: alphabetical by name when no ISO exists return byNameAsc(a, b); }); }; // Check if this is a wishlist board or if ISO filter is specifically selected const isWishlistBoard = currentBoard?.name?.toLowerCase().includes('wishlist') || currentBoard?.name?.toLowerCase().includes('wish list') || boardId?.includes('wishlist'); // ISO visibility control (global per board) // All Pins: always show ISO pins (no filtering here) // Other boards: always show ISO pins but with opacity control via dimIsoPins // The opacity is handled by the grid components, not by filtering here // Only filter ISO pins if specifically filtering for non-ISO availability if (!isAllPinsBoard && tradableFilter !== 'all' && tradableFilter !== 'iso') { // When filtering for specific availability (display, trade, sale, sale_trade), exclude ISO pinsToDisplay = pins.filter((pin: UIPin) => pin.availability !== 'iso'); } pinsToDisplay = uniqueById(pinsToDisplay); // Check if pins actually changed (comparison by IDs and length) const newPinIds = pinsToDisplay .map((p: UIPin) => p.id) .sort() .join(','); const currentPinIds = orderedPins .map((p: UIPin) => p.id) .sort() .join(','); // Se os IDs são iguais e o comprimento é o mesmo, não fazer nada // EXCEÇÃO: Se um novo pin foi adicionado (pins.length > orderedPins.length), sempre atualizar if ( newPinIds === currentPinIds && pinsToDisplay.length === orderedPins.length && pinsToDisplay.length <= orderedPins.length ) { return; } // Load saved order from localStorage or use default order const savedOrder = localStorage.getItem(`board-${boardId}-pin-order`); if (savedOrder) { try { const orderIds = JSON.parse(savedOrder); // Manter ordem salva + adicionar novos pins no final const orderedPinsList = uniqueById( orderIds .map((id: string) => pinsToDisplay.find((pin: UIPin) => pin.id === id)) .filter(Boolean) .concat(pinsToDisplay.filter((pin: UIPin) => !orderIds.includes(pin.id))) ); setOrderedPins(orderedPinsList); } catch { setOrderedPins(sortByCollectionSetIso(uniqueById(pinsToDisplay))); } } else { setOrderedPins(sortByCollectionSetIso(uniqueById(pinsToDisplay))); } }, [pins, boardId, tradableFilter, currentBoard?.name, showIsoPins]); // include showIsoPins // Filter and sort pins based on search query, tradable status and sort option useEffect(() => { let filtered = orderedPins; const getSetKey = (pin: UIPin): string => { const pinAny = pin as unknown as Record; return ( (pinAny?.set_id as string | undefined) || (pin.series as string | undefined) || (pinAny?.set as string | undefined) || 'no-set' ); }; // Filter by search query if (searchQuery.trim()) { const query = searchQuery.toLowerCase().trim(); filtered = filtered.filter( (pin) => pin.name?.toLowerCase().includes(query) || pin.description?.toLowerCase().includes(query) || pin.origin?.toLowerCase().includes(query) || pin.series?.toLowerCase().includes(query) ); } // Filter by tradable status if (tradableFilter !== 'all') { filtered = filtered.filter((pin) => { if (tradableFilter === 'display') { return pin.availability === 'display'; } else if (tradableFilter === 'trade') { return pin.availability === 'trade'; } else if (tradableFilter === 'sale') { return pin.availability === 'sale'; } else if (tradableFilter === 'sale_trade') { return pin.availability === 'sale_trade'; } else if (tradableFilter === 'iso') { return pin.availability === 'iso'; } else if (tradableFilter === 'placeholder') { return pin.availability === 'placeholder'; } return true; }); } // Placeholder pins visibility and scoping: // - Hide placeholders: show only physically owned pins (non-placeholder) // - Show placeholders: show placeholder pins ONLY for sets where the user has at least one non-placeholder pin // (so it acts as "missing pins reference" rather than flooding the board) if (!showPlaceholderPins) { filtered = filtered.filter((pin) => pin.availability !== 'placeholder'); } else { // In "placeholder-only" filter, there are no non-placeholder pins in `filtered`, // so we derive the owned sets from the full board list (orderedPins). const sourceForOwnedSets = tradableFilter === 'placeholder' ? orderedPins : // In "all" view, derive from what's currently visible after search/status filtering filtered; const ownedSetKeys = new Set(); for (const pin of sourceForOwnedSets) { if (pin.availability === 'placeholder') continue; const key = getSetKey(pin); if (!key || key === 'no-set') continue; ownedSetKeys.add(String(key)); } if (ownedSetKeys.size === 0) { // If user has no owned pins in view, don't show any placeholder pins. filtered = filtered.filter((pin) => pin.availability !== 'placeholder'); } else { filtered = filtered.filter((pin) => { if (pin.availability !== 'placeholder') return true; const key = getSetKey(pin); return key !== 'no-set' && ownedSetKeys.has(String(key)); }); } } // Filter by favorites (only for All Pins when status filter is 'all') if (isAllPinsBoard && tradableFilter === 'all' && showFavoritesOnly && user?.id) { // We'll use a set of favorite pin ids stored in local state; for performance, // we rely on a memoized set loaded once per board owner via API below. if (favoritePinIdsSetRef.current) { filtered = filtered.filter((pin) => favoritePinIdsSetRef.current!.has(pin.id)); } } // Sort pins const sorted = [...filtered].sort((a, b) => { const getCollection = (p: UIPin): string => { // Try to infer collection from tags or origin; fallback empty const fromTags = Array.isArray((p as any).tags) ? ((p as any).tags as string[]).find((t) => /collection/i.test(t)) : ''; const origin = (p.origin || '').toString(); return (fromTags || origin || '').toString(); }; const getSet = (p: UIPin): string => { return (p.series || (p as any).set || '').toString(); }; switch (pinSortBy) { case 'name-asc': return (a.name || '').localeCompare(b.name || ''); case 'name-desc': return (b.name || '').localeCompare(a.name || ''); case 'collection': { const aCol = getCollection(a); const bCol = getCollection(b); if (aCol && bCol) { const byCol = aCol.localeCompare(bCol); return byCol !== 0 ? byCol : (a.name || '').localeCompare(b.name || ''); } if (aCol && !bCol) return -1; if (!aCol && bCol) return 1; return (a.name || '').localeCompare(b.name || ''); } case 'set': { const aSet = getSet(a); const bSet = getSet(b); if (aSet && bSet) { const bySet = aSet.localeCompare(bSet); return bySet !== 0 ? bySet : (a.name || '').localeCompare(b.name || ''); } if (aSet && !bSet) return -1; if (!aSet && bSet) return 1; return (a.name || '').localeCompare(b.name || ''); } default: return 0; } }); // Ensure uniqueness to avoid any duplicate rendering const uniqueSorted = (() => { const seen = new Set(); const res: UIPin[] = []; for (const p of sorted) { const key = String(p?.id ?? ''); if (!key || seen.has(key)) continue; seen.add(key); res.push(p); } return res; })(); setFilteredPins(uniqueSorted); }, [ orderedPins, tradableFilter, pinSortBy, searchQuery, isAllPinsBoard, showFavoritesOnly, showPlaceholderPins, user?.id, ]); // Scroll to and highlight pin from search results useEffect(() => { if (highlightPinId && filteredPins.length > 0 && !hasScrolledToHighlight) { // Wait for DOM to render const timer = setTimeout(() => { const pinElement = document.querySelector(`[data-pin-id="${highlightPinId}"]`); if (pinElement) { pinElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Add highlight effect pinElement.classList.add( 'ring-4', 'ring-yellow-400', 'dark:ring-yellow-500', 'rounded-lg' ); // Remove highlight after 3 seconds setTimeout(() => { pinElement.classList.remove( 'ring-4', 'ring-yellow-400', 'dark:ring-yellow-500', 'rounded-lg' ); // Remove highlightPin from URL after animation const params = new URLSearchParams(searchParams); params.delete('highlightPin'); setSearchParams(params, { replace: true }); }, 3000); setHasScrolledToHighlight(true); } }, 500); return () => clearTimeout(timer); } }, [highlightPinId, filteredPins, hasScrolledToHighlight, searchParams, setSearchParams]); // Sistema de undo com Ctrl+Z / Cmd+Z useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { // Check if it's Ctrl+Z (Windows/Linux) or Cmd+Z (Mac) const isUndo = (event.ctrlKey || event.metaKey) && event.key === 'z' && !event.shiftKey; if (isUndo && lastDeletedPin) { // Check if deletion was recent (maximum 30 seconds) const timeSinceDelete = Date.now() - lastDeletedPin.timestamp; if (timeSinceDelete <= 30000) { // 30 segundos event.preventDefault(); handleUndoDelete(); } } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [lastDeletedPin]); // Auto-clear undo state after 30 seconds useEffect(() => { if (lastDeletedPin) { const timeout = setTimeout(() => { setLastDeletedPin(null); }, 30000); // 30 segundos return () => clearTimeout(timeout); } }, [lastDeletedPin]); // Function to undo deletion const handleUndoDelete = async () => { if (!lastDeletedPin || !boardId) return; try { // Restore pin from trash await handleRestorePin(lastDeletedPin.pin); // Limpar estado de undo setLastDeletedPin(null); showToast( pinsTexts('board.toasts.pinRestored', { name: lastDeletedPin.pin.name || pinsTexts('untitledPin', { defaultValue: 'Untitled Pin' }), defaultValue: 'Pin "{{name}}" restored! 🎉', }), 'success' ); } catch (error) { console.error('Error undoing delete:', error); showToast( pinsTexts('board.restoreFailed', { defaultValue: 'Failed to restore pin' }), 'error' ); } }; // Check if current user owns this board const isOwner = user && currentBoard && (currentBoard.userId === user.id || currentBoard.user_id === user.id); // Determine if user has editing privileges (authenticated and is owner) const canEdit = isAuthenticated && isOwner && !isAllPinsBoard; // Disable editing for All Pins virtual board const canAddPins = isAuthenticated && isOwnProfile && !isAllPinsBoard; // All Pins is view-only (no direct insertion) const hasFullAccess = isAuthenticated; // 🎯 NEW: Allow individual pin editing in All Pins board const canEditPins = isAuthenticated; // ✅ FIXED: Pin editing should depend on pin ownership, not board ownership const canReorderPins = canEdit; // Only allow reordering in regular boards const canDeletePins = isAuthenticated && isOwner; // --- Bulk selection helpers (mobile-first) --- const selectedCount = selectedPinIds.size; const clearSelection = useCallback(() => { setSelectedPinIds(new Set()); }, []); const toggleSelectMode = useCallback(() => { setIsSelectMode((prev) => { const next = !prev; if (next) { // Close any open pin modals to avoid conflicting interactions setSelectedPin(null); setEditingPin(null); } else { setSelectedPinIds(new Set()); } return next; }); }, []); const togglePinSelected = useCallback((pinId: string) => { if (!pinId) return; setSelectedPinIds((prev) => { const next = new Set(prev); if (next.has(pinId)) next.delete(pinId); else next.add(pinId); return next; }); }, []); const selectAllVisiblePins = useCallback(() => { setSelectedPinIds(new Set(filteredPins.map((p) => p.id))); }, [filteredPins]); // Keep selection consistent with what is visible (avoid selecting hidden pins after filters change) useEffect(() => { if (!isSelectMode) return; setSelectedPinIds((prev) => { if (prev.size === 0) return prev; const visible = new Set(filteredPins.map((p) => p.id)); const next = new Set(Array.from(prev).filter((id) => visible.has(id))); return next.size === prev.size ? prev : next; }); }, [isSelectMode, filteredPins]); const bulkStatusOptions: BulkStatusOption[] = [ { value: 'display', label: pinsTexts('userPin.availability.display', { defaultValue: 'Display' }), }, { value: 'trade', label: pinsTexts('userPin.availability.trade', { defaultValue: 'For Trade' }), }, { value: 'sale', label: pinsTexts('userPin.availability.sale', { defaultValue: 'For Sale' }), }, { value: 'sale_trade', label: pinsTexts('userPin.availability.sale_trade', { defaultValue: 'Sale/Trade' }), }, { value: 'iso', label: pinsTexts('userPin.availability.iso', { defaultValue: 'In Search Of' }), }, { value: 'placeholder', label: pinsTexts('userPin.availability.placeholder', { defaultValue: 'Placeholder' }), }, ]; const isUuid = (v: unknown): v is string => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(v); const getRealBoardIdForPin = (pin: UIPin): string | undefined => { if (isAllPinsBoard) { const real = (pin as any)?.boardId; return typeof real === 'string' && real.length > 0 ? real : undefined; } return boardId; }; const handleBulkRemoveSelected = async () => { if (!canDeletePins) return; if (isBulkProcessing) return; if (selectedPinIds.size === 0) return; setIsBulkProcessing(true); try { const pinMap = new Map(); for (const p of orderedPins) pinMap.set(p.id, p); for (const p of filteredPins) pinMap.set(p.id, p); const removals: Array<{ pinId: string; boardId: string }> = []; for (const pinId of selectedPinIds) { const pin = pinMap.get(pinId); if (!pin) continue; const realBoardId = getRealBoardIdForPin(pin); if (!realBoardId) continue; removals.push({ pinId, boardId: realBoardId }); } if (removals.length === 0) { showToast( pinsTexts('board.toasts.deleteFailed', { defaultValue: 'Failed to remove pin' }), 'error' ); return; } for (const r of removals) { await removePinFromBoardMutation.mutateAsync({ pinId: r.pinId, boardId: r.boardId }); } const removedIdSet = new Set(removals.map((r) => r.pinId)); setOrderedPins((prev) => prev.filter((p) => !removedIdSet.has(p.id))); setFilteredPins((prev) => prev.filter((p) => !removedIdSet.has(p.id))); setSelectedPinIds(new Set()); showToast( pinsTexts('board.toasts.removedFromBoard', { defaultValue: 'Pin removed from board successfully!', }), 'success' ); // Ensure queries refresh across boards/all pins queryClient.invalidateQueries({ queryKey: queryKeys.pins.all }); } catch (e) { console.error('Bulk remove failed:', e); showToast( pinsTexts('board.toasts.deleteFailed', { defaultValue: 'Failed to remove pin' }), 'error' ); } finally { setIsBulkProcessing(false); } }; const handleBulkMoveToBoard = async (targetBoardId: string) => { if (!user?.id) return; if (isBulkProcessing) return; if (selectedPinIds.size === 0) return; if (!isUuid(targetBoardId)) return; setIsBulkProcessing(true); try { const pinMap = new Map(); for (const p of orderedPins) pinMap.set(p.id, p); for (const p of filteredPins) pinMap.set(p.id, p); const movedOutOfCurrentBoard = new Set(); for (const pinId of selectedPinIds) { const pin = pinMap.get(pinId); if (!pin) continue; const oldBoardId = getRealBoardIdForPin(pin); if (!oldBoardId) continue; if (oldBoardId === targetBoardId) continue; const payload: Record = { boardId: targetBoardId, userId: user.id }; if (isUuid(oldBoardId)) { payload.oldBoardId = oldBoardId; } const res = await makeApiRequest(`/api/pins/${pinId}/board`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) { const txt = await res.text().catch(() => ''); throw new Error(txt || `Failed to move pin (${res.status})`); } // If we are viewing a real board, pins moved away should disappear from this view if (!isAllPinsBoard && boardId && oldBoardId === boardId) { movedOutOfCurrentBoard.add(pinId); } } if (movedOutOfCurrentBoard.size > 0) { setOrderedPins((prev) => prev.filter((p) => !movedOutOfCurrentBoard.has(p.id))); setFilteredPins((prev) => prev.filter((p) => !movedOutOfCurrentBoard.has(p.id))); } else if (isAllPinsBoard) { // Update local boardId references for correctness in All Pins view setOrderedPins((prev) => prev.map((p) => selectedPinIds.has(p.id) ? ({ ...(p as any), boardId: targetBoardId } as any) : p ) ); setFilteredPins((prev) => prev.map((p) => selectedPinIds.has(p.id) ? ({ ...(p as any), boardId: targetBoardId } as any) : p ) ); } setSelectedPinIds(new Set()); showToast( pinsTexts('board.bulkActions.moved', { defaultValue: 'Pins moved successfully!' }), 'success' ); // Refresh pins + boards lists (counts/last updated) queryClient.invalidateQueries({ queryKey: queryKeys.pins.all }); queryClient.invalidateQueries({ queryKey: queryKeys.boards.byUser(user.id) }); } catch (e) { console.error('Bulk move failed:', e); showToast( pinsTexts('board.bulkActions.moveFailed', { defaultValue: 'Failed to move pins' }), 'error' ); } finally { setIsBulkProcessing(false); } }; const handleBulkChangeStatus = async (status: BulkStatusValue) => { if (!user?.id) return; if (isBulkProcessing) return; if (selectedPinIds.size === 0) return; setIsBulkProcessing(true); try { for (const pinId of selectedPinIds) { const res = await makeApiRequest(`/api/pins/user-pins/${pinId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ availability: status }), }); if (!res.ok) { const txt = await res.text().catch(() => ''); throw new Error(txt || `Failed to change status (${res.status})`); } } setOrderedPins((prev) => prev.map((p) => (selectedPinIds.has(p.id) ? { ...p, availability: status } : p)) ); setFilteredPins((prev) => prev.map((p) => (selectedPinIds.has(p.id) ? { ...p, availability: status } : p)) ); setSelectedPinIds(new Set()); showToast( pinsTexts('board.bulkActions.statusUpdated', { defaultValue: 'Status updated successfully!', }), 'success' ); queryClient.invalidateQueries({ queryKey: queryKeys.pins.all }); } catch (e) { console.error('Bulk status update failed:', e); showToast( pinsTexts('board.bulkActions.statusUpdateFailed', { defaultValue: 'Failed to update status', }), 'error' ); } finally { setIsBulkProcessing(false); } }; // Collision detection (trash removed) const customCollisionDetection: CollisionDetection = useCallback( (args) => rectIntersection(args), [] ); const handleDragStart = (event: DragStartEvent) => { const isDragDisabled = isAnyModalOpen || isSelectMode || !canReorderPins || (viewMode !== 'compact' && pinSortBy !== 'custom'); if (isDragDisabled) { return; } // Disable global text selection and touch callouts during drag (mobile) try { if (typeof document !== 'undefined') { const b = document.body as any; b.style.userSelect = 'none'; b.style.webkitUserSelect = 'none'; b.style.webkitTouchCallout = 'none'; } } catch {} const { active } = event; setActiveId(active.id as string); }; const handleDragEnd = (event: DragEndEvent) => { const isDragDisabled = isAnyModalOpen || isSelectMode || !canReorderPins || (viewMode !== 'compact' && pinSortBy !== 'custom'); if (isDragDisabled) return; const { active, over } = event; // No trash drop target anymore // Reordering is handled inside DraggablePinGrid to avoid double-processing setActiveId(null); // Restore global selection behavior try { if (typeof document !== 'undefined') { const b = document.body as any; b.style.userSelect = ''; b.style.webkitUserSelect = ''; b.style.webkitTouchCallout = ''; } } catch {} }; // Loading and error states if (boardLoading || pinsLoading) { return ; } if (boardError || pinsError) { // ✅ FIX: Log only in development e apenas uma vez por erro if (process.env.NODE_ENV === 'development') { const lastLogged = errorLoggedRef.current; if ( !lastLogged || lastLogged.boardError !== boardError || lastLogged.pinsError !== pinsError ) { console.error('Board or pins error:', { boardError: boardError ? { message: boardError.message, name: boardError.name, stack: boardError.stack, } : null, pinsError: pinsError ? { message: pinsError.message, name: pinsError.name, stack: pinsError.stack, } : null, boardId, }); errorLoggedRef.current = { boardError, pinsError }; } } return (

{commonTexts('error', { defaultValue: 'Error' })}

{pinsTexts('board.status.errorLoadingBoard', { defaultValue: 'Error Loading Board' })}

{boardError ? pinsTexts('board.errors.board', { message: String(boardError.message || ''), defaultValue: 'Board error: {{message}}', }) : pinsTexts('board.errors.pins', { message: String(pinsError?.message || ''), defaultValue: 'Pins error: {{message}}', })}

); } if (!currentBoard) { // ✅ FIX: Log only in development if (process.env.NODE_ENV === 'development') { console.error('Board not found:', { boardId, board }); } return (

{currentBoard?.name || 'Board'}

{pinsTexts('board.status.notFoundTitle', { defaultValue: 'Board Not Found' })}

{pinsTexts('board.status.notFoundDescription', { defaultValue: "The board you're looking for could not be found. It may have been deleted or moved. You'll be redirected to your boards in a few seconds.", })}

{pinsTexts('board.status.redirectingInSeconds', { defaultValue: 'Redirecting automatically in 3 seconds...', })}
); } // Handler para mudança de aba do perfil const handleProfileTabChange = (tab: 'boards' | 'listings' | 'wishlist') => { setProfileActiveTab(tab); // Navigate to appropriate route based on tab selection if (!boardOwnerId) return; switch (tab) { case 'boards': navigate(`/${boardOwnerData?.username || boardOwnerId}?tab=boards`); break; case 'listings': navigate(`/${boardOwnerData?.username || boardOwnerId}?tab=listings`); break; case 'wishlist': navigate(`/${boardOwnerData?.username || boardOwnerId}?tab=wishlist`); break; } }; // Handler that receives the full pin object (preferred for updated state) const handlePinClickWithObject = (pin: Pin) => { // Prevent rapid successive clicks (debounce with 300ms) const now = Date.now(); if (now - lastClickTime < 300) { console.log('🚫 Ignoring rapid click - too soon after last click'); return; } // Prevent opening if a modal is already opening if (isModalOpening) { console.log('🚫 Ignoring click - modal already opening'); return; } setLastClickTime(now); setIsModalOpening(true); console.log(' [PinBoardViewPage] handlePinClickWithObject called'); console.log('📌 Pin ID:', pin.id); console.log('📌 Pin name:', pin.name); console.log('📌 Pin availability:', pin.availability); console.log('📌 Full pin object:', pin); setFocusOnComment(false); // Reset focus on comment when opened via normal click // Add boardId to pin context so MyPin knows which board the pin is in const pinWithBoardContext = { ...pin, // IMPORTANT: "All Pins" is a virtual board id (not UUID). Preserve the real pin.boardId. boardId: isAllPinsBoard ? (pin as any).boardId : boardId || (pin as any).boardId, }; setSelectedPin(pinWithBoardContext); // Reset the opening flag after a short delay setTimeout(() => { setIsModalOpening(false); }, 500); }; // New: open modal focused on comment with full pin object (keeps updated availability) const handlePinCommentWithObject = (pin: Pin) => { setFocusOnComment(true); // Add boardId to pin context so MyPin knows which board the pin is in const pinWithBoardContext = { ...pin, // IMPORTANT: "All Pins" is a virtual board id (not UUID). Preserve the real pin.boardId. boardId: isAllPinsBoard ? (pin as any).boardId : boardId || (pin as any).boardId, }; setSelectedPin(pinWithBoardContext); // Ensure modal opening is not blocked by debounce setIsModalOpening(false); }; const handlePinClick = (pinId: string | undefined) => { if (!pinId) return; // Prevent rapid successive clicks (debounce with 300ms) const now = Date.now(); if (now - lastClickTime < 300) { console.log('🚫 Ignoring rapid click - too soon after last click'); return; } // Prevent opening if a modal is already opening if (isModalOpening) { console.log('🚫 Ignoring click - modal already opening'); return; } setLastClickTime(now); setIsModalOpening(true); console.log(' PinBoardViewPage: handlePinClick called with pinId:', pinId); // Try to find pin in filtered pins first, then fall back to ordered pins const pin = filteredPins.find((p) => p.id === pinId) || orderedPins.find((p) => p.id === pinId); if (pin) { console.log(' PinBoardViewPage: Setting selectedPin to:', pin.name); setFocusOnComment(false); // Reset focus on comment when opened via normal click // Add boardId to pin context so MyPin knows which board the pin is in const pinWithBoardContext = { ...pin, // IMPORTANT: "All Pins" is a virtual board id (not UUID). Preserve the real pin.boardId. boardId: isAllPinsBoard ? (pin as any).boardId : boardId || (pin as any).boardId, }; setSelectedPin(pinWithBoardContext); // Reset the opening flag after a short delay setTimeout(() => { setIsModalOpening(false); }, 500); // ✅ FIX: Conditional log only in development if (process.env.NODE_ENV === 'development') { console.log(' Pin selected:', pin.name); } } else { setIsModalOpening(false); console.log(' PinBoardViewPage: Pin not found:', pinId); // ✅ FIX: Log only in development if (process.env.NODE_ENV === 'development') { console.error(' Pin not found:', pinId); } } }; const handlePinLike = async (pinId: string | undefined) => { if (!pinId) return; try { // ✅ FIXED: Use new simplified toggle method const result = await pinsService.toggleLike(pinId); showToast(result.message, 'success'); if (isAllPinsBoard) { analyticsService.trackEvent({ event: 'pin_action', category: 'pin', action: 'like_toggle', metadata: { context: 'all_pins', pinId }, }); } // ✅ CRITICAL FIX: Force immediate refetch instead of just invalidate // This ensures fresh data is loaded even if staleTime hasn't expired await queryClient.refetchQueries({ // Prefix match to cover ensurePlaceholders variants queryKey: ['pins', 'board', boardId!], }); // Also invalidate for consistency queryClient.invalidateQueries({ // Prefix match to cover ensurePlaceholders variants queryKey: ['pins', 'board', boardId!], }); } catch (err) { // ✅ FIX: Log only in development if (process.env.NODE_ENV === 'development') { console.error('Error toggling pin like:', err); } showToast( commonTexts('errors.toggleLikeFailed', { defaultValue: 'Failed to toggle like' }), 'error' ); } }; const handlePinSave = async (pinId: string | undefined) => { if (!pinId) return; try { // ✅ FIXED: Use new simplified toggle method const result = await pinsService.toggleSave(pinId); showToast(result.message, 'success'); if (isAllPinsBoard) { analyticsService.trackEvent({ event: 'pin_action', category: 'pin', action: 'save_toggle', metadata: { context: 'all_pins', pinId }, }); } // ✅ CRITICAL FIX: Force immediate refetch instead of just invalidate // This ensures fresh data is loaded even if staleTime hasn't expired await queryClient.refetchQueries({ // Prefix match to cover ensurePlaceholders variants queryKey: ['pins', 'board', boardId!], }); // Also invalidate for consistency queryClient.invalidateQueries({ // Prefix match to cover ensurePlaceholders variants queryKey: ['pins', 'board', boardId!], }); } catch (err) { console.error('Error toggling pin save:', err); showToast( commonTexts('errors.toggleSaveFailed', { defaultValue: 'Failed to toggle save' }), 'error' ); } }; const handlePinComment = (pinId: string | undefined) => { if (!pinId) return; console.log(' PinBoardViewPage: handlePinComment called with pinId:', pinId); if (isAllPinsBoard) { analyticsService.trackEvent({ event: 'pin_action', category: 'pin', action: 'comment_open', metadata: { context: 'all_pins', pinId }, }); } // Find the pin and open the modal with focus on comment input const pin = filteredPins.find((p) => p.id === pinId) || orderedPins.find((p) => p.id === pinId); if (pin) { console.log(' PinBoardViewPage: Setting selectedPin for comment focus:', pin.name); setFocusOnComment(true); // Add boardId to pin context so MyPin knows which board the pin is in const pinWithBoardContext = { ...pin, // IMPORTANT: "All Pins" is a virtual board id (not UUID). Preserve the real pin.boardId. boardId: isAllPinsBoard ? (pin as any).boardId : boardId || (pin as any).boardId, }; setSelectedPin(pinWithBoardContext); // The modal will auto-focus on the comment input when opened via comment click } else { console.log(' PinBoardViewPage: Pin not found for comment:', pinId); } }; const handlePinShare = (pinId: string | undefined) => { if (!pinId) return; // The share functionality is now handled by the PinDetailModal // which will open the SharePinModal when the share button is clicked console.log('Share pin:', pinId); if (isAllPinsBoard) { analyticsService.trackEvent({ event: 'pin_action', category: 'pin', action: 'share_open', metadata: { context: 'all_pins', pinId }, }); } }; const handleUserClick = (userId: string) => { navigate(`/profile/${userId}`); }; const handlePinEdit = (pinId: string | undefined) => { if (!pinId) return; console.log(' PinBoardViewPage: handlePinEdit called with pinId:', pinId); // Find the pin in our data const pin = filteredPins.find((p) => p.id === pinId) || orderedPins.find((p) => p.id === pinId); if (pin) { console.log(' PinBoardViewPage: Setting editingPin to:', pin.name); setEditingPin(pin); } else { console.log(' PinBoardViewPage: Pin not found for edit:', pinId); } }; const handlePinDelete = async (pinId: string | undefined) => { if (!pinId) return; console.log(' PinBoardViewPage: handlePinDelete called with pinId:', pinId); // Find the pin in our data const pin = filteredPins.find((p) => p.id === pinId) || orderedPins.find((p) => p.id === pinId); if (!pin) { console.log(' PinBoardViewPage: Pin not found for delete:', pinId); return; } try { // ✅ Always remove from the real board (All Pins is just a view, never Trash here) const realBoardId = isAllPinsBoard ? ((pin as any)?.boardId as string | undefined) : boardId!; if (!realBoardId) { showToast( pinsTexts('board.toasts.deleteFailed', { defaultValue: 'Failed to remove pin' }), 'error' ); return; } await removePinFromBoardMutation.mutateAsync({ boardId: realBoardId, pinId: pinId, }); // Optimistic UI update setOrderedPins((prev) => prev.filter((p) => p.id !== pinId)); setFilteredPins((prev) => prev.filter((p) => p.id !== pinId)); showToast( pinsTexts('board.toasts.removedFromBoard', { defaultValue: 'Pin removed from board successfully!', }), 'success' ); // Refresh data if (isAllPinsBoard) { // Invalidate both the in-boards query and the general user pins query queryClient.invalidateQueries({ queryKey: ['pins', 'user', allPinsUserId, 'in-boards'], }); queryClient.invalidateQueries({ queryKey: ['pins', 'user', allPinsUserId], }); } else { queryClient.invalidateQueries({ queryKey: ['pins', 'board', boardId, user?.id], }); } } catch (error) { console.error('Error deleting pin:', error); showToast( pinsTexts('board.toasts.deleteFailed', { defaultValue: 'Failed to remove pin' }), 'error' ); } }; const handlePinAdded = async () => { // This will be called by AddPinModal after successful pin creation console.log(' Pin added callback triggered, refreshing data...'); // Force refresh the queries to ensure new pin appears try { // Invalidate queries await queryClient.invalidateQueries({ queryKey: ['pins', 'board', boardId, user?.id] }); await queryClient.invalidateQueries({ queryKey: ['boards', 'detail', boardId] }); // Force immediate refetch await queryClient.refetchQueries({ queryKey: ['pins', 'board', boardId, user?.id] }); console.log(' Queries invalidated and refetched successfully after pin addition'); } catch (error) { console.error(' Error invalidating queries after pin addition:', error); } setShowAddPinModal(false); setDroppedImageFile(null); // Clear the dropped image file }; const handlePinReorder = async (newOrderedPins: UIPin[]) => { console.log( '🔄 Handling pin reorder:', newOrderedPins.map((p) => ({ id: p.id, name: p.name })) ); // Se não estamos no modo 'custom', alternar automaticamente para 'custom' if (pinSortBy !== 'custom') { setPinSortBy('custom'); } // Reorder only the visible subset within the full orderedPins list const visibleDedup = uniqueById(newOrderedPins); const visibleIdQueue = visibleDedup.map((p) => String(p.id)); const visibleIdSet = new Set(visibleIdQueue); // Build a mapping from id to pin (from new visible order) const idToPin = new Map(visibleDedup.map((p) => [String(p.id), p])); // Reconstruct full list preserving non-visible positions and replacing visible pins in new order let nextIndex = 0; const nextOrdered = orderedPins.map((pin) => { const key = String(pin.id); if (visibleIdSet.has(key)) { const nextVisibleId = visibleIdQueue[nextIndex++]; const nextVisiblePin = idToPin.get(nextVisibleId); return nextVisiblePin || pin; } return pin; }); const prevOrderIds = orderedPins.map((p) => p.id); const dedupFull = uniqueById(nextOrdered); setOrderedPins(dedupFull); // Update localStorage for persistence const orderIds = dedupFull.map((p) => p.id); localStorage.setItem(`board-${boardId}-pin-order`, JSON.stringify(orderIds)); // Save order to database try { const response = await makeApiRequest(`/api/pins/board/${boardId}/order`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ pinIds: orderIds }), }); if (!response.ok) { throw new Error('Failed to save pin order'); } console.log(' Pin order saved to database:', orderIds); analyticsService.trackEvent({ event: 'pin_reorder', category: 'pin', action: 'reorder', metadata: { boardId, prevOrder: prevOrderIds, nextOrder: orderIds, count: orderIds.length }, }); // CRÍTICO: Invalidate board queries to update lastUpdated timestamp dinamically if (user?.id && boardId) { await Promise.all([ // Invalidate board details (contains lastUpdated) queryClient.invalidateQueries({ queryKey: ['boards', 'detail', boardId] }), queryClient.invalidateQueries({ queryKey: queryKeys.boards.detail(boardId) }), // Invalidate user boards list (contains lastUpdated for board cards) queryClient.invalidateQueries({ queryKey: ['boards', 'user', user.id] }), queryClient.invalidateQueries({ queryKey: queryKeys.boards.byUser(user.id) }), ]); // FORÇA refetch imediato para garantir atualização do board card await Promise.all([ // Use exact+active to avoid refetching hydrated/inactive queries that have no queryFn yet queryClient.refetchQueries({ queryKey: queryKeys.boards.byUser(user.id), exact: true, type: 'active', }), queryClient.refetchQueries({ queryKey: queryKeys.boards.detail(boardId), exact: true, type: 'active', }), ]); } } catch (error) { console.error(' Error saving pin order:', error); } }; const handleRestorePin = async (deletedPin: any) => { try { // Check if pin already exists to prevent duplication const existingPin = orderedPins.find((p) => p.id === deletedPin.id); if (existingPin) { console.log('Pin already exists, skipping restore:', deletedPin.id); return; } // Add back to the board at original position or end const position = deletedPin.originalPosition ?? orderedPins.length; const newOrderedPins = [...orderedPins]; newOrderedPins.splice(position, 0, deletedPin); setOrderedPins(newOrderedPins); // Update localStorage order const orderIds = newOrderedPins.map((p) => p.id); localStorage.setItem(`board-${boardId}-pin-order`, JSON.stringify(orderIds)); // ✅ FIX: Restore in database try { // Import the pinsService to add pin back to board const { pinsService } = await import('@/services/pinsService'); // Add pin back to board in database await pinsService.addPinToBoard(deletedPin.id, boardId!); console.log(' Pin restored to board in database'); analyticsService.trackEvent({ event: 'pin_restore', category: 'pin', action: 'restore', metadata: { pinId: deletedPin.id, boardId }, }); // Invalidate queries to refresh data queryClient.invalidateQueries({ queryKey: ['boardPins', boardId] }); queryClient.invalidateQueries({ queryKey: ['pins'] }); showToast( pinsTexts('board.toasts.pinRestoredSimple', { name: deletedPin.name, defaultValue: '"{{name}}" restored successfully!', }), 'success' ); } catch (dbError) { console.error(' Error restoring pin in database:', dbError); showToast( pinsTexts('board.toasts.restoreDbFailed', { defaultValue: 'Failed to restore pin in database', }), 'error' ); // Revert local changes if database operation failed const revertedPins = orderedPins.filter((p) => p.id !== deletedPin.id); setOrderedPins(revertedPins); const revertedOrderIds = revertedPins.map((p) => p.id); localStorage.setItem(`board-${boardId}-pin-order`, JSON.stringify(revertedOrderIds)); return; } } catch (error) { console.error('Error restoring pin:', error); showToast( pinsTexts('board.toasts.restoreFailed', { defaultValue: 'Failed to restore pin' }), 'error' ); } }; const handlePermanentDelete = async (pinId: string) => { try { console.log('🔥 Permanently deleting pin:', pinId); // Remove from any local state if needed const updatedPins = orderedPins.filter((p) => p.id !== pinId); setOrderedPins(updatedPins); // Update localStorage order const orderIds = updatedPins.map((p) => p.id); localStorage.setItem(`board-${boardId}-pin-order`, JSON.stringify(orderIds)); // ✅ FIX: Permanently delete from database try { // Import the pinsService to delete the pin const { pinsService } = await import('@/services/pinsService'); // Execute permanent deletion await pinsService.delete(pinId); console.log(' Pin permanently deleted from database'); analyticsService.trackEvent({ event: 'pin_delete', category: 'pin', action: 'delete_permanent', metadata: { pinId, context: 'board', boardId, permanent: true }, }); // Invalidate queries to refresh data queryClient.invalidateQueries({ queryKey: ['boardPins', boardId] }); queryClient.invalidateQueries({ queryKey: ['pins'] }); showToast( pinsTexts('board.toasts.permanentlyDeleted', { defaultValue: 'Pin permanently deleted' }), 'success' ); } catch (dbError) { console.error(' Error permanently deleting pin from database:', dbError); showToast( pinsTexts('board.toasts.permanentDeleteDbFailed', { defaultValue: 'Failed to permanently delete pin from database', }), 'error' ); return; } } catch (error) { console.error('Error permanently deleting pin:', error); showToast( pinsTexts('board.toasts.permanentDeleteFailed', { defaultValue: 'Failed to permanently delete pin', }), 'error' ); } }; // Removido handleToggleEditMode - não há mais modo de edição // Helper: ensure unique pins by id const uniqueById = (arr: UIPin[]) => { const seen = new Set(); const result: UIPin[] = []; for (const p of arr) { if (!p?.id) continue; if (seen.has(p.id)) continue; seen.add(p.id); result.push(p); } return result; }; const handlePinUpdate = (updatedPin: any) => { // Check if pin was moved to a different board const wasMoved = updatedPin._movedFromBoard && updatedPin._movedFromBoard === boardId; if (wasMoved) { // Pin was moved OUT of current board - remove it from the list console.log('🔄 [PinBoardViewPage] Pin moved to different board, removing from current view'); setOrderedPins((prevPins) => prevPins.filter((pin) => pin.id !== updatedPin.id)); // Close the modal since pin is no longer in this board setSelectedPin(null); showToast( pinsTexts('board.pinMovedOut', { defaultValue: 'Pin moved to another board' }), 'info' ); return; } // Pin was updated but stayed in same board - update it setOrderedPins((prevPins) => prevPins.map((pin) => (pin.id === updatedPin.id ? updatedPin : pin)) ); // Update selectedPin if it's the same, preserving boardId if (selectedPin && selectedPin.id === updatedPin.id) { const pinWithBoardContext = { ...updatedPin, // IMPORTANT: "All Pins" is a virtual board id (not UUID). Preserve the real pin.boardId. boardId: isAllPinsBoard ? (selectedPin as any).boardId || (updatedPin as any).boardId : boardId || selectedPin.boardId || (updatedPin as any).boardId, }; setSelectedPin(pinWithBoardContext); } }; const handleEditPinSave = async (pinData: Partial) => { if (!editingPin) return; try { // Import pinsService to update the pin const { pinsService } = await import('@/services/pinsService'); // Update pin in database await pinsService.update(editingPin.id, pinData as any); // Update local state const updatedPin = { ...editingPin, ...pinData }; setOrderedPins((prevPins) => prevPins.map((pin) => (pin.id === editingPin.id ? updatedPin : pin)) ); // Close modal setEditingPin(null); // CRÍTICO: Invalidate ALL relevant queries to refresh board lastUpdated timestamp if (user?.id && boardId) { await Promise.all([ // Invalidate board pins queryClient.invalidateQueries({ queryKey: ['boardPins', boardId] }), // Prefix match to cover ensurePlaceholders variants queryClient.invalidateQueries({ queryKey: ['pins', 'board', boardId] }), // Invalidate board details (contains lastUpdated) queryClient.invalidateQueries({ queryKey: ['boards', 'detail', boardId] }), queryClient.invalidateQueries({ queryKey: queryKeys.boards.detail(boardId) }), // MOST IMPORTANT: Invalidate user boards list (contains lastUpdated for board cards) queryClient.invalidateQueries({ queryKey: ['boards', 'user', user.id] }), queryClient.invalidateQueries({ queryKey: queryKeys.boards.byUser(user.id) }), ]); // Force immediate refetch of critical queries await Promise.all([ // Use exact+active to avoid refetching hydrated/inactive queries that have no queryFn yet queryClient.refetchQueries({ queryKey: queryKeys.boards.byUser(user.id), exact: true, type: 'active', }), queryClient.refetchQueries({ queryKey: queryKeys.boards.detail(boardId), exact: true, type: 'active', }), ]); } console.log(' Pin updated and all board queries invalidated/refetched'); showToast( pinsTexts('board.toasts.updateSuccess', { defaultValue: 'Pin updated successfully!' }), 'success' ); } catch (error) { console.error('Error updating pin:', error); showToast( pinsTexts('board.toasts.updateFailed', { defaultValue: 'Failed to update pin' }), 'error' ); } }; // Pin sort options const pinSortOptions = [ { value: 'name-asc', label: 'Name (A-Z)' }, { value: 'name-desc', label: 'Name (Z-A)' }, { value: 'collection', label: 'Collection' }, { value: 'set', label: 'Set' }, ]; // Handle reset filters const handleResetFilters = () => { setSearchQuery(''); setTradableFilter('all'); setShowFavoritesOnly(false); }; // Drag-to-trash removed // Mouse position tracking removed to fix Rules of Hooks error return ( <> {/* Pull to refresh indicator - outside container to stay on top */}
{/* Profile Header - shared component */} {boardOwnerData && (
navigate('/settings?section=account&direct=true')} pinsCount={boardOwnerPinsInBoards.length} rightActions={ setShowProfileMenu(!showProfileMenu)} onClose={() => setShowProfileMenu(false)} isOwnProfile={isOwnProfile || false} user={boardOwnerData} onViewAsPublic={() => { if (boardOwnerData?.username) { navigate(`/u/${boardOwnerData.username}`); } else { navigate(`/public/${boardOwnerData?.id}`); } }} /> } />
)} {/* Navigation Tabs */} {boardOwnerData && (

{currentBoard?.name || pinsTexts('board.title', { defaultValue: 'Board' })}

)} {/* Content with DndContext */}
{ if (canReorderPins && pinSortBy === 'custom') e.preventDefault(); }} > {/* Board Description - Removed as per user request */} {/* Board Toolbar */}
setShowShareModal(true)} onAddPin={() => setShowAddPinModal(true)} // Dropdowns sortOptions={sortOptionsForToolbar} sortValue={pinSortBy} onSortChange={(value) => setPinSortBy(value as PinSortOption)} viewOptions={isMobile ? [] : viewModeOptions} viewValue={viewMode} onViewChange={ isMobile ? () => {} : (value) => { const newMode = value as 'compact' | 'grid'; setViewMode(newMode); localStorage.setItem(`board-${boardId}-view-mode`, newMode); } } statusOptions={shouldShowTradableFilter(currentBoard?.name) ? statusOptions : []} statusValue={tradableFilter} onStatusChange={(value) => setTradableFilter( value as | 'all' | 'display' | 'trade' | 'sale' | 'sale_trade' | 'iso' | 'placeholder' ) } className="pb-4" customFilterElement={
{/* ISO toggle hidden on All Pins; visible elsewhere */} {!isAllPinsBoard && ( )} {/* Placeholders toggle - visible on all boards */} {/* Favorites toggle - only for All Pins with status All */} {isAllPinsBoard && tradableFilter === 'all' && ( setShowFavoritesOnly(value)} /> )} {/* Grouping control */} setGroupBy(value as 'none' | 'set' | 'collection' | 'status') } placeholder={pinsTexts('board.group.placeholder', { defaultValue: 'Group', })} size="sm" sheetTitle={pinsTexts('board.group.title', { defaultValue: 'Grouping' })} isActive={groupBy !== 'none'} /> {/* Bulk Select (Instagram-like) */} {canDeletePins && ( )}
} onResetFilters={handleResetFilters} />
{/* Board Info Row - Desktop (verbose) */}
{/* Display pin count – fallback to runtime count if backend value is outdated */} {(() => { const total = currentBoard.pinCount && currentBoard.pinCount > 0 ? currentBoard.pinCount : orderedPins.length; if (tradableFilter === 'all' && !(isAllPinsBoard && showFavoritesOnly)) { return pinsTexts('board.count.totalPins', { count: total, defaultValue: '{{count}} pins', }); } return pinsTexts('board.count.filteredOfTotal', { count: filteredPins.length, total, filter: isAllPinsBoard && showFavoritesOnly ? commonTexts('favorites', { defaultValue: 'favorites' }) : tradableFilter.replace('-', ' '), defaultValue: '{{count}} of {{total}} pins ({{filter}})', }); })()} {pinsTexts('board.privacyLabel', { privacy: currentBoard.isPrivate ? pinsTexts('board.privacy.private', { defaultValue: 'Private' }) : pinsTexts('board.privacy.public', { defaultValue: 'Public' }), defaultValue: '{{privacy}} board', })} {pinSortBy === 'custom' && ( <> {pinsTexts('board.customOrderHint', { defaultValue: 'Custom order - drag to reorder', })} )} {pinSortBy !== 'custom' && ( <> {pinsTexts('board.sortedBy', { label: pinSortOptions.find((opt) => opt.value === pinSortBy)?.label, defaultValue: 'Sorted by {{label}}', })} )} {currentBoard.lastUpdated && ( <> )}
{/* Board Info Row - Mobile (compact with icons) */}
{(() => { const total = currentBoard.pinCount && currentBoard.pinCount > 0 ? currentBoard.pinCount : orderedPins.length; return ( {pinsTexts('board.count.totalPins', { count: total, defaultValue: '{{count}} pins', })} ); })()} {currentBoard.isPrivate ? ( ) : ( )} {currentBoard.isPrivate ? pinsTexts('board.privacy.private', { defaultValue: 'Private' }) : pinsTexts('board.privacy.public', { defaultValue: 'Public' })} {pinSortBy === 'custom' ? ( ) : ( currentBoard.lastUpdated && ( {(() => { const d = new Date(currentBoard.lastUpdated as any); const diffMs = Date.now() - d.getTime(); const minutes = Math.floor(diffMs / 60000); if (minutes < 60) return `${Math.max(0, minutes)}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h`; const days = Math.floor(hours / 24); return `${days}d`; })()} ) )}
{/* Pins Display */} {orderedPins.length === 0 ? (

{pinsTexts('board.empty.noPinsYet', { defaultValue: 'No pins yet' })}

{!isOwner ? pinsTexts('board.empty.description.default', { defaultValue: "This board doesn't have any pins yet.", }) : currentBoard?.name === 'Tradable Pins' ? pinsTexts('board.empty.description.tradable', { defaultValue: 'Mark pins as tradable to see them appear here automatically.', }) : pinsTexts('board.empty.description.favorites', { defaultValue: "Pins you add to favorites will show up here. You can't add pins to this board directly.", })}

) : showGroupedView ? (
{statusOrder.map((statusKey) => { const pinsInGroup = groupedPinsByStatus[statusKey]; if (!pinsInGroup || pinsInGroup.length === 0) { return null; } return (
setCollapsedStatusGroups((prev) => ({ ...prev, [statusKey]: !prev[statusKey], })) } onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setCollapsedStatusGroups((prev) => ({ ...prev, [statusKey]: !prev[statusKey], })); } }} >

{groupLabels[statusKey]}

{(() => { // 1) Agrupar por Coleção const collectionGroups: { [key: string]: UIPin[] } = {}; const collectionLabels: Record = {}; const getCollectionData = (p: UIPin) => { const anyP = p as unknown as Record; const tags = Array.isArray(anyP?.tags) ? (anyP.tags as string[]) : []; const tagCol = tags.find((t) => /collection/i.test(t)) || ''; const origin = (p.origin || '').toString(); const label = (anyP?.collection_name as string) || tagCol || origin || pinsTexts('board.group.noCollection', { defaultValue: 'No Collection', }); const key = (anyP?.collection_id as string) || (anyP?.collection_name as string) || (origin ? `origin:${origin}` : 'no-collection'); return { key, label }; }; pinsInGroup.forEach((pin) => { const { key, label } = getCollectionData(pin); if (!collectionGroups[key]) collectionGroups[key] = []; collectionGroups[key].push(pin); if (!collectionLabels[key]) collectionLabels[key] = label; }); const collectionKeys = Object.keys(collectionGroups); collectionKeys.sort((a, b) => { if (a === 'no-collection') return 1; if (b === 'no-collection') return -1; return (collectionLabels[a] || a).localeCompare( collectionLabels[b] || b, undefined, { sensitivity: 'base', } ); }); // 2) Para cada coleção, agrupar por Set e renderizar separadores return collectionKeys.map((collectionKey) => { const pinsOfCollection = collectionGroups[collectionKey]; if (!pinsOfCollection || pinsOfCollection.length === 0) return null; // Separador da Coleção (não-sticky) const collectionLabel = collectionLabels[collectionKey]; // Agrupar por Set dentro da coleção const setGroups: { [key: string]: UIPin[] } = {}; const setLabels: Record = {}; pinsOfCollection.forEach((pin) => { const pinAny = pin as unknown as Record; const sk = (pinAny?.set_id as string | undefined) || 'no-set'; if (!setGroups[sk]) setGroups[sk] = []; setGroups[sk].push(pin); if (!setLabels[sk]) { setLabels[sk] = sk === 'no-set' ? pinsTexts('board.group.noSet', { defaultValue: 'No Set' }) : (pinAny?.set_name as string) || pinsTexts('board.group.unknownSet', { defaultValue: 'Unknown Set', }); } }); const setKeys = Object.keys(setGroups); setKeys.sort((a, b) => { if (a === 'no-set') return 1; if (b === 'no-set') return -1; return (setLabels[a] || a).localeCompare( setLabels[b] || b, undefined, { sensitivity: 'base', } ); }); return (
{/* Linha separadora da Coleção */}
{collectionLabel}
{/* Para cada Set dentro da coleção, exibir linha separadora e grid */} {setKeys.map((setKey) => { const setPins = setGroups[setKey]; if (!setPins || setPins.length === 0) return null; return (
{setLabels[setKey]}
{}} onPinClick={handlePinClick} onPinClickWithObject={handlePinClickWithObject} onPinCommentWithObject={handlePinCommentWithObject} onPinLike={hasFullAccess ? handlePinLike : async () => {}} onPinSave={hasFullAccess ? handlePinSave : async () => {}} onPinComment={hasFullAccess ? handlePinComment : () => {}} onPinDelete={ canDeletePins ? handlePinDelete : async () => {} } onPinEdit={canEditPins ? handlePinEdit : () => {}} onPinDropToTrash={undefined} onDragStateChange={() => {}} viewMode={viewMode} isEditMode={canEditPins} isDragDisabled={true} activeId={activeId} isOverTrash={false} searchQuery={searchQuery} enablePinchZoom={true} enableKeyboardShortcuts={true} zoomStorageId={boardId ? `board-${boardId}` : undefined} dimIsoPins={(() => { const currentName = String( currentBoard?.name || '' ).toLowerCase(); const isWishlist = currentName.includes('wishlist') || currentName.includes('wish list'); return !isWishlist && !showIsoPins; })()} isoOpacity={1} dimPlaceholderPins={showPlaceholderPins} placeholderOpacity={0.35} showAvailabilityChipOnlyForSale={Boolean(isAllPinsBoard)} ownedPinDatabaseIds={ !isOwnProfile ? ownedPinDatabaseIds : undefined } onRefreshOwnership={ !isOwnProfile ? refreshOwnership : undefined } showOwnedIndicator={!isViewingOwnBoard} selectionMode={isSelectMode} selectedPinIds={selectedPinIds} onToggleSelected={togglePinSelected} />
); })}
); }); })()}
); })}
) : showGroupedByCollectionView ? (
{(() => { const keys = Object.keys(groupedPinsByCollection); keys.sort((a, b) => { if (a === 'no-collection') return 1; if (b === 'no-collection') return -1; return (collectionGroupLabels[a] || a).localeCompare( collectionGroupLabels[b] || b, undefined, { sensitivity: 'base' } ); }); return keys.map((collectionKey) => { const pinsInCollection = groupedPinsByCollection[collectionKey]; if (!pinsInCollection || pinsInCollection.length === 0) return null; return (

{collectionGroupLabels[collectionKey]}

{(() => { // Subdivisão por Set dentro de cada coleção const setGroups: { [key: string]: UIPin[] } = {}; const setLabels: Record = {}; pinsInCollection.forEach((pin) => { const pinAny = pin as unknown as Record; const sk = (pinAny?.set_id as string | undefined) || 'no-set'; if (!setGroups[sk]) setGroups[sk] = []; setGroups[sk].push(pin); if (!setLabels[sk]) { setLabels[sk] = sk === 'no-set' ? pinsTexts('board.group.noSet', { defaultValue: 'No Set' }) : (pinAny?.set_name as string) || pinsTexts('board.group.unknownSet', { defaultValue: 'Unknown Set', }); } }); const setKeys = Object.keys(setGroups); setKeys.sort((a, b) => { if (a === 'no-set') return 1; if (b === 'no-set') return -1; return (setLabels[a] || a).localeCompare( setLabels[b] || b, undefined, { sensitivity: 'base', } ); }); return setKeys.map((setKey) => { const setPins = setGroups[setKey]; if (!setPins || setPins.length === 0) return null; return (
{setLabels[setKey]}
{}} onPinClick={handlePinClick} onPinClickWithObject={handlePinClickWithObject} onPinCommentWithObject={handlePinCommentWithObject} onPinLike={hasFullAccess ? handlePinLike : async () => {}} onPinSave={hasFullAccess ? handlePinSave : async () => {}} onPinComment={hasFullAccess ? handlePinComment : () => {}} onPinDelete={canDeletePins ? handlePinDelete : async () => {}} onPinEdit={canEditPins ? handlePinEdit : () => {}} onPinDropToTrash={undefined} onDragStateChange={() => {}} viewMode={viewMode} isEditMode={canEditPins} isDragDisabled={true} activeId={activeId} isOverTrash={false} searchQuery={searchQuery} enablePinchZoom={true} enableKeyboardShortcuts={true} zoomStorageId={boardId ? `board-${boardId}` : undefined} dimIsoPins={(() => { const currentName = String( currentBoard?.name || '' ).toLowerCase(); const isWishlist = currentName.includes('wishlist') || currentName.includes('wish list'); return !isWishlist && !showIsoPins; })()} isoOpacity={1} dimPlaceholderPins={showPlaceholderPins} placeholderOpacity={0.35} showAvailabilityChipOnlyForSale={Boolean(isAllPinsBoard)} ownedPinDatabaseIds={ !isOwnProfile ? ownedPinDatabaseIds : undefined } onRefreshOwnership={ !isOwnProfile ? refreshOwnership : undefined } showOwnedIndicator={!isViewingOwnBoard} selectionMode={isSelectMode} selectedPinIds={selectedPinIds} onToggleSelected={togglePinSelected} />
); }); })()}
); }); })()}
) : showGroupedBySetView ? (
{(() => { const keys = Object.keys(groupedPinsBySet); // Sort groups by label; place "no-set" at the end keys.sort((a, b) => { if (a === 'no-set') return 1; if (b === 'no-set') return -1; return (setGroupLabels[a] || a).localeCompare( setGroupLabels[b] || b, undefined, { sensitivity: 'base', } ); }); return keys.map((setKey) => { const pinsInGroup = groupedPinsBySet[setKey]; if (!pinsInGroup || pinsInGroup.length === 0) return null; return (
setCollapsedSetGroups((prev) => ({ ...prev, [setKey]: !prev[setKey], })) } onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setCollapsedSetGroups((prev) => ({ ...prev, [setKey]: !prev[setKey], })); } }} >

{setGroupLabels[setKey]}

{}} onPinClick={handlePinClick} onPinClickWithObject={handlePinClickWithObject} onPinCommentWithObject={handlePinCommentWithObject} onPinLike={hasFullAccess ? handlePinLike : async () => {}} onPinSave={hasFullAccess ? handlePinSave : async () => {}} onPinComment={hasFullAccess ? handlePinComment : () => {}} onPinDelete={canDeletePins ? handlePinDelete : async () => {}} onPinEdit={canEditPins ? handlePinEdit : () => {}} onPinDropToTrash={undefined} onDragStateChange={() => {}} viewMode={viewMode} isEditMode={canEditPins} isDragDisabled={true} activeId={activeId} isOverTrash={false} searchQuery={searchQuery} enablePinchZoom={true} enableKeyboardShortcuts={true} zoomStorageId={boardId ? `board-${boardId}` : undefined} dimIsoPins={(() => { const currentName = String(currentBoard?.name || '').toLowerCase(); const isWishlist = currentName.includes('wishlist') || currentName.includes('wish list'); // Dim ISO pins when showIsoPins is false (except in wishlist) return !isWishlist && !showIsoPins; })()} isoOpacity={0.5} dimPlaceholderPins={showPlaceholderPins} placeholderOpacity={0.35} showAvailabilityChipOnlyForSale={Boolean(isAllPinsBoard)} ownedPinDatabaseIds={!isOwnProfile ? ownedPinDatabaseIds : undefined} onRefreshOwnership={!isOwnProfile ? refreshOwnership : undefined} showOwnedIndicator={!isViewingOwnBoard} />
); }); })()}
) : filteredPins.length === 0 ? (

{pinsTexts('board.filtered.noneFoundTitle', { filter: tradableFilter.replace('-', ' '), defaultValue: 'No {{filter}} pins found', })}

{pinsTexts('board.filtered.changeFilter', { defaultValue: 'Try changing the filter to see more pins.', })}

) : ( {}} onPinClick={handlePinClick} onPinClickWithObject={handlePinClickWithObject} onPinCommentWithObject={handlePinCommentWithObject} onPinLike={hasFullAccess ? handlePinLike : async () => {}} onPinSave={hasFullAccess ? handlePinSave : async () => {}} onPinComment={hasFullAccess ? handlePinComment : () => {}} onPinDelete={canDeletePins ? handlePinDelete : async () => {}} onPinEdit={canEditPins ? handlePinEdit : () => {}} onPinDropToTrash={undefined} onDragStateChange={() => {}} viewMode={viewMode} isEditMode={canEditPins} isDragDisabled={ !canReorderPins || isAnyModalOpen || isSelectMode || (viewMode !== 'compact' && pinSortBy !== 'custom') } activeId={activeId} isOverTrash={false} searchQuery={searchQuery} enablePinchZoom={true} enableKeyboardShortcuts={true} zoomStorageId={boardId ? `board-${boardId}` : undefined} onZoomChange={(level) => { // Optionally show toast or update UI based on zoom level if (process.env.NODE_ENV === 'development') { console.log(`📱 Zoom changed to ${level} columns`); } }} dimIsoPins={(() => { const currentName = String(currentBoard?.name || '').toLowerCase(); const isWishlist = currentName.includes('wishlist') || currentName.includes('wish list'); // Dim ISO pins when showIsoPins is false (except in wishlist) return !isWishlist && !showIsoPins; })()} isoOpacity={0.5} dimPlaceholderPins={showPlaceholderPins} placeholderOpacity={0.35} showAvailabilityChipOnlyForSale={Boolean(isAllPinsBoard)} ownedPinDatabaseIds={!isOwnProfile ? ownedPinDatabaseIds : undefined} onRefreshOwnership={!isOwnProfile ? refreshOwnership : undefined} showOwnedIndicator={!isViewingOwnBoard} selectionMode={isSelectMode} selectedPinIds={selectedPinIds} onToggleSelected={togglePinSelected} /> )} {/* Trash Bin removed */}
{/* Bulk selection actions bar (mobile-first, Instagram-like) */} {isSelectMode && canDeletePins && ( b?.id && b?.name) .filter((b) => shouldAppearInBoardSelection(b.name)) .filter((b) => (isAllPinsBoard ? true : b.id !== boardId)) .map((b) => ({ id: b.id, name: b.name, pinCount: b.pinCount }))} onMoveToBoard={handleBulkMoveToBoard} statusOptions={bulkStatusOptions} onChangeStatus={handleBulkChangeStatus} /> )} {/* Modals - outside DndContext */} {/* Add Pin Flow Modal - Upload → Search → Confirm/Create */} {canAddPins && showAddPinModal && ( setShowAddPinModal(false)} onPinAdded={async (pinId) => { console.log('Pin adicionado:', pinId); analyticsService.trackEvent({ event: 'pin_add', category: 'pin', action: 'add', metadata: { pinId, boardId, source: 'board_view', }, }); // Call the pin added handler to refresh the view await handlePinAdded(); // Toast is already shown by AddPinFlowModal, no need to duplicate setShowAddPinModal(false); }} /> )} {/* Share Board Modal - always available */} {showShareModal && ( setShowShareModal(false)} /> )} {/* Pin Detail Modal - always available */} {selectedPin && ( { setSelectedPin(null); setFocusOnComment(false); // Reset focus state when modal closes setIsModalOpening(false); // Reset opening flag when modal closes // Ensure board list behind the modal reflects latest server state (no manual refresh) if (boardId) { queryClient.invalidateQueries({ predicate: (q) => Array.isArray(q.queryKey) && q.queryKey[0] === 'pins' && q.queryKey[1] === 'board' && q.queryKey[2] === boardId, }); } }} context="board" onLike={hasFullAccess ? handlePinLike : undefined} onSave={hasFullAccess ? handlePinSave : undefined} onShare={handlePinShare} onUserClick={handleUserClick} onUpdate={canEditPins ? handlePinUpdate : undefined} focusOnComment={focusOnComment} // Navigation props onNavigateNext={() => { const currentIndex = filteredPins.findIndex((p) => p.id === selectedPin.id); if (currentIndex < filteredPins.length - 1) { const nextPin = filteredPins[currentIndex + 1]; const pinWithBoardContext = { ...nextPin, // IMPORTANT: "All Pins" is a virtual board id (not UUID). Preserve the real pin.boardId. boardId: isAllPinsBoard ? (nextPin as any).boardId : boardId || (nextPin as any).boardId, }; setSelectedPin(pinWithBoardContext); setFocusOnComment(false); // Reset focus when navigating } }} onNavigatePrevious={() => { const currentIndex = filteredPins.findIndex((p) => p.id === selectedPin.id); if (currentIndex > 0) { const prevPin = filteredPins[currentIndex - 1]; const pinWithBoardContext = { ...prevPin, // IMPORTANT: "All Pins" is a virtual board id (not UUID). Preserve the real pin.boardId. boardId: isAllPinsBoard ? (prevPin as any).boardId : boardId || (prevPin as any).boardId, }; setSelectedPin(pinWithBoardContext); setFocusOnComment(false); // Reset focus when navigating } }} hasNext={(() => { const currentIndex = filteredPins.findIndex((p) => p.id === selectedPin.id); return currentIndex < filteredPins.length - 1; })()} hasPrevious={(() => { const currentIndex = filteredPins.findIndex((p) => p.id === selectedPin.id); return currentIndex > 0; })()} currentIndex={filteredPins.findIndex((p) => p.id === selectedPin.id)} totalCount={filteredPins.length} /> )} {/* Edit Pin Modal - show for users who can edit pins (including All Pins board) */} {canEditPins && editingPin && ( setEditingPin(null)} onSave={handleEditPinSave} /> )} {/* Bulk Upload Modal - only for users who can edit regular boards */} {canEdit && showBulkUploadModal && ( setShowBulkUploadModal(false)} boardId={boardId!} onSuccess={handlePinAdded} /> )} {/* Drag and Drop Overlay */} {/* Flying animation removed - drag-to-trash now uses simple approach */} {/* Change Profile Photo Modal */} setShowPhotoEditor(false)} onUploadPhoto={handleUploadPhoto} onRemovePhoto={handleRemovePhoto} hasCurrentPhoto={ !!(boardOwnerData?.avatarUrl && boardOwnerData.avatarUrl.trim() !== '') } />
{/* Tour My Pins */} {isOwnProfile && ( )}
); };