From 145551ba1fa9e069916ccd8457a37de7b5763768 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9C=D0=B0?=
 =?UTF-8?q?=D0=BB=D1=8E=D0=B3=D0=B8=D0=BD?= <d.malygin@iqdev.digital>
Date: Thu, 30 Jan 2025 17:09:48 +0500
Subject: [PATCH] feat: 'Carousel' in process ("size", "buttonsBelow" and
 "theme" are remaining)

---
 src/Playground.vue                            |  27 ++++
 src/common/interfaces/componentsProps.ts      |   4 +-
 src/components/Carousel/Carousel.stories.ts   | 122 ++++++++++--------
 src/components/Carousel/Carousel.vue          |  84 ++++++++++--
 .../Carousel/CarouselArrowContainer.vue       |  50 +++----
 src/components/Carousel/helpers.ts            |  32 +++++
 6 files changed, 222 insertions(+), 97 deletions(-)
 create mode 100644 src/components/Carousel/helpers.ts

diff --git a/src/Playground.vue b/src/Playground.vue
index a101e80..b62d5b2 100644
--- a/src/Playground.vue
+++ b/src/Playground.vue
@@ -21,6 +21,7 @@ import Knob from '@components/Knob/Knob.vue';
 import Rating from '@components/Rating/Rating.vue';
 import HomeIcon from '@icons/Mono/HomeIcon.vue';
 import ProgressBar from '@components/ProgressBar/ProgressBar.vue';
+import Carousel from '@components/Carousel/Carousel.vue';
 
 const visibleDrawer = ref(false);
 const sliderOptions: ISliderOptions[] = [
@@ -201,6 +202,31 @@ const openDrawer = () => (visibleDrawer.value = true);
   <Checkbox v-model="activeCheckbox" size="large" />
   <Checkbox v-model="activeCheckbox" size="huge" />
   <ProgressBar v-model="pbValue" />
+  <Carousel
+    style="margin: 20px"
+    :itemsProps="[
+      {
+        index: 1,
+        text: 'This is SPARTA!',
+      },
+      {
+        index: 2,
+        text: 'This is the second item!',
+      },
+      {
+        index: 3,
+        text:
+          'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi atque blanditiis debitis distinctio, doloribus,\n' +
+          '        eius est eveniet excepturi facere id iure laboriosam laborum libero, minus nesciunt nostrum nulla repellat\n' +
+          '        veritatis.',
+      },
+    ]"
+  >
+    <template v-slot="item: unknown">
+      <h2 style="text-align: center; margin-bottom: 20px">Element {{ item?.index }}</h2>
+      <p>{{ item?.text }}</p></template
+    >
+  </Carousel>
   {{ tableData[1] }}
   <Table
     center
@@ -211,6 +237,7 @@ const openDrawer = () => (visibleDrawer.value = true);
     theme="black"
     stripedRows
     paginator
+    editable
     :no-editing-settings="{
       cells: [[0, 0]],
     }"
diff --git a/src/common/interfaces/componentsProps.ts b/src/common/interfaces/componentsProps.ts
index 829d2a3..730438a 100644
--- a/src/common/interfaces/componentsProps.ts
+++ b/src/common/interfaces/componentsProps.ts
@@ -81,10 +81,12 @@ export interface IPaginatorProps {
 
 export interface ICarouselProps {
   itemsProps: unknown[];
-  width?: string;
+  innerWidth?: string;
   size?: TSize;
   perView?: number;
   perScroll?: number;
+  circular?: boolean;
+  buttonsBelow?: boolean;
   theme?: TThemeColor;
   darknessTheme?: TDarkness;
 }
diff --git a/src/components/Carousel/Carousel.stories.ts b/src/components/Carousel/Carousel.stories.ts
index a69e2a2..e23bf89 100644
--- a/src/components/Carousel/Carousel.stories.ts
+++ b/src/components/Carousel/Carousel.stories.ts
@@ -14,25 +14,14 @@ const meta: Meta = {
     },
   },
   argTypes: {
-    buttons: { control: 'boolean' },
-    showLabel: { control: 'boolean' },
-    colorAsTheme: { control: 'boolean' },
-    textBold: { control: 'boolean' },
-    min: { control: 'number' },
-    max: { control: 'number' },
-    step: { control: 'number' },
-    fontSize: { control: 'text' },
-    textBefore: { control: 'text' },
-    textAfter: { control: 'text' },
-    colorGaps: { control: 'object' },
+    itemsProps: { control: 'object' },
+    innerWidth: { control: 'text' },
+    perView: { control: 'number' },
+    perScroll: { control: 'number' },
+    circular: { control: 'boolean' },
+    buttonsBelow: { control: 'boolean' },
     size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] },
-    background: { control: 'color' },
     darknessTheme: { control: 'select', options: ['100', '200', '300', '400', '500', '600', '700', '800', '900'] },
