-
+
-
You've reached the end of your notifications
+
You've reached the end of your notifications
)}
@@ -548,9 +546,9 @@ export default function NotificationsPage() {
animate={{ opacity: 1, scale: 1 }}
className="text-center py-12"
>
-
-
No notifications found
-
+
+
No notifications found
+
{searchQuery
? "Try adjusting your search terms or filters"
: "You're all caught up! No new notifications."
diff --git a/src/pages/private-files-page.tsx b/src/pages/private-files-page.tsx
index 8c52c91..a64179b 100644
--- a/src/pages/private-files-page.tsx
+++ b/src/pages/private-files-page.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState } from "react"
+import { useState, useEffect, useRef, useMemo } from "react"
import { motion } from "framer-motion"
import {
Grid3X3,
@@ -8,16 +8,11 @@ import {
Search,
Filter,
SortAsc,
+ SortDesc,
Download,
Trash2,
- FileText,
- Music,
- Archive,
- FolderIcon,
- Plus,
Upload,
Lock,
- Shield,
EyeOff,
} from "lucide-react"
@@ -26,123 +21,165 @@ 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"
-
-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 { VerifyPinModal } from "@/components/session/VerifyPin"
+import { useAuth } from "@/contexts/useAuth"
+import { useSocket } from "@/contexts/SocketContext"
+import { ArrowLeft } from "lucide-react"
+import { ACCESS_LEVEL, type AccessLevel } 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 [sortDirection, setSortDirection] = useState<"ASC" | "DESC">("ASC")
const [selectedFiles, setSelectedFiles] = useState([])
+ const [currentPath, setCurrentPath] = useState>([])
const [showSensitiveOnly, setShowSensitiveOnly] = useState(false)
-
- const { deleteFileOrFolder } = useFile()
+ const [isPinVerified, setIsPinVerified] = useState(false)
+ const [showPinModal, setShowPinModal] = useState(false)
+ const [isCheckingSession, setIsCheckingSession] = useState(true)
+ const hasCheckedSession = useRef(false)
+
+ const { deleteFileOrFolder, privateFiles, createFolder, updateFileAccessLevel } = useFile()
+ const { user, getPinSession } = useAuth()
+ const navigate = useNavigate()
+
+ const handlePinModalClose = () => {
+ // Navigate back to previous route when modal is closed without verification
+ navigate(-1)
+ }
+
+ // Check for active PIN session on mount (only once)
+ useEffect(() => {
+ // Prevent multiple calls
+ if (hasCheckedSession.current) {
+ return
+ }
+
+ const checkSession = async () => {
+ hasCheckedSession.current = true
+
+ if (!user?.pin_hash) {
+ // If no PIN is set, allow access
+ setIsPinVerified(true)
+ setShowPinModal(false)
+ setIsCheckingSession(false)
+ return
+ }
+
+ try {
+ // Try to get the current session
+ const sessionResult = await getPinSession()
+
+ if (sessionResult.success && sessionResult.session) {
+ // Check if session is valid (within 20 minutes)
+ const verifiedAt = new Date(sessionResult.session.verified_at)
+ const now = new Date()
+ const diffInMinutes = (now.getTime() - verifiedAt.getTime()) / (1000 * 60)
+ const isValid = sessionResult.session.pin_verified && diffInMinutes <= 20
+
+ if (isValid) {
+ // Session is valid (within 20 minutes), auto-verify
+ setIsPinVerified(true)
+ setShowPinModal(false)
+ } else {
+ // Session expired, show PIN modal
+ setIsPinVerified(false)
+ setShowPinModal(true)
+ }
+ } else {
+ // No valid session, show PIN modal
+ setIsPinVerified(false)
+ setShowPinModal(true)
+ }
+ } catch (error) {
+ // If session check fails, show PIN modal
+ setIsPinVerified(false)
+ setShowPinModal(true)
+ } finally {
+ setIsCheckingSession(false)
+ }
+ }
+
+ checkSession()
+ }, [user, getPinSession])
+
+ const handlePinVerified = () => {
+ setIsPinVerified(true)
+ setShowPinModal(false)
+ }
+
+ // 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])
+
+ // Helper function to convert size string to bytes for proper sorting
+ const sizeToBytes = (sizeStr: string): number => {
+ if (!sizeStr || sizeStr === '-') return 0;
+
+ const units: { [key: string]: number } = {
+ 'B': 1,
+ 'KB': 1024,
+ 'MB': 1024 * 1024,
+ 'GB': 1024 * 1024 * 1024,
+ 'TB': 1024 * 1024 * 1024 * 1024,
+ };
- const filteredFiles = mockPrivateFiles.filter((file) => {
- const matchesSearch = file.name.toLowerCase().includes(searchQuery.toLowerCase())
- const matchesSensitive = !showSensitiveOnly || file.sensitive
- return matchesSearch && matchesSensitive
- })
+ const match = sizeStr.trim().match(/^([\d.]+)\s*([A-Z]+)$/i);
+ if (!match) return 0;
+
+ const value = parseFloat(match[1]);
+ const unit = match[2].toUpperCase();
+
+ return value * (units[unit] || 0);
+ };
+
+ const filteredFiles = useMemo(() => {
+ const matchesSearch = (file: PrivateFileItem) => file.name.toLowerCase().includes(searchQuery.toLowerCase())
+ const matchesSensitive = (file: PrivateFileItem) => !showSensitiveOnly || file.sensitive
+
+ let filtered = currentItems.filter((file) => matchesSearch(file) && matchesSensitive(file));
+
+ // Sort by size
+ filtered.sort((a, b) => {
+ // Folders always come first
+ if (a.type === 'folder' && b.type !== 'folder') return -1;
+ if (a.type !== 'folder' && b.type === 'folder') return 1;
+
+ // If both are folders or both are files, sort by size
+ const sizeA = sizeToBytes(a.size);
+ const sizeB = sizeToBytes(b.size);
+
+ if (sortDirection === "ASC") {
+ return sizeA - sizeB;
+ } else {
+ return sizeB - sizeA;
+ }
+ });
+
+ return filtered;
+ }, [currentItems, searchQuery, showSensitiveOnly, sortDirection])
const toggleFileSelection = (fileId: string) => {
setSelectedFiles((prev) => (prev.includes(fileId) ? prev.filter((id) => id !== fileId) : [...prev, fileId]))
@@ -152,8 +189,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 {
@@ -166,144 +261,266 @@ export function PrivateFilesPage() {
toast.error(result.error || 'Failed to delete item');
}
} catch (error) {
- console.error('Delete error:', error);
toast.error('An error occurred while deleting the item');
}
}
+ const handleChangeAccessLevel = async (file: FileItem, accessLevel: string) => {
+ try {
+ const result = await updateFileAccessLevel(file.id, { access_level: accessLevel as AccessLevel });
+ if (result.success) {
+ toast.success("Access level updated successfully!");
+ } else {
+ toast.error(result.error || "Failed to update access level");
+ }
+ } catch (error: any) {
+ toast.error("An error occurred while updating access level");
+ }
+ }
+
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,
- onEncrypt: (file) => console.log("Encrypt file:", file.name),
- onDecrypt: (file) => console.log("Decrypt file:", file.name),
+ onChangeAccessLevel: handleChangeAccessLevel,
}
- return (
-
- {/* Header */}
-
-
-
-
-
-
Private Files
-
-
- {filteredFiles.length} private items • {encryptedCount} encrypted • {sensitiveCount} sensitive
-
-
-
-
-
-
+ // Show loading state while checking session
+ if (isCheckingSession) {
+ return (
+
+
-
-
-
-
- {/* Toolbar */}
-
-
-
-
- setSearchQuery(e.target.value)}
- className="pl-10"
- />
+
+ )
+ }
+
+ // Don't render content until PIN is verified (if PIN is set)
+ if (user?.pin_hash && !isPinVerified) {
+ return (
+ <>
+
+
+
+
+
Please verify your PIN to access private files
-
-
-
+ >
+ )
+ }
-
- {selectedFiles.length > 0 &&
{selectedFiles.length} selected}
-
-
-
+ return (
+ <>
+
+
+ {/* Header */}
+
+
+
+ {currentPath.length > 0 && (
+
+ )}
+
+
+
+
Private Files
+
+
+ {filteredFiles.length} private items
+ •
+ {encryptedCount} encrypted
+ •
+ {sensitiveCount} sensitive
+
+
+
+
+
+
+
-
-
+
+
+ {currentPath.length > 0 && (
+
+ )}
+
+
- {/* Bulk Actions */}
- {selectedFiles.length > 0 && (
+ {/* Toolbar */}
-
- {selectedFiles.length} private items selected
-
-
-
-
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10 w-full"
+ />
+
+
+
+
+
+
+
+
+
+ {selectedFiles.length > 0 && (
+
+ {selectedFiles.length}
+
+ )}
+ {selectedFiles.length > 0 && (
+
+ {selectedFiles.length} selected
+
+ )}
+
+
+
+
- )}
-
- {/* Unified File Manager */}
-
-
+
+ {/* Bulk Actions */}
+ {selectedFiles.length > 0 && (
+
+
+
+
+ {selectedFiles.length} private items selected
+ {selectedFiles.length} selected
+
+
+
+
+
+
+
+ )}
+
+ {/* Unified File Manager */}
+
+
+ >
)
}
diff --git a/src/pages/shared-files-page.tsx b/src/pages/shared-files-page.tsx
index 74b3dba..62fca04 100644
--- a/src/pages/shared-files-page.tsx
+++ b/src/pages/shared-files-page.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useEffect, useState } from "react"
+import { useEffect, useState, useMemo } from "react"
import { motion } from "framer-motion"
import {
Grid3X3,
@@ -9,13 +9,7 @@ import {
Filter,
SortAsc,
Download,
- Trash2,
- FileText,
- Video,
- FolderIcon,
Users,
- UserPlus,
- Link,
Settings,
} from "lucide-react"
@@ -27,99 +21,28 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { FileManager } from "@/components/file-manager/FileManager"
import { sharedPageConfig, defaultViewConfig } from "@/config/page-configs"
import { useFile } from "@/contexts/fileContext"
-import { toast } from "sonner"
-import type { SharedFileItem, FileActionHandlers, FileItem } from "@/types/file-manager"
+import type { FileActionHandlers, FileItem } from "@/types/file-manager"
+import { transformSharedFileSystemNodesToSharedFileItems } from "@/lib/utils"
+import { useAuth } from "@/contexts/useAuth"
+import { BreadcrumbNavigation } from "@/components/file-manager/BreadcrumbNavigation"
+
-const mockSharedFiles: SharedFileItem[] = [
- {
- id: "1",
- name: "Marketing Campaign.pptx",
- type: "file",
- fileType: "presentation",
- size: "12.4 MB",
- modified: "2 hours ago",
- icon: FileText,
- thumbnail: null,
- starred: true,
- shared: true,
- parentPath: [],
- variant: "shared",
- sharedBy: { name: "John Doe", avatar: "/john-avatar.png", initials: "JD" },
- sharedWith: [
- { name: "Alice Smith", avatar: "/alice-avatar.png", initials: "AS" },
- { name: "Bob Wilson", avatar: "/bob-avatar.png", initials: "BW" },
- ],
- permission: "edit",
- sharedDate: "2024-01-15",
- isOwner: false,
- },
- {
- id: "2",
- name: "Design Assets",
- type: "folder",
- fileType: "folder",
- size: "234 MB",
- modified: "1 day ago",
- icon: FolderIcon,
- thumbnail: null,
- starred: false,
- shared: true,
- parentPath: [],
- variant: "shared",
- sharedBy: { name: "Sophie Chamberlain", avatar: "/sophie-avatar.png", initials: "SC" },
- sharedWith: [
- { name: "Design Team", avatar: null, initials: "DT" },
- { name: "Marketing Team", avatar: null, initials: "MT" },
- ],
- permission: "view",
- sharedDate: "2024-01-10",
- isOwner: true,
- },
- {
- id: "3",
- name: "Project Demo.mp4",
- type: "file",
- fileType: "video",
- size: "89.2 MB",
- modified: "3 days ago",
- icon: Video,
- starred: false,
- shared: true,
- parentPath: [],
- variant: "shared",
- sharedBy: { name: "Mike Johnson", avatar: "/mike-avatar.png", initials: "MJ" },
- sharedWith: [{ name: "Client Team", avatar: null, initials: "CT" }],
- permission: "view",
- sharedDate: "2024-01-08",
- isOwner: false,
- },
- {
- id: "4",
- name: "Brand Guidelines.pdf",
- type: "file",
- fileType: "pdf",
- size: "5.8 MB",
- modified: "1 week ago",
- icon: FileText,
- thumbnail: null,
- starred: true,
- shared: true,
- parentPath: [],
- variant: "shared",
- sharedBy: { name: "Sophie Chamberlain", avatar: "/sophie-avatar.png", initials: "SC" },
- sharedWith: [{ name: "Everyone", avatar: null, initials: "EV" }],
- permission: "view",
- sharedDate: "2024-01-01",
- isOwner: true,
- },
-]
export function SharedFilesPage() {
const [viewMode, setViewMode] = useState<"grid" | "list">("grid")
const [searchQuery, setSearchQuery] = useState("")
const [selectedFiles, setSelectedFiles] = useState
([])
const [activeTab, setActiveTab] = useState("all")
- const { getAllSharedFilesWithMe, getAllSharedFilesByMe, getAllSharedFiles, deleteFileOrFolder } = useFile()
+ const [currentPath, setCurrentPath] = useState>([])
+ const {
+ getAllSharedFilesWithMe,
+ getAllSharedFilesByMe,
+ getAllSharedFiles,
+ sharedFiles,
+ sharedFilesByMe,
+ sharedFilesWithMe,
+ } = useFile()
+ const { user } = useAuth()
useEffect(() => {
getAllSharedFilesWithMe()
@@ -127,14 +50,45 @@ export function SharedFilesPage() {
getAllSharedFiles()
}, [])
- const filteredFiles = mockSharedFiles.filter((file) => {
- const matchesSearch = file.name.toLowerCase().includes(searchQuery.toLowerCase())
- const matchesTab =
- activeTab === "all" ||
- (activeTab === "shared-by-me" && file.isOwner) ||
- (activeTab === "shared-with-me" && !file.isOwner)
- return matchesSearch && matchesTab
- })
+ // Reset path when tab changes
+ useEffect(() => {
+ setCurrentPath([])
+ }, [activeTab])
+
+ const transformedFiles = useMemo(() => {
+ let filesToTransform: any[] = [];
+
+ if (activeTab === "all") {
+ filesToTransform = sharedFiles;
+ } else if (activeTab === "shared-by-me") {
+ filesToTransform = sharedFilesByMe;
+ } else if (activeTab === "shared-with-me") {
+ filesToTransform = sharedFilesWithMe;
+ }
+
+ return transformSharedFileSystemNodesToSharedFileItems(filesToTransform).map(file => ({
+ ...file,
+ isOwner: file.sharedBy.name === user?.display_name // Simple check, ideally check ID
+ }));
+ }, [sharedFiles, sharedFilesByMe, sharedFilesWithMe, activeTab, user]);
+
+ const currentItems = useMemo(() => {
+ let items: FileItem[] = transformedFiles
+ for (const folder of currentPath) {
+ const foundFolder = items.find((item) => item.id === folder.id && item.type === "folder")
+ if (foundFolder?.children) {
+ items = foundFolder.children
+ }
+ }
+ return items
+ }, [currentPath, transformedFiles])
+
+ const filteredFiles = useMemo(() => {
+ return currentItems.filter((file) => {
+ const matchesSearch = file.name.toLowerCase().includes(searchQuery.toLowerCase())
+ return matchesSearch
+ })
+ }, [currentItems, searchQuery])
const toggleFileSelection = (fileId: string) => {
setSelectedFiles((prev) => (prev.includes(fileId) ? prev.filter((id) => id !== fileId) : [...prev, fileId]))
@@ -144,57 +98,48 @@ export function SharedFilesPage() {
setSelectedFiles(selectedFiles.length === filteredFiles.length ? [] : filteredFiles.map((f) => f.id))
}
- const sharedByMeCount = mockSharedFiles.filter((f) => f.isOwner).length
- const sharedWithMeCount = mockSharedFiles.filter((f) => !f.isOwner).length
+ const sharedByMeCount = sharedFilesByMe.length
+ const sharedWithMeCount = sharedFilesWithMe.length
- const handleDeleteFile = async (file: FileItem) => {
- try {
- const result = await deleteFileOrFolder(file.id);
- if (result.success) {
- toast.success(`${file.type === 'folder' ? 'Folder' : 'File'} deleted successfully!`);
- // Remove from selected files if it was selected
- setSelectedFiles(prev => prev.filter(id => id !== file.id));
- } else {
- toast.error(result.error || 'Failed to delete item');
- }
- } catch (error) {
- console.error('Delete error:', error);
- toast.error('An error occurred while deleting the item');
+ const handleItemClick = (item: FileItem) => {
+ if (item.type === "folder") {
+ setCurrentPath([...currentPath, { id: item.id, name: item.name }])
+ setSelectedFiles([])
+ setSearchQuery("")
+ }
+ }
+
+ const handleBreadcrumbNavigate = (index: number) => {
+ if (index === -1) {
+ setCurrentPath([])
+ } else {
+ setCurrentPath(currentPath.slice(0, index + 1))
}
+ setSelectedFiles([])
+ setSearchQuery("")
}
const actionHandlers: FileActionHandlers = {
onFileSelect: toggleFileSelection,
- onItemClick: (item) => console.log("Clicked on shared item:", item.name),
+ onItemClick: handleItemClick,
onDownload: (file) => console.log("Download file:", file.name),
onShare: (file) => console.log("Share file:", file.name),
- onDelete: handleDeleteFile,
}
return (
-
+
{/* Header */}
-
+
Shared with me
-
+
{filteredFiles.length} shared items • {sharedByMeCount} shared by me • {sharedWithMeCount} shared with me
-
-
-
-
@@ -215,36 +160,42 @@ export function SharedFilesPage() {
+ {currentPath.length > 0 && (
+
+ )}
+
{/* Toolbar */}
-
-
+
+
setSearchQuery(e.target.value)}
- className="pl-10"
+ className="pl-10 w-full"
/>
-
-
+
+
+
+
-
+
{selectedFiles.length > 0 &&
{selectedFiles.length} selected}
-
+