@betterstart/cli 0.1.3 → 0.1.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 (234) hide show
  1. package/README.md +133 -0
  2. package/dist/cli.d.ts +1 -9
  3. package/dist/cli.js +13484 -367
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +24 -266
  6. package/dist/index.js +4 -11378
  7. package/dist/index.js.map +1 -1
  8. package/package.json +29 -42
  9. package/templates/schema.json +959 -0
  10. package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
  11. package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
  12. package/templates/tiptap/hooks/use-element-rect.ts +166 -0
  13. package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
  14. package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
  15. package/templates/tiptap/hooks/use-scrolling.ts +64 -0
  16. package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
  17. package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
  18. package/templates/tiptap/hooks/use-unmount.ts +21 -0
  19. package/templates/tiptap/hooks/use-window-size.ts +87 -0
  20. package/templates/tiptap/lib/tiptap-utils.ts +587 -0
  21. package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
  22. package/templates/tiptap/styles/_variables.scss +296 -0
  23. package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
  24. package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
  25. package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
  26. package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
  27. package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
  28. package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
  29. package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
  30. package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
  31. package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
  32. package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
  33. package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
  34. package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
  35. package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
  36. package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
  37. package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
  38. package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
  39. package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
  40. package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
  41. package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
  42. package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
  43. package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
  44. package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
  45. package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
  46. package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
  47. package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
  48. package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
  49. package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
  50. package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
  51. package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
  52. package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
  53. package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
  54. package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
  55. package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
  56. package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
  57. package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
  58. package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
  59. package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
  60. package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
  61. package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
  62. package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
  63. package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
  64. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
  65. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
  66. package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
  67. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
  68. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
  69. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
  70. package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
  71. package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
  72. package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
  73. package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
  74. package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
  75. package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
  76. package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
  77. package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
  78. package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
  79. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
  80. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
  81. package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
  82. package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
  83. package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
  84. package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
  85. package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
  86. package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
  87. package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
  88. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
  89. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
  90. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
  91. package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
  92. package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
  93. package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
  94. package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
  95. package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
  96. package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
  97. package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
  98. package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
  99. package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
  100. package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
  101. package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
  102. package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
  103. package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
  104. package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
  105. package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
  106. package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
  107. package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
  108. package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
  109. package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
  110. package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
  111. package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
  112. package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
  113. package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
  114. package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
  115. package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
  116. package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
  117. package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
  118. package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
  119. package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
  120. package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
  121. package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
  122. package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
  123. package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
  124. package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
  125. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
  126. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
  127. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
  128. package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
  129. package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
  130. package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
  131. package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
  132. package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
  133. package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
  134. package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
  135. package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
  136. package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
  137. package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
  138. package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
  139. package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
  140. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
  141. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
  142. package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
  143. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
  144. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
  145. package/templates/ui/accordion.tsx +52 -0
  146. package/templates/ui/alert-dialog.tsx +116 -0
  147. package/templates/ui/alert.tsx +48 -0
  148. package/templates/ui/aspect-ratio.tsx +7 -0
  149. package/templates/ui/avatar.tsx +46 -0
  150. package/templates/ui/badge.tsx +32 -0
  151. package/templates/ui/breadcrumb.tsx +98 -0
  152. package/templates/ui/button-group.tsx +77 -0
  153. package/templates/ui/button.tsx +48 -0
  154. package/templates/ui/calendar.tsx +176 -0
  155. package/templates/ui/card.tsx +54 -0
  156. package/templates/ui/carousel.tsx +234 -0
  157. package/templates/ui/chart.tsx +349 -0
  158. package/templates/ui/checkbox.tsx +27 -0
  159. package/templates/ui/collapsible.tsx +11 -0
  160. package/templates/ui/command.tsx +142 -0
  161. package/templates/ui/context-menu.tsx +188 -0
  162. package/templates/ui/curriculum-editor.tsx +601 -0
  163. package/templates/ui/date-picker.tsx +70 -0
  164. package/templates/ui/dialog.tsx +103 -0
  165. package/templates/ui/drawer.tsx +99 -0
  166. package/templates/ui/dropdown-menu.tsx +185 -0
  167. package/templates/ui/dynamic-list-field.tsx +95 -0
  168. package/templates/ui/empty.tsx +90 -0
  169. package/templates/ui/field.tsx +231 -0
  170. package/templates/ui/file-upload-example.tsx +113 -0
  171. package/templates/ui/form.tsx +172 -0
  172. package/templates/ui/hover-card.tsx +28 -0
  173. package/templates/ui/icon-picker.tsx +435 -0
  174. package/templates/ui/icons-data.ts +6 -0
  175. package/templates/ui/image-upload-field.tsx +360 -0
  176. package/templates/ui/input-group.tsx +160 -0
  177. package/templates/ui/input-otp.tsx +70 -0
  178. package/templates/ui/input.tsx +21 -0
  179. package/templates/ui/item.tsx +171 -0
  180. package/templates/ui/kbd.tsx +28 -0
  181. package/templates/ui/label.tsx +20 -0
  182. package/templates/ui/logo.tsx +113 -0
  183. package/templates/ui/markdown-editor.tsx +303 -0
  184. package/templates/ui/markdown-utils.ts +128 -0
  185. package/templates/ui/media-upload-field.tsx +255 -0
  186. package/templates/ui/menubar.tsx +230 -0
  187. package/templates/ui/navigation-menu.tsx +119 -0
  188. package/templates/ui/pagination.tsx +96 -0
  189. package/templates/ui/placeholder.tsx +25 -0
  190. package/templates/ui/popover.tsx +32 -0
  191. package/templates/ui/progress.tsx +24 -0
  192. package/templates/ui/radio-group.tsx +37 -0
  193. package/templates/ui/resizable.tsx +41 -0
  194. package/templates/ui/rich-text-editor.tsx +374 -0
  195. package/templates/ui/scroll-area.tsx +45 -0
  196. package/templates/ui/select.tsx +151 -0
  197. package/templates/ui/separator.tsx +25 -0
  198. package/templates/ui/sheet.tsx +120 -0
  199. package/templates/ui/sidebar.tsx +684 -0
  200. package/templates/ui/skeleton.tsx +7 -0
  201. package/templates/ui/slider.tsx +24 -0
  202. package/templates/ui/sonner.tsx +29 -0
  203. package/templates/ui/spinner.tsx +15 -0
  204. package/templates/ui/switch.tsx +28 -0
  205. package/templates/ui/table.tsx +93 -0
  206. package/templates/ui/tabs.tsx +54 -0
  207. package/templates/ui/textarea.tsx +20 -0
  208. package/templates/ui/toast.tsx +127 -0
  209. package/templates/ui/toggle-group.tsx +56 -0
  210. package/templates/ui/toggle.tsx +43 -0
  211. package/templates/ui/tooltip.tsx +31 -0
  212. package/templates/ui/use-mobile.tsx +19 -0
  213. package/templates/ui/video-upload-field.tsx +368 -0
  214. package/dist/chunk-EIH4RRIJ.js +0 -183
  215. package/dist/chunk-EIH4RRIJ.js.map +0 -1
  216. package/dist/chunk-NKRQYAS6.js +0 -260
  217. package/dist/chunk-NKRQYAS6.js.map +0 -1
  218. package/dist/chunk-QLVSHP7X.js +0 -235
  219. package/dist/chunk-QLVSHP7X.js.map +0 -1
  220. package/dist/chunk-WY6BC55D.js +0 -357
  221. package/dist/chunk-WY6BC55D.js.map +0 -1
  222. package/dist/config/index.d.ts +0 -93
  223. package/dist/config/index.js +0 -58
  224. package/dist/config/index.js.map +0 -1
  225. package/dist/core/index.d.ts +0 -415
  226. package/dist/core/index.js +0 -906
  227. package/dist/core/index.js.map +0 -1
  228. package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
  229. package/dist/logger-awLb347n.d.ts +0 -81
  230. package/dist/plugins/index.d.ts +0 -213
  231. package/dist/plugins/index.js +0 -365
  232. package/dist/plugins/index.js.map +0 -1
  233. package/dist/types-ByX_gl6y.d.ts +0 -232
  234. package/dist/types-eI549DEG.d.ts +0 -331
