Skip to content

Commit

Permalink
feat(frontend): visualise submission photos via slider (#1857)
Browse files Browse the repository at this point in the history
* feat(submission): submission photos api call and slice add

* feat(assetModules): icons add

* fix(modal): fix close btn styling

* feat(imageSlider): image slider component add

* fix(submissionDetails): add imageSlider to visualize uploaded submission photos

* fix(submissionDetails): add images heading
  • Loading branch information
NSUWAL123 authored Nov 11, 2024
1 parent ae6ed15 commit f629087
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 4 deletions.
18 changes: 18 additions & 0 deletions src/frontend/src/api/Submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,21 @@ export const SubmissionService: Function = (url: string) => {
await getSubmissionDetails(url);
};
};

export const GetSubmissionPhotosService: Function = (url: string) => {
return async (dispatch) => {
dispatch(SubmissionActions.SetSubmissionPhotosLoading(true));
const getSubmissionPhotos = async (url: string) => {
try {
const response = await axios.get(url);
dispatch(SubmissionActions.SetSubmissionPhotos(response?.data?.image_urls));
dispatch(SubmissionActions.SetSubmissionPhotosLoading(false));
} catch (error) {
dispatch(SubmissionActions.SetSubmissionPhotosLoading(false));
} finally {
dispatch(SubmissionActions.SetSubmissionPhotosLoading(false));
}
};
await getSubmissionPhotos(url);
};
};
109 changes: 109 additions & 0 deletions src/frontend/src/components/common/ImageSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useEffect, useRef, useState } from 'react';
import AssetModules from '@/shared/AssetModules';
import { Dialog, DialogContent } from '@/components/common/Modal';

type ImageSliderProps = {
images: string[];
};

const ImageSlider = ({ images }: ImageSliderProps) => {
const scrollContainerRef = useRef<HTMLDivElement>(null);

const [isOverflowing, setIsOverflowing] = useState(false);
const [showRightButton, setShowRightButton] = useState(false);
const [showLeftButton, setShowLeftButton] = useState(false);
const [selectedImageURL, setSelectedImageURL] = useState<string | null>(null);
const [translateImage, setTranslateImage] = useState(0);

useEffect(() => {
if (scrollContainerRef.current) {
const container = scrollContainerRef.current;
setIsOverflowing(container.scrollWidth > container.clientWidth);
}
}, [images]);

const handleScroll = () => {
if (scrollContainerRef.current) {
const container = scrollContainerRef.current;
const atStart = container.scrollLeft === 0;
const atEnd = container.scrollLeft + container.clientWidth >= container.scrollWidth - 1;
setShowLeftButton(!atStart);
setShowRightButton(!atEnd);
}
};

return (
<>
<Dialog
open={selectedImageURL ? true : false}
onOpenChange={(status) => {
if (!status) {
setSelectedImageURL(null);
setTranslateImage(0);
}
}}
>
<DialogContent className="!fmtm-bg-transparent fmtm-border-none fmtm-shadow-none fmtm-h-[100vh] fmtm-w-[100vw]">
<div className="fmtm-z-50 fmtm-relative fmtm-overflow-hidden fmtm-p-4 fmtm-w-full fmtm-flex fmtm-justify-center">
<img
src={selectedImageURL || ''}
alt="submission"
className="fmtm-max-w-[95%] fmtm-max-h-[80vh] fmtm-object-cover"
style={{
transform: `rotate(${translateImage}deg)`,
}}
/>
</div>
<div className="fmtm-flex fmtm-items-center fmtm-gap-4 fmtm-px-4 fmtm-py-2 fmtm-absolute fmtm-bottom-5 fmtm-w-fit fmtm-bg-black fmtm-bg-opacity-30 fmtm-rounded-md fmtm-z-50 fmtm-left-[50%] fmtm-translate-x-[-50%]">
<AssetModules.RotateLeftIcon
className="fmtm-text-white fmtm-cursor-pointer hover:fmtm-scale-110 fmtm-duration-150"
onClick={() => setTranslateImage(translateImage - 90)}
/>
<AssetModules.RotateRightIcon
className="fmtm-text-white fmtm-cursor-pointer hover:fmtm-scale-110 fmtm-duration-150"
onClick={() => setTranslateImage(translateImage + 90)}
/>
</div>
</DialogContent>
</Dialog>
<div
className="fmtm-flex fmtm-gap-x-3 fmtm-w-full fmtm-overflow-x-scroll scrollbar fmtm-relative"
ref={scrollContainerRef}
onScroll={handleScroll}
>
{showLeftButton && (
<button
className={`fmtm-sticky fmtm-left-2 fmtm-my-auto fmtm-z-50 fmtm-w-fit fmtm-p-1 fmtm-rounded-full fmtm-bg-black fmtm-bg-opacity-50 fmtm-h-fit hover:fmtm-scale-110 fmtm-cursor-pointer fmtm-duration-300`}
onClick={() => {
scrollContainerRef?.current?.scrollBy({ left: -300, behavior: 'smooth' });
}}
>
<AssetModules.ChevronLeftIcon className="fmtm-text-white" />
</button>
)}
{images?.map((imageUrl, index) => (
<div
key={index}
onClick={() => setSelectedImageURL(imageUrl)}
className="fmtm-h-[10.313rem] fmtm-w-[9.688rem] fmtm-min-w-[9.688rem] fmtm-rounded-lg fmtm-overflow-hidden fmtm-cursor-pointer"
>
<img src={imageUrl} alt="submission image" className="fmtm-h-full fmtm-w-full fmtm-object-cover" />
</div>
))}

{((isOverflowing && showRightButton) || (isOverflowing && scrollContainerRef?.current?.scrollLeft === 0)) && (
<button
className={`fmtm-sticky fmtm-right-2 fmtm-my-auto fmtm-z-50 fmtm-w-fit fmtm-p-1 fmtm-rounded-full fmtm-bg-black fmtm-bg-opacity-50 fmtm-h-fit hover:fmtm-scale-110 fmtm-cursor-pointer fmtm-duration-300`}
onClick={() => {
scrollContainerRef?.current?.scrollBy({ left: 300, behavior: 'smooth' });
}}
>
<AssetModules.ChevronRightIcon className="fmtm-text-white" />
</button>
)}
</div>
</>
);
};

