@dyrected/admin 2.4.0 → 2.4.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 (170) hide show
  1. package/dist/App.d.ts +1 -0
  2. package/dist/admin.css +2 -0
  3. package/dist/components/auth/auth-gate.d.ts +13 -0
  4. package/dist/components/error-boundary.d.ts +16 -0
  5. package/dist/components/forms/field-renderer.d.ts +22 -0
  6. package/dist/components/forms/fields/block-builder.d.ts +9 -0
  7. package/dist/components/forms/fields/date-picker.d.ts +8 -0
  8. package/dist/components/forms/fields/json-editor.d.ts +8 -0
  9. package/dist/components/forms/fields/media-picker.d.ts +12 -0
  10. package/dist/components/forms/fields/multi-select.d.ts +19 -0
  11. package/dist/components/forms/fields/radio-field.d.ts +8 -0
  12. package/dist/components/forms/fields/relationship-picker.d.ts +10 -0
  13. package/dist/components/forms/fields/rich-text-editor.d.ts +9 -0
  14. package/dist/components/forms/fields/select-field.d.ts +8 -0
  15. package/dist/components/forms/fields/switch-field.d.ts +6 -0
  16. package/dist/components/forms/fields/text-area-field.d.ts +8 -0
  17. package/dist/components/forms/fields/text-field.d.ts +8 -0
  18. package/dist/components/forms/form-engine.d.ts +14 -0
  19. package/dist/components/forms/form-field-renderer.d.ts +20 -0
  20. package/dist/components/forms/utils.d.ts +11 -0
  21. package/dist/components/layout/admin-shell.d.ts +5 -0
  22. package/dist/components/layout/branding-provider.d.ts +4 -0
  23. package/dist/components/live-preview/LivePreviewPane.d.ts +7 -0
  24. package/dist/components/media/focal-point-picker.d.ts +12 -0
  25. package/dist/components/media/media-card.d.ts +8 -0
  26. package/dist/components/media/media-grid.d.ts +8 -0
  27. package/dist/components/media/media-library-dialog.d.ts +11 -0
  28. package/dist/components/ui/aspect-ratio.d.ts +3 -0
  29. package/dist/components/ui/badge.d.ts +9 -0
  30. package/dist/components/ui/button.d.ts +11 -0
  31. package/dist/components/ui/calendar.d.ts +8 -0
  32. package/dist/components/ui/card.d.ts +8 -0
  33. package/dist/components/ui/checkbox.d.ts +4 -0
  34. package/dist/components/ui/command.d.ts +80 -0
  35. package/dist/components/ui/data-table.d.ts +14 -0
  36. package/dist/components/ui/dialog.d.ts +19 -0
  37. package/dist/components/ui/dropdown-menu.d.ts +27 -0
  38. package/dist/components/ui/form.d.ts +23 -0
  39. package/dist/components/ui/input.d.ts +3 -0
  40. package/dist/components/ui/label.d.ts +5 -0
  41. package/dist/components/ui/page-header.d.ts +10 -0
  42. package/dist/components/ui/pagination.d.ts +11 -0
  43. package/dist/components/ui/popover.d.ts +6 -0
  44. package/dist/components/ui/progress.d.ts +4 -0
  45. package/dist/components/ui/radio-group.d.ts +5 -0
  46. package/dist/components/ui/render-cell.d.ts +8 -0
  47. package/dist/components/ui/scroll-area.d.ts +5 -0
  48. package/dist/components/ui/select.d.ts +13 -0
  49. package/dist/components/ui/separator.d.ts +4 -0
  50. package/dist/components/ui/sheet.d.ts +25 -0
  51. package/dist/components/ui/sidebar.d.ts +65 -0
  52. package/dist/components/ui/skeleton.d.ts +2 -0
  53. package/dist/components/ui/sonner.d.ts +4 -0
  54. package/dist/components/ui/switch.d.ts +4 -0
  55. package/dist/components/ui/table.d.ts +10 -0
  56. package/dist/components/ui/tabs.d.ts +7 -0
  57. package/dist/components/ui/textarea.d.ts +3 -0
  58. package/dist/components/ui/toggle.d.ts +12 -0
  59. package/dist/components/ui/tooltip.d.ts +7 -0
  60. package/dist/hooks/use-mobile.d.ts +1 -0
  61. package/dist/hooks/use-preferences.d.ts +6 -0
  62. package/dist/index.d.ts +38 -0
  63. package/dist/index.mjs +69091 -0
  64. package/dist/lib/utils.d.ts +3 -0
  65. package/dist/main.d.ts +0 -0
  66. package/dist/pages/auth/first-user-page.d.ts +4 -0
  67. package/dist/pages/auth/login-page.d.ts +4 -0
  68. package/dist/pages/collections/edit-page.d.ts +1 -0
  69. package/dist/pages/collections/list-page.d.ts +5 -0
  70. package/dist/pages/dashboard/dashboard.d.ts +1 -0
  71. package/dist/pages/globals/editor-page.d.ts +1 -0
  72. package/dist/pages/media/media-page.d.ts +4 -0
  73. package/dist/pages/setup/setup-prompt.d.ts +6 -0
  74. package/dist/providers/dyrected-provider.d.ts +29 -0
  75. package/dist/providers/query-provider.d.ts +3 -0
  76. package/package.json +6 -3
  77. package/CHANGELOG.md +0 -153
  78. package/components.json +0 -17
  79. package/eslint.config.js +0 -22
  80. package/index.html +0 -13
  81. package/postcss.config.js +0 -6
  82. package/scripts/prefix-tailwind-precision.py +0 -98
  83. package/scripts/prefix-tailwind.py +0 -67
  84. package/src/App.css +0 -184
  85. package/src/App.tsx +0 -25
  86. package/src/assets/dyrected.svg +0 -155
  87. package/src/assets/hero.png +0 -0
  88. package/src/assets/react.svg +0 -1
  89. package/src/assets/vite.svg +0 -1
  90. package/src/components/auth/auth-gate.tsx +0 -64
  91. package/src/components/error-boundary.tsx +0 -45
  92. package/src/components/forms/field-renderer.tsx +0 -111
  93. package/src/components/forms/fields/block-builder.tsx +0 -213
  94. package/src/components/forms/fields/date-picker.tsx +0 -60
  95. package/src/components/forms/fields/json-editor.tsx +0 -62
  96. package/src/components/forms/fields/media-picker.tsx +0 -286
  97. package/src/components/forms/fields/multi-select.tsx +0 -145
  98. package/src/components/forms/fields/radio-field.tsx +0 -51
  99. package/src/components/forms/fields/relationship-picker.tsx +0 -143
  100. package/src/components/forms/fields/rich-text-editor.tsx +0 -224
  101. package/src/components/forms/fields/select-field.tsx +0 -35
  102. package/src/components/forms/fields/switch-field.tsx +0 -16
  103. package/src/components/forms/fields/text-area-field.tsx +0 -15
  104. package/src/components/forms/fields/text-field.tsx +0 -24
  105. package/src/components/forms/form-engine.tsx +0 -87
  106. package/src/components/forms/form-field-renderer.tsx +0 -269
  107. package/src/components/forms/utils.ts +0 -97
  108. package/src/components/layout/admin-shell.tsx +0 -479
  109. package/src/components/layout/branding-provider.tsx +0 -112
  110. package/src/components/live-preview/LivePreviewPane.tsx +0 -128
  111. package/src/components/media/focal-point-picker.tsx +0 -66
  112. package/src/components/media/media-card.tsx +0 -44
  113. package/src/components/media/media-grid.tsx +0 -32
  114. package/src/components/media/media-library-dialog.tsx +0 -465
  115. package/src/components/ui/aspect-ratio.tsx +0 -7
  116. package/src/components/ui/badge.tsx +0 -36
  117. package/src/components/ui/button.tsx +0 -56
  118. package/src/components/ui/calendar.tsx +0 -214
  119. package/src/components/ui/card.tsx +0 -79
  120. package/src/components/ui/checkbox.tsx +0 -28
  121. package/src/components/ui/command.tsx +0 -151
  122. package/src/components/ui/data-table.tsx +0 -219
  123. package/src/components/ui/dialog.tsx +0 -122
  124. package/src/components/ui/dropdown-menu.tsx +0 -200
  125. package/src/components/ui/form.tsx +0 -178
  126. package/src/components/ui/input.tsx +0 -24
  127. package/src/components/ui/label.tsx +0 -24
  128. package/src/components/ui/page-header.tsx +0 -30
  129. package/src/components/ui/pagination.tsx +0 -57
  130. package/src/components/ui/popover.tsx +0 -29
  131. package/src/components/ui/progress.tsx +0 -26
  132. package/src/components/ui/radio-group.tsx +0 -42
  133. package/src/components/ui/render-cell.tsx +0 -110
  134. package/src/components/ui/scroll-area.tsx +0 -46
  135. package/src/components/ui/select.tsx +0 -160
  136. package/src/components/ui/separator.tsx +0 -29
  137. package/src/components/ui/sheet.tsx +0 -140
  138. package/src/components/ui/sidebar.tsx +0 -771
  139. package/src/components/ui/skeleton.tsx +0 -15
  140. package/src/components/ui/sonner.tsx +0 -27
  141. package/src/components/ui/switch.tsx +0 -27
  142. package/src/components/ui/table.tsx +0 -117
  143. package/src/components/ui/tabs.tsx +0 -53
  144. package/src/components/ui/textarea.tsx +0 -22
  145. package/src/components/ui/toggle.tsx +0 -43
  146. package/src/components/ui/tooltip.tsx +0 -28
  147. package/src/hooks/use-mobile.tsx +0 -19
  148. package/src/hooks/use-preferences.ts +0 -56
  149. package/src/index.css +0 -111
  150. package/src/index.tsx +0 -198
  151. package/src/lib/utils.ts +0 -36
  152. package/src/main.tsx +0 -10
  153. package/src/pages/auth/first-user-page.tsx +0 -115
  154. package/src/pages/auth/login-page.tsx +0 -91
  155. package/src/pages/collections/edit-page.tsx +0 -280
  156. package/src/pages/collections/list-page.tsx +0 -343
  157. package/src/pages/dashboard/dashboard.tsx +0 -150
  158. package/src/pages/globals/editor-page.tsx +0 -122
  159. package/src/pages/media/media-page.tsx +0 -564
  160. package/src/pages/setup/setup-prompt.tsx +0 -181
  161. package/src/providers/dyrected-provider.tsx +0 -122
  162. package/src/providers/query-provider.tsx +0 -19
  163. package/src/types/jexl.d.ts +0 -11
  164. package/tailwind.config.ts +0 -103
  165. package/tsconfig.app.json +0 -28
  166. package/tsconfig.json +0 -12
  167. package/tsconfig.node.json +0 -25
  168. package/vite.config.ts +0 -39
  169. /package/{public → dist}/favicon.svg +0 -0
  170. /package/{public → dist}/icons.svg +0 -0
