@carlonicora/nextjs-jsonapi 1.0.3 → 1.0.4

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 (261) hide show
  1. package/package.json +2 -1
  2. package/src/atoms/index.ts +1 -0
  3. package/src/atoms/recentPagesAtom.ts +10 -0
  4. package/src/client/context/JsonApiContext.ts +61 -0
  5. package/src/client/context/JsonApiProvider.tsx +27 -0
  6. package/src/client/context/index.ts +2 -0
  7. package/src/client/hooks/index.ts +3 -0
  8. package/src/client/hooks/useJsonApiGet.ts +188 -0
  9. package/src/client/hooks/useJsonApiMutation.ts +193 -0
  10. package/src/client/hooks/useRehydration.ts +47 -0
  11. package/src/client/index.ts +11 -0
  12. package/src/client/request.ts +97 -0
  13. package/src/client/token.ts +10 -0
  14. package/src/components/containers/PageContainer.tsx +15 -0
  15. package/src/components/containers/ReactMarkdownContainer.tsx +119 -0
  16. package/src/components/containers/TabsContainer.tsx +93 -0
  17. package/src/components/containers/index.ts +3 -0
  18. package/src/components/contents/AttributeElement.tsx +20 -0
  19. package/src/components/contents/index.ts +1 -0
  20. package/src/components/details/AllowedUsersDetails.tsx +23 -0
  21. package/src/components/details/index.ts +1 -0
  22. package/src/components/editors/BlockNoteDiffInlineContent.tsx +152 -0
  23. package/src/components/editors/BlockNoteEditor.tsx +404 -0
  24. package/src/components/editors/BlockNoteEditorContainer.tsx +13 -0
  25. package/src/components/editors/BlockNoteEditorFormattingToolbar.tsx +38 -0
  26. package/src/components/editors/index.ts +1 -0
  27. package/src/components/errors/ErrorDetails.tsx +41 -0
  28. package/src/components/errors/errorToast.ts +9 -0
  29. package/src/components/errors/index.ts +2 -0
  30. package/src/components/forms/CommonAssociationForm.tsx +162 -0
  31. package/src/components/forms/CommonDeleter.tsx +94 -0
  32. package/src/components/forms/CommonEditorButtons.tsx +30 -0
  33. package/src/components/forms/CommonEditorHeader.tsx +35 -0
  34. package/src/components/forms/CommonEditorTrigger.tsx +26 -0
  35. package/src/components/forms/DatePickerPopover.tsx +219 -0
  36. package/src/components/forms/DateRangeSelector.tsx +110 -0
  37. package/src/components/forms/FileUploader.tsx +324 -0
  38. package/src/components/forms/FormCheckbox.tsx +66 -0
  39. package/src/components/forms/FormContainerGeneric.tsx +39 -0
  40. package/src/components/forms/FormDate.tsx +247 -0
  41. package/src/components/forms/FormDateTime.tsx +231 -0
  42. package/src/components/forms/FormInput.tsx +110 -0
  43. package/src/components/forms/FormPassword.tsx +54 -0
  44. package/src/components/forms/FormPlaceAutocomplete.tsx +286 -0
  45. package/src/components/forms/FormSelect.tsx +72 -0
  46. package/src/components/forms/FormSlider.tsx +51 -0
  47. package/src/components/forms/FormSwitch.tsx +25 -0
  48. package/src/components/forms/FormTextarea.tsx +44 -0
  49. package/src/components/forms/MultiFileUploader.tsx +107 -0
  50. package/src/components/forms/PasswordInput.tsx +47 -0
  51. package/src/components/forms/index.ts +21 -0
  52. package/src/components/index.ts +11 -0
  53. package/src/components/navigations/Breadcrumb.tsx +83 -0
  54. package/src/components/navigations/ContentTitle.tsx +39 -0
  55. package/src/components/navigations/Header.tsx +27 -0
  56. package/src/components/navigations/ModeToggleSwitch.tsx +25 -0
  57. package/src/components/navigations/PageSection.tsx +64 -0
  58. package/src/components/navigations/RecentPagesNavigator.tsx +52 -0
  59. package/src/components/navigations/index.ts +6 -0
  60. package/src/components/pages/PageContainerContentDetails.tsx +76 -0
  61. package/src/components/pages/PageContentContainer.tsx +31 -0
  62. package/src/components/pages/index.ts +2 -0
  63. package/src/components/tables/ContentListTable.tsx +165 -0
  64. package/src/components/tables/ContentTableSearch.tsx +105 -0
  65. package/src/components/tables/cells/cell.component.tsx +18 -0
  66. package/src/components/tables/cells/cell.date.tsx +16 -0
  67. package/src/components/tables/cells/cell.id.tsx +27 -0
  68. package/src/components/tables/cells/cell.link.tsx +18 -0
  69. package/src/components/tables/cells/cell.text.tsx +12 -0
  70. package/src/components/tables/cells/cell.url.tsx +13 -0
  71. package/src/components/tables/cells/index.ts +5 -0
  72. package/src/components/tables/index.ts +3 -0
  73. package/src/contexts/SharedContext.tsx +35 -0
  74. package/src/contexts/index.ts +2 -0
  75. package/src/core/abstracts/AbstractApiData.ts +138 -0
  76. package/src/core/abstracts/AbstractService.ts +263 -0
  77. package/src/core/abstracts/index.ts +2 -0
  78. package/src/core/endpoint/EndpointCreator.ts +97 -0
  79. package/src/core/endpoint/index.ts +1 -0
  80. package/src/core/factories/JsonApiDataFactory.ts +12 -0
  81. package/src/core/factories/RehydrationFactory.ts +30 -0
  82. package/src/core/factories/index.ts +2 -0
  83. package/src/core/fields/FieldSelector.ts +15 -0
  84. package/src/core/fields/index.ts +1 -0
  85. package/src/core/index.ts +20 -0
  86. package/src/core/interfaces/ApiData.ts +8 -0
  87. package/src/core/interfaces/ApiDataInterface.ts +15 -0
  88. package/src/core/interfaces/ApiRequestDataTypeInterface.ts +14 -0
  89. package/src/core/interfaces/ApiResponseInterface.ts +17 -0
  90. package/src/core/interfaces/JsonApiHydratedDataInterface.ts +5 -0
  91. package/src/core/interfaces/index.ts +5 -0
  92. package/src/core/registry/DataClassRegistry.ts +51 -0
  93. package/src/core/registry/ModuleRegistrar.ts +43 -0
  94. package/src/core/registry/ModuleRegistry.ts +64 -0
  95. package/src/core/registry/index.ts +3 -0
  96. package/src/core/utils/index.ts +2 -0
  97. package/src/core/utils/rehydrate.ts +24 -0
  98. package/src/core/utils/translateResponse.ts +125 -0
  99. package/src/features/auth/auth.module.ts +9 -0
  100. package/src/features/auth/config.ts +57 -0
  101. package/src/features/auth/data/auth.interface.ts +31 -0
  102. package/src/features/auth/data/auth.service.ts +159 -0
  103. package/src/features/auth/data/auth.ts +54 -0
  104. package/src/features/auth/data/index.ts +3 -0
  105. package/src/features/auth/index.ts +3 -0
  106. package/src/features/company/company.module.ts +10 -0
  107. package/src/features/company/data/company.fields.ts +6 -0
  108. package/src/features/company/data/company.interface.ts +28 -0
  109. package/src/features/company/data/company.service.ts +73 -0
  110. package/src/features/company/data/company.ts +104 -0
  111. package/src/features/company/data/index.ts +4 -0
  112. package/src/features/company/index.ts +2 -0
  113. package/src/features/content/content.module.ts +20 -0
  114. package/src/features/content/data/content.fields.ts +13 -0
  115. package/src/features/content/data/content.interface.ts +23 -0
  116. package/src/features/content/data/content.service.ts +75 -0
  117. package/src/features/content/data/content.ts +85 -0
  118. package/src/features/content/data/index.ts +4 -0
  119. package/src/features/content/index.ts +2 -0
  120. package/src/features/feature/components/forms/FormFeatures.tsx +149 -0
  121. package/src/features/feature/components/index.ts +1 -0
  122. package/src/features/feature/data/feature.interface.ts +9 -0
  123. package/src/features/feature/data/feature.service.ts +19 -0
  124. package/src/features/feature/data/feature.ts +33 -0
  125. package/src/features/feature/data/index.ts +3 -0
  126. package/src/features/feature/feature.module.ts +10 -0
  127. package/src/features/feature/index.ts +3 -0
  128. package/src/features/index.ts +12 -0
  129. package/src/features/module/data/index.ts +2 -0
  130. package/src/features/module/data/module.interface.ts +12 -0
  131. package/src/features/module/data/module.ts +42 -0
  132. package/src/features/module/index.ts +2 -0
  133. package/src/features/module/module.module.ts +10 -0
  134. package/src/features/notification/data/index.ts +4 -0
  135. package/src/features/notification/data/notification.fields.ts +8 -0
  136. package/src/features/notification/data/notification.interface.ts +14 -0
  137. package/src/features/notification/data/notification.service.ts +34 -0
  138. package/src/features/notification/data/notification.ts +51 -0
  139. package/src/features/notification/index.ts +2 -0
  140. package/src/features/notification/notification.module.ts +10 -0
  141. package/src/features/push/data/index.ts +3 -0
  142. package/src/features/push/data/push.interface.ts +8 -0
  143. package/src/features/push/data/push.service.ts +17 -0
  144. package/src/features/push/data/push.ts +18 -0
  145. package/src/features/push/index.ts +2 -0
  146. package/src/features/push/push.module.ts +10 -0
  147. package/src/features/role/data/index.ts +4 -0
  148. package/src/features/role/data/role.fields.ts +8 -0
  149. package/src/features/role/data/role.interface.ts +16 -0
  150. package/src/features/role/data/role.service.ts +117 -0
  151. package/src/features/role/data/role.ts +62 -0
  152. package/src/features/role/index.ts +2 -0
  153. package/src/features/role/role.module.ts +10 -0
  154. package/src/features/s3/data/index.ts +3 -0
  155. package/src/features/s3/data/s3.interface.ts +11 -0
  156. package/src/features/s3/data/s3.service.ts +30 -0
  157. package/src/features/s3/data/s3.ts +60 -0
  158. package/src/features/s3/index.ts +2 -0
  159. package/src/features/s3/s3.module.ts +10 -0
  160. package/src/features/search/index.ts +1 -0
  161. package/src/features/search/interfaces/index.ts +1 -0
  162. package/src/features/search/interfaces/search.result.interface.ts +3 -0
  163. package/src/features/user/author.module.ts +10 -0
  164. package/src/features/user/components/index.ts +2 -0
  165. package/src/features/user/components/lists/ContributorsList.tsx +41 -0
  166. package/src/features/user/components/lists/index.ts +1 -0
  167. package/src/features/user/components/widgets/UserAvatar.tsx +86 -0
  168. package/src/features/user/components/widgets/index.ts +1 -0
  169. package/src/features/user/contexts/CurrentUserContext.tsx +156 -0
  170. package/src/features/user/contexts/index.ts +1 -0
  171. package/src/features/user/data/index.ts +4 -0
  172. package/src/features/user/data/user.fields.ts +8 -0
  173. package/src/features/user/data/user.interface.ts +41 -0
  174. package/src/features/user/data/user.service.ts +246 -0
  175. package/src/features/user/data/user.ts +162 -0
  176. package/src/features/user/index.ts +4 -0
  177. package/src/features/user/user.module.ts +21 -0
  178. package/src/hooks/TableGeneratorRegistry.ts +53 -0
  179. package/src/hooks/index.ts +33 -0
  180. package/src/hooks/types.ts +35 -0
  181. package/src/hooks/url.rewriter.ts +22 -0
  182. package/src/hooks/useCustomD3Graph.tsx +705 -0
  183. package/src/hooks/useDataListRetriever.ts +349 -0
  184. package/src/hooks/useDebounce.ts +33 -0
  185. package/src/hooks/usePageUrlGenerator.ts +50 -0
  186. package/src/hooks/useTableGenerator.ts +16 -0
  187. package/src/i18n/config.ts +73 -0
  188. package/src/i18n/index.ts +18 -0
  189. package/src/index.ts +16 -0
  190. package/src/interfaces/breadcrumb.item.data.interface.ts +4 -0
  191. package/src/interfaces/d3.link.interface.ts +7 -0
  192. package/src/interfaces/d3.node.interface.ts +12 -0
  193. package/src/interfaces/index.ts +3 -0
  194. package/src/permissions/check.ts +127 -0
  195. package/src/permissions/index.ts +2 -0
  196. package/src/permissions/types.ts +109 -0
  197. package/src/roles/config.ts +46 -0
  198. package/src/roles/index.ts +1 -0
  199. package/src/server/cache.ts +28 -0
  200. package/src/server/index.ts +3 -0
  201. package/src/server/request.ts +113 -0
  202. package/src/server/token.ts +10 -0
  203. package/src/shadcnui/custom/kanban.tsx +1001 -0
  204. package/src/shadcnui/custom/link.tsx +18 -0
  205. package/src/shadcnui/custom/multi-select.tsx +382 -0
  206. package/src/shadcnui/index.ts +49 -0
  207. package/src/shadcnui/ui/accordion.tsx +52 -0
  208. package/src/shadcnui/ui/alert-dialog.tsx +141 -0
  209. package/src/shadcnui/ui/alert.tsx +43 -0
  210. package/src/shadcnui/ui/avatar.tsx +50 -0
  211. package/src/shadcnui/ui/badge.tsx +40 -0
  212. package/src/shadcnui/ui/breadcrumb.tsx +115 -0
  213. package/src/shadcnui/ui/button.tsx +51 -0
  214. package/src/shadcnui/ui/calendar.tsx +73 -0
  215. package/src/shadcnui/ui/card.tsx +43 -0
  216. package/src/shadcnui/ui/carousel.tsx +225 -0
  217. package/src/shadcnui/ui/chart.tsx +320 -0
  218. package/src/shadcnui/ui/checkbox.tsx +29 -0
  219. package/src/shadcnui/ui/collapsible.tsx +11 -0
  220. package/src/shadcnui/ui/command.tsx +155 -0
  221. package/src/shadcnui/ui/context-menu.tsx +179 -0
  222. package/src/shadcnui/ui/dialog.tsx +96 -0
  223. package/src/shadcnui/ui/drawer.tsx +89 -0
  224. package/src/shadcnui/ui/dropdown-menu.tsx +205 -0
  225. package/src/shadcnui/ui/form.tsx +138 -0
  226. package/src/shadcnui/ui/hover-card.tsx +29 -0
  227. package/src/shadcnui/ui/input.tsx +21 -0
  228. package/src/shadcnui/ui/label.tsx +26 -0
  229. package/src/shadcnui/ui/navigation-menu.tsx +168 -0
  230. package/src/shadcnui/ui/popover.tsx +33 -0
  231. package/src/shadcnui/ui/progress.tsx +25 -0
  232. package/src/shadcnui/ui/radio-group.tsx +37 -0
  233. package/src/shadcnui/ui/resizable.tsx +47 -0
  234. package/src/shadcnui/ui/scroll-area.tsx +40 -0
  235. package/src/shadcnui/ui/select.tsx +164 -0
  236. package/src/shadcnui/ui/separator.tsx +28 -0
  237. package/src/shadcnui/ui/sheet.tsx +139 -0
  238. package/src/shadcnui/ui/sidebar.tsx +677 -0
  239. package/src/shadcnui/ui/skeleton.tsx +13 -0
  240. package/src/shadcnui/ui/slider.tsx +25 -0
  241. package/src/shadcnui/ui/sonner.tsx +25 -0
  242. package/src/shadcnui/ui/switch.tsx +31 -0
  243. package/src/shadcnui/ui/table.tsx +120 -0
  244. package/src/shadcnui/ui/tabs.tsx +55 -0
  245. package/src/shadcnui/ui/textarea.tsx +24 -0
  246. package/src/shadcnui/ui/toggle.tsx +39 -0
  247. package/src/shadcnui/ui/tooltip.tsx +61 -0
  248. package/src/unified/JsonApiRequest.ts +325 -0
  249. package/src/unified/index.ts +1 -0
  250. package/src/utils/blocknote-diff.util.ts +815 -0
  251. package/src/utils/blocknote-word-diff-renderer.util.ts +413 -0
  252. package/src/utils/cn.ts +6 -0
  253. package/src/utils/compose-refs.ts +61 -0
  254. package/src/utils/date-formatter.ts +53 -0
  255. package/src/utils/exists.ts +7 -0
  256. package/src/utils/index.ts +15 -0
  257. package/src/utils/schemas/entity.object.schema.ts +8 -0
  258. package/src/utils/schemas/index.ts +2 -0
  259. package/src/utils/schemas/user.object.schema.ts +9 -0
  260. package/src/utils/table-options.ts +67 -0
  261. package/src/utils/use-mobile.tsx +21 -0
