diff --git a/.gitignore b/.gitignore
index 5285aeb..862fef6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,3 +106,5 @@ dist
.eslintcache
**/*/env-config.js
+
+**/.DS_Store
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 84c2d21..4d2254c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -15,8 +15,8 @@
"editor.tabSize": 2,
"stylelint.validate": ["css", "scss"],
"editor.codeActionsOnSave": {
- "source.fixAll.eslint": true,
- "source.fixAll.stylelint": true
+ "source.fixAll.eslint": "explicit",
+ "source.fixAll.stylelint": "explicit"
},
}
\ No newline at end of file
diff --git a/README.md b/README.md
index f5e2e64..1a15f94 100644
--- a/README.md
+++ b/README.md
@@ -82,5 +82,5 @@ docker build -t tourmanique-ui .
### Run
```bash
-docker run --detach --publish 7551:80 --rm --name tourmanique-ui --env ENV_KEY="'dev'" --env API_ROOT="'http://localhost:7501/api'" tourmanique-ui
+docker run --detach --publish 9551:80 --rm --name tourmanique-ui --env ENV_KEY="'dev'" --env API_ROOT="'http://localhost:7501/api'" tourmanique-ui
```
\ No newline at end of file
diff --git a/src/pages/Galleries/components/GalleriesList/GalleriesList.tsx b/src/pages/Galleries/components/GalleriesList/GalleriesList.tsx
index 05e743b..8b60311 100644
--- a/src/pages/Galleries/components/GalleriesList/GalleriesList.tsx
+++ b/src/pages/Galleries/components/GalleriesList/GalleriesList.tsx
@@ -42,6 +42,8 @@ function GalleriesList({
id,
name,
previewPhotos,
+ // @ts-ignore
+ photosCount
}) => (
{
onGalleryDelete(id);
}}
- photosCount={previewPhotos.length}
+ photosCount={photosCount}
previewPhotos={previewPhotos}
/>
diff --git a/src/pages/Metrics/MetricsPage.scss b/src/pages/Metrics/MetricsPage.scss
index d112e40..38b301b 100644
--- a/src/pages/Metrics/MetricsPage.scss
+++ b/src/pages/Metrics/MetricsPage.scss
@@ -27,5 +27,7 @@
@include noselect;
width: 100%;
+
+ border-radius: 3%;
}
}
diff --git a/src/pages/Metrics/MetricsPage.tsx b/src/pages/Metrics/MetricsPage.tsx
index b580530..523cf9b 100644
--- a/src/pages/Metrics/MetricsPage.tsx
+++ b/src/pages/Metrics/MetricsPage.tsx
@@ -1,46 +1,56 @@
/* eslint-disable no-nested-ternary */
-import { useEffect, useState } from 'react';
-import metricImage from '../../assets/images/metric-image.png';
+import { useMemo } from 'react';
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
-import MetricSimilarList from './components/MetricsSimilarList/MetricsSimilarList';
-import {
- similarPhotosArray,
-} from './MetricsTestValues.data';
-import MetricsInfo from './components/MetricsInfo/MetricsInfo';
+// import MetricSimilarList from './components/MetricsSimilarList/MetricsSimilarList';
+
+import { MetricsContainer } from './sections/metrics/MetricsContainer';
+import { useParams } from 'react-router-dom';
+import { MetricsStateContext } from './sections/metrics/state/MetricsStateContext';
+import { MetricsState } from './sections/metrics/state/MetricsState';
+import { PhotoStateContext } from './sections/photo/state/PhotoStateContext';
+import { PhotoContainer } from './sections/photo/PhotoContainer';
+import { PhotoState } from './sections/photo/state/PhotoState';
function MetricsPage() {
- const [isLoading, setIsLoading] = useState(true);
+ const {
+ galleryId,
+ id
+ } = useParams();
- const delay = 1;
+ const photoState = useMemo(
+ () => new PhotoState(),
+ [],
+ )
- useEffect(() => {
- const timer1 = setTimeout(() => setIsLoading(false), delay * 1000);
- return () => {
- clearTimeout(timer1);
- };
- }, []);
+ const metricsState = useMemo(
+ () => new MetricsState(),
+ [],
+ )
return (
-
-

+
-
+
-
+
+
+
-
+ /> */}
-
);
}
diff --git a/src/pages/Metrics/sections/metrics/MetricsContainer.cy.tsx b/src/pages/Metrics/sections/metrics/MetricsContainer.cy.tsx
new file mode 100644
index 0000000..0863505
--- /dev/null
+++ b/src/pages/Metrics/sections/metrics/MetricsContainer.cy.tsx
@@ -0,0 +1,66 @@
+import { associationsArray, colorsArray, emotionsArray, objectsArray } from "../../MetricsTestValues.data"
+import { MetricsContainer } from "./MetricsContainer"
+import { MetricsState } from "./state/MetricsState"
+import { MetricsStateContext } from "./state/MetricsStateContext"
+
+const GALLERY_ID = 1
+const PHOTO_ID = 2
+
+describe(`ToDosContainer`, () => {
+ beforeEach(() => {
+ const metricsResponse = {
+ metrics: {
+ uniqueness: {
+ mainInPercentage: 75,
+ colorsInPercentage: 44,
+ otherInPercentage: 32,
+ },
+ features: {
+ colors: colorsArray,
+ emotions: emotionsArray,
+ objects: objectsArray,
+ associations: associationsArray,
+ }
+ },
+ }
+
+ cy.intercept(
+ `GET`,
+ `*/galleries/${GALLERY_ID}/${PHOTO_ID}/metrics`,
+ metricsResponse,
+ )
+ })
+
+ describe(`Initialization`, initializationTests)
+})
+
+
+function initializationTests() {
+ it(`
+ GIVEN metrics from network
+ WHEN render the component
+ SHOULD see them
+ `, () => {
+ mountComponent()
+
+ cy.contains(`75%`)
+ cy.contains(`44%`)
+ cy.contains(`32%`)
+ cy.contains(`calm`)
+ cy.contains(`sky`)
+ cy.contains(`vacation`)
+ })
+}
+
+function mountComponent() {
+ const metricsState = new MetricsState()
+
+ cy.mount(
+
+
+ ,
+ )
+}
\ No newline at end of file
diff --git a/src/pages/Metrics/sections/metrics/MetricsContainer.tsx b/src/pages/Metrics/sections/metrics/MetricsContainer.tsx
new file mode 100644
index 0000000..deca1e0
--- /dev/null
+++ b/src/pages/Metrics/sections/metrics/MetricsContainer.tsx
@@ -0,0 +1,45 @@
+import { observer } from "mobx-react-lite"
+import { useContext, useEffect } from "react"
+import { api } from "../../../../common/utils/HttpClient"
+import { MetricsContent } from "./MetricsContent"
+import { MetricsStateContext } from "./state/MetricsStateContext"
+
+export const MetricsContainer = observer(({
+ galleryId,
+ photoId,
+}: {
+ galleryId: number,
+ photoId: number,
+}) => {
+ const metricsState = useContext(MetricsStateContext)
+
+ useEffect(() => {
+ async function loadMetricsAsync() {
+ metricsState.setIsLoading({
+ isLoading: true
+ })
+
+ try {
+ const {
+ data: {
+ metrics,
+ }
+ } = await api.get(`/galleries/${galleryId}/${photoId}/metrics`)
+
+ metricsState.initialize({
+ metrics,
+ })
+ } finally {
+ metricsState.setIsLoading({
+ isLoading: false
+ })
+ }
+ }
+
+ loadMetricsAsync()
+ }, [])
+
+ return (
+
+ )
+})
\ No newline at end of file
diff --git a/src/pages/Metrics/sections/metrics/MetricsContent.tsx b/src/pages/Metrics/sections/metrics/MetricsContent.tsx
new file mode 100644
index 0000000..ff00997
--- /dev/null
+++ b/src/pages/Metrics/sections/metrics/MetricsContent.tsx
@@ -0,0 +1,16 @@
+import { observer } from "mobx-react-lite"
+import { useContext } from "react"
+import MetricsInfo from "./components/MetricsInfo/MetricsInfo"
+import { MetricsStateContext } from "./state/MetricsStateContext"
+
+export const MetricsContent = observer(() => {
+ const metricsState = useContext(MetricsStateContext)
+
+ return (
+
+ )
+})
\ No newline at end of file
diff --git a/src/pages/Metrics/components/MetricsInfo/MetricsInfo.cy.tsx b/src/pages/Metrics/sections/metrics/components/MetricsInfo/MetricsInfo.cy.tsx
similarity index 100%
rename from src/pages/Metrics/components/MetricsInfo/MetricsInfo.cy.tsx
rename to src/pages/Metrics/sections/metrics/components/MetricsInfo/MetricsInfo.cy.tsx
diff --git a/src/pages/Metrics/components/MetricsInfo/MetricsInfo.scss b/src/pages/Metrics/sections/metrics/components/MetricsInfo/MetricsInfo.scss
similarity index 100%
rename from src/pages/Metrics/components/MetricsInfo/MetricsInfo.scss
rename to src/pages/Metrics/sections/metrics/components/MetricsInfo/MetricsInfo.scss
diff --git a/src/pages/Metrics/components/MetricsInfo/MetricsInfo.tsx b/src/pages/Metrics/sections/metrics/components/MetricsInfo/MetricsInfo.tsx
similarity index 86%
rename from src/pages/Metrics/components/MetricsInfo/MetricsInfo.tsx
rename to src/pages/Metrics/sections/metrics/components/MetricsInfo/MetricsInfo.tsx
index d94fb58..6f9cff4 100644
--- a/src/pages/Metrics/components/MetricsInfo/MetricsInfo.tsx
+++ b/src/pages/Metrics/sections/metrics/components/MetricsInfo/MetricsInfo.tsx
@@ -1,11 +1,34 @@
-import CircleProgressBar from '../../../../components/CircleProgressBar/CircleProgressBar';
-import {
- associationsArray, colorsArray, emotionsArray, objectsArray,
-} from '../../MetricsTestValues.data';
+import CircleProgressBar from '../../../../../../components/CircleProgressBar/CircleProgressBar';
function MetricsInfo({
+ uniqueness: {
+ mainInPercentage,
+ colorsInPercentage,
+ otherInPercentage,
+ },
+ features: {
+ associations,
+ colors,
+ emotions,
+ objects,
+ },
isLoading,
}: {
+ uniqueness: {
+ mainInPercentage: number,
+ colorsInPercentage: number,
+ otherInPercentage: number,
+ },
+ features: {
+ associations: string[],
+ colors: {
+ red: number,
+ green: number,
+ blue: number,
+ }[],
+ emotions: string[],
+ objects: string[],
+ }
isLoading: boolean;
}) {
return (
@@ -21,7 +44,7 @@ function MetricsInfo({
) : (
- {20}
+ {colorsInPercentage}
% completed
@@ -52,12 +75,12 @@ function MetricsInfo({
- {20}
+ {colorsInPercentage}
%
@@ -80,7 +103,7 @@ function MetricsInfo({
- {15}
+ {otherInPercentage}
% completed
@@ -90,12 +113,12 @@ function MetricsInfo({
- {15}
+ {otherInPercentage}
%
@@ -122,7 +145,7 @@ function MetricsInfo({
) : (
- {colorsArray.map((color) => (
+ {colors.map((color) => (
) : (
- {emotionsArray.map((emotion) => (
+ {emotions.map((emotion) => (
) : (
- {objectsArray.map((object) => (
+ {objects.map((object) => (
) : (
- {associationsArray.map((association) => (
+ {associations.map((association) => (
(null as unknown as MetricsState)
\ No newline at end of file
diff --git a/src/pages/Metrics/sections/photo/PhotoContainer.tsx b/src/pages/Metrics/sections/photo/PhotoContainer.tsx
new file mode 100644
index 0000000..b2885bd
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/PhotoContainer.tsx
@@ -0,0 +1,45 @@
+import { observer } from "mobx-react-lite"
+import { useContext, useEffect } from "react"
+import { api } from "../../../../common/utils/HttpClient"
+import { PhotoContent } from "./PhotoContent"
+import { PhotoStateContext } from "./state/PhotoStateContext"
+
+export const PhotoContainer = observer(({
+ galleryId,
+ photoId,
+}: {
+ galleryId: number,
+ photoId: number,
+}) => {
+ const photoState = useContext(PhotoStateContext)
+
+ useEffect(() => {
+ async function loadPhotoAsync() {
+ photoState.setIsLoading({
+ isLoading: true
+ })
+
+ try {
+ const {
+ data: {
+ photo,
+ }
+ } = await api.get(`/galleries/${galleryId}/${photoId}/photo`)
+
+ photoState.initialize({
+ photo,
+ })
+ } finally {
+ photoState.setIsLoading({
+ isLoading: false
+ })
+ }
+ }
+
+ loadPhotoAsync()
+ }, [])
+
+ return (
+
+ )
+})
\ No newline at end of file
diff --git a/src/pages/Metrics/sections/photo/PhotoContent.tsx b/src/pages/Metrics/sections/photo/PhotoContent.tsx
new file mode 100644
index 0000000..712aec5
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/PhotoContent.tsx
@@ -0,0 +1,30 @@
+import { observer } from "mobx-react-lite"
+import { useContext } from "react"
+import Skeleton from "react-loading-skeleton"
+import { PhotoStateContext } from "./state/PhotoStateContext"
+
+export const PhotoContent = observer(() => {
+ const photoState = useContext(PhotoStateContext)
+
+ return (
+
+ {
+ photoState.isLoading
+ ? (
+
+ )
+ : (
+

+ )
+ }
+
+ )
+})
\ No newline at end of file
diff --git a/src/pages/Metrics/sections/photo/components/MetricsInfo/MetricsInfo.cy.tsx b/src/pages/Metrics/sections/photo/components/MetricsInfo/MetricsInfo.cy.tsx
new file mode 100644
index 0000000..810fab8
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/components/MetricsInfo/MetricsInfo.cy.tsx
@@ -0,0 +1,22 @@
+import MetricsInfo from "./MetricsInfo";
+
+describe(`MetricsSimilarCard`, () => {
+ beforeEach(() => {
+ cy.mount();
+ });
+ it(`count of colors item SHOULD BE more then one and less then three`, () => {
+ cy.getByData(`color`).its(`length`).should(`be.within`, 1, 3);
+ });
+
+ it(`count of emotions item SHOULD BE more then one`, () => {
+ cy.getByData(`emotion`).its(`length`).should(`be.at.least`, 1);
+ });
+
+ it(`count of objects item SHOULD BE more then one`, () => {
+ cy.getByData(`object`).its(`length`).should(`be.at.least`, 1);
+ });
+
+ it(`count of associations item SHOULD BE more then one`, () => {
+ cy.getByData(`association`).its(`length`).should(`be.at.least`, 1);
+ });
+});
diff --git a/src/pages/Metrics/sections/photo/components/MetricsInfo/MetricsInfo.scss b/src/pages/Metrics/sections/photo/components/MetricsInfo/MetricsInfo.scss
new file mode 100644
index 0000000..84adc49
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/components/MetricsInfo/MetricsInfo.scss
@@ -0,0 +1,305 @@
+.metrics-info {
+ display: flex;
+ flex-direction: column;
+ margin-top: 24px;
+ width: 100%;
+
+ &__uniqueness-bars {
+ border-radius: 20px;
+ padding: 16px;
+ box-shadow: 1px 4px 16px rgb(180 173 189 / 15%);
+
+ @include tablet {
+ display: flex;
+ align-items: center;
+ padding: 16px 24px;
+ }
+ }
+
+ &__stroke-bars {
+ display: flex;
+ flex-direction: column;
+ margin-top: 32px;
+ width: 100%;
+
+ @include tablet {
+ margin-top: 0;
+ margin-left: 24px;
+ }
+
+ @include desktop {
+ margin-left: 48px;
+ }
+ }
+
+ &__box {
+ margin-bottom: 24px;
+ }
+
+ &__bar-box {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: row;
+ margin-bottom: 16px;
+ width: 100%;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &__tag {
+ margin-right: 8px;
+ margin-bottom: 8px;
+ border-radius: 8px;
+ padding: 8px 16px;
+ line-height: 1.2;
+ background-color: $color-grey-light-100;
+ }
+
+ &__tags {
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ margin-top: 12px;
+ }
+
+ &__colors {
+ display: flex;
+ align-items: center;
+ margin-top: 12px;
+ }
+
+ &__color {
+ margin-right: 16px;
+ border-radius: 4px;
+ width: 24px;
+ height: 24px;
+ }
+
+ &__subtitle {
+ width: 100%;
+ max-width: 30%;
+ text-align: left;
+ color: $color-dark-grey;
+ }
+
+ &__bar {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ margin-left: 16px;
+ width: 100%;
+ max-width: 70%;
+ }
+
+ &__bar-background {
+ border-radius: 6px;
+ width: 100%;
+ height: 8px;
+ min-width: 48px;
+ background: $color-grey-light-300;
+ }
+
+ &__bar-progress {
+ border-radius: 6px;
+ height: 8px;
+ background: linear-gradient(90deg, #735be4 0%, #9f8af8 100%);
+ }
+
+ &__bar-percentage-text {
+ margin-left: 4px;
+ line-height: 1.1;
+
+ span {
+ font-size: 12px;
+ color: $color-dark-grey;
+ }
+ }
+
+ &__loader-container {
+ margin-left: 30px;
+ width: 100%;
+ max-width: 70%;
+ text-align: left;
+ }
+
+ &__loader-text {
+ font-size: 12px;
+ line-height: 1.2;
+ color: $color-grey-light-400;
+ }
+
+ &__image-loader-text {
+ position: relative;
+ display: inline-block;
+ font-size: 12px;
+ line-height: 1.2;
+ color: $color-grey-light-400;
+
+ &::after {
+ content: "";
+ position: absolute;
+ right: -3px;
+ bottom: 4px;
+ border-radius: 50%;
+ width: 2px;
+ height: 2px;
+ box-sizing: border-box;
+ background: currentColor;
+ animation: animloader 1s linear infinite;
+ }
+ }
+
+ &__loader {
+ position: relative;
+ display: inline-block;
+ overflow: hidden;
+ border-radius: 4px;
+ width: 100%;
+
+ &::after {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ border-radius: 4px;
+ width: 100%;
+ height: 8px;
+ box-sizing: border-box;
+ animation: defaultLoader 2s linear infinite;
+ }
+
+ &--purple {
+ height: 4.8px;
+
+ &::after {
+ height: 4.8px;
+ background: linear-gradient(90deg, #6349ea 0%, #bcacff 100%);
+ }
+ }
+
+ &--default {
+ height: 8px;
+
+ &::after {
+ height: 8px;
+ background: linear-gradient(90deg, #f9f9fc 0%, #d2d0dc 100%, #d2d0dc 100%);
+ }
+ }
+ }
+
+ &__image-loader-container {
+ display: flex;
+ align-items: center;
+ flex: none;
+ flex-direction: column;
+ justify-content: space-between;
+ margin: 0 auto;
+ width: 72px;
+ height: 52px;
+
+ @include tablet {
+ margin: 0;
+ }
+ }
+
+ &__image-loader {
+ position: relative;
+ overflow: hidden;
+ border-radius: 7px;
+ width: 32px;
+ height: 32px;
+ background-color: $color-grey-light-300;
+
+ &::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ border-radius: 3px;
+ width: 20px;
+ height: 20px;
+ box-shadow: 16px -15px 0 5px $primary-color-default;
+ background: $primary-color-disable;
+ transform: rotate(45deg) translate(30%, 40%);
+ animation: slide 3s infinite ease-in-out alternate;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ left: 10px;
+ top: 7px;
+ border-radius: 50%;
+ width: 8px;
+ height: 8px;
+ background: $primary-color-hover;
+ transform: rotate(0deg);
+ transform-origin: 35px 145px;
+ animation: sunRotate 3s infinite ease-in-out;
+ }
+ }
+
+ &__loader-default-container {
+ text-align: left;
+ }
+
+ &__uniqueness {
+ margin-bottom: 28px;
+ }
+
+ &__title {
+ margin-bottom: 16px;
+ font-size: 18px;
+ text-align: left;
+ }
+
+ @include desktop {
+ margin-top: 0;
+ }
+
+ @keyframes defaultLoader {
+ 0% {
+ left: 0;
+ transform: translateX(-100%);
+ }
+
+ 100% {
+ left: 100%;
+ transform: translateX(0%);
+ }
+ }
+
+ @keyframes animloader {
+ 0% {
+ box-shadow: 5px 0 rgb(255 255 255 / 0%), 10px 0 rgb(255 255 255 / 0%);
+ }
+
+ 50% {
+ box-shadow: 5px 0 $color-dark-grey, 10px 0 rgb(255 255 255 / 0%);
+ }
+
+ 100% {
+ box-shadow: 5px 0 $color-dark-grey, 10px 0 $color-dark-grey;
+ }
+ }
+
+ @keyframes slide {
+ 0% { bottom: -35px; }
+ 100% { bottom: -35px; }
+
+ 25% { bottom: -2px; }
+ 75% { bottom: -2px; }
+
+ 20% { bottom: 2px; }
+ 80% { bottom: 2px; }
+ }
+
+ @keyframes sunRotate {
+ 0% { transform: rotate(-15deg); }
+ 25% { transform: rotate(0deg); }
+ 75% { transform: rotate(0deg); }
+ 100% { transform: rotate(25deg); }
+ }
+}
diff --git a/src/pages/Metrics/sections/photo/components/MetricsInfo/MetricsInfo.tsx b/src/pages/Metrics/sections/photo/components/MetricsInfo/MetricsInfo.tsx
new file mode 100644
index 0000000..6f9cff4
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/components/MetricsInfo/MetricsInfo.tsx
@@ -0,0 +1,248 @@
+import CircleProgressBar from '../../../../../../components/CircleProgressBar/CircleProgressBar';
+
+function MetricsInfo({
+ uniqueness: {
+ mainInPercentage,
+ colorsInPercentage,
+ otherInPercentage,
+ },
+ features: {
+ associations,
+ colors,
+ emotions,
+ objects,
+ },
+ isLoading,
+}: {
+ uniqueness: {
+ mainInPercentage: number,
+ colorsInPercentage: number,
+ otherInPercentage: number,
+ },
+ features: {
+ associations: string[],
+ colors: {
+ red: number,
+ green: number,
+ blue: number,
+ }[],
+ emotions: string[],
+ objects: string[],
+ }
+ isLoading: boolean;
+}) {
+ return (
+
+
+
Uniqueness
+
+ {
+ isLoading ? (
+
+
+ counting
+
+ ) : (
+
+ )
+ }
+
+
+
+
Colors
+
+ {isLoading ? (
+
+
+
+ {colorsInPercentage}
+ % completed
+
+
+ ) : (
+
+
+
+ {colorsInPercentage}
+ %
+
+
+ )}
+
+
+
+
+ Objects,
+ emotions,
+ associations
+
+
+ {
+ isLoading ? (
+
+
+
+ {otherInPercentage}
+ % completed
+
+
+ ) : (
+
+
+
+ {otherInPercentage}
+ %
+
+
+ )
+ }
+
+
+
+
+
+
+
+
Features
+
+
Main colors
+ {isLoading ? (
+
+
+
+ Almost done
+
+
+ ) : (
+
+ {colors.map((color) => (
+
+ ))}
+
+ )}
+
+
+
+
Emotions
+ {isLoading ? (
+
+
+
+ It's going to take a little longer than we expected
+
+
+ ) : (
+
+ {emotions.map((emotion) => (
+
+ {emotion}
+
+ ))}
+
+ )}
+
+
+
+
Objects
+ {isLoading ? (
+
+
+
+ Select objects from the photo (usually takes about 3 minutes)
+
+
+ ) : (
+
+ {objects.map((object) => (
+
+ {object}
+
+ ))}
+
+ )}
+
+
+
+
Associations
+ {isLoading ? (
+
+
+
+ It's going to take a little longer than we expected
+
+
+ ) : (
+
+ {associations.map((association) => (
+
+ {association}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+export default MetricsInfo;
diff --git a/src/pages/Metrics/sections/photo/components/MetricsSimilarCard/MetricsSimilarCard.scss b/src/pages/Metrics/sections/photo/components/MetricsSimilarCard/MetricsSimilarCard.scss
new file mode 100644
index 0000000..b80d84a
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/components/MetricsSimilarCard/MetricsSimilarCard.scss
@@ -0,0 +1,86 @@
+.metric-similar-card {
+ position: relative;
+ margin-bottom: 16px;
+ width: 100%;
+
+ &__image-container {
+ position: relative;
+ overflow: hidden;
+ border-radius: 6px;
+ min-width: 100%;
+ aspect-ratio: 8 / 5;
+ }
+
+ &__subtitle {
+ margin: 12px 0 4px;
+ text-align: left;
+ color: $color-dark-grey;
+
+ @include desktop {
+ margin: 12px 0 8px;
+ }
+ }
+
+ &__tag {
+ margin-top: 4px;
+ margin-right: 4px;
+ border-radius: 8px;
+ padding: 4px 8px;
+ line-height: 1.2;
+ background-color: $color-grey-light-100;
+
+ @include desktop {
+ margin-top: 8px;
+ margin-right: 8px;
+ }
+ }
+
+ &__tags {
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ }
+
+ &__image {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ &__colors {
+ position: absolute;
+ right: 16px;
+ bottom: 16px;
+ display: flex;
+ align-items: center;
+ }
+
+ &__color {
+ margin-right: 12px;
+ border: 2px solid $color-white;
+ border-radius: 6px;
+ width: 24px;
+ height: 24px;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+
+ &:last-child {
+ margin-bottom: 32px;
+ }
+
+ @include desktop {
+ flex: none;
+ margin-bottom: 24px;
+ min-width: 224px;
+
+ &:last-child {
+ margin-bottom: 224px;
+ }
+ }
+}
diff --git a/src/pages/Metrics/sections/photo/components/MetricsSimilarCard/MetricsSimilarCard.tsx b/src/pages/Metrics/sections/photo/components/MetricsSimilarCard/MetricsSimilarCard.tsx
new file mode 100644
index 0000000..cc47067
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/components/MetricsSimilarCard/MetricsSimilarCard.tsx
@@ -0,0 +1,64 @@
+export type SimilarPhotoType = {
+ photoId: number;
+ photoPath: string;
+ relatedColors: {
+ red: number;
+ green: number;
+ blue: number;
+ }[];
+ relatedFeatures: string[];
+};
+
+function MetricsSimilarCard({
+ photoId,
+ photoPath,
+ relatedColors,
+ relatedFeatures,
+}: SimilarPhotoType) {
+ return (
+
+
+

+
+ {
+ relatedColors.map((color) => (
+
+ ))
+ }
+
+
+
+
Related features
+
+ {relatedFeatures.map((feature) => (
+
+ {feature}
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export default MetricsSimilarCard;
diff --git a/src/pages/Metrics/sections/photo/components/MetricsSimilarCard/MetricsSimilarCart.cy.tsx b/src/pages/Metrics/sections/photo/components/MetricsSimilarCard/MetricsSimilarCart.cy.tsx
new file mode 100644
index 0000000..33354d3
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/components/MetricsSimilarCard/MetricsSimilarCart.cy.tsx
@@ -0,0 +1,81 @@
+import MetricsSimilarCard, { SimilarPhotoType } from "./MetricsSimilarCard";
+
+describe(`MetricsSimilarCard`, () => {
+ it(`count of colors item SHOULD be one`, () => {
+ mountComponent({
+ photoId: 1,
+ photoPath: `https://stickerbase.ru/wp-content/uploads/2021/07/61607.png`,
+ relatedColors: [{
+ red: 57,
+ green: 56,
+ blue: 56,
+ },
+ ],
+ relatedFeatures: [`calm`],
+ });
+ cy.getByData(`color`).its(`length`).should(`eq`, 1);
+ });
+
+ it(`count of colors item SHOULD be two`, () => {
+ mountComponent({
+ photoId: 1,
+ photoPath: `https://stickerbase.ru/wp-content/uploads/2021/07/61607.png`,
+ relatedColors: [{
+ red: 57,
+ green: 56,
+ blue: 56,
+ },
+ {
+ red: 30,
+ green: 20,
+ blue: 16,
+ },
+ ],
+ relatedFeatures: [`calm`],
+ });
+ cy.getByData(`color`).its(`length`).should(`eq`, 2);
+ });
+
+ it(`count of tags item SHOULD be one`, () => {
+ mountComponent({
+ photoId: 1,
+ photoPath: `https://stickerbase.ru/wp-content/uploads/2021/07/61607.png`,
+ relatedColors: [{
+ red: 57,
+ green: 56,
+ blue: 56,
+ },
+ ],
+ relatedFeatures: [`calm`],
+ });
+ cy.getByData(`tag`).its(`length`).should(`eq`, 1);
+ });
+
+ it(`count of tags item SHOULD be two`, () => {
+ mountComponent({
+ photoId: 1,
+ photoPath: `https://stickerbase.ru/wp-content/uploads/2021/07/61607.png`,
+ relatedColors: [{
+ red: 57,
+ green: 56,
+ blue: 56,
+ },
+ ],
+ relatedFeatures: [`calm`, `love`],
+ });
+ cy.getByData(`tag`).its(`length`).should(`eq`, 2);
+ });
+});
+function mountComponent({
+ photoId,
+ photoPath,
+ relatedColors,
+ relatedFeatures,
+}: SimilarPhotoType) {
+ cy.mount();
+}
diff --git a/src/pages/Metrics/sections/photo/components/MetricsSimilarList/MetricsSimilarList.cy.tsx b/src/pages/Metrics/sections/photo/components/MetricsSimilarList/MetricsSimilarList.cy.tsx
new file mode 100644
index 0000000..e19d5a2
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/components/MetricsSimilarList/MetricsSimilarList.cy.tsx
@@ -0,0 +1,54 @@
+import { SimilarPhotoType } from "../MetricsSimilarCard/MetricsSimilarCard";
+import MetricsSimilarList from "./MetricsSimilarList";
+
+describe(`MetricsSimilarList`, () => {
+ it(`SHOULD render no similar images WHEN there are no images`, () => {
+ mountComponent({
+ isLoading: false,
+ similarPhotosArray: [],
+ });
+ cy.getByData(`empty-container`).should(`be.exist`);
+ });
+ it(`SHOULD render loading text WHEN loading props true`, () => {
+ mountComponent({
+ isLoading: true,
+ similarPhotosArray: [],
+ });
+ cy.getByData(`empty-loader-text`).should(`be.exist`);
+ });
+ it(`SHOULD render cards WHEN loading props false and array has values`, () => {
+ mountComponent({
+ isLoading: false,
+ similarPhotosArray: [{
+ photoId: 24,
+ photoPath: `https://stickerbase.ru/wp-content/uploads/2021/07/61607.png`,
+ relatedColors: [{
+ red: 57,
+ green: 56,
+ blue: 56,
+ },
+ {
+ red: 58,
+ green: 48,
+ blue: 106,
+ },
+ ],
+ relatedFeatures: [`calm`, `mountain`, `tree`, `lake`, `snow`],
+ }],
+ });
+ cy.getByData(`full-container`).should(`be.exist`);
+ });
+});
+
+function mountComponent({
+ isLoading,
+ similarPhotosArray,
+}: {
+ isLoading: boolean;
+ similarPhotosArray: SimilarPhotoType[];
+}) {
+ cy.mount();
+}
diff --git a/src/pages/Metrics/sections/photo/components/MetricsSimilarList/MetricsSimilarList.scss b/src/pages/Metrics/sections/photo/components/MetricsSimilarList/MetricsSimilarList.scss
new file mode 100644
index 0000000..b016e6c
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/components/MetricsSimilarList/MetricsSimilarList.scss
@@ -0,0 +1,68 @@
+.metrics-similar-list {
+ position: relative;
+
+ &__title {
+ margin-bottom: 16px;
+ font-size: 18px;
+ text-align: left;
+ }
+
+ &__empty {
+ font-weight: 500;
+ text-align: center;
+ color: #7f7d85;
+ }
+
+ &__empty-container {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ justify-content: center;
+ margin-bottom: 32px;
+ width: 100%;
+ height: 100%;
+ min-width: 224px;
+
+ @include desktop {
+ margin-bottom: 0;
+ }
+ }
+
+ &__empty-loader-svg {
+ margin-bottom: 24px;
+ }
+
+ &__full-container {
+ @include reset-list;
+
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ padding-left: 0;
+ width: 100%;
+ height: 100%;
+ min-width: 224px;
+
+ @include tablet {
+ max-width: 224px;
+ }
+
+ @include desktop {
+ position: absolute;
+ overflow-y: scroll;
+ height: 100vh;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+ }
+
+ @include desktop {
+ overflow: hidden;
+ margin-left: 48px;
+ border-left: 2px solid $color-grey-light-100;
+ padding-left: 32px;
+ min-width: 259px;
+ }
+}
diff --git a/src/pages/Metrics/sections/photo/components/MetricsSimilarList/MetricsSimilarList.tsx b/src/pages/Metrics/sections/photo/components/MetricsSimilarList/MetricsSimilarList.tsx
new file mode 100644
index 0000000..045bfea
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/components/MetricsSimilarList/MetricsSimilarList.tsx
@@ -0,0 +1,65 @@
+/* eslint-disable no-nested-ternary */
+import MetricsSimilarCard, { SimilarPhotoType } from '../MetricsSimilarCard/MetricsSimilarCard';
+import MetricSimilarLoader from '../../../../assets/images/similar-loader-image.svg';
+
+function MetricsSimilarList({
+ isLoading,
+ similarPhotosArray,
+}: {
+ isLoading: boolean;
+ similarPhotosArray: SimilarPhotoType[];
+}) {
+ return (
+
+
Similar photos
+ {isLoading ? (
+
+

+
+ We'll start searching for photos once we've highlighted
+ all the tags
+
+
+ ) : similarPhotosArray.length === 0 ? (
+
+
+ The photo is unique so it has
+ no similar photos
+
+
+ ) : (
+
+ {similarPhotosArray.map((photo) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default MetricsSimilarList;
diff --git a/src/pages/Metrics/sections/photo/state/PhotoState.tsx b/src/pages/Metrics/sections/photo/state/PhotoState.tsx
new file mode 100644
index 0000000..b954052
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/state/PhotoState.tsx
@@ -0,0 +1,37 @@
+import { makeAutoObservable } from 'mobx'
+
+export class PhotoState {
+ private _photo = {
+ url: ``
+ }
+ private _isLoading: boolean = false
+
+ constructor() {
+ makeAutoObservable(this)
+ }
+
+ initialize({
+ photo,
+ }: {
+ photo: any,
+ }) {
+ this._photo = photo
+ }
+
+ get photo() {
+ return this._photo
+ }
+
+ get isLoading() {
+ return this._isLoading
+ }
+
+
+ setIsLoading({
+ isLoading,
+ }: {
+ isLoading: boolean,
+ }) {
+ this._isLoading = isLoading
+ }
+}
\ No newline at end of file
diff --git a/src/pages/Metrics/sections/photo/state/PhotoStateContext.tsx b/src/pages/Metrics/sections/photo/state/PhotoStateContext.tsx
new file mode 100644
index 0000000..36fdbf0
--- /dev/null
+++ b/src/pages/Metrics/sections/photo/state/PhotoStateContext.tsx
@@ -0,0 +1,4 @@
+import { createContext } from "react"
+import { PhotoState } from "./PhotoState"
+
+export const PhotoStateContext = createContext(null as unknown as PhotoState)
\ No newline at end of file
diff --git a/src/styles/index.scss b/src/styles/index.scss
index c4972f9..143627f 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -23,8 +23,8 @@
@import "../pages/Photos/components/PhotosList/PhotoList.scss";
@import "../pages/Photos/components/Sort/Sort.scss";
@import "../components/CircleProgressBar/CircleProgressBar.scss";
-@import "../pages/Metrics/components/MetricsSimilarCard/MetricsSimilarCard.scss";
-@import "../pages/Metrics/components/MetricsSimilarList/MetricsSimilarList.scss";
-@import "../pages/Metrics/components/MetricsInfo/MetricsInfo.scss";
+@import "../pages/Metrics/sections/metrics/components/MetricsSimilarCard/MetricsSimilarCard.scss";
+@import "../pages/Metrics/sections/metrics/components/MetricsSimilarList/MetricsSimilarList.scss";
+@import "../pages/Metrics/sections/metrics/components/MetricsInfo/MetricsInfo.scss";
@import "../pages/Galleries/GalleriesPage.scss";
@import "../components/Loader/Loader.scss";