Skip to content

Commit

Permalink
feat: save label position as percentage
Browse files Browse the repository at this point in the history
  • Loading branch information
LinaYahya committed Aug 7, 2024
1 parent f773355 commit a3823c8
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 107 deletions.
18 changes: 4 additions & 14 deletions src/@types/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
export type Settings = {
description: string;
labels: Label[];
imageDimension: { width: number; height: number };
};

export enum SettingsKeys {
File = 'file',
SettingsData = 'settings-data',
}

export type Position = { x: number; y: number };
// x and y are relative to image size (percentage)
export type Position = { x: string; y: string };

export type Label = Position & {
content: string;
id: string;
};

export type Choice = { content: string; id: string };

export type DraggableLabelType = {
// x and y are relative to image size (percentage)
y: string;
x: string;
id: string;
content: string;
};

export type AnsweredLabel = {
expected: DraggableLabelType;
actual: null | DraggableLabelType;
expected: Label;
actual: null | Label;
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Label } from '@/@types';
import { useAppTranslation } from '@/config/i18n';
import { APP } from '@/langs/constants';
import { LabelsContext } from '@/modules/context/LabelsContext';
import { useImageDimensionsContext } from '@/modules/context/imageDimensionContext';

import AddLabelForm from './AddLabelForm';
import DraggableLabel from './DraggableLabel';
Expand Down Expand Up @@ -39,10 +40,12 @@ const AddLabelWithinFrame = (): JSX.Element => {
const [content, setContent] = useState('');
const [labelToEdit, setLabelToEdit] = useState<Label | null>(null);
const [formPosition, setFormPosition] = useState({
y: 0,
x: 0,
y: '0%',
x: '0%',
});

const { dimension } = useImageDimensionsContext();

const handleFormInputChange = (
event: React.ChangeEvent<HTMLInputElement>,
): void => {
Expand All @@ -64,11 +67,7 @@ const AddLabelWithinFrame = (): JSX.Element => {
saveLabelsChanges(newLabel);
} else {
const id = v4();
const p = {
y: formPosition.y,
x: formPosition.x,
};
const newLabel = { ...p, id, content };
const newLabel = { ...formPosition, id, content };
saveLabelsChanges(newLabel);
}
handleShowLabelForm(false);
Expand All @@ -79,10 +78,9 @@ const AddLabelWithinFrame = (): JSX.Element => {
): void => {
if (!isDragging) {
const { offsetX, offsetY } = event.nativeEvent;

setFormPosition({
y: offsetY,
x: offsetX,
y: `${(offsetY / dimension.height) * 100}%`,
x: `${(offsetX / dimension.width) * 100}%`,
});
setOpenForm(true);
setLabelToEdit(null);
Expand Down
16 changes: 11 additions & 5 deletions src/modules/builder/configuration/AddLabelsStep/DraggableLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Button, styled } from '@mui/material';

import { Label } from '@/@types';
import { LabelsContext } from '@/modules/context/LabelsContext';
import { useImageDimensionsContext } from '@/modules/context/imageDimensionContext';

const StyledLabel = styled(Button)(({ theme }) => ({
background: theme.palette.primary.main,
Expand All @@ -29,11 +30,14 @@ type Props = {
};

const DraggableLabel = ({ showEditForm, label }: Props): JSX.Element => {
const [position, setPosition] = useState({ x: label.x, y: label.y });
const [position, setPosition] = useState({ x: 0, y: 0 });
const { dimension } = useImageDimensionsContext();

useEffect(() => {
setPosition({ x: label.x, y: label.y });
}, [label]);
const x = (parseFloat(label.x) * dimension.width) / 100;
const y = (parseFloat(label.y) * dimension.height) / 100;
setPosition({ x, y });
}, [label, dimension]);

const { saveLabelsChanges, setIsDragging, isDragging } =
useContext(LabelsContext);
Expand All @@ -49,14 +53,16 @@ const DraggableLabel = ({ showEditForm, label }: Props): JSX.Element => {
const onStop = (e: DraggableEvent): void => {
e.stopPropagation();
e.preventDefault();

const newLabel = { ...label, ...position };
const y = `${(position.y / dimension.height) * 100}%`;
const x = `${(position.x / dimension.width) * 100}%`;
const newLabel = { ...label, x, y };
// Set a delay before enabling actions like opening a new form or applying zoom/move to the image frame
setTimeout(() => {
setIsDragging(false);
}, 2000);
saveLabelsChanges(newLabel);
};

return (
<Draggable
position={position}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { useRef } from 'react';

import { Alert, Skeleton, styled } from '@mui/material';

import { Settings, SettingsKeys } from '@/@types';
import { useAppTranslation } from '@/config/i18n';
import { hooks } from '@/config/queryClient';
import { APP } from '@/langs/constants';
import { useImageObserver } from '@/modules/context/imageDimensionContext';
import { useImageDimensionsContext } from '@/modules/context/imageDimensionContext';

const Container = styled('div')(() => ({
display: 'flex',
Expand All @@ -19,11 +17,11 @@ const Container = styled('div')(() => ({
const ImageFrame = (): JSX.Element | null => {
const { data: appSettings, isLoading: settingLoading } =
hooks.useAppSettings<Settings>();
const imgRef = useRef<HTMLImageElement | null>(null);

const image = appSettings?.find(({ name }) => name === SettingsKeys.File);
const appSettingId = image?.id || '';

const { imgRef } = useImageDimensionsContext();
const {
data: dataFile,
isLoading: isImageLoading,
Expand All @@ -32,8 +30,6 @@ const ImageFrame = (): JSX.Element | null => {
appSettingId,
});

useImageObserver({ imgRef });

const { t } = useAppTranslation();

if (dataFile) {
Expand Down
11 changes: 7 additions & 4 deletions src/modules/builder/configuration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAppTranslation } from '@/config/i18n';
import { hooks } from '@/config/queryClient';
import { CONFIGURATION_TAB_ID } from '@/config/selectors';
import { APP } from '@/langs/constants';
import { ImageDimensionsProvider } from '@/modules/context/imageDimensionContext';

import AddImageStep from './AddImageStep';
import AddLabelsStep from './AddLabelsStep/AddLabelsStep';
Expand Down Expand Up @@ -47,10 +48,12 @@ const Configurations = (): JSX.Element => {
{
label: t(APP.ADD_LABELS_STEP_LABEL),
component: (
<AddLabelsStep
moveToNextStep={() => setActiveStep(2)}
moveToPrevStep={() => setActiveStep(0)}
/>
<ImageDimensionsProvider>
<AddLabelsStep
moveToNextStep={() => setActiveStep(2)}
moveToPrevStep={() => setActiveStep(0)}
/>
</ImageDimensionsProvider>
),
disabled: !image?.id,
},
Expand Down
4 changes: 2 additions & 2 deletions src/modules/common/AllLabelsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Droppable } from 'react-beautiful-dnd';

import { styled } from '@mui/material';

import { DraggableLabelType } from '@/@types';
import { Label } from '@/@types';

import DraggableLabel from './DraggableLabel';

Expand All @@ -22,7 +22,7 @@ export const Container = styled('div')<{
}));

type Props = {
labels: DraggableLabelType[];
labels: Label[];
};

const AllLabelsContainer = ({ labels }: Props): JSX.Element => (
Expand Down
34 changes: 7 additions & 27 deletions src/modules/common/PlayerFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ import { DragDropContext, DropResult } from 'react-beautiful-dnd';

import { Box, Typography } from '@mui/material';

import {
AnsweredLabel,
DraggableLabelType,
Settings,
SettingsKeys,
} from '@/@types';
import { AnsweredLabel, Label, Settings, SettingsKeys } from '@/@types';
import { useAppTranslation } from '@/config/i18n';
import { hooks } from '@/config/queryClient';
import { APP } from '@/langs/constants';
Expand All @@ -25,36 +20,21 @@ const PlayerFrame = (): JSX.Element => {
});

const [labels, setLabels] = useState<AnsweredLabel[]>([]);
const [nonAnsweredLabels, setNonAnsweredLabels] = useState<
DraggableLabelType[]
>([]);
const [nonAnsweredLabels, setNonAnsweredLabels] = useState<Label[]>([]);

const [isDragging, setIsDragging] = useState(false);

useEffect(() => {
const settingLabels = appSettings?.[0].data.labels;
const imageDimension = appSettings?.[0].data.imageDimension;

if (imageDimension && settingLabels) {
const answeredLabels = settingLabels.map(({ id, content, x, y }) => ({
expected: {
id,
content,
x: `${(x / imageDimension.width) * 100}%`,
y: `${(y / imageDimension.height) * 100}%`,
},
actual: null,
}));

const AllChoices = settingLabels.map(({ id, content, x, y }) => ({
id,
content,
x: `${(x / imageDimension.width) * 100}%`,
y: `${(y / imageDimension.height) * 100}%`,
if (settingLabels) {
const answeredLabels = settingLabels.map((label) => ({
expected: label,
actual: null,
}));

setLabels(answeredLabels);
setNonAnsweredLabels(AllChoices);
setNonAnsweredLabels(settingLabels);
}
}, [appSettings]);

Expand Down
82 changes: 43 additions & 39 deletions src/modules/context/imageDimensionContext.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,45 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';

import { Settings, SettingsKeys } from '@/@types';
import { hooks, mutations } from '@/config/queryClient';
import { hooks } from '@/config/queryClient';
import { debounce } from '@/utils';

type Props = {
type ImageDimensionContextType = {
imgRef: React.MutableRefObject<HTMLImageElement | null> | null;
dimension: { width: number; height: number };
};
export const useImageObserver = ({ imgRef }: Props): void => {
const { mutate: patchSetting } = mutations.usePatchAppSetting();

const ImageDimensionsContext = createContext<ImageDimensionContextType>({
imgRef: null,
dimension: { width: 0, height: 0 },
});

type Props1 = {
children: JSX.Element;
};
export const ImageDimensionsProvider = ({ children }: Props1): JSX.Element => {
const imgRef = useRef<HTMLImageElement | null>(null);

const { data: settingsData } = hooks.useAppSettings<Settings>({
name: SettingsKeys.SettingsData,
});

const [imgDimension, setImgDimension] = useState({ width: 0, height: 0 });
const saveImageDimension = useCallback(
(
imageDimension: {
width: number;
height: number;
},
id: string,
): void => {
(imageDimension: { width: number; height: number }): void => {
if (imageDimension.height && imageDimension.width) {
const labels = settingsData?.[0]?.data.labels;
const prevDimension = settingsData?.[0]?.data.imageDimension;

// Calculate new offsets if image dimensions have changed
const newLabels = labels?.map((label) => {
const { x, y } = label;
if (
prevDimension &&
(prevDimension.width !== imageDimension.width ||
prevDimension?.height !== imageDimension.height)
) {
const newX = (x * imageDimension.width) / prevDimension.width;
const newY = (y * imageDimension.height) / prevDimension.height;
return { ...label, x: newX, y: newY };
}
return label;
});

const payload = {
...settingsData?.[0]?.data,
imageDimension,
labels: newLabels,
};

patchSetting({ id, data: payload });
setImgDimension(imageDimension);
}
},
[settingsData, patchSetting],
[],
);

const debouncedSaveImageDimension = useMemo(
Expand All @@ -71,7 +60,7 @@ export const useImageObserver = ({ imgRef }: Props): void => {
const { width, height } = entry.contentRect;

if ((width !== dimension.width || height !== dimension.height) && id) {
debouncedSaveImageDimension({ width, height }, id);
debouncedSaveImageDimension({ width, height });
}
};

Expand All @@ -84,4 +73,19 @@ export const useImageObserver = ({ imgRef }: Props): void => {
resizeObserver.disconnect();
};
}, [imgRef, debouncedSaveImageDimension, settingsData]);

const value: ImageDimensionContextType = useMemo(() => {
const dimension = imgDimension;

return { imgRef, dimension };
}, [imgRef, imgDimension]);

return (
<ImageDimensionsContext.Provider value={value}>
{children}
</ImageDimensionsContext.Provider>
);
};

export const useImageDimensionsContext = (): ImageDimensionContextType =>
useContext<ImageDimensionContextType>(ImageDimensionsContext);

0 comments on commit a3823c8

Please sign in to comment.