@@ -0,0 +1,231 @@
1
+ "use client";
2
+
3
+ import { Calendar as CalendarIcon, CircleXIcon } from "lucide-react";
4
+ import { useMemo, useState } from "react";
5
+ import { useI18nDateFnsLocale, useI18nLocale, useI18nTranslations } from "../../i18n";
6
+ import {
7
+ Button,
8
+ Calendar,
9
+ FormControl,
10
+ FormField,
11
+ FormItem,
12
+ FormLabel,
13
+ FormMessage,
14
+ Label,
15
+ Popover,
16
+ PopoverContent,
17
+ PopoverTrigger,
18
+ Select,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ } from "../../shadcnui";
24
+ import { cn } from "../../utils";
25
+
26
+ export function FormDateTime({
27
+ form,
28
+ id,
29
+ name,
30
+ minDate,
31
+ onChange,
32
+ allowEmpty,
33
+ }: {
34
+ form: any;
35
+ id: string;
36
+ name?: string;
37
+ placeholder?: string;
38
+ minDate?: Date;
39
+ onChange?: (date?: Date) => Promise<void>;
40
+ allowEmpty?: boolean;
41
+ }) {
42
+ const [open, setOpen] = useState<boolean>(false);
43
+ const t = useI18nTranslations();
44
+ const locale = useI18nLocale();
45
+ const dateFnsLocale = useI18nDateFnsLocale();
46
+
47
+ // Locale-aware date-time formatter
48
+ const dateTimeFormatter = useMemo(
49
+ () =>
50
+ new Intl.DateTimeFormat(locale, {
51
+ year: "numeric",
52
+ month: "long",
53
+ day: "numeric",
54
+ hour: "2-digit",
55
+ minute: "2-digit",
56
+ }),
57
+ [locale],
58
+ );
59
+
60
+ // Format date-time for display
61
+ const formatDateTime = (date: Date): string => dateTimeFormatter.format(date);
62
+
63
+ const [selectedHours, setSelectedHours] = useState<number>(new Date().getHours());
64
+ const [selectedMinutes, setSelectedMinutes] = useState<number>(roundToNearestFiveMinutes(new Date().getMinutes()));
65
+
66
+ const hoursOptions = Array.from({ length: 24 }, (_, i) => {
67
+ const hour = i;
68
+ return {
69
+ value: hour,
70
+ label: hour.toString().padStart(2, "0"),
71
+ };
72
+ });
73
+
74
+ const minutesOptions = Array.from({ length: 12 }, (_, i) => {
75
+ const minute = i * 5;
76
+ return {
77
+ value: minute,
78
+ label: minute.toString().padStart(2, "0"),
79
+ };
80
+ });
81
+
82
+ function roundToNearestFiveMinutes(minutes: number): number {
83
+ return (Math.round(minutes / 5) * 5) % 60;
84
+ }
85
+
86
+ const handleTimeChange = (hours: number, minutes: number) => {
87
+ const currentDate = form.getValues(id);
88
+ if (currentDate) {
89
+ const updatedDate = new Date(currentDate);
90
+ updatedDate.setHours(hours);
91
+ updatedDate.setMinutes(minutes);
92
+ form.setValue(id, updatedDate);
93
+ if (onChange) onChange(updatedDate);
94
+ }
95
+ };
96
+
97
+ return (
98
+ <div className="flex w-full flex-col">
99
+ <FormField
100
+ control={form.control}
101
+ name={id}
102
+ render={({ field }) => (
103
+ <FormItem className={`${name ? "mb-5" : "mb-1"} w-full`}>
104
+ {name && <FormLabel>{name}</FormLabel>}
105
+ <FormControl>
106
+ <div className="relative flex flex-row">
107
+ <Popover open={open} onOpenChange={setOpen} modal={true}>
108
+ <div className="flex w-full flex-row items-center justify-between">
109
+ <PopoverTrigger asChild>
110
+ <FormControl>
111
+ <Button
112
+ variant={"outline"}
113
+ className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
114
+ >
115
+ {field.value ? formatDateTime(field.value) : <span>{t(`generic.pick_date_time`)}</span>}
116
+ <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
117
+ </Button>
118
+ </FormControl>
119
+ </PopoverTrigger>
120
+ {field.value && allowEmpty !== false && (
121
+ <CircleXIcon
122
+ className="text-muted hover:text-destructive ml-2 h-6 w-6 cursor-pointer"
123
+ onClick={() => {
124
+ if (onChange) onChange(undefined);
125
+ form.setValue(id, "");
126
+ }}
127
+ />
128
+ )}
129
+ </div>
130
+ <PopoverContent className="w-auto p-4" align="start">
131
+ <div className="flex flex-col space-y-4">
132
+ <Calendar
133
+ mode="single"
134
+ selected={field.value}
135
+ onSelect={(date) => {
136
+ if (date) {
137
+ // Preserve the current time when selecting a new date
138
+ const newDate = new Date(date);
139
+ if (field.value) {
140
+ const currentDate = new Date(field.value);
141
+ newDate.setHours(currentDate.getHours(), currentDate.getMinutes());
142
+ } else {
143
+ newDate.setHours(selectedHours, selectedMinutes);
144
+ }
145
+ form.setValue(id, newDate);
146
+ if (onChange) onChange(newDate);
147
+
148
+ // Update time state values
149
+ setSelectedHours(newDate.getHours());
150
+ setSelectedMinutes(roundToNearestFiveMinutes(newDate.getMinutes()));
151
+ }
152
+ }}
153
+ disabled={(date) => (minDate && date < minDate ? true : false)}
154
+ locale={dateFnsLocale}
155
+ />
156
+ <div className="flex flex-row items-end justify-center space-x-4">
157
+ <div className="flex flex-col space-y-2">
158
+ <Label htmlFor="hours-select">{t(`generic.hours`)}</Label>
159
+ <Select
160
+ value={String(field.value ? new Date(field.value).getHours() : selectedHours)}
161
+ onValueChange={(value) => {
162
+ const hours = parseInt(value);
163
+ setSelectedHours(hours);
164
+ handleTimeChange(
165
+ hours,
166
+ field.value
167
+ ? roundToNearestFiveMinutes(new Date(field.value).getMinutes())
168
+ : selectedMinutes,
169
+ );
170
+ }}
171
+ >
172
+ <SelectTrigger id="hours-select" className="w-[70px]">
173
+ <SelectValue placeholder="Hour" />
174
+ </SelectTrigger>
175
+ <SelectContent>
176
+ {hoursOptions.map((option) => (
177
+ <SelectItem key={option.value} value={String(option.value)}>
178
+ {option.label}
179
+ </SelectItem>
180
+ ))}
181
+ </SelectContent>
182
+ </Select>
183
+ </div>
184
+ <div className="mb-[9px] text-xl">:</div>
185
+ <div className="flex flex-col space-y-2">
186
+ <Label htmlFor="minutes-select">{t(`generic.minutes`)}</Label>
187
+ <Select
188
+ value={String(
189
+ field.value
190
+ ? roundToNearestFiveMinutes(new Date(field.value).getMinutes())
191
+ : selectedMinutes,
192
+ )}
193
+ onValueChange={(value) => {
194
+ const minutes = parseInt(value);
195
+ setSelectedMinutes(minutes);
196
+ handleTimeChange(field.value ? new Date(field.value).getHours() : selectedHours, minutes);
197
+ }}
198
+ >
199
+ <SelectTrigger id="minutes-select" className="w-[70px]">
200
+ <SelectValue placeholder="Min" />
201
+ </SelectTrigger>
202
+ <SelectContent>
203
+ {minutesOptions.map((option) => (
204
+ <SelectItem key={option.value} value={String(option.value)}>
205
+ {option.label}
206
+ </SelectItem>
207
+ ))}
208
+ </SelectContent>
209
+ </Select>
210
+ </div>
211
+ </div>
212
+ <Button
213
+ className="mt-2"
214
+ onClick={() => {
215
+ setOpen(false);
216
+ }}
217
+ >
218
+ {t(`generic.buttons.select_date`)}
219
+ </Button>
220
+ </div>
221
+ </PopoverContent>
222
+ </Popover>
223
+ </div>
224
+ </FormControl>
225
+ <FormMessage />
226
+ </FormItem>
227
+ )}
228
+ />
229
+ </div>
230
+ );
231
+ }
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import React from "react";
5
+ import { FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from "../../shadcnui";
6
+
7
+ export function FormInput({
8
+ form,
9
+ id,
10
+ name,
11
+ placeholder,
12
+ type,
13
+ onBlur,
14
+ disabled,
15
+ onKeyDown,
16
+ autoFocus,
17
+ onChange,
18
+ testId,
19
+ isRequired = false,
20
+ }: {
21
+ form: any;
22
+ id: string;
23
+ name?: string;
24
+ placeholder?: string;
25
+ type?: "text" | "number" | "currency" | "password" | "link";
26
+ onBlur?: () => Promise<void>;
27
+ disabled?: boolean;
28
+ onKeyDown?: (event: React.KeyboardEvent) => void;
29
+ autoFocus?: boolean;
30
+ onChange?: (value: string | number) => Promise<void>;
31
+ testId?: string;
32
+ isRequired?: boolean;
33
+ }) {
34
+ const t = useTranslations();
35
+
36
+ return (
37
+ <div className="flex w-full flex-col">
38
+ <FormField
39
+ control={form.control}
40
+ name={id}
41
+ render={({ field }) => {
42
+ const handleBlur = async (e: React.FocusEvent<HTMLInputElement>) => {
43
+ let value = e.target.value;
44
+
45
+ if (type === "link" && value) {
46
+ if (!/^https?:\/\//i.test(value)) {
47
+ value = "https://" + value;
48
+ field.onChange(value);
49
+ }
50
+ try {
51
+ new URL(value);
52
+ form.clearErrors(id);
53
+ } catch (error) {
54
+ form.setError(id, {
55
+ type: "validate",
56
+ message: t(`generic.errors.valid_url`),
57
+ });
58
+ }
59
+ }
60
+
61
+ if (onBlur) await onBlur();
62
+ field.onBlur();
63
+ };
64
+
65
+ return (
66
+ <FormItem className={`${name ? "mb-5" : "mb-1"}`}>
67
+ {name && (
68
+ <FormLabel className="flex items-center">
69
+ {name}
70
+ {isRequired && <span className="text-destructive ml-2 font-semibold">*</span>}
71
+ </FormLabel>
72
+ )}
73
+ <FormControl>
74
+ <div className="relative">
75
+ {type === "currency" && (
76
+ <span className="text-muted-foreground absolute top-0 left-0 pt-2 pl-3">€</span>
77
+ )}
78
+ <Input
79
+ data-testid={testId}
80
+ {...field}
81
+ autoFocus={autoFocus === true}
82
+ type={
83
+ type === "number" || type === "currency" ? "number" : type === "password" ? "password" : "text"
84
+ }
85
+ className={`w-full ${type === "number" || type === "currency" ? "text-end" : ""}`}
86
+ disabled={disabled === true || form.formState.isSubmitting}
87
+ placeholder={placeholder || ""}
88
+ onBlur={handleBlur}
89
+ onKeyDown={onKeyDown}
90
+ onChange={(e) => {
91
+ if (type === "number" || type === "currency") {
92
+ const value = e.target.value.replace(/[^0-9]/g, "");
93
+ field.onChange(+value);
94
+ if (onChange) onChange(+value);
95
+ } else {
96
+ field.onChange(e.target.value);
97
+ if (onChange) onChange(e.target.value);
98
+ }
99
+ }}
100
+ />
101
+ </div>
102
+ </FormControl>
103
+ <FormMessage data-testid={testId ? `${testId}-error` : undefined} />
104
+ </FormItem>
105
+ );
106
+ }}
107
+ />
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "../../shadcnui";
4
+ import { PasswordInput } from "./PasswordInput";
5
+
6
+ export function FormPassword({
7
+ form,
8
+ id,
9
+ name,
10
+ placeholder,
11
+ onBlur,
12
+ disabled,
13
+ testId,
14
+ isRequired,
15
+ }: {
16
+ form: any;
17
+ id: string;
18
+ name?: string;
19
+ placeholder?: string;
20
+ onBlur?: () => Promise<void>;
21
+ disabled?: boolean;
22
+ testId?: string;
23
+ isRequired?: boolean;
24
+ }) {
25
+ return (
26
+ <div className="flex w-full flex-col">
27
+ <FormField
28
+ control={form.control}
29
+ name={id}
30
+ render={({ field }) => (
31
+ <FormItem className={`${name ? "mb-5" : "mb-1"}`}>
32
+ {name && (
33
+ <FormLabel>
34
+ {name}
35
+ {isRequired && <span className="text-destructive ml-2 font-semibold">*</span>}
36
+ </FormLabel>
37
+ )}
38
+ <FormControl>
39
+ <PasswordInput
40
+ {...field}
41
+ className={`w-full`}
42
+ disabled={disabled === true || form.formState.isSubmitting}
43
+ placeholder={placeholder ? placeholder : ""}
44
+ onBlur={onBlur}
45
+ data-testid={testId}
46
+ />
47
+ </FormControl>
48
+ <FormMessage data-testid={testId ? `${testId}-error` : undefined} />
49
+ </FormItem>
50
+ )}
51
+ />
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,286 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { FormControl, FormField, FormItem, FormLabel, FormMessage, Input } from "../../shadcnui";
5
+ import { cn } from "../../utils";
6
+
7
+ /**
8
+ * FormPlaceAutocomplete component integrates Google Places API (New)
9
+ * to provide address suggestions as the user types.
10
+ *
11
+ * Prerequisites:
12
+ * 1. Set NEXT_PUBLIC_GOOGLE_MAPS_API_KEY environment variable
13
+ * 2. Enable Places API (New) in Google Cloud Console
14
+ * 3. Configure API key restrictions as needed
15
+ *
16
+ * Note: This uses the new Places API via REST calls, not the legacy JavaScript API
17
+ */
18
+
19
+ interface PlaceSuggestion {
20
+ place_id: string;
21
+ description: string;
22
+ structured_formatting: {
23
+ main_text: string;
24
+ secondary_text: string;
25
+ };
26
+ }
27
+
28
+ interface PlaceAutocompleteProps {
29
+ form: any;
30
+ id: string;
31
+ name?: string;
32
+ placeholder?: string;
33
+ disabled?: boolean;
34
+ testId?: string;
35
+ isRequired?: boolean;
36
+ onPlaceSelect?: (place: PlaceSuggestion) => void;
37
+ className?: string;
38
+ /**
39
+ * Optional array of place types to include in search results.
40
+ * When not specified, defaults to address types: ["street_address", "premise", "subpremise"]
41
+ * For cities, use: ["locality", "administrative_area_level_3"]
42
+ * For regions, use: ["administrative_area_level_1", "administrative_area_level_2"]
43
+ */
44
+ includeTypes?: string[];
45
+ }
46
+
47
+ export function FormPlaceAutocomplete({
48
+ form,
49
+ id,
50
+ name,
51
+ placeholder,
52
+ disabled,
53
+ testId,
54
+ isRequired = false,
55
+ onPlaceSelect,
56
+ className,
57
+ includeTypes,
58
+ }: PlaceAutocompleteProps) {
59
+ const [inputValue, setInputValue] = useState("");
60
+ const [suggestions, setSuggestions] = useState<PlaceSuggestion[]>([]);
61
+ const [isLoading, setIsLoading] = useState(false);
62
+ const [showSuggestions, setShowSuggestions] = useState(false);
63
+ const [loadError, setLoadError] = useState(false);
64
+ const [apiKey, setApiKey] = useState<string | null>(null);
65
+ const debounceRef = useRef<NodeJS.Timeout | null>(null);
66
+ const containerRef = useRef<HTMLDivElement>(null);
67
+
68
+ // Initialize API key
69
+ useEffect(() => {
70
+ const key = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
71
+
72
+ if (!key) {
73
+ console.error("Google Maps API key not found. Please set NEXT_PUBLIC_GOOGLE_MAPS_API_KEY environment variable.");
74
+ setLoadError(true);
75
+ return;
76
+ }
77
+
78
+ setApiKey(key);
79
+ }, []);
80
+
81
+ // Update input value when form value changes
82
+ useEffect(() => {
83
+ const formValue = form.getValues(id);
84
+ if (formValue !== inputValue) {
85
+ setInputValue(formValue || "");
86
+ }
87
+ }, [form.watch(id), id, inputValue]);
88
+
89
+ // Fetch place suggestions from Google Places API (New)
90
+ const fetchSuggestions = async (input: string) => {
91
+ if (!apiKey) return;
92
+
93
+ try {
94
+ setIsLoading(true);
95
+
96
+ // Using the new Places API autocomplete endpoint
97
+ const response = await fetch(`https://places.googleapis.com/v1/places:autocomplete`, {
98
+ method: "POST",
99
+ headers: {
100
+ "Content-Type": "application/json",
101
+ "X-Goog-Api-Key": apiKey,
102
+ },
103
+ body: JSON.stringify({
104
+ input: input,
105
+ includedPrimaryTypes: includeTypes || ["street_address", "premise", "subpremise"],
106
+ languageCode: "en",
107
+ }),
108
+ });
109
+
110
+ if (!response.ok) {
111
+ throw new Error(`Places API error: ${response.status}`);
112
+ }
113
+
114
+ const data = await response.json();
115
+
116
+ if (data.suggestions) {
117
+ const formattedSuggestions: PlaceSuggestion[] = data.suggestions.map((suggestion: any) => ({
118
+ place_id: suggestion.placePrediction?.placeId || "",
119
+ description: suggestion.placePrediction?.text?.text || "",
120
+ structured_formatting: {
121
+ main_text: suggestion.placePrediction?.structuredFormat?.mainText?.text || "",
122
+ secondary_text: suggestion.placePrediction?.structuredFormat?.secondaryText?.text || "",
123
+ },
124
+ }));
125
+
126
+ setSuggestions(formattedSuggestions);
127
+ setShowSuggestions(true);
128
+ } else {
129
+ setSuggestions([]);
130
+ }
131
+ } catch (error) {
132
+ console.error("Error fetching place suggestions:", error);
133
+ setSuggestions([]);
134
+ } finally {
135
+ setIsLoading(false);
136
+ }
137
+ };
138
+
139
+ // Handle input changes with debouncing
140
+ const handleInputChange = (value: string) => {
141
+ setInputValue(value);
142
+ form.setValue(id, value);
143
+
144
+ if (debounceRef.current) {
145
+ clearTimeout(debounceRef.current);
146
+ }
147
+
148
+ if (value.length > 2 && apiKey) {
149
+ debounceRef.current = setTimeout(() => {
150
+ fetchSuggestions(value);
151
+ }, 300);
152
+ } else {
153
+ setSuggestions([]);
154
+ setShowSuggestions(false);
155
+ setIsLoading(false);
156
+ }
157
+ };
158
+
159
+ // Handle suggestion selection
160
+ const handleSuggestionSelect = (suggestion: PlaceSuggestion) => {
161
+ setInputValue(suggestion.description);
162
+ form.setValue(id, suggestion.description);
163
+ setShowSuggestions(false);
164
+ setSuggestions([]);
165
+
166
+ if (onPlaceSelect) {
167
+ onPlaceSelect(suggestion);
168
+ }
169
+ };
170
+
171
+ // Close suggestions when clicking outside
172
+ useEffect(() => {
173
+ const handleClickOutside = (event: MouseEvent) => {
174
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
175
+ setShowSuggestions(false);
176
+ }
177
+ };
178
+
179
+ document.addEventListener("mousedown", handleClickOutside);
180
+ return () => {
181
+ document.removeEventListener("mousedown", handleClickOutside);
182
+ };
183
+ }, []);
184
+
185
+ // Cleanup debounce on unmount
186
+ useEffect(() => {
187
+ return () => {
188
+ if (debounceRef.current) {
189
+ clearTimeout(debounceRef.current);
190
+ }
191
+ };
192
+ }, []);
193
+
194
+ // Fallback to regular input if API key is not available
195
+ if (loadError) {
196
+ return (
197
+ <div className="flex w-full flex-col">
198
+ <FormField
199
+ control={form.control}
200
+ name={id}
201
+ render={({ field }) => (
202
+ <FormItem className={`${name ? "mb-5" : "mb-1"}`}>
203
+ {name && (
204
+ <FormLabel className="flex items-center">
205
+ {name}
206
+ {isRequired && <span className="text-destructive ml-2 font-semibold">*</span>}
207
+ </FormLabel>
208
+ )}
209
+ <FormControl>
210
+ <Input
211
+ {...field}
212
+ placeholder={placeholder}
213
+ disabled={disabled}
214
+ data-testid={testId}
215
+ className={cn("w-full", className)}
216
+ />
217
+ </FormControl>
218
+ <FormMessage />
219
+ </FormItem>
220
+ )}
221
+ />
222
+ </div>
223
+ );
224
+ }
225
+
226
+ return (
227
+ <div className="flex w-full flex-col" ref={containerRef}>
228
+ <FormField
229
+ control={form.control}
230
+ name={id}
231
+ render={({ field }) => (
232
+ <FormItem className={`${name ? "mb-5" : "mb-1"}`}>
233
+ {name && (
234
+ <FormLabel className="flex items-center">
235
+ {name}
236
+ {isRequired && <span className="text-destructive ml-2 font-semibold">*</span>}
237
+ </FormLabel>
238
+ )}
239
+ <FormControl>
240
+ <div className="relative">
241
+ <Input
242
+ value={inputValue}
243
+ onChange={(e) => handleInputChange(e.target.value)}
244
+ onBlur={field.onBlur}
245
+ onFocus={() => {
246
+ if (suggestions.length > 0) {
247
+ setShowSuggestions(true);
248
+ }
249
+ }}
250
+ placeholder={placeholder}
251
+ disabled={disabled || !apiKey}
252
+ data-testid={testId}
253
+ className={cn("w-full", className)}
254
+ />
255
+
256
+ {/* Loading indicator */}
257
+ {isLoading && (
258
+ <div className="absolute right-3 top-1/2 -translate-y-1/2">
259
+ <div className="border-primary h-4 w-4 animate-spin rounded-full border-2 border-t-transparent"></div>
260
+ </div>
261
+ )}
262
+
263
+ {/* Suggestions dropdown */}
264
+ {showSuggestions && suggestions.length > 0 && (
265
+ <div className="bg-background absolute left-0 right-0 top-full z-50 mt-1 max-h-60 overflow-auto rounded-md border shadow-lg">
266
+ {suggestions.map((suggestion, index) => (
267
+ <div
268
+ key={suggestion.place_id || index}
269
+ className="hover:bg-muted cursor-pointer px-3 py-2 text-sm"
270
+ onClick={() => handleSuggestionSelect(suggestion)}
271
+ >
272
+ <div className="font-medium">{suggestion.structured_formatting?.main_text}</div>
273
+ <div className="text-muted-foreground">{suggestion.structured_formatting?.secondary_text}</div>
274
+ </div>
275
+ ))}
276
+ </div>
277
+ )}
278
+ </div>
279
+ </FormControl>
280
+ <FormMessage />
281
+ </FormItem>
282
+ )}
283
+ />
284
+ </div>
285
+ );
286
+ }