-    darknessNegativeTheme: {
-      control: 'select',
-      options: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
-    },
-    darknessColor: { control: 'select', options: ['100', '200', '300', '400', '500', '600', '700', '800', '900'] },
     theme: {
       control: 'select',
       options: [
@@ -53,46 +42,6 @@ const meta: Meta = {
         'black',
       ],
     },
-    negativeTheme: {
-      control: 'select',
-      options: [
-        'white',
-        'blue',
-        'sky',
-        'cyan',
-        'teal',
-        'green',
-        'yellow',
-        'orange',
-        'pink',
-        'fuchsia',
-        'purple',
-        'indigo',
-        'rose',
-        'red',
-        'black',
-      ],
-    },
-    color: {
-      control: 'select',
-      options: [
-        'white',
-        'blue',
-        'sky',
-        'cyan',
-        'teal',
-        'green',
-        'yellow',
-        'orange',
-        'pink',
-        'fuchsia',
-        'purple',
-        'indigo',
-        'rose',
-        'red',
-        'black',
-      ],
-    },
   },
 } satisfies Meta<typeof Carousel>;
 
@@ -103,3 +52,62 @@ type Story = StoryObj<typeof meta>;
 export const Simple: Story = {
   args: {},
 };
+
+export const Half: Story = {
+  args: {
+    circular: true,
+    perView: 2,
+    perScroll: 1,
+    itemsProps: [
+      {
+        header: 'First',
+        text: 'Some text',
+      },
+      {
+        header: 'Second',
+        text: 'Some text',
+      },
+      {
+        header: 'Third',
+        text: 'Some text',
+      },
+      {
+        header: 'Forth',
+        text: 'Some text',
+      },
+    ],
+  },
+};
+
+export const Full: Story = {
+  args: {
+    circular: true,
+    perView: 2,
+    perScroll: 2,
+
+    itemsProps: [
+      {
+        header: 'First',
+        text: 'Some text',
+      },
+      {
+        header: 'Second',
+        text: 'Some text',
+      },
+      {
+        header: 'Third',
+        text: 'Some text',
+      },
+      {
+        header: 'Forth',
+        text: 'Some text',
+      },
+      {
+        header: 'Fifth',
+        text: 'Some text',
+      },
+    ],
+
+    buttonsBelow: true,
+  },
+};
diff --git a/src/components/Carousel/Carousel.vue b/src/components/Carousel/Carousel.vue
index e9310a7..ae856fb 100644
--- a/src/components/Carousel/Carousel.vue
+++ b/src/components/Carousel/Carousel.vue
@@ -5,19 +5,26 @@ import { computed, ref } from 'vue';
 import { convertThemeToColor, convertThemeToTextColor } from '@helpers/common';
 import ArrowLeftShortIcon from '@icons/Mono/ArrowLeftShortIcon.vue';
 import ArrowRightShortIcon from '@icons/Mono/ArrowRightShortIcon.vue';
+import { defaultProps, getNewValue } from './helpers';
 
 const props = withDefaults(defineProps<ICarouselProps>(), {
-  itemsProps: () => [],
   size: 'normal',
+  innerWidth: '300px',
+  perView: 1,
+  perScroll: 1,
+  theme: 'white',
+  darknessTheme: '500',
 });
 
 const current = ref(1);
 
-const itemsLength = computed(() => Math.ceil(props.itemsProps.length / props.perView));
+const itemsLength = computed(() => props.itemsProps?.length ?? 3);
 const color = computed(() => convertThemeToColor(props.theme, props.darknessTheme));
 const textColor = computed(() => convertThemeToTextColor(props.theme, props.darknessTheme));
