diff --git a/backend/app/api/endpoints/datasets.py b/backend/app/api/endpoints/datasets.py index c6e2786..1a0de9e 100644 --- a/backend/app/api/endpoints/datasets.py +++ b/backend/app/api/endpoints/datasets.py @@ -130,18 +130,30 @@ async def get_dataset_details(dataset_id: int, db: Session = Depends(database.ge return data -@router.get("/recent", response_model=List[schemas.LastResponse]) -def get_recent_datasets(db: Session = Depends(database.get_db)): - recent_datasets = db.query(models.Dataset).order_by(models.Dataset.last_modified.desc()).limit(3).all() - return [ - schemas.LastResponse( - dataset_id=dataset.dataset_id, - name=dataset.name, - description=dataset.description, - last_modified=dataset.last_modified - ) for dataset in recent_datasets - ] - +@router.get("/recent",response_model=schemas.PaginatedResponse[schemas.LastResponse]) +def get_recent_datasets( + page: int = Query(1, alias="page", ge=1), + limit: int = Query(10, alias="limit", le=100), + db: Session = Depends(database.get_db) + ): + skip = (page - 1) * limit + datasets = db.query(models.Dataset).order_by(models.Dataset.last_modified.desc()).offset(skip).limit(limit).all() + total = db.query(models.Dataset).count() + + return schemas.PaginatedResponse[schemas.LastResponse]( + data=[ + schemas.LastResponse( + dataset_id=dataset.dataset_id, + name=dataset.name, + description=dataset.description, + last_modified=dataset.last_modified + ) for dataset in datasets + ], + total=total, + page=page, + limit=limit, + has_more=(page * limit) < total + ) @router.post("/{dataset_id}/transform", response_model=schemas.BasicQueryResponse) async def transform_dataset( diff --git a/backend/app/schemas.py b/backend/app/schemas.py index a060d79..3509f4b 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -1,8 +1,9 @@ -from pydantic import BaseModel +from pydantic import BaseModel from enum import Enum -from typing import Optional, Union, Any, List +from typing import Optional, TypeVar, Union, Any, List , Generic import datetime +T = TypeVar("T") # Basic Functions class FilterParameters(BaseModel): @@ -130,4 +131,10 @@ class LastResponse(BaseModel): class Config: from_attributes = True +class PaginatedResponse(BaseModel, Generic[T]): + data: List[T] + total: int + page: int + limit: int + has_more: bool \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index d56fd6f..a339177 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", @@ -10,25 +10,35 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^4.0.0", "@repo/ui": "*", + "@tanstack/react-query": "^5.66.0", "axios": "^1.7.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^0.475.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.54.2", "react-icons": "^5.2.1", - "react-router-dom": "^6.24.0" + "react-router-dom": "^6.24.0", + "sonner": "^1.7.4", + "tailwind-merge": "^3.0.1", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.19", + "autoprefixer": "^10.4.20", "eslint": "^8.57.0", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.4", + "postcss": "^8.5.2", + "tailwindcss": "^3.4.17", "vite": "^5.2.0" } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1ee0c8c..ef4477a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,11 +4,16 @@ import {BrowserRouter as Router, Route, Routes, useLocation} from 'react-router- import DataScreen from './Components/DataScreen'; import HomeScreen from './Components/Homescreen'; import Navbar from './Components/Navbar'; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); function App() { return ( + + ); } diff --git a/frontend/src/Components/CreateProjectModal.jsx b/frontend/src/Components/CreateProjectModal.jsx new file mode 100644 index 0000000..59e2b10 --- /dev/null +++ b/frontend/src/Components/CreateProjectModal.jsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { uploadDataset } from '../api'; + +const CreateProjectModal = ({ isOpen, onClose }) => { + const [fileUpload, setFileUpload] = useState(null); + const [formData, setFormData] = useState({ + projectName: "", + projectDescription: "" + }); + const [errors, setErrors] = useState({}); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + // React Query mutation for uploading dataset + const uploadMutation = useMutation({ + mutationFn: async ({ file, projectName, projectDescription }) => { + return await uploadDataset(file, projectName, projectDescription); + }, + onSuccess: (data) => { + queryClient.invalidateQueries(["recentProjects"]); + toast.success("Project created successfully!"); + onClose(); + navigate("/data", { state: { datasetId: data.dataset_id, apiData: data } }); + }, + onError: (error) => { + toast.error("Error creating project: " + error.message); + }, + }); + + const validateForm = () => { + const newErrors = {}; + if (!formData.projectName.trim()) { + newErrors.projectName = "Project name is required"; + } + if (!formData.projectDescription.trim()) { + newErrors.projectDescription = "Project description is required"; + } + if (!fileUpload) { + newErrors.file = "Please select a file to upload"; + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!validateForm()) return; + + uploadMutation.mutate({ + file: fileUpload, + projectName: formData.projectName, + projectDescription: formData.projectDescription, + }); + }; + + const handleFileUpload = (event) => { + const file = event.target.files[0]; + setFileUpload(file); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + if (!isOpen) return null; + + return ( +
+
+

Create New Project

+
+
+ + + {errors.projectName && ( +

{errors.projectName}

+ )} +
+ +
+ + + {errors.file && ( +

{errors.file}

+ )} +
+ +
+ +