diff --git a/examples/react-dynamic/App.tsx b/examples/react-dynamic/App.tsx new file mode 100644 index 0000000..7c56973 --- /dev/null +++ b/examples/react-dynamic/App.tsx @@ -0,0 +1,114 @@ +import { useEffect } from 'react' +import './style.css' +import { createSwapy } from '../../src/index' +import { useState } from 'react' +import { SlotItemMap, Swapy } from '../../src/instance' +import { useMemo } from 'react' +import { useRef } from 'react' + +function App() { + const swapyRef = useRef(null) + + // Your items. + const [items, setItems] = useState([ + { id: '1', title: 'A' }, + { id: '2', title: 'B' }, + { id: '3', title: 'C' }, + ]) + + // Define a state for maintain the mapping between slots and items. + const [slotItemsMap, setSlotItemsMap] = useState([...items.map(item => ({ + slotId: item.id, + itemId: item.id + })), + // Defining an empty slot by setting itemId to null. + { slotId: `${Math.round(Math.random() * 99999)}`, itemId: null } + ]) + + // This is what you'll use to display your items. + const slottedItems = useMemo(() => slotItemsMap.map(({ slotId, itemId }) => ({ + slotId, + itemId, + item: items.find(item => item.id === itemId) + })), [items, slotItemsMap]) + + useEffect(() => { + // Get the newly added items and convert them to slotItem objects + const newItems = items.filter(item => !slotItemsMap.some(slotItem => slotItem.itemId === item.id)).map(item => ({ + slotId: item.id, + itemId: item.id + })) + + // Remove items from slotItemsMap if they no longer exist in items + const withoutRemovedItems = slotItemsMap.filter(slotItem => + items.some(item => item.id === slotItem.itemId) || !slotItem.itemId + ) + + /******* Below is how you would remove items and keep their slots empty ******/ + // const withoutRemovedItems = slotItemsMap.map(slotItem => { + // if (!items.some(item => item.id === slotItem.itemId)) { + // return { slotId: slotItem.slotId, itemId: null } + // } + // return slotItem + // }) + + const updatedSlotItemsMap = [...withoutRemovedItems, ...newItems] + + setSlotItemsMap(updatedSlotItemsMap) + swapyRef.current?.setData({ array: updatedSlotItemsMap }) + }, [items]) + + useEffect(() => { + const container = document.querySelector('.container')! + swapyRef.current = createSwapy(container, { + manualSwap: true + }) + + swapyRef.current.onSwap(({ data }) => { + // You need to call setData because it's a manualSwap instance + swapyRef.current?.setData({ array: data.array }) + setSlotItemsMap(data.array) + }) + + return () => { + swapyRef.current?.destroy() + } + }, []) + + return ( +
+ + {/* ADD BUTTON */} + + +
+ {slottedItems.map(({ itemId, slotId, item }) => ( +
+ + {/* ITEM */} + {item ? +
+
+
{item.title}
+ + {/* DELETE ITEM BUTTON */} + + +
+ : null} +
+ ))} +
+
+ ) +} + +export default App diff --git a/examples/react-dynamic/index.html b/examples/react-dynamic/index.html new file mode 100644 index 0000000..eb5be82 --- /dev/null +++ b/examples/react-dynamic/index.html @@ -0,0 +1,16 @@ + + + + + + React Swapy Example + + + +
+ + + diff --git a/examples/react-dynamic/main.tsx b/examples/react-dynamic/main.tsx new file mode 100644 index 0000000..f6c7f06 --- /dev/null +++ b/examples/react-dynamic/main.tsx @@ -0,0 +1,5 @@ +import { createRoot } from 'react-dom/client' +import App from './App' + +const root = createRoot(document.getElementById('app')!) +root.render() diff --git a/examples/react-dynamic/style.css b/examples/react-dynamic/style.css new file mode 100644 index 0000000..b7b148f --- /dev/null +++ b/examples/react-dynamic/style.css @@ -0,0 +1,100 @@ +* { + box-sizing: border-box; +} + +:root { + background: #242424; +} + +body, +#app { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + width: 100%; + min-height: 100dvh; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + margin: 0; +} + +.app { + width: 100%; + max-width: 600px; +} + +.container { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + max-width: 500px; + padding: 10px; +} + +.second-row { + display: flex; + gap: 5px; +} + +.slot { + background: #111; + flex: 1; + flex-basis: 80px; + height: 80px; +} + +.item { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 40px; + user-select: none; + -webkit-user-select: none; + position: relative; + background: #508db9; +} + +[data-swapy-highlighted] { + background: #444; +} + +.handle { + position: absolute; + left: 0; + top: 0; + width: 40px; + height: 100%; + background: rgba(0, 0, 0, 0.5); + cursor: pointer; +} + +.add { + position: fixed; + top: 10px; + left: 10px; + border-radius: 5px; + background: white; + width: 100px; + padding: 5px 0; + cursor: pointer; +} + +.delete { + color: #222; + background: rgba(255, 255, 255, 0.5); + width: 30px; + height: 30px; + font-size: 18px; + border-radius: 50%; + position: absolute; + top: 10px; + right: 10px; + display: flex; + align-items: center; + justify-content: center; +}