export default ImageSlider;
4 changes: 2 additions & 2 deletions src/frontend/src/components/common/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ const DialogContent = ({
{...props}
>
{children}
<DialogPrimitive.Close className="fmtm-absolute fmtm-right-4 fmtm-top-4 fmtm-rounded-sm fmtm-opacity-70 fmtm-ring-offset-black fmtm-transition-opacity hover:fmtm-opacity-100 focus:fmtm-outline-none disabled:fmtm-pointer-events-none data-[state=open]:fmtm-bg-white data-[state=open]:fmtm-text-black fmtm-w-fit">
<X className="fmtm-h-4 fmtm-w-4" />
<DialogPrimitive.Close className="fmtm-absolute fmtm-z-50 fmtm-right-4 fmtm-top-4 fmtm-rounded-sm fmtm-opacity-70 fmtm-ring-offset-black fmtm-transition-opacity hover:fmtm-opacity-100 focus:fmtm-outline-none disabled:fmtm-pointer-events-none data-[state=open]:fmtm-bg-white data-[state=open]:fmtm-text-black fmtm-w-fit">
<X className="fmtm-h-6 fmtm-w-6 fmtm-text-black fmtm-cursor-pointer hover:fmtm-scale-110 fmtm-duration-150 fmtm-bg-white fmtm-rounded-full fmtm-p-1" />
<span className="fmtm-sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/src/shared/AssetModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ import {
QrCode2Outlined as QrCode2OutlinedIcon,
BarChart as BarChartIcon,
CalendarTodayOutlined as CalendarTodayOutlinedIcon,
ChevronRight as ChevronRightIcon,
ChevronLeft as ChevronLeftIcon,
RotateLeft as RotateLeftIcon,
RotateRight as RotateRightIcon,
} from '@mui/icons-material';
import LockPng from '@/assets/images/lock.png';
import RedLockPng from '@/assets/images/red-lock.png';
Expand Down Expand Up @@ -182,4 +186,8 @@ export default {
QrCode2OutlinedIcon,
BarChartIcon,
CalendarTodayOutlinedIcon,
ChevronRightIcon,
ChevronLeftIcon,
RotateLeftIcon,
RotateRightIcon,
};
8 changes: 8 additions & 0 deletions src/frontend/src/store/slices/SubmissionSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const initialState: SubmissionStateTypes = {
updateReviewStateLoading: false,
mappedVsValidatedTask: [],
mappedVsValidatedTaskLoading: false,
submissionPhotos: [],
submissionPhotosLoading: false,
};

const SubmissionSlice = createSlice({
Expand Down Expand Up @@ -106,6 +108,12 @@ const SubmissionSlice = createSlice({
SetMappedVsValidatedTaskLoading(state, action) {
state.mappedVsValidatedTaskLoading = action.payload;
},
SetSubmissionPhotos(state, action) {
state.submissionPhotos = action.payload;
},
SetSubmissionPhotosLoading(state, action) {
state.submissionPhotosLoading = action.payload;
},
},
});

Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/store/types/ISubmissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type SubmissionStateTypes = {
updateReviewStateLoading: boolean;
mappedVsValidatedTask: mappedVsValidatedTaskType[];
mappedVsValidatedTaskLoading: boolean;
submissionPhotos: string[];
submissionPhotosLoading: boolean;
};

type updateReviewStatusModal = {
Expand Down
30 changes: 28 additions & 2 deletions src/frontend/src/views/SubmissionDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import CoreModules from '@/shared/CoreModules.js';
import React, { useEffect } from 'react';
import { SubmissionService } from '@/api/Submission';
import { SubmissionService, GetSubmissionPhotosService } from '@/api/Submission';
import SubmissionInstanceMap from '@/components/SubmissionMap/SubmissionInstanceMap';
import { GetSubmissionDashboard } from '@/api/Project';
import Button from '@/components/common/Button';
Expand All @@ -12,6 +12,7 @@ import useDocumentTitle from '@/utilfunctions/useDocumentTitle';
import Accordion from '@/components/common/Accordion';
import { GetProjectComments } from '@/api/Project';
import SubmissionComments from '@/components/SubmissionInstance/SubmissionComments';
import ImageSlider from '@/components/common/ImageSlider';

const renderValue = (value: any, key: string = '') => {
if (key === 'start' || key === 'end') {
Expand Down Expand Up @@ -95,6 +96,8 @@ const SubmissionDetails = () => {
const submissionDetails = useAppSelector((state) => state.submission.submissionDetails);
const submissionDetailsLoading = useAppSelector((state) => state.submission.submissionDetailsLoading);
const taskId = submissionDetails?.task_id ? submissionDetails?.task_id : '-';
const submissionPhotosLoading = useAppSelector((state) => state.submission.submissionPhotosLoading);
const submissionPhotos = useAppSelector((state) => state.submission.submissionPhotos);

const { start, end, today, deviceid, ...restSubmissionDetails } = submissionDetails || {};
const dateDeviceDetails = { start, end, today, deviceid };
Expand All @@ -119,6 +122,12 @@ const SubmissionDetails = () => {
);
}, [taskUid]);

useEffect(() => {
if (paramsInstanceId) {
dispatch(GetSubmissionPhotosService(`${import.meta.env.VITE_API_URL}/submission/${paramsInstanceId}/photos`));
}
}, [paramsInstanceId]);

const filteredData = restSubmissionDetails ? removeNullValues(restSubmissionDetails) : {};

const coordinatesArray: [number, number][] = restSubmissionDetails?.xlocation?.split(';').map(function (
Expand Down Expand Up @@ -263,7 +272,7 @@ const SubmissionDetails = () => {
</div>
</div>
</div>
<div className="fmtm-grid fmtm-grid-cols-1 md:fmtm-grid-cols-2 fmtm-gap-x-8 fmtm-mt-10 fmtm-gap-y-10">
<div className="fmtm-grid fmtm-grid-cols-1 md:fmtm-grid-cols-2 fmtm-gap-x-8 fmtm-mt-10 fmtm-gap-y-10 fmtm-mb-5">
{submissionDetailsLoading ? (
<div className="fmtm-flex fmtm-flex-col fmtm-gap-3 fmtm-mt-5">
{Array.from({ length: 8 }).map((_, i) => (
Expand All @@ -285,6 +294,23 @@ const SubmissionDetails = () => {
<SubmissionComments />
</div>
</div>
{/* submission photos */}
{submissionPhotosLoading ? (
<div className="fmtm-flex fmtm-gap-x-3 fmtm-overflow-x-scroll scrollbar fmtm-bg-white fmtm-p-6 fmtm-rounded-xl">
{Array.from({ length: 5 }).map((_, i) => (
<CoreModules.Skeleton
key={i}
style={{ width: '9.688rem', height: '10.313rem' }}
className="!fmtm-rounded-lg"
/>
))}
</div>
) : submissionPhotos?.length > 0 ? (
<div className="fmtm-bg-white fmtm-rounded-xl fmtm-p-6">
<p className="fmtm-text-base fmtm-font-bold fmtm-text-[#555] fmtm-mb-2">Images</p>
<ImageSlider images={submissionPhotos || []} />
</div>
) : null}
</div>
</>
);
Expand Down

0 comments on commit f629087

Please sign in to comment.