diff --git a/src/api/file.api.ts b/src/api/file.api.ts index 125e2df..2b8f13d 100644 --- a/src/api/file.api.ts +++ b/src/api/file.api.ts @@ -41,7 +41,13 @@ const getAllSharedFilesWithMe = async () => { }; const getFileSystemTree = async () => { - const response = await apiClient.get("/file-flow/file/all"); + const response = await apiClient.get("/file-flow/file/all?accessLevel=protected"); + return response.data; +}; + + +const getPrivateFiles = async () => { + const response = await apiClient.get("/file-flow/file/all?accessLevel=private"); return response.data; }; @@ -77,6 +83,7 @@ export default { renameFolder, moveFileOrFolder, createFile, + getPrivateFiles, shareFileOrFolder, getAllSharedFiles, getAllSharedFilesByMe, diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index f3efc75..8c6992d 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -44,6 +44,51 @@ const customStyles = ` background-color: #ef4444; } + /* Ensure timer is always visible */ + .video-js .vjs-current-time, + .video-js .vjs-duration { + display: inline-block !important; + padding: 0 0.5em; + } + + .video-js .vjs-time-divider { + display: inline-block !important; + padding: 0 0.2em; + } + + /* Skip indicator styles */ + .video-skip-indicator { + position: absolute; + top: 50%; + transform: translateY(-50%); + font-size: 2em; + font-weight: bold; + color: white; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); + pointer-events: none; + z-index: 1000; + animation: fadeOut 0.5s ease-out forwards; + } + + .video-skip-indicator.left { + left: 20%; + } + + .video-skip-indicator.right { + right: 20%; + } + + @keyframes fadeOut { + 0% { + opacity: 1; + transform: translateY(-50%) scale(1.2); + } + 100% { + opacity: 0; + transform: translateY(-50%) scale(1); + } + } + /* Mobile optimizations */ @media (max-width: 640px) { .video-js .vjs-control-bar { @@ -130,6 +175,7 @@ export function VideoPlayer({ const videoRef = useRef(null); const playerRef = useRef(null); const containerRef = useRef(null); + const doubleClickHandlerRef = useRef<((event: MouseEvent) => void) | null>(null); useEffect(() => { // Add custom styles to document @@ -208,6 +254,49 @@ export function VideoPlayer({ if (onError) onError(error); }); + // Double-click handler for skip forward/backward + const handleDoubleClick = (event: MouseEvent) => { + const playerEl = player.el() as HTMLElement | null; + if (!playerEl) return; + + const rect = playerEl.getBoundingClientRect(); + const clickX = event.clientX - rect.left; + const playerWidth = rect.width; + const isLeftSide = clickX < playerWidth / 2; + + const currentTime = player.currentTime(); + const duration = player.duration(); + + if (typeof currentTime === 'number' && typeof duration === 'number') { + let newTime: number; + let skipText: string; + + if (isLeftSide) { + // Skip backward 10 seconds + newTime = Math.max(0, currentTime - 10); + skipText = '-10s'; + } else { + // Skip forward 10 seconds + newTime = Math.min(duration, currentTime + 10); + skipText = '+10s'; + } + + player.currentTime(newTime); + + // Show skip indicator + showSkipIndicator(playerEl, skipText, isLeftSide); + } + }; + + // Store handler reference for cleanup + doubleClickHandlerRef.current = handleDoubleClick; + + // Listen for double-click events on the player element + const playerEl = player.el() as HTMLElement | null; + if (playerEl) { + playerEl.addEventListener('dblclick', handleDoubleClick); + } + // Keyboard shortcuts player.on('keydown', (e: any) => { const event = e as KeyboardEvent; @@ -268,6 +357,14 @@ export function VideoPlayer({ } return () => { + // Clean up double-click listener + if (playerRef.current && doubleClickHandlerRef.current) { + const playerEl = playerRef.current.el() as HTMLElement | null; + if (playerEl) { + playerEl.removeEventListener('dblclick', doubleClickHandlerRef.current); + } + doubleClickHandlerRef.current = null; + } if (playerRef.current) { playerRef.current.dispose(); playerRef.current = null; @@ -275,6 +372,45 @@ export function VideoPlayer({ }; }, [url, autoplay, muted, controls, loop, preload, poster]); + // Helper function to show skip indicator + function showSkipIndicator(playerEl: HTMLElement, text: string, isLeft: boolean) { + // Remove existing indicator if any + const existingIndicator = playerEl.querySelector('.video-skip-indicator') as HTMLElement | null; + if (existingIndicator) { + existingIndicator.remove(); + } + + // Create new indicator + const indicator = document.createElement('div'); + indicator.className = `video-skip-indicator ${isLeft ? 'left' : 'right'}`; + indicator.textContent = text; + indicator.style.position = 'absolute'; + indicator.style.top = '50%'; + indicator.style.transform = 'translateY(-50%)'; + indicator.style.fontSize = '2em'; + indicator.style.fontWeight = 'bold'; + indicator.style.color = 'white'; + indicator.style.textShadow = '2px 2px 4px rgba(0, 0, 0, 0.8)'; + indicator.style.pointerEvents = 'none'; + indicator.style.zIndex = '1000'; + indicator.style.animation = 'fadeOut 0.5s ease-out forwards'; + + if (isLeft) { + indicator.style.left = '20%'; + } else { + indicator.style.right = '20%'; + } + + playerEl.appendChild(indicator); + + // Remove after animation + setTimeout(() => { + if (indicator.parentNode) { + indicator.remove(); + } + }, 500); + } + // Helper function to determine video type function getVideoType(url: string): string { const extension = url.split('.').pop()?.toLowerCase(); diff --git a/src/components/upload/FIleUploader.tsx b/src/components/upload/FIleUploader.tsx index 5cb2983..cd83197 100644 --- a/src/components/upload/FIleUploader.tsx +++ b/src/components/upload/FIleUploader.tsx @@ -19,6 +19,7 @@ import { import { useNavigate } from 'react-router-dom'; import { useUpload } from '@/contexts/UploadContext'; import { useFile } from '@/contexts/fileContext'; +import type { AccessLevel } from '@/types/file.types'; export type FileType = 'excel' | 'pdf' | 'image' | 'video' | 'audio' | 'archive' | 'text' | 'any'; @@ -32,6 +33,7 @@ interface FileUploaderProps { allowedTypes?: FileConfig[]; maxFiles?: number; folderId?: string; + accessLevel?: AccessLevel; // 'public' | 'private' | 'protected' } const DEFAULT_FILE_CONFIGS: FileConfig[] = [ @@ -46,6 +48,7 @@ const FileUploader: React.FC = ({ allowedTypes = DEFAULT_FILE_CONFIGS, maxFiles = 10, folderId, + accessLevel, }) => { const { createFile } = useFile(); const navigate = useNavigate(); @@ -64,7 +67,7 @@ const FileUploader: React.FC = ({ autoClearCompleted } = useUpload(); - const { fileStates } = state; + const { fileStates, error: uploadError } = state; // Check if any uploads are in progress useEffect(() => { @@ -173,81 +176,95 @@ const FileUploader: React.FC = ({ const fileType = getFileType(file); try { - if (fileType === 'video') { - // Use chunked upload for video files - const result = await handleUpload(file, folderId); - if (result) { - await createFile({ - name: file.name, - parent_id: folderId === "root" ? null : folderId, - file_info: result - }); - updateFileState(file.name, { - url: result.storage_path, - status: 'completed', - progress: 100, - lastUploadedChunk: Math.ceil(file.size / (5 * 1024 * 1024)) - }); - setCompletedFiles(prev => new Set([...prev, file.name])); + switch (fileType) { + case 'video': { + // Use chunked upload for video files + const result = await handleUpload(file, folderId); + if (result) { + await createFile({ + name: file.name, + parent_id: folderId === "root" ? null : folderId, + access_level: accessLevel, + file_info: result + }); + updateFileState(file.name, { + url: result.storage_path, + status: 'completed', + progress: 100, + lastUploadedChunk: Math.ceil(file.size / (5 * 1024 * 1024)) + }); + setCompletedFiles(prev => new Set([...prev, file.name])); + } + break; } - } else { - // Use direct upload for non-video files (images, excel, pdf, text, etc.) - // Initialize file state if not exists - if (!fileStates[file.name]) { - updateFileState(file.name, { - uploadId: null, - url: null, - fileKey: null, - progress: 0, - status: 'uploading', - error: null, - lastUploadedChunk: 0, - totalChunks: 1, - fileName: file.name, - fileSize: file.size, - fileType: file.type - }); - } else { + case 'excel': + case 'pdf': + case 'image': + case 'audio': + case 'archive': + case 'text': + case 'any': + default: { + // Use direct upload for non-video files (images, excel, pdf, text, etc.) + // Initialize file state if not exists + if (!fileStates[file.name]) { + updateFileState(file.name, { + uploadId: null, + url: null, + fileKey: null, + progress: 0, + status: 'uploading', + error: null, + lastUploadedChunk: 0, + totalChunks: 1, + fileName: file.name, + fileSize: file.size, + fileType: file.type + }); + } else { + updateFileState(file.name, { + status: 'uploading', + progress: 0, + error: null + }); + } + + // Update progress to show upload started updateFileState(file.name, { - status: 'uploading', - progress: 0, - error: null - }); - } - - // Update progress to show upload started - updateFileState(file.name, { - progress: 50 - }); - - const uploadedFiles = await uploadFiles([file]); - - if (uploadedFiles && uploadedFiles.length > 0) { - const uploadedFile = uploadedFiles[0]; - - // Create file entry in database - await createFile({ - name: file.name, - parent_id: folderId === "root" ? null : folderId, - file_info: { - file_type: file.type, - file_size: file.size, - storage_path: uploadedFile.url, - thumbnail_path: uploadedFile.url, - duration: undefined - } + progress: 50 }); - updateFileState(file.name, { - url: uploadedFile.url, - status: 'completed', - progress: 100, - lastUploadedChunk: 1, - totalChunks: 1 // Set to 1 for direct uploads - }); - setCompletedFiles(prev => new Set([...prev, file.name])); - } else { - throw new Error('Upload failed - no file returned'); + const uploadedFiles = await uploadFiles([file]); + + if (uploadedFiles && uploadedFiles.length > 0) { + const uploadedFile = uploadedFiles[0]; + + // Create file entry in database + await createFile({ + name: file.name, + parent_id: folderId === "root" ? null : folderId, + access_level: accessLevel, + file_info: { + file_type: file.type, + file_size: file.size, + storage_path: uploadedFile.url, + thumbnail_path: uploadedFile.url, + duration: undefined + } + }); + + updateFileState(file.name, { + url: uploadedFile.url, + status: 'completed', + progress: 100, + lastUploadedChunk: 1, + totalChunks: 1 // Set to 1 for direct uploads + }); + setCompletedFiles(prev => new Set([...prev, file.name])); + } else { + throw new Error(uploadError || 'Upload failed - no file returned'); + } + break; } } } catch (error: any) { diff --git a/src/components/user/user-dropdown.tsx b/src/components/user/user-dropdown.tsx index 0379dec..c0aa9ad 100644 --- a/src/components/user/user-dropdown.tsx +++ b/src/components/user/user-dropdown.tsx @@ -36,6 +36,7 @@ export function UserDropdown({ name = "Sophie Chamberlain", email = "hi@sophie.c await handleLogout() } else { setIsOpen(false) + navigate(href) } } return ( diff --git a/src/contexts/UploadContext.tsx b/src/contexts/UploadContext.tsx index a370d99..bf3646e 100644 --- a/src/contexts/UploadContext.tsx +++ b/src/contexts/UploadContext.tsx @@ -306,6 +306,7 @@ export const UploadProvider: React.FC<{ children: ReactNode }> = ({ children }) parts.push({ PartNumber: chunkResponse.PartNumber, ETag: chunkResponse.ETag }); updateFileState(file.name, { lastUploadedChunk: chunkNumber + 1 }); } catch (error: any) { + console.error('Failed to upload chunk', error); updateFileState(file.name, { status: 'error', error: `Failed to upload chunk ${chunkNumber + 1}: ${error.message}` @@ -384,7 +385,7 @@ export const UploadProvider: React.FC<{ children: ReactNode }> = ({ children }) throw new Error(response.data?.message || 'Upload failed - invalid response'); } } catch (error: any) { - dispatch({ type: 'ERROR', payload: error?.message || 'Network error' }); + dispatch({ type: 'ERROR', payload: error?.response?.data?.message || 'Network error' }); return []; } }; diff --git a/src/contexts/fileContext.tsx b/src/contexts/fileContext.tsx index 6859635..7080e7d 100644 --- a/src/contexts/fileContext.tsx +++ b/src/contexts/fileContext.tsx @@ -14,6 +14,7 @@ import type { interface FileState { fileSystemTree: FileSystemNode[]; + privateFiles: FileSystemNode[]; trash: FileSystemNode[]; sharedFiles: SharedFileSystemNode[]; sharedFilesByMe: SharedFileSystemNode[]; @@ -24,6 +25,7 @@ interface FileState { type FileAction = | { type: 'SET_FILE_SYSTEM_TREE'; fileSystemTree: FileSystemNode[] } + | { type: 'SET_PRIVATE_FILES'; privateFiles: FileSystemNode[] } | { type: 'SET_TRASH'; trash: FileSystemNode[] } | { type: 'SET_SHARED_FILES'; sharedFiles: SharedFileSystemNode[] } | { type: 'SET_SHARED_FILES_BY_ME'; sharedFilesByMe: SharedFileSystemNode[] } @@ -33,6 +35,7 @@ type FileAction = const initialState: FileState = { fileSystemTree: [], + privateFiles: [], trash: [], sharedFiles: [], sharedFilesByMe: [], @@ -45,6 +48,8 @@ function fileReducer(state: FileState, action: FileAction): FileState { switch (action.type) { case 'SET_FILE_SYSTEM_TREE': return { ...state, fileSystemTree: action.fileSystemTree }; + case 'SET_PRIVATE_FILES': + return { ...state, privateFiles: action.privateFiles }; case 'SET_TRASH': return { ...state, trash: action.trash }; case 'SET_SHARED_FILES': @@ -74,6 +79,7 @@ interface FileContextType extends FileState { getAllSharedFilesByMe: () => Promise; getAllSharedFilesWithMe: () => Promise; getFileSystemTree: () => Promise; + getPrivateFiles: () => Promise; getTrash: () => Promise; getRecents: (page?: number, limit?: number) => Promise; deleteFileOrFolder: (id: string) => MutationResult; @@ -101,6 +107,23 @@ export const FileProvider: React.FC<{ children: ReactNode }> = ({ children }) => enabled: !!user, // Only enable the query when user is authenticated }); + const { data: privateFilesData, isLoading: privateFilesLoading } = useQuery({ + queryKey: ['privateFiles'], + queryFn: async () => { + const result = await fileApi.getPrivateFiles(); + return result.data; + }, + retry: 2, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + enabled: !!user, // Only enable the query when user is authenticated + }); + React.useEffect(() => { + if (privateFilesData) { + dispatch({ type: 'SET_PRIVATE_FILES', privateFiles: privateFilesData }); + } + }, [privateFilesData]); + const { data: trashData, isLoading: trashLoading } = useQuery({ queryKey: ['trash'], queryFn: async () => { @@ -484,9 +507,30 @@ export const FileProvider: React.FC<{ children: ReactNode }> = ({ children }) => } }; + const getPrivateFiles = async () => { + try { + dispatch({ type: 'SET_LOADING', loading: true }); + const data = await queryClient.fetchQuery({ + queryKey: ['privateFiles'], + queryFn: async () => { + const result = await fileApi.getPrivateFiles(); + return result.data; + }, + }); + dispatch({ type: 'SET_PRIVATE_FILES', privateFiles: data }); + dispatch({ type: 'SET_LOADING', loading: false }); + return data; + } + catch (error: any) { + dispatch({ type: 'SET_LOADING', loading: false }); + const errorMessage = error?.response?.data?.message || error?.message || 'Failed to get private files.'; + throw new Error(errorMessage); + } + }; + const value: FileContextType = { ...state, - loading: state.loading || fileSystemTreeLoading || trashLoading, + loading: state.loading || fileSystemTreeLoading || trashLoading || privateFilesLoading, createFolder, renameFolder, moveFileOrFolder, @@ -496,6 +540,7 @@ export const FileProvider: React.FC<{ children: ReactNode }> = ({ children }) => getAllSharedFilesByMe, getAllSharedFilesWithMe, getFileSystemTree, + getPrivateFiles, getTrash, getRecents, deleteFileOrFolder, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index cff9ffc..078f860 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,7 +1,7 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" import type { FileSystemNode } from "@/types/file.types" -import type { StandardFileItem, DeletedFileItem } from "@/types/file-manager" +import type { StandardFileItem, DeletedFileItem, PrivateFileItem } from "@/types/file-manager" import { FileText, FolderIcon, @@ -151,3 +151,44 @@ export function transformFileSystemNodeToDeletedFileItem(node: FileSystemNode, p export function transformFileSystemNodesToDeletedFileItems(nodes: FileSystemNode[]): DeletedFileItem[] { return nodes.map(node => transformFileSystemNodeToDeletedFileItem(node)) } + +// Transform FileSystemNode to PrivateFileItem +export function transformFileSystemNodeToPrivateFileItem(node: FileSystemNode, parentPath: string[] = []): PrivateFileItem { + const isFolder = node.is_folder + const fileType = node.file_info?.file_type || null + const size = node.file_info?.file_size || 0 + const thumbnail = node.file_info?.thumbnail_path || null + + // Handle potential null updated_at + const modifiedDate = node.updated_at || node.created_at + + // Determine encrypted and sensitive from metadata or defaults + // For private files, we can check metadata or use defaults + const metadata = node.metadata || {} + const encrypted = metadata.encrypted !== undefined ? metadata.encrypted : (node.access_level === "private") + const sensitive = metadata.sensitive !== undefined ? metadata.sensitive : (node.tags?.includes("sensitive") || false) + + return { + id: node.id, + name: node.name, + type: isFolder ? "folder" : "file", + fileType: isFolder ? "folder" : getFileTypeCategory(fileType), + size: formatFileSize(size), + modified: formatRelativeTime(modifiedDate), + icon: isFolder ? FolderIcon : getFileIcon(fileType), + thumbnail, + file_info: node.file_info || undefined, + starred: false, // This would need to be fetched from favorites API + shared: false, // Private files are not shared + parentPath, + variant: "private", + encrypted, + sensitive, + children: node.children ? node.children.map(child => transformFileSystemNodeToPrivateFileItem(child, [...parentPath, node.name])) : undefined, + } +} + +// Transform FileSystemNode array to PrivateFileItem array +export function transformFileSystemNodesToPrivateFileItems(nodes: FileSystemNode[]): PrivateFileItem[] { + return nodes.map(node => transformFileSystemNodeToPrivateFileItem(node)) +} \ No newline at end of file diff --git a/src/pages/private-files-page.tsx b/src/pages/private-files-page.tsx index 21f4ab4..876d556 100644 --- a/src/pages/private-files-page.tsx +++ b/src/pages/private-files-page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect, useRef } from "react" +import { useState, useEffect, useRef, useMemo } from "react" import { motion } from "framer-motion" import { Grid3X3, @@ -10,11 +10,6 @@ import { SortAsc, Download, Trash2, - FileText, - Music, - Archive, - FolderIcon, - Plus, Upload, Lock, Shield, @@ -26,124 +21,35 @@ import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" import { FileManager } from "@/components/file-manager/FileManager" +import { BreadcrumbNavigation } from "@/components/file-manager/BreadcrumbNavigation" import { privatePageConfig, defaultViewConfig } from "@/config/page-configs" import { useFile } from "@/contexts/fileContext" +import { transformFileSystemNodesToPrivateFileItems } from "@/lib/utils" import { toast } from "sonner" import type { PrivateFileItem, FileActionHandlers, FileItem } from "@/types/file-manager" import { VerifyPinModal } from "@/components/session/VerifyPin" import { useAuth } from "@/contexts/useAuth" - -const mockPrivateFiles: PrivateFileItem[] = [ - { - id: "1", - name: "Personal Journal.docx", - type: "file", - fileType: "document", - size: "1.2 MB", - modified: "1 hour ago", - icon: FileText, - thumbnail: null, - starred: true, - shared: false, - parentPath: [], - variant: "private", - encrypted: true, - sensitive: true, - }, - { - id: "2", - name: "Family Photos", - type: "folder", - fileType: "folder", - size: "234 MB", - modified: "3 hours ago", - icon: FolderIcon, - thumbnail: null, - starred: false, - shared: false, - parentPath: [], - variant: "private", - encrypted: true, - sensitive: false, - }, - { - id: "3", - name: "Tax Documents 2024.pdf", - type: "file", - fileType: "pdf", - size: "5.8 MB", - modified: "2 days ago", - icon: FileText, - thumbnail: null, - starred: true, - shared: false, - parentPath: [], - variant: "private", - encrypted: true, - sensitive: true, - }, - { - id: "4", - name: "Private Notes.txt", - type: "file", - fileType: "text", - size: "45 KB", - modified: "1 week ago", - icon: FileText, - thumbnail: null, - starred: false, - shared: false, - parentPath: [], - variant: "private", - encrypted: false, - sensitive: false, - }, - { - id: "5", - name: "Confidential Recording.mp3", - type: "file", - fileType: "audio", - size: "23.4 MB", - modified: "2 weeks ago", - icon: Music, - thumbnail: null, - starred: false, - shared: false, - parentPath: [], - variant: "private", - encrypted: true, - sensitive: true, - }, - { - id: "6", - name: "Personal Backup.zip", - type: "file", - fileType: "archive", - size: "156 MB", - modified: "1 month ago", - icon: Archive, - thumbnail: null, - starred: false, - shared: false, - parentPath: [], - variant: "private", - encrypted: true, - sensitive: false, - }, -] +import { useSocket } from "@/contexts/SocketContext" +import { ArrowLeft } from "lucide-react" +import { ACCESS_LEVEL } from "@/types/file.types" +import { AddNewFolder } from "@/components/file-manager/AddNewFolder" +import { useNavigate } from "react-router-dom" export function PrivateFilesPage() { + const { socket } = useSocket() const [viewMode, setViewMode] = useState<"grid" | "list">("grid") const [searchQuery, setSearchQuery] = useState("") const [selectedFiles, setSelectedFiles] = useState([]) + const [currentPath, setCurrentPath] = useState>([]) const [showSensitiveOnly, setShowSensitiveOnly] = useState(false) const [isPinVerified, setIsPinVerified] = useState(false) const [showPinModal, setShowPinModal] = useState(false) const [isCheckingSession, setIsCheckingSession] = useState(true) const hasCheckedSession = useRef(false) - const { deleteFileOrFolder } = useFile() + const { deleteFileOrFolder, privateFiles, createFolder } = useFile() const { user, getPinSession } = useAuth() + const navigate = useNavigate() // Check for active PIN session on mount (only once) useEffect(() => { @@ -205,11 +111,28 @@ export function PrivateFilesPage() { setShowPinModal(false) } - const filteredFiles = mockPrivateFiles.filter((file) => { - const matchesSearch = file.name.toLowerCase().includes(searchQuery.toLowerCase()) - const matchesSensitive = !showSensitiveOnly || file.sensitive - return matchesSearch && matchesSensitive - }) + // Transform dynamic data to PrivateFileItem format + const transformedPrivateFiles = useMemo(() => { + return transformFileSystemNodesToPrivateFileItems(privateFiles) + }, [privateFiles]) + + // Get current items based on currentPath (similar to all-files-page) + let currentItems = useMemo(() => { + let items: PrivateFileItem[] = transformedPrivateFiles + for (const folder of currentPath) { + const foundFolder = items.find((item) => item.id === folder.id && item.type === "folder") + if (foundFolder?.children) { + items = foundFolder.children as PrivateFileItem[] + } + } + return items + }, [currentPath, transformedPrivateFiles]) + + const filteredFiles = useMemo(() => { + const matchesSearch = (file: PrivateFileItem) => file.name.toLowerCase().includes(searchQuery.toLowerCase()) + const matchesSensitive = (file: PrivateFileItem) => !showSensitiveOnly || file.sensitive + return currentItems.filter((file) => matchesSearch(file) && matchesSensitive(file)) + }, [currentItems, searchQuery, showSensitiveOnly]) const toggleFileSelection = (fileId: string) => { setSelectedFiles((prev) => (prev.includes(fileId) ? prev.filter((id) => id !== fileId) : [...prev, fileId])) @@ -219,8 +142,66 @@ export function PrivateFilesPage() { setSelectedFiles(selectedFiles.length === filteredFiles.length ? [] : filteredFiles.map((f) => f.id)) } - const encryptedCount = mockPrivateFiles.filter((f) => f.encrypted).length - const sensitiveCount = mockPrivateFiles.filter((f) => f.sensitive).length + const handleItemClick = (item: FileItem) => { + if (item.type === "folder") { + setCurrentPath([...currentPath, { id: item.id, name: item.name }]) + setSelectedFiles([]) + setSearchQuery("") + } else { + if (!socket) return; + socket?.emit("last_accessed", { file_id: item.id }) + } + } + + const handleBreadcrumbNavigate = (index: number) => { + if (index === -1) { + setCurrentPath([]) + } else { + setCurrentPath(currentPath.slice(0, index + 1)) + } + setSelectedFiles([]) + setSearchQuery("") + } + + const handleBackClick = () => { + if (currentPath.length > 0) { + setCurrentPath(currentPath.slice(0, -1)) + setSelectedFiles([]) + } + } + + const encryptedCount = filteredFiles.filter((f) => f.encrypted).length + const sensitiveCount = filteredFiles.filter((f) => f.sensitive).length + + const handleCreateFolder = async (folderName: string): Promise<{ success: boolean; error?: string }> => { + try { + // Find the parent folder ID based on current path + let parentId: string | undefined = undefined + + // Get the last folder in the current path as the parent + if (currentPath.length > 0) { + const lastFolder = currentPath[currentPath.length - 1] + parentId = lastFolder.id + } + + const result = await createFolder({ + name: folderName.trim(), + parent_id: parentId, + access_level: ACCESS_LEVEL.PRIVATE // Set access level to private for private files page + }) + + if (result.success) { + toast.success("Private folder created successfully!") + } + + return result + } catch (error: any) { + return { + success: false, + error: error?.message || 'Failed to create folder' + } + } + } const handleDeleteFile = async (file: FileItem) => { try { @@ -240,7 +221,7 @@ export function PrivateFilesPage() { const actionHandlers: FileActionHandlers = { onFileSelect: toggleFileSelection, - onItemClick: (item) => console.log("Clicked on private item:", item.name), + onItemClick: handleItemClick, onDownload: (file) => console.log("Download file:", file.name), onShare: (file) => console.log("Share file:", file.name), onDelete: handleDeleteFile, @@ -294,28 +275,59 @@ export function PrivateFilesPage() { {/* Header */}
-
-
- -

Private Files

+
+ {currentPath.length > 0 && ( + + )} +
+
+ +

Private Files

+
+

+ {filteredFiles.length} private items • {encryptedCount} encrypted • {sensitiveCount} sensitive +

-

- {filteredFiles.length} private items • {encryptedCount} encrypted • {sensitiveCount} sensitive -

- - +
+ {currentPath.length > 0 && ( + + )} + {/* Toolbar */} @@ -411,6 +423,7 @@ export function PrivateFilesPage() { viewConfig={defaultViewConfig} actionHandlers={actionHandlers} viewMode={viewMode} + onCreateFolder={handleCreateFolder} />
diff --git a/src/routes/Upload.tsx b/src/routes/Upload.tsx index 47a06d4..1609e3c 100644 --- a/src/routes/Upload.tsx +++ b/src/routes/Upload.tsx @@ -6,7 +6,7 @@ const Upload = () => { const { folder_id } = useParams<{ folder_id: string }>(); const location = useLocation(); const navigate = useNavigate(); - const { folder_name } = location.state || {}; + const { folder_name, access_level } = location.state || {}; const allowedTypes: FileConfig[] = [ { @@ -56,6 +56,7 @@ const Upload = () => { allowedTypes={allowedTypes} maxFiles={10} folderId={folder_id} + accessLevel={access_level} />