@helpwave/hightide 0.0.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.
Files changed (162) hide show
  1. package/.storybook/main.ts +24 -0
  2. package/.storybook/preview.tsx +67 -0
  3. package/LICENSE +373 -0
  4. package/README.md +8 -0
  5. package/coloring/shading.ts +46 -0
  6. package/coloring/types.ts +13 -0
  7. package/components/Avatar.tsx +58 -0
  8. package/components/AvatarGroup.tsx +48 -0
  9. package/components/BreadCrumb.tsx +35 -0
  10. package/components/Button.tsx +236 -0
  11. package/components/ChipList.tsx +89 -0
  12. package/components/Circle.tsx +27 -0
  13. package/components/ErrorComponent.tsx +40 -0
  14. package/components/Expandable.tsx +61 -0
  15. package/components/HelpwaveBadge.tsx +35 -0
  16. package/components/HideableContentSection.tsx +43 -0
  17. package/components/InputGroup.tsx +72 -0
  18. package/components/LoadingAndErrorComponent.tsx +47 -0
  19. package/components/LoadingAnimation.tsx +40 -0
  20. package/components/LoadingButton.tsx +27 -0
  21. package/components/MarkdownInterpreter.tsx +278 -0
  22. package/components/Pagination.tsx +65 -0
  23. package/components/Profile.tsx +124 -0
  24. package/components/ProgressIndicator.tsx +58 -0
  25. package/components/Ring.tsx +286 -0
  26. package/components/SearchableList.tsx +69 -0
  27. package/components/SortButton.tsx +33 -0
  28. package/components/Span.tsx +0 -0
  29. package/components/StepperBar.tsx +124 -0
  30. package/components/Table.tsx +330 -0
  31. package/components/TechRadar.tsx +247 -0
  32. package/components/TextImage.tsx +86 -0
  33. package/components/TimeDisplay.tsx +121 -0
  34. package/components/Tooltip.tsx +92 -0
  35. package/components/VerticalDivider.tsx +51 -0
  36. package/components/date/DatePicker.tsx +164 -0
  37. package/components/date/DayPicker.tsx +95 -0
  38. package/components/date/TimePicker.tsx +167 -0
  39. package/components/date/YearMonthPicker.tsx +130 -0
  40. package/components/examples/InputGroupExample.tsx +58 -0
  41. package/components/examples/MultiSelectExample.tsx +57 -0
  42. package/components/examples/SearchableSelectExample.tsx +34 -0
  43. package/components/examples/SelectExample.tsx +28 -0
  44. package/components/examples/StackingModals.tsx +54 -0
  45. package/components/examples/TableExample.tsx +159 -0
  46. package/components/examples/TextareaExample.tsx +23 -0
  47. package/components/examples/TileExample.tsx +25 -0
  48. package/components/examples/Title.tsx +0 -0
  49. package/components/examples/date/DateTimePickerExample.tsx +53 -0
  50. package/components/examples/properties/CheckboxPropertyExample.tsx +29 -0
  51. package/components/examples/properties/DatePropertyExample.tsx +44 -0
  52. package/components/examples/properties/MultiSelectPropertyExample.tsx +39 -0
  53. package/components/examples/properties/NumberPropertyExample.tsx +28 -0
  54. package/components/examples/properties/SelectPropertyExample.tsx +39 -0
  55. package/components/examples/properties/TextPropertyExample.tsx +30 -0
  56. package/components/icons/Helpwave.tsx +51 -0
  57. package/components/icons/Tag.tsx +29 -0
  58. package/components/layout/Carousel.tsx +396 -0
  59. package/components/layout/DividerInserter.tsx +37 -0
  60. package/components/layout/FAQSection.tsx +57 -0
  61. package/components/layout/Tile.tsx +67 -0
  62. package/components/modals/ConfirmDialog.tsx +105 -0
  63. package/components/modals/DiscardChangesDialog.tsx +71 -0
  64. package/components/modals/InputModal.tsx +26 -0
  65. package/components/modals/LanguageModal.tsx +76 -0
  66. package/components/modals/Modal.tsx +149 -0
  67. package/components/modals/ModalRegister.tsx +45 -0
  68. package/components/properties/CheckboxProperty.tsx +62 -0
  69. package/components/properties/DateProperty.tsx +58 -0
  70. package/components/properties/MultiSelectProperty.tsx +82 -0
  71. package/components/properties/NumberProperty.tsx +86 -0
  72. package/components/properties/PropertyBase.tsx +84 -0
  73. package/components/properties/SelectProperty.tsx +67 -0
  74. package/components/properties/TextProperty.tsx +81 -0
  75. package/components/user-input/Checkbox.tsx +139 -0
  76. package/components/user-input/DateAndTimePicker.tsx +156 -0
  77. package/components/user-input/Input.tsx +192 -0
  78. package/components/user-input/Label.tsx +32 -0
  79. package/components/user-input/Menu.tsx +75 -0
  80. package/components/user-input/MultiSelect.tsx +158 -0
  81. package/components/user-input/ScrollPicker.tsx +240 -0
  82. package/components/user-input/SearchableSelect.tsx +36 -0
  83. package/components/user-input/Select.tsx +132 -0
  84. package/components/user-input/Textarea.tsx +86 -0
  85. package/components/user-input/ToggleableInput.tsx +115 -0
  86. package/eslint.config.js +3 -0
  87. package/globals.css +488 -0
  88. package/hooks/useHoverState.ts +88 -0
  89. package/hooks/useLanguage.tsx +78 -0
  90. package/hooks/useLocalStorage.tsx +33 -0
  91. package/hooks/useOutsideClick.ts +25 -0
  92. package/hooks/useSaveDelay.ts +46 -0
  93. package/hooks/useTheme.tsx +57 -0
  94. package/hooks/useTranslation.ts +43 -0
  95. package/index.ts +0 -0
  96. package/package.json +71 -0
  97. package/postcss.config.mjs +7 -0
  98. package/stories/README.md +23 -0
  99. package/stories/coloring/shading.stories.tsx +54 -0
  100. package/stories/geometry/Circle.stories.tsx +16 -0
  101. package/stories/geometry/rings/AnimatedRing.stories.tsx +18 -0
  102. package/stories/geometry/rings/RadialRings.stories.tsx +19 -0
  103. package/stories/geometry/rings/Ring.stories.tsx +17 -0
  104. package/stories/geometry/rings/RingWave.stories.tsx +20 -0
  105. package/stories/layout/FAQSection.stories.tsx +49 -0
  106. package/stories/layout/InputGroup.stories.tsx +19 -0
  107. package/stories/layout/Table.stories.tsx +19 -0
  108. package/stories/layout/TextImage.stories.tsx +24 -0
  109. package/stories/layout/chip/Chip.stories.tsx +19 -0
  110. package/stories/layout/chip/ChipList.stories.tsx +27 -0
  111. package/stories/layout/tile/Tile.stories.ts +20 -0
  112. package/stories/layout/tile/TileWithImage.stories.tsx +27 -0
  113. package/stories/other/BreadCrumbs.stories.tsx +21 -0
  114. package/stories/other/HelpwaveBadge.stories.tsx +18 -0
  115. package/stories/other/HelpwaveSpinner.stories.tsx +19 -0
  116. package/stories/other/MarkdownInterpreter.stories.tsx +18 -0
  117. package/stories/other/Profile.stories.tsx +52 -0
  118. package/stories/other/SearchableList.stories.tsx +21 -0
  119. package/stories/other/StackingModals.stories.tsx +16 -0
  120. package/stories/other/TechRadar.stories.tsx +14 -0
  121. package/stories/other/Translation.stories.tsx +56 -0
  122. package/stories/other/VerticalDivider.stories.tsx +20 -0
  123. package/stories/other/avatar/Avatar.stories.tsx +19 -0
  124. package/stories/other/avatar/AvatarGroup.stories.tsx +26 -0
  125. package/stories/other/tooltip/Tooltip.stories.tsx +30 -0
  126. package/stories/other/tooltip/TooltipStack.stories.tsx +39 -0
  127. package/stories/user-action/button/LoadingButton.stories.tsx +21 -0
  128. package/stories/user-action/button/OutlineButton.stories.tsx +22 -0
  129. package/stories/user-action/button/SolidButton.stories.tsx +22 -0
  130. package/stories/user-action/button/TextButton.stories.tsx +22 -0
  131. package/stories/user-action/input/Checkbox.stories.tsx +20 -0
  132. package/stories/user-action/input/Label.stories.tsx +18 -0
  133. package/stories/user-action/input/ScrollPicker.stories.tsx +20 -0
  134. package/stories/user-action/input/Textarea.stories.tsx +22 -0
  135. package/stories/user-action/input/date/DatePicker.stories.tsx +23 -0
  136. package/stories/user-action/input/date/DateTimePicker.stories.tsx +26 -0
  137. package/stories/user-action/input/date/DayPicker.stories.tsx +20 -0
  138. package/stories/user-action/input/date/TimePicker.stories.tsx +20 -0
  139. package/stories/user-action/input/date/YearMonthPicker.stories.tsx +21 -0
  140. package/stories/user-action/input/select/MultiSelect.stories.tsx +39 -0
  141. package/stories/user-action/input/select/SearchableSelect.stories.tsx +32 -0
  142. package/stories/user-action/input/select/Select.stories.tsx +30 -0
  143. package/stories/user-action/properties/CheckboxProperty.stories.tsx +20 -0
  144. package/stories/user-action/properties/DateProperty.stories.tsx +21 -0
  145. package/stories/user-action/properties/MultiSelectProperty.stories.tsx +33 -0
  146. package/stories/user-action/properties/NumberProperty.stories.tsx +21 -0
  147. package/stories/user-action/properties/PropertyBase.stories.tsx +28 -0
  148. package/stories/user-action/properties/SingleSelectProperty.stories.tsx +35 -0
  149. package/stories/user-action/properties/TextProperty.stories.tsx +20 -0
  150. package/tsconfig.json +20 -0
  151. package/util/array.ts +115 -0
  152. package/util/builder.ts +9 -0
  153. package/util/date.ts +180 -0
  154. package/util/easeFunctions.ts +37 -0
  155. package/util/emailValidation.ts +3 -0
  156. package/util/loopingArray.ts +94 -0
  157. package/util/math.ts +3 -0
  158. package/util/news.ts +43 -0
  159. package/util/noop.ts +1 -0
  160. package/util/simpleSearch.ts +65 -0
  161. package/util/storage.ts +37 -0
  162. package/util/types.ts +4 -0
