@tpzdsp/next-toolkit 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +70 -8
- package/src/assets/styles/globals.css +2 -0
- package/src/assets/styles/ol.css +122 -0
- package/src/components/Button/Button.test.tsx +1 -1
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Card/Card.test.tsx +1 -1
- package/src/components/ErrorText/ErrorText.test.tsx +1 -1
- package/src/components/ErrorText/ErrorText.tsx +1 -1
- package/src/components/Heading/Heading.test.tsx +1 -1
- package/src/components/Hint/Hint.test.tsx +1 -1
- package/src/components/Hint/Hint.tsx +1 -1
- package/src/components/Modal/Modal.stories.tsx +252 -0
- package/src/components/Modal/Modal.test.tsx +248 -0
- package/src/components/Modal/Modal.tsx +61 -0
- package/src/components/Paragraph/Paragraph.test.tsx +1 -1
- package/src/components/SlidingPanel/SlidingPanel.test.tsx +1 -2
- package/src/components/accordion/Accordion.stories.tsx +235 -0
- package/src/components/accordion/Accordion.test.tsx +199 -0
- package/src/components/accordion/Accordion.tsx +47 -0
- package/src/components/divider/RuleDivider.stories.tsx +255 -0
- package/src/components/divider/RuleDivider.test.tsx +164 -0
- package/src/components/divider/RuleDivider.tsx +18 -0
- package/src/components/dropdown/DropdownMenu.test.tsx +1 -1
- package/src/components/dropdown/useDropdownMenu.ts +1 -1
- package/src/components/index.ts +6 -2
- package/src/components/layout/header/Header.tsx +2 -1
- package/src/components/layout/header/HeaderAuthClient.tsx +17 -9
- package/src/components/layout/header/HeaderNavClient.tsx +3 -3
- package/src/components/link/ExternalLink.tsx +1 -1
- package/src/components/link/Link.tsx +1 -1
- package/src/components/select/Select.stories.tsx +336 -0
- package/src/components/select/Select.test.tsx +473 -0
- package/src/components/select/Select.tsx +132 -0
- package/src/components/select/SelectSkeleton.stories.tsx +195 -0
- package/src/components/select/SelectSkeleton.test.tsx +105 -0
- package/src/components/select/SelectSkeleton.tsx +16 -0
- package/src/components/select/common.ts +4 -0
- package/src/contexts/index.ts +0 -5
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useClickOutside.test.ts +290 -0
- package/src/hooks/useClickOutside.ts +26 -0
- package/src/index.ts +3 -0
- package/src/map/LayerSwitcherControl.ts +147 -0
- package/src/map/Map.tsx +230 -0
- package/src/map/MapContext.tsx +211 -0
- package/src/map/Popup.tsx +74 -0
- package/src/map/basemaps.ts +79 -0
- package/src/map/geocoder.ts +61 -0
- package/src/map/geometries.ts +60 -0
- package/src/map/images/basemaps/OS.png +0 -0
- package/src/map/images/basemaps/dark.png +0 -0
- package/src/map/images/basemaps/sat-map-tiler.png +0 -0
- package/src/map/images/basemaps/satellite-map-tiler.png +0 -0
- package/src/map/images/basemaps/satellite.png +0 -0
- package/src/map/images/basemaps/streets.png +0 -0
- package/src/map/images/openlayers-logo.png +0 -0
- package/src/map/index.ts +10 -0
- package/src/map/map.ts +40 -0
- package/src/map/osOpenNamesSearch.ts +54 -0
- package/src/map/projections.ts +14 -0
- package/src/ol-geocoder.d.ts +1 -0
- package/src/test/index.ts +1 -0
- package/src/types/api.ts +52 -0
- package/src/types/auth.ts +13 -0
- package/src/types/index.ts +6 -0
- package/src/types/map.ts +26 -0
- package/src/types/navigation.ts +8 -0
- package/src/types/utils.ts +13 -0
- package/src/utils/auth.ts +1 -1
- package/src/utils/http.ts +143 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/utils.ts +1 -1
- package/src/components/link/NextLinkWrapper.tsx +0 -66
- package/src/contexts/ThemeContext.tsx +0 -72
- package/src/types.ts +0 -99
- /package/src/{utils → test}/renderers.tsx +0 -0
package/src/components/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { OglLogo } from './images/OglLogo';
|
|
|
11
11
|
export { ExternalLink } from './link/ExternalLink';
|
|
12
12
|
export { Link } from './link/Link';
|
|
13
13
|
export { Paragraph } from './Paragraph/Paragraph';
|
|
14
|
+
export { RuleDivider } from './divider/RuleDivider';
|
|
14
15
|
|
|
15
16
|
// Export default component types
|
|
16
17
|
export type { ButtonProps } from './Button/Button';
|
|
@@ -25,13 +26,16 @@ export type { ParagraphProps } from './Paragraph/Paragraph';
|
|
|
25
26
|
export type { SlidingPanelProps } from './SlidingPanel/SlidingPanel';
|
|
26
27
|
|
|
27
28
|
// Client components - these require 'use client' directive
|
|
28
|
-
export { NextLinkWrapper } from './link/NextLinkWrapper';
|
|
29
29
|
export { DropdownMenu } from './dropdown/DropdownMenu';
|
|
30
30
|
export { useDropdownMenu } from './dropdown/useDropdownMenu';
|
|
31
31
|
export { SlidingPanel } from './SlidingPanel/SlidingPanel';
|
|
32
|
+
export { Accordion } from './accordion/Accordion';
|
|
33
|
+
export { Modal } from './Modal/Modal';
|
|
34
|
+
export { Select } from './select/Select';
|
|
35
|
+
export { SelectSkeleton } from './select/SelectSkeleton';
|
|
32
36
|
|
|
33
37
|
// Export client component types
|
|
34
|
-
export type {
|
|
38
|
+
export type { AccordionProps } from './accordion/Accordion';
|
|
35
39
|
|
|
36
40
|
// Export layout components
|
|
37
41
|
export { Header } from './layout/header/Header';
|
|
@@ -4,7 +4,8 @@ import { FaHouse } from 'react-icons/fa6';
|
|
|
4
4
|
|
|
5
5
|
import { HeaderAuthClient } from './HeaderAuthClient';
|
|
6
6
|
import { HeaderNavClient } from './HeaderNavClient';
|
|
7
|
-
import type { Credentials
|
|
7
|
+
import type { Credentials } from '../../../types/auth';
|
|
8
|
+
import type { NavLink } from '../../../types/navigation';
|
|
8
9
|
import { DefraLogo } from '../../images/DefraLogo';
|
|
9
10
|
import { EaLogo } from '../../images/EaLogo';
|
|
10
11
|
import { ExternalLink } from '../../link/ExternalLink';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { Credentials } from '../../../types/auth';
|
|
4
6
|
import { Link } from '../../link/Link';
|
|
5
7
|
|
|
6
8
|
type HeaderAuthClientProps = {
|
|
@@ -9,7 +11,11 @@ type HeaderAuthClientProps = {
|
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
export const HeaderAuthClient = ({ hostname, credentials }: HeaderAuthClientProps) => {
|
|
12
|
-
|
|
14
|
+
const [isClient, setIsClient] = useState(false);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setIsClient(true);
|
|
18
|
+
}, []);
|
|
13
19
|
|
|
14
20
|
return (
|
|
15
21
|
<span
|
|
@@ -20,13 +26,15 @@ export const HeaderAuthClient = ({ hostname, credentials }: HeaderAuthClientProp
|
|
|
20
26
|
<span>Welcome,</span> {credentials ? credentials.user.email : 'Guest'}
|
|
21
27
|
</span>
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
{isClient && (
|
|
30
|
+
<Link
|
|
31
|
+
href={`${hostname}/${credentials ? 'api/logout' : 'login'}?redirect-uri=${encodeURIComponent(window.location.href)}`}
|
|
32
|
+
className="text-white visited:text-white active:text-black hover:text-white"
|
|
33
|
+
prefetch={false}
|
|
34
|
+
>
|
|
35
|
+
<span className="text-base">{credentials ? 'Logout' : 'Login'}</span>
|
|
36
|
+
</Link>
|
|
37
|
+
)}
|
|
30
38
|
</span>
|
|
31
39
|
);
|
|
32
40
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type { NavLink } from '../../../types';
|
|
3
|
+
import type { NavLink } from '../../../types/navigation';
|
|
4
4
|
import { DropdownMenu, type DropdownMenuItem } from '../../dropdown/DropdownMenu';
|
|
5
5
|
import { ExternalLink } from '../../link/ExternalLink';
|
|
6
6
|
import { Link } from '../../link/Link';
|
|
@@ -23,8 +23,8 @@ const InternalNavItem = ({ label, url, icon, ...props }: DropdownMenuItem<NavLin
|
|
|
23
23
|
</Link>
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
-
const NavItem = (props: DropdownMenuItem<NavLink>) => {
|
|
27
|
-
if (
|
|
26
|
+
const NavItem = ({ isExternal, ...props }: DropdownMenuItem<NavLink>) => {
|
|
27
|
+
if (isExternal) {
|
|
28
28
|
return <ExternalNavItem {...props} />;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/* eslint-disable storybook/no-renderer-packages */
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
5
|
+
import { fn } from '@storybook/test';
|
|
6
|
+
|
|
7
|
+
import { Select } from './Select';
|
|
8
|
+
|
|
9
|
+
const meta = {
|
|
10
|
+
title: 'Components/Select',
|
|
11
|
+
component: Select,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered',
|
|
14
|
+
},
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
args: {
|
|
17
|
+
onChange: fn(),
|
|
18
|
+
},
|
|
19
|
+
} satisfies Meta<typeof Select>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
const OPTIONS = [
|
|
25
|
+
{ value: 'chocolate', label: 'Chocolate' },
|
|
26
|
+
{ value: 'strawberry', label: 'Strawberry' },
|
|
27
|
+
{ value: 'vanilla', label: 'Vanilla' },
|
|
28
|
+
{ value: 'mint', label: 'Mint' },
|
|
29
|
+
{ value: 'cookies', label: 'Cookies & Cream' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const GROUPED_OPTIONS = [
|
|
33
|
+
{
|
|
34
|
+
label: 'Fruits',
|
|
35
|
+
options: [
|
|
36
|
+
{ value: 'apple', label: 'Apple' },
|
|
37
|
+
{ value: 'banana', label: 'Banana' },
|
|
38
|
+
{ value: 'orange', label: 'Orange' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'Vegetables',
|
|
43
|
+
options: [
|
|
44
|
+
{ value: 'carrot', label: 'Carrot' },
|
|
45
|
+
{ value: 'broccoli', label: 'Broccoli' },
|
|
46
|
+
{ value: 'spinach', label: 'Spinach' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const USERS = [
|
|
52
|
+
{ value: 'john', label: 'John Doe', email: 'john@example.com' },
|
|
53
|
+
{ value: 'jane', label: 'Jane Smith', email: 'jane@example.com' },
|
|
54
|
+
{ value: 'bob', label: 'Bob Johnson', email: 'bob@example.com' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const FLAVOUR_PLACEHOLDER_TEXT = 'Select a flavour...';
|
|
58
|
+
const FLAVOUR_MULTI_PLACEHOLDER_TEXT = 'Select multiple flavours...';
|
|
59
|
+
|
|
60
|
+
export const Default: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
options: OPTIONS,
|
|
63
|
+
placeholder: FLAVOUR_PLACEHOLDER_TEXT,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const WithDefaultValue: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
options: OPTIONS,
|
|
70
|
+
defaultValue: OPTIONS[0],
|
|
71
|
+
placeholder: FLAVOUR_PLACEHOLDER_TEXT,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const Clearable: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
options: OPTIONS,
|
|
78
|
+
defaultValue: OPTIONS[1],
|
|
79
|
+
isClearable: true,
|
|
80
|
+
placeholder: FLAVOUR_PLACEHOLDER_TEXT,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const Searchable: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
options: OPTIONS,
|
|
87
|
+
isSearchable: true,
|
|
88
|
+
placeholder: 'Search and select...',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const MultiSelect: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
options: OPTIONS,
|
|
95
|
+
isMulti: true,
|
|
96
|
+
placeholder: FLAVOUR_MULTI_PLACEHOLDER_TEXT,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const MultiSelectWithValues: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
options: OPTIONS,
|
|
103
|
+
isMulti: true,
|
|
104
|
+
defaultValue: [OPTIONS[0], OPTIONS[2]],
|
|
105
|
+
placeholder: FLAVOUR_MULTI_PLACEHOLDER_TEXT,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const MultiSelectClearable: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
options: OPTIONS,
|
|
112
|
+
isMulti: true,
|
|
113
|
+
isClearable: true,
|
|
114
|
+
defaultValue: [OPTIONS[0], OPTIONS[1]],
|
|
115
|
+
placeholder: FLAVOUR_MULTI_PLACEHOLDER_TEXT,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const Disabled: Story = {
|
|
120
|
+
args: {
|
|
121
|
+
options: OPTIONS,
|
|
122
|
+
isDisabled: true,
|
|
123
|
+
defaultValue: OPTIONS[0],
|
|
124
|
+
placeholder: 'This select is disabled',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const Loading: Story = {
|
|
129
|
+
args: {
|
|
130
|
+
options: OPTIONS,
|
|
131
|
+
isLoading: true,
|
|
132
|
+
placeholder: 'Loading...',
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const GroupedOptions: Story = {
|
|
137
|
+
args: {
|
|
138
|
+
options: GROUPED_OPTIONS,
|
|
139
|
+
placeholder: 'Select a food item...',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const NoOptionsMessage: Story = {
|
|
144
|
+
args: {
|
|
145
|
+
options: [],
|
|
146
|
+
placeholder: 'No options available',
|
|
147
|
+
noOptionsMessage: () => 'No options found',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const CustomFormatOptionLabel: Story = {
|
|
152
|
+
args: {
|
|
153
|
+
options: USERS,
|
|
154
|
+
placeholder: 'Select a user...',
|
|
155
|
+
formatOptionLabel: (data: unknown) => {
|
|
156
|
+
const option = data as { value: string; label: string; email: string };
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div>
|
|
160
|
+
<div className="font-medium">{option.label}</div>
|
|
161
|
+
|
|
162
|
+
<div className="text-sm text-gray-500">{option.email}</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const CustomClassNames: Story = {
|
|
170
|
+
args: {
|
|
171
|
+
options: OPTIONS,
|
|
172
|
+
placeholder: 'Custom styled select...',
|
|
173
|
+
classNames: {
|
|
174
|
+
control: () => 'border-2 border-purple-500 rounded-lg',
|
|
175
|
+
option: (state) =>
|
|
176
|
+
// eslint-disable-next-line sonarjs/no-nested-conditional
|
|
177
|
+
state.isSelected ? 'bg-purple-500 text-white' : state.isFocused ? 'bg-purple-100' : '',
|
|
178
|
+
menu: () => 'border-purple-500',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const SmallSize: Story = {
|
|
184
|
+
args: {
|
|
185
|
+
options: OPTIONS,
|
|
186
|
+
placeholder: 'Small select...',
|
|
187
|
+
classNames: {
|
|
188
|
+
control: () => 'min-h-8 text-sm',
|
|
189
|
+
option: () => 'px-2 py-1 text-sm',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const LargeSize: Story = {
|
|
195
|
+
args: {
|
|
196
|
+
options: OPTIONS,
|
|
197
|
+
placeholder: 'Large select...',
|
|
198
|
+
classNames: {
|
|
199
|
+
control: () => 'min-h-12 text-lg',
|
|
200
|
+
option: () => 'px-6 py-3 text-lg',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const ControlledComponent: Story = {
|
|
206
|
+
render: () => {
|
|
207
|
+
const [value, setValue] = useState<{ value: string; label: string } | null>(OPTIONS[0]);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className="space-y-4">
|
|
211
|
+
<Select
|
|
212
|
+
options={OPTIONS}
|
|
213
|
+
value={value}
|
|
214
|
+
onChange={(newValue) => setValue(newValue)}
|
|
215
|
+
placeholder="Controlled select..."
|
|
216
|
+
/>
|
|
217
|
+
|
|
218
|
+
<div className="text-sm text-gray-600">Selected: {value ? value.label : 'None'}</div>
|
|
219
|
+
|
|
220
|
+
<button
|
|
221
|
+
onClick={() => setValue(OPTIONS[2])}
|
|
222
|
+
className="px-3 py-1 bg-blue-500 text-white rounded text-sm"
|
|
223
|
+
>
|
|
224
|
+
Set to Vanilla
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
export const WithLongOptions: Story = {
|
|
231
|
+
args: {
|
|
232
|
+
options: [
|
|
233
|
+
{ value: 'short', label: 'Short' },
|
|
234
|
+
{
|
|
235
|
+
value: 'long',
|
|
236
|
+
label: 'This is a very long option that might overflow and needs to be handled properly',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
value: 'medium',
|
|
240
|
+
label: 'Medium length option text',
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
placeholder: 'Select an option...',
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const Async: Story = {
|
|
248
|
+
render: () => {
|
|
249
|
+
const [options, setOptions] = useState<{ value: string; label: string }[]>([]);
|
|
250
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
251
|
+
|
|
252
|
+
const loadOptions = () => {
|
|
253
|
+
setIsLoading(true);
|
|
254
|
+
// Simulate API call
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
setOptions([
|
|
257
|
+
{ value: 'async1', label: 'Async Option 1' },
|
|
258
|
+
{ value: 'async2', label: 'Async Option 2' },
|
|
259
|
+
{ value: 'async3', label: 'Async Option 3' },
|
|
260
|
+
]);
|
|
261
|
+
setIsLoading(false);
|
|
262
|
+
}, 1000);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
loadOptions();
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div className="space-y-4">
|
|
271
|
+
<Select options={options} isLoading={isLoading} placeholder="Loading async options..." />
|
|
272
|
+
|
|
273
|
+
<button onClick={loadOptions} className="px-3 py-1 bg-green-500 text-white rounded text-sm">
|
|
274
|
+
Reload Options
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const FormIntegration: Story = {
|
|
282
|
+
render: () => {
|
|
283
|
+
const [formData, setFormData] = useState<{
|
|
284
|
+
flavour: { value: string; label: string } | null;
|
|
285
|
+
toppings: { value: string; label: string }[];
|
|
286
|
+
}>({
|
|
287
|
+
flavour: null,
|
|
288
|
+
toppings: [],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<form className="space-y-4 p-4 border rounded">
|
|
293
|
+
<div>
|
|
294
|
+
<label htmlFor="flavour" className="block text-sm font-medium mb-1">
|
|
295
|
+
Flavour
|
|
296
|
+
</label>
|
|
297
|
+
|
|
298
|
+
<Select
|
|
299
|
+
id="flavour"
|
|
300
|
+
options={OPTIONS}
|
|
301
|
+
value={formData.flavour}
|
|
302
|
+
onChange={(value) => setFormData({ ...formData, flavour: value })}
|
|
303
|
+
placeholder="Select a flavour..."
|
|
304
|
+
isClearable
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<div>
|
|
309
|
+
<label htmlFor="toppings" className="block text-sm font-medium mb-1">
|
|
310
|
+
Toppings
|
|
311
|
+
</label>
|
|
312
|
+
|
|
313
|
+
<Select
|
|
314
|
+
id="toppings"
|
|
315
|
+
options={[
|
|
316
|
+
{ value: 'sprinkles', label: 'Sprinkles' },
|
|
317
|
+
{ value: 'nuts', label: 'Nuts' },
|
|
318
|
+
{ value: 'chocolate-chips', label: 'Chocolate Chips' },
|
|
319
|
+
{ value: 'cherry', label: 'Cherry' },
|
|
320
|
+
]}
|
|
321
|
+
value={formData.toppings}
|
|
322
|
+
onChange={(value) =>
|
|
323
|
+
setFormData({ ...formData, toppings: Array.isArray(value) ? [...value] : [] })
|
|
324
|
+
}
|
|
325
|
+
placeholder="Select toppings..."
|
|
326
|
+
isMulti
|
|
327
|
+
/>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div className="text-sm text-gray-600">
|
|
331
|
+
<pre>{JSON.stringify(formData, null, 2)}</pre>
|
|
332
|
+
</div>
|
|
333
|
+
</form>
|
|
334
|
+
);
|
|
335
|
+
},
|
|
336
|
+
};
|