diff --git a/services/web-ui/package-lock.json b/services/web-ui/package-lock.json index 205f725d..eef2feb9 100644 --- a/services/web-ui/package-lock.json +++ b/services/web-ui/package-lock.json @@ -56,6 +56,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/lodash": "^4.14.191", "@types/react-plotly.js": "^2.6.0", + "@types/react-swipeable-views": "^0.13.3", "@typescript-eslint/eslint-plugin": "5.54.1", "@typescript-eslint/parser": "5.54.1", "babel-plugin-named-exports-order": "^0.0.2", @@ -66,6 +67,7 @@ "jest": "^29.5.0", "prettier": "2.8.4", "prop-types": "^15.8.1", + "react-swipeable-views": "^0.14.0", "standard-version": "9.5.0", "storybook": "^7.0.11", "storybook-addon-react-router-v6": "^1.0.2", @@ -8315,6 +8317,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-swipeable-views": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@types/react-swipeable-views/-/react-swipeable-views-0.13.3.tgz", + "integrity": "sha512-gVAQb5AxZTSLVTrJ/Fxwsk0axdBqGzXC8NxAD8MNwEf+qZynsb+15KL9TpNCaGGk4SCE2iyU/JNi6nGNB61AyA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", @@ -19951,6 +19962,12 @@ "node": ">=4.0" } }, + "node_modules/keycode": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", + "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==", + "dev": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -23412,6 +23429,20 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-event-listener": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.6.tgz", + "integrity": "sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.2.0", + "prop-types": "^15.6.0", + "warning": "^4.0.1" + }, + "peerDependencies": { + "react": "^16.3.0" + } + }, "node_modules/react-i18next": { "version": "12.3.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.3.1.tgz", @@ -24927,6 +24958,100 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "node_modules/react-swipeable-views": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-swipeable-views/-/react-swipeable-views-0.14.0.tgz", + "integrity": "sha512-wrTT6bi2nC3JbmyNAsPXffUXLn0DVT9SbbcFr36gKpbaCgEp7rX/OFxsu5hPc/NBsUhHyoSRGvwqJNNrWTwCww==", + "dev": true, + "dependencies": { + "@babel/runtime": "7.0.0", + "prop-types": "^15.5.4", + "react-swipeable-views-core": "^0.14.0", + "react-swipeable-views-utils": "^0.14.0", + "warning": "^4.0.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/react-swipeable-views-core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-swipeable-views-core/-/react-swipeable-views-core-0.14.0.tgz", + "integrity": "sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA==", + "dev": true, + "dependencies": { + "@babel/runtime": "7.0.0", + "warning": "^4.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/react-swipeable-views-core/node_modules/@babel/runtime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", + "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.12.0" + } + }, + "node_modules/react-swipeable-views-core/node_modules/regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true + }, + "node_modules/react-swipeable-views-utils": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-swipeable-views-utils/-/react-swipeable-views-utils-0.14.0.tgz", + "integrity": "sha512-W+fXBOsDqgFK1/g7MzRMVcDurp3LqO3ksC8UgInh2P/tKgb5DusuuB1geKHFc6o1wKl+4oyER4Zh3Lxmr8xbXA==", + "dev": true, + "dependencies": { + "@babel/runtime": "7.0.0", + "keycode": "^2.1.7", + "prop-types": "^15.6.0", + "react-event-listener": "^0.6.0", + "react-swipeable-views-core": "^0.14.0", + "shallow-equal": "^1.2.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/react-swipeable-views-utils/node_modules/@babel/runtime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", + "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.12.0" + } + }, + "node_modules/react-swipeable-views-utils/node_modules/regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true + }, + "node_modules/react-swipeable-views/node_modules/@babel/runtime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", + "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.12.0" + } + }, + "node_modules/react-swipeable-views/node_modules/regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -25947,6 +26072,12 @@ "node": ">=8" } }, + "node_modules/shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -27772,6 +27903,15 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -34442,6 +34582,15 @@ "@types/react": "*" } }, + "@types/react-swipeable-views": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@types/react-swipeable-views/-/react-swipeable-views-0.13.3.tgz", + "integrity": "sha512-gVAQb5AxZTSLVTrJ/Fxwsk0axdBqGzXC8NxAD8MNwEf+qZynsb+15KL9TpNCaGGk4SCE2iyU/JNi6nGNB61AyA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-transition-group": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", @@ -43152,6 +43301,12 @@ "object.assign": "^4.1.3" } }, + "keycode": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", + "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==", + "dev": true + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -45512,6 +45667,17 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-event-listener": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.6.tgz", + "integrity": "sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.2.0", + "prop-types": "^15.6.0", + "warning": "^4.0.1" + } + }, "react-i18next": { "version": "12.3.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.3.1.tgz", @@ -46645,6 +46811,94 @@ } } }, + "react-swipeable-views": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-swipeable-views/-/react-swipeable-views-0.14.0.tgz", + "integrity": "sha512-wrTT6bi2nC3JbmyNAsPXffUXLn0DVT9SbbcFr36gKpbaCgEp7rX/OFxsu5hPc/NBsUhHyoSRGvwqJNNrWTwCww==", + "dev": true, + "requires": { + "@babel/runtime": "7.0.0", + "prop-types": "^15.5.4", + "react-swipeable-views-core": "^0.14.0", + "react-swipeable-views-utils": "^0.14.0", + "warning": "^4.0.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", + "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.12.0" + } + }, + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true + } + } + }, + "react-swipeable-views-core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-swipeable-views-core/-/react-swipeable-views-core-0.14.0.tgz", + "integrity": "sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA==", + "dev": true, + "requires": { + "@babel/runtime": "7.0.0", + "warning": "^4.0.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", + "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.12.0" + } + }, + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true + } + } + }, + "react-swipeable-views-utils": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-swipeable-views-utils/-/react-swipeable-views-utils-0.14.0.tgz", + "integrity": "sha512-W+fXBOsDqgFK1/g7MzRMVcDurp3LqO3ksC8UgInh2P/tKgb5DusuuB1geKHFc6o1wKl+4oyER4Zh3Lxmr8xbXA==", + "dev": true, + "requires": { + "@babel/runtime": "7.0.0", + "keycode": "^2.1.7", + "prop-types": "^15.6.0", + "react-event-listener": "^0.6.0", + "react-swipeable-views-core": "^0.14.0", + "shallow-equal": "^1.2.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", + "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.12.0" + } + }, + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true + } + } + }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -47402,6 +47656,12 @@ "kind-of": "^6.0.2" } }, + "shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", + "dev": true + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -48763,6 +49023,15 @@ "makeerror": "1.0.12" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/services/web-ui/package.json b/services/web-ui/package.json index e0c6cec3..9600952d 100644 --- a/services/web-ui/package.json +++ b/services/web-ui/package.json @@ -75,6 +75,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/lodash": "^4.14.191", "@types/react-plotly.js": "^2.6.0", + "@types/react-swipeable-views": "^0.13.3", "@typescript-eslint/eslint-plugin": "5.54.1", "@typescript-eslint/parser": "5.54.1", "babel-plugin-named-exports-order": "^0.0.2", @@ -85,6 +86,7 @@ "jest": "^29.5.0", "prettier": "2.8.4", "prop-types": "^15.8.1", + "react-swipeable-views": "^0.14.0", "standard-version": "9.5.0", "storybook": "^7.0.11", "storybook-addon-react-router-v6": "^1.0.2", diff --git a/services/web-ui/src/components/ui/Carousel/Carousel.stories.tsx b/services/web-ui/src/components/ui/Carousel/Carousel.stories.tsx new file mode 100644 index 00000000..df69d495 --- /dev/null +++ b/services/web-ui/src/components/ui/Carousel/Carousel.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Carousel } from "."; + +const IMAGES = [ + { + label: "San Francisco – Oakland Bay Bridge, United States", + src: "https://images.unsplash.com/photo-1537944434965-cf4679d1a598?auto=format&fit=crop&w=400&h=250&q=60", + }, + { + label: "Bird", + src: "https://images.unsplash.com/photo-1538032746644-0212e812a9e7?auto=format&fit=crop&w=400&h=250&q=60", + }, + { + label: "Bali, Indonesia", + src: "https://images.unsplash.com/photo-1537996194471-e657df975ab4?auto=format&fit=crop&w=400&h=250", + }, + { + label: "Goč, Serbia", + src: "https://images.unsplash.com/photo-1512341689857-198e7e2f3ca8?auto=format&fit=crop&w=400&h=250&q=60", + }, +]; + +export default { + title: "Core/Carousel", + component: Carousel, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + images: IMAGES, + }, +}; + +export const SingleImage: Story = { + args: { + images: IMAGES.slice(0, 1), + }, +}; diff --git a/services/web-ui/src/components/ui/Carousel/Carousel.tsx b/services/web-ui/src/components/ui/Carousel/Carousel.tsx new file mode 100644 index 00000000..c0a0e5b6 --- /dev/null +++ b/services/web-ui/src/components/ui/Carousel/Carousel.tsx @@ -0,0 +1,112 @@ +import { ReactElement, useState } from "react"; +import SwipeableViews from "react-swipeable-views"; + +import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; +import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import MobileStepper from "@mui/material/MobileStepper"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; + +interface CarouselImage { + label: string; + src: string; +} + +export interface CarouselProps { + images: CarouselImage[]; +} + +export function Carousel({ images }: CarouselProps): ReactElement { + const theme = useTheme(); + const [activeStep, setActiveStep] = useState(0); + const maxSteps = images.length; + + const handleNext = () => { + setActiveStep((prevActiveStep) => prevActiveStep + 1); + }; + + const handleBack = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1); + }; + + const handleStepChange = (step: number) => { + setActiveStep(step); + }; + + return ( + + + {images[activeStep].label} + + + + {images.map((image, index) => ( +
+ {Math.abs(activeStep - index) <= 2 && ( + + )} +
+ ))} +
+ + + Next + {theme.direction === "rtl" ? ( + + ) : ( + + )} + + } + backButton={ + + } + /> +
+ ); +} diff --git a/services/web-ui/src/components/ui/Carousel/index.ts b/services/web-ui/src/components/ui/Carousel/index.ts new file mode 100644 index 00000000..8be87703 --- /dev/null +++ b/services/web-ui/src/components/ui/Carousel/index.ts @@ -0,0 +1 @@ +export { Carousel } from "./Carousel";