diff --git a/src/frontend/package.json b/src/frontend/package.json index 2c39bcbc..94a95a59 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -26,6 +26,7 @@ "@tanstack/react-table": "^8.9.3", "@terraformer/wkt": "^2.2.0", "@turf/area": "^7.0.0", + "@turf/bbox": "^7.0.0", "@turf/centroid": "^7.0.0", "@turf/flatten": "^7.0.0", "@turf/length": "^7.0.0", diff --git a/src/frontend/src/api/projects.ts b/src/frontend/src/api/projects.ts new file mode 100644 index 00000000..f2483769 --- /dev/null +++ b/src/frontend/src/api/projects.ts @@ -0,0 +1,11 @@ +import { UseQueryOptions, useQuery } from "@tanstack/react-query"; +import { getProjectsList } from "@Services/createproject"; + +export const useGetProjectsListQuery = (id?:number, queryOptions?: Partial) => { + return useQuery({ + queryKey: ['projects-list'], + queryFn: () => getProjectsList(id), + select: (res: any) => res.data, + ...queryOptions, + }); +}; \ No newline at end of file diff --git a/src/frontend/src/components/CreateProject/CreateProjectHeader/index.tsx b/src/frontend/src/components/CreateProject/CreateProjectHeader/index.tsx index d4d36203..3deefdaa 100644 --- a/src/frontend/src/components/CreateProject/CreateProjectHeader/index.tsx +++ b/src/frontend/src/components/CreateProject/CreateProjectHeader/index.tsx @@ -6,7 +6,7 @@ export default function CreateProjectHeader() { const navigate = useNavigate(); return ( - navigate('/')} /> + navigate('/projects')} />
Project /
 Add Project
