From 6c6b503546b085e7caa9c33dc32480e89f1db44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Fri, 8 Dec 2023 11:38:28 +0100 Subject: [PATCH] feat: add render layer of overview image --- .../FunctionalArea/Modal/Modal.component.tsx | 4 +- .../OverviewImageModal.types.ts | 9 ++++ .../OverviewImagesModal.component.tsx | 24 +++++++++- .../utils/useOverviewImage.ts | 24 ++++++++++ .../utils/useOverviewImageSize.ts | 48 +++++++++++++++++++ .../utils/useOverviewImageUrl.ts | 13 +++++ .../MapAdditionalOptions.component.tsx | 7 ++- src/constants/project.ts | 9 ++++ src/models/project.ts | 1 + src/redux/map/map.constants.ts | 2 + src/redux/modal/modal.constants.ts | 4 ++ src/redux/modal/modal.mock.ts | 4 ++ src/redux/modal/modal.reducers.ts | 8 +++- src/redux/modal/modal.selector.ts | 6 +++ src/redux/modal/modal.slice.ts | 6 +-- src/redux/modal/modal.types.ts | 5 ++ src/redux/project/project.selectors.ts | 22 +++++++++ yarn.lock | 6 +-- 18 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.ts create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts create mode 100644 src/constants/project.ts diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx index 1a0f2f38..4ca533ed 100644 --- a/src/components/FunctionalArea/Modal/Modal.component.tsx +++ b/src/components/FunctionalArea/Modal/Modal.component.tsx @@ -24,14 +24,14 @@ export const Modal = (): React.ReactNode => { role={MODAL_ROLE} > <div className="flex h-full w-full items-center justify-center"> - <div className="flex flex-col overflow-hidden rounded-lg "> + <div className="flex h-5/6 w-10/12 flex-col overflow-hidden rounded-lg"> <div className="flex items-center justify-between bg-white p-[24px] text-xl"> <div>{modalTitle}</div> <button type="button" onClick={handleCloseModal} aria-label="close button"> <Icon name="close" className="fill-font-500" /> </button> </div> - <div>{isOpen && modalName === 'overview-images' && <OverviewImagesModal />}</div> + {isOpen && modalName === 'overview-images' && <OverviewImagesModal />} </div> </div> </div> diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts new file mode 100644 index 00000000..ec227bb3 --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImageModal.types.ts @@ -0,0 +1,9 @@ +export interface OverviewImageSize { + width: number; + height: number; +} + +export interface ImageContainerSize { + width: number; + height: number; +} diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx index 29c97f24..927b3805 100644 --- a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx @@ -1,5 +1,27 @@ +/* eslint-disable @next/next/no-img-element */ import * as React from 'react'; +import { useCallback, useState } from 'react'; +import { useOverviewImage } from './utils/useOverviewImage'; export const OverviewImagesModal: React.FC = () => { - return <div className="h-[200px] w-[500px] bg-white " />; + const [containerRect, setContainerRect] = useState<DOMRect>(); + const { imageUrl, size } = useOverviewImage({ containerRect }); + const { width, height } = size; + + const handleRect = useCallback((node: HTMLDivElement | null) => { + if (!node) { + return; + } + + setContainerRect(node.getBoundingClientRect()); + }, []); + + return ( + <div className="flex h-full w-full items-center justify-center bg-white" ref={handleRect}> + <div className="relative" style={{ width, height }}> + <img alt="overview" className="block h-full w-full" src={imageUrl} /> + {/* TODO: interactions - clickable elements (in next task) */} + </div> + </div> + ); }; diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts new file mode 100644 index 00000000..db956acd --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImage.ts @@ -0,0 +1,24 @@ +import { OverviewImageSize } from '../OverviewImageModal.types'; +import { useOverviewImageSize } from './useOverviewImageSize'; +import { useOverviewImageUrl } from './useOverviewImageUrl'; + +interface UseOverviewImageArgs { + containerRect?: DOMRect; +} + +interface UseOverviewImageResults { + imageUrl: string; + size: OverviewImageSize; +} + +export const useOverviewImage = ({ + containerRect, +}: UseOverviewImageArgs): UseOverviewImageResults => { + const imageUrl = useOverviewImageUrl(); + const size = useOverviewImageSize({ containerRect }); + + return { + size, + imageUrl, + }; +}; diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.ts new file mode 100644 index 00000000..96015735 --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageSize.ts @@ -0,0 +1,48 @@ +import { DEFAULT_OVERVIEW_IMAGE_SIZE } from '@/constants/project'; +import { currentOverviewImageSelector } from '@/redux/project/project.selectors'; +import { useSelector } from 'react-redux'; +import { ImageContainerSize, OverviewImageSize } from '../OverviewImageModal.types'; + +interface UseOverviewImageArgs { + containerRect?: DOMRect; +} + +interface UseOverviewImageResult extends OverviewImageSize { + sizeFactor: number; +} + +const getFinalImageSize = ( + containerSize: ImageContainerSize, + maxImageSize: OverviewImageSize, +): UseOverviewImageResult => { + const maxHeight = Math.min(containerSize.height, maxImageSize.height); + const maxWidth = Math.min(containerSize.width, maxImageSize.width); + + const heightSizeFactor = maxHeight / maxImageSize.height; + const widthSizeFactor = maxWidth / maxImageSize.width; + const sizeFactor = Math.min(heightSizeFactor, widthSizeFactor); + + const width = maxImageSize.width * sizeFactor; + const height = maxImageSize.height * sizeFactor; + + return { + height, + width, + sizeFactor, + }; +}; + +export const useOverviewImageSize = ({ + containerRect, +}: UseOverviewImageArgs): UseOverviewImageResult => { + const currentImage = useSelector(currentOverviewImageSelector); + + if (!currentImage || !containerRect) return DEFAULT_OVERVIEW_IMAGE_SIZE; + + const maxImageSize = { + width: currentImage.width, + height: currentImage.height, + }; + + return getFinalImageSize(containerRect, maxImageSize); +}; diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts new file mode 100644 index 00000000..af9e09fe --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/utils/useOverviewImageUrl.ts @@ -0,0 +1,13 @@ +import { BASE_MAP_IMAGES_URL } from '@/constants'; +import { currentOverviewImageSelector } from '@/redux/project/project.selectors'; +import { useSelector } from 'react-redux'; + +export const useOverviewImageUrl = (): string => { + const currentImage = useSelector(currentOverviewImageSelector); + + if (!currentImage) { + return ''; + } + + return `${BASE_MAP_IMAGES_URL}/map_images/${currentImage.filename}`; +}; diff --git a/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx b/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx index 1435e558..84dec36a 100644 --- a/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx +++ b/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx @@ -1,6 +1,8 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; -import { openOverviewImagesModal } from '@/redux/modal/modal.slice'; +import { openOverviewImagesModalById } from '@/redux/modal/modal.slice'; +import { projectDefaultOverviewImageIdSelector } from '@/redux/project/project.selectors'; import { Button } from '@/shared/Button'; +import { useSelector } from 'react-redux'; import { twMerge } from 'tailwind-merge'; import { BackgroundSelector } from './BackgroundsSelector'; @@ -8,9 +10,10 @@ import { BackgroundSelector } from './BackgroundsSelector'; export const MapAdditionalOptions = (): JSX.Element => { const dispatch = useAppDispatch(); + const defaultOverviewImageId = useSelector(projectDefaultOverviewImageIdSelector); const handleBrowseOverviewImagesClick = (): void => { - dispatch(openOverviewImagesModal()); + dispatch(openOverviewImagesModalById(defaultOverviewImageId)); }; return ( diff --git a/src/constants/project.ts b/src/constants/project.ts new file mode 100644 index 00000000..04fad94b --- /dev/null +++ b/src/constants/project.ts @@ -0,0 +1,9 @@ +export const DEFAULT_OVERVIEW_IMAGE_WIDTH = 800; + +export const DEFAULT_OVERVIEW_IMAGE_HEIGHT = 500; + +export const DEFAULT_OVERVIEW_IMAGE_SIZE = { + width: DEFAULT_OVERVIEW_IMAGE_WIDTH, + height: DEFAULT_OVERVIEW_IMAGE_HEIGHT, + sizeFactor: 0, +}; diff --git a/src/models/project.ts b/src/models/project.ts index 9f763751..844fe680 100644 --- a/src/models/project.ts +++ b/src/models/project.ts @@ -20,4 +20,5 @@ export const projectSchema = z.object({ creationDate: z.string(), mapCanvasType: z.string(), overviewImageViews: z.array(overviewImageView), + topOverviewImage: overviewImageView, }); diff --git a/src/redux/map/map.constants.ts b/src/redux/map/map.constants.ts index 333a2062..e02cb78f 100644 --- a/src/redux/map/map.constants.ts +++ b/src/redux/map/map.constants.ts @@ -14,6 +14,8 @@ export const MODEL_ID_DEFAULT: number = 0; export const BACKGROUND_ID_DEFAULT: number = 0; +export const OVERVIEW_IMAGE_ID_DEFAULT: number = 0; + export const MAP_DATA_INITIAL_STATE: MapData = { projectId: PROJECT_ID, meshId: '', diff --git a/src/redux/modal/modal.constants.ts b/src/redux/modal/modal.constants.ts index 90ee066d..2266911d 100644 --- a/src/redux/modal/modal.constants.ts +++ b/src/redux/modal/modal.constants.ts @@ -1,7 +1,11 @@ +import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants'; import { ModalState } from './modal.types'; export const MODAL_INITIAL_STATE: ModalState = { isOpen: false, modalName: 'none', modalTitle: '', + overviewImagesState: { + imageId: OVERVIEW_IMAGE_ID_DEFAULT, + }, }; diff --git a/src/redux/modal/modal.mock.ts b/src/redux/modal/modal.mock.ts index f145fba0..2f202d4b 100644 --- a/src/redux/modal/modal.mock.ts +++ b/src/redux/modal/modal.mock.ts @@ -1,7 +1,11 @@ +import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants'; import { ModalState } from './modal.types'; export const MODAL_INITIAL_STATE_MOCK: ModalState = { isOpen: false, modalName: 'none', modalTitle: '', + overviewImagesState: { + imageId: OVERVIEW_IMAGE_ID_DEFAULT, + }, }; diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts index 17687aca..b5534465 100644 --- a/src/redux/modal/modal.reducers.ts +++ b/src/redux/modal/modal.reducers.ts @@ -12,8 +12,14 @@ export const closeModalReducer = (state: ModalState): void => { state.modalName = 'none'; }; -export const openOverviewImagesModalReducer = (state: ModalState): void => { +export const openOverviewImagesModalByIdReducer = ( + state: ModalState, + action: PayloadAction<number>, +): void => { state.isOpen = true; state.modalName = 'overview-images'; state.modalTitle = 'Overview images'; + state.overviewImagesState = { + imageId: action.payload, + }; }; diff --git a/src/redux/modal/modal.selector.ts b/src/redux/modal/modal.selector.ts index 6221d93c..c233d253 100644 --- a/src/redux/modal/modal.selector.ts +++ b/src/redux/modal/modal.selector.ts @@ -1,6 +1,12 @@ import { createSelector } from '@reduxjs/toolkit'; +import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants'; import { rootSelector } from '../root/root.selectors'; export const modalSelector = createSelector(rootSelector, state => state.modal); export const isModalOpenSelector = createSelector(modalSelector, state => state.isOpen); + +export const currentOverviewImageId = createSelector( + modalSelector, + modal => modal?.overviewImagesState.imageId || OVERVIEW_IMAGE_ID_DEFAULT, +); diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts index 9ba21287..a5ae1796 100644 --- a/src/redux/modal/modal.slice.ts +++ b/src/redux/modal/modal.slice.ts @@ -3,7 +3,7 @@ import { MODAL_INITIAL_STATE } from './modal.constants'; import { closeModalReducer, openModalReducer, - openOverviewImagesModalReducer, + openOverviewImagesModalByIdReducer, } from './modal.reducers'; const modalSlice = createSlice({ @@ -12,10 +12,10 @@ const modalSlice = createSlice({ reducers: { openModal: openModalReducer, closeModal: closeModalReducer, - openOverviewImagesModal: openOverviewImagesModalReducer, + openOverviewImagesModalById: openOverviewImagesModalByIdReducer, }, }); -export const { openModal, closeModal, openOverviewImagesModal } = modalSlice.actions; +export const { openModal, closeModal, openOverviewImagesModalById } = modalSlice.actions; export default modalSlice.reducer; diff --git a/src/redux/modal/modal.types.ts b/src/redux/modal/modal.types.ts index 77b0e71f..cad6ec7d 100644 --- a/src/redux/modal/modal.types.ts +++ b/src/redux/modal/modal.types.ts @@ -1,7 +1,12 @@ import { ModalName } from '@/types/modal'; +export type OverviewImagesModalState = { + imageId?: number; +}; + export interface ModalState { isOpen: boolean; modalName: ModalName; modalTitle: string; + overviewImagesState: OverviewImagesModalState; } diff --git a/src/redux/project/project.selectors.ts b/src/redux/project/project.selectors.ts index 2725956a..970ef322 100644 --- a/src/redux/project/project.selectors.ts +++ b/src/redux/project/project.selectors.ts @@ -1,6 +1,28 @@ +import { OverviewImageView } from '@/types/models'; import { createSelector } from '@reduxjs/toolkit'; +import { OVERVIEW_IMAGE_ID_DEFAULT } from '../map/map.constants'; +import { currentOverviewImageId } from '../modal/modal.selector'; import { rootSelector } from '../root/root.selectors'; export const projectSelector = createSelector(rootSelector, state => state.project); export const projectDataSelector = createSelector(projectSelector, project => project?.data); + +export const projectDefaultOverviewImageIdSelector = createSelector( + projectDataSelector, + projectData => projectData?.topOverviewImage.idObject || OVERVIEW_IMAGE_ID_DEFAULT, +); + +export const currentOverviewImageSelector = createSelector( + projectDataSelector, + currentOverviewImageId, + (projectData, imageId): OverviewImageView | undefined => + (projectData?.overviewImageViews || []).find( + overviewImage => overviewImage.idObject === imageId, + ), +); + +export const projectDirectorySelector = createSelector( + projectDataSelector, + projectData => projectData?.directory, +); diff --git a/yarn.lock b/yarn.lock index d403655e..3ba2ee3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -847,9 +847,9 @@ "resolved" "https://registry.npmjs.org/@next/font/-/font-13.5.4.tgz" "version" "13.5.4" -"@next/swc-darwin-x64@13.4.19": - "integrity" "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==" - "resolved" "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz" +"@next/swc-darwin-arm64@13.4.19": + "integrity" "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==" + "resolved" "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz" "version" "13.4.19" "@nodelib/fs.scandir@2.1.5": -- GitLab