@datability/8ui 0.1.69 → 1.1.0

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.
Files changed (199) hide show
  1. package/.prettierrc +8 -0
  2. package/.vscode/extensions.json +6 -0
  3. package/README.md +66 -18
  4. package/declaration.d.ts +10 -0
  5. package/docker-compose.yml +20 -0
  6. package/eslint.config.js +23 -0
  7. package/index.html +13 -0
  8. package/package.json +37 -36
  9. package/public/vite.svg +1 -0
  10. package/src/App.tsx +370 -0
  11. package/src/components/blackdrop/index.tsx +18 -0
  12. package/src/components/blackdrop/index.type.ts +7 -0
  13. package/src/components/button/index.tsx +44 -0
  14. package/src/components/button/index.type.ts +13 -0
  15. package/src/components/chip/index.tsx +39 -0
  16. package/src/components/chip/index.type.ts +12 -0
  17. package/src/components/context.tsx +26 -0
  18. package/src/components/divider/index.tsx +13 -0
  19. package/src/components/index.ts +62 -0
  20. package/{dist/components/Input/InputAutoComplete → src/components/input/input-auto-complete}/index.scss +1 -1
  21. package/src/components/input/input-auto-complete/index.tsx +140 -0
  22. package/src/components/input/input-auto-complete/index.type.tsx +13 -0
  23. package/src/components/input/input-base/index.tsx +39 -0
  24. package/src/components/input/input-base/index.type.tsx +13 -0
  25. package/src/components/input/input-basic/index.tsx +47 -0
  26. package/src/components/input/input-basic/index.type.tsx +8 -0
  27. package/src/components/input/input-checkbox/index.tsx +69 -0
  28. package/src/components/input/input-checkbox/index.type.tsx +11 -0
  29. package/src/components/input/input-date/index.tsx +354 -0
  30. package/src/components/input/input-date/index.type.tsx +11 -0
  31. package/src/components/input/input-date-range/index.tsx +284 -0
  32. package/src/components/input/input-date-range/index.type.tsx +11 -0
  33. package/src/components/input/input-date-time/index.tsx +367 -0
  34. package/src/components/input/input-date-time/index.type.tsx +11 -0
  35. package/src/components/input/input-number/index.tsx +118 -0
  36. package/src/components/input/input-number/index.type.tsx +11 -0
  37. package/src/components/input/input-password/index.tsx +60 -0
  38. package/src/components/input/input-password/index.type.tsx +8 -0
  39. package/src/components/input/input-radio/index.tsx +72 -0
  40. package/src/components/input/input-radio/index.type.tsx +12 -0
  41. package/{dist/components/Input/InputSelect → src/components/input/input-select}/index.scss +1 -1
  42. package/src/components/input/input-select/index.tsx +113 -0
  43. package/src/components/input/input-select/index.type.tsx +15 -0
  44. package/{dist/components/InputNonContext/InputSwitch → src/components/input/input-switch}/index.scss +1 -1
  45. package/src/components/input/input-switch/index.tsx +44 -0
  46. package/src/components/input/input-switch/index.type.tsx +4 -0
  47. package/src/components/input/input-textarea/index.tsx +48 -0
  48. package/src/components/input/input-textarea/index.type.tsx +10 -0
  49. package/src/components/menu/index.tsx +136 -0
  50. package/src/components/menu/index.type.ts +8 -0
  51. package/{dist/components/Modal → src/components/modal}/index.scss +0 -0
  52. package/src/components/modal/index.tsx +99 -0
  53. package/src/components/modal/index.type.tsx +8 -0
  54. package/src/index.scss +44 -0
  55. package/src/index.ts +62 -0
  56. package/src/logoDownload.svg +3 -0
  57. package/src/main.tsx +9 -0
  58. package/tsconfig.app.json +28 -0
  59. package/tsconfig.json +42 -0
  60. package/tsconfig.node.json +29 -0
  61. package/vite.config.d.ts +2 -0
  62. package/vite.config.ts +35 -0
  63. package/dist/components/Blackdrop/index.d.ts +0 -5
  64. package/dist/components/Blackdrop/index.js +0 -11
  65. package/dist/components/Blackdrop/index.js.map +0 -1
  66. package/dist/components/Blackdrop/index.type.d.ts +0 -6
  67. package/dist/components/Blackdrop/index.type.js +0 -2
  68. package/dist/components/Blackdrop/index.type.js.map +0 -1
  69. package/dist/components/Button/index.d.ts +0 -5
  70. package/dist/components/Button/index.js +0 -16
  71. package/dist/components/Button/index.js.map +0 -1
  72. package/dist/components/Button/index.type.d.ts +0 -12
  73. package/dist/components/Button/index.type.js +0 -2
  74. package/dist/components/Button/index.type.js.map +0 -1
  75. package/dist/components/Chip/index.d.ts +0 -5
  76. package/dist/components/Chip/index.js +0 -18
  77. package/dist/components/Chip/index.js.map +0 -1
  78. package/dist/components/Chip/index.type.d.ts +0 -9
  79. package/dist/components/Chip/index.type.js +0 -2
  80. package/dist/components/Chip/index.type.js.map +0 -1
  81. package/dist/components/Divider/index.d.ts +0 -4
  82. package/dist/components/Divider/index.js +0 -10
  83. package/dist/components/Divider/index.js.map +0 -1
  84. package/dist/components/Input/InputAutoComplete/index.d.ts +0 -5
  85. package/dist/components/Input/InputAutoComplete/index.js +0 -68
  86. package/dist/components/Input/InputAutoComplete/index.js.map +0 -1
  87. package/dist/components/Input/InputAutoComplete/index.type.d.ts +0 -12
  88. package/dist/components/Input/InputAutoComplete/index.type.js +0 -2
  89. package/dist/components/Input/InputAutoComplete/index.type.js.map +0 -1
  90. package/dist/components/Input/InputBase/index.d.ts +0 -5
  91. package/dist/components/Input/InputBase/index.js +0 -23
  92. package/dist/components/Input/InputBase/index.js.map +0 -1
  93. package/dist/components/Input/InputBase/index.type.d.ts +0 -9
  94. package/dist/components/Input/InputBase/index.type.js +0 -2
  95. package/dist/components/Input/InputBase/index.type.js.map +0 -1
  96. package/dist/components/Input/InputBasic/index.d.ts +0 -5
  97. package/dist/components/Input/InputBasic/index.js +0 -16
  98. package/dist/components/Input/InputBasic/index.js.map +0 -1
  99. package/dist/components/Input/InputBasic/index.type.d.ts +0 -10
  100. package/dist/components/Input/InputBasic/index.type.js +0 -2
  101. package/dist/components/Input/InputBasic/index.type.js.map +0 -1
  102. package/dist/components/Input/InputCheckbox/index.d.ts +0 -5
  103. package/dist/components/Input/InputCheckbox/index.js +0 -19
  104. package/dist/components/Input/InputCheckbox/index.js.map +0 -1
  105. package/dist/components/Input/InputCheckbox/index.type.d.ts +0 -11
  106. package/dist/components/Input/InputCheckbox/index.type.js +0 -2
  107. package/dist/components/Input/InputCheckbox/index.type.js.map +0 -1
  108. package/dist/components/Input/InputDate/index.d.ts +0 -23
  109. package/dist/components/Input/InputDate/index.js +0 -190
  110. package/dist/components/Input/InputDate/index.js.map +0 -1
  111. package/dist/components/Input/InputDate/index.type.d.ts +0 -11
  112. package/dist/components/Input/InputDate/index.type.js +0 -2
  113. package/dist/components/Input/InputDate/index.type.js.map +0 -1
  114. package/dist/components/Input/InputDateRange/index.d.ts +0 -5
  115. package/dist/components/Input/InputDateRange/index.js +0 -137
  116. package/dist/components/Input/InputDateRange/index.js.map +0 -1
  117. package/dist/components/Input/InputDateRange/index.type.d.ts +0 -11
  118. package/dist/components/Input/InputDateRange/index.type.js +0 -2
  119. package/dist/components/Input/InputDateRange/index.type.js.map +0 -1
  120. package/dist/components/Input/InputDateTime/index.d.ts +0 -5
  121. package/dist/components/Input/InputDateTime/index.js +0 -185
  122. package/dist/components/Input/InputDateTime/index.js.map +0 -1
  123. package/dist/components/Input/InputDateTime/index.type.d.ts +0 -11
  124. package/dist/components/Input/InputDateTime/index.type.js +0 -2
  125. package/dist/components/Input/InputDateTime/index.type.js.map +0 -1
  126. package/dist/components/Input/InputNumber/index.d.ts +0 -5
  127. package/dist/components/Input/InputNumber/index.js +0 -78
  128. package/dist/components/Input/InputNumber/index.js.map +0 -1
  129. package/dist/components/Input/InputNumber/index.type.d.ts +0 -10
  130. package/dist/components/Input/InputNumber/index.type.js +0 -2
  131. package/dist/components/Input/InputNumber/index.type.js.map +0 -1
  132. package/dist/components/Input/InputPassword/index.d.ts +0 -5
  133. package/dist/components/Input/InputPassword/index.js +0 -21
  134. package/dist/components/Input/InputPassword/index.js.map +0 -1
  135. package/dist/components/Input/InputPassword/index.type.d.ts +0 -8
  136. package/dist/components/Input/InputPassword/index.type.js +0 -2
  137. package/dist/components/Input/InputPassword/index.type.js.map +0 -1
  138. package/dist/components/Input/InputRadio/index.d.ts +0 -5
  139. package/dist/components/Input/InputRadio/index.js +0 -31
  140. package/dist/components/Input/InputRadio/index.js.map +0 -1
  141. package/dist/components/Input/InputRadio/index.type.d.ts +0 -12
  142. package/dist/components/Input/InputRadio/index.type.js +0 -2
  143. package/dist/components/Input/InputRadio/index.type.js.map +0 -1
  144. package/dist/components/Input/InputSelect/index.d.ts +0 -5
  145. package/dist/components/Input/InputSelect/index.js +0 -45
  146. package/dist/components/Input/InputSelect/index.js.map +0 -1
  147. package/dist/components/Input/InputSelect/index.type.d.ts +0 -14
  148. package/dist/components/Input/InputSelect/index.type.js +0 -2
  149. package/dist/components/Input/InputSelect/index.type.js.map +0 -1
  150. package/dist/components/Input/InputTextarea/index.d.ts +0 -5
  151. package/dist/components/Input/InputTextarea/index.js +0 -16
  152. package/dist/components/Input/InputTextarea/index.js.map +0 -1
  153. package/dist/components/Input/InputTextarea/index.type.d.ts +0 -10
  154. package/dist/components/Input/InputTextarea/index.type.js +0 -2
  155. package/dist/components/Input/InputTextarea/index.type.js.map +0 -1
  156. package/dist/components/InputNonContext/InputSwitch/index.d.ts +0 -5
  157. package/dist/components/InputNonContext/InputSwitch/index.js +0 -19
  158. package/dist/components/InputNonContext/InputSwitch/index.js.map +0 -1
  159. package/dist/components/InputNonContext/InputSwitch/index.type.d.ts +0 -6
  160. package/dist/components/InputNonContext/InputSwitch/index.type.js +0 -2
  161. package/dist/components/InputNonContext/InputSwitch/index.type.js.map +0 -1
  162. package/dist/components/Menu/index.d.ts +0 -5
  163. package/dist/components/Menu/index.js +0 -103
  164. package/dist/components/Menu/index.js.map +0 -1
  165. package/dist/components/Menu/index.type.d.ts +0 -11
  166. package/dist/components/Menu/index.type.js +0 -2
  167. package/dist/components/Menu/index.type.js.map +0 -1
  168. package/dist/components/Modal/index.d.ts +0 -5
  169. package/dist/components/Modal/index.js +0 -83
  170. package/dist/components/Modal/index.js.map +0 -1
  171. package/dist/components/Modal/index.type.d.ts +0 -7
  172. package/dist/components/Modal/index.type.js +0 -2
  173. package/dist/components/Modal/index.type.js.map +0 -1
  174. package/dist/components/context.d.ts +0 -8
  175. package/dist/components/context.js +0 -12
  176. package/dist/components/context.js.map +0 -1
  177. package/dist/components/index.d.ts +0 -41
  178. package/dist/components/index.js +0 -21
  179. package/dist/components/index.js.map +0 -1
  180. /package/{dist → src}/components/assets/closed.svg +0 -0
  181. /package/{dist/components/assets/expandArrow.svg → src/components/assets/expand-arrow.svg} +0 -0
  182. /package/{dist/components/assets/visibilityOff.svg → src/components/assets/visibility-off.svg} +0 -0
  183. /package/{dist → src}/components/assets/visibility.svg +0 -0
  184. /package/{dist/components/Blackdrop → src/components/blackdrop}/index.scss +0 -0
  185. /package/{dist/components/Button → src/components/button}/index.scss +0 -0
  186. /package/{dist/components/Chip → src/components/chip}/index.scss +0 -0
  187. /package/{dist/components/Divider → src/components/divider}/index.scss +0 -0
  188. /package/{dist/components/Input → src/components/input}/extend.scss +0 -0
  189. /package/{dist/components/Input/InputBase → src/components/input/input-base}/index.scss +0 -0
  190. /package/{dist/components/Input/InputBasic → src/components/input/input-basic}/index.scss +0 -0
  191. /package/{dist/components/Input/InputCheckbox → src/components/input/input-checkbox}/index.scss +0 -0
  192. /package/{dist/components/Input/InputDate → src/components/input/input-date}/index.scss +0 -0
  193. /package/{dist/components/Input/InputDateRange → src/components/input/input-date-range}/index.scss +0 -0
  194. /package/{dist/components/Input/InputDateTime → src/components/input/input-date-time}/index.scss +0 -0
  195. /package/{dist/components/Input/InputNumber → src/components/input/input-number}/index.scss +0 -0
  196. /package/{dist/components/Input/InputPassword → src/components/input/input-password}/index.scss +0 -0
  197. /package/{dist/components/Input/InputRadio → src/components/input/input-radio}/index.scss +0 -0
  198. /package/{dist/components/Input/InputTextarea → src/components/input/input-textarea}/index.scss +0 -0
  199. /package/{dist/components/Menu → src/components/menu}/index.scss +0 -0
