diff --git a/src/frontend/package.json b/src/frontend/package.json index 1b89e5ec..d0e314b4 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -9,6 +9,7 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.6", + "@react-spring/web": "^9.7.5", "@reduxjs/toolkit": "^1.9.3", "@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-query": "^4.32.6", diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index d3fb97b5..1308bc00 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -50,6 +50,7 @@ export default function App() { // add routes where you dont want navigation bar const routesWithoutNavbar = [ '/', + '/tutorials', '/login', '/forgot-password', '/complete-profile', diff --git a/src/frontend/src/api/tasks.ts b/src/frontend/src/api/tasks.ts index ddce0bad..96e4f395 100644 --- a/src/frontend/src/api/tasks.ts +++ b/src/frontend/src/api/tasks.ts @@ -1,7 +1,7 @@ /* eslint-disable import/prefer-default-export */ import { getIndividualTask, - // getTaskAssetsInfo, + getTaskAssetsInfo, getTaskWaypoint, } from '@Services/tasks'; import { useQuery, UseQueryOptions } from '@tanstack/react-query'; @@ -35,16 +35,16 @@ export const useGetIndividualTaskQuery = ( }); }; -// export const useGetTaskAssetsInfo = ( -// projectId: string, -// taskId: string, -// queryOptions?: Partial, -// ) => { -// return useQuery({ -// queryKey: ['task-assets-info'], -// enabled: !!taskId, -// queryFn: () => getTaskAssetsInfo(projectId, taskId), -// select: (res: any) => res.data, -// ...queryOptions, -// }); -// }; +export const useGetTaskAssetsInfo = ( + projectId: string, + taskId: string, + queryOptions?: Partial, +) => { + return useQuery({ + queryKey: ['task-assets-info'], + enabled: !!taskId, + queryFn: () => getTaskAssetsInfo(projectId, taskId), + select: (res: any) => res.data, + ...queryOptions, + }); +}; diff --git a/src/frontend/src/assets/images/tutorials/flight_plan_load_on_Rc.png b/src/frontend/src/assets/images/tutorials/flight_plan_load_on_Rc.png new file mode 100644 index 00000000..d1199b14 Binary files /dev/null and b/src/frontend/src/assets/images/tutorials/flight_plan_load_on_Rc.png differ diff --git a/src/frontend/src/assets/images/tutorials/flight_plan_replacement_on_rc.png b/src/frontend/src/assets/images/tutorials/flight_plan_replacement_on_rc.png new file mode 100644 index 00000000..e5490797 Binary files /dev/null and b/src/frontend/src/assets/images/tutorials/flight_plan_replacement_on_rc.png differ diff --git a/src/frontend/src/assets/images/tutorials/image_processing.png b/src/frontend/src/assets/images/tutorials/image_processing.png new file mode 100644 index 00000000..1ec84985 Binary files /dev/null and b/src/frontend/src/assets/images/tutorials/image_processing.png differ diff --git a/src/frontend/src/assets/images/tutorials/project_creation.png b/src/frontend/src/assets/images/tutorials/project_creation.png new file mode 100644 index 00000000..bce8d747 Binary files /dev/null and b/src/frontend/src/assets/images/tutorials/project_creation.png differ diff --git a/src/frontend/src/assets/images/tutorials/sign_up_and_login.png b/src/frontend/src/assets/images/tutorials/sign_up_and_login.png new file mode 100644 index 00000000..36ec5b31 Binary files /dev/null and b/src/frontend/src/assets/images/tutorials/sign_up_and_login.png differ diff --git a/src/frontend/src/assets/images/tutorials/viewing_final_output.png b/src/frontend/src/assets/images/tutorials/viewing_final_output.png new file mode 100644 index 00000000..76a5881c Binary files /dev/null and b/src/frontend/src/assets/images/tutorials/viewing_final_output.png differ diff --git a/src/frontend/src/assets/images/tutorials/work_flow_for_drone_operator.png b/src/frontend/src/assets/images/tutorials/work_flow_for_drone_operator.png new file mode 100644 index 00000000..54016673 Binary files /dev/null and b/src/frontend/src/assets/images/tutorials/work_flow_for_drone_operator.png differ diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx index dc8051a5..0b3bc30c 100644 --- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx @@ -2,7 +2,11 @@ import { useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { toast } from 'react-toastify'; -import { useGetIndividualTaskQuery, useGetTaskWaypointQuery } from '@Api/tasks'; +import { + useGetIndividualTaskQuery, + useGetTaskAssetsInfo, + useGetTaskWaypointQuery, +} from '@Api/tasks'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { postProcessImagery } from '@Services/tasks'; import { formatString } from '@Utils/index'; @@ -12,7 +16,7 @@ import SwitchTab from '@Components/common/SwitchTab'; import { resetFilesExifData, setSelectedTaskDetailToViewOrthophoto, - setTaskAssetsInformation, + // setTaskAssetsInformation, setUploadedImagesType, } from '@Store/actions/droneOperatorTask'; import { useTypedSelector } from '@Store/hooks'; @@ -51,6 +55,14 @@ const DescriptionBox = () => { }, ); + const { + data: taskAssetsInformation, + // isFetching: taskAssetsInfoLoading, + }: Record = useGetTaskAssetsInfo( + projectId as string, + taskId as string, + ); + const { mutate: updateStatus, isLoading: statusUpdating } = useMutation< any, any, @@ -169,27 +181,27 @@ const DescriptionBox = () => { }, ], }, - { - total_image_uploaded: taskData?.total_image_uploaded || 0, - assets_url: taskData?.assets_url, - state: taskData?.state, - }, + // { + // total_image_uploaded: taskData?.total_image_uploaded || 0, + // assets_url: taskData?.assets_url, + // state: taskData?.state, + // }, ]; }, }); - const taskAssetsInformation = useMemo(() => { - if (!taskDescription) return {}; - dispatch(setTaskAssetsInformation(taskDescription?.[2])); - return taskDescription?.[2]; - }, [taskDescription, dispatch]); + // const taskAssetsInformation = useMemo(() => { + // if (!taskDescription) return {}; + // dispatch(setTaskAssetsInformation(taskDescription?.[2])); + // return taskDescription?.[2]; + // }, [taskDescription, dispatch]); const handleDownloadResult = () => { if (!taskAssetsInformation?.assets_url) return; try { const link = document.createElement('a'); link.href = taskAssetsInformation?.assets_url; - link.download = `${projectId}-${taskId}.tif`; + link.setAttribute('download', ''); document.body.appendChild(link); link.click(); link.remove(); @@ -215,20 +227,20 @@ const DescriptionBox = () => { ))} - {taskAssetsInformation?.total_image_uploaded === 0 && ( + {taskAssetsInformation?.image_count === 0 && ( )} - {taskAssetsInformation?.total_image_uploaded > 0 && ( + {taskAssetsInformation?.image_count > 0 && (
{ const newTakeOffPoint = useTypedSelector( state => state.droneOperatorTask.selectedTakeOffPoint, ); - const taskAssetsInformation = useTypedSelector( - state => state.droneOperatorTask.taskAssetsInformation, - ); + // const taskAssetsInformation = useTypedSelector( + // state => state.droneOperatorTask.taskAssetsInformation, + // ); const rotatedFlightPlanData = useTypedSelector( state => state.droneOperatorTask.rotatedFlightPlan, ); @@ -137,6 +141,14 @@ const MapSection = ({ className }: { className?: string }) => { }, ); + const { + data: taskAssetsInformation, + // isFetching: taskAssetsInfoLoading, + }: Record = useGetTaskAssetsInfo( + projectId as string, + taskId as string, + ); + const { mutate: postWaypoint, isLoading: isUpdatingTakeOffPoint } = useMutation({ mutationFn: postTaskWaypoint, diff --git a/src/frontend/src/components/IndividualProject/MapSection/index.tsx b/src/frontend/src/components/IndividualProject/MapSection/index.tsx index 3a3f1560..5ffd01cd 100644 --- a/src/frontend/src/components/IndividualProject/MapSection/index.tsx +++ b/src/frontend/src/components/IndividualProject/MapSection/index.tsx @@ -27,6 +27,8 @@ import { import { Button } from '@Components/RadixComponents/Button'; import ToolTip from '@Components/RadixComponents/ToolTip'; import Legend from './Legend'; +import ProjectPromptDialog from '../ModalContent'; +import UnlockTaskPromptDialog from '../ModalContent/UnlockTaskPromptDialog'; const MapSection = ({ projectData }: { projectData: Record }) => { const { id } = useParams(); @@ -39,6 +41,7 @@ const MapSection = ({ projectData }: { projectData: Record }) => { const [lockedUser, setLockedUser] = useState | null>( null, ); + const [showUnlockDialog, setShowUnlockDialog] = useState(false); const [showOverallOrthophoto, setShowOverallOrthophoto] = useState(false); const { data: userDetails }: Record = useGetUserDetailsQuery(); @@ -210,197 +213,213 @@ const MapSection = ({ projectData }: { projectData: Record }) => { }; return ( - - - - {projectArea && ( - - )} - - {projectData?.no_fly_zones_geojson && ( - - )} + <> + + + + {projectArea && ( + + )} - {taskStatusObj && - tasksData && - tasksData?.map((task: Record) => { - return ( - - ); - })} + {projectData?.no_fly_zones_geojson && ( + + )} - {/* visualize tasks orthophoto */} - {visibleTaskOrthophoto?.map(orthophotoDetails => ( - - ))} - {/* visualize tasks orthophoto end */} + {taskStatusObj && + tasksData && + tasksData?.map((task: Record) => { + return ( + + ); + })} - {/* visualize overall project orthophoto */} - {projectData?.orthophoto_url && ( - - )} - {/* visualize tasks orthophoto end */} + {/* visualize tasks orthophoto */} + {visibleTaskOrthophoto?.map(orthophotoDetails => ( + + ))} + {/* visualize tasks orthophoto end */} - {/* additional controls */} -
+ {/* visualize overall project orthophoto */} {projectData?.orthophoto_url && ( - + )} -
- {/* additional controls */} + {/* visualize tasks orthophoto end */} - ) => { - if (!userDetails) return false; + {/* additional controls */} +
+ {projectData?.orthophoto_url && ( + + )} +
+ {/* additional controls */} - return ( - feature?.source?.includes('tasks-layer') && - !( - ( - (userDetails?.role?.length === 1 && - userDetails?.role?.includes('REGULATOR')) || - signedInAs === 'REGULATOR' - ) // Don't show popup if user role is regulator any and no other roles + ) => { + if (!userDetails) return false; + + return ( + feature?.source?.includes('tasks-layer') && + !( + ( + (userDetails?.role?.length === 1 && + userDetails?.role?.includes('REGULATOR')) || + signedInAs === 'REGULATOR' + ) // Don't show popup if user role is regulator any and no other roles + ) + ); + }} + fetchPopupData={(properties: Record) => { + dispatch( + setProjectState({ + taskClickedOnTable: null, + }), + ); + dispatch(setProjectState({ selectedTaskId: properties.id })); + setLockedUser({ + id: properties?.locked_user_id || userDetails?.id || '', + name: properties?.locked_user_name || userDetails?.name || '', + }); + }} + hideButton={ + !showPrimaryButton( + taskStatusObj?.[selectedTaskId], + lockedUser?.id, + userDetails?.id, + projectData?.author_id, + ) || + projectData?.regulator_approval_status === 'REJECTED' || // Don't task lock button if regulator rejected the approval + projectData?.regulator_approval_status === 'PENDING' + } + buttonText={ + taskStatusObj?.[selectedTaskId] === 'UNLOCKED_TO_MAP' || + !taskStatusObj?.[selectedTaskId] + ? 'Lock Task' + : 'Go To Task' + } + handleBtnClick={() => + taskStatusObj?.[selectedTaskId] === 'UNLOCKED_TO_MAP' + ? handleTaskLockClick() + : navigate(`/projects/${id}/tasks/${selectedTaskId}`) + } + hasSecondaryButton={ + taskStatusObj?.[selectedTaskId] === 'LOCKED_FOR_MAPPING' && + (lockedUser?.id === userDetails?.id || + projectData?.author_id === userDetails?.id) // enable task unlock to the project author + } + secondaryButtonText="Unlock Task" + handleSecondaryBtnClick={() => setShowUnlockDialog(true)} + // trigger from popup outside + openPopupFor={ + projectData?.regulator_approval_status === 'REJECTED' // ignore click if the regulator rejected the approval + ? null + : taskClickedOnTable + } + popupCoordinate={taskClickedOnTable?.centroidCoordinates} + onClose={() => + dispatch( + setProjectState({ + taskClickedOnTable: null, + }), ) - ); - }} - fetchPopupData={(properties: Record) => { - dispatch( - setProjectState({ - taskClickedOnTable: null, - }), - ); - dispatch(setProjectState({ selectedTaskId: properties.id })); - setLockedUser({ - id: properties?.locked_user_id || userDetails?.id || '', - name: properties?.locked_user_name || userDetails?.name || '', - }); - }} - hideButton={ - !showPrimaryButton( - taskStatusObj?.[selectedTaskId], - lockedUser?.id, - userDetails?.id, - projectData?.author_id, - ) || - projectData?.regulator_approval_status === 'REJECTED' || // Don't task lock button if regulator rejected the approval - projectData?.regulator_approval_status === 'PENDING' - } - buttonText={ - taskStatusObj?.[selectedTaskId] === 'UNLOCKED_TO_MAP' || - !taskStatusObj?.[selectedTaskId] - ? 'Lock Task' - : 'Go To Task' - } - handleBtnClick={() => - taskStatusObj?.[selectedTaskId] === 'UNLOCKED_TO_MAP' - ? handleTaskLockClick() - : navigate(`/projects/${id}/tasks/${selectedTaskId}`) - } - hasSecondaryButton={ - taskStatusObj?.[selectedTaskId] === 'LOCKED_FOR_MAPPING' && - lockedUser?.id === userDetails?.id - } - secondaryButtonText="Unlock Task" - handleSecondaryBtnClick={() => handleTaskUnLockClick()} - // trigger from popup outside - openPopupFor={ - projectData?.regulator_approval_status === 'REJECTED' // ignore click if the regulator rejected the approval - ? null - : taskClickedOnTable - } - popupCoordinate={taskClickedOnTable?.centroidCoordinates} - onClose={() => - dispatch( - setProjectState({ - taskClickedOnTable: null, - }), - ) - } - /> - -
+ } + /> + +
+ + setShowUnlockDialog(false)} + > + + + ); }; diff --git a/src/frontend/src/components/IndividualProject/ModalContent/DeleteProjectConfirmation.tsx b/src/frontend/src/components/IndividualProject/ModalContent/DeleteProjectConfirmation.tsx new file mode 100644 index 00000000..3861dae0 --- /dev/null +++ b/src/frontend/src/components/IndividualProject/ModalContent/DeleteProjectConfirmation.tsx @@ -0,0 +1,70 @@ +import Input from '@Components/common/FormUI/Input'; +import { Button } from '@Components/RadixComponents/Button'; +import { useState } from 'react'; + +interface IDeleteProjectProps { + isLoading: boolean; + projectName: string; + handleDeleteProject: () => void; + setShowUnlockDialog: React.Dispatch>; +} + +const DeleteProjectPromptDialog = ({ + isLoading, + projectName, + handleDeleteProject, + setShowUnlockDialog, +}: IDeleteProjectProps) => { + const [value, setValue] = useState(''); + const [error, setError] = useState(''); + + const deleteProject = () => { + if ( + projectName?.trim()?.toLowerCase() === value.trim()?.toLocaleLowerCase() + ) { + handleDeleteProject(); + setShowUnlockDialog(false); + } else { + setError('Invalid Project Name'); + } + }; + + return ( +
+
+ Enter Project Name to Delete the Project +
+ { + setError(''); + setValue(e.target.value); + }} + /> + + {error} + +
+ + +
+
+ ); +}; + +export default DeleteProjectPromptDialog; diff --git a/src/frontend/src/components/IndividualProject/ModalContent/UnlockTaskPromptDialog.tsx b/src/frontend/src/components/IndividualProject/ModalContent/UnlockTaskPromptDialog.tsx new file mode 100644 index 00000000..87095b8f --- /dev/null +++ b/src/frontend/src/components/IndividualProject/ModalContent/UnlockTaskPromptDialog.tsx @@ -0,0 +1,38 @@ +import { Button } from '@Components/RadixComponents/Button'; + +interface IUnlockTaskPromptDialogProps { + handleUnlockTask: () => void; + setShowUnlockDialog: React.Dispatch>; +} + +const UnlockTaskPromptDialog = ({ + handleUnlockTask, + setShowUnlockDialog, +}: IUnlockTaskPromptDialogProps) => { + return ( +
+
+ Are you sure you want to unlock the task? +
+
+ + +
+
+ ); +}; + +export default UnlockTaskPromptDialog; diff --git a/src/frontend/src/components/IndividualProject/ModalContent/index.tsx b/src/frontend/src/components/IndividualProject/ModalContent/index.tsx new file mode 100644 index 00000000..751eeeab --- /dev/null +++ b/src/frontend/src/components/IndividualProject/ModalContent/index.tsx @@ -0,0 +1,24 @@ +import Modal from '@Components/common/Modal'; +import { MouseEventHandler, ReactNode } from 'react'; + +interface IPromptDialogProps { + title: string; + show: boolean; + onClose: MouseEventHandler; + children: ReactNode; +} + +export function ProjectPromptDialog({ + title = '', + show = false, + onClose = () => {}, + children, +}: IPromptDialogProps) { + return ( + + {children} + + ); +} + +export default ProjectPromptDialog; diff --git a/src/frontend/src/components/LandingPage/MobileAppDownload/index.tsx b/src/frontend/src/components/LandingPage/MobileAppDownload/index.tsx index 243ab12e..59079313 100644 --- a/src/frontend/src/components/LandingPage/MobileAppDownload/index.tsx +++ b/src/frontend/src/components/LandingPage/MobileAppDownload/index.tsx @@ -7,7 +7,7 @@ const MobileAppDownload = () => { return (
-
+
DTM-logo {/*

About

FAQs

*/} + + Tutorials + + -

Documentations

+

Documentations

diff --git a/src/frontend/src/components/RegulatorsApprovalPage/Description/DescriptionSection.tsx b/src/frontend/src/components/RegulatorsApprovalPage/Description/DescriptionSection.tsx index 89634385..55e0d0bb 100644 --- a/src/frontend/src/components/RegulatorsApprovalPage/Description/DescriptionSection.tsx +++ b/src/frontend/src/components/RegulatorsApprovalPage/Description/DescriptionSection.tsx @@ -1,6 +1,9 @@ /* eslint-disable no-nested-ternary */ +import { useMemo } from 'react'; +import { toast } from 'react-toastify'; import { useDispatch } from 'react-redux'; import { Button } from '@Components/RadixComponents/Button'; +import { descriptionItems } from '@Constants/projectDescription'; import { toggleModal } from '@Store/actions/common'; import ApprovalSection from './ApprovalSection'; @@ -13,6 +16,27 @@ const DescriptionSection = ({ }) => { const dispatch = useDispatch(); + // know if any of the task is completed (assets_url) is the key that provides the final results of a task + const isAbleToStartProcessing = useMemo( + () => + projectData?.tasks?.some((task: Record) => task?.assets_url), + [projectData?.tasks], + ); + + const handleDownloadResult = () => { + if (!projectData?.assets_url) return; + try { + const link = document.createElement('a'); + link.href = projectData?.assets_url; + link.setAttribute('download', ''); + document.body.appendChild(link); + link.click(); + link.remove(); + } catch (error) { + toast.error(`There wan an error while downloading file ${error}`); + } + }; + return (
{page === 'project-approval' && ( @@ -23,92 +47,94 @@ const DescriptionSection = ({

{projectData?.description || ''}

-
-

Total Project Area

-

:

-

- {projectData?.project_area?.toFixed(3)?.replace(/\.00$/, '') || - ''}{' '} - km² -

-
-
-

Project Created By

-

:

{' '} -

- {projectData?.author_name || ''} -

-
-
-

Total Tasks

-

:

{' '} -

- {projectData?.tasks?.length || ''} -

-
- {projectData?.regulator_comment && ( -
-

Local Regulator Comment

-

:

{' '} -

- {projectData?.regulator_comment || ''} -

-
- )} - {projectData?.regulator_approval_status && ( -
-

- Local Regulator Approval Status -

-

:

{' '} -

- {projectData?.regulator_approval_status || ''} -

-
- )} + {descriptionItems.map(descriptionItem => { + if ( + projectData?.[descriptionItem.key] || + descriptionItem.expectedDataType === 'boolean' + ) { + const dataType = descriptionItem.expectedDataType; + const value = projectData?.[descriptionItem.key]; + const unite = descriptionItem?.unite || ''; + return ( +
+

{descriptionItem.label}

+

:

+

+ {dataType === 'boolean' + ? value + ? 'Yes' + : 'No' + : dataType === 'double' + ? value.toFixed(3)?.replace(/\.00$/, '') || '' + : dataType === 'array' + ? value?.length + : value}{' '} + {unite} +

+
+ ); + } + return <>; + })}
{page !== 'project-approval' && - projectData?.image_processing_status === 'NOT_STARTED' ? ( -
- -
- ) : projectData?.image_processing_status === 'SUCCESS' ? ( - <> - ) : projectData?.image_processing_status === 'PROCESSING' ? ( -
- -
- ) : ( -
- -
- )} + (!projectData?.requires_approval_from_regulator || + projectData?.regulator_approval_status === 'APPROVED') && + isAbleToStartProcessing && ( + <> + {projectData?.image_processing_status === 'NOT_STARTED' ? ( +
+ +
+ ) : projectData?.image_processing_status === 'SUCCESS' ? ( + + ) : projectData?.image_processing_status === 'PROCESSING' ? ( +
+ +
+ ) : ( +
+ +
+ )} + + )} {page === 'project-approval' && projectData?.regulator_approval_status === 'PENDING' && ( diff --git a/src/frontend/src/components/Tutorials/VideoTutorials/VideoCards/index.tsx b/src/frontend/src/components/Tutorials/VideoTutorials/VideoCards/index.tsx new file mode 100644 index 00000000..a9243b1f --- /dev/null +++ b/src/frontend/src/components/Tutorials/VideoTutorials/VideoCards/index.tsx @@ -0,0 +1,90 @@ +/* eslint-disable jsx-a11y/media-has-caption */ +import Icon from '@Components/common/Icon'; +import { FlexColumn, FlexRow } from '@Components/common/Layouts'; +import { useState } from 'react'; + +export interface IVideoCardProps { + title: string; + onClick: () => void; + thumbnail?: string; +} + +export const RowVideoCards = ({ + title, + onClick, + thumbnail, +}: IVideoCardProps) => { + const [hover, setHover] = useState(false); + + return ( + <> + setHover(true)} + onMouseLeave={() => setHover(false)} + className="naxatw-h-fit naxatw-w-full naxatw-cursor-pointer naxatw-items-stretch naxatw-justify-center naxatw-gap-4 naxatw-px-4" + onClick={() => onClick()} + > +
+
+ Thumbnail + +
+ +
+

+ {title} +

+
+
+
+
+ + ); +}; + +export const ColumnVideoCards = ({ + title, + onClick, + thumbnail, +}: IVideoCardProps) => { + const [hover, setHover] = useState(false); + + return ( + <> + setHover(true)} + onMouseLeave={() => setHover(false)} + className="naxatw-group naxatw-relative naxatw-w-full naxatw-flex-1 naxatw-cursor-pointer naxatw-items-start naxatw-gap-4 naxatw-overflow-hidden naxatw-rounded-lg naxatw-border naxatw-bg-white naxatw-shadow-sm hover:naxatw-shadow-lg sm:naxatw-h-full sm:naxatw-min-h-[15rem]" + onClick={() => onClick()} + > +
+ Thumbnail + +
+ +
+

+ {title} +

+
+
+
+ + ); +}; diff --git a/src/frontend/src/components/Tutorials/VideoTutorials/index.tsx b/src/frontend/src/components/Tutorials/VideoTutorials/index.tsx new file mode 100644 index 00000000..7fde7956 --- /dev/null +++ b/src/frontend/src/components/Tutorials/VideoTutorials/index.tsx @@ -0,0 +1,38 @@ +/* eslint-disable jsx-a11y/media-has-caption */ +import { FlexColumn } from '@Components/common/Layouts'; +import React, { useEffect, useRef } from 'react'; + +interface VideoPlayerProps { + src: string; + title: string; +} + +const VideoPlayer: React.FC = ({ src, title }) => { + const videoRef = useRef(null); + useEffect(() => { + if (videoRef.current) { + videoRef.current.play(); + } + }, [src]); + + return ( + +
+ +

+ {title} +

+
+
+
+ ); +}; + +export default VideoPlayer; diff --git a/src/frontend/src/constants/projectDescription.ts b/src/frontend/src/constants/projectDescription.ts index 94d1c163..c9ccb620 100644 --- a/src/frontend/src/constants/projectDescription.ts +++ b/src/frontend/src/constants/projectDescription.ts @@ -119,3 +119,60 @@ export const startProcessingOptions = [ name: 'start_processing', }, ]; + +export const descriptionItems = [ + { + label: 'Total Project Area', + key: 'project_area', + expectedDataType: 'double', + unite: 'km²', + }, + { + label: 'Total Tasks', + key: 'tasks', + expectedDataType: 'array', + }, + { + label: 'Project Created By', + key: 'author_name', + expectedDataType: 'string', + }, + { + label: 'Flight Altitude', + key: 'flight_altitude', + expectedDataType: 'number', + unite: 'm', + }, + { + label: 'Front Overlap', + key: 'front_overlap', + expectedDataType: 'double', + unit: '%', + }, + { + label: 'Side Overlap', + key: 'side_overlap', + expectedDataType: 'double', + unit: '%', + }, + { + label: 'Terrain Following', + key: 'has_terrain_flow', + expectedDataType: 'boolean', + }, + { + label: 'Require Approval to Lock Task', + key: 'requires_approval_from_manager_for_locking', + expectedDataType: 'boolean', + }, + { + label: 'Local Regulator Approval Status', + key: 'regulator_approval_status', + expectedDataType: 'string', + }, + { + label: 'Local Regulator Comment', + key: 'regulator_comment', + expectedDataType: 'string', + }, +]; diff --git a/src/frontend/src/constants/tutorials.ts b/src/frontend/src/constants/tutorials.ts new file mode 100644 index 00000000..4c7f5289 --- /dev/null +++ b/src/frontend/src/constants/tutorials.ts @@ -0,0 +1,75 @@ +import flightPlanLoadOnRc from '@Assets/images/tutorials/flight_plan_load_on_Rc.png'; +import flightPlanReplacementOnRc from '@Assets/images/tutorials/flight_plan_replacement_on_rc.png'; +import imageProcessing from '@Assets/images/tutorials/image_processing.png'; +import projectCreation from '@Assets/images/tutorials/project_creation.png'; +import signUpAndLogin from '@Assets/images/tutorials/sign_up_and_login.png'; +import viewingFinalOutput from '@Assets/images/tutorials/viewing_final_output.png'; +import workflowForDroneOperator from '@Assets/images/tutorials/work_flow_for_drone_operator.png'; + +export interface IVideoTutorialItems { + id: string; + videoUrl: string; + title: string; + description: string; + thumbnail: string; +} + +export const videoTutorialData: IVideoTutorialItems[] = [ + { + id: 'Sign+Up+and+Log+In', + videoUrl: + 'https://dronetm.s3.ap-south-1.amazonaws.com/dtm-data/tutorials/Sign+Up+and+Log+In.mp4', + title: 'How to Sign Up and Log In', + description: '', + thumbnail: signUpAndLogin, + }, + { + id: 'Project+Creation', + videoUrl: + 'https://dronetm.s3.ap-south-1.amazonaws.com/dtm-data/tutorials/Project+Creation.mp4', + title: 'How To Create Projects', + description: '', + thumbnail: projectCreation, + }, + { + id: 'Workflow+for+Drone+Operators', + videoUrl: + 'https://dronetm.s3.ap-south-1.amazonaws.com/dtm-data/tutorials/Workflow+for+Drone+Operators.mp4', + title: 'Workflow for Drone Operators', + description: '', + thumbnail: workflowForDroneOperator, + }, + { + id: 'Flight+Plan+Loading+on+RC', + videoUrl: + 'https://dronetm.s3.ap-south-1.amazonaws.com/dtm-data/tutorials/Flight+Plan+Loading+on+RC.mp4', + title: 'How to Load Flight Plan on RC of Drone and Start the Flight', + description: '', + thumbnail: flightPlanLoadOnRc, + }, + { + id: 'Flight+Plan+Replacement+on+RC', + videoUrl: + 'https://dronetm.s3.ap-south-1.amazonaws.com/dtm-data/tutorials/Flight+Plan+Replacement+on+RC.mp4', + title: 'How to Replace the Flight Plan on RC of Drone using Laptop', + description: '', + thumbnail: flightPlanReplacementOnRc, + }, + { + id: 'Image+Processing', + videoUrl: + 'https://dronetm.s3.ap-south-1.amazonaws.com/dtm-data/tutorials/Image+Processing.mp4', + title: 'How to Upload Raw images and Start Processing', + description: '', + thumbnail: imageProcessing, + }, + + { + id: 'Viewing+Final+Output', + videoUrl: + 'https://dronetm.s3.ap-south-1.amazonaws.com/dtm-data/tutorials/Viewing+Final+Output.mp4', + title: 'How to Visualize Final Output', + description: '', + thumbnail: viewingFinalOutput, + }, +]; diff --git a/src/frontend/src/routes/appRoutes.ts b/src/frontend/src/routes/appRoutes.ts index acecdee0..4d8ed5b2 100644 --- a/src/frontend/src/routes/appRoutes.ts +++ b/src/frontend/src/routes/appRoutes.ts @@ -9,6 +9,7 @@ import IndividualProject from '@Views/IndividualProject'; import TaskDescription from '@Views/TaskDescription'; import UpdateUserProfile from '@Views/UpdateUserProfile'; import RegulatorsApprovalPage from '@Views/RegulatorsApprovalPage'; +import Tutorials from '@Views/Tutorial'; import { IRoute } from './types'; const appRoutes: IRoute[] = [ @@ -19,6 +20,11 @@ const appRoutes: IRoute[] = [ component: LandingPage, authenticated: false, }, + { + path: 'tutorials', + name: 'tutorials', + component: Tutorials, + }, { path: '/projects', name: 'Projects ', diff --git a/src/frontend/src/services/project.ts b/src/frontend/src/services/project.ts index a7f423a0..2b1d0613 100644 --- a/src/frontend/src/services/project.ts +++ b/src/frontend/src/services/project.ts @@ -24,3 +24,6 @@ export const processAllImagery = (data: Record) => { } return authenticated(api).post(`/projects/process_all_imagery/${projectId}/`); }; + +export const deleteProject = (projectId: string) => + authenticated(api).delete(`/projects/${projectId}`); diff --git a/src/frontend/src/services/tasks.ts b/src/frontend/src/services/tasks.ts index 01be03d3..ed86f5b4 100644 --- a/src/frontend/src/services/tasks.ts +++ b/src/frontend/src/services/tasks.ts @@ -23,8 +23,8 @@ export const postTaskWaypoint = (payload: Record) => { }, ); }; -// export const getTaskAssetsInfo = (projectId: string, taskId: string) => -// authenticated(api).get(`/projects/assets/${projectId}/?task_id=${taskId}`); +export const getTaskAssetsInfo = (projectId: string, taskId: string) => + authenticated(api).get(`/projects/assets/${projectId}/?task_id=${taskId}`); export const postProcessImagery = (projectId: string, taskId: string) => authenticated(api).post(`/projects/process_imagery/${projectId}/${taskId}/`); diff --git a/src/frontend/src/views/IndividualProject/index.tsx b/src/frontend/src/views/IndividualProject/index.tsx index c2637635..2e753baa 100644 --- a/src/frontend/src/views/IndividualProject/index.tsx +++ b/src/frontend/src/views/IndividualProject/index.tsx @@ -1,5 +1,10 @@ /* eslint-disable jsx-a11y/interactive-supports-focus */ /* eslint-disable jsx-a11y/click-events-have-key-events */ +import { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import centroid from '@turf/centroid'; +import html2canvas from 'html2canvas'; import { useGetProjectsDetailQuery } from '@Api/projects'; import BreadCrumb from '@Components/common/Breadcrumb'; import Tab from '@Components/common/Tabs'; @@ -11,17 +16,17 @@ import { } from '@Components/IndividualProject'; import ExportSection from '@Components/IndividualProject/ExportSection'; import GcpEditor from '@Components/IndividualProject/GcpEditor'; +import ProjectPromptDialog from '@Components/IndividualProject/ModalContent'; +import DeleteProjectPromptDialog from '@Components/IndividualProject/ModalContent/DeleteProjectConfirmation'; import { Button } from '@Components/RadixComponents/Button'; import Skeleton from '@Components/RadixComponents/Skeleton'; import DescriptionSection from '@Components/RegulatorsApprovalPage/Description/DescriptionSection'; import { projectOptions } from '@Constants/index'; +import { deleteProject } from '@Services/project'; import { setProjectState } from '@Store/actions/project'; import { useTypedDispatch, useTypedSelector } from '@Store/hooks'; -import centroid from '@turf/centroid'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import hasErrorBoundary from '@Utils/hasErrorBoundary'; -import html2canvas from 'html2canvas'; -import { useEffect, useRef, useState } from 'react'; -import { useParams } from 'react-router-dom'; // eslint-disable-next-line camelcase const { BASE_URL } = process.env; @@ -62,9 +67,12 @@ const getActiveTabContent = ( const IndividualProject = () => { const { id } = useParams(); + const queryClient = useQueryClient(); + const navigate = useNavigate(); const dispatch = useTypedDispatch(); const exportRef = useRef(null); const [exportingContent, setExportingContent] = useState(false); + const [showProjectDeletePrompt, setShowProjectDeletePrompt] = useState(false); const individualProjectActiveTab = useTypedSelector( state => state.project.individualProjectActiveTab, @@ -97,6 +105,15 @@ const IndividualProject = () => { }, }); + const { mutate, isLoading } = useMutation({ + mutationFn: (projectId: string) => deleteProject(projectId), + onSuccess: () => { + queryClient.invalidateQueries(['projects-list']); + toast.error('Project Deleted Successfully'); + navigate('/projects'); + }, + }); + const handleTableRowClick = (taskData: any) => { const clickedTask = tasksList?.find( (task: Record) => taskData?.task_id === task?.id, @@ -123,6 +140,10 @@ const IndividualProject = () => { }; }, [dispatch]); + const handleDeleteProject = () => { + mutate(id as string); + }; + return ( <>
@@ -133,26 +154,30 @@ const IndividualProject = () => { { name: projectData?.name || '--', navLink: '' }, ]} /> - +
{showGcpEditor ? (
@@ -174,7 +199,7 @@ const IndividualProject = () => {
) : (
-
+
{ activeTab={individualProjectActiveTab} clickable /> -
+
{getActiveTabContent( individualProjectActiveTab, projectData as Record, @@ -198,6 +223,21 @@ const IndividualProject = () => { handleTableRowClick, )}
+
+ +
{isProjectDataFetching ? ( @@ -219,6 +259,19 @@ const IndividualProject = () => {
+ + setShowProjectDeletePrompt(false)} + > + + ); }; diff --git a/src/frontend/src/views/RegulatorsApprovalPage/index.tsx b/src/frontend/src/views/RegulatorsApprovalPage/index.tsx index 5364c990..ad9b0ff6 100644 --- a/src/frontend/src/views/RegulatorsApprovalPage/index.tsx +++ b/src/frontend/src/views/RegulatorsApprovalPage/index.tsx @@ -72,6 +72,7 @@ const RegulatorsApprovalPage = () => { useEffect(() => { return () => { localStorage.clear(); + window.location.reload(); }; }, []); diff --git a/src/frontend/src/views/Tutorial/index.tsx b/src/frontend/src/views/Tutorial/index.tsx new file mode 100644 index 00000000..812ddd34 --- /dev/null +++ b/src/frontend/src/views/Tutorial/index.tsx @@ -0,0 +1,210 @@ +/* eslint-disable no-nested-ternary */ +import { FlexColumn, FlexRow } from '@Components/common/Layouts'; +import { motion } from 'framer-motion'; +import { useSpring, animated } from '@react-spring/web'; +import { useState } from 'react'; +import VideoPlayer from '@Components/Tutorials/VideoTutorials'; +import { + RowVideoCards, + ColumnVideoCards, +} from '@Components/Tutorials/VideoTutorials/VideoCards'; +import { IVideoTutorialItems, videoTutorialData } from '@Constants/tutorials'; +import { Link } from 'react-router-dom'; +import Icon from '@Components/common/Icon'; + +const Tutorials = () => { + const [isVideoBoxVisible, setIsVideoBoxVisible] = useState(false); + const [currentVideo, setCurrentVideo] = useState( + null, + ); + + const springs = useSpring({ + from: { y: 100 }, + to: { y: 0 }, + }); + + const videoCardVariants = { + show: { + opacity: 1, + x: 0, + transition: { + duration: 1, // Slower animation + staggerChildren: 0.2, // Delay for each child to create row-to-column effect + delayChildren: 0.1, // Adds a slight delay before the first child animates + }, + }, + hidden: { + opacity: 0, + x: '-100%', + transition: { duration: 1 }, + }, + }; + + const childVariants = { + show: { + opacity: 1, + x: 0, + y: 0, + transition: { duration: 0.25 }, + }, + hidden: { + opacity: 0, + x: '-50%', + y: '50%', + transition: { duration: 0.25 }, + }, + }; + const videoColumnCardVariants = { + show: { + opacity: 1, + y: 0, + transition: { + duration: 1, // Slower animation + staggerChildren: 0.2, // Delay for each child to create row-to-column effect + delayChildren: 0.1, // Adds a slight delay before the first child animates + }, + }, + hidden: { + opacity: 0, + y: '-100%', + transition: { duration: 1 }, + }, + }; + + const columnChildVariants = { + show: { + opacity: 1, + x: 0, + y: 0, + transition: { duration: 0.25 }, + }, + hidden: { + opacity: 0, + x: '50%', + y: '-50%', + transition: { duration: 0.25 }, + }, + }; + + const videoBoxVariants = { + show: { + opacity: 1, + y: 0, + transition: { duration: 0.75, ease: 'easeOut' }, // Slowed down to 1.5 seconds + }, + hidden: { + opacity: 0, + y: '100%', + transition: { duration: 0.75, ease: 'easeIn' }, // Matching duration for hiding + }, + }; + + return ( + + {isVideoBoxVisible ? ( +
+
+ + { + setIsVideoBoxVisible(false); + setCurrentVideo(null); + }} + /> + +

+ Video Tutorial +

+
+
1 ? 'lg:naxatw-grid-cols-[1fr_30rem]' : 'naxatw-w-full'}`} + > + + {currentVideo && ( + + )} + + + + {videoTutorialData.map((video: IVideoTutorialItems) => { + if (video.id === currentVideo?.id) return null; + return ( + + { + setCurrentVideo(video); + setIsVideoBoxVisible(true); + }} + /> + + ); + })} + + +
+
+
+ ) : ( + <> +
+
+ + + + + +

+ Video Tutorial +

+
+ + +
+ {videoTutorialData?.map((video: IVideoTutorialItems) => ( + + { + setCurrentVideo(video); + setIsVideoBoxVisible(true); + }} + /> + + ))} +
+
+
+
+
+ + )} +
+ ); +}; + +export default Tutorials;