diff --git a/app/front/package-lock.json b/app/front/package-lock.json index 0d4f0dd..6392333 100644 --- a/app/front/package-lock.json +++ b/app/front/package-lock.json @@ -15,7 +15,7 @@ "@radix-ui/react-slot": "^1.1.1", "@tailwindcss/postcss": "^4.0.3", "@tailwindcss/vite": "^4.0.3", - "@tanstack/react-router": "^1.99.6", + "@tanstack/react-router": "^1.103.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.474.0", @@ -2083,9 +2083,9 @@ } }, "node_modules/@tanstack/history": { - "version": "1.99.0", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.99.0.tgz", - "integrity": "sha512-MQS1Lg8D+1vpasEJKf4zs1sxhxbXcoejmVCZDbo0bq2wq+xVK+kRixj5Pae2kb2APzdXYga4u236GBbgCKTcnQ==", + "version": "1.99.13", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.99.13.tgz", + "integrity": "sha512-JMd7USmnp8zV8BRGIjALqzPxazvKtQ7PGXQC7n39HpbqdsmfV2ePCzieO84IvN+mwsTrXErpbjI4BfKCa+ZNCg==", "license": "MIT", "engines": { "node": ">=12" @@ -2096,14 +2096,14 @@ } }, "node_modules/@tanstack/react-router": { - "version": "1.99.6", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.99.6.tgz", - "integrity": "sha512-mH4wkW2h4uILKsCB4qxJWZ8LtVlxvDzZXnXmb5FuAigdTN2qsQhgpxM1bxecII1cDQMjqq8bZvqYw/62MZ5elg==", + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.104.1.tgz", + "integrity": "sha512-c/l6TNRuUP6FTmHKyDwTGdxU0bCQhZM7QeKq/Utkyzj2i8awjWLILhBTsCXhLGY1JZtDl4dXixNgXGBiNo8Cnw==", "license": "MIT", "dependencies": { - "@tanstack/history": "1.99.0", + "@tanstack/history": "1.99.13", "@tanstack/react-store": "^0.7.0", - "@tanstack/router-core": "^1.99.6", + "@tanstack/router-core": "^1.104.1", "jsesc": "^3.1.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" @@ -2139,12 +2139,12 @@ } }, "node_modules/@tanstack/router-core": { - "version": "1.99.6", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.99.6.tgz", - "integrity": "sha512-tEfMLeONfyoyI1e/ygUeGFtTWeWQu0GR3OT8OR75EOeNXRmUEtI6H4ThrXcV8nwBd6B88wmp9LhSPLl9H2VwSA==", + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.104.1.tgz", + "integrity": "sha512-8nP/V5paP+S/17rlw+B2F12R2bB9PixU/+qnD2QdCjK1ajnG4qA0pVN3VSTQe2oCKND6GPZpm2ikmQWumwss9Q==", "license": "MIT", "dependencies": { - "@tanstack/history": "1.99.0", + "@tanstack/history": "1.99.13", "@tanstack/store": "^0.7.0" }, "engines": { diff --git a/app/front/package.json b/app/front/package.json index c3bd230..234aa5a 100644 --- a/app/front/package.json +++ b/app/front/package.json @@ -17,7 +17,7 @@ "@radix-ui/react-slot": "^1.1.1", "@tailwindcss/postcss": "^4.0.3", "@tailwindcss/vite": "^4.0.3", - "@tanstack/react-router": "^1.99.6", + "@tanstack/react-router": "^1.103.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.474.0", diff --git a/app/front/src/app.tsx b/app/front/src/app.tsx index 2b6d8e1..5bb8e30 100644 --- a/app/front/src/app.tsx +++ b/app/front/src/app.tsx @@ -1,7 +1,7 @@ -import { createRouter, RouterProvider } from "@tanstack/react-router"; -import useAuth, { AuthProvider } from "./hooks/useAuth"; -import { routeTree } from "./routeTree.gen"; -import { StrictMode } from "react"; +import { createRouter, RouterProvider } from '@tanstack/react-router'; +import useAuth, { AuthProvider } from './hooks/useAuth'; +import { routeTree } from './routeTree.gen'; +import { StrictMode } from 'react'; // Create a new router instance const router = createRouter({ @@ -15,22 +15,21 @@ const router = createRouter({ }, }); - // Register the router instance for type safety -declare module "@tanstack/react-router" { +declare module '@tanstack/react-router' { interface Register { router: typeof router; } } -export function App(){ - const auth = useAuth(); - - return ( +export function App() { + const auth = useAuth(); + + return ( - ) - } \ No newline at end of file + ); +} diff --git a/app/front/src/components/Folder.tsx b/app/front/src/components/Folder.tsx new file mode 100644 index 0000000..d53bba8 --- /dev/null +++ b/app/front/src/components/Folder.tsx @@ -0,0 +1,127 @@ +import { X } from 'lucide-react'; +import axios from 'axios'; +import { useState } from 'react'; + +interface CreateFolderModalProps { + isOpen: boolean; + onClose: () => void; + onCreateFolder: (folderName: string) => void; +} + +const CreateFolderModal = ({ isOpen, onClose, onCreateFolder }: CreateFolderModalProps) => { + const [folderName, setFolderName] = useState(''); + const [error, setError] = useState(null); + + const api = axios.create({ + baseURL: 'http://localhost:8082/api', + headers: { + Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3Q0QGdtYWlsLmNvbSIsImV4cCI6MTczOTYyODAwNCwidXNlcl9pZCI6IjY3YWY0NzVmZjI0ZWVlZjFmYWI2ZmU5NSJ9.LrLYhffochKEgF8PJhK-S7uH6_WhFkmrkPtWEja1pl0', + 'Content-Type': 'application/json', + }, + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!folderName.trim()) { + setError('Le nom du dossier est requis'); + return; + } + + try { + const response = await api.post('/folders', { + name: folderName + }); + + console.log('Dossier créé:', response.data); + + onCreateFolder(folderName); + setFolderName(''); + setError(null); + onClose(); + } catch (err) { + if (axios.isAxiosError(err)) { + setError(err.response?.data?.message || 'Erreur lors de la création du dossier'); + } else { + setError('Une erreur inattendue est survenue'); + } + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+ +
+
+

+ Nouveau dossier +

+ +
+ +
+
+
+ + setFolderName(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 + shadow-sm py-2 px-3 bg-white dark:bg-gray-700 + text-gray-900 dark:text-white + focus:border-blue-500 focus:ring-1 focus:ring-blue-500" + placeholder="Mon nouveau dossier" + /> +
+ + {error && ( +

+ {error} +

+ )} +
+ +
+ + +
+
+
+
+
+ ); +}; + +export default CreateFolderModal; \ No newline at end of file diff --git a/app/front/src/components/Table.tsx b/app/front/src/components/Table.tsx new file mode 100644 index 0000000..db816d3 --- /dev/null +++ b/app/front/src/components/Table.tsx @@ -0,0 +1,79 @@ +import { Folder } from '@/types'; + +const formatDate = (dateString: string) => { + const date = new Date(dateString); + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear(); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + return `${day}/${month}/${year} ${hours}:${minutes}`; +}; + +const Table = ({ + folders, + onFolderClick, +}: { + folders: Folder[]; + onFolderClick: (folder: Folder) => void; +}) => { + if (!folders || folders.length === 0) { + return ( +
Aucun dossier trouvé
+ ); + } + + return ( +
+ + + + + + + + + + + + {folders.map((folder: Folder) => ( + onFolderClick(folder)} + > + + + + + + + ))} + +
+ Nom + + Chemin + + Profondeur + + Créé le + + Modifié le +
+ {folder.name} + + {folder.path} + + {folder.depth} + + {formatDate(folder.created_at)} + + {formatDate(folder.updated_at)} +
+
+ ); +}; + +export default Table; diff --git a/app/front/src/components/header.tsx b/app/front/src/components/header.tsx index 7215c37..3048314 100644 --- a/app/front/src/components/header.tsx +++ b/app/front/src/components/header.tsx @@ -2,13 +2,13 @@ import { Popover, PopoverContent, PopoverTrigger, -} from "@/components/ui/popover"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import useAuth from "@/hooks/useAuth"; -import { useNavigate, useParams, useRouter } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; -import { Button } from "./ui/button"; -import { PlusIcon } from "lucide-react"; +} from '@/components/ui/popover'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import useAuth from '@/hooks/useAuth'; +import { useNavigate, useParams, useRouter } from '@tanstack/react-router'; +import { useEffect, useState } from 'react'; +import { Button } from './ui/button'; +import { PlusIcon } from 'lucide-react'; import { Dialog, DialogContent, @@ -16,8 +16,9 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "./ui/dialog"; -import { Input } from "./ui/input"; +} from './ui/dialog'; +import { Input } from './ui/input'; +import { Folder } from '@/types'; interface FolderItem { id: string; @@ -28,84 +29,83 @@ export function SiteHeader() { const navigate = useNavigate(); const router = useRouter(); const params = useParams({ - from: "/_auth/drive/$folderPath", + from: '/_auth/drive/$folderPath', + shouldThrow: false, }); const folderPath = params?.folderPath ?? undefined; const { logout, isAuth, user, accessToken } = useAuth(); const [email, setEmail] = useState(null); - const [folderName, setFolderName] = useState(""); + const [folderName, setFolderName] = useState(''); const [isDialogOpen, setIsDialogOpen] = useState(false); const [isFileDialogOpen, setIsFileDialogOpen] = useState(false); + const [currentFolder, setCurrentFolder] = useState(null); useEffect(() => { if (!isAuth) { - navigate({ to: "/login" }); + navigate({ to: '/login' }); } else { - setEmail(user?.email ?? ""); + setEmail(user?.email ?? ''); } }, [isAuth, navigate, user?.email]); - const handleLogout = async () => { - await logout(); - - navigate({ to: "/login" }); - }; - - const handleCreateFolder = async () => { - if (!folderName.trim()) return; - - let parentId = folderPath; - if (!parentId) { - try { - const foldersResponse = await fetch( - "http://localhost:8082/api/folders", - { - method: "GET", + useEffect(() => { + const fetchCurrentFolder = async () => { + if (folderPath) { + try { + const response = await fetch('http://localhost:8082/api/folders', { + method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, + }); + + if (!response.ok) { + throw new Error('Error fetching folders'); } - ); - if (!foldersResponse.ok) { - throw new Error("Error recuperating folder"); - } - const folders = await foldersResponse.json(); - const rootFolder = folders.find( - (folder: { id: string; name: string }) => - folder.name.toLowerCase() === "root" - ); - if (!rootFolder) { - throw new Error("Root folder not found"); + + const folders: FolderItem[] = await response.json(); + const folder = folders.find((f) => f.name === folderPath); + setCurrentFolder(folder || null); + } catch (error) { + console.error(error); } - parentId = rootFolder.name; - } catch (error) { - console.error(error); - return; + } else { + setCurrentFolder(null); } - } + }; + + fetchCurrentFolder(); + }, [folderPath, accessToken]); + + const handleLogout = async () => { + await logout(); + navigate({ to: '/login' }); + }; + + const handleCreateFolder = async () => { + if (!folderName.trim()) return; try { - const response = await fetch("http://localhost:8082/api/folders", { - method: "POST", + const response = await fetch('http://localhost:8082/api/folders', { + method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ name: folderName, - parent_id: parentId, + parent_id: currentFolder?.id, }), }); if (!response.ok) { - throw new Error("Error creating folder"); + throw new Error('Error creating folder'); } router.invalidate(); - - setFolderName(""); + setFolderName(''); setIsDialogOpen(false); } catch (error) { console.error(error); @@ -121,86 +121,79 @@ export function SiteHeader() { const file = target.file.files?.[0]; if (!file) { - throw Error('No file found') + throw Error('No file found'); } - let currentfolderName = folderPath; - if (!currentfolderName) { - try { + try { + let folderId = currentFolder?.id; + + if (!folderId) { const foldersResponse = await fetch( - "http://localhost:8082/api/folders", + 'http://localhost:8082/api/folders', { - method: "GET", + method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, } ); if (!foldersResponse.ok) { - throw new Error("Error recuperating file"); - } - - const folders: FolderItem[] = await foldersResponse.json(); - const rootFolder = folders.find( - (folder) => folder.name.toLowerCase() === "root" - ); - - if (!rootFolder) { - throw new Error("Root folder not found"); + throw new Error('Error fetching folders'); } - currentfolderName = rootFolder.name; - } catch (error) { - console.error(error); - return; + const folders: Folder[] = await foldersResponse.json(); + const rootFolder = folders.find((folder) => folder.path === '/'); + folderId = rootFolder?.id; } - } - - const formData = new FormData(); - formData.append("file", file); - formData.append("folder_id", currentfolderName); + const formData = new FormData(); + formData.append('file', file); + formData.append('folder_id', folderId as string); - const res = await fetch("http://localhost:8082/api/files", { - method: "POST", - headers: { - Authorization: `Bearer ${accessToken}`, - }, - body: formData, - }); + const res = await fetch('http://localhost:8082/api/files', { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + body: formData, + }); - if (res.ok) { - setIsFileDialogOpen(false); - router.invalidate(); - } else { - throw new Error('Error when sending file') + if (res.ok) { + setIsFileDialogOpen(false); + router.invalidate(); + } else { + throw new Error('Error when sending file'); + } + } catch (error) { + console.error(error); } }; + // Le reste du JSX reste identique return ( -
-
+
+
- - + Create a new folder - + setFolderName(e.target.value)} /> -
- - + Add a new file - + Please select a file to upload -
+ - +
@@ -241,16 +234,16 @@ export function SiteHeader() { - {email ? email.slice(0, 2).toUpperCase() : "U"} + {email ? email.slice(0, 2).toUpperCase() : 'U'} -
- {email ? email : "Utilisateur"} +
+ {email ? email : 'Utilisateur'} diff --git a/app/front/src/routes/__root.tsx b/app/front/src/routes/__root.tsx index 745c97c..a5370b1 100644 --- a/app/front/src/routes/__root.tsx +++ b/app/front/src/routes/__root.tsx @@ -1,6 +1,6 @@ -import { AuthState } from "@/hooks/hooksTypes"; -import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; -import { TanStackRouterDevtools } from "@tanstack/router-devtools"; +import { AuthState } from '@/hooks/hooksTypes'; +import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'; +import { TanStackRouterDevtools } from '@tanstack/router-devtools'; interface RouterAuthContext { auth: AuthState; @@ -10,7 +10,7 @@ export const Route = createRootRouteWithContext()({ component: () => ( <> - + ), }); diff --git a/app/front/src/routes/_auth.tsx b/app/front/src/routes/_auth.tsx index 87f5e99..d4c6d69 100644 --- a/app/front/src/routes/_auth.tsx +++ b/app/front/src/routes/_auth.tsx @@ -1,5 +1,5 @@ -import { SiteHeader } from '@/components/header' -import { createFileRoute, redirect } from '@tanstack/react-router' +import { SiteHeader } from '@/components/header'; +import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; export const Route = createFileRoute('/_auth')({ beforeLoad: ({ context, location }) => { @@ -9,9 +9,16 @@ export const Route = createFileRoute('/_auth')({ search: { redirect: location.href, }, - }) + }); } }, - component: SiteHeader, -}) + component: () => { + return ( + <> + + + + ); + }, +}); diff --git a/app/front/src/routes/_auth/drive/$folderPath.tsx b/app/front/src/routes/_auth/drive/$folderPath.tsx index bb08a49..473b252 100644 --- a/app/front/src/routes/_auth/drive/$folderPath.tsx +++ b/app/front/src/routes/_auth/drive/$folderPath.tsx @@ -1,9 +1,58 @@ -import { createFileRoute } from '@tanstack/react-router' +import Table from '@/components/Table'; +import { Folder } from '@/types'; +import { createFileRoute } from '@tanstack/react-router'; +import { useNavigate } from '@tanstack/react-router'; -export const Route = createFileRoute('/_auth/drive/$folderPath')({ - component: RouteComponent, -}) +function DriveComponent() { + const { folders } = Route.useLoaderData() as { folders: Folder[] }; + const { folderPath } = Route.useParams(); + const navigate = useNavigate(); + + const filteredFolders = folders.filter((folder: Folder) => { + if (!folderPath) { + return folder.depth === 0; + } + const parentFolder = folders.find((f: Folder) => f.name === folderPath); + return parentFolder && folder.parent_id === parentFolder.id; + }); + + const handleFolderClick = (folder: Folder) => { + navigate({ to: `/auth/drive/${folder.name}` }); + }; -function RouteComponent() { - return
Hello "/_auth/drive/$folderPath"!
+ return ; } + +export const Route = createFileRoute('/_auth/drive/$folderPath')({ + loader: async ({ context }) => { + try { + const response = await fetch('http://localhost:8082/api/folders', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${context.auth.accessToken}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch folders'); + } + + const folders = await response.json(); + return { folders }; + } catch (error) { + console.error('Error loading folders:', error); + return { folders: [] }; + } + }, + + component: DriveComponent, + + parseParams: (params) => { + return { + folderPath: params.folderPath + ? decodeURIComponent(params.folderPath) + : '', + }; + }, +}); diff --git a/app/front/src/routes/_auth/drive/index.tsx b/app/front/src/routes/_auth/drive/index.tsx index 1624666..17e725e 100644 --- a/app/front/src/routes/_auth/drive/index.tsx +++ b/app/front/src/routes/_auth/drive/index.tsx @@ -1,6 +1,86 @@ -import Drive from '@/pages/drive' -import { createFileRoute } from '@tanstack/react-router' +import Table from '@/components/Table'; +import { Folder } from '@/types'; +import { createFileRoute } from '@tanstack/react-router'; +import { useNavigate } from '@tanstack/react-router'; + +const DriveComponent = () => { + const { folders } = Route.useLoaderData(); + const { folderPath } = Route.useParams() as { folderPath: string }; + const navigate = useNavigate(); + + const currentPath = folderPath ? `/${folderPath}` : '/'; + + const handleFolderClick = (folder: Folder) => { + const newPath = folder.path === '/' ? '' : folder.path; + navigate({ to: `/drive${newPath}` }); + }; + + const filteredFolders = folders.filter((folder: Folder) => { + if (!folderPath) { + return folder.depth === 0; + } + + return ( + folder.path.startsWith(currentPath) && + folder.path !== currentPath && + !folder.path.slice(currentPath.length + 1).includes('/') + ); + }); + + return ( +
+
+ navigate({ to: '/drive' })} + className='cursor-pointer hover:text-blue-500' + > + Root + + {folderPath && + folderPath + .split('/') + .filter(Boolean) + .map((segment, index, array) => ( + + {' / '} + { + const pathTo = array.slice(0, index + 1).join('/'); + navigate({ to: `/drive/${pathTo}` }); + }} + > + {segment} + + + ))} +
+
+ + ); +}; export const Route = createFileRoute('/_auth/drive/')({ - component: () => , -}) + loader: async ({ context }) => { + try { + const response = await fetch('http://localhost:8082/api/folders', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${context.auth.accessToken}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch folders'); + } + + const folders = await response.json(); + return { folders }; + } catch (error) { + console.error('Error loading folders:', error); + return { folders: [] }; + } + }, + component: DriveComponent, +}); diff --git a/app/front/src/types/index.ts b/app/front/src/types/index.ts new file mode 100644 index 0000000..67ed1b1 --- /dev/null +++ b/app/front/src/types/index.ts @@ -0,0 +1,10 @@ +export interface Folder { + id: string; + name: string; + path: string; + parent_id?: string; + user_id: string; + depth: number; + created_at: string; + updated_at: string; +} \ No newline at end of file