From 7585d2668b1cc847d169f92f4b74d85d54e1f0dd Mon Sep 17 00:00:00 2001 From: vikas Date: Thu, 27 Mar 2025 03:44:43 +0530 Subject: [PATCH 1/9] Added folders and integerated files and folders with API --- .../dashboard/buckets/[bucket-name]/page.tsx | 277 ------------------ .../dashboard/buckets/[bucketName]/page.tsx | 215 ++++++++++++++ app/(pages)/dashboard/buckets/page.tsx | 117 ++------ app/(pages)/login/components/login-form.tsx | 64 ++-- app/(pages)/login/components/login-info.tsx | 101 ++++--- app/(pages)/login/page.tsx | 10 +- components/bucket-component/breadcrumb.tsx | 54 ++++ .../bucket-component/bucket-grid-skeleton.tsx | 69 +++++ components/bucket-component/bucket-grid.tsx | 33 +++ .../bucket-component/bucket-toolbar.tsx | 61 ++++ .../bucket-component/file-type-filter.tsx | 83 ++++++ .../bucket-component/loading-skeleton.tsx | 19 ++ components/files-component/files.tsx | 172 +++++------ components/health-check.tsx | 106 ++++--- hooks/useAuth.ts | 50 ++++ hooks/useBucketFiltering.ts | 57 ++++ hooks/useFiles.ts | 148 ++++++++++ package-lock.json | 17 ++ package.json | 2 + services/base.service.ts | 84 ++++++ services/s3.service.ts | 23 ++ static/files.ts | 49 ---- types/S3Objects.ts | 37 ++- types/api.ts | 16 + types/fileTypes.ts | 25 ++ 25 files changed, 1231 insertions(+), 658 deletions(-) delete mode 100644 app/(pages)/dashboard/buckets/[bucket-name]/page.tsx create mode 100644 app/(pages)/dashboard/buckets/[bucketName]/page.tsx create mode 100644 components/bucket-component/breadcrumb.tsx create mode 100644 components/bucket-component/bucket-grid-skeleton.tsx create mode 100644 components/bucket-component/bucket-grid.tsx create mode 100644 components/bucket-component/bucket-toolbar.tsx create mode 100644 components/bucket-component/file-type-filter.tsx create mode 100644 components/bucket-component/loading-skeleton.tsx create mode 100644 hooks/useAuth.ts create mode 100644 hooks/useBucketFiltering.ts create mode 100644 hooks/useFiles.ts create mode 100644 services/base.service.ts create mode 100644 services/s3.service.ts delete mode 100644 static/files.ts create mode 100644 types/api.ts create mode 100644 types/fileTypes.ts diff --git a/app/(pages)/dashboard/buckets/[bucket-name]/page.tsx b/app/(pages)/dashboard/buckets/[bucket-name]/page.tsx deleted file mode 100644 index e63d32a..0000000 --- a/app/(pages)/dashboard/buckets/[bucket-name]/page.tsx +++ /dev/null @@ -1,277 +0,0 @@ -"use client"; - -import React from "react"; -import { Button } from "@/components/ui/button"; -import { Upload, Check, ChevronsUpDown } from "lucide-react"; -import dynamic from "next/dynamic"; - -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { cn } from "@/lib/utils"; -import { S3File, S3Root } from "@/types/S3Objects"; - -type FileType = - | "document" - | "compressed" - | "image" - | "audio" - | "video" - | "unknown" - | "folder"; - -const mockFileData: S3Root = { - folders: [ - { - folders: [], - name: "compressed", - files: [ - { - extension: "rar", - etag: '"dd9bb762731449c0e06a2c869b739db1"', - size: "1.56 MB", - type: "compressed", - name: "lab.rar", - location: "compressed/lab.rar", - lastModified: "2025-03-08T05:15:12Z", - }, - ], - location: "compressed/", - type: "folder", - itemCount: 1, - }, - { - folders: [], - name: "documents", - files: [ - { - extension: "pdf", - etag: '"52e419c0508aaeea9213f513b84e8d73"', - size: "9.17 KB", - type: "document", - name: "rptTimeTableStudent.pdf", - location: "documents/rptTimeTableStudent.pdf", - lastModified: "2025-03-08T05:15:11Z", - }, - ], - location: "documents/", - type: "folder", - itemCount: 1, - }, - { - folders: [], - name: "videos", - files: [ - { - extension: "mp4", - etag: '"447b7f6a669fc87a1e5c1d506474a7cb"', - size: "1.12 MB", - type: "video", - name: "VID_4556446.mp4", - location: "videos/VID_4556446.mp4", - lastModified: "2025-03-08T05:15:12Z", - }, - { - extension: "mp4", - etag: '"4aa28625598a7adfb7ec3fef5e44453b-2"', - size: "31.48 MB", - type: "video", - name: "VID_5468553.mp4", - location: "videos/VID_5468553.mp4", - lastModified: "2025-03-08T05:15:11Z", - }, - ], - location: "videos/", - type: "folder", - itemCount: 2, - }, - ], - name: "root", - files: [ - { - extension: "jpg", - etag: '"8aa445cbeca5119ec77c4e4bb760e12e"', - size: "130.04 KB", - type: "image", - name: "IMG_2135545.jpg", - location: "IMG_2135545.jpg", - lastModified: "2025-03-08T05:15:13Z", - }, - { - extension: "jpg", - etag: '"3ee4b3bb7fe145c77a883db5b201a172"', - size: "93.66 KB", - type: "image", - name: "IMG_2135546.jpg", - location: "IMG_2135546.jpg", - lastModified: "2025-03-08T05:15:13Z", - }, - ], - location: "", - type: "folder", - itemCount: 5, -}; - -const fileTypes = [ - { value: "all", label: "All Files" }, - { value: "document", label: "Documents" }, - { value: "compressed", label: "Compressed" }, - { value: "image", label: "Images" }, - { value: "audio", label: "Audio" }, - { value: "video", label: "Video" }, - { value: "folder", label: "Folders" }, -]; - -const Files = dynamic(() => import("@/components/files-component/files")); -const SearchBar = dynamic(() => import("@/components/bucket-component/search")); -const FilterButtons = dynamic( - () => import("@/components/bucket-component/filter") -); - -export default function Page() { - const [searchTerm, setSearchTerm] = React.useState(""); - const [activeFilter, setActiveFilter] = React.useState< - "size" | "date" | null - >(null); - const [open, setOpen] = React.useState(false); - const [selectedType, setSelectedType] = React.useState("all"); - - const filteredAndSortedData = React.useMemo(() => { - const filterItems = (items: S3File[] | S3Root["folders"]) => { - return items.filter((item) => - item.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - }; - - let filteredFiles = filterItems(mockFileData.files); - const filteredFolders = filterItems(mockFileData.folders); - - // Apply type filter - if (selectedType !== "all") { - filteredFiles = filteredFiles.filter((file) => file.type === selectedType); - } - - // Apply date or size filter only to files - if (activeFilter === "date") { - filteredFiles = [...filteredFiles].sort((a, b) => { - if ('lastModified' in a && 'lastModified' in b) { - return new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime(); - } - return 0; - }); - } else if (activeFilter === "size") { - const convertToBytes = (size: string) => { - const num = parseFloat(size); - if (size.includes("GB")) return num * 1024 * 1024 * 1024; - if (size.includes("MB")) return num * 1024 * 1024; - if (size.includes("KB")) return num * 1024; - return num; - }; - filteredFiles = [...filteredFiles].sort((a, b) => { - if ('size' in a && 'size' in b) { - return convertToBytes(b.size) - convertToBytes(a.size); - } - return 0; - }); - } - - return { - files: filteredFiles, - folders: filteredFolders, - }; - }, [searchTerm, activeFilter, selectedType]); - - - - return ( -
-
-
- -
-
- - - - - - - - - - - No results found. - - - {fileTypes.map((type) => ( - { - setSelectedType(currentValue); - setOpen(false); - }} - className="dark:text-gray-100 dark:hover:bg-gray-700" - > - {type.label} - - - ))} - - - - - - -
-
-
- {filteredAndSortedData.folders.map((folder) => ( - - ))} - {filteredAndSortedData.files.map((file) => ( - - ))} -
-
- ); -} diff --git a/app/(pages)/dashboard/buckets/[bucketName]/page.tsx b/app/(pages)/dashboard/buckets/[bucketName]/page.tsx new file mode 100644 index 0000000..1d4d208 --- /dev/null +++ b/app/(pages)/dashboard/buckets/[bucketName]/page.tsx @@ -0,0 +1,215 @@ +"use client"; + +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Upload } from "lucide-react"; +import dynamic from "next/dynamic"; +import { S3File, S3Folder, S3Root } from "@/types/S3Objects"; +import Breadcrumb from "@/components/bucket-component/breadcrumb"; +import LoadingSkeleton from "@/components/bucket-component/loading-skeleton"; +import FileTypeFilter from "@/components/bucket-component/file-type-filter"; +import { s3Service } from "@/services/s3.service"; + +type FileType = + | "document" + | "compressed" + | "image" + | "audio" + | "video" + | "unknown" + | "folder"; + +const Files = dynamic(() => import("@/components/files-component/files")); +const SearchBar = dynamic(() => import("@/components/bucket-component/search")); +const FilterButtons = dynamic( + () => import("@/components/bucket-component/filter") +); + +interface PageProps { + params: Promise<{ + bucketName: string; + }>; +} + +interface FileData { + name: string; + itemCount?: number; + size?: string; + extension?: string; +} + +export default function Page({ params }: PageProps) { + const resolvedParams = React.use(params); + const [searchTerm, setSearchTerm] = React.useState(""); + const [activeFilter, setActiveFilter] = React.useState< + "size" | "date" | null + >(null); + const [selectedType, setSelectedType] = React.useState("all"); + const [currentPath, setCurrentPath] = React.useState(""); + const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(null); + const [fileData, setFileData] = React.useState(null); + + const fetchFiles = React.useCallback(async () => { + try { + setIsLoading(true); + setError(null); + + const response = await s3Service.listObjects({ + bucketName: resolvedParams.bucketName, + objectPrefix: currentPath, + }); + + setFileData(response.data); + } catch (err) { + if (err instanceof Error && err.name !== "AbortError") { + console.error("Error fetching files:", err); + setError(err.message || "Failed to fetch files"); + } + } finally { + setIsLoading(false); + } + }, [resolvedParams.bucketName, currentPath]); + + React.useEffect(() => { + fetchFiles(); + }, [fetchFiles]); + + React.useEffect(() => { + return () => { + s3Service.cancelAllRequests(); + }; + }, []); + + const handleFolderClick = (folder: S3Folder) => { + setCurrentPath(folder.key); + }; + + const filteredAndSortedData = React.useMemo(() => { + if (!fileData) return { files: [], folders: [] }; + + const filterFiles = (files: S3File[] = []) => { + return files.filter((file) => { + const fileName = file.key.split("/").pop() || ""; + return fileName.toLowerCase().includes(searchTerm.toLowerCase()); + }); + }; + + const filterFolders = (folders: S3Folder[] = []) => { + return folders.filter((folder) => { + const folderName = folder.key.replace(/\/$/, ""); + return folderName.toLowerCase().includes(searchTerm.toLowerCase()); + }); + }; + + let filteredFiles = filterFiles(fileData.files || []); + const filteredFolders = filterFolders(fileData.folders || []); + + if (selectedType !== "all") { + filteredFiles = filteredFiles.filter( + (file) => file.fileType.toLowerCase() === selectedType + ); + } + + if (activeFilter === "date") { + filteredFiles = [...filteredFiles].sort((a, b) => { + return ( + new Date(b.lastModified).getTime() - + new Date(a.lastModified).getTime() + ); + }); + } else if (activeFilter === "size") { + const convertToBytes = (size: string) => { + const num = parseFloat(size); + if (size.includes("GB")) return num * 1024 * 1024 * 1024; + if (size.includes("MB")) return num * 1024 * 1024; + if (size.includes("KB")) return num * 1024; + return num; + }; + filteredFiles = [...filteredFiles].sort((a, b) => { + return convertToBytes(b.size) - convertToBytes(a.size); + }); + } + + return { + files: filteredFiles, + folders: filteredFolders, + }; + }, [fileData, searchTerm, activeFilter, selectedType]); + + const mapS3FileToFileData = (file: S3File): FileData => ({ + name: file.key.split("/").pop() || "", + size: file.size, + extension: file.extension, + }); + + const mapS3FolderToFileData = (folder: S3Folder): FileData => ({ + name: folder.key.replace(/\/$/, ""), + itemCount: 0, + }); + + if (error) { + return ( +
+
Error: {error}
+
+ ); + } + + return ( +
+
+
+ +
+
+ + + +
+
+ + + +
+ {isLoading ? ( + + ) : ( +
+ {filteredAndSortedData.folders.map((folder) => ( +
+ handleFolderClick(folder)} + /> +
+ ))} + {filteredAndSortedData.files.map((file) => ( +
+ +
+ ))} +
+ )} +
+
+ ); +} diff --git a/app/(pages)/dashboard/buckets/page.tsx b/app/(pages)/dashboard/buckets/page.tsx index dff3b7f..97fd1ef 100644 --- a/app/(pages)/dashboard/buckets/page.tsx +++ b/app/(pages)/dashboard/buckets/page.tsx @@ -1,37 +1,11 @@ "use client"; -import { useState, useMemo } from "react"; -import dynamic from "next/dynamic"; -import { CreateBucket } from "@/components/bucket-component/create-bkt"; +import { useState } from "react"; import useBuckets from "@/hooks/useBuckets"; - -// Dynamically imported components with loading placeholders -const SearchBar = dynamic( - () => import("@/components/bucket-component/search"), - { - loading: () => ( -
- ), - } -); -const FilterButtons = dynamic( - () => import("@/components/bucket-component/filter"), - { - loading: () =>
, - } -); -const BucketCard = dynamic( - () => import("@/components/bucket-component/bucket-card"), - { - loading: () =>
, - } -); -const SelectRegions = dynamic( - () => import("@/components/bucket-component/regions"), - { - loading: () =>
, - } -); +import { useBucketFiltering } from "@/hooks/useBucketFiltering"; +import BucketToolbar from "@/components/bucket-component/bucket-toolbar"; +import BucketGrid from "@/components/bucket-component/bucket-grid"; +import BucketGridSkeleton from "@/components/bucket-component/bucket-grid-skeleton"; export default function Page() { const { buckets, isLoading, isError, refetch } = useBuckets(); @@ -40,69 +14,36 @@ export default function Page() { ); const [searchTerm, setSearchTerm] = useState(""); - // Compute the displayed buckets based on the search term and active filter - const displayedBuckets = useMemo(() => { - let data = [...buckets]; + const filteredBuckets = useBucketFiltering({ + buckets, + searchTerm, + activeFilter, + }); - if (searchTerm) { - data = data.filter((bucket) => - bucket.bucketName.toLowerCase().includes(searchTerm.toLowerCase()) - ); - } - if (activeFilter === "size") { - data = data.sort((a, b) => { - const sizeToBytes = (size: string) => { - const [value, unit] = size.split(" "); - const numValue = parseFloat(value); - switch (unit?.toUpperCase()) { - case "KB": - return numValue * 1024; - case "MB": - return numValue * 1024 * 1024; - case "GB": - return numValue * 1024 * 1024 * 1024; - case "TB": - return numValue * 1024 * 1024 * 1024 * 1024; - default: - return numValue; - } - }; - return sizeToBytes(b.size || "0") - sizeToBytes(a.size || "0"); - }); - } else if (activeFilter === "date") { - data = data.sort( - (a, b) => - new Date(b.createdOn).getTime() - new Date(a.createdOn).getTime() - ); - } - return data; - }, [buckets, searchTerm, activeFilter]); + if (isError) { + return ( +
+
+ Error: {isError} +
+
+ ); + } return (
-
- - - - -
+ + {isLoading ? ( -
- {[...Array(4)].map((_, index) => ( -
- ))} -
- ) : isError ? ( -

{isError}

+ ) : ( -
- {displayedBuckets.map((bucket) => ( - - ))} -
+ )}
); diff --git a/app/(pages)/login/components/login-form.tsx b/app/(pages)/login/components/login-form.tsx index 9775cd2..a9f3052 100644 --- a/app/(pages)/login/components/login-form.tsx +++ b/app/(pages)/login/components/login-form.tsx @@ -9,12 +9,20 @@ import { useRouter } from "next/navigation"; import { toast } from "sonner"; import SpinnerIcon from "@/icons/spinner-icon"; import Redirecting from "@/skeleton/redirecting"; +import { useAuth } from "@/hooks/useAuth"; -export default function LoginForm({ - className, - ...props -}: React.ComponentPropsWithoutRef<"form">) { - const [credentials, setCredentials] = useState({ +interface LoginCredentials { + accessKey: string; + secretKey: string; + region: string; +} + +interface LoginFormProps extends React.ComponentPropsWithoutRef<"form"> { + className?: string; +} + +export default function LoginForm({ className, ...props }: LoginFormProps) { + const [credentials, setCredentials] = useState({ accessKey: "", secretKey: "", region: "ap-south-1", @@ -22,37 +30,31 @@ export default function LoginForm({ const [loading, setLoading] = useState(false); const [isRedirecting, setIsRedirecting] = useState(false); const router = useRouter(); + const { login } = useAuth(); + + const handleInputChange = + (field: keyof LoginCredentials) => + (e: React.ChangeEvent) => { + setCredentials((prev) => ({ + ...prev, + [field]: e.target.value, + })); + }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/login`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(credentials), - } - ); - - const data = await response.json(); - - if (response.ok && data.status === "SUCCESS" && data.data.sessionToken) { - document.cookie = `sessionToken=${data.data.sessionToken}; path=/; Secure; SameSite=Strict`; - toast.success("Login successful!"); + const success = await login(credentials); + if (success) { setIsRedirecting(true); setTimeout(() => { router.push("/dashboard/buckets"); }, 200); - } else { - toast.error(data.message || "Invalid credentials"); } } catch (error) { - console.error("Error:", error); + console.error("Login error:", error); toast.error("Failed to validate credentials"); } finally { setLoading(false); @@ -88,12 +90,7 @@ export default function LoginForm({ maxLength={264} value={credentials.accessKey} className="dark:bg-gray-800 dark:text-white dark:border-gray-700 h-12" - onChange={(e) => - setCredentials((prev) => ({ - ...prev, - accessKey: e.target.value, - })) - } + onChange={handleInputChange("accessKey")} />
@@ -109,12 +106,7 @@ export default function LoginForm({ disabled={loading} value={credentials.secretKey} className="dark:bg-gray-800 dark:text-white dark:border-gray-700 h-12" - onChange={(e) => - setCredentials((prev) => ({ - ...prev, - secretKey: e.target.value, - })) - } + onChange={handleInputChange("secretKey")} />
+ {pathSegments.map((segment, index) => { + const path = pathSegments.slice(0, index + 1).join("/") + "/"; + const isLast = index === pathSegments.length - 1; + + return ( +
+ + +
+ ); + })} +
+ ); +} diff --git a/components/bucket-component/bucket-grid-skeleton.tsx b/components/bucket-component/bucket-grid-skeleton.tsx new file mode 100644 index 0000000..84a9d2b --- /dev/null +++ b/components/bucket-component/bucket-grid-skeleton.tsx @@ -0,0 +1,69 @@ +import { cn } from "@/lib/utils"; + +interface BucketGridSkeletonProps { + count?: number; + className?: string; + columnCount?: { + sm?: number; + md?: number; + lg?: number; + xl?: number; + "2xl"?: number; + }; +} + +export default function BucketGridSkeleton({ + count = 4, + className, + columnCount = { + sm: 2, + md: 2, + lg: 3, + xl: 4, + "2xl": 5, + }, +}: BucketGridSkeletonProps) { + // Generate dynamic grid columns class + const gridColsClass = Object.entries(columnCount) + .map(([breakpoint, count]) => { + if (breakpoint === "sm") return `sm:grid-cols-${count}`; + if (breakpoint === "md") return `md:grid-cols-${count}`; + if (breakpoint === "lg") return `lg:grid-cols-${count}`; + if (breakpoint === "xl") return `xl:grid-cols-${count}`; + if (breakpoint === "2xl") return `2xl:grid-cols-${count}`; + return ""; + }) + .filter(Boolean) + .join(" "); + + return ( +
+ {[...Array(count)].map((_, index) => ( +
+ {/* Menu button skeleton */} +
+
+
+ +
+ {/* Region and size badges skeleton */} +
+
+
+
+
+ + {/* Bucket name skeleton */} +
+ + {/* Creation date skeleton */} +
+
+
+ ))} +
+ ); +} diff --git a/components/bucket-component/bucket-grid.tsx b/components/bucket-component/bucket-grid.tsx new file mode 100644 index 0000000..63e0062 --- /dev/null +++ b/components/bucket-component/bucket-grid.tsx @@ -0,0 +1,33 @@ +import { Bucket } from "@/types/bucket"; +import dynamic from "next/dynamic"; + +const BucketCard = dynamic( + () => import("@/components/bucket-component/bucket-card"), + { + loading: () => ( +
+ ), + } +); + +interface BucketGridProps { + buckets: Bucket[]; + onBucketDelete: () => void; +} + +export default function BucketGrid({ + buckets, + onBucketDelete, +}: BucketGridProps) { + return ( +
+ {buckets.map((bucket) => ( + + ))} +
+ ); +} diff --git a/components/bucket-component/bucket-toolbar.tsx b/components/bucket-component/bucket-toolbar.tsx new file mode 100644 index 0000000..8f59d13 --- /dev/null +++ b/components/bucket-component/bucket-toolbar.tsx @@ -0,0 +1,61 @@ +import dynamic from "next/dynamic"; +import { CreateBucket } from "@/components/bucket-component/create-bkt"; + +const SearchBar = dynamic( + () => import("@/components/bucket-component/search"), + { + loading: () => ( +
+ ), + } +); + +const FilterButtons = dynamic( + () => import("@/components/bucket-component/filter"), + { + loading: () => ( +
+ ), + } +); + +const SelectRegions = dynamic( + () => import("@/components/bucket-component/regions"), + { + loading: () => ( +
+ ), + } +); + +interface BucketToolbarProps { + searchTerm: string; + setSearchTerm: (term: string) => void; + activeFilter: "size" | "date" | null; + setActiveFilter: (filter: "size" | "date" | null) => void; + onBucketCreated: () => void; +} + +export default function BucketToolbar({ + searchTerm, + setSearchTerm, + activeFilter, + setActiveFilter, + onBucketCreated, +}: BucketToolbarProps) { + return ( +
+
+ +
+
+ + + +
+
+ ); +} diff --git a/components/bucket-component/file-type-filter.tsx b/components/bucket-component/file-type-filter.tsx new file mode 100644 index 0000000..c0d442c --- /dev/null +++ b/components/bucket-component/file-type-filter.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +const fileTypes = [ + { value: "all", label: "All Files" }, + { value: "document", label: "Documents" }, + { value: "compressed", label: "Compressed" }, + { value: "image", label: "Images" }, + { value: "audio", label: "Audio" }, + { value: "video", label: "Video" }, + { value: "folder", label: "Folders" }, +]; + +interface FileTypeFilterProps { + selectedType: string; + setSelectedType: (type: string) => void; +} + +export default function FileTypeFilter({ + selectedType, + setSelectedType, +}: FileTypeFilterProps) { + const [open, setOpen] = React.useState(false); + + return ( + + + + + + + + + No results found. + + {fileTypes.map((type) => ( + { + setSelectedType(currentValue); + setOpen(false); + }} + > + {type.label} + + + ))} + + + + + + ); +} diff --git a/components/bucket-component/loading-skeleton.tsx b/components/bucket-component/loading-skeleton.tsx new file mode 100644 index 0000000..140b00c --- /dev/null +++ b/components/bucket-component/loading-skeleton.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +export default function LoadingSkeleton() { + return ( +
+ {[...Array(8)].map((_, index) => ( +
+
+
+
+
+
+
+
+
+ ))} +
+ ); +} diff --git a/components/files-component/files.tsx b/components/files-component/files.tsx index a3706d6..ef64dc2 100644 --- a/components/files-component/files.tsx +++ b/components/files-component/files.tsx @@ -1,4 +1,3 @@ -import Link from "next/link"; import React from "react"; import { ContextMenu, @@ -9,6 +8,15 @@ import { ContextMenuTrigger, } from "@/components/ui/context-menu"; import { Card, CardContent } from "@/components/ui/card"; +import { + FileText, + FileArchive, + FileImage, + FileAudio, + FileVideo, + Folder, + FileQuestion, +} from "lucide-react"; interface FileData { name: string; @@ -18,133 +26,111 @@ interface FileData { } interface FileProps { - type: "document" | "compressed" | "image" | "audio" | "video" | "folder" | "unknown"; + type: + | "document" + | "compressed" + | "image" + | "audio" + | "video" + | "folder" + | "unknown"; data: FileData; + onClick?: () => void; + icon?: React.ElementType; } -export default function Files({ type, data }: FileProps) { - const getIconProperties = () => { +export default function Files({ type, data, onClick, icon: Icon }: FileProps) { + const getIconColor = () => { + switch (type) { + case "document": + return "text-gray-700 dark:text-gray-300"; + case "compressed": + return "text-gray-700 dark:text-gray-300"; + case "image": + return "text-gray-700 dark:text-gray-300"; + case "audio": + return "text-gray-700 dark:text-gray-300"; + case "video": + return "text-gray-700 dark:text-gray-300"; + case "folder": + return "text-gray-700 dark:text-gray-300"; + default: + return "text-gray-700 dark:text-gray-300"; + } + }; + + const getDefaultIcon = () => { switch (type) { case "document": - return { - color: "text-blue-500 dark:text-blue-400", - path: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z", - }; + return FileText; case "compressed": - return { - color: "text-green-500 dark:text-green-400", - path: "M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8M10 12h4", - }; + return FileArchive; case "image": - return { - color: "text-purple-500 dark:text-purple-400", - path: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z", - }; + return FileImage; case "audio": - return { - color: "text-red-500 dark:text-red-400", - path: "M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3", - }; + return FileAudio; case "video": - return { - color: "text-yellow-500 dark:text-yellow-400", - path: "M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z", - }; + return FileVideo; case "folder": - return { - color: "text-orange-500 dark:text-orange-400", - path: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z", - }; + return Folder; + default: + return FileQuestion; } }; - const icon = getIconProperties(); + const IconComponent = Icon || getDefaultIcon(); + const iconColor = getIconColor(); return ( - - -
- - - + + +
+
-
+
- 13 ? "group-hover:animate-marquee" : "" } inline-block`} > {data.name} - +
-

- {(data.extension ?? "").toUpperCase()}{" "} - {type === "folder" - ? `${data.itemCount} items` - : `• ${data.size}`} -

+ {data.size && ( +

+ {data.size} +

+ )} + {data.itemCount !== undefined && ( +

+ {data.itemCount} items +

+ )}
- - - Open - ⌘O - - + + Download ⌘D - + Share ⌘S - - - Copy URL - ⌘C - - - Rename - ⌘R - - - + + Delete - + ⌘⌫ diff --git a/components/health-check.tsx b/components/health-check.tsx index 468fa13..6386579 100644 --- a/components/health-check.tsx +++ b/components/health-check.tsx @@ -5,43 +5,20 @@ import React, { useEffect, useState } from "react"; interface HealthCheckProps { className?: string; + isCollapsed?: boolean; } -export default function HealthCheck({ className = "" }: HealthCheckProps) { +export default function HealthCheck({ + className = "", + isCollapsed = false, +}: HealthCheckProps) { const [status, setStatus] = useState("loading"); - useEffect(() => { - const checkHealth = async () => { - try { - const response = await healthCheck(); - setStatus(response.status); - } catch (error) { - setStatus("FAILED"); - console.log("Health check failed", error); - } - }; - - checkHealth(); - - const interval = setInterval(checkHealth, 10000); - - return () => clearInterval(interval); - }, []); - - return ( -
- {status === "loading" ? ( + const renderStatus = () => { + if (status === "loading") { + return ( - + - Connecting... + {!isCollapsed && Connecting...} - ) : status === "SUCCESS" ? ( + ); + } + + if (status === "SUCCESS") { + return ( - + - Connected to server - - ) : ( - - - - - Connection failed + {!isCollapsed && Connected to server} + ); + } + + return ( + + + + + {!isCollapsed && Connection failed} + + ); + }; + + useEffect(() => { + const checkHealth = async () => { + try { + const response = await healthCheck(); + setStatus(response.status); + } catch (error) { + setStatus("FAILED"); + console.log("Health check failed", error); + } + }; + + checkHealth(); + + const interval = setInterval(checkHealth, 10000); + + return () => clearInterval(interval); + }, []); + + return ( +
+ {renderStatus()}
); } diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts new file mode 100644 index 0000000..62d6c83 --- /dev/null +++ b/hooks/useAuth.ts @@ -0,0 +1,50 @@ +import { toast } from "sonner"; + +interface LoginCredentials { + accessKey: string; + secretKey: string; + region: string; +} + +export function useAuth() { + const login = async (credentials: LoginCredentials): Promise => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/auth/login`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(credentials), + } + ); + + const data = await response.json(); + + if (response.ok && data.status === "SUCCESS" && data.data.sessionToken) { + document.cookie = `sessionToken=${data.data.sessionToken}; path=/; Secure; SameSite=Strict`; + toast.success("Login successful!"); + return true; + } else { + toast.error(data.message || "Invalid credentials"); + return false; + } + } catch (error) { + console.error("Auth error:", error); + toast.error("Failed to validate credentials"); + return false; + } + }; + + const logout = () => { + document.cookie = + "sessionToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"; + window.location.href = "/login"; + }; + + return { + login, + logout, + }; +} diff --git a/hooks/useBucketFiltering.ts b/hooks/useBucketFiltering.ts new file mode 100644 index 0000000..ac3bdac --- /dev/null +++ b/hooks/useBucketFiltering.ts @@ -0,0 +1,57 @@ +import { useMemo } from "react"; +import { Bucket } from "@/types/bucket"; + +interface UseBucketFilteringProps { + buckets: Bucket[]; + searchTerm: string; + activeFilter: "size" | "date" | null; +} + +export function useBucketFiltering({ + buckets, + searchTerm, + activeFilter, +}: UseBucketFilteringProps) { + const filteredBuckets = useMemo(() => { + let data = [...buckets]; + + // Apply search filter + if (searchTerm) { + data = data.filter((bucket) => + bucket.bucketName.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + // Apply sorting + if (activeFilter === "size") { + data = data.sort((a, b) => { + const sizeToBytes = (size: string) => { + const [value, unit] = size.split(" "); + const numValue = parseFloat(value); + switch (unit?.toUpperCase()) { + case "KB": + return numValue * 1024; + case "MB": + return numValue * 1024 * 1024; + case "GB": + return numValue * 1024 * 1024 * 1024; + case "TB": + return numValue * 1024 * 1024 * 1024 * 1024; + default: + return numValue; + } + }; + return sizeToBytes(b.size || "0") - sizeToBytes(a.size || "0"); + }); + } else if (activeFilter === "date") { + data = data.sort( + (a, b) => + new Date(b.createdOn).getTime() - new Date(a.createdOn).getTime() + ); + } + + return data; + }, [buckets, searchTerm, activeFilter]); + + return filteredBuckets; +} diff --git a/hooks/useFiles.ts b/hooks/useFiles.ts new file mode 100644 index 0000000..82decd5 --- /dev/null +++ b/hooks/useFiles.ts @@ -0,0 +1,148 @@ +import { useState, useEffect, useMemo } from "react"; +import { FileType, FilterType } from "@/types/fileTypes"; + +interface File { + name: string; + type: FileType; + size: number; + lastModified: Date; + path: string; +} + +interface Folder { + name: string; + path: string; +} + +interface UseFilesProps { + bucketName: string; + path?: string; +} + +interface S3Object { + name: string; + type: FileType; + size: number; + lastModified: string; + path: string; +} + +export const useFiles = ({ bucketName, path = "" }: UseFilesProps) => { + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [files, setFiles] = useState([]); + const [folders, setFolders] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [activeFilter, setActiveFilter] = useState(null); + const [selectedType, setSelectedType] = useState("all"); + + const fetchFiles = async () => { + try { + setIsLoading(true); + setError(null); + const response = await fetch("http://localhost:8080/s3/list-objects", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + bucketName, + prefix: path, + }), + }); + + if (!response.ok) { + throw new Error("Failed to fetch files"); + } + + const data = (await response.json()) as { objects: S3Object[] }; + + // Process files and folders from the response + const newFiles: File[] = []; + const newFolders: Folder[] = []; + + data.objects.forEach((obj: S3Object) => { + if (obj.type === "folder") { + newFolders.push({ + name: obj.name, + path: obj.path, + }); + } else { + newFiles.push({ + name: obj.name, + type: obj.type, + size: obj.size, + lastModified: new Date(obj.lastModified), + path: obj.path, + }); + } + }); + + setFiles(newFiles); + setFolders(newFolders); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchFiles(); + }, [bucketName, path]); + + const filteredAndSortedData = useMemo(() => { + let filteredFiles = [...files]; + let filteredFolders = [...folders]; + + // Apply search filter + if (searchTerm) { + const term = searchTerm.toLowerCase(); + filteredFiles = filteredFiles.filter((file) => + file.name.toLowerCase().includes(term) + ); + filteredFolders = filteredFolders.filter((folder) => + folder.name.toLowerCase().includes(term) + ); + } + + // Apply type filter + if (selectedType !== "all") { + filteredFiles = filteredFiles.filter( + (file) => file.type === selectedType + ); + } + + // Apply sorting + if (activeFilter) { + filteredFiles.sort((a, b) => { + switch (activeFilter) { + case "size": + return b.size - a.size; + case "date": + return b.lastModified.getTime() - a.lastModified.getTime(); + default: + return 0; + } + }); + } + + return { + files: filteredFiles, + folders: filteredFolders, + }; + }, [files, folders, searchTerm, activeFilter, selectedType]); + + return { + ...filteredAndSortedData, + isLoading, + error, + searchTerm, + setSearchTerm, + activeFilter, + setActiveFilter, + selectedType, + setSelectedType, + refresh: fetchFiles, + }; +}; diff --git a/package-lock.json b/package-lock.json index c5a56a8..543d0e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,9 +22,11 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.7", + "@types/js-cookie": "^3.0.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "js-cookie": "^3.0.5", "lucide-react": "^0.474.0", "next": "15.1.6", "next-themes": "^0.4.4", @@ -2610,6 +2612,12 @@ "tslib": "^2.8.0" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -5885,6 +5893,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index c2e7d50..38733df 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,11 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.7", + "@types/js-cookie": "^3.0.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "js-cookie": "^3.0.5", "lucide-react": "^0.474.0", "next": "15.1.6", "next-themes": "^0.4.4", diff --git a/services/base.service.ts b/services/base.service.ts new file mode 100644 index 0000000..51edb07 --- /dev/null +++ b/services/base.service.ts @@ -0,0 +1,84 @@ +import Cookies from "js-cookie"; + +export interface RequestConfig extends RequestInit { + signal?: AbortSignal; +} + +export class BaseService { + private abortControllers: Map = new Map(); + + constructor(protected readonly baseURL: string) {} + + protected getHeaders() { + const sessionToken = Cookies.get("sessionToken"); + if (!sessionToken) { + throw new Error("No authentication token found. Please log in again."); + } + + return { + "Content-Type": "application/json", + sessionToken, + }; + } + + protected async handleResponse(response: Response): Promise { + if (response.status === 401) { + Cookies.remove("sessionToken"); + window.location.href = "/login"; + throw new Error("Session expired. Please log in again."); + } + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + if (data.status === "SUCCESS") { + return data; + } + throw new Error(data.message || "Request failed"); + } + + protected async request( + endpoint: string, + config: RequestConfig = {}, + requestId: string + ): Promise { + // Cancel any existing request with the same ID + this.cancelRequest(requestId); + + // Create new abort controller for this request + const controller = new AbortController(); + this.abortControllers.set(requestId, controller); + + try { + const response = await fetch(`${this.baseURL}${endpoint}`, { + ...config, + signal: controller.signal, + headers: { + ...this.getHeaders(), + ...config.headers, + }, + }); + + return this.handleResponse(response); + } catch (error) { + throw error; + } finally { + // Clean up abort controller + this.abortControllers.delete(requestId); + } + } + + cancelRequest(requestId: string) { + const controller = this.abortControllers.get(requestId); + if (controller) { + controller.abort(); + } + } + + cancelAllRequests() { + this.abortControllers.forEach((controller) => controller.abort()); + this.abortControllers.clear(); + } +} diff --git a/services/s3.service.ts b/services/s3.service.ts new file mode 100644 index 0000000..351f5e8 --- /dev/null +++ b/services/s3.service.ts @@ -0,0 +1,23 @@ +import { ListObjectsRequest, ListObjectsResponse } from "@/types/api"; +import { BaseService } from "./base.service"; + +const API_BASE_URL = "http://localhost:8080"; + +class S3Service extends BaseService { + constructor() { + super(API_BASE_URL); + } + + async listObjects(request: ListObjectsRequest): Promise { + return this.request( + "/s3/list-objects", + { + method: "POST", + body: JSON.stringify(request), + }, + `list-objects-${request.bucketName}-${request.objectPrefix}` + ); + } +} + +export const s3Service = new S3Service(); diff --git a/static/files.ts b/static/files.ts deleted file mode 100644 index b12b52d..0000000 --- a/static/files.ts +++ /dev/null @@ -1,49 +0,0 @@ -export const files = [ - { - name: "root", - location: "", - folders: [ - { - name: "Important Docs", - location: "Important Docs/", - folders: [ - { - name: "Academic Docs", - location: "Important Docs/Academic Docs/", - folders: [], - files: [ - { - name: "Adhaar Card.jpg", - location: "Important Docs/Academic Docs/Adhaar Card.jpg", - size: 217267, - lastModified: "2023-08-19T13:23:44Z", - extension: "jpg", - etag: "93b0647a51bffcce370ef6eba91e791c", - }, - { - name: "Class X Marksheet (Digilocker).pdf", - location: - "Important Docs/Academic Docs/Class X Marksheet (Digilocker).pdf", - size: 128659, - lastModified: "2023-08-19T13:23:46Z", - extension: "pdf", - etag: "797704ac2c66f0efb6974549c195c1f5", - }, - ], - }, - ], - files: [], - }, - ], - files: [ - { - name: "CCNA-Cert.pdf", - location: "CCNA-Cert.pdf", - size: 130155, - lastModified: "2025-02-23T02:21:17Z", - extension: "pdf", - etag: "c9c7a4f58629b9f7cfc908815a158038", - }, - ], - }, -]; diff --git a/types/S3Objects.ts b/types/S3Objects.ts index 83a8627..29116eb 100644 --- a/types/S3Objects.ts +++ b/types/S3Objects.ts @@ -1,29 +1,26 @@ -interface S3File { - name: string; - location: string; - size: string; - lastModified: string; +export interface S3File { extension: string; + size: string; etag: string; - type: "document" | "compressed" | "image" | "audio" | "video" | "unknown"; + lastModified: string; + key: string; + fileType: string; + name?: string; + type?: string; } -interface S3Folder { - name: string; - location: string; - type: "folder"; - itemCount: number; - folders: S3Folder[]; - files: S3File[]; +export interface S3Folder { + etag: string | null; + lastModified: string | null; + key: string; } -interface S3Root { - name: string; - location: string; - type: "folder"; - itemCount: number; +export interface S3Root { + filesCount: number; folders: S3Folder[]; files: S3File[]; + currentFolder: string; + etag: string | null; + lastModified: string | null; + foldersCount: number; } - -export type { S3File, S3Folder, S3Root }; diff --git a/types/api.ts b/types/api.ts new file mode 100644 index 0000000..8803bad --- /dev/null +++ b/types/api.ts @@ -0,0 +1,16 @@ +import { S3Root } from "./S3Objects"; + +export interface ApiResponse { + status: string; + message: string; + data: T; + statusCode: number; + timestamp: string; +} + +export interface ListObjectsRequest { + bucketName: string; + objectPrefix: string; +} + +export interface ListObjectsResponse extends ApiResponse {} diff --git a/types/fileTypes.ts b/types/fileTypes.ts new file mode 100644 index 0000000..a7cabab --- /dev/null +++ b/types/fileTypes.ts @@ -0,0 +1,25 @@ +export type FileType = + | "document" + | "compressed" + | "image" + | "audio" + | "video" + | "unknown" + | "folder"; + +export type FilterType = "size" | "date" | null; + +export interface FileTypeOption { + value: string; + label: string; +} + +export const FILE_TYPES: FileTypeOption[] = [ + { value: "all", label: "All Files" }, + { value: "document", label: "Documents" }, + { value: "compressed", label: "Compressed" }, + { value: "image", label: "Images" }, + { value: "audio", label: "Audio" }, + { value: "video", label: "Video" }, + { value: "folder", label: "Folders" }, +]; From 512b45d911c1333ff5dd66d8d02b60b22ebec947 Mon Sep 17 00:00:00 2001 From: vikas Date: Thu, 27 Mar 2025 03:44:58 +0530 Subject: [PATCH 2/9] Fixed --- app/(pages)/dashboard/buckets/page.tsx | 20 +++++++++++++++++--- app/(pages)/login/page.tsx | 10 +++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/(pages)/dashboard/buckets/page.tsx b/app/(pages)/dashboard/buckets/page.tsx index 97fd1ef..d37cb88 100644 --- a/app/(pages)/dashboard/buckets/page.tsx +++ b/app/(pages)/dashboard/buckets/page.tsx @@ -3,9 +3,23 @@ import { useState } from "react"; import useBuckets from "@/hooks/useBuckets"; import { useBucketFiltering } from "@/hooks/useBucketFiltering"; -import BucketToolbar from "@/components/bucket-component/bucket-toolbar"; -import BucketGrid from "@/components/bucket-component/bucket-grid"; -import BucketGridSkeleton from "@/components/bucket-component/bucket-grid-skeleton"; +import dynamic from "next/dynamic"; + +const BucketToolbar = dynamic(() => + import("@/components/bucket-component/bucket-toolbar").then( + (mod) => mod.default + ) +); + +const BucketGrid = dynamic(() => + import("@/components/bucket-component/bucket-grid").then((mod) => mod.default) +); + +const BucketGridSkeleton = dynamic(() => + import("@/components/bucket-component/bucket-grid-skeleton").then( + (mod) => mod.default + ) +); export default function Page() { const { buckets, isLoading, isError, refetch } = useBuckets(); diff --git a/app/(pages)/login/page.tsx b/app/(pages)/login/page.tsx index f30c2c2..e7edd63 100644 --- a/app/(pages)/login/page.tsx +++ b/app/(pages)/login/page.tsx @@ -2,7 +2,6 @@ import { GalleryVerticalEnd } from "lucide-react"; import dynamic from "next/dynamic"; import Image from "next/image"; import Link from "next/link"; -import { LoginInfo } from "./components/login-info"; const LoginForm = dynamic(() => import("./components/login-form"), { loading: () => ( @@ -10,6 +9,15 @@ const LoginForm = dynamic(() => import("./components/login-form"), { ), }); +const LoginInfo = dynamic( + () => import("./components/login-info").then((mod) => mod.default), + { + loading: () => ( +
+ ), + } +); + export default function LoginPage() { return (
From bfac1a6f47cbe4d58d4bfe59ee172f7a095ed91c Mon Sep 17 00:00:00 2001 From: vikas Date: Thu, 27 Mar 2025 03:48:08 +0530 Subject: [PATCH 3/9] Fixed login page --- app/(pages)/login/page.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/(pages)/login/page.tsx b/app/(pages)/login/page.tsx index e7edd63..f30c2c2 100644 --- a/app/(pages)/login/page.tsx +++ b/app/(pages)/login/page.tsx @@ -2,6 +2,7 @@ import { GalleryVerticalEnd } from "lucide-react"; import dynamic from "next/dynamic"; import Image from "next/image"; import Link from "next/link"; +import { LoginInfo } from "./components/login-info"; const LoginForm = dynamic(() => import("./components/login-form"), { loading: () => ( @@ -9,15 +10,6 @@ const LoginForm = dynamic(() => import("./components/login-form"), { ), }); -const LoginInfo = dynamic( - () => import("./components/login-info").then((mod) => mod.default), - { - loading: () => ( -
- ), - } -); - export default function LoginPage() { return (
From 3a820d7e36e6d44aac2b018cc0cf53d3d3bde728 Mon Sep 17 00:00:00 2001 From: vikas Date: Thu, 27 Mar 2025 18:45:25 +0530 Subject: [PATCH 4/9] Added folder hierarchy and media preview --- .../dashboard/buckets/[bucketName]/page.tsx | 4 + components/files-component/files.tsx | 197 +++++++----- components/files-component/media-preview.tsx | 284 ++++++++++++++++++ .../media-preview/image-viewer.tsx | 75 +++++ .../files-component/media-preview/index.tsx | 161 ++++++++++ .../media-preview/navigation.tsx | 39 +++ .../files-component/media-preview/toolbar.tsx | 74 +++++ .../files-component/media-preview/types.ts | 23 ++ components/ui/loading-spinner.tsx | 9 + services/base.service.ts | 11 + services/s3.service.ts | 55 ++++ types/S3Objects.ts | 1 + 12 files changed, 861 insertions(+), 72 deletions(-) create mode 100644 components/files-component/media-preview.tsx create mode 100644 components/files-component/media-preview/image-viewer.tsx create mode 100644 components/files-component/media-preview/index.tsx create mode 100644 components/files-component/media-preview/navigation.tsx create mode 100644 components/files-component/media-preview/toolbar.tsx create mode 100644 components/files-component/media-preview/types.ts create mode 100644 components/ui/loading-spinner.tsx diff --git a/app/(pages)/dashboard/buckets/[bucketName]/page.tsx b/app/(pages)/dashboard/buckets/[bucketName]/page.tsx index 1d4d208..4110682 100644 --- a/app/(pages)/dashboard/buckets/[bucketName]/page.tsx +++ b/app/(pages)/dashboard/buckets/[bucketName]/page.tsx @@ -36,6 +36,7 @@ interface FileData { itemCount?: number; size?: string; extension?: string; + key?: string; } export default function Page({ params }: PageProps) { @@ -141,11 +142,13 @@ export default function Page({ params }: PageProps) { name: file.key.split("/").pop() || "", size: file.size, extension: file.extension, + key: file.key, }); const mapS3FolderToFileData = (folder: S3Folder): FileData => ({ name: folder.key.replace(/\/$/, ""), itemCount: 0, + key: folder.key, }); if (error) { @@ -204,6 +207,7 @@ export default function Page({ params }: PageProps) {
))} diff --git a/components/files-component/files.tsx b/components/files-component/files.tsx index ef64dc2..51668d4 100644 --- a/components/files-component/files.tsx +++ b/components/files-component/files.tsx @@ -17,12 +17,18 @@ import { Folder, FileQuestion, } from "lucide-react"; +import dynamic from "next/dynamic"; +import { s3Service } from "@/services/s3.service"; +import Cookies from "js-cookie"; + +const MediaPreview = dynamic(() => import("./media-preview")); interface FileData { - name: string; - itemCount?: number; - size?: string; - extension?: string; + readonly name: string; + readonly itemCount?: number; + readonly size?: string; + readonly extension?: string; + readonly key?: string; } interface FileProps { @@ -37,26 +43,21 @@ interface FileProps { data: FileData; onClick?: () => void; icon?: React.ElementType; + bucketName?: string; } -export default function Files({ type, data, onClick, icon: Icon }: FileProps) { +export default function Files({ + type, + data, + onClick, + icon: Icon, + bucketName, +}: FileProps) { + const [isPreviewOpen, setIsPreviewOpen] = React.useState(false); + const [previewUrl, setPreviewUrl] = React.useState(null); + const getIconColor = () => { - switch (type) { - case "document": - return "text-gray-700 dark:text-gray-300"; - case "compressed": - return "text-gray-700 dark:text-gray-300"; - case "image": - return "text-gray-700 dark:text-gray-300"; - case "audio": - return "text-gray-700 dark:text-gray-300"; - case "video": - return "text-gray-700 dark:text-gray-300"; - case "folder": - return "text-gray-700 dark:text-gray-300"; - default: - return "text-gray-700 dark:text-gray-300"; - } + return "text-gray-700 dark:text-gray-300"; }; const getDefaultIcon = () => { @@ -78,61 +79,113 @@ export default function Files({ type, data, onClick, icon: Icon }: FileProps) { } }; + const handleClick = async () => { + if (type === "folder" && onClick) { + onClick(); + return; + } + + if ((type === "image" || type === "video") && bucketName && data.key) { + try { + const sessionToken = Cookies.get("sessionToken"); + if (!sessionToken) { + throw new Error("No session token found. Please log in again."); + } + + console.log("Fetching presigned URL for:", { + bucketName, + objectKey: data.key, + type, + hasSessionToken: !!sessionToken, + }); + + const url = await s3Service.getPresignedUrl(bucketName, data.key); + + if (!url) { + throw new Error("No URL received from presigned URL request"); + } + + setPreviewUrl(url.url); + setIsPreviewOpen(true); + } catch (error) { + console.error("Error getting presigned URL:", error); + // You might want to show a toast notification here + } + } else { + console.log("Click conditions not met:", { + type, + bucketName, + hasKey: !!data.key, + data, + }); + } + }; + const IconComponent = Icon || getDefaultIcon(); const iconColor = getIconColor(); return ( - - - - -
- -
-
-
- 13 ? "group-hover:animate-marquee" : "" - } inline-block`} - > - {data.name} - + <> + + + + +
+
- {data.size && ( -

- {data.size} -

- )} - {data.itemCount !== undefined && ( -

- {data.itemCount} items -

- )} -
- - - - - - Download - ⌘D - - - Share - ⌘S - - - - Delete - ⌘⌫ - - - +
+
+ 13 ? "group-hover:animate-marquee" : "" + } inline-block`} + > + {data.name} + +
+ {data.size && ( +

+ {data.size} +

+ )} + {data.itemCount !== undefined && ( +

+ {data.itemCount} items +

+ )} +
+ + + + + + Download + ⌘D + + + Share + ⌘S + + + + Delete + ⌘⌫ + + + + + {previewUrl && isPreviewOpen && ( + setIsPreviewOpen(false)} + title={data.name} + mediaUrl={previewUrl} + /> + )} + ); } diff --git a/components/files-component/media-preview.tsx b/components/files-component/media-preview.tsx new file mode 100644 index 0000000..f8dad4b --- /dev/null +++ b/components/files-component/media-preview.tsx @@ -0,0 +1,284 @@ +import React, { + useEffect, + useState, + MouseEvent, + WheelEvent, + useRef, +} from "react"; +import { + X, + Download, + ZoomIn, + ZoomOut, + RotateCw, + ChevronLeft, + ChevronRight, +} from "lucide-react"; + +interface MediaPreviewProps { + onClose: () => void; + title: string; + mediaUrl: string; + onNext?: () => void; + onPrev?: () => void; + hasNext?: boolean; + hasPrev?: boolean; +} + +interface Position { + x: number; + y: number; +} + +interface ToolbarProps { + title: string; + onClose: () => void; + onZoomIn: () => void; + onZoomOut: () => void; + onRotate: () => void; + onDownload: () => void; + scale: number; +} + +function Toolbar({ + title, + onClose, + onZoomIn, + onZoomOut, + onRotate, + onDownload, + scale, +}: ToolbarProps) { + return ( +
+
+ +
+

+ {title} +

+
+
+ + + +
+ +
+
+ ); +} + +export default function MediaPreview({ + onClose, + title, + mediaUrl, + onNext, + onPrev, + hasNext = false, + hasPrev = false, +}: MediaPreviewProps) { + const [state, setState] = useState({ + isLoading: true, + scale: 1, + rotation: 0, + position: { x: 0, y: 0 } as Position, + isDragging: false, + dragStart: { x: 0, y: 0 } as Position, + }); + + const containerRef = useRef(null); + const imageRef = useRef(null); + + const getBoundedPosition = (newX: number, newY: number): Position => { + if (!containerRef.current || !imageRef.current) return { x: newX, y: newY }; + + const container = containerRef.current.getBoundingClientRect(); + const image = imageRef.current.getBoundingClientRect(); + const scaledWidth = image.width * state.scale; + const scaledHeight = image.height * state.scale; + const maxX = Math.max(0, (scaledWidth - container.width) / 2); + const maxY = Math.max(0, (scaledHeight - container.height) / 2); + + return { + x: Math.min(Math.max(newX, -maxX), maxX), + y: Math.min(Math.max(newY, -maxY), maxY), + }; + }; + + const handleZoom = (delta: number) => { + setState((prev) => { + const newScale = Math.min(Math.max(prev.scale + delta, 0.5), 2); + const deltaScale = newScale - prev.scale; + const newPos = { + x: prev.position.x - prev.position.x * deltaScale, + y: prev.position.y - prev.position.y * deltaScale, + }; + const boundedPos = getBoundedPosition(newPos.x, newPos.y); + return { ...prev, scale: newScale, position: boundedPos }; + }); + }; + + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + handleZoom(e.deltaY < 0 ? 0.25 : -0.25); + }; + + const handleMouseDown = (e: MouseEvent) => { + e.preventDefault(); + setState((prev) => ({ + ...prev, + isDragging: true, + dragStart: { + x: e.clientX - prev.position.x, + y: e.clientY - prev.position.y, + }, + })); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!state.isDragging) return; + e.preventDefault(); + + requestAnimationFrame(() => { + const newX = e.clientX - state.dragStart.x; + const newY = e.clientY - state.dragStart.y; + const boundedPosition = getBoundedPosition(newX, newY); + setState((prev) => ({ ...prev, position: boundedPosition })); + }); + }; + + const handleMouseUp = () => { + setState((prev) => ({ ...prev, isDragging: false })); + }; + + useEffect(() => { + setState((prev) => ({ + ...prev, + position: { x: 0, y: 0 }, + scale: 1, + rotation: 0, + })); + }, [mediaUrl]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "ArrowRight" && hasNext && onNext) onNext(); + else if (e.key === "ArrowLeft" && hasPrev && onPrev) onPrev(); + else if ((e.key === "+" || e.key === "=") && state.scale < 2) + handleZoom(0.25); + else if (e.key === "-" && state.scale > 0.5) handleZoom(-0.25); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [onNext, onPrev, hasNext, hasPrev, state.scale]); + + return ( +
+ handleZoom(0.25)} + onZoomOut={() => handleZoom(-0.25)} + onRotate={() => + setState((prev) => ({ + ...prev, + rotation: (prev.rotation + 90) % 360, + position: { x: 0, y: 0 }, + })) + } + onDownload={() => window.open(mediaUrl, "_blank")} + scale={state.scale} + /> + +
+
+ {state.isLoading && ( +
+
+
+ )} + {title} setState((prev) => ({ ...prev, isLoading: false }))} + onMouseDown={handleMouseDown} + draggable={false} + /> +
+
+ + {hasPrev && ( + + )} + {hasNext && ( + + )} +
+ ); +} diff --git a/components/files-component/media-preview/image-viewer.tsx b/components/files-component/media-preview/image-viewer.tsx new file mode 100644 index 0000000..3090a6d --- /dev/null +++ b/components/files-component/media-preview/image-viewer.tsx @@ -0,0 +1,75 @@ +import React, { useRef, MouseEvent, WheelEvent } from "react"; + +interface Position { + x: number; + y: number; +} + +interface ImageViewerProps { + title: string; + mediaUrl: string; + isLoading: boolean; + scale: number; + rotation: number; + position: Position; + isDragging: boolean; + onLoad: () => void; + onMouseDown: (e: MouseEvent) => void; + onMouseMove: (e: MouseEvent) => void; + onMouseUp: () => void; + onWheel: (e: WheelEvent) => void; +} + +export function ImageViewer({ + title, + mediaUrl, + isLoading, + scale, + rotation, + position, + isDragging, + onLoad, + onMouseDown, + onMouseMove, + onMouseUp, + onWheel, +}: ImageViewerProps) { + const containerRef = useRef(null); + const imageRef = useRef(null); + + return ( +
+
+ {isLoading && ( +
+
+
+ )} + {title} +
+
+ ); +} diff --git a/components/files-component/media-preview/index.tsx b/components/files-component/media-preview/index.tsx new file mode 100644 index 0000000..15e24d3 --- /dev/null +++ b/components/files-component/media-preview/index.tsx @@ -0,0 +1,161 @@ +import React, { useEffect, useState, MouseEvent, WheelEvent } from "react"; +import dynamic from "next/dynamic"; +import { MediaPreviewProps, PreviewState } from "./types"; + +// Dynamically import components +const Toolbar = dynamic(() => + import("./toolbar").then((mod) => ({ default: mod.Toolbar })) +); +const Navigation = dynamic(() => + import("./navigation").then((mod) => ({ default: mod.Navigation })) +); +const ImageViewer = dynamic(() => + import("./image-viewer").then((mod) => ({ default: mod.ImageViewer })) +); + +export default function MediaPreview({ + onClose, + title, + mediaUrl, + onNext, + onPrev, + hasNext = false, + hasPrev = false, +}: MediaPreviewProps) { + const [state, setState] = useState({ + isLoading: true, + scale: 1, + rotation: 0, + position: { x: 0, y: 0 }, + isDragging: false, + dragStart: { x: 0, y: 0 }, + }); + + const getBoundedPosition = (newX: number, newY: number) => { + const container = document.querySelector(".image-container"); + const image = document.querySelector(".preview-image"); + if (!container || !image) return { x: newX, y: newY }; + + const containerRect = container.getBoundingClientRect(); + const imageRect = image.getBoundingClientRect(); + const scaledWidth = imageRect.width * state.scale; + const scaledHeight = imageRect.height * state.scale; + const maxX = Math.max(0, (scaledWidth - containerRect.width) / 2); + const maxY = Math.max(0, (scaledHeight - containerRect.height) / 2); + + return { + x: Math.min(Math.max(newX, -maxX), maxX), + y: Math.min(Math.max(newY, -maxY), maxY), + }; + }; + + const handleZoom = (delta: number) => { + setState((prev) => { + const newScale = Math.min(Math.max(prev.scale + delta, 0.5), 2); + const deltaScale = newScale - prev.scale; + const newPos = { + x: prev.position.x - prev.position.x * deltaScale, + y: prev.position.y - prev.position.y * deltaScale, + }; + const boundedPos = getBoundedPosition(newPos.x, newPos.y); + return { ...prev, scale: newScale, position: boundedPos }; + }); + }; + + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + handleZoom(e.deltaY < 0 ? 0.25 : -0.25); + }; + + const handleMouseDown = (e: MouseEvent) => { + e.preventDefault(); + setState((prev) => ({ + ...prev, + isDragging: true, + dragStart: { + x: e.clientX - prev.position.x, + y: e.clientY - prev.position.y, + }, + })); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!state.isDragging) return; + e.preventDefault(); + + requestAnimationFrame(() => { + const newX = e.clientX - state.dragStart.x; + const newY = e.clientY - state.dragStart.y; + const boundedPosition = getBoundedPosition(newX, newY); + setState((prev) => ({ ...prev, position: boundedPosition })); + }); + }; + + const handleMouseUp = () => { + setState((prev) => ({ ...prev, isDragging: false })); + }; + + useEffect(() => { + setState((prev) => ({ + ...prev, + position: { x: 0, y: 0 }, + scale: 1, + rotation: 0, + })); + }, [mediaUrl]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "ArrowRight" && hasNext && onNext) onNext(); + else if (e.key === "ArrowLeft" && hasPrev && onPrev) onPrev(); + else if ((e.key === "+" || e.key === "=") && state.scale < 2) + handleZoom(0.25); + else if (e.key === "-" && state.scale > 0.5) handleZoom(-0.25); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [onNext, onPrev, hasNext, hasPrev, state.scale]); + + return ( +
+ handleZoom(0.25)} + onZoomOut={() => handleZoom(-0.25)} + onRotate={() => + setState((prev) => ({ + ...prev, + rotation: (prev.rotation + 90) % 360, + position: { x: 0, y: 0 }, + })) + } + onDownload={() => window.open(mediaUrl, "_blank")} + scale={state.scale} + /> + + setState((prev) => ({ ...prev, isLoading: false }))} + onMouseDown={handleMouseDown} + onMouseMove={handleMouseMove} + onMouseUp={handleMouseUp} + onWheel={handleWheel} + /> + + +
+ ); +} diff --git a/components/files-component/media-preview/navigation.tsx b/components/files-component/media-preview/navigation.tsx new file mode 100644 index 0000000..5be498c --- /dev/null +++ b/components/files-component/media-preview/navigation.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +interface NavigationProps { + onNext?: () => void; + onPrev?: () => void; + hasNext?: boolean; + hasPrev?: boolean; +} + +export function Navigation({ + onNext, + onPrev, + hasNext = false, + hasPrev = false, +}: NavigationProps) { + return ( + <> + {hasPrev && ( + + )} + {hasNext && ( + + )} + + ); +} diff --git a/components/files-component/media-preview/toolbar.tsx b/components/files-component/media-preview/toolbar.tsx new file mode 100644 index 0000000..d3827cc --- /dev/null +++ b/components/files-component/media-preview/toolbar.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { X, Download, ZoomIn, ZoomOut, RotateCw } from "lucide-react"; + +interface ToolbarProps { + title: string; + onClose: () => void; + onZoomIn: () => void; + onZoomOut: () => void; + onRotate: () => void; + onDownload: () => void; + scale: number; +} + +export function Toolbar({ + title, + onClose, + onZoomIn, + onZoomOut, + onRotate, + onDownload, + scale, +}: ToolbarProps) { + return ( +
+
+ +
+

+ {title} +

+
+
+ + + +
+ +
+
+ ); +} diff --git a/components/files-component/media-preview/types.ts b/components/files-component/media-preview/types.ts new file mode 100644 index 0000000..193170b --- /dev/null +++ b/components/files-component/media-preview/types.ts @@ -0,0 +1,23 @@ +export interface Position { + x: number; + y: number; +} + +export interface MediaPreviewProps { + onClose: () => void; + title: string; + mediaUrl: string; + onNext?: () => void; + onPrev?: () => void; + hasNext?: boolean; + hasPrev?: boolean; +} + +export interface PreviewState { + isLoading: boolean; + scale: number; + rotation: number; + position: Position; + isDragging: boolean; + dragStart: Position; +} diff --git a/components/ui/loading-spinner.tsx b/components/ui/loading-spinner.tsx new file mode 100644 index 0000000..f848429 --- /dev/null +++ b/components/ui/loading-spinner.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +export default function LoadingSpinner() { + return ( +
+
+
+ ); +} diff --git a/services/base.service.ts b/services/base.service.ts index 51edb07..664de08 100644 --- a/services/base.service.ts +++ b/services/base.service.ts @@ -33,9 +33,19 @@ export class BaseService { } const data = await response.json(); + console.log("Raw response data:", data); + + // Handle different response formats if (data.status === "SUCCESS") { return data; + } else if (data.url) { + // Handle presigned URL response format + return data; + } else if (data.data) { + // Handle response with data wrapper + return data.data; } + throw new Error(data.message || "Request failed"); } @@ -63,6 +73,7 @@ export class BaseService { return this.handleResponse(response); } catch (error) { + console.error("Request error:", error); throw error; } finally { // Clean up abort controller diff --git a/services/s3.service.ts b/services/s3.service.ts index 351f5e8..e0a9fa5 100644 --- a/services/s3.service.ts +++ b/services/s3.service.ts @@ -1,8 +1,17 @@ import { ListObjectsRequest, ListObjectsResponse } from "@/types/api"; import { BaseService } from "./base.service"; +import Cookies from "js-cookie"; const API_BASE_URL = "http://localhost:8080"; +interface PresignedUrlResponse { + status: string; + message: string; + data: string; + statusCode: number; + timestamp: number[]; +} + class S3Service extends BaseService { constructor() { super(API_BASE_URL); @@ -18,6 +27,52 @@ class S3Service extends BaseService { `list-objects-${request.bucketName}-${request.objectPrefix}` ); } + + async getPresignedUrl( + bucketName: string, + objectKey: string + ): Promise { + const sessionToken = Cookies.get("sessionToken"); + if (!sessionToken) { + throw new Error("No session token found. Please log in again."); + } + + console.log("Making presigned URL request with:", { + bucketName, + objectKey, + sessionToken: sessionToken.substring(0, 10) + "...", // Log partial token for debugging + }); + + try { + const response = await this.request( + "/s3/presigned-url", + { + method: "POST", + headers: { + "Content-Type": "application/json", + sessionToken: sessionToken, + }, + body: JSON.stringify({ + bucketName, + objectKey, + }), + }, + `presigned-url-${bucketName}-${objectKey}` + ); + + console.log("Raw response from presigned URL request:", response); + + if (!response || !response.data) { + console.error("Invalid response format:", response); + throw new Error("Invalid response format from server"); + } + + return response.data; + } catch (error) { + console.error("Error in getPresignedUrl:", error); + throw error; + } + } } export const s3Service = new S3Service(); diff --git a/types/S3Objects.ts b/types/S3Objects.ts index 29116eb..5b48437 100644 --- a/types/S3Objects.ts +++ b/types/S3Objects.ts @@ -13,6 +13,7 @@ export interface S3Folder { etag: string | null; lastModified: string | null; key: string; + itemCount?: number; } export interface S3Root { From ae9a43ab0233406141d67e19304f3e9521a94f5f Mon Sep 17 00:00:00 2001 From: vikas Date: Fri, 28 Mar 2025 01:24:47 +0530 Subject: [PATCH 5/9] Signed URL Added -T1 --- .../dashboard/buckets/[bucketName]/page.tsx | 10 +- components/files-component/files.tsx | 14 ++- components/files-component/media-preview.tsx | 115 +++++++++++++++--- components/files.tsx | 98 +++++++++++++++ components/media-preview.tsx | 95 +++++++++++++++ services/s3.service.ts | 6 +- types/api.ts | 8 +- 7 files changed, 316 insertions(+), 30 deletions(-) create mode 100644 components/files.tsx create mode 100644 components/media-preview.tsx diff --git a/app/(pages)/dashboard/buckets/[bucketName]/page.tsx b/app/(pages)/dashboard/buckets/[bucketName]/page.tsx index 4110682..4fd531c 100644 --- a/app/(pages)/dashboard/buckets/[bucketName]/page.tsx +++ b/app/(pages)/dashboard/buckets/[bucketName]/page.tsx @@ -32,11 +32,11 @@ interface PageProps { } interface FileData { - name: string; - itemCount?: number; - size?: string; - extension?: string; - key?: string; + readonly name: string; + readonly itemCount?: number; + readonly size?: string; + readonly extension?: string; + readonly key?: string; } export default function Page({ params }: PageProps) { diff --git a/components/files-component/files.tsx b/components/files-component/files.tsx index 51668d4..b777520 100644 --- a/components/files-component/files.tsx +++ b/components/files-component/files.tsx @@ -55,6 +55,7 @@ export default function Files({ }: FileProps) { const [isPreviewOpen, setIsPreviewOpen] = React.useState(false); const [previewUrl, setPreviewUrl] = React.useState(null); + const [error, setError] = React.useState(null); const getIconColor = () => { return "text-gray-700 dark:text-gray-300"; @@ -105,11 +106,14 @@ export default function Files({ throw new Error("No URL received from presigned URL request"); } - setPreviewUrl(url.url); + setPreviewUrl(url); setIsPreviewOpen(true); + setError(null); } catch (error) { console.error("Error getting presigned URL:", error); - // You might want to show a toast notification here + setError( + error instanceof Error ? error.message : "Failed to load preview" + ); } } else { console.log("Click conditions not met:", { @@ -184,8 +188,14 @@ export default function Files({ onClose={() => setIsPreviewOpen(false)} title={data.name} mediaUrl={previewUrl} + type={type} /> )} + {error && ( +
+ {error} +
+ )} ); } diff --git a/components/files-component/media-preview.tsx b/components/files-component/media-preview.tsx index f8dad4b..c36117e 100644 --- a/components/files-component/media-preview.tsx +++ b/components/files-component/media-preview.tsx @@ -5,6 +5,7 @@ import React, { WheelEvent, useRef, } from "react"; +import Image from "next/image"; import { X, Download, @@ -19,6 +20,14 @@ interface MediaPreviewProps { onClose: () => void; title: string; mediaUrl: string; + type: + | "document" + | "compressed" + | "image" + | "audio" + | "video" + | "folder" + | "unknown"; onNext?: () => void; onPrev?: () => void; hasNext?: boolean; @@ -106,6 +115,7 @@ export default function MediaPreview({ onClose, title, mediaUrl, + type, onNext, onPrev, hasNext = false, @@ -118,6 +128,7 @@ export default function MediaPreview({ position: { x: 0, y: 0 } as Position, isDragging: false, dragStart: { x: 0, y: 0 } as Position, + error: null as string | null, }); const containerRef = useRef(null); @@ -191,6 +202,8 @@ export default function MediaPreview({ position: { x: 0, y: 0 }, scale: 1, rotation: 0, + isLoading: true, + error: null, })); }, [mediaUrl]); @@ -207,6 +220,76 @@ export default function MediaPreview({ return () => window.removeEventListener("keydown", handleKeyDown); }, [onNext, onPrev, hasNext, hasPrev, state.scale]); + const handleError = (error: string) => { + setState((prev) => ({ ...prev, isLoading: false, error })); + }; + + const renderMedia = () => { + switch (type) { + case "image": + return ( + {title} setState((prev) => ({ ...prev, isLoading: false }))} + onMouseDown={handleMouseDown} + onMouseMove={handleMouseMove} + onMouseUp={handleMouseUp} + onMouseLeave={handleMouseUp} + onWheel={handleWheel} + onError={() => handleError("Failed to load image")} + unoptimized + /> + ); + case "video": + return ( + + ); + case "audio": + return ( + + ); + default: + return ( +
+

Preview not available for this file type

+
+ ); + } + }; + return (
)} - {title} setState((prev) => ({ ...prev, isLoading: false }))} - onMouseDown={handleMouseDown} - draggable={false} - /> + {state.error ? ( +
+

{state.error}

+ +
+ ) : ( + renderMedia() + )}
diff --git a/components/files.tsx b/components/files.tsx new file mode 100644 index 0000000..10f0c22 --- /dev/null +++ b/components/files.tsx @@ -0,0 +1,98 @@ +import React, { useState, useEffect } from "react"; +import { s3Service } from "@/services/s3.service"; +import { S3Object } from "@/types/api"; +import { MediaPreview } from "./media-preview"; + +interface FilesProps { + bucketName: string; + prefix?: string; +} + +export const Files: React.FC = ({ bucketName, prefix = "" }) => { + const [objects, setObjects] = useState([]); + const [selectedObject, setSelectedObject] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchObjects = async () => { + try { + setIsLoading(true); + setError(null); + const response = await s3Service.listObjects({ + bucketName, + objectPrefix: prefix, + }); + setObjects(response.objects); + } catch (err) { + console.error("Error fetching objects:", err); + setError(err instanceof Error ? err.message : "Failed to load files"); + } finally { + setIsLoading(false); + } + }; + + fetchObjects(); + }, [bucketName, prefix]); + + const handleFileClick = (object: S3Object) => { + setSelectedObject(object); + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+

Error: {error}

+
+ ); + } + + return ( +
+
+

Files

+
+ {objects.map((object) => ( +
handleFileClick(object)} + > +
+ + {object.key.endsWith("/") ? "📁" : "📄"} + + {object.key} +
+
+ Size: {Math.round(object.size / 1024)} KB +
+
+ ))} +
+
+
+ {selectedObject ? ( +
+

{selectedObject.key}

+
+ +
+
+ ) : ( +
Select a file to preview
+ )} +
+
+ ); +}; diff --git a/components/media-preview.tsx b/components/media-preview.tsx new file mode 100644 index 0000000..8822258 --- /dev/null +++ b/components/media-preview.tsx @@ -0,0 +1,95 @@ +import React, { useState, useEffect } from "react"; +import { s3Service } from "@/services/s3.service"; +import { S3Object } from "@/types/api"; +import Image from "next/image"; + +interface MediaPreviewProps { + object: S3Object; + bucketName: string; +} + +export const MediaPreview: React.FC = ({ + object, + bucketName, +}) => { + const [previewUrl, setPreviewUrl] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchPreviewUrl = async () => { + try { + setIsLoading(true); + setError(null); + const url = await s3Service.getPresignedUrl(bucketName, object.key); + setPreviewUrl(url); + console.log("Preview URL:", url); + } catch (err) { + console.error("Error fetching preview URL:", err); + setError(err instanceof Error ? err.message : "Failed to load preview"); + } finally { + setIsLoading(false); + } + }; + + fetchPreviewUrl(); + }, [object.key, bucketName]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+

Error: {error}

+
+ ); + } + + if (!previewUrl) { + return ( +
+

No preview available

+
+ ); + } + + const fileType = object.key.split(".").pop()?.toLowerCase(); + const isImage = ["jpg", "jpeg", "png", "gif", "webp"].includes( + fileType || "" + ); + const isVideo = ["mp4", "webm", "ogg"].includes(fileType || ""); + + return ( +
+ {isImage && ( + {object.key} setError("Failed to load image")} + /> + )} + {isVideo && ( + + )} + {!isImage && !isVideo && ( +
+

Preview not available for this file type

+
+ )} +
+ ); +}; diff --git a/services/s3.service.ts b/services/s3.service.ts index e0a9fa5..2915973 100644 --- a/services/s3.service.ts +++ b/services/s3.service.ts @@ -5,11 +5,9 @@ import Cookies from "js-cookie"; const API_BASE_URL = "http://localhost:8080"; interface PresignedUrlResponse { - status: string; + success: boolean; message: string; data: string; - statusCode: number; - timestamp: number[]; } class S3Service extends BaseService { @@ -54,7 +52,7 @@ class S3Service extends BaseService { }, body: JSON.stringify({ bucketName, - objectKey, + objectPrefix: objectKey, }), }, `presigned-url-${bucketName}-${objectKey}` diff --git a/types/api.ts b/types/api.ts index 8803bad..73c9d0e 100644 --- a/types/api.ts +++ b/types/api.ts @@ -13,4 +13,10 @@ export interface ListObjectsRequest { objectPrefix: string; } -export interface ListObjectsResponse extends ApiResponse {} +export interface ListObjectsResponse { + status: string; + message: string; + data: S3Root; + statusCode: number; + timestamp: string; +} From 031e6d27069440f8af4900e8ff4b0cbbaea27d12 Mon Sep 17 00:00:00 2001 From: vikas Date: Fri, 28 Mar 2025 01:44:31 +0530 Subject: [PATCH 6/9] Added Image Preview --- components/files-component/files.tsx | 10 +- components/files-component/media-preview.tsx | 100 ++++++++++-------- .../media-preview/image-viewer.tsx | 35 +++--- .../files-component/media-preview/index.tsx | 37 ++++--- .../media-preview/navigation.tsx | 8 +- .../files-component/media-preview/toolbar.tsx | 14 +-- .../files-component/media-preview/types.ts | 14 +-- components/files.tsx | 100 +++++++++++++----- components/media-preview.tsx | 95 ----------------- services/s3.service.ts | 2 +- 10 files changed, 196 insertions(+), 219 deletions(-) delete mode 100644 components/media-preview.tsx diff --git a/components/files-component/files.tsx b/components/files-component/files.tsx index b777520..01009e9 100644 --- a/components/files-component/files.tsx +++ b/components/files-component/files.tsx @@ -32,7 +32,7 @@ interface FileData { } interface FileProps { - type: + readonly type: | "document" | "compressed" | "image" @@ -40,10 +40,10 @@ interface FileProps { | "video" | "folder" | "unknown"; - data: FileData; - onClick?: () => void; - icon?: React.ElementType; - bucketName?: string; + readonly data: FileData; + readonly onClick?: () => void; + readonly icon?: React.ElementType; + readonly bucketName?: string; } export default function Files({ diff --git a/components/files-component/media-preview.tsx b/components/files-component/media-preview.tsx index c36117e..2f20ee9 100644 --- a/components/files-component/media-preview.tsx +++ b/components/files-component/media-preview.tsx @@ -4,6 +4,7 @@ import React, { MouseEvent, WheelEvent, useRef, + useCallback, } from "react"; import Image from "next/image"; import { @@ -17,10 +18,10 @@ import { } from "lucide-react"; interface MediaPreviewProps { - onClose: () => void; - title: string; - mediaUrl: string; - type: + readonly onClose: () => void; + readonly title: string; + readonly mediaUrl: string; + readonly type: | "document" | "compressed" | "image" @@ -28,10 +29,10 @@ interface MediaPreviewProps { | "video" | "folder" | "unknown"; - onNext?: () => void; - onPrev?: () => void; - hasNext?: boolean; - hasPrev?: boolean; + readonly onNext?: () => void; + readonly onPrev?: () => void; + readonly hasNext?: boolean; + readonly hasPrev?: boolean; } interface Position { @@ -40,13 +41,13 @@ interface Position { } interface ToolbarProps { - title: string; - onClose: () => void; - onZoomIn: () => void; - onZoomOut: () => void; - onRotate: () => void; - onDownload: () => void; - scale: number; + readonly title: string; + readonly onClose: () => void; + readonly onZoomIn: () => void; + readonly onZoomOut: () => void; + readonly onRotate: () => void; + readonly onDownload: () => void; + readonly scale: number; } function Toolbar({ @@ -131,37 +132,44 @@ export default function MediaPreview({ error: null as string | null, }); - const containerRef = useRef(null); + const containerRef = useRef(null); const imageRef = useRef(null); - const getBoundedPosition = (newX: number, newY: number): Position => { - if (!containerRef.current || !imageRef.current) return { x: newX, y: newY }; + const getBoundedPosition = useCallback( + (newX: number, newY: number): Position => { + if (!containerRef.current || !imageRef.current) + return { x: newX, y: newY }; - const container = containerRef.current.getBoundingClientRect(); - const image = imageRef.current.getBoundingClientRect(); - const scaledWidth = image.width * state.scale; - const scaledHeight = image.height * state.scale; - const maxX = Math.max(0, (scaledWidth - container.width) / 2); - const maxY = Math.max(0, (scaledHeight - container.height) / 2); + const container = containerRef.current.getBoundingClientRect(); + const image = imageRef.current.getBoundingClientRect(); + const scaledWidth = image.width * state.scale; + const scaledHeight = image.height * state.scale; + const maxX = Math.max(0, (scaledWidth - container.width) / 2); + const maxY = Math.max(0, (scaledHeight - container.height) / 2); - return { - x: Math.min(Math.max(newX, -maxX), maxX), - y: Math.min(Math.max(newY, -maxY), maxY), - }; - }; - - const handleZoom = (delta: number) => { - setState((prev) => { - const newScale = Math.min(Math.max(prev.scale + delta, 0.5), 2); - const deltaScale = newScale - prev.scale; - const newPos = { - x: prev.position.x - prev.position.x * deltaScale, - y: prev.position.y - prev.position.y * deltaScale, + return { + x: Math.min(Math.max(newX, -maxX), maxX), + y: Math.min(Math.max(newY, -maxY), maxY), }; - const boundedPos = getBoundedPosition(newPos.x, newPos.y); - return { ...prev, scale: newScale, position: boundedPos }; - }); - }; + }, + [state.scale] + ); + + const handleZoom = useCallback( + (delta: number) => { + setState((prev) => { + const newScale = Math.min(Math.max(prev.scale + delta, 0.5), 2); + const deltaScale = newScale - prev.scale; + const newPos = { + x: prev.position.x - prev.position.x * deltaScale, + y: prev.position.y - prev.position.y * deltaScale, + }; + const boundedPos = getBoundedPosition(newPos.x, newPos.y); + return { ...prev, scale: newScale, position: boundedPos }; + }); + }, + [getBoundedPosition] + ); const handleWheel = (e: WheelEvent) => { e.preventDefault(); @@ -218,7 +226,7 @@ export default function MediaPreview({ window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [onNext, onPrev, hasNext, hasPrev, state.scale]); + }, [onNext, onPrev, hasNext, hasPrev, state.scale, handleZoom]); const handleError = (error: string) => { setState((prev) => ({ ...prev, isLoading: false, error })); @@ -264,6 +272,7 @@ export default function MediaPreview({ onError={() => handleError("Failed to load video")} > + Your browser does not support the video tag. ); @@ -278,6 +287,7 @@ export default function MediaPreview({ onError={() => handleError("Failed to load audio")} > + Your browser does not support the audio tag. ); @@ -308,9 +318,9 @@ export default function MediaPreview({ scale={state.scale} /> -
-
+ {hasPrev && (
-
+ ); } diff --git a/components/files-component/media-preview/index.tsx b/components/files-component/media-preview/index.tsx index 15e24d3..08f488e 100644 --- a/components/files-component/media-preview/index.tsx +++ b/components/files-component/media-preview/index.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useState, MouseEvent, WheelEvent } from "react"; +import React, { + useEffect, + useState, + MouseEvent, + WheelEvent, + useCallback, +} from "react"; import dynamic from "next/dynamic"; import { MediaPreviewProps, PreviewState } from "./types"; @@ -49,18 +55,21 @@ export default function MediaPreview({ }; }; - const handleZoom = (delta: number) => { - setState((prev) => { - const newScale = Math.min(Math.max(prev.scale + delta, 0.5), 2); - const deltaScale = newScale - prev.scale; - const newPos = { - x: prev.position.x - prev.position.x * deltaScale, - y: prev.position.y - prev.position.y * deltaScale, - }; - const boundedPos = getBoundedPosition(newPos.x, newPos.y); - return { ...prev, scale: newScale, position: boundedPos }; - }); - }; + const handleZoom = useCallback( + (delta: number) => { + setState((prev) => { + const newScale = Math.min(Math.max(prev.scale + delta, 0.5), 2); + const deltaScale = newScale - prev.scale; + const newPos = { + x: prev.position.x - prev.position.x * deltaScale, + y: prev.position.y - prev.position.y * deltaScale, + }; + const boundedPos = getBoundedPosition(newPos.x, newPos.y); + return { ...prev, scale: newScale, position: boundedPos }; + }); + }, + [getBoundedPosition] + ); const handleWheel = (e: WheelEvent) => { e.preventDefault(); @@ -115,7 +124,7 @@ export default function MediaPreview({ window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [onNext, onPrev, hasNext, hasPrev, state.scale]); + }, [onNext, onPrev, hasNext, hasPrev, state.scale, handleZoom]); return (
diff --git a/components/files-component/media-preview/navigation.tsx b/components/files-component/media-preview/navigation.tsx index 5be498c..c76fad1 100644 --- a/components/files-component/media-preview/navigation.tsx +++ b/components/files-component/media-preview/navigation.tsx @@ -2,10 +2,10 @@ import React from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; interface NavigationProps { - onNext?: () => void; - onPrev?: () => void; - hasNext?: boolean; - hasPrev?: boolean; + readonly onNext?: () => void; + readonly onPrev?: () => void; + readonly hasNext?: boolean; + readonly hasPrev?: boolean; } export function Navigation({ diff --git a/components/files-component/media-preview/toolbar.tsx b/components/files-component/media-preview/toolbar.tsx index d3827cc..1e48974 100644 --- a/components/files-component/media-preview/toolbar.tsx +++ b/components/files-component/media-preview/toolbar.tsx @@ -2,13 +2,13 @@ import React from "react"; import { X, Download, ZoomIn, ZoomOut, RotateCw } from "lucide-react"; interface ToolbarProps { - title: string; - onClose: () => void; - onZoomIn: () => void; - onZoomOut: () => void; - onRotate: () => void; - onDownload: () => void; - scale: number; + readonly title: string; + readonly onClose: () => void; + readonly onZoomIn: () => void; + readonly onZoomOut: () => void; + readonly onRotate: () => void; + readonly onDownload: () => void; + readonly scale: number; } export function Toolbar({ diff --git a/components/files-component/media-preview/types.ts b/components/files-component/media-preview/types.ts index 193170b..c7ae1f6 100644 --- a/components/files-component/media-preview/types.ts +++ b/components/files-component/media-preview/types.ts @@ -4,13 +4,13 @@ export interface Position { } export interface MediaPreviewProps { - onClose: () => void; - title: string; - mediaUrl: string; - onNext?: () => void; - onPrev?: () => void; - hasNext?: boolean; - hasPrev?: boolean; + readonly onClose: () => void; + readonly title: string; + readonly mediaUrl: string; + readonly onNext?: () => void; + readonly onPrev?: () => void; + readonly hasNext?: boolean; + readonly hasPrev?: boolean; } export interface PreviewState { diff --git a/components/files.tsx b/components/files.tsx index 10f0c22..808a4ed 100644 --- a/components/files.tsx +++ b/components/files.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { s3Service } from "@/services/s3.service"; -import { S3Object } from "@/types/api"; -import { MediaPreview } from "./media-preview"; +import { S3File, S3Folder } from "@/types/S3Objects"; +import MediaPreview from "./files-component/media-preview"; interface FilesProps { bucketName: string; @@ -9,10 +9,12 @@ interface FilesProps { } export const Files: React.FC = ({ bucketName, prefix = "" }) => { - const [objects, setObjects] = useState([]); - const [selectedObject, setSelectedObject] = useState(null); + const [files, setFiles] = useState([]); + const [folders, setFolders] = useState([]); + const [selectedFile, setSelectedFile] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); useEffect(() => { const fetchObjects = async () => { @@ -23,7 +25,8 @@ export const Files: React.FC = ({ bucketName, prefix = "" }) => { bucketName, objectPrefix: prefix, }); - setObjects(response.objects); + setFiles(response.data.files); + setFolders(response.data.folders); } catch (err) { console.error("Error fetching objects:", err); setError(err instanceof Error ? err.message : "Failed to load files"); @@ -35,8 +38,36 @@ export const Files: React.FC = ({ bucketName, prefix = "" }) => { fetchObjects(); }, [bucketName, prefix]); - const handleFileClick = (object: S3Object) => { - setSelectedObject(object); + const handleFileClick = async (file: S3File) => { + try { + setSelectedFile(file); + const url = await s3Service.getPresignedUrl(bucketName, file.key); + setPreviewUrl(url); + } catch (err) { + console.error("Error getting presigned URL:", err); + setError(err instanceof Error ? err.message : "Failed to load preview"); + } + }; + + const getFileType = ( + key: string + ): + | "document" + | "compressed" + | "image" + | "audio" + | "video" + | "folder" + | "unknown" => { + if (key.endsWith("/")) return "folder"; + const ext = key.split(".").pop()?.toLowerCase(); + if (["jpg", "jpeg", "png", "gif", "webp"].includes(ext ?? "")) + return "image"; + if (["mp4", "webm", "ogg"].includes(ext ?? "")) return "video"; + if (["mp3", "wav", "ogg"].includes(ext ?? "")) return "audio"; + if (["zip", "rar", "7z"].includes(ext ?? "")) return "compressed"; + if (["pdf", "doc", "docx", "txt"].includes(ext ?? "")) return "document"; + return "unknown"; }; if (isLoading) { @@ -60,33 +91,54 @@ export const Files: React.FC = ({ bucketName, prefix = "" }) => {

Files

- {objects.map((object) => ( -
handleFileClick(object)} + {folders.map((folder) => ( + + ))} + {files.map((file) => ( +
+
Size: {file.size}
+ ))}
- {selectedObject ? ( + {selectedFile && previewUrl ? (
-

{selectedObject.key}

+

{selectedFile.key}

- + setPreviewUrl(null)} + title={selectedFile.key} + mediaUrl={previewUrl} + type={getFileType(selectedFile.key)} + />
) : ( diff --git a/components/media-preview.tsx b/components/media-preview.tsx deleted file mode 100644 index 8822258..0000000 --- a/components/media-preview.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { s3Service } from "@/services/s3.service"; -import { S3Object } from "@/types/api"; -import Image from "next/image"; - -interface MediaPreviewProps { - object: S3Object; - bucketName: string; -} - -export const MediaPreview: React.FC = ({ - object, - bucketName, -}) => { - const [previewUrl, setPreviewUrl] = useState(null); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const fetchPreviewUrl = async () => { - try { - setIsLoading(true); - setError(null); - const url = await s3Service.getPresignedUrl(bucketName, object.key); - setPreviewUrl(url); - console.log("Preview URL:", url); - } catch (err) { - console.error("Error fetching preview URL:", err); - setError(err instanceof Error ? err.message : "Failed to load preview"); - } finally { - setIsLoading(false); - } - }; - - fetchPreviewUrl(); - }, [object.key, bucketName]); - - if (isLoading) { - return ( -
-
-
- ); - } - - if (error) { - return ( -
-

Error: {error}

-
- ); - } - - if (!previewUrl) { - return ( -
-

No preview available

-
- ); - } - - const fileType = object.key.split(".").pop()?.toLowerCase(); - const isImage = ["jpg", "jpeg", "png", "gif", "webp"].includes( - fileType || "" - ); - const isVideo = ["mp4", "webm", "ogg"].includes(fileType || ""); - - return ( -
- {isImage && ( - {object.key} setError("Failed to load image")} - /> - )} - {isVideo && ( - - )} - {!isImage && !isVideo && ( -
-

Preview not available for this file type

-
- )} -
- ); -}; diff --git a/services/s3.service.ts b/services/s3.service.ts index 2915973..eceba00 100644 --- a/services/s3.service.ts +++ b/services/s3.service.ts @@ -60,7 +60,7 @@ class S3Service extends BaseService { console.log("Raw response from presigned URL request:", response); - if (!response || !response.data) { + if (!response?.data) { console.error("Invalid response format:", response); throw new Error("Invalid response format from server"); } From b165eba7912eacb9c0b7ffe424dc876b6de66dae Mon Sep 17 00:00:00 2001 From: vikas Date: Fri, 28 Mar 2025 01:44:45 +0530 Subject: [PATCH 7/9] Fixed erros --- components/bucket-component/loading-skeleton.tsx | 2 +- components/files-component/media-preview/index.tsx | 2 +- .../files-component/media-preview/types.ts => types/media.ts | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename components/files-component/media-preview/types.ts => types/media.ts (100%) diff --git a/components/bucket-component/loading-skeleton.tsx b/components/bucket-component/loading-skeleton.tsx index 140b00c..55c737f 100644 --- a/components/bucket-component/loading-skeleton.tsx +++ b/components/bucket-component/loading-skeleton.tsx @@ -3,7 +3,7 @@ import React from "react"; export default function LoadingSkeleton() { return (
- {[...Array(8)].map((_, index) => ( + {[...Array(4)].map((_, index) => (
diff --git a/components/files-component/media-preview/index.tsx b/components/files-component/media-preview/index.tsx index 08f488e..3bd63cb 100644 --- a/components/files-component/media-preview/index.tsx +++ b/components/files-component/media-preview/index.tsx @@ -6,7 +6,7 @@ import React, { useCallback, } from "react"; import dynamic from "next/dynamic"; -import { MediaPreviewProps, PreviewState } from "./types"; +import { MediaPreviewProps, PreviewState } from "../../../types/media"; // Dynamically import components const Toolbar = dynamic(() => diff --git a/components/files-component/media-preview/types.ts b/types/media.ts similarity index 100% rename from components/files-component/media-preview/types.ts rename to types/media.ts From e44a6343e2a88cefa4b57497bfbcaffe6555de64 Mon Sep 17 00:00:00 2001 From: vikas Date: Fri, 28 Mar 2025 03:05:06 +0530 Subject: [PATCH 8/9] Optimized application --- components/app-sidebar.tsx | 2 - components/health-check.tsx | 111 ++++++++++++++++++------------------ 2 files changed, 57 insertions(+), 56 deletions(-) diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 900039b..b18f26e 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -14,7 +14,6 @@ import { SidebarRail, } from "@/components/ui/sidebar"; import { Bucket } from "@/types/bucket"; -import HealthCheck from "./health-check"; import useBuckets from "@/hooks/useBuckets"; import { NavSupport } from "./nav-support"; @@ -71,7 +70,6 @@ export function AppSidebar(props: React.ComponentProps) { - diff --git a/components/health-check.tsx b/components/health-check.tsx index 6386579..e8ef648 100644 --- a/components/health-check.tsx +++ b/components/health-check.tsx @@ -2,68 +2,56 @@ import { cn } from "@/lib/utils"; import { healthCheck } from "@/services/bucketService"; import React, { useEffect, useState } from "react"; +import { + SidebarMenu, + SidebarMenuItem, + SidebarMenuButton, +} from "@/components/ui/sidebar"; -interface HealthCheckProps { - className?: string; - isCollapsed?: boolean; -} - -export default function HealthCheck({ - className = "", - isCollapsed = false, -}: HealthCheckProps) { +export default function HealthCheck() { const [status, setStatus] = useState("loading"); - const renderStatus = () => { + const renderStatusIcon = () => { if (status === "loading") { return ( - - - - - - {!isCollapsed && Connecting...} - + + + + ); } if (status === "SUCCESS") { return ( - - - - - {!isCollapsed && Connected to server} - - ); - } - - return ( - - {!isCollapsed && Connection failed} - + ); + } + + return ( + + + ); }; @@ -86,13 +74,28 @@ export default function HealthCheck({ }, []); return ( -
- {renderStatus()} -
+ + + +
+ {renderStatusIcon()} +
+ + {status === "loading" && "Connecting..."} + {status === "SUCCESS" && "Connected to server"} + {status === "FAILED" && "Connection failed"} + +
+
+
); } From 696ff2ef3a82d1c4f4377422a781fe34d0bcf1f5 Mon Sep 17 00:00:00 2001 From: vikas Date: Fri, 28 Mar 2025 23:05:23 +0530 Subject: [PATCH 9/9] Fixed bug --- .../dashboard/buckets/[bucketName]/page.tsx | 8 +---- services/base.service.ts | 29 +------------------ 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/app/(pages)/dashboard/buckets/[bucketName]/page.tsx b/app/(pages)/dashboard/buckets/[bucketName]/page.tsx index 4fd531c..37ed119 100644 --- a/app/(pages)/dashboard/buckets/[bucketName]/page.tsx +++ b/app/(pages)/dashboard/buckets/[bucketName]/page.tsx @@ -63,7 +63,7 @@ export default function Page({ params }: PageProps) { setFileData(response.data); } catch (err) { - if (err instanceof Error && err.name !== "AbortError") { + if (err instanceof Error) { console.error("Error fetching files:", err); setError(err.message || "Failed to fetch files"); } @@ -76,12 +76,6 @@ export default function Page({ params }: PageProps) { fetchFiles(); }, [fetchFiles]); - React.useEffect(() => { - return () => { - s3Service.cancelAllRequests(); - }; - }, []); - const handleFolderClick = (folder: S3Folder) => { setCurrentPath(folder.key); }; diff --git a/services/base.service.ts b/services/base.service.ts index 664de08..302e8e1 100644 --- a/services/base.service.ts +++ b/services/base.service.ts @@ -1,12 +1,8 @@ import Cookies from "js-cookie"; -export interface RequestConfig extends RequestInit { - signal?: AbortSignal; -} +export interface RequestConfig extends RequestInit {} export class BaseService { - private abortControllers: Map = new Map(); - constructor(protected readonly baseURL: string) {} protected getHeaders() { @@ -54,17 +50,9 @@ export class BaseService { config: RequestConfig = {}, requestId: string ): Promise { - // Cancel any existing request with the same ID - this.cancelRequest(requestId); - - // Create new abort controller for this request - const controller = new AbortController(); - this.abortControllers.set(requestId, controller); - try { const response = await fetch(`${this.baseURL}${endpoint}`, { ...config, - signal: controller.signal, headers: { ...this.getHeaders(), ...config.headers, @@ -75,21 +63,6 @@ export class BaseService { } catch (error) { console.error("Request error:", error); throw error; - } finally { - // Clean up abort controller - this.abortControllers.delete(requestId); } } - - cancelRequest(requestId: string) { - const controller = this.abortControllers.get(requestId); - if (controller) { - controller.abort(); - } - } - - cancelAllRequests() { - this.abortControllers.forEach((controller) => controller.abort()); - this.abortControllers.clear(); - } }