@@ -1,213 +0,0 @@
1
- import { useState } from "react"
2
- import { useFieldArray } from "react-hook-form"
3
- import { FormFieldRenderer } from "../form-field-renderer"
4
- import { buildDefaultValues } from "../utils"
5
- import type { FieldSchema, BlockSchema } from "../form-engine"
6
- import { Button } from "../../ui/button"
7
- import { X, GripVertical, ChevronDown, ChevronUp } from "lucide-react"
8
- import {
9
- DropdownMenu,
10
- DropdownMenuContent,
11
- DropdownMenuItem,
12
- DropdownMenuTrigger,
13
- } from "../../ui/dropdown-menu"
14
- import {
15
- DndContext,
16
- closestCenter,
17
- KeyboardSensor,
18
- PointerSensor,
19
- useSensor,
20
- useSensors,
21
- } from "@dnd-kit/core"
22
- import type { DragEndEvent } from "@dnd-kit/core"
23
- import {
24
- SortableContext,
25
- sortableKeyboardCoordinates,
26
- verticalListSortingStrategy,
27
- useSortable,
28
- } from "@dnd-kit/sortable"
29
- import { CSS } from "@dnd-kit/utilities"
30
-
31
- interface BlockBuilderProps {
32
- schema: FieldSchema
33
- basePath: string
34
- control: any
35
- collection: string
36
- }
37
-
38
- function SortableBlockItem({
39
- id,
40
- index,
41
- item,
42
- schema,
43
- basePath,
44
- control,
45
- collection,
46
- remove
47
- }: {
48
- id: string;
49
- index: number;
50
- item: any;
51
- schema: FieldSchema;
52
- basePath: string;
53
- control: any;
54
- collection: string;
55
- remove: (index: number) => void
56
- }) {
57
- const {
58
- attributes,
59
- listeners,
60
- setNodeRef,
61
- transform,
62
- transition,
63
- isDragging,
64
- } = useSortable({ id })
65
-
66
- const [isExpanded, setIsExpanded] = useState(true)
67
-
68
- const style = {
69
- transform: CSS.Transform.toString(transform),
70
- transition,
71
- zIndex: isDragging ? 10 : 1,
72
- opacity: isDragging ? 0.8 : 1,
73
- }
74
-
75
- const blockConfig = schema.blocks?.find(b => b.slug === item.blockType)
76
-
77
- if (!blockConfig) return null
78
-
79
- return (
80
- <div ref={setNodeRef} style={style} className="dy-relative dy-group dy-left-accent dy-mb-4 dy-py-4 dy-animate-in">
81
- {/* Header / Drag Handle */}
82
- <div className="dy-flex dy-items-center dy-justify-between dy-pb-3">
83
- <div className="dy-flex dy-items-center dy-justify-between dy-gap-2">
84
- <div {...attributes} {...listeners} className="dy-cursor-grab dy-opacity-20 dy-group-hover:dy-opacity-100 hover:dy-bg-muted dy-p-1 dy-rounded-md dy-transition-all">
85
- <GripVertical className="dy-w-3.5 dy-h-3.5 dy-text-muted-foreground" />
86
- </div>
87
- <span className="dy-font-bold dy-text-xs dy-text-foreground/70 dy-tracking-tight">
88
- {blockConfig.labels?.singular || blockConfig.slug}
89
- </span>
90
- <span className="dy-text-[10px] dy-text-muted-foreground/40 dy-ml-2 dy-uppercase dy-tracking-widest dy-font-semibold">
91
- Item {index + 1}
92
- </span>
93
- </div>
94
- <div className="dy-flex dy-items-center dy-gap-1 dy-opacity-0 dy-group-hover:dy-opacity-100 dy-transition-opacity">
95
- <Button type="button" variant="ghost" size="icon" className="dy-h-7 dy-w-7 dy-text-muted-foreground/40" onClick={() => setIsExpanded(!isExpanded)}>
96
- {isExpanded ? <ChevronUp className="dy-w-3.5 dy-h-3.5" /> : <ChevronDown className="dy-w-3.5 dy-h-3.5" />}
97
- </Button>
98
- <Button type="button" variant="ghost" size="icon" className="dy-h-7 dy-w-7 dy-text-muted-foreground/30 hover:dy-text-destructive hover:dy-bg-destructive/10" onClick={() => remove(index)}>
99
- <X className="dy-w-3.5 dy-h-3.5" />
100
- </Button>
101
- </div>
102
- </div>
103
-
104
- {/* Content */}
105
- {isExpanded && (
106
- <div className="dy-space-y-6">
107
- {blockConfig.fields.map(subField => (
108
- <FormFieldRenderer
109
- key={subField.name}
110
- schema={subField}
111
- basePath={`${basePath}.${index}`}
112
- control={control}
113
- collection={collection}
114
- />
115
- ))}
116
- </div>
117
- )}
118
- </div>
119
- )
120
- }
121
-
122
- export function BlockBuilder({ schema, basePath, control, collection }: BlockBuilderProps) {
123
- const { fields, append, remove, move } = useFieldArray({ control, name: basePath })
124
-
125
- const sensors = useSensors(
126
- useSensor(PointerSensor),
127
- useSensor(KeyboardSensor, {
128
- coordinateGetter: sortableKeyboardCoordinates,
129
- })
130
- )
131
-
132
- function handleDragEnd(event: DragEndEvent) {
133
- const { active, over } = event
134
-
135
- if (over && active.id !== over.id) {
136
- const oldIndex = fields.findIndex((f) => f.id === active.id)
137
- const newIndex = fields.findIndex((f) => f.id === over.id)
138
- move(oldIndex, newIndex)
139
- }
140
- }
141
-
142
- const handleAddBlock = (block: BlockSchema) => {
143
- const defaultVals = buildDefaultValues(block.fields, {})
144
- append({
145
- blockType: block.slug,
146
- ...defaultVals
147
- })
148
- }
149
-
150
- return (
151
- <div className="dy-space-y-4">
152
- <div className="dy-flex dy-justify-between dy-items-center dy-pb-2">
153
- <div>
154
- <h4 className="dy-font-bold dy-text-sm dy-text-foreground dy-tracking-tight">{schema.label}</h4>
155
- {schema.admin?.description && (
156
- <p className="dy-text-[11px] dy-text-muted-foreground/60 dy-italic">{schema.admin.description}</p>
157
- )}
158
- </div>
159
-
160
- {schema.blocks && schema.blocks.length > 0 && (
161
- <DropdownMenu>
162
- <DropdownMenuTrigger asChild>
163
- <Button type="button" variant="outline" size="sm" className="dy-h-7 dy-text-[11px] dy-rounded-md dy-border-primary/20 hover:dy-bg-primary/5 hover:dy-text-primary">
164
- Add Block
165
- <ChevronDown className="dy-w-3 dy-h-3 dy-ml-1.5" />
166
- </Button>
167
- </DropdownMenuTrigger>
168
- <DropdownMenuContent align="end" className="dy-rounded-lg dy-border-border/40 dy-shadow-xl">
169
- {schema.blocks.map((block) => (
170
- <DropdownMenuItem key={block.slug} onClick={() => handleAddBlock(block)} className="dy-text-[13px] dy-rounded-md focus:dy-bg-primary/5 focus:dy-text-primary dy-transition-colors">
171
- {block.labels?.singular || block.slug}
172
- </DropdownMenuItem>
173
- ))}
174
- </DropdownMenuContent>
175
- </DropdownMenu>
176
- )}
177
- </div>
178
-
179
- {fields.length === 0 ? (
180
- <div className="dy-text-center dy-p-8 dy-border dy-border-dashed dy-border-border/40 dy-rounded-md">
181
- <p className="dy-text-[11px] dy-text-muted-foreground/50">No blocks added yet.</p>
182
- </div>
183
- ) : (
184
- <DndContext
185
- sensors={sensors}
186
- collisionDetection={closestCenter}
187
- onDragEnd={handleDragEnd}
188
- >
189
- <SortableContext
190
- items={fields.map(f => f.id)}
191
- strategy={verticalListSortingStrategy}
192
- >
193
- <div className="dy-pt-2">
194
- {fields.map((item, index) => (
195
- <SortableBlockItem
196
- key={item.id}
197
- id={item.id}
198
- index={index}
199
- item={item}
200
- schema={schema}
201
- basePath={basePath}
202
- control={control}
203
- collection={collection}
204
- remove={remove}
205
- />
206
- ))}
207
- </div>
208
- </SortableContext>
209
- </DndContext>
210
- )}
211
- </div>
212
- )
213
- }
@@ -1,60 +0,0 @@
1
- import { format } from "date-fns"
2
- import { Calendar as CalendarIcon } from "lucide-react"
3
-
4
- import { cn } from "../../../lib/utils"
5
- import { Button } from "../../ui/button"
6
- import { Calendar } from "../../ui/calendar"
7
- import {
8
- Popover,
9
- PopoverContent,
10
- PopoverTrigger,
11
- } from "../../ui/popover"
12
-
13
- interface DatePickerProps {
14
- value?: string | Date
15
- onChange: (date?: string) => void
16
- label?: string
17
- disabled?: boolean
18
- }
19
-
20
- export function DatePicker({ value, onChange, label, disabled }: DatePickerProps) {
21
- const date = value ? new Date(value) : undefined
22
-
23
- return (
24
- <div className="dy-flex dy-flex-col dy-gap-2">
25
- {label && <label className="dy-text-sm dy-font-medium dy-leading-none dy-peer-disabled:dy-cursor-not-allowed dy-peer-disabled:dy-opacity-70">{label}</label>}
26
- <Popover open={disabled ? false : undefined}>
27
- <PopoverTrigger asChild>
28
- <Button
29
- variant={"outline"}
30
- disabled={disabled}
31
- className={cn(
32
- "dy-w-full dy-justify-start dy-text-left dy-font-normal dy-h-11 dy-px-4 dy-bg-white hover:dy-bg-muted/50 dy-border-border/60 dy-shadow-sm dy-transition-all hover:dy-shadow-md",
33
- !date && "dy-text-muted-foreground"
34
- )}
35
- >
36
- <CalendarIcon className="dy-mr-3 dy-h-4 dy-w-4 dy-text-primary" />
37
- <span className="dy-flex-1 dy-truncate">
38
- {date ? format(date, "PPP") : "Pick a date..."}
39
- </span>
40
- </Button>
41
- </PopoverTrigger>
42
- <PopoverContent className="dy-w-auto dy-p-0 dy-border-border/50 dy-shadow-2xl dy-rounded-xl" align="start">
43
- <Calendar
44
- mode="single"
45
- selected={date}
46
- onSelect={(newDate) => {
47
- if (newDate) {
48
- // Store date as ISO string
49
- onChange(newDate.toISOString())
50
- } else {
51
- onChange(undefined)
52
- }
53
- }}
54
- initialFocus
55
- />
56
- </PopoverContent>
57
- </Popover>
58
- </div>
59
- )
60
- }
@@ -1,62 +0,0 @@
1
- import * as React from "react"
2
- import { Textarea } from "../../ui/textarea"
3
- import { cn } from "../../../lib/utils"
4
-
5
- interface JsonEditorProps {
6
- value?: any
7
- onChange: (value: any) => void
8
- label?: string
9
- disabled?: boolean
10
- }
11
-
12
- export function JsonEditor({ value, onChange, label, disabled }: JsonEditorProps) {
13
- const [internalValue, setInternalValue] = React.useState("")
14
- const [error, setError] = React.useState<string | null>(null)
15
-
16
- // Initialize internal string value from the JSON prop
17
- React.useEffect(() => {
18
- try {
19
- if (value !== undefined) {
20
- setInternalValue(JSON.stringify(value, null, 2))
21
- }
22
- } catch (e) {
23
- // Ignore initial parse errors if the value is somehow malformed
24
- }
25
- }, [value])
26
-
27
- const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
28
- const val = e.target.value
29
- setInternalValue(val)
30
-
31
- if (!val.trim()) {
32
- setError(null)
33
- onChange(null)
34
- return
35
- }
36
-
37
- try {
38
- const parsed = JSON.parse(val)
39
- setError(null)
40
- onChange(parsed)
41
- } catch (err: any) {
42
- setError("Invalid JSON format")
43
- }
44
- }
45
-
46
- return (
47
- <div className="dy-flex dy-flex-col dy-gap-2">
48
- {label && <label className="dy-text-sm dy-font-medium dy-leading-none">{label}</label>}
49
- <Textarea
50
- value={internalValue}
51
- onChange={handleChange}
52
- disabled={disabled}
53
- className={cn(
54
- "dy-font-mono dy-text-xs dy-min-h-[150px]",
55
- error && "dy-border-destructive focus-visible:dy-ring-destructive"
56
- )}
57
- placeholder='{ "key": "value" }'
58
- />
59
- {error && <span className="dy-text-xs dy-text-destructive">{error}</span>}
60
- </div>
61
- )
62
- }
@@ -1,286 +0,0 @@
1
- import * as React from "react"
2
- import { useQuery } from "@tanstack/react-query"
3
- import { useDyrected } from "../../../providers/dyrected-provider"
4
- import { Button } from "../../ui/button"
5
- import {
6
- Image as ImageIcon,
7
- X,
8
- Plus,
9
- Trash2,
10
- } from "lucide-react"
11
- import { Input } from "../../ui/input"
12
- import { cn, getMediaUrl } from "../../../lib/utils"
13
- import { MediaLibraryDialog } from "../../media/media-library-dialog"
14
-
15
- interface MediaPickerProps {
16
- collection: string
17
- value?: string | string[]
18
- onChange: (value: string | string[]) => void
19
- label?: string
20
- variant?: "default" | "icon"
21
- disabled?: boolean
22
- multiple?: boolean
23
- placeholder?: string
24
- }
25
-
26
- export function MediaPicker({
27
- collection,
28
- value,
29
- onChange,
30
- label,
31
- variant = "default",
32
- disabled,
33
- multiple,
34
- placeholder
35
- }: MediaPickerProps) {
36
- const { client } = useDyrected()
37
- const [isOpen, setIsOpen] = React.useState(false)
38
-
39
- const selectedValues = React.useMemo(() => {
40
- if (!value) return []
41
- return Array.isArray(value) ? value : [value]
42
- }, [value])
43
-
44
- const toggleValue = (id: string) => {
45
- if (multiple) {
46
- const next = selectedValues.includes(id)
47
- ? selectedValues.filter(v => v !== id)
48
- : [...selectedValues, id]
49
- onChange(next)
50
- } else {
51
- onChange(id)
52
- }
53
- }
54
-
55
- const handleConfirm = (ids: string[]) => {
56
- if (multiple) {
57
- const next = [...new Set([...selectedValues, ...ids])]
58
- onChange(next)
59
- } else if (ids.length > 0) {
60
- onChange(ids[0])
61
- }
62
- setIsOpen(false)
63
- }
64
-
65
- // Fetch media for thumbnails in the field view
66
- const { data: media } = useQuery({
67
- queryKey: [collection, "previews", selectedValues],
68
- queryFn: () => {
69
- if (selectedValues.length === 0) return []
70
- return client!.listMedia({
71
- where: { id: { in: selectedValues } }
72
- }, collection).then((r: any) => r.docs)
73
- },
74
- enabled: !!client && selectedValues.length > 0,
75
- })
76
-
77
- const getPreviewUrl = (item: any) => {
78
- if (!item) return ""
79
- if (item.mimeType === 'video/youtube') {
80
- const match = item.url?.match(/(?:youtu\.be\/|youtube\.com\/(?:v\/|u\/\w\/|embed\/|watch\?v=))([^#\&\?]*)/)
81
- const videoId = match && match[1]
82
- return `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
83
- }
84
- return getMediaUrl(item, client?.getBaseUrl() || "");
85
- }
86
-
87
- const isIcon = variant === "icon"
88
- const getDisplayString = (val: any): string => {
89
- if (!val) return ""
90
- if (Array.isArray(val)) {
91
- if (val.length === 0) return ""
92
- return `${val.length} items selected`
93
- }
94
- if (typeof val === 'object') return val.filename || val.id || val.slug || "Object"
95
- return String(val)
96
- }
97
- const displayValue = getDisplayString(value)
98
-
99
- if (multiple && !isIcon) {
100
- return (
101
- <div className="dy-space-y-4">
102
- {label && (
103
- <label className="dy-text-sm dy-font-semibold dy-text-foreground/70 dy-tracking-tight dy-leading-none">
104
- {label}
105
- </label>
106
- )}
107
-
108
- <div className="dy-grid dy-grid-cols-2 md:dy-grid-cols-3 lg:dy-grid-cols-4 dy-gap-4">
109
- <button
110
- type="button"
111
- onClick={() => setIsOpen(true)}
112
- className="dy-group dy-relative dy-aspect-square dy-rounded-xl dy-border-2 dy-border-dashed dy-border-muted hover:dy-border-primary/40 hover:dy-bg-primary/5 dy-transition-all dy-flex dy-flex-col dy-items-center dy-justify-center dy-gap-3 dy-overflow-hidden"
113
- >
114
- <div className="dy-absolute dy-inset-0 dy-bg-primary/5 dy-opacity-0 dy-group-hover:dy-opacity-100 dy-transition-opacity" />
115
- <div className="dy-h-12 dy-w-12 dy-bg-muted dy-rounded-full dy-flex dy-items-center dy-justify-center dy-text-muted-foreground dy-group-hover:dy-bg-primary/10 dy-group-hover:dy-text-primary dy-transition-all dy-shadow-inner">
116
- <Plus className="dy-h-6 dy-w-6" />
117
- </div>
118
- <div className="dy-text-center dy-px-4">
119
- <p className="dy-text-[11px] dy-font-bold dy-uppercase dy-tracking-widest dy-text-muted-foreground dy-group-hover:dy-text-primary dy-transition-colors">Add Media</p>
120
- <p className="dy-text-[10px] dy-text-muted-foreground/40 dy-mt-1 dy-font-medium dy-group-hover:dy-text-primary/60">Select or upload</p>
121
- </div>
122
- </button>
123
-
124
- {selectedValues.map((val, index) => {
125
- const item = media?.find((m: any) => m.id === val)
126
- return (
127
- <div key={val} className="dy-relative dy-group dy-animate-in dy-zoom-in dy-duration-300">
128
- <div className={cn(
129
- "dy-relative dy-aspect-square dy-rounded-xl dy-overflow-hidden dy-border-2 dy-bg-muted/20 dy-transition-all dy-shadow-sm",
130
- index === 0 ? "dy-border-primary dy-ring-4 dy-ring-primary/10" : "dy-border-border/40 hover:dy-border-border/80"
131
- )}>
132
- {item ? (
133
- <img
134
- src={getPreviewUrl(item)}
135
- alt=""
136
- className="dy-w-full dy-h-full dy-object-cover dy-transition-transform dy-group-hover:dy-scale-110"
137
- />
138
- ) : (
139
- <div className="dy-w-full dy-h-full dy-animate-pulse dy-bg-muted/50 dy-flex dy-items-center dy-justify-center">
140
- <ImageIcon className="dy-h-6 dy-w-6 dy-text-muted-foreground/20" />
141
- </div>
142
- )}
143
-
144
- {index === 0 && (
145
- <div className="dy-absolute dy-top-0 dy-left-0 dy-w-full dy-text-center dy-z-10 dy-px-3 dy-py-1 dy-bg-primary dy-text-white dy-text-[9px] dy-font-black dy-uppercase dy-tracking-widest dy-shadow-primary/20">
146
- Main Image
147
- </div>
148
- )}
149
-
150
- <div className="dy-absolute dy-inset-0 dy-bg-black/40 dy-opacity-0 dy-group-hover:dy-opacity-100 dy-transition-all dy-flex dy-items-start dy-justify-end dy-backdrop-blur-[2px]">
151
- <Button
152
- type="button"
153
- variant="outline"
154
- size="icon"
155
- className="dy-h-8 dy-w-8 dy-rounded-lg dy-text-destructive dy-bg-destructive-foreground dy-shadow-2xl dy-scale-75 dy-group-hover:dy-scale-100 dy-transition-all"
156
- onClick={() => toggleValue(val)}
157
- >
158
- <Trash2 className="dy-w-5 dy-h-5" />
159
- </Button>
160
- </div>
161
-
162
- {item && (
163
- <div className="dy-absolute dy-inset-x-0 dy-bottom-0 dy-p-2 dy-bg-gradient-to-t dy-from-black/60 dy-to-transparent dy-opacity-0 dy-group-hover:dy-opacity-100 dy-transition-opacity">
164
- <p className="dy-text-[10px] dy-text-white dy-truncate dy-font-medium">{item.filename}</p>
165
- </div>
166
- )}
167
- </div>
168
- </div>
169
- )
170
- })}
171
- </div>
172
-
173
- <MediaLibraryDialog
174
- collection={collection}
175
- isOpen={isOpen}
176
- onOpenChange={setIsOpen}
177
- selectedValues={selectedValues}
178
- onSelect={toggleValue}
179
- multiple={multiple}
180
- onConfirm={handleConfirm}
181
- />
182
- </div>
183
- )
184
- }
185
-
186
- return (
187
- <div className={isIcon ? "" : "space-y-3"}>
188
- {label && !isIcon && (
189
- <label className="dy-text-sm dy-font-semibold dy-text-foreground/70 dy-tracking-tight dy-leading-none dy-peer-disabled:dy-cursor-not-allowed dy-peer-disabled:dy-opacity-70">
190
- {label}
191
- </label>
192
- )}
193
-
194
- <div className={isIcon ? "" : "relative flex items-center gap-2"}>
195
- {!isIcon && (
196
- <div className="dy-relative dy-flex-1 dy-group">
197
- <Input
198
- value={displayValue}
199
- readOnly
200
- disabled={disabled}
201
- placeholder={placeholder || "No media selected"}
202
- className="dy-pr-24 dy-bg-muted/30 dy-border-dashed focus-visible:dy-ring-offset-0 focus-visible:dy-ring-1 dy-h-10 dy-rounded-xl"
203
- />
204
- <div className="dy-absolute dy-right-1 dy-top-1/2 dy--translate-y-1/2 dy-flex dy-items-center dy-gap-1 dy-pr-1">
205
- {value && (
206
- <Button
207
- type="button"
208
- variant="ghost"
209
- size="icon"
210
- className="dy-h-7 dy-w-7 dy-text-muted-foreground hover:dy-text-destructive dy-transition-colors dy-rounded-lg"
211
- onClick={(e) => {
212
- e.preventDefault();
213
- onChange(multiple ? [] : "");
214
- }}
215
- >
216
- <X className="dy-h-4 dy-w-4" />
217
- </Button>
218
- )}
219
- <Button
220
- type="button"
221
- variant="secondary"
222
- size="sm"
223
- className="dy-h-8 dy-text-xs dy-font-bold dy-px-3 dy-rounded-lg dy-shadow-sm dy-border dy-border-border/50"
224
- disabled={disabled}
225
- onClick={() => setIsOpen(true)}
226
- >
227
- {value ? "Change" : "Select"}
228
- </Button>
229
- </div>
230
- </div>
231
- )}
232
-
233
- {isIcon && (
234
- <Button variant="ghost" size="sm" className="dy-px-2 dy-h-8 dy-w-8 dy-rounded-lg" disabled={disabled} onClick={() => setIsOpen(true)}>
235
- <ImageIcon className="dy-h-4 dy-w-4" />
236
- </Button>
237
- )}
238
-
239
- <MediaLibraryDialog
240
- collection={collection}
241
- isOpen={isOpen}
242
- onOpenChange={setIsOpen}
243
- selectedValues={selectedValues}
244
- onSelect={toggleValue}
245
- multiple={multiple}
246
- onConfirm={handleConfirm}
247
- />
248
- </div>
249
-
250
- {!isIcon && selectedValues.length > 0 && !multiple && (
251
- <div className="dy-grid dy-grid-cols-2 sm:dy-grid-cols-3 md:dy-grid-cols-4 lg:dy-grid-cols-5 dy-gap-4 dy-pt-2">
252
- {selectedValues.map((val) => {
253
- const item = media?.find((m: any) => m.id === val)
254
- if (!item) return (
255
- <div key={val} className="dy-aspect-square dy-rounded-xl dy-bg-muted/20 dy-animate-pulse dy-border-2 dy-border-dashed dy-border-border/50" />
256
- )
257
- return (
258
- <div
259
- key={val}
260
- className="dy-relative dy-aspect-square dy-group dy-rounded-2xl dy-overflow-hidden dy-border-2 dy-border-border/50 hover:dy-border-primary/50 dy-transition-all dy-bg-muted/20 dy-shadow-sm"
261
- >
262
- <img
263
- src={getPreviewUrl(item)}
264
- alt=""
265
- className="dy-w-full dy-h-full dy-object-cover dy-transition-transform dy-group-hover:dy-scale-110"
266
- />
267
- {!disabled && (
268
- <button
269
- type="button"
270
- onClick={() => toggleValue(val)}
271
- className="dy-absolute dy-top-2 dy-right-2 dy-p-1.5 dy-bg-destructive dy-text-destructive-foreground dy-rounded-full dy-opacity-0 dy-group-hover:dy-opacity-100 dy-transition-all hover:dy-scale-110 dy-shadow-lg dy-border-2 dy-border-white"
272
- >
273
- <X className="dy-h-3.5 dy-w-3.5" />
274
- </button>
275
- )}
276
- <div className="dy-absolute dy-inset-x-0 dy-bottom-0 dy-p-2 dy-bg-gradient-to-t dy-from-black/60 dy-to-transparent dy-opacity-0 dy-group-hover:dy-opacity-100 dy-transition-opacity">
277
- <p className="dy-text-[10px] dy-text-white dy-truncate dy-font-medium">{item.filename}</p>
278
- </div>
279
- </div>
280
- )
281
- })}
282
- </div>
283
- )}
284
- </div>
285
- )
286
- }