@@ -0,0 +1,601 @@
1
+ 'use client'
2
+
3
+ import { Clock, Plus, X } from 'lucide-react'
4
+ import * as React from 'react'
5
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './accordion'
6
+ import { Button } from './button'
7
+ import { FormDescription, FormLabel } from './form'
8
+ import { Input } from './input'
9
+ import { Placeholder } from './placeholder'
10
+ import { Separator } from './separator'
11
+ import { Textarea } from './textarea'
12
+ import { ToggleGroup, ToggleGroupItem } from './toggle-group'
13
+
14
+ // ============================================================================
15
+ // Types
16
+ // ============================================================================
17
+
18
+ export interface CurriculumSequentialItem {
19
+ title?: string
20
+ description?: string
21
+ }
22
+
23
+ export interface CurriculumWeeklyItem {
24
+ title?: string
25
+ description?: string
26
+ }
27
+
28
+ export interface CurriculumWeek {
29
+ weekNumber: number
30
+ weekTitle?: string
31
+ weekDescription?: string
32
+ durationHours?: number
33
+ items: CurriculumWeeklyItem[]
34
+ }
35
+
36
+ export type CurriculumMode = 'sequential' | 'weekly'
37
+
38
+ // CurriculumData stores BOTH items and weeks - mode determines which editor to show
39
+ // This prevents data loss when toggling between modes
40
+ export interface CurriculumData {
41
+ mode: CurriculumMode
42
+ items: CurriculumSequentialItem[]
43
+ weeks: CurriculumWeek[]
44
+ }
45
+
46
+ export interface CurriculumEditorProps {
47
+ /** Current curriculum value */
48
+ value: CurriculumData | null | undefined
49
+ /** Callback when curriculum changes */
50
+ onChange: (curriculum: CurriculumData) => void
51
+ /** Whether the form is submitting */
52
+ disabled?: boolean
53
+ /** Label for the field */
54
+ label?: string
55
+ /** Hint text */
56
+ hint?: string
57
+ }
58
+
59
+ // ============================================================================
60
+ // Component
61
+ // ============================================================================
62
+
63
+ export const CurriculumEditor = ({
64
+ value,
65
+ onChange,
66
+ disabled = false,
67
+ label = 'Curriculum',
68
+ hint
69
+ }: CurriculumEditorProps) => {
70
+ // Default to sequential mode with empty items/weeks
71
+ const currentMode: CurriculumMode = value?.mode || 'sequential'
72
+ const [expandedItem, setExpandedItem] = React.useState<string | undefined>(undefined)
73
+ const [expandedWeek, setExpandedWeek] = React.useState<string | undefined>(undefined)
74
+
75
+ // Get items and weeks - both are always available in the data structure
76
+ const sequentialItems: CurriculumSequentialItem[] = value?.items || []
77
+ const weeklyData: CurriculumWeek[] = value?.weeks || []
78
+
79
+ // ========== Mode Change ==========
80
+ // Simply switch the mode - data for both modes is preserved
81
+ const handleModeChange = (newMode: CurriculumMode) => {
82
+ if (newMode === currentMode) return
83
+ onChange({
84
+ mode: newMode,
85
+ items: sequentialItems,
86
+ weeks: weeklyData
87
+ })
88
+ }
89
+
90
+ // ========== Sequential Mode Handlers ==========
91
+ const addSequentialItem = () => {
92
+ const newItems = [...sequentialItems, { title: '', description: '' }]
93
+ onChange({ mode: currentMode, items: newItems, weeks: weeklyData })
94
+ setExpandedItem(`item-${newItems.length}`)
95
+ }
96
+
97
+ const removeSequentialItem = (index: number) => {
98
+ const newItems = sequentialItems.filter((_, i) => i !== index)
99
+ onChange({ mode: currentMode, items: newItems, weeks: weeklyData })
100
+ }
101
+
102
+ const updateSequentialItem = (
103
+ index: number,
104
+ field: keyof CurriculumSequentialItem,
105
+ newValue: string
106
+ ) => {
107
+ const newItems = [...sequentialItems]
108
+ newItems[index] = { ...newItems[index], [field]: newValue }
109
+ onChange({ mode: currentMode, items: newItems, weeks: weeklyData })
110
+ }
111
+
112
+ // ========== Weekly Mode Handlers ==========
113
+ const addWeek = () => {
114
+ const nextWeekNumber =
115
+ weeklyData.length > 0 ? Math.max(...weeklyData.map((w) => w.weekNumber)) + 1 : 1
116
+ const newWeeks: CurriculumWeek[] = [
117
+ ...weeklyData,
118
+ { weekNumber: nextWeekNumber, weekTitle: '', weekDescription: '', items: [] }
119
+ ]
120
+ onChange({ mode: currentMode, items: sequentialItems, weeks: newWeeks })
121
+ setExpandedWeek(`week-${newWeeks.length}`)
122
+ }
123
+
124
+ const removeWeek = (weekIndex: number) => {
125
+ const newWeeks = weeklyData.filter((_, i) => i !== weekIndex)
126
+ // Renumber weeks
127
+ const renumberedWeeks = newWeeks.map((week, i) => ({ ...week, weekNumber: i + 1 }))
128
+ onChange({ mode: currentMode, items: sequentialItems, weeks: renumberedWeeks })
129
+ }
130
+
131
+ const updateWeek = (
132
+ weekIndex: number,
133
+ field: 'weekTitle' | 'weekDescription' | 'durationHours',
134
+ newValue: string | number
135
+ ) => {
136
+ const newWeeks = [...weeklyData]
137
+ newWeeks[weekIndex] = { ...newWeeks[weekIndex], [field]: newValue }
138
+ onChange({ mode: currentMode, items: sequentialItems, weeks: newWeeks })
139
+ }
140
+
141
+ const addWeekItem = (weekIndex: number) => {
142
+ const newWeeks = [...weeklyData]
143
+ newWeeks[weekIndex] = {
144
+ ...newWeeks[weekIndex],
145
+ items: [...newWeeks[weekIndex].items, { title: '', description: '' }]
146
+ }
147
+ onChange({ mode: currentMode, items: sequentialItems, weeks: newWeeks })
148
+ }
149
+
150
+ const removeWeekItem = (weekIndex: number, itemIndex: number) => {
151
+ const newWeeks = [...weeklyData]
152
+ newWeeks[weekIndex] = {
153
+ ...newWeeks[weekIndex],
154
+ items: newWeeks[weekIndex].items.filter((_, i) => i !== itemIndex)
155
+ }
156
+ onChange({ mode: currentMode, items: sequentialItems, weeks: newWeeks })
157
+ }
158
+
159
+ const updateWeekItem = (
160
+ weekIndex: number,
161
+ itemIndex: number,
162
+ field: keyof CurriculumWeeklyItem,
163
+ newValue: string
164
+ ) => {
165
+ const newWeeks = [...weeklyData]
166
+ const newItems = [...newWeeks[weekIndex].items]
167
+ newItems[itemIndex] = { ...newItems[itemIndex], [field]: newValue }
168
+ newWeeks[weekIndex] = { ...newWeeks[weekIndex], items: newItems }
169
+ onChange({ mode: currentMode, items: sequentialItems, weeks: newWeeks })
170
+ }
171
+
172
+ // ========== Render ==========
173
+ return (
174
+ <div className="space-y-4">
175
+ {/* Header with mode toggle */}
176
+ <div className="flex items-center justify-between">
177
+ <FormLabel className="text-base">{label}</FormLabel>
178
+ <ToggleGroup
179
+ type="single"
180
+ value={currentMode}
181
+ onValueChange={(val) => val && handleModeChange(val as CurriculumMode)}
182
+ disabled={disabled}
183
+ className="bg-muted rounded-md corner-squircle p-0.5"
184
+ >
185
+ <ToggleGroupItem
186
+ value="sequential"
187
+ size="sm"
188
+ className="text-xs px-3 data-[state=on]:bg-background data-[state=on]:shadow-sm"
189
+ >
190
+ Sequential
191
+ </ToggleGroupItem>
192
+ <ToggleGroupItem
193
+ value="weekly"
194
+ size="sm"
195
+ className="text-xs px-3 data-[state=on]:bg-background data-[state=on]:shadow-sm"
196
+ >
197
+ Weekly
198
+ </ToggleGroupItem>
199
+ </ToggleGroup>
200
+ </div>
201
+
202
+ {hint && <FormDescription>{hint}</FormDescription>}
203
+
204
+ {/* Sequential Mode */}
205
+ {currentMode === 'sequential' && (
206
+ <SequentialEditor
207
+ items={sequentialItems}
208
+ expandedItem={expandedItem}
209
+ setExpandedItem={setExpandedItem}
210
+ disabled={disabled}
211
+ onAdd={addSequentialItem}
212
+ onRemove={removeSequentialItem}
213
+ onUpdate={updateSequentialItem}
214
+ />
215
+ )}
216
+
217
+ {/* Weekly Mode */}
218
+ {currentMode === 'weekly' && (
219
+ <WeeklyEditor
220
+ weeks={weeklyData}
221
+ expandedWeek={expandedWeek}
222
+ setExpandedWeek={setExpandedWeek}
223
+ disabled={disabled}
224
+ onAddWeek={addWeek}
225
+ onRemoveWeek={removeWeek}
226
+ onUpdateWeek={updateWeek}
227
+ onAddItem={addWeekItem}
228
+ onRemoveItem={removeWeekItem}
229
+ onUpdateItem={updateWeekItem}
230
+ />
231
+ )}
232
+ </div>
233
+ )
234
+ }
235
+
236
+ // ============================================================================
237
+ // Sequential Editor Sub-component
238
+ // ============================================================================
239
+
240
+ interface SequentialEditorProps {
241
+ items: CurriculumSequentialItem[]
242
+ expandedItem: string | undefined
243
+ setExpandedItem: (value: string | undefined) => void
244
+ disabled: boolean
245
+ onAdd: () => void
246
+ onRemove: (index: number) => void
247
+ onUpdate: (index: number, field: keyof CurriculumSequentialItem, value: string) => void
248
+ }
249
+
250
+ const SequentialEditor = ({
251
+ items,
252
+ expandedItem,
253
+ setExpandedItem,
254
+ disabled,
255
+ onAdd,
256
+ onRemove,
257
+ onUpdate
258
+ }: SequentialEditorProps) => {
259
+ if (items.length === 0) {
260
+ return (
261
+ <Placeholder
262
+ label="No curriculum items added yet"
263
+ description="Add items to build your curriculum content."
264
+ action={
265
+ <Button type="button" variant="outline" size="lg" onClick={onAdd} disabled={disabled}>
266
+ <Plus className="size-4 mr-2" />
267
+ Add Item
268
+ </Button>
269
+ }
270
+ />
271
+ )
272
+ }
273
+
274
+ return (
275
+ <div className="space-y-5">
276
+ <div className="flex items-center justify-between">
277
+ <FormLabel className="text-base">Items</FormLabel>
278
+ <Button
279
+ type="button"
280
+ variant="outline"
281
+ size="sm"
282
+ onClick={onAdd}
283
+ disabled={disabled || items.length >= 50}
284
+ >
285
+ <Plus className="size-3" />
286
+ Add Item
287
+ </Button>
288
+ </div>
289
+
290
+ <Accordion
291
+ type="single"
292
+ collapsible
293
+ className="w-full gap-1 flex flex-col"
294
+ value={expandedItem}
295
+ onValueChange={setExpandedItem}
296
+ >
297
+ {items.map((item, index) => (
298
+ <AccordionItem key={index} value={`item-${index + 1}`} className="p-0 border-none">
299
+ <div className="space-y-5 rounded-lg border p-4 bg-secondary/50 corner-squircle [&_h3]:m-0 w-full">
300
+ <AccordionTrigger className="flex items-center p-0 justify-between w-full">
301
+ <div className="flex w-full items-center justify-between">
302
+ <h4 className="text-sm font-medium w-full">
303
+ {item.title || `Item ${index + 1}`}
304
+ </h4>
305
+ <Button asChild variant="ghost" size="sm" disabled={disabled}>
306
+ <span
307
+ role="button"
308
+ tabIndex={0}
309
+ onClick={(e) => {
310
+ e.stopPropagation()
311
+ onRemove(index)
312
+ }}
313
+ onKeyDown={(e) => {
314
+ if (e.key === 'Enter' || e.key === ' ') {
315
+ e.preventDefault()
316
+ e.stopPropagation()
317
+ onRemove(index)
318
+ }
319
+ }}
320
+ className="cursor-pointer"
321
+ >
322
+ <X className="size-3" />
323
+ </span>
324
+ </Button>
325
+ </div>
326
+ </AccordionTrigger>
327
+ <AccordionContent className="flex flex-col gap-4 px-1">
328
+ <Separator className="mt-2" />
329
+ <div className="space-y-5">
330
+ <div className="space-y-2">
331
+ <span className="text-sm font-medium">Title</span>
332
+ <Input
333
+ value={item.title || ''}
334
+ onChange={(e) => onUpdate(index, 'title', e.target.value)}
335
+ placeholder="Enter title"
336
+ disabled={disabled}
337
+ />
338
+ </div>
339
+ <div className="space-y-2">
340
+ <span className="text-sm font-medium">Description</span>
341
+ <Textarea
342
+ value={item.description || ''}
343
+ onChange={(e) => onUpdate(index, 'description', e.target.value)}
344
+ placeholder="Enter description"
345
+ disabled={disabled}
346
+ className="min-h-[80px] resize-y"
347
+ />
348
+ </div>
349
+ </div>
350
+ </AccordionContent>
351
+ </div>
352
+ </AccordionItem>
353
+ ))}
354
+ </Accordion>
355
+ </div>
356
+ )
357
+ }
358
+
359
+ // ============================================================================
360
+ // Weekly Editor Sub-component
361
+ // ============================================================================
362
+
363
+ interface WeeklyEditorProps {
364
+ weeks: CurriculumWeek[]
365
+ expandedWeek: string | undefined
366
+ setExpandedWeek: (value: string | undefined) => void
367
+ disabled: boolean
368
+ onAddWeek: () => void
369
+ onRemoveWeek: (weekIndex: number) => void
370
+ onUpdateWeek: (
371
+ weekIndex: number,
372
+ field: 'weekTitle' | 'weekDescription' | 'durationHours',
373
+ value: string | number
374
+ ) => void
375
+ onAddItem: (weekIndex: number) => void
376
+ onRemoveItem: (weekIndex: number, itemIndex: number) => void
377
+ onUpdateItem: (
378
+ weekIndex: number,
379
+ itemIndex: number,
380
+ field: keyof CurriculumWeeklyItem,
381
+ value: string
382
+ ) => void
383
+ }
384
+
385
+ const WeeklyEditor = ({
386
+ weeks,
387
+ expandedWeek,
388
+ setExpandedWeek,
389
+ disabled,
390
+ onAddWeek,
391
+ onRemoveWeek,
392
+ onUpdateWeek,
393
+ onAddItem,
394
+ onRemoveItem,
395
+ onUpdateItem
396
+ }: WeeklyEditorProps) => {
397
+ if (weeks.length === 0) {
398
+ return (
399
+ <Placeholder
400
+ label="No weeks added yet"
401
+ description="Add weeks to organize your curriculum by time periods."
402
+ action={
403
+ <Button type="button" variant="outline" size="lg" onClick={onAddWeek} disabled={disabled}>
404
+ <Plus className="size-4 mr-2" />
405
+ Add Week
406
+ </Button>
407
+ }
408
+ />
409
+ )
410
+ }
411
+
412
+ return (
413
+ <div className="space-y-5">
414
+ <div className="flex items-center justify-between">
415
+ <FormLabel className="text-base">Weeks</FormLabel>
416
+ <Button
417
+ type="button"
418
+ variant="outline"
419
+ size="sm"
420
+ onClick={onAddWeek}
421
+ disabled={disabled || weeks.length >= 20}
422
+ >
423
+ <Plus className="size-3" />
424
+ Add Week
425
+ </Button>
426
+ </div>
427
+
428
+ <Accordion
429
+ type="single"
430
+ collapsible
431
+ className="w-full gap-1 flex flex-col"
432
+ value={expandedWeek}
433
+ onValueChange={setExpandedWeek}
434
+ >
435
+ {weeks.map((week, weekIndex) => (
436
+ <AccordionItem
437
+ key={weekIndex}
438
+ value={`week-${weekIndex + 1}`}
439
+ className="p-0 border-none"
440
+ >
441
+ <div className="space-y-5 rounded-lg border p-4 bg-secondary/50 corner-squircle [&_h3]:m-0 w-full">
442
+ <AccordionTrigger className="flex items-center p-0 justify-between w-full">
443
+ <div className="flex w-full items-center justify-between">
444
+ <div className="flex flex-col items-start">
445
+ <h4 className="text-sm font-medium">
446
+ Week {week.weekNumber}
447
+ {week.weekTitle && `: ${week.weekTitle}`}
448
+ </h4>
449
+ {week.durationHours && (
450
+ <span className="text-xs text-muted-foreground flex items-center gap-1">
451
+ <Clock className="size-3" />
452
+ {week.durationHours} hours
453
+ </span>
454
+ )}
455
+ </div>
456
+ <Button asChild variant="ghost" size="sm" disabled={disabled}>
457
+ <span
458
+ role="button"
459
+ tabIndex={0}
460
+ onClick={(e) => {
461
+ e.stopPropagation()
462
+ onRemoveWeek(weekIndex)
463
+ }}
464
+ onKeyDown={(e) => {
465
+ if (e.key === 'Enter' || e.key === ' ') {
466
+ e.preventDefault()
467
+ e.stopPropagation()
468
+ onRemoveWeek(weekIndex)
469
+ }
470
+ }}
471
+ className="cursor-pointer"
472
+ >
473
+ <X className="size-3" />
474
+ </span>
475
+ </Button>
476
+ </div>
477
+ </AccordionTrigger>
478
+
479
+ <AccordionContent className="flex flex-col gap-4 px-1">
480
+ <Separator className="mt-2" />
481
+
482
+ {/* Week metadata */}
483
+ <div className="grid gap-4 sm:grid-cols-2">
484
+ <div className="space-y-2">
485
+ <span className="text-sm font-medium">Week Title (optional)</span>
486
+ <Input
487
+ value={week.weekTitle || ''}
488
+ onChange={(e) => onUpdateWeek(weekIndex, 'weekTitle', e.target.value)}
489
+ placeholder="e.g., Introduction"
490
+ disabled={disabled}
491
+ />
492
+ </div>
493
+ <div className="space-y-2">
494
+ <span className="text-sm font-medium">Duration (hours)</span>
495
+ <Input
496
+ type="number"
497
+ value={week.durationHours || ''}
498
+ onChange={(e) =>
499
+ onUpdateWeek(
500
+ weekIndex,
501
+ 'durationHours',
502
+ e.target.value ? Number(e.target.value) : 0
503
+ )
504
+ }
505
+ placeholder="e.g., 10"
506
+ disabled={disabled}
507
+ min={0}
508
+ />
509
+ </div>
510
+ </div>
511
+
512
+ <div className="space-y-2">
513
+ <span className="text-sm font-medium">Week Overview (optional)</span>
514
+ <Textarea
515
+ value={week.weekDescription || ''}
516
+ onChange={(e) => onUpdateWeek(weekIndex, 'weekDescription', e.target.value)}
517
+ placeholder="Brief overview of what students will learn this week..."
518
+ disabled={disabled}
519
+ className="min-h-[60px] resize-y"
520
+ />
521
+ </div>
522
+
523
+ <Separator />
524
+
525
+ {/* Week items */}
526
+ <div className="space-y-3">
527
+ <div className="flex items-center justify-between">
528
+ <span className="text-sm font-medium text-muted-foreground">
529
+ Topics ({week.items.length})
530
+ </span>
531
+ <Button
532
+ type="button"
533
+ variant="outline"
534
+ size="sm"
535
+ onClick={() => onAddItem(weekIndex)}
536
+ disabled={disabled || week.items.length >= 20}
537
+ >
538
+ <Plus className="size-3" />
539
+ Add Topic
540
+ </Button>
541
+ </div>
542
+
543
+ {week.items.length === 0 ? (
544
+ <div className="rounded-md corner-squircle border border-dashed p-4 text-center text-sm text-muted-foreground">
545
+ No topics added yet. Click &quot;Add Topic&quot; to add content.
546
+ </div>
547
+ ) : (
548
+ <div className="space-y-2">
549
+ {week.items.map((item, itemIndex) => (
550
+ <div
551
+ key={itemIndex}
552
+ className="group relative rounded-md corner-squircle border p-3 bg-background"
553
+ >
554
+ <div className="flex gap-3">
555
+ <span className="text-xs font-medium text-muted-foreground pt-2">
556
+ {itemIndex + 1}.
557
+ </span>
558
+ <div className="flex-1 space-y-3">
559
+ <Input
560
+ value={item.title || ''}
561
+ onChange={(e) =>
562
+ onUpdateItem(weekIndex, itemIndex, 'title', e.target.value)
563
+ }
564
+ placeholder="Topic title"
565
+ disabled={disabled}
566
+ className="h-8 text-sm"
567
+ />
568
+ <Textarea
569
+ value={item.description || ''}
570
+ onChange={(e) =>
571
+ onUpdateItem(weekIndex, itemIndex, 'description', e.target.value)
572
+ }
573
+ placeholder="Topic description (optional)"
574
+ disabled={disabled}
575
+ className="min-h-[50px] resize-y text-sm"
576
+ />
577
+ </div>
578
+ <Button
579
+ type="button"
580
+ variant="ghost"
581
+ size="icon"
582
+ onClick={() => onRemoveItem(weekIndex, itemIndex)}
583
+ disabled={disabled}
584
+ className="size-7 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
585
+ >
586
+ <X className="size-3" />
587
+ </Button>
588
+ </div>
589
+ </div>
590
+ ))}
591
+ </div>
592
+ )}
593
+ </div>
594
+ </AccordionContent>
595
+ </div>
596
+ </AccordionItem>
597
+ ))}
598
+ </Accordion>
599
+ </div>
600
+ )
601
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@cms/utils/cn'
4
+ import { format } from 'date-fns'
5
+ import { CalendarIcon } from 'lucide-react'
6
+ import * as React from 'react'
7
+ import { Button } from './button'
8
+ import { Calendar } from './calendar'
9
+ import { Popover, PopoverContent, PopoverTrigger } from './popover'
10
+
11
+ interface DatePickerProps {
12
+ value?: string
13
+ onChange?: (value: string) => void
14
+ disabled?: boolean
15
+ placeholder?: string
16
+ className?: string
17
+ /** Start year for the year dropdown (defaults to 1900) */
18
+ startYear?: number
19
+ /** End year for the year dropdown (defaults to current year + 100) */
20
+ endYear?: number
21
+ }
22
+
23
+ export function DatePicker({
24
+ value,
25
+ onChange,
26
+ disabled,
27
+ placeholder = 'Pick a date',
28
+ className,
29
+ startYear = 2022,
30
+ endYear = new Date().getFullYear() + 5
31
+ }: DatePickerProps) {
32
+ const [date, setDate] = React.useState<Date | undefined>(value ? new Date(value) : undefined)
33
+ const [open, setOpen] = React.useState(false)
34
+
35
+ return (
36
+ <Popover open={open} onOpenChange={setOpen}>
37
+ <PopoverTrigger asChild>
38
+ <Button
39
+ variant="outline"
40
+ size="lg"
41
+ className={cn(
42
+ 'w-full justify-start text-left font-normal pl-3!',
43
+ !date && 'text-muted-foreground',
44
+ className
45
+ )}
46
+ disabled={disabled}
47
+ >
48
+ <CalendarIcon className="size-4" />
49
+ {date ? format(date, 'PPP') : <span>{placeholder}</span>}
50
+ </Button>
51
+ </PopoverTrigger>
52
+ <PopoverContent className="w-auto p-0 overflow-hidden" align="start">
53
+ <Calendar
54
+ mode="single"
55
+ selected={date}
56
+ captionLayout="dropdown"
57
+ startMonth={new Date(startYear, 0)}
58
+ endMonth={new Date(endYear, 11)}
59
+ onSelect={(_date) => {
60
+ setDate(_date)
61
+ if (onChange) {
62
+ onChange(_date ? format(_date, 'yyyy-MM-dd') : '')
63
+ }
64
+ setOpen(false)
65
+ }}
66
+ />
67
+ </PopoverContent>
68
+ </Popover>
69
+ )
70
+ }