@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.
- package/README.md +133 -0
- package/dist/cli.d.ts +1 -9
- package/dist/cli.js +13484 -367
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +24 -266
- package/dist/index.js +4 -11378
- package/dist/index.js.map +1 -1
- package/package.json +29 -42
- package/templates/schema.json +959 -0
- package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
- package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
- package/templates/tiptap/hooks/use-element-rect.ts +166 -0
- package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
- package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
- package/templates/tiptap/hooks/use-scrolling.ts +64 -0
- package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
- package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
- package/templates/tiptap/hooks/use-unmount.ts +21 -0
- package/templates/tiptap/hooks/use-window-size.ts +87 -0
- package/templates/tiptap/lib/tiptap-utils.ts +587 -0
- package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
- package/templates/tiptap/styles/_variables.scss +296 -0
- package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
- package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
- package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
- package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
- package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
- package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
- package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
- package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
- package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
- package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
- package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
- package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
- package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
- package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
- package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
- package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
- package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
- package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
- package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
- package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
- package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
- package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
- package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
- package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
- package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
- package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
- package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
- package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
- package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
- package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
- package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
- package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
- package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
- package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
- package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
- package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
- package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
- package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
- package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
- package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
- package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
- package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
- package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
- package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
- package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
- package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
- package/templates/ui/accordion.tsx +52 -0
- package/templates/ui/alert-dialog.tsx +116 -0
- package/templates/ui/alert.tsx +48 -0
- package/templates/ui/aspect-ratio.tsx +7 -0
- package/templates/ui/avatar.tsx +46 -0
- package/templates/ui/badge.tsx +32 -0
- package/templates/ui/breadcrumb.tsx +98 -0
- package/templates/ui/button-group.tsx +77 -0
- package/templates/ui/button.tsx +48 -0
- package/templates/ui/calendar.tsx +176 -0
- package/templates/ui/card.tsx +54 -0
- package/templates/ui/carousel.tsx +234 -0
- package/templates/ui/chart.tsx +349 -0
- package/templates/ui/checkbox.tsx +27 -0
- package/templates/ui/collapsible.tsx +11 -0
- package/templates/ui/command.tsx +142 -0
- package/templates/ui/context-menu.tsx +188 -0
- package/templates/ui/curriculum-editor.tsx +601 -0
- package/templates/ui/date-picker.tsx +70 -0
- package/templates/ui/dialog.tsx +103 -0
- package/templates/ui/drawer.tsx +99 -0
- package/templates/ui/dropdown-menu.tsx +185 -0
- package/templates/ui/dynamic-list-field.tsx +95 -0
- package/templates/ui/empty.tsx +90 -0
- package/templates/ui/field.tsx +231 -0
- package/templates/ui/file-upload-example.tsx +113 -0
- package/templates/ui/form.tsx +172 -0
- package/templates/ui/hover-card.tsx +28 -0
- package/templates/ui/icon-picker.tsx +435 -0
- package/templates/ui/icons-data.ts +6 -0
- package/templates/ui/image-upload-field.tsx +360 -0
- package/templates/ui/input-group.tsx +160 -0
- package/templates/ui/input-otp.tsx +70 -0
- package/templates/ui/input.tsx +21 -0
- package/templates/ui/item.tsx +171 -0
- package/templates/ui/kbd.tsx +28 -0
- package/templates/ui/label.tsx +20 -0
- package/templates/ui/logo.tsx +113 -0
- package/templates/ui/markdown-editor.tsx +303 -0
- package/templates/ui/markdown-utils.ts +128 -0
- package/templates/ui/media-upload-field.tsx +255 -0
- package/templates/ui/menubar.tsx +230 -0
- package/templates/ui/navigation-menu.tsx +119 -0
- package/templates/ui/pagination.tsx +96 -0
- package/templates/ui/placeholder.tsx +25 -0
- package/templates/ui/popover.tsx +32 -0
- package/templates/ui/progress.tsx +24 -0
- package/templates/ui/radio-group.tsx +37 -0
- package/templates/ui/resizable.tsx +41 -0
- package/templates/ui/rich-text-editor.tsx +374 -0
- package/templates/ui/scroll-area.tsx +45 -0
- package/templates/ui/select.tsx +151 -0
- package/templates/ui/separator.tsx +25 -0
- package/templates/ui/sheet.tsx +120 -0
- package/templates/ui/sidebar.tsx +684 -0
- package/templates/ui/skeleton.tsx +7 -0
- package/templates/ui/slider.tsx +24 -0
- package/templates/ui/sonner.tsx +29 -0
- package/templates/ui/spinner.tsx +15 -0
- package/templates/ui/switch.tsx +28 -0
- package/templates/ui/table.tsx +93 -0
- package/templates/ui/tabs.tsx +54 -0
- package/templates/ui/textarea.tsx +20 -0
- package/templates/ui/toast.tsx +127 -0
- package/templates/ui/toggle-group.tsx +56 -0
- package/templates/ui/toggle.tsx +43 -0
- package/templates/ui/tooltip.tsx +31 -0
- package/templates/ui/use-mobile.tsx +19 -0
- package/templates/ui/video-upload-field.tsx +368 -0
- package/dist/chunk-EIH4RRIJ.js +0 -183
- package/dist/chunk-EIH4RRIJ.js.map +0 -1
- package/dist/chunk-NKRQYAS6.js +0 -260
- package/dist/chunk-NKRQYAS6.js.map +0 -1
- package/dist/chunk-QLVSHP7X.js +0 -235
- package/dist/chunk-QLVSHP7X.js.map +0 -1
- package/dist/chunk-WY6BC55D.js +0 -357
- package/dist/chunk-WY6BC55D.js.map +0 -1
- package/dist/config/index.d.ts +0 -93
- package/dist/config/index.js +0 -58
- package/dist/config/index.js.map +0 -1
- package/dist/core/index.d.ts +0 -415
- package/dist/core/index.js +0 -906
- package/dist/core/index.js.map +0 -1
- package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
- package/dist/logger-awLb347n.d.ts +0 -81
- package/dist/plugins/index.d.ts +0 -213
- package/dist/plugins/index.js +0 -365
- package/dist/plugins/index.js.map +0 -1
- package/dist/types-ByX_gl6y.d.ts +0 -232
- 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 "Add Topic" 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
|
+
}
|