-const isStartDisabled = computed(() => current.value === 1);
-const isEndDisabled = computed(() => current.value === itemsLength.value);
+const isStartDisabled = computed(() => (props.circular ? false : current.value === 1 || itemsLength.value <= 1));
+const isEndDisabled = computed(() =>
+  props.circular ? false : current.value === Math.ceil(itemsLength.value / props.perView) || !itemsLength.value,
+);
 const iconSize = computed(() => {
   const size = props.size;
   if (size === 'normal') return '10';
@@ -25,22 +32,83 @@ const iconSize = computed(() => {
   if (size === 'huge') return '18';
   return '7';
 });
+const itemWidth = computed(() => `calc(${props.innerWidth} / ${props.perView}`);
+const translate = computed(() => `translateX(calc(-${props.innerWidth} / ${props.perView} * ${current.value - 1}))`);
 </script>
 
 <template>
-  <section :style="`width: ${width}; min-height: 100px`" class="carouselContainer">
-    <CarouselArrowContainer :textColor="textColor" :color="color" :disable="isStartDisabled">
+  <section class="carouselContainer">
+    <CarouselArrowContainer
+      @click="!isStartDisabled ? (current = getNewValue('-', current, itemsLength, perScroll, perView)) : null"
+      :textColor="textColor"
+      :color="color"
+      :disable="isStartDisabled"
+    >
       <ArrowLeftShortIcon :color="isStartDisabled ? '#aaa' : textColor" :size="iconSize" />
     </CarouselArrowContainer>
-    <slot />
-    <CarouselArrowContainer :textColor="textColor" :color="color" :disable="isEndDisabled">
+    <div class="content">
+      <ul class="list">
+        <li v-for="item of Array(itemsLength).keys()" :key="item" class="item">
+          <slot v-bind="itemsProps?.[item]" :key="current - 1" />
+          <div v-if="!$slots.default && !itemsProps">
+            <h2 style="text-align: center; margin-bottom: 10px">{{ defaultProps[item].header }}</h2>
+            <p>
+              {{ defaultProps[item].text }}
+            </p>
+          </div>
+          <div v-else-if="!$slots.default">
+            <h2 style="text-align: center; margin-bottom: 10px">{{ itemsProps[item].header }}</h2>
+            <p>
+              {{ itemsProps[item].text }}
+            </p>
+          </div>
+        </li>
+      </ul>
+    </div>
+    <CarouselArrowContainer
+      @click="!isEndDisabled ? (current = getNewValue('+', current, itemsLength, perScroll, perView)) : null"
+      :textColor="textColor"
+      :color="color"
+      :disable="isEndDisabled"
+    >
       <ArrowRightShortIcon :color="isEndDisabled ? '#aaa' : textColor" :size="iconSize" />
     </CarouselArrowContainer>
+    <ul class="buttons">
+      <li
+        v-for="item of Array(itemsLength).keys()"
+        :key="item"
+        class="button"
+        :style="`width: ${iconSize}px; height: ${iconSize}px`"
+      ></li>
+    </ul>
   </section>
 </template>
 
 <style scoped>
 .carouselContainer {
   display: flex;
+  min-height: 100px;
+  position: relative;
+}
+.content {
+  max-width: v-bind(innerWidth);
+  overflow: hidden;
+}
+.list {
+  display: flex;
+  transform: v-bind(translate);
+  transition: transform 0.3s ease-out;
+}
+.item {
+  min-width: v-bind(itemWidth);
+}
+.buttons {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  transform: translateX(-50%);
+}
+.button {
+  background-color: v-bind(color);
 }
 </style>
diff --git a/src/components/Carousel/CarouselArrowContainer.vue b/src/components/Carousel/CarouselArrowContainer.vue
index 7dbf50e..4e15837 100644
--- a/src/components/Carousel/CarouselArrowContainer.vue
+++ b/src/components/Carousel/CarouselArrowContainer.vue
@@ -7,23 +7,23 @@ defineProps<{
 </script>
 
 <template>
-  <div class="arrowContainer">
+  <div
+    :class="[
+      'arrowContainer',
+      {
+        disable,
+      },
+    ]"
+  >
     <div
       :class="[
-        'icon',
+        'bg',
         {
-          disable,
+          disableBg: disable,
         },
       ]"
-    >
-      <div
-        :class="[
-          'bg',
-          {
-            disableBg: disable,
-          },
-        ]"
-      ></div>
+    ></div>
+    <div class="icon">
       <slot />
     </div>
   </div>
@@ -31,49 +31,37 @@ defineProps<{
 
 <style scoped>
 .arrowContainer {
-  width: 50px;
+  position: relative;
+  min-width: 50px;
   min-height: 100%;
   display: flex;
   justify-content: center;
   align-items: center;
+  cursor: pointer;
 }
 .icon {
-  position: relative;
   display: flex;
   justify-content: center;
   align-items: center;
-  cursor: pointer;
   line-height: 1.2;
   color: v-bind(textColor);
 }
-.icon::before {
-  content: '';
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  border-radius: 50%;
-  z-index: -1;
-  background-color: v-bind(color);
-}
-.icon:hover > .bg {
+.arrowContainer:hover > .bg {
   background-color: v-bind(textColor);
   opacity: 0.1;
 }
-.icon:active > .bg {
+.arrowContainer:active > .bg {
   opacity: 0.2;
 }
 .bg {
   width: 100%;
   height: 100%;
   position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
   padding: 10px;
   z-index: 5;
-  border-radius: 50%;
-  background-color: transparent;
   opacity: 0;
+  border-radius: 5px;
+  background-color: transparent;
   transition: all 0.2s ease;
 }
 .disable {
diff --git a/src/components/Carousel/helpers.ts b/src/components/Carousel/helpers.ts
new file mode 100644
index 0000000..81d1016
--- /dev/null
+++ b/src/components/Carousel/helpers.ts
@@ -0,0 +1,32 @@
+export const getNewValue = (
+  action: '+' | '-',
+  current: number,
+  itemsLength: number,
+  perScroll: number,
+  perView: number,
+): number => {
+  const additional = perView - 1;
+  if (action === '+') {
+    if (current + additional === itemsLength) return 1;
+    if (current + perScroll >= itemsLength) return itemsLength - additional;
+    return current + perScroll;
+  }
+  if (current === 1) return itemsLength - additional;
+  if (current - perScroll < 1) return 1;
+  return current - perScroll;
+};
+
+export const defaultProps = [
+  {
+    header: 'Element â„–1',
+    text: 'This is the first element.',
+  },
+  {
+    header: 'Element â„–2',
+    text: 'Oh...one more!',
+  },
+  {
+    header: 'Element â„–3',
+    text: 'I feel sick.....',
+  },
+];
-- 
GitLab