diff --git a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx index ec02e863..af40ad69 100644 --- a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx +++ b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable no-console */ import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { useTypedSelector, useTypedDispatch } from '@Store/hooks'; @@ -17,9 +15,11 @@ import { ContributionsForm, GenerateTaskForm, } from '@Components/CreateProject/FormContents'; -import { postCreateProject } from '@Services/createproject'; +import { postCreateProject, postTaskBoundary } from '@Services/createproject'; import { useMutation } from '@tanstack/react-query'; import { toast } from 'react-toastify'; +import { convertGeojsonToFile } from '@Utils/convertLayerUtils'; +import prepareFormData from '@Utils/prepareFormData'; /** * This function looks up the provided map of components to find and return @@ -59,6 +59,9 @@ export default function CreateprojectLayout() { const dispatch = useTypedDispatch(); const navigate = useNavigate(); const activeStep = useTypedSelector(state => state.createproject.activeStep); + const splitGeojson = useTypedSelector( + state => state.createproject.splitGeojson, + ); const initialState = { name: '', @@ -68,11 +71,26 @@ export default function CreateprojectLayout() { outline_geojson: {}, }; - const { mutate } = useMutation({ + const { mutate: uploadTaskBoundary } = useMutation({ + mutationFn: postTaskBoundary, + onSuccess: () => { + toast.success('Project Boundary Uploaded'); + navigate('/projects'); + }, + onError: err => { + toast.error(err.message); + }, + }); + + const { mutate: createProject } = useMutation({ mutationFn: postCreateProject, onSuccess: (res: any) => { toast.success('Project Created Successfully'); - navigate('/'); + dispatch(setCreateProjectState({ projectId: res.data.id })); + if (!splitGeojson) return; + const geojson = convertGeojsonToFile(splitGeojson); + const formData = prepareFormData({ task_geojson: geojson }); + uploadTaskBoundary({ id: res.data.id, data: formData }); }, onError: err => { toast.error(err.message); @@ -81,10 +99,11 @@ export default function CreateprojectLayout() { const onSubmit = (data: any) => { if (activeStep < 5) return; - mutate(data); + createProject(data); + reset(); }; - const { register, setValue, handleSubmit } = useForm({ + const { register, setValue, handleSubmit, reset } = useForm({ defaultValues: initialState, }); diff --git a/src/frontend/src/components/CreateProject/FormContents/DefineAOI/MapSection/index.tsx b/src/frontend/src/components/CreateProject/FormContents/DefineAOI/MapSection/index.tsx index 4e6e433b..24e87e48 100644 --- a/src/frontend/src/components/CreateProject/FormContents/DefineAOI/MapSection/index.tsx +++ b/src/frontend/src/components/CreateProject/FormContents/DefineAOI/MapSection/index.tsx @@ -5,8 +5,11 @@ import MapContainer from '@Components/common/MapLibreComponents/MapContainer'; import MeasureTool from '@Components/common/MapLibreComponents/MeasureTool'; import { setCreateProjectState } from '@Store/actions/createproject'; import VectorLayer from '@Components/common/MapLibreComponents/Layers/VectorLayer'; -import { Map } from 'maplibre-gl'; +import { LngLatBoundsLike, Map } from 'maplibre-gl'; import { GeojsonType } from '@Components/common/MapLibreComponents/types'; +import getBbox from '@turf/bbox'; +import { useEffect } from 'react'; +import { FeatureCollection } from 'geojson'; export default function MapSection() { const dispatch = useTypedDispatch(); @@ -26,6 +29,12 @@ export default function MapSection() { state => state.createproject.measureType, ); + useEffect(() => { + if (!uploadedGeojson) return; + const bbox = getBbox(uploadedGeojson as FeatureCollection); + map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 }); + }, [map, uploadedGeojson]); + return ( state.createproject.uploadedGeojson, + ); const uploadAreaOption = useTypedSelector( state => state.createproject.uploadAreaOption, ); @@ -26,7 +30,6 @@ export default function DefineAOI({ formProps }: { formProps: any }) { dispatch( setCreateProjectState({ uploadedGeojson: convertedGeojson }), ); - setValue('outline_geojson', convertedGeojson); } }); } catch (err: any) { @@ -35,6 +38,11 @@ export default function DefineAOI({ formProps }: { formProps: any }) { } }; + useEffect(() => { + if (!uploadedGeojson) return; + setValue('outline_geojson', uploadedGeojson); + }, [uploadedGeojson]); + return (
diff --git a/src/frontend/src/components/CreateProject/FormContents/GenerateTask/MapSection/index.tsx b/src/frontend/src/components/CreateProject/FormContents/GenerateTask/MapSection/index.tsx index 6a83a00c..9eb159b6 100644 --- a/src/frontend/src/components/CreateProject/FormContents/GenerateTask/MapSection/index.tsx +++ b/src/frontend/src/components/CreateProject/FormContents/GenerateTask/MapSection/index.tsx @@ -1,10 +1,13 @@ +import { useEffect } from 'react'; import { useTypedSelector } from '@Store/hooks'; +import { LngLatBoundsLike, Map } from 'maplibre-gl'; import { useMapLibreGLMap } from '@Components/common/MapLibreComponents'; import BaseLayerSwitcher from '@Components/common/MapLibreComponents/BaseLayerSwitcher'; import MapContainer from '@Components/common/MapLibreComponents/MapContainer'; import VectorLayer from '@Components/common/MapLibreComponents/Layers/VectorLayer'; -import { Map } from 'maplibre-gl'; import { GeojsonType } from '@Components/common/MapLibreComponents/types'; +import getBbox from '@turf/bbox'; +import { FeatureCollection } from 'geojson'; export default function MapSection() { const { map, isMapLoaded } = useMapLibreGLMap({ @@ -20,6 +23,12 @@ export default function MapSection() { state => state.createproject.splitGeojson, ); + useEffect(() => { + if (!splitGeojson) return; + const bbox = getBbox(splitGeojson as FeatureCollection); + map?.fitBounds(bbox as LngLatBoundsLike, { padding: 30 }); + }, [splitGeojson, map]); + return ( { if (authcode) { const callbackUrl = `${BASE_URL}/users/callback/?code=${authcode}&state=${state}`; + const userDetailsUrl = `${BASE_URL}/users/me/`; const completeLogin = async () => { - await fetch(callbackUrl, { credentials: 'include' }); + const response = await fetch(callbackUrl, { credentials: 'include' }); + const token = await response.json(); + localStorage.setItem('token', token); }; - await completeLogin(); + + toast.success('Logged In Successfully'); + navigate('/user-profile'); } setIsReadyToRedirect(true); }; diff --git a/src/frontend/src/components/IndividualProject/index.tsx b/src/frontend/src/components/IndividualProject/index.tsx new file mode 100644 index 00000000..6334b91c --- /dev/null +++ b/src/frontend/src/components/IndividualProject/index.tsx @@ -0,0 +1,10 @@ +import { useParams } from 'react-router-dom'; + +export default function IndividualProject() { + const { id } = useParams(); + return ( +
+

This is {id} project section

+
+ ); +} diff --git a/src/frontend/src/components/Projects/ProjectCard/MapSection/index.tsx b/src/frontend/src/components/Projects/ProjectCard/MapSection/index.tsx index caa17321..3c05854e 100644 --- a/src/frontend/src/components/Projects/ProjectCard/MapSection/index.tsx +++ b/src/frontend/src/components/Projects/ProjectCard/MapSection/index.tsx @@ -1,8 +1,20 @@ +import { useEffect } from 'react'; +import { LngLatBoundsLike, Map } from 'maplibre-gl'; import { useMapLibreGLMap } from '@Components/common/MapLibreComponents'; import BaseLayerSwitcher from '@Components/common/MapLibreComponents/BaseLayerSwitcher'; +import VectorLayer from '@Components/common/MapLibreComponents/Layers/VectorLayer'; import MapContainer from '@Components/common/MapLibreComponents/MapContainer'; +import { GeojsonType } from '@Components/common/MapLibreComponents/types'; +import getBbox from '@turf/bbox'; +import { FeatureCollection } from 'geojson'; -export default function MapSection({ containerId }: { containerId: string }) { +export default function MapSection({ + containerId, + geojson, +}: { + containerId: string; + geojson: GeojsonType; +}) { const { map, isMapLoaded } = useMapLibreGLMap({ containerId, mapOptions: { @@ -12,6 +24,13 @@ export default function MapSection({ containerId }: { containerId: string }) { }, disableRotation: true, }); + + useEffect(() => { + if (!geojson) return; + const bbox = getBbox(geojson as FeatureCollection); + map?.fitBounds(bbox as LngLatBoundsLike, { padding: 30 }); + }, [geojson, map]); + return ( + {geojson && ( + + )} ); diff --git a/src/frontend/src/components/Projects/ProjectCard/index.tsx b/src/frontend/src/components/Projects/ProjectCard/index.tsx index d52c7715..f54aee6b 100644 --- a/src/frontend/src/components/Projects/ProjectCard/index.tsx +++ b/src/frontend/src/components/Projects/ProjectCard/index.tsx @@ -1,18 +1,37 @@ +import { useNavigate } from 'react-router-dom'; import MapSection from './MapSection'; +import { GeojsonType } from '@Components/common/MapLibreComponents/types'; + +interface IProjectCardProps { + containerId: string; + id: number; + title: string; + description: string; + geojson: GeojsonType; +} + +export default function ProjectCard({ + containerId, + id, + title, + description, + geojson, +}: IProjectCardProps) { + const navigate = useNavigate(); + + const onProjectCardClick = () => { + navigate(`/projects/${id}`); + }; -export default function ProjectCard({ containerId }: { containerId: string }) { return ( -
- -

ID: #12468

-

- Lorem ipsum dolor sit amet consectur -

-

- Cameroon RoLorem ipsum dolor sit amet consec.Lorem ipsum dolor sit amet - consectetur.Lorem ipsum dolor sit amet consectetur.ad Assessment for - Sustainable Development in Rural Communities in Africa -

+
+ +

ID: #{id}

+

{title}

+

{description}

); } diff --git a/src/frontend/src/components/Projects/ProjectCardSkeleton/index.tsx b/src/frontend/src/components/Projects/ProjectCardSkeleton/index.tsx new file mode 100644 index 00000000..2971d844 --- /dev/null +++ b/src/frontend/src/components/Projects/ProjectCardSkeleton/index.tsx @@ -0,0 +1,11 @@ +import Skeleton from '@Components/RadixComponents/Skeleton'; + +export default function ProjectCardSkeleton() { + return ( + + + + + + ); +} diff --git a/src/frontend/src/constants/index.ts b/src/frontend/src/constants/index.ts index 5890e130..f48c75b5 100644 --- a/src/frontend/src/constants/index.ts +++ b/src/frontend/src/constants/index.ts @@ -2,7 +2,7 @@ export const navLinks = [ { id: 1, - link: '/', + link: '/projects', linkName: 'Projects', }, { diff --git a/src/frontend/src/modules/user-auth-module/src/components/Authentication/Login/index.tsx b/src/frontend/src/modules/user-auth-module/src/components/Authentication/Login/index.tsx index 4cc6060f..72738505 100644 --- a/src/frontend/src/modules/user-auth-module/src/components/Authentication/Login/index.tsx +++ b/src/frontend/src/modules/user-auth-module/src/components/Authentication/Login/index.tsx @@ -40,7 +40,7 @@ export default function Login() { localStorage.setItem('token', res.data.access_token); localStorage.setItem('refresh', res.data.refresh_token); toast.success('Logged In Successfully'); - navigate('/'); + navigate('/projects'); }, onError: err => { toast.error(err.response.data.detail); diff --git a/src/frontend/src/routes/appRoutes.ts b/src/frontend/src/routes/appRoutes.ts index ef1938b3..cd7047de 100644 --- a/src/frontend/src/routes/appRoutes.ts +++ b/src/frontend/src/routes/appRoutes.ts @@ -5,11 +5,12 @@ import CreateProject from '@Components/CreateProject'; import GoogleAuth from '@Components/GoogleAuth'; import userRoutes from '@UserModule/routes'; import { IRoute } from './types'; +import IndividualProject from '@Components/IndividualProject'; const appRoutes: IRoute[] = [ ...userRoutes, { - path: '/', + path: '/projects', name: 'Projects ', component: Projects, authenticated: true, @@ -32,6 +33,12 @@ const appRoutes: IRoute[] = [ component: CreateProject, authenticated: true, }, + { + path: '/projects/:id', + name: 'Individual Project', + component: IndividualProject, + authenticated: true, + }, { path: '/user-profile', name: 'User Profile', diff --git a/src/frontend/src/services/createproject.ts b/src/frontend/src/services/createproject.ts index 46c8aff7..88340b6b 100644 --- a/src/frontend/src/services/createproject.ts +++ b/src/frontend/src/services/createproject.ts @@ -1,6 +1,11 @@ /* eslint-disable import/prefer-default-export */ import { authenticated, api } from '.'; +export const getProjectsList = (id?: number) => { + const endpoint = `/projects${id ? `/${id}` : '/'}`; + return authenticated(api).get(endpoint); +} + export const postCreateProject = (data: any) => authenticated(api).post('/projects/create_project', data, { headers: { 'Content-Type': 'application/json' }, @@ -8,3 +13,6 @@ export const postCreateProject = (data: any) => export const postPreviewSplitBySquare = (data: any) => authenticated(api).post('/projects/preview-split-by-square/', data); + +export const postTaskBoundary = ({ id, data }: {id: number; data: any}) => + authenticated(api).post(`/projects/${id}/upload-task-boundaries`, data); diff --git a/src/frontend/src/store/slices/createproject.ts b/src/frontend/src/store/slices/createproject.ts index a063c46e..efa9dce2 100644 --- a/src/frontend/src/store/slices/createproject.ts +++ b/src/frontend/src/store/slices/createproject.ts @@ -3,6 +3,7 @@ import type { CaseReducer, PayloadAction } from '@reduxjs/toolkit'; import persist from '@Store/persist'; export interface CreateProjectState { + projectId: number | null; activeStep: number; uploadAreaOption: 'draw' | 'upload_file'; keyParamOption: 'basic' | 'advanced'; @@ -14,6 +15,7 @@ export interface CreateProjectState { } const initialState: CreateProjectState = { + projectId: null, activeStep: 1, uploadAreaOption: 'upload_file', keyParamOption: 'basic', diff --git a/src/frontend/src/views/Projects/index.tsx b/src/frontend/src/views/Projects/index.tsx index 33bad8a9..f7f9d6ca 100644 --- a/src/frontend/src/views/Projects/index.tsx +++ b/src/frontend/src/views/Projects/index.tsx @@ -4,9 +4,15 @@ import { ProjectsHeader, ProjectsMapSection, } from '@Components/Projects'; +import { useGetProjectsListQuery } from '@Api/projects'; +import ProjectCardSkeleton from '@Components/Projects/ProjectCardSkeleton'; export default function Projects() { const showMap = useTypedSelector(state => state.common.showMap); + + // fetch api for projectsList + const { data: projectsList, isLoading } = useGetProjectsListQuery(); + return (
@@ -19,9 +25,26 @@ export default function Projects() { : 'lg:scrollbar naxatw-grid-cols-1 sm:naxatw-grid-cols-2 md:naxatw-grid-cols-3 lg:naxatw-h-[75vh] lg:naxatw-grid-cols-2 lg:naxatw-overflow-y-scroll 2xl:naxatw-grid-cols-3' }`} > - {[1, 2, 3, 4, 5, 6].map(item => ( - - ))} + {isLoading ? ( + <> + {Array.from({ length: 6 }, (_, index) => ( + + ))} + + ) : ( + (projectsList as Record[])?.map( + (singleproject: Record) => ( + + ), + ) + )}
{showMap && }