@@ -0,0 +1,330 @@
1
+ import type { ReactElement } from 'react'
2
+ import { Scrollbars } from 'react-custom-scrollbars-2'
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { noop } from '../util/noop'
5
+ import { Checkbox } from './user-input/Checkbox'
6
+ import { Pagination } from './Pagination'
7
+ import clsx from 'clsx'
8
+
9
+ export type TableStatePagination = {
10
+ currentPage: number,
11
+ entriesPerPage: number,
12
+ }
13
+ export const defaultTableStatePagination = {
14
+ currentPage: 0,
15
+ entriesPerPage: 5
16
+ }
17
+
18
+ export type TableStateSelection<T> = {
19
+ currentSelection: T[],
20
+ hasSelectedAll: boolean,
21
+ hasSelectedSome: boolean,
22
+ hasSelectedNone: boolean,
23
+ }
24
+
25
+ export const defaultTableStateSelection = {
26
+ currentSelection: [],
27
+ hasSelectedAll: false,
28
+ hasSelectedSome: false,
29
+ hasSelectedNone: true
30
+ }
31
+
32
+ export type TableState = {
33
+ pagination?: TableStatePagination,
34
+ selection?: {
35
+ /**
36
+ * The mapped ids of the dataType
37
+ */
38
+ currentSelection: string[],
39
+ hasSelectedAll: boolean,
40
+ hasSelectedSome: boolean,
41
+ hasSelectedNone: boolean,
42
+ },
43
+ }
44
+
45
+ type IdentifierMapping<T> = (dataObject: T) => string
46
+
47
+ export const isDataObjectSelected = <T, >(tableState: TableState, dataObject: T, identifierMapping: IdentifierMapping<T>) => {
48
+ if (!tableState.selection) {
49
+ return false
50
+ }
51
+
52
+ return !!tableState.selection.currentSelection.find(value => value.localeCompare(identifierMapping(dataObject)) === 0)
53
+ }
54
+
55
+ export const pageForItem = <T, >(data: T[], item: T, entriesPerPage: number, identifierMapping: IdentifierMapping<T>) => {
56
+ const index = data.findIndex(value => identifierMapping(value) === identifierMapping(item))
57
+ if (index !== -1) {
58
+ return Math.floor(index / entriesPerPage)
59
+ }
60
+ console.warn("item doesn't exist on data", item, data)
61
+ return 0
62
+ }
63
+
64
+ export const updatePagination = (pagination: TableStatePagination, dataLength: number): TableStatePagination => ({
65
+ ...pagination,
66
+ currentPage: Math.min(Math.max(Math.ceil(dataLength / pagination.entriesPerPage) - 1, 0), pagination.currentPage)
67
+ })
68
+
69
+ export const addElementToTable = <T, >(tableState: TableState, data: T[], dataObject: T, identifierMapping: IdentifierMapping<T>) => {
70
+ return {
71
+ ...tableState,
72
+ pagination: tableState.pagination ? {
73
+ ...tableState.pagination,
74
+ currentPage: pageForItem(data, dataObject, tableState.pagination.entriesPerPage, identifierMapping)
75
+ } : undefined,
76
+ selection: tableState.selection ? {
77
+ ...tableState.selection,
78
+ hasSelectedAll: false,
79
+ hasSelectedSome: tableState.selection.hasSelectedAll || tableState.selection.hasSelectedSome
80
+ } : undefined
81
+ }
82
+ }
83
+
84
+ /**
85
+ * data length before delete
86
+ */
87
+ export const removeFromTableSelection = <T, >(tableState: TableState, deletedObjects: T[], dataLength: number, identifierMapping: IdentifierMapping<T>): TableState => {
88
+ if (!tableState.selection) {
89
+ return tableState
90
+ }
91
+
92
+ const deletedObjectIds = deletedObjects.map(identifierMapping)
93
+ const elementsBefore = tableState.selection.currentSelection.length
94
+ const currentSelection = tableState.selection.currentSelection.filter((value) => !deletedObjectIds.includes(value))
95
+ dataLength -= elementsBefore - currentSelection.length
96
+
97
+ return {
98
+ ...tableState,
99
+ selection: {
100
+ currentSelection,
101
+ hasSelectedAll: currentSelection.length === dataLength && dataLength !== 0,
102
+ hasSelectedSome: currentSelection.length > 0 && currentSelection.length !== dataLength,
103
+ hasSelectedNone: currentSelection.length === 0,
104
+ },
105
+ pagination: tableState.pagination ? updatePagination(tableState.pagination, dataLength) : undefined
106
+ }
107
+ }
108
+
109
+ export const changeTableSelectionSingle = <T, >(tableState: TableState, dataObject: T, dataLength: number, identifierMapping: IdentifierMapping<T>) => {
110
+ if (!tableState.selection) {
111
+ return tableState
112
+ }
113
+
114
+ const hasSelectedObject = isDataObjectSelected(tableState, dataObject, identifierMapping)
115
+ let currentSelection = [...tableState.selection.currentSelection, identifierMapping(dataObject)] // case !hasSelectedObject
116
+ if (hasSelectedObject) {
117
+ currentSelection = tableState.selection.currentSelection.filter(value => value.localeCompare(identifierMapping(dataObject)) !== 0)
118
+ }
119
+
120
+ return {
121
+ ...tableState,
122
+ selection: {
123
+ currentSelection,
124
+ hasSelectedAll: currentSelection.length === dataLength,
125
+ hasSelectedSome: currentSelection.length > 0 && currentSelection.length !== dataLength,
126
+ hasSelectedNone: currentSelection.length === 0,
127
+ }
128
+ }
129
+ }
130
+
131
+ const changeTableSelectionAll = <T, >(tableState: TableState, data: T[], identifierMapping: IdentifierMapping<T>) => {
132
+ if (!tableState.selection) {
133
+ return tableState
134
+ }
135
+
136
+ if (data.length === 0) {
137
+ return {
138
+ ...tableState,
139
+ selection: {
140
+ currentSelection: [],
141
+ hasSelectedAll: false,
142
+ hasSelectedSome: false,
143
+ hasSelectedNone: true
144
+ }
145
+ }
146
+ }
147
+
148
+ const hasSelectedAll = !(tableState.selection.hasSelectedSome || tableState.selection.hasSelectedAll)
149
+ return {
150
+ ...tableState,
151
+ selection: {
152
+ currentSelection: hasSelectedAll ? data.map(identifierMapping) : [],
153
+ hasSelectedAll,
154
+ hasSelectedSome: false,
155
+ hasSelectedNone: !hasSelectedAll
156
+ }
157
+ }
158
+ }
159
+
160
+ export type TableSortingType = 'ascending' | 'descending'
161
+ export type TableSortingFunctionType<T> = (t1: T, t2: T) => number
162
+
163
+ export type TableProps<T> = {
164
+ data: T[],
165
+ /**
166
+ * When using selection or pagination
167
+ */
168
+ stateManagement?: [TableState, (tableState: TableState) => void],
169
+ identifierMapping: IdentifierMapping<T>,
170
+ /**
171
+ * Only the cell itself no boilerplate <tr> or <th> required
172
+ */
173
+ header?: ReactElement[],
174
+ /**
175
+ * Only the cells of the row no boilerplate <tr> or <td> required
176
+ */
177
+ rowMappingToCells: (dataObject: T) => ReactElement[],
178
+ sorting?: [TableSortingFunctionType<T>, TableSortingType],
179
+ /**
180
+ * Always go to the page of this element
181
+ */
182
+ focusElement?: T,
183
+ className?: string,
184
+ }
185
+
186
+ /* Possible extension for better customization
187
+ * Map each element to the displayed row
188
+ * make sure to wrap it in the <tr> and <td> you require
189
+ rowMappingToHTMLRow?: (dataObject: T) => ReactElement
190
+ */
191
+
192
+ /**
193
+ * A Basic stateless reusable table
194
+ * The state must be handled and saved with the updateTableState method
195
+ */
196
+ export const Table = <T, >({
197
+ data,
198
+ stateManagement = [{}, noop],
199
+ identifierMapping,
200
+ header,
201
+ rowMappingToCells,
202
+ sorting,
203
+ focusElement,
204
+ className
205
+ }: TableProps<T>) => {
206
+ const sortedData = [...data]
207
+ if (sorting) {
208
+ const [sortingFunction, sortingType] = sorting
209
+ sortedData.sort((a, b) => sortingFunction(a, b) * (sortingType === 'ascending' ? 1 : -1))
210
+ }
211
+ let currentPage = 0
212
+ let pageCount = 1
213
+ let entriesPerPage = 5
214
+ const [tableState, updateTableState] = stateManagement
215
+
216
+ let shownElements = sortedData
217
+
218
+ if (tableState?.pagination) {
219
+ if (tableState.pagination.entriesPerPage < 1) {
220
+ console.error('tableState.pagination.entriesPerPage must be >= 1', tableState.pagination.entriesPerPage)
221
+ }
222
+ entriesPerPage = Math.max(1, tableState.pagination.entriesPerPage)
223
+ pageCount = Math.ceil(sortedData.length / entriesPerPage)
224
+
225
+ if (tableState.pagination.currentPage < 0 || (tableState.pagination.currentPage >= pageCount && pageCount !== 0)) {
226
+ console.error('tableState.pagination.currentPage < 0 || (tableState.pagination.currentPage >= pageCount && pageCount !== 0) must be fullfilled',
227
+ [`pageCount: ${pageCount}`, `tableState.pagination.currentPage: ${tableState.pagination.currentPage}`])
228
+ } else {
229
+ currentPage = tableState.pagination.currentPage
230
+ }
231
+
232
+ if (focusElement) {
233
+ currentPage = pageForItem(sortedData, focusElement, entriesPerPage, identifierMapping)
234
+ }
235
+
236
+ shownElements = sortedData.slice(currentPage * entriesPerPage, Math.min(sortedData.length, (currentPage + 1) * entriesPerPage))
237
+ } else {
238
+ currentPage = 0
239
+ }
240
+
241
+ const headerRow = 'border-b-2 border-gray-300'
242
+ const headerPaddingHead = 'pb-2'
243
+ const headerPaddingBody = 'pt-2'
244
+ const cellPadding = 'py-1 px-2'
245
+
246
+ const [scrollbarsAutoHeightMin, setScrollbarsAutoHeightMin] = useState(0)
247
+ const tableRef = useRef<HTMLTableElement>(null)
248
+
249
+ const calculateHeight = () => {
250
+ if (tableRef.current) {
251
+ const tableHeight = tableRef.current.offsetHeight
252
+ const offset = 25
253
+ setScrollbarsAutoHeightMin(tableHeight + offset)
254
+ }
255
+ }
256
+
257
+ useEffect(() => {
258
+ calculateHeight()
259
+
260
+ // New function to unbind properly
261
+ const handleResize = () => {
262
+ calculateHeight()
263
+ }
264
+
265
+ window.addEventListener('resize', handleResize)
266
+
267
+ return () => {
268
+ window.removeEventListener('resize', handleResize)
269
+ }
270
+ }, [data, currentPage])
271
+
272
+ return (
273
+ <div className={clsx('col gap-y-4 overflow-hidden', className)}>
274
+ <div>
275
+ <Scrollbars autoHeight autoHeightMin={scrollbarsAutoHeightMin}>
276
+ <table ref={tableRef} className="w-full mb-[12px]">
277
+ <thead>
278
+ <tr className={headerRow}>
279
+ {header && tableState.selection && (
280
+ <th className={headerPaddingHead}>
281
+ <Checkbox
282
+ checked={tableState.selection.hasSelectedSome ? 'indeterminate' : tableState.selection.hasSelectedAll}
283
+ onChange={() => updateTableState(changeTableSelectionAll(tableState, data, identifierMapping))}
284
+ />
285
+ </th>
286
+ )}
287
+ {header && header.map((value, index) => (
288
+ <th key={`tableHeader${index}`} className={headerPaddingHead}>
289
+ <div className="row justify-start px-2">
290
+ {value}
291
+ </div>
292
+ </th>
293
+ ))}
294
+ </tr>
295
+ </thead>
296
+ <tbody>
297
+ {shownElements.map((value, rowIndex) => (
298
+ <tr key={identifierMapping(value)}>
299
+ {tableState.selection && (
300
+ <td className={clsx(cellPadding, { [headerPaddingBody]: rowIndex === 0 })}>
301
+ <Checkbox
302
+ checked={isDataObjectSelected(tableState, value, identifierMapping)}
303
+ onChange={() => {
304
+ updateTableState(changeTableSelectionSingle(tableState, value, data.length, identifierMapping))
305
+ }}
306
+ />
307
+ </td>
308
+ )}
309
+ {rowMappingToCells(value).map((value1, index) => (
310
+ <td key={index} className={clsx(cellPadding, { [headerPaddingBody]: rowIndex === 0 })}>
311
+ {value1}
312
+ </td>
313
+ ))}
314
+ </tr>
315
+ ))}
316
+ </tbody>
317
+ </table>
318
+ </Scrollbars>
319
+ </div>
320
+ <div className="row justify-center">
321
+ {tableState.pagination && (
322
+ <Pagination page={currentPage} numberOfPages={pageCount} onPageChanged={page => updateTableState({
323
+ ...tableState,
324
+ pagination: { entriesPerPage, currentPage: page }
325
+ })}/>
326
+ )}
327
+ </div>
328
+ </div>
329
+ )
330
+ }
@@ -0,0 +1,247 @@
1
+ import { useEffect, useState } from 'react'
2
+ import Script from 'next/script'
3
+
4
+ type Config = {
5
+ colors: {
6
+ background: string, // hex
7
+ grid: string, // hex
8
+ inactive: string, // hex
9
+ },
10
+ title: string,
11
+ quadrants: {
12
+ name: string,
13
+ }[],
14
+ rings: {
15
+ name: string,
16
+ color: string, // hex
17
+ }[],
18
+ print_layout: boolean,
19
+ links_in_new_tabs: boolean,
20
+ zoomed_quadrant?: 0 | 1 | 2 | 3,
21
+ }
22
+
23
+ type Entry = {
24
+ label: string,
25
+ link?: string,
26
+ active: boolean,
27
+ quadrant: 0 | 1 | 2 | 3, // Clockwise, starts from bottom right
28
+ ring: 0 | 1 | 2 | 3, // Starts from the inside
29
+ moved: -1 | 0 | 1, // -1: moved out, 0: not moved, 1: moved in
30
+ }
31
+
32
+ const helpwaveTechRadar = {
33
+ date: new Date('1/28/2024'),
34
+ config: {
35
+ colors: {
36
+ background: '#fff',
37
+ grid: '#dddde0',
38
+ inactive: '#ddd'
39
+ },
40
+ title: 'helpwave Tech-Radar',
41
+ quadrants: [
42
+ { name: 'Languages' },
43
+ { name: 'Language dependencies' },
44
+ { name: 'Infrastructure' },
45
+ { name: 'Datastores' },
46
+ ],
47
+ rings: [
48
+ { name: 'ADOPT', color: '#5ba300' },
49
+ { name: 'TRIAL', color: '#009eb0' },
50
+ { name: 'ASSESS', color: '#c7ba00' },
51
+ { name: 'HOLD', color: '#e09b96' }
52
+ ],
53
+ print_layout: true,
54
+ links_in_new_tabs: true,
55
+ } satisfies Config,
56
+ entries: [
57
+ // Languages
58
+ {
59
+ label: 'Golang',
60
+ active: true,
61
+ quadrant: 0,
62
+ ring: 0,
63
+ moved: 0,
64
+ },
65
+ {
66
+ label: 'Python',
67
+ active: true,
68
+ quadrant: 0,
69
+ ring: 2,
70
+ moved: 0,
71
+ },
72
+ {
73
+ label: 'TypeScript',
74
+ active: true,
75
+ quadrant: 0,
76
+ ring: 0,
77
+ moved: 0,
78
+ },
79
+ {
80
+ label: 'Dart',
81
+ active: true,
82
+ quadrant: 0,
83
+ ring: 0,
84
+ moved: 0,
85
+ },
86
+ // Language dependencies
87
+ {
88
+ label: 'Dapr',
89
+ link: 'https://dapr.io/',
90
+ active: true,
91
+ quadrant: 1,
92
+ ring: 0,
93
+ moved: 0,
94
+ },
95
+ {
96
+ label: 'GORM',
97
+ active: true,
98
+ quadrant: 1,
99
+ ring: 3,
100
+ moved: -1,
101
+ },
102
+ {
103
+ label: 'SQLc',
104
+ active: true,
105
+ quadrant: 1,
106
+ ring: 0,
107
+ moved: 1,
108
+ },
109
+
110
+ {
111
+ label: 'Flutter',
112
+ active: true,
113
+ quadrant: 1,
114
+ ring: 0,
115
+ moved: 0,
116
+ },
117
+ {
118
+ label: 'Next.js',
119
+ active: true,
120
+ quadrant: 1,
121
+ ring: 0,
122
+ moved: 0,
123
+ },
124
+ // Infrastructure
125
+ {
126
+ label: 'Docker',
127
+ active: true,
128
+ quadrant: 2,
129
+ ring: 0,
130
+ moved: 0,
131
+ },
132
+ {
133
+ label: 'Kubernetes',
134
+ active: true,
135
+ quadrant: 2,
136
+ ring: 2,
137
+ moved: 1,
138
+ },
139
+ {
140
+ label: 'gRPC',
141
+ active: true,
142
+ quadrant: 2,
143
+ ring: 0,
144
+ moved: 0,
145
+ },
146
+ {
147
+ label: 'APISIX',
148
+ active: true,
149
+ quadrant: 2,
150
+ ring: 0,
151
+ moved: 0,
152
+ },
153
+ {
154
+ label: 'GitHub Actions',
155
+ active: true,
156
+ quadrant: 2,
157
+ ring: 0,
158
+ moved: 0,
159
+ },
160
+ {
161
+ label: 'devenv',
162
+ active: true,
163
+ quadrant: 2,
164
+ ring: 1,
165
+ moved: 0,
166
+ },
167
+ // Datastores
168
+ {
169
+ label: 'PostgreSQL',
170
+ active: true,
171
+ quadrant: 3,
172
+ ring: 0,
173
+ moved: 0,
174
+ },
175
+ {
176
+ label: 'Redis',
177
+ active: true,
178
+ quadrant: 3,
179
+ ring: 0,
180
+ moved: 0,
181
+ },
182
+ {
183
+ label: 'EventStoreDB',
184
+ active: true,
185
+ quadrant: 3,
186
+ ring: 1,
187
+ moved: 0,
188
+ },
189
+ ] satisfies Entry[]
190
+ }
191
+
192
+ type TechRadarProps = {
193
+ date?: Date,
194
+ config?: Config,
195
+ entries?: Entry[],
196
+ }
197
+
198
+ /**
199
+ * This component wraps https://github.com/zalando/tech-radar
200
+ */
201
+ export const TechRadar = ({
202
+ date = helpwaveTechRadar.date,
203
+ config = helpwaveTechRadar.config,
204
+ entries = helpwaveTechRadar.entries,
205
+ }: TechRadarProps) => {
206
+ const [isD3Loaded, setIsD3Loaded] = useState(false)
207
+ const [isRadarLoaded, setIsRadarLoaded] = useState(false)
208
+
209
+ const dateString = date.toLocaleDateString('en-US', { year: 'numeric', month: 'numeric', day: 'numeric' })
210
+
211
+ useEffect(() => {
212
+ if (!isD3Loaded || !isRadarLoaded) return
213
+
214
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
215
+ // @ts-expect-error
216
+ d3.select('svg#radar').select('*').remove()
217
+
218
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
219
+ // @ts-expect-error
220
+ radar_visualization({
221
+ svg_id: 'radar',
222
+ width: 1500,
223
+ height: 1000,
224
+ date: dateString,
225
+ entries,
226
+ ...config,
227
+ })
228
+ }, [isD3Loaded, isRadarLoaded, config, entries]) // eslint-disable-line react-hooks/exhaustive-deps
229
+
230
+ return (
231
+ <>
232
+ <Script
233
+ src="https://cdn.helpwave.de/js/d3.v4.min.js"
234
+ onLoad={() => {
235
+ setIsD3Loaded(true)
236
+ }}
237
+ />
238
+ <Script
239
+ src="https://cdn.helpwave.de/js/radar-0.8.js"
240
+ onLoad={() => {
241
+ setIsRadarLoaded(true)
242
+ }}
243
+ />
244
+ <svg id="radar"></svg>
245
+ </>
246
+ )
247
+ }
@@ -0,0 +1,86 @@
1
+ import type { Languages } from '../hooks/useLanguage'
2
+ import type { PropsForTranslation } from '../hooks/useTranslation'
3
+ import { useTranslation } from '../hooks/useTranslation'
4
+ import clsx from 'clsx'
5
+
6
+ type TextImageColor = 'primary' | 'secondary' | 'dark'
7
+
8
+ type TextImageTranslation = {
9
+ showMore: string,
10
+ }
11
+
12
+ const defaultTextImageTranslation: Record<Languages, TextImageTranslation> = {
13
+ de: {
14
+ showMore: 'Mehr anzeigen'
15
+ },
16
+ en: {
17
+ showMore: 'Show more'
18
+ }
19
+ }
20
+
21
+ export type TextImageProps = {
22
+ title: string,
23
+ description: string,
24
+ imageUrl: string,
25
+ onShowMoreClicked?: () => void,
26
+ color?: TextImageColor,
27
+ badge?: string,
28
+ contentClassName?: string,
29
+ className?: string,
30
+ }
31
+
32
+ /**
33
+ * A Component for layering a Text upon a Image
34
+ */
35
+ export const TextImage = ({
36
+ overwriteTranslation,
37
+ title,
38
+ description,
39
+ imageUrl,
40
+ onShowMoreClicked,
41
+ color = 'primary',
42
+ badge,
43
+ contentClassName = '',
44
+ className = '',
45
+ }: PropsForTranslation<TextImageTranslation, TextImageProps>) => {
46
+ const translation = useTranslation(defaultTextImageTranslation, overwriteTranslation)
47
+
48
+ const chipColorMapping: Record<TextImageColor, string> = {
49
+ primary: 'text-text-image-primary-background bg-text-text-image-primary-text',
50
+ secondary: 'text-text-image-secondary-background bg-text-text-image-secondary-text',
51
+ dark: 'text-text-image-dark-background bg-text-text-image-dark-text',
52
+ }
53
+
54
+ const colorMapping: Record<TextImageColor, string> = {
55
+ primary: 'text-text-image-primary-text bg-linear-to-r from-30% from-text-image-primary-background to-text-image-primary-background/55',
56
+ secondary: 'text-text-image-secondary-text bg-linear-to-r from-30% from-text-image-secondary-background to-text-image-secondary-background/55',
57
+ dark: 'text-text-image-dark-text bg-linear-to-r from-30% from-text-image-dark-background to-text-image-dark-background/55',
58
+ }
59
+
60
+ return (
61
+ <div
62
+ className={clsx('rounded-2xl w-full', className)}
63
+ style={{
64
+ backgroundImage: `url(${imageUrl})`,
65
+ backgroundSize: 'cover',
66
+ }}>
67
+ <div
68
+ className={clsx(`col px-6 py-12 rounded-2xl h-full`, colorMapping[color], contentClassName)}>
69
+ {badge && (
70
+ <div className={clsx(`chip-full bg-white mb-2 py-2 px-4 w-fit`, chipColorMapping[color])}>
71
+ <span className="text-lg font-bold">{badge}</span>
72
+ </div>
73
+ )}
74
+ <div className="col gap-y-1 text-white overflow-hidden">
75
+ <span className="textstyle-title-xl">{title}</span>
76
+ <span className="text-ellipsis overflow-hidden">{description}</span>
77
+ </div>
78
+ {onShowMoreClicked && (
79
+ <div className="row mt-2 text-white underline">
80
+ <button onClick={onShowMoreClicked}>{translation.showMore}</button>
81
+ </div>
82
+ )}
83
+ </div>
84
+ </div>
85
+ )
86
+ }