diff --git a/.eslintrc b/.eslintrc index 90c38fc2..6a186304 100644 --- a/.eslintrc +++ b/.eslintrc @@ -35,7 +35,7 @@ "indent": ["warn", "tab", { "SwitchCase": 1 }], "no-debugger": 0, "no-console": [ - "error", + "warn", { "allow": ["debug"] } diff --git a/package-lock.json b/package-lock.json index d3afab2f..9987e862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@babel/preset-react": "7.18.6", "@babel/preset-typescript": "7.18.6", "@babel/runtime": "7.20.1", + "@types/d3-ease": "^3.0.2", "@types/jest": "29.2.3", "@types/react": "18.0.25", "@types/react-dom": "18.0.9", @@ -32,6 +33,7 @@ "copy-webpack-plugin": "11.0.0", "core-js": "3.26.1", "css-loader": "6.7.2", + "d3-ease": "^3.0.1", "eslint": "8.27.0", "eslint-plugin-react": "7.31.10", "eslint-webpack-plugin": "^3.2.0", @@ -50,6 +52,7 @@ "markdown-loader": "8.0.0", "marked": "4.2.2", "mini-css-extract-plugin": "2.7.0", + "nanoid": "^5.0.6", "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "3.0.2", @@ -3054,6 +3057,12 @@ "@types/node": "*" } }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.37.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", @@ -5765,6 +5774,15 @@ "type": "^1.0.1" } }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -12778,9 +12796,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", + "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==", "dev": true, "funding": [ { @@ -12789,10 +12807,10 @@ } ], "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, "node_modules/nanomatch": { @@ -13846,6 +13864,24 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -19823,6 +19859,12 @@ "@types/node": "*" } }, + "@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true + }, "@types/eslint": { "version": "8.37.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", @@ -21927,6 +21969,12 @@ "type": "^1.0.1" } }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true + }, "data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -27241,9 +27289,9 @@ "optional": true }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", + "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==", "dev": true }, "nanomatch": { @@ -27980,6 +28028,14 @@ "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" + }, + "dependencies": { + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + } } }, "postcss-modules-extract-imports": { diff --git a/package.json b/package.json index 7e954cbc..0a34c484 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@babel/preset-react": "7.18.6", "@babel/preset-typescript": "7.18.6", "@babel/runtime": "7.20.1", + "@types/d3-ease": "^3.0.2", "@types/jest": "29.2.3", "@types/react": "18.0.25", "@types/react-dom": "18.0.9", @@ -87,6 +88,7 @@ "copy-webpack-plugin": "11.0.0", "core-js": "3.26.1", "css-loader": "6.7.2", + "d3-ease": "^3.0.1", "eslint": "8.27.0", "eslint-plugin-react": "7.31.10", "eslint-webpack-plugin": "^3.2.0", @@ -105,6 +107,7 @@ "markdown-loader": "8.0.0", "marked": "4.2.2", "mini-css-extract-plugin": "2.7.0", + "nanoid": "^5.0.6", "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "3.0.2", diff --git a/src/common.scss b/src/common.scss index fc23a709..9036ce67 100644 --- a/src/common.scss +++ b/src/common.scss @@ -1,5 +1,5 @@ $color-light: #fff; -$color-bg: #C7CDDE; +$color-bg: #c7cdde; $color-accent: darkred; $color-dark: #435794; $color-dark-lighten: #3f51b5; @@ -8,6 +8,25 @@ $color-base-lighten: #b1e9ea; $content-max-width: 1200px; $content-min-width: 320px; +$color-roulette-6: rgba(18, 4, 28, 1); +$color-roulette-5: rgba(58, 8, 95, 1); +$color-roulette-4: rgba(75, 30, 121, 1); +$color-roulette-3: rgb(92, 49, 151); +$color-roulette-2: rgb(131, 110, 179); +$color-roulette-1: rgb(186, 175, 213); + +//$color-roulette-5: rgba(10, 28, 102, 0.9); +//$color-roulette-4: rgba(10, 28, 102, 0.7); +//$color-roulette-3: rgba(10, 28, 102, 0.6); +//$color-roulette-2: rgba(10, 28, 102, 0.5); +//$color-roulette-1: rgba(10, 28, 102, 0.3); +// +//$color-roulette-5: rgba(63, 81, 181, 1); +//$color-roulette-4: rgba(63, 81, 181, 0.8); +//$color-roulette-3: rgba(63, 81, 181, 0.6); +//$color-roulette-2: rgba(63, 81, 181, 0.5); +//$color-roulette-1: rgba(63, 81, 181, 0.4); + @mixin container { width: 100%; height: 100%; @@ -16,5 +35,5 @@ $content-min-width: 320px; box-sizing: border-box; margin: auto; overflow-x: hidden; - padding: 20px; + padding: 20px; } diff --git a/src/components/pages/index.tsx b/src/components/pages/index.tsx index ba28e7e7..e6e5d9e3 100644 --- a/src/components/pages/index.tsx +++ b/src/components/pages/index.tsx @@ -8,6 +8,7 @@ import StagePaddingPage from './stage-padding'; import Events from './events'; import CustomComponents from './custom-components'; import LazyLoadingPage from './lazy-loading'; +import SandboxPage from './sandbox'; import './styles.scss'; export default function getPageComponent(pageID = '') { @@ -38,5 +39,8 @@ export default function getPageComponent(pageID = '') { if (pageID === 'lazy-loading') { return ; } + if (pageID === 'sandbox') { + return ; + } return null; } diff --git a/src/components/pages/sandbox/index.tsx b/src/components/pages/sandbox/index.tsx new file mode 100644 index 00000000..cd7d8d9f --- /dev/null +++ b/src/components/pages/sandbox/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import RouletteExample from './roulette/Roulette'; +import Anchor, { genAnchorProps } from '../../the-anchor'; + +export default function SandboxPage() { + return ( +
+

+ +   Roulette Animation +

+ +
+
+
+
+ ); +} diff --git a/src/components/pages/sandbox/roulette/Roulette.tsx b/src/components/pages/sandbox/roulette/Roulette.tsx new file mode 100644 index 00000000..e8f56a60 --- /dev/null +++ b/src/components/pages/sandbox/roulette/Roulette.tsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect } from 'react'; +// import { nanoid } from 'nanoid'; +// import { easeCircleOut } from 'd3-ease'; +// +// import markdown from './code-roulette.md'; +// import TheCode from '../../the-code'; +import AliceCarousel from '../../../../lib/react-alice-carousel'; +import { shuffleArray, genItems, genRandomInt } from './Roulette.utils'; +import type { Props, Item } from './Roulette.types'; + +const responsive = { + 0: { items: 1 }, + 640: { items: 3 }, + 768: { items: 5 }, +}; + +const itemsAmount = 100; +const itemsPadding = 10; + +function itemsToElements(items: Item[] = []) { + const itemsLeftPadding = items.slice(-itemsPadding); + const itemsRightPadding = items.slice(0, itemsPadding); + const itemsWithAnimationPadding = items.concat(items); + + return [...itemsLeftPadding, ...itemsWithAnimationPadding, ...itemsRightPadding].map((item, i) => { + return
; + }); +} + +function getOffset(itemsOnScreen: number) { + return Math.floor(itemsOnScreen / 2); +} + +function loadWinner() { + return new Promise<{ id: number }>((resolve) => { + setTimeout(() => resolve({ id: genRandomInt() }), 2_000); + }); +} + +export default function SandboxPage(props: Partial) { + const loadAnimationIndex = 10; + const loadAnimationDuration = 2_000; + const loadAnimationFunction = 'cubic-bezier(.97,-0.2,.77,.39)'; + + const rouletteAnimationDuration = 8_000; + const rouletteAnimationFunction = 'cubic-bezier(0,.4,.11,1.16)'; + + const [key, setKey] = useState(0); + const [items] = useState(shuffleArray(props.items || genItems())); + const [elements] = useState(itemsToElements(items)); + const [activeIndex, setActive] = useState(itemsPadding); + const [itemsOffset, setItemsOffset] = useState(0); + const [itemsOnScreen, setItemsOnScreen] = useState(0); + const [animationDuration, setAnimationDuration] = useState(loadAnimationDuration); + const [animationEasingFunction, setAnimationEasingFunction] = useState(loadAnimationFunction); + + const [winner, setWinner] = useState<{ id: number } | null>(null); + const [winnerId, setWinnerId] = useState(null); + const [isWinnerLoading, setSetIsWinnerLoading] = useState(false); + const [isWinnerHighlighted, setIsWinnerHighlighted] = useState(false); + const [isRouletteAnimationProcess, setIsRouletteAnimationProcess] = useState(false); + + useEffect(() => { + if (winner) { + setWinnerId(winner.id); + setAnimationDuration(rouletteAnimationDuration); + setAnimationEasingFunction(rouletteAnimationFunction); + } else { + setAnimationDuration(loadAnimationDuration); + setAnimationEasingFunction(loadAnimationFunction); + } + }, [winner]); + + function getRouletteAnimationIndex({ id = 0 }) { + const winnerIndex = items.findIndex((item) => item.id === id); + return winnerIndex + itemsAmount + itemsPadding - itemsOffset; + } + + function handleInit({ itemsInSlide = 0 }) { + setItemsOnScreen(itemsInSlide); + setItemsOffset(getOffset(itemsInSlide)); + } + + function handleSlideChange({ item = 0 }) { + if (winner) { + const nexIndex = getRouletteAnimationIndex(winner); + setActive(nexIndex); + setIsRouletteAnimationProcess(true); + + if (isRouletteAnimationProcess) { + setKey(key + 1); + setWinner(null); + setActive(nexIndex - itemsAmount); + setIsWinnerHighlighted(true); + setIsRouletteAnimationProcess(false); + } + } else { + // repeat load animation until the winner is loaded + setActive(item - 1); + } + } + + return ( +
+
+ +
+
+ {' '} + {' '} + {isWinnerLoading && 'Loading winner...'} + {!isWinnerLoading && winnerId && `Loaded winner: ${winnerId}`} +
+ ); +} diff --git a/src/components/pages/sandbox/roulette/Roulette.types.ts b/src/components/pages/sandbox/roulette/Roulette.types.ts new file mode 100644 index 00000000..d7d34f14 --- /dev/null +++ b/src/components/pages/sandbox/roulette/Roulette.types.ts @@ -0,0 +1,2 @@ +export type Item = Record<'id' | 'type', number>; +export type Props = { items: Item[] }; diff --git a/src/components/pages/sandbox/roulette/Roulette.utils.ts b/src/components/pages/sandbox/roulette/Roulette.utils.ts new file mode 100644 index 00000000..5595ea94 --- /dev/null +++ b/src/components/pages/sandbox/roulette/Roulette.utils.ts @@ -0,0 +1,61 @@ +export function genItems(length = 100) { + return Array.from({ length }).map((v, i) => { + const id = i++; + if (i % 7 === 0) return { id, type: 5 }; + if (i % 5 === 0) return { id, type: 4 }; + if (i % 3 === 0) return { id, type: 3 }; + if (i % 2 === 0) return { id, type: 2 }; + if (i % 2 === 1) return { id, type: 1 }; + return { id, type: i }; + }); +} + +export function genRandomInt(min = 0, max = 100) { + const minCeiled = Math.ceil(min); + const maxFloored = Math.floor(max); + return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled); // The maximum is inclusive and the minimum is inclusive +} + +export function shuffleArray(array: T[] = []) { + const shuffledArray = [...array]; + for (let i = shuffledArray.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; + } + return shuffledArray; +} + +// Function to shuffle array and try to maintain for the uniqueness of neighboring elements +export function shuffleArrayUniquely(items: T[] = [], key?: keyof T) { + const shuffledItems = shuffleArray(items); + const getItemValue = (item: T) => (key ? item[key] : item); + + // Iterate through the shuffled array and try to maintain for the uniqueness of neighboring elements + for (let i = 1; i < shuffledItems.length; i++) { + const currentItem = shuffledItems[i]; + const prevItem = shuffledItems[i - 1]; + + // if key passed we work with object, else with primitives + const prevItemValue = getItemValue(prevItem); + const currentItemValue = getItemValue(currentItem); + + if (currentItemValue === prevItemValue) { + let newItemIndex: null | number = null; + + const newItem = shuffledItems.find((item, itemIndex) => { + const itemValue = getItemValue(item); + if (itemIndex > i && itemValue !== currentItemValue) { + newItemIndex = itemIndex; + return true; + } + }); + + if (newItem && newItemIndex !== null) { + // Swap the current item with the found item + shuffledItems[i] = newItem; + shuffledItems[newItemIndex] = currentItem; + } + } + } + return shuffledItems; +} diff --git a/src/components/pages/sandbox/roulette/code.md b/src/components/pages/sandbox/roulette/code.md new file mode 100644 index 00000000..0d9781a0 --- /dev/null +++ b/src/components/pages/sandbox/roulette/code.md @@ -0,0 +1,28 @@ +```javascript +import React from 'react'; +import AliceCarousel from 'react-alice-carousel'; +import 'react-alice-carousel/lib/alice-carousel.css'; + +const responsive = { + 0: { items: 1 }, + 568: { items: 2 }, + 1024: { items: 3 }, +}; + +const items = [ +
1
, +
2
, +
3
, +
4
, +
5
, +]; + +const Carousel = () => ( + +); +``` diff --git a/src/components/pages/styles.scss b/src/components/pages/styles.scss index 5988dfc9..eeeb957f 100644 --- a/src/components/pages/styles.scss +++ b/src/components/pages/styles.scss @@ -33,6 +33,45 @@ li.alice-carousel__dots-item.__custom { } } +.text { + color: $color-dark; +} + +.b-roulette { + margin-bottom: 10px; + + .item { + padding: 4px; + } + .__target { + &:after { + border: none !important; + } + } + + &.__highlighted { + background: none; + } + + //&.__highlighted.__size-1 { + // .__target::after { + // border: solid 2px $color-base !important; + // } + //} + // + //&.__highlighted.__size-3 { + // .__target + li::after { + // border: solid 2px $color-base !important; + // } + //} + // + //&.__highlighted.__size-5 { + // .__target + li + li::after { + // border: solid 2px $color-base !important; + // } + //} +} + .alice-carousel__stage-item.__active { &:after { content: ''; @@ -86,6 +125,22 @@ li.alice-carousel__dots-item.__custom { bottom: 0; z-index: 1; } + + &.roulette-1:before { + background-color: $color-roulette-1; + } + &.roulette-2:before { + background-color: $color-roulette-2; + } + &.roulette-3:before { + background-color: $color-roulette-3; + } + &.roulette-4:before { + background-color: $color-roulette-4; + } + &.roulette-5:before { + background-color: $color-roulette-5; + } } .link { @@ -101,3 +156,34 @@ li.alice-carousel__dots-item.__custom { height: 100%; } } + +.b-content { + position: relative; +} + +.b-opacity { + top: 0; + height: 100%; + width: 100%; + position: absolute; + background: linear-gradient( + 90deg, + rgba($color-roulette-6, 0.6), + transparent 30%, + transparent 70%, + rgba($color-roulette-6, 1) + ); + + &:after { + content: ''; + width: 2px; + height: calc(100% + 10px); + left: calc(50% - 1px); + top: -5px; + position: absolute; + border-left-width: 2px; + border-left-style: dashed; + border-left-color: $color-dark-lighten; + z-index: 1; + } +} diff --git a/src/components/scheme.ts b/src/components/scheme.ts index fd19a107..d6b5001b 100644 --- a/src/components/scheme.ts +++ b/src/components/scheme.ts @@ -35,8 +35,8 @@ export default [ id: 'stage-padding', title: 'stage padding', }, - // { - // id: 'sandbox', - // title: 'sandbox', - // }, + { + id: 'sandbox', + title: 'sandbox', + }, ]; diff --git a/src/index.tsx b/src/index.tsx index 5ad524fe..e91eec3b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,9 +1,13 @@ -import React from 'react'; +import React, { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './components/app'; const element = document.getElementById('root'); if (element) { - createRoot(element).render(); + createRoot(element).render( + + + , + ); } diff --git a/src/lib/react-alice-carousel.tsx b/src/lib/react-alice-carousel.tsx index 87fca882..e24e925f 100644 --- a/src/lib/react-alice-carousel.tsx +++ b/src/lib/react-alice-carousel.tsx @@ -249,7 +249,7 @@ export default class AliceCarousel extends React.PureComponent { await this.setState(nextState); - this._handleResized(); + this._handleResized({ itemsInSlide: nextState.itemsInSlide }); this.isAnimationDisabled = false; isAutoPlaying && this._handlePlay(); } @@ -429,9 +429,9 @@ export default class AliceCarousel extends React.PureComponent { } } - _handleResized() { + _handleResized(e: Partial = {}) { if (this.props.onResized) { - this.props.onResized({ ...this.eventObject, type: EventType.RESIZE }); + this.props.onResized({ ...this.eventObject, ...e, type: EventType.RESIZE }); } }