@@ -0,0 +1,354 @@
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ // Lib
3
+ import React, { useEffect, useState } from "react"
4
+ import { Controller, useFormContext } from "react-hook-form"
5
+
6
+ // Images
7
+ import expandArrowSVG from "../../assets/expand-arrow.svg"
8
+ import closedSVG from "../../assets/closed.svg"
9
+
10
+ // Include in project
11
+ import "./index.scss"
12
+ import InputBase from "../input-base"
13
+ import type { PropsInputDate } from "./index.type"
14
+ import Menu from "../../menu"
15
+ import Modal from "../../modal"
16
+ import { createPortal } from "react-dom"
17
+
18
+ // 2025-09-01
19
+ const InputDate: React.FC<PropsInputDate> = ({
20
+ name,
21
+ label,
22
+ placeholder,
23
+ disabled = false,
24
+ require = false,
25
+ fullWidth = false,
26
+ isHideClearIcon = true,
27
+ minYear,
28
+ maxYear,
29
+ }) => {
30
+ const { control } = useFormContext()
31
+
32
+ const [showDay, setShowDay] = useState(getDateNow())
33
+ const [isOpenModal, setIsOpenModal] = useState(false)
34
+
35
+ const [mounted, setMounted] = useState(false)
36
+ useEffect(() => {
37
+ setMounted(true)
38
+ return () => setMounted(false)
39
+ }, [])
40
+
41
+ const yearOption = generateYearOptions(minYear, maxYear)
42
+ const monthOption = generateMonthOptions()
43
+
44
+ const portalRoot =
45
+ (document.getElementById("root") as HTMLElement) ||
46
+ (document.getElementById("__next") as HTMLElement) ||
47
+ document.body
48
+
49
+ return (
50
+ <Controller
51
+ name={name}
52
+ control={control}
53
+ render={({ field, fieldState }) => {
54
+ const value: string = field.value || ""
55
+ const { onChange } = field
56
+ const { invalid, error } = fieldState
57
+
58
+ // sync showDay เมื่อเปิด modal หรือค่าเปลี่ยน
59
+ // eslint-disable-next-line react-hooks/rules-of-hooks
60
+ useEffect(() => {
61
+ if (!isOpenModal) return
62
+ setShowDay(value || getDateNow())
63
+ }, [isOpenModal])
64
+
65
+ function CalendarRow({ year, month }: { year: number; month: string }) {
66
+ const days = getDaysInMonthWithWeekday(year, month)
67
+
68
+ const mapToThaiWeek = (dayIndex: number) => dayIndex % 7
69
+ const firstDayThaiIndex = mapToThaiWeek(new Date(`${year}-${month}-01`).getDay())
70
+ const emptyStartDays = Array(firstDayThaiIndex).fill(null)
71
+
72
+ const allDays = [...emptyStartDays, ...days.map((d) => d.date.split("-")[2])]
73
+
74
+ while (allDays.length < 42) allDays.push(null)
75
+
76
+ const weeks: Array<Array<string | null>> = []
77
+ for (let i = 0; i < allDays.length; i += 7) weeks.push(allDays.slice(i, i + 7))
78
+
79
+ return (
80
+ <>
81
+ {weeks.map((week, rowIndex) => (
82
+ <div key={rowIndex} className="DBui-inputDateRowDay">
83
+ {week.map((day, i) => (
84
+ <p
85
+ key={i}
86
+ className="DBui-inputDateDay"
87
+ onClick={() => {
88
+ if (!day) return
89
+ const next = `${getYearInDateString(showDay)}-${getMonthInDateString(showDay)}-${day}`
90
+ onChange(next)
91
+ setIsOpenModal(false)
92
+ }}
93
+ data-checked={
94
+ `${getYearInDateString(value || showDay)}-${getMonthInDateString(value || showDay)}-${getDayInDateString(
95
+ value || showDay,
96
+ )}` === `${getYearInDateString(showDay)}-${getMonthInDateString(showDay)}-${day}`
97
+ }
98
+ data-hidden-hover={!day}
99
+ >
100
+ {day || ""}
101
+ </p>
102
+ ))}
103
+ </div>
104
+ ))}
105
+ </>
106
+ )
107
+ }
108
+
109
+ const handleClear = (e: React.MouseEvent<HTMLImageElement>) => {
110
+ e.stopPropagation()
111
+ onChange("")
112
+ }
113
+
114
+ return (
115
+ <InputBase
116
+ name={name}
117
+ label={label}
118
+ require={require}
119
+ fullWidth={fullWidth}
120
+ isInvalid={invalid}
121
+ errorMessage={error?.message}
122
+ >
123
+ <>
124
+ <div
125
+ className="DBui-inputDate"
126
+ onClick={() => (disabled ? null : setIsOpenModal(true))}
127
+ data-invalid={invalid}
128
+ data-disabled={disabled}
129
+ >
130
+ <p>{value ? value : placeholder}</p>
131
+
132
+ <img
133
+ src={closedSVG}
134
+ className="DBui-clearIconInputDate"
135
+ onClick={handleClear}
136
+ data-hidden={value === "" || disabled || isHideClearIcon}
137
+ />
138
+ </div>
139
+
140
+ {mounted &&
141
+ createPortal(
142
+ <Modal id="modalInputDate" open={isOpenModal} onClose={() => setIsOpenModal(false)}>
143
+ <div className="DBui-inputDateWrapperCalendar">
144
+ <div className="DBui-inputDateRowMonthYear">
145
+ <img
146
+ src={expandArrowSVG}
147
+ className="DBui-inputDateRowMonthYearSelected"
148
+ style={{ transform: "rotate(90deg)" }}
149
+ onClick={() => {
150
+ setShowDay(
151
+ updateMonthInDateString(
152
+ showDay,
153
+ String(Number(showDay.split("-")[1]) - 1).padStart(2, "0"),
154
+ ),
155
+ )
156
+ }}
157
+ />
158
+
159
+ <Menu
160
+ isInModal
161
+ trigger={() => <h4 className="DBui-inputDateRowMonthYearSelected">{showDay.slice(0, 4)}</h4>}
162
+ >
163
+ {({ close }) =>
164
+ yearOption.map((y, index) => (
165
+ <p
166
+ key={index}
167
+ className="DBui-inputDateRowMonthYearOption"
168
+ onClick={() => {
169
+ setShowDay(updateYearInDateString(showDay, y.value))
170
+ close()
171
+ }}
172
+ data-checked={getYearInDateString(showDay) === y.value}
173
+ >
174
+ {y.label}
175
+ </p>
176
+ ))
177
+ }
178
+ </Menu>
179
+
180
+ <Menu
181
+ isInModal
182
+ trigger={() => (
183
+ <h4 className="DBui-inputDateRowMonthYearSelected" style={{ width: "7rem" }}>
184
+ {monthOption.find((e) => e.value === showDay.slice(5, 7))?.label}
185
+ </h4>
186
+ )}
187
+ >
188
+ {({ close }) =>
189
+ monthOption.map((m, index) => (
190
+ <p
191
+ key={index}
192
+ className="DBui-inputDateRowMonthYearOption"
193
+ onClick={() => {
194
+ setShowDay(updateMonthInDateString(showDay, m.value))
195
+ close()
196
+ }}
197
+ data-checked={getMonthInDateString(showDay) === m.value}
198
+ >
199
+ {m.label}
200
+ </p>
201
+ ))
202
+ }
203
+ </Menu>
204
+
205
+ <img
206
+ src={expandArrowSVG}
207
+ className="DBui-inputDateRowMonthYearSelected"
208
+ style={{ transform: "rotate(-90deg)" }}
209
+ onClick={() => {
210
+ setShowDay(
211
+ updateMonthInDateString(
212
+ showDay,
213
+ String(Number(showDay.split("-")[1]) + 1).padStart(2, "0"),
214
+ ),
215
+ )
216
+ }}
217
+ />
218
+ </div>
219
+
220
+ <div>
221
+ <div className="DBui-inputDateRowHeaderDay">
222
+ <p className="DBui-inputDateHeader">Sun</p>
223
+ <p className="DBui-inputDateHeader">Mon</p>
224
+ <p className="DBui-inputDateHeader">Tue</p>
225
+ <p className="DBui-inputDateHeader">Wed</p>
226
+ <p className="DBui-inputDateHeader">Thu</p>
227
+ <p className="DBui-inputDateHeader">Fri</p>
228
+ <p className="DBui-inputDateHeader">Sat</p>
229
+ </div>
230
+
231
+ <CalendarRow year={getYearInDateString(showDay)} month={getMonthInDateString(showDay)} />
232
+ </div>
233
+ </div>
234
+ </Modal>,
235
+ portalRoot,
236
+ )}
237
+ {!mounted && (
238
+ <Modal id="modalInputDate" open={isOpenModal} onClose={() => setIsOpenModal(false)}>
239
+ <div className="DBui-inputDateWrapperCalendar"></div>
240
+ </Modal>
241
+ )}
242
+ </>
243
+ </InputBase>
244
+ )
245
+ }}
246
+ />
247
+ )
248
+ }
249
+
250
+ export default InputDate
251
+
252
+ export function generateYearOptions(minYear?: number, maxYear?: number): { label: string; value: number }[] {
253
+ const currentYear = new Date().getFullYear()
254
+
255
+ // Default values ถ้าไม่ส่งมา
256
+ const finalMax = maxYear ?? currentYear
257
+ const finalMin = minYear ?? currentYear - 100
258
+
259
+ const years = []
260
+
261
+ // ไล่ปีแบบมาก -> น้อย
262
+ for (let year = finalMax; year >= finalMin; year--) {
263
+ years.push({ label: String(year), value: year })
264
+ }
265
+
266
+ return years
267
+ }
268
+
269
+ export function generateMonthOptions(): { label: string; value: string }[] {
270
+ const monthNames = [
271
+ "January",
272
+ "February",
273
+ "March",
274
+ "April",
275
+ "May",
276
+ "June",
277
+ "July",
278
+ "August",
279
+ "September",
280
+ "October",
281
+ "November",
282
+ "December",
283
+ ]
284
+
285
+ return monthNames.map((name, index) => ({
286
+ label: name,
287
+ value: String(index + 1).padStart(2, "0"),
288
+ }))
289
+ }
290
+
291
+ export function updateYearInDateString(dateStr: string, newYear: number): string {
292
+ const newDateStr = dateStr ? dateStr : new Date().toISOString().split("T")[0]
293
+ const [, month, day] = newDateStr.split("-")
294
+
295
+ return `${newYear}-${month}-${day}`
296
+ }
297
+
298
+ export function updateMonthInDateString(dateStr: string, newMonth: string): string {
299
+ const newDateStr = dateStr || new Date().toISOString().split("T")[0]
300
+ const [yearStr, , dayStr] = newDateStr.split("-")
301
+
302
+ let year = parseInt(yearStr, 10)
303
+ let month = parseInt(newMonth, 10)
304
+
305
+ // Handle overflow/underflow
306
+ year += Math.floor((month - 1) / 12)
307
+ month = ((((month - 1) % 12) + 12) % 12) + 1 // Wrap to 1–12
308
+
309
+ const monthStr = String(month).padStart(2, "0")
310
+ return `${year}-${monthStr}-${dayStr}`
311
+ }
312
+
313
+ export function getYearInDateString(dateStr: string): number {
314
+ const [year] = dateStr.split("-")
315
+ return Number(year)
316
+ }
317
+
318
+ export function getMonthInDateString(dateStr: string): string {
319
+ const [, month] = dateStr.split("-")
320
+ return month
321
+ }
322
+
323
+ export function getDayInDateString(dateStr: string): string {
324
+ const [, , day] = dateStr.split("-")
325
+ return day
326
+ }
327
+
328
+ export function getDaysInMonthWithWeekday(year: number, month: string): { date: string; weekday: string }[] {
329
+ const days: { date: string; weekday: string }[] = []
330
+ const monthIndex = parseInt(month, 10) - 1 // JavaScript months are 0-based
331
+
332
+ const date = new Date(year, monthIndex, 1)
333
+ const weekdayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
334
+
335
+ while (date.getMonth() === monthIndex) {
336
+ const day = String(date.getDate()).padStart(2, "0")
337
+ const weekday = weekdayNames[date.getDay()]
338
+ const formattedDate = `${year}-${month}-${day}`
339
+
340
+ days.push({ date: formattedDate, weekday })
341
+ date.setDate(date.getDate() + 1)
342
+ }
343
+
344
+ return days
345
+ }
346
+
347
+ export function getDateNow(): string {
348
+ const dateNow = new Date()
349
+
350
+ const year = dateNow.getFullYear()
351
+ const month = `${dateNow.getMonth() + 1}`.padStart(2, "0")
352
+ const date = `${dateNow.getDate()}`.padStart(2, "0")
353
+ return `${year}-${month}-${date}`
354
+ }
@@ -0,0 +1,11 @@
1
+ export type PropsInputDate = {
2
+ name: string
3
+ label?: string
4
+ placeholder?: string
5
+ disabled?: boolean
6
+ require?: boolean
7
+ fullWidth?: boolean
8
+ isHideClearIcon?: boolean
9
+ minYear?: number
10
+ maxYear?: number
11
+ }
@@ -0,0 +1,284 @@
1
+ // Lib
2
+ import React, { useEffect, useState } from "react"
3
+ import { Controller, useFormContext } from "react-hook-form"
4
+
5
+ // Images
6
+ import expandArrowSVG from "../../assets/expand-arrow.svg"
7
+ import closedSVG from "../../assets/closed.svg"
8
+
9
+ // Include in project
10
+ import "./index.scss"
11
+ import InputBase from "../input-base"
12
+ import type { PropsInputDateRange } from "./index.type"
13
+ import Menu from "../../menu"
14
+ import Modal from "../../modal"
15
+ import { createPortal } from "react-dom"
16
+ import {
17
+ generateMonthOptions,
18
+ generateYearOptions,
19
+ getDateNow,
20
+ getDaysInMonthWithWeekday,
21
+ getMonthInDateString,
22
+ getYearInDateString,
23
+ updateMonthInDateString,
24
+ updateYearInDateString,
25
+ } from "../input-date"
26
+
27
+ const InputDateRange: React.FC<PropsInputDateRange> = ({
28
+ name,
29
+ label,
30
+ placeholder,
31
+ disabled = false,
32
+ require = false,
33
+ fullWidth = false,
34
+ isHideClearIcon = true,
35
+ maxYear,
36
+ minYear,
37
+ }) => {
38
+ const { control } = useFormContext()
39
+
40
+ const [showDay, setShowDay] = useState<string>(getDateNow())
41
+ const [isOpenModal, setIsOpenModal] = useState(false)
42
+
43
+ const yearOption = generateYearOptions(minYear, maxYear)
44
+ const monthOption = generateMonthOptions()
45
+
46
+ const [mounted, setMounted] = useState(false)
47
+ useEffect(() => {
48
+ setMounted(true)
49
+ return () => setMounted(false)
50
+ }, [])
51
+
52
+ const portalRoot =
53
+ (document.getElementById("root") as HTMLElement) ||
54
+ (document.getElementById("__next") as HTMLElement) ||
55
+ document.body
56
+
57
+ return (
58
+ <Controller
59
+ name={name}
60
+ control={control}
61
+ render={({ field, fieldState }) => {
62
+ const value: [string, string] = field.value ?? ["", ""]
63
+ const [startDate, endDate] = value
64
+ const { onChange } = field
65
+ const { invalid, error } = fieldState
66
+
67
+ // sync เดือนที่โชว์ตอนเปิด modal
68
+ // eslint-disable-next-line react-hooks/rules-of-hooks
69
+ useEffect(() => {
70
+ if (isOpenModal) {
71
+ setShowDay(startDate || getDateNow())
72
+ }
73
+ }, [isOpenModal])
74
+
75
+ function CalendarRow({ year, month }: { year: number; month: string }) {
76
+ const days = getDaysInMonthWithWeekday(year, month)
77
+
78
+ const mapToThaiWeek = (dayIndex: number) => dayIndex % 7
79
+ const firstDayThaiIndex = mapToThaiWeek(new Date(`${year}-${month}-01`).getDay())
80
+ const emptyStartDays = Array(firstDayThaiIndex).fill(null)
81
+
82
+ const allDays = [...emptyStartDays, ...days.map((d) => d.date.split("-")[2])]
83
+ while (allDays.length < 42) allDays.push(null)
84
+
85
+ const weeks: Array<Array<string | null>> = []
86
+ for (let i = 0; i < allDays.length; i += 7) weeks.push(allDays.slice(i, i + 7))
87
+
88
+ return (
89
+ <>
90
+ {weeks.map((week, rowIndex) => (
91
+ <div key={rowIndex} className="DBui-inputDateRangeRowDay">
92
+ {week.map((day, i) => {
93
+ if (!day) {
94
+ return (
95
+ <p key={i} className="DBui-inputDateRangeDay" data-hidden-hover>
96
+ {" "}
97
+ </p>
98
+ )
99
+ }
100
+
101
+ const targetDate = `${getYearInDateString(showDay)}-${getMonthInDateString(showDay)}-${day}`
102
+
103
+ const isChecked =
104
+ startDate && !endDate
105
+ ? targetDate === startDate
106
+ : startDate && endDate
107
+ ? targetDate >= startDate && targetDate <= endDate
108
+ : false
109
+
110
+ return (
111
+ <p
112
+ key={i}
113
+ className="DBui-inputDateRangeDay"
114
+ data-checked={isChecked}
115
+ onClick={() => {
116
+ if (!startDate) {
117
+ onChange([targetDate, ""])
118
+ } else if (startDate && !endDate) {
119
+ if (targetDate >= startDate) {
120
+ onChange([startDate, targetDate])
121
+ setIsOpenModal(false)
122
+ } else {
123
+ onChange([targetDate, ""])
124
+ }
125
+ } else {
126
+ onChange([targetDate, ""])
127
+ }
128
+ }}
129
+ >
130
+ {day}
131
+ </p>
132
+ )
133
+ })}
134
+ </div>
135
+ ))}
136
+ </>
137
+ )
138
+ }
139
+
140
+ const handleClear = (e: React.MouseEvent<HTMLImageElement>) => {
141
+ e.stopPropagation()
142
+ onChange(["", ""])
143
+ }
144
+
145
+ return (
146
+ <InputBase
147
+ name={name}
148
+ label={label}
149
+ require={require}
150
+ fullWidth={fullWidth}
151
+ isInvalid={invalid}
152
+ errorMessage={error?.message}
153
+ >
154
+ <>
155
+ <div
156
+ className="DBui-inputDateRange"
157
+ onClick={() => (disabled ? null : setIsOpenModal(true))}
158
+ data-invalid={invalid}
159
+ data-disabled={disabled}
160
+ >
161
+ <p>{startDate ? `${startDate}${endDate ? ` to ${endDate}` : ""}` : placeholder}</p>
162
+
163
+ <img
164
+ src={closedSVG}
165
+ className="DBui-clearIconInputDateRange"
166
+ onClick={handleClear}
167
+ data-hidden={(startDate === "" && endDate === "") || disabled || isHideClearIcon}
168
+ />
169
+ </div>
170
+
171
+ {mounted &&
172
+ createPortal(
173
+ <Modal id="modalInputDateRange" open={isOpenModal} onClose={() => setIsOpenModal(false)}>
174
+ <div className="DBui-inputDateRangeWrapperCalendar">
175
+ <div className="DBui-inputDateRangeRowMonthYear">
176
+ <img
177
+ src={expandArrowSVG}
178
+ className="DBui-inputDateRangeRowMonthYearSelected"
179
+ style={{ transform: "rotate(90deg)" }}
180
+ onClick={() => {
181
+ setShowDay(
182
+ updateMonthInDateString(
183
+ showDay,
184
+ String(Number(showDay.split("-")[1]) - 1).padStart(2, "0"),
185
+ ),
186
+ )
187
+ }}
188
+ />
189
+
190
+ <Menu
191
+ isInModal
192
+ trigger={() => (
193
+ <h4 className="DBui-inputDateRangeRowMonthYearSelected">{showDay.slice(0, 4)}</h4>
194
+ )}
195
+ >
196
+ {({ close }) =>
197
+ yearOption.map((y, index) => (
198
+ <p
199
+ key={index}
200
+ className="DBui-inputDateRangeRowMonthYearOption"
201
+ onClick={() => {
202
+ setShowDay(updateYearInDateString(showDay, y.value))
203
+ close()
204
+ }}
205
+ data-checked={getYearInDateString(showDay) === y.value}
206
+ >
207
+ {y.label}
208
+ </p>
209
+ ))
210
+ }
211
+ </Menu>
212
+
213
+ <Menu
214
+ isInModal
215
+ trigger={() => (
216
+ <h4 className="DBui-inputDateRangeRowMonthYearSelected" style={{ width: "7rem" }}>
217
+ {monthOption.find((m) => m.value === showDay.slice(5, 7))?.label}
218
+ </h4>
219
+ )}
220
+ >
221
+ {({ close }) =>
222
+ monthOption.map((m, index) => (
223
+ <p
224
+ key={index}
225
+ className="DBui-inputDateRangeRowMonthYearOption"
226
+ onClick={() => {
227
+ setShowDay(updateMonthInDateString(showDay, m.value))
228
+ close()
229
+ }}
230
+ data-checked={getMonthInDateString(showDay) === m.value}
231
+ >
232
+ {m.label}
233
+ </p>
234
+ ))
235
+ }
236
+ </Menu>
237
+
238
+ <img
239
+ src={expandArrowSVG}
240
+ className="DBui-inputDateRangeRowMonthYearSelected"
241
+ style={{ transform: "rotate(-90deg)" }}
242
+ onClick={() => {
243
+ setShowDay(
244
+ updateMonthInDateString(
245
+ showDay,
246
+ String(Number(showDay.split("-")[1]) + 1).padStart(2, "0"),
247
+ ),
248
+ )
249
+ }}
250
+ />
251
+ </div>
252
+
253
+ <div>
254
+ <div className="DBui-inputDateRangeRowHeaderDay">
255
+ <p className="DBui-inputDateRangeHeader">Sun</p>
256
+ <p className="DBui-inputDateRangeHeader">Mon</p>
257
+ <p className="DBui-inputDateRangeHeader">Tue</p>
258
+ <p className="DBui-inputDateRangeHeader">Wed</p>
259
+ <p className="DBui-inputDateRangeHeader">Thu</p>
260
+ <p className="DBui-inputDateRangeHeader">Fri</p>
261
+ <p className="DBui-inputDateRangeHeader">Sat</p>
262
+ </div>
263
+
264
+ <CalendarRow year={getYearInDateString(showDay)} month={getMonthInDateString(showDay)} />
265
+ </div>
266
+ </div>
267
+ </Modal>,
268
+ portalRoot,
269
+ )}
270
+
271
+ {!mounted && (
272
+ <Modal id="modalInputDateRange" open={isOpenModal} onClose={() => setIsOpenModal(false)}>
273
+ <div className="DBui-inputDateRangeWrapperCalendar" />
274
+ </Modal>
275
+ )}
276
+ </>
277
+ </InputBase>
278
+ )
279
+ }}
280
+ />
281
+ )
282
+ }
283
+
284
+ export default InputDateRange
@@ -0,0 +1,11 @@
1
+ export type PropsInputDateRange = {
2
+ name: string
3
+ label?: string
4
+ placeholder?: string
5
+ disabled?: boolean
6
+ require?: boolean
7
+ fullWidth?: boolean
8
+ isHideClearIcon?: boolean
9
+ minYear?: number
10
+ maxYear?: number
11
+ }