Skip to content

Commit 5c7527f

Browse files
authored
Merge pull request #31 from maemreyo/feat/imprv-ui
Feat/imprv UI
2 parents 596c39f + eea38b9 commit 5c7527f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1649
-746
lines changed

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.0.11",
2+
"version": "0.0.12",
33
"publishConfig": {
44
"access": "public",
55
"registry": "https://registry.npmjs.org/"
@@ -112,6 +112,7 @@
112112
"@testing-library/dom": "^10.0.0",
113113
"@testing-library/jest-dom": "^6.6.3",
114114
"@testing-library/react": "^16.1.0",
115+
"@types/lodash": "^4",
115116
"@types/react": "^19.0.2",
116117
"@types/react-dom": "^19.0.2",
117118
"@types/styled-components": "^5.1.34",
@@ -148,6 +149,9 @@
148149
},
149150
"dependencies": {
150151
"@hookform/resolvers": "^3.9.1",
152+
"lodash": "^4.17.21",
153+
"react-dnd": "^16.0.1",
154+
"react-dnd-html5-backend": "^16.0.1",
151155
"react-hook-form": "^7.49.3",
152156
"styled-components": "^6.1.13",
153157
"yup": "^1.6.1"

src/DynamicForm.stories.tsx

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Filepath: /src/DynamicForm.stories.tsx
21
import React from 'react';
32
import { Meta, StoryFn } from '@storybook/react';
43
import { fn } from '@storybook/test';
@@ -36,7 +35,14 @@ BasicInputTypes.args = {
3635
firstName: {
3736
label: 'First Name',
3837
type: 'text',
39-
// defaultValue: 'John',
38+
defaultValue: 'John',
39+
inputProps: {
40+
placeholder: 'Enter your first name',
41+
},
42+
validation: {
43+
required: { value: true, message: 'This field is required' },
44+
minLength: { value: 3, message: 'Minimum length is 3' },
45+
},
4046
},
4147
lastName: {
4248
label: 'Last Name',
@@ -52,6 +58,9 @@ BasicInputTypes.args = {
5258
label: 'Age',
5359
type: 'number',
5460
// defaultValue: 30,
61+
inputProps: {
62+
disabled: true,
63+
},
5564
},
5665
subscribe: {
5766
label: 'Subscribe to newsletter?',
@@ -75,6 +84,9 @@ AdvancedInputTypes.args = {
7584
label: 'Start Date',
7685
type: 'date',
7786
defaultValue: '2023-11-20',
87+
validation: {
88+
required: { value: true, message: 'Start date is required' },
89+
},
7890
},
7991
startTime: {
8092
label: 'Start Time',
@@ -116,16 +128,6 @@ AdvancedInputTypes.args = {
116128
type: 'switch',
117129
defaultValue: true,
118130
},
119-
favoriteFruit: {
120-
label: 'Favorite Fruit',
121-
type: 'combobox',
122-
defaultValue: 'Apple',
123-
options: [
124-
{ value: 'Apple', label: 'Apple' },
125-
{ value: 'Banana', label: 'Banana' },
126-
{ value: 'Orange', label: 'Orange' },
127-
],
128-
},
129131
},
130132
onSubmit: (data) => {
131133
console.log('🚀 ~ file: DynamicForm.stories.tsx ~ data:', data);
@@ -864,3 +866,69 @@ CustomInput.args = {
864866
onFormReady: fn(),
865867
};
866868
CustomInput.storyName = 'Custom Input (ColorPicker)';
869+
870+
// Story 8: ComboBox Input
871+
// Mock data for ComboBox
872+
const mockComboBoxData = [
873+
{ value: 'apple', label: 'Apple' },
874+
{ value: 'banana', label: 'Banana' },
875+
{ value: 'orange', label: 'Orange' },
876+
{ value: 'grape', label: 'Grape' },
877+
{ value: 'watermelon', label: 'Watermelon' },
878+
{ value: 'pineapple', label: 'Pineapple' },
879+
{ value: 'mango', label: 'Mango' },
880+
{ value: 'strawberry', label: 'Strawberry' },
881+
{ value: 'blueberry', label: 'Blueberry' },
882+
{ value: 'raspberry', label: 'Raspberry' },
883+
];
884+
885+
// Mock search API function for ComboBox
886+
const mockSearchApi = async (params: { query: string }) => {
887+
return new Promise<{ data: { value: string; label: string }[] }>(
888+
(resolve) => {
889+
setTimeout(() => {
890+
const filteredData = mockComboBoxData.filter((item) =>
891+
item.label.toLowerCase().includes(params.query.toLowerCase())
892+
);
893+
resolve({ data: filteredData });
894+
}, 500); // Simulate 500ms delay
895+
}
896+
);
897+
};
898+
899+
export const ComboBoxInput = Template.bind({});
900+
ComboBoxInput.args = {
901+
theme: defaultTheme,
902+
config: {
903+
favoriteFruit: {
904+
label: 'Favorite Fruit',
905+
type: 'combobox',
906+
inputProps: {
907+
placeholder: 'Search for a fruit...',
908+
searchApi: mockSearchApi,
909+
noResultsMessage: 'No fruits found.',
910+
loadingMessage: 'Loading fruits...',
911+
disabled: false,
912+
required: true,
913+
},
914+
validation: {
915+
validate: (value) => {
916+
console.log('🚀 ~ file: DynamicForm.stories.tsx ~ value:', value);
917+
if (!value) {
918+
return 'This field is required';
919+
}
920+
if (value.length < 3) {
921+
return 'Please select at least 3 fruits';
922+
}
923+
return undefined;
924+
},
925+
},
926+
},
927+
},
928+
onSubmit: (data) => {
929+
console.log('🚀 ~ file: DynamicForm.stories.tsx ~ data:', data);
930+
alert(JSON.stringify(data));
931+
},
932+
onFormReady: fn(),
933+
};
934+
ComboBoxInput.storyName = 'ComboBox Input';

src/DynamicForm.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Filepath: /src/DynamicForm.tsx
21
import React, { useMemo } from 'react';
32
import {
43
useDynamicForm,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react';
2+
import { DraggableItemProps } from './types';
3+
import { useDrag, useDrop } from 'react-dnd';
4+
import { DraggableItemWrapper } from './styled';
5+
6+
const ItemType = {
7+
ITEM: 'ITEM',
8+
} as const;
9+
10+
interface DragItem<T> {
11+
index: number;
12+
item: T;
13+
}
14+
15+
const DraggableItem = <T,>({
16+
item,
17+
index,
18+
onDragStart,
19+
onDragOver,
20+
onDrop,
21+
renderItem,
22+
}: DraggableItemProps<T>) => {
23+
const isDisabled = (item as any).disabled;
24+
25+
const [{ isDragging }, dragRef] = useDrag<
26+
DragItem<T>,
27+
unknown,
28+
{ isDragging: boolean }
29+
>({
30+
type: ItemType.ITEM,
31+
item: { index, item },
32+
canDrag: !isDisabled,
33+
collect: (monitor) => ({
34+
isDragging: monitor.isDragging(),
35+
}),
36+
});
37+
38+
const [{ isOver }, dropRef] = useDrop<
39+
DragItem<T>,
40+
unknown,
41+
{ isOver: boolean }
42+
>({
43+
accept: ItemType.ITEM,
44+
canDrop: () => !isDisabled,
45+
hover: (draggedItem) => {
46+
if (draggedItem.index !== index && !isDisabled) {
47+
onDragOver(index);
48+
}
49+
},
50+
drop: onDrop,
51+
collect: (monitor) => ({
52+
isOver: monitor.isOver(),
53+
}),
54+
});
55+
56+
const combinedRef = (node: HTMLDivElement | null) => {
57+
dragRef(dropRef(node));
58+
};
59+
60+
return (
61+
<DraggableItemWrapper
62+
ref={combinedRef}
63+
isDisabled={isDisabled}
64+
isDragging={isDragging}
65+
isOver={isOver}
66+
onDragStart={() => !isDisabled && onDragStart(index)}
67+
>
68+
{renderItem(item, index)}
69+
</DraggableItemWrapper>
70+
);
71+
};
72+
73+
export default DraggableItem;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useState, useEffect, useRef } from 'react';
2+
3+
export const useDraggableList = <T extends { disabled?: boolean }>(
4+
initialItems: T[],
5+
sortBy?: keyof T
6+
) => {
7+
const [items, setItems] = useState<T[]>(initialItems);
8+
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
9+
const isSortedInitially = useRef(false);
10+
11+
useEffect(() => {
12+
if (!isSortedInitially.current && sortBy) {
13+
let sortedItems = [...initialItems];
14+
15+
sortedItems = sortedItems.sort((a, b) => {
16+
const fieldA = a[sortBy];
17+
const fieldB = b[sortBy];
18+
19+
if (fieldA < fieldB) return -1;
20+
if (fieldA > fieldB) return 1;
21+
return 0;
22+
});
23+
24+
setItems(sortedItems);
25+
isSortedInitially.current = true;
26+
} else {
27+
setItems(initialItems);
28+
}
29+
}, [initialItems, sortBy]);
30+
31+
const onDragStart = (index: number) => {
32+
if (items[index].disabled) return;
33+
setDraggingIndex(index);
34+
};
35+
36+
const onDragOver = (index: number) => {
37+
if (
38+
draggingIndex === null ||
39+
draggingIndex === index ||
40+
items[index].disabled
41+
)
42+
return;
43+
44+
const updatedItems = [...items];
45+
const [draggedItem] = updatedItems.splice(draggingIndex, 1);
46+
updatedItems.splice(index, 0, draggedItem);
47+
setDraggingIndex(index);
48+
setItems(updatedItems);
49+
};
50+
51+
const onDrop = () => {
52+
setDraggingIndex(null);
53+
};
54+
55+
return {
56+
items,
57+
setItems,
58+
onDragStart,
59+
onDragOver,
60+
onDrop,
61+
};
62+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import { DraggableListProps } from './types';
3+
import { useDraggableList } from './hooks';
4+
import DraggableItem from './DraggableItem';
5+
import { ListContainer } from './styled';
6+
import ScrollArea from '../ScrollArea';
7+
8+
const DraggableList = <T extends { id?: string }>({
9+
items,
10+
onUpdate,
11+
renderItem,
12+
sortBy,
13+
}: DraggableListProps<T>) => {
14+
const {
15+
items: sortedItems,
16+
onDragStart,
17+
onDragOver,
18+
onDrop,
19+
} = useDraggableList(items, sortBy);
20+
21+
const handleDrop = () => {
22+
onUpdate(sortedItems);
23+
};
24+
25+
return (
26+
<ScrollArea>
27+
<ListContainer>
28+
{sortedItems.map((item, index) => (
29+
<DraggableItem
30+
key={item.id}
31+
item={item}
32+
index={index}
33+
renderItem={renderItem}
34+
onDragStart={() => onDragStart(index)}
35+
onDragOver={() => onDragOver(index)}
36+
onDrop={handleDrop}
37+
/>
38+
))}
39+
</ListContainer>
40+
</ScrollArea>
41+
);
42+
};
43+
44+
export default DraggableList;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import styled from 'styled-components';
2+
3+
export const ListContainer = styled.div`
4+
display: flex;
5+
flex-direction: column;
6+
`;
7+
8+
export const DraggableItemWrapper = styled.div<{
9+
isDisabled?: boolean;
10+
isDragging?: boolean;
11+
isOver?: boolean;
12+
}>`
13+
display: flex;
14+
align-items: center;
15+
margin-bottom: 4px;
16+
cursor: ${({ isDisabled }) => (isDisabled ? 'not-allowed' : 'move')};
17+
opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)};
18+
transition: all 0.3s ease;
19+
20+
${({ isDragging }) =>
21+
isDragging &&
22+
`
23+
opacity: 0.5;
24+
background: rgba(0, 0, 0, 0.05);
25+
`}
26+
27+
${({ isOver }) =>
28+
isOver &&
29+
`
30+
position: relative;
31+
&::before {
32+
content: '';
33+
position: absolute;
34+
top: -2px;
35+
left: 0;
36+
right: 0;
37+
height: 2px;
38+
background: #0066cc;
39+
}
40+
`}
41+
`;
42+
43+
export const DragHandle = styled.div`
44+
margin-right: 8px;
45+
cursor: grab;
46+
47+
&:active {
48+
cursor: grabbing;
49+
}
50+
`;
51+
52+
export const CustomDragLayerWrapper = styled.div`
53+
position: fixed;
54+
pointer-events: none;
55+
z-index: 1000;
56+
opacity: 0.9;
57+
`;

0 commit comments

Comments
 (0)