@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.
- package/dist/App.d.ts +1 -0
- package/dist/admin.css +2 -0
- package/dist/components/auth/auth-gate.d.ts +13 -0
- package/dist/components/error-boundary.d.ts +16 -0
- package/dist/components/forms/field-renderer.d.ts +22 -0
- package/dist/components/forms/fields/block-builder.d.ts +9 -0
- package/dist/components/forms/fields/date-picker.d.ts +8 -0
- package/dist/components/forms/fields/json-editor.d.ts +8 -0
- package/dist/components/forms/fields/media-picker.d.ts +12 -0
- package/dist/components/forms/fields/multi-select.d.ts +19 -0
- package/dist/components/forms/fields/radio-field.d.ts +8 -0
- package/dist/components/forms/fields/relationship-picker.d.ts +10 -0
- package/dist/components/forms/fields/rich-text-editor.d.ts +9 -0
- package/dist/components/forms/fields/select-field.d.ts +8 -0
- package/dist/components/forms/fields/switch-field.d.ts +6 -0
- package/dist/components/forms/fields/text-area-field.d.ts +8 -0
- package/dist/components/forms/fields/text-field.d.ts +8 -0
- package/dist/components/forms/form-engine.d.ts +14 -0
- package/dist/components/forms/form-field-renderer.d.ts +20 -0
- package/dist/components/forms/utils.d.ts +11 -0
- package/dist/components/layout/admin-shell.d.ts +5 -0
- package/dist/components/layout/branding-provider.d.ts +4 -0
- package/dist/components/live-preview/LivePreviewPane.d.ts +7 -0
- package/dist/components/media/focal-point-picker.d.ts +12 -0
- package/dist/components/media/media-card.d.ts +8 -0
- package/dist/components/media/media-grid.d.ts +8 -0
- package/dist/components/media/media-library-dialog.d.ts +11 -0
- package/dist/components/ui/aspect-ratio.d.ts +3 -0
- package/dist/components/ui/badge.d.ts +9 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/calendar.d.ts +8 -0
- package/dist/components/ui/card.d.ts +8 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/command.d.ts +80 -0
- package/dist/components/ui/data-table.d.ts +14 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/dropdown-menu.d.ts +27 -0
- package/dist/components/ui/form.d.ts +23 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/page-header.d.ts +10 -0
- package/dist/components/ui/pagination.d.ts +11 -0
- package/dist/components/ui/popover.d.ts +6 -0
- package/dist/components/ui/progress.d.ts +4 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/render-cell.d.ts +8 -0
- package/dist/components/ui/scroll-area.d.ts +5 -0
- package/dist/components/ui/select.d.ts +13 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/sheet.d.ts +25 -0
- package/dist/components/ui/sidebar.d.ts +65 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/sonner.d.ts +4 -0
- package/dist/components/ui/switch.d.ts +4 -0
- package/dist/components/ui/table.d.ts +10 -0
- package/dist/components/ui/tabs.d.ts +7 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/toggle.d.ts +12 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-preferences.d.ts +6 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.mjs +69091 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/main.d.ts +0 -0
- package/dist/pages/auth/first-user-page.d.ts +4 -0
- package/dist/pages/auth/login-page.d.ts +4 -0
- package/dist/pages/collections/edit-page.d.ts +1 -0
- package/dist/pages/collections/list-page.d.ts +5 -0
- package/dist/pages/dashboard/dashboard.d.ts +1 -0
- package/dist/pages/globals/editor-page.d.ts +1 -0
- package/dist/pages/media/media-page.d.ts +4 -0
- package/dist/pages/setup/setup-prompt.d.ts +6 -0
- package/dist/providers/dyrected-provider.d.ts +29 -0
- package/dist/providers/query-provider.d.ts +3 -0
- package/package.json +6 -3
- package/CHANGELOG.md +0 -153
- package/components.json +0 -17
- package/eslint.config.js +0 -22
- package/index.html +0 -13
- package/postcss.config.js +0 -6
- package/scripts/prefix-tailwind-precision.py +0 -98
- package/scripts/prefix-tailwind.py +0 -67
- package/src/App.css +0 -184
- package/src/App.tsx +0 -25
- package/src/assets/dyrected.svg +0 -155
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/assets/vite.svg +0 -1
- package/src/components/auth/auth-gate.tsx +0 -64
- package/src/components/error-boundary.tsx +0 -45
- package/src/components/forms/field-renderer.tsx +0 -111
- package/src/components/forms/fields/block-builder.tsx +0 -213
- package/src/components/forms/fields/date-picker.tsx +0 -60
- package/src/components/forms/fields/json-editor.tsx +0 -62
- package/src/components/forms/fields/media-picker.tsx +0 -286
- package/src/components/forms/fields/multi-select.tsx +0 -145
- package/src/components/forms/fields/radio-field.tsx +0 -51
- package/src/components/forms/fields/relationship-picker.tsx +0 -143
- package/src/components/forms/fields/rich-text-editor.tsx +0 -224
- package/src/components/forms/fields/select-field.tsx +0 -35
- package/src/components/forms/fields/switch-field.tsx +0 -16
- package/src/components/forms/fields/text-area-field.tsx +0 -15
- package/src/components/forms/fields/text-field.tsx +0 -24
- package/src/components/forms/form-engine.tsx +0 -87
- package/src/components/forms/form-field-renderer.tsx +0 -269
- package/src/components/forms/utils.ts +0 -97
- package/src/components/layout/admin-shell.tsx +0 -479
- package/src/components/layout/branding-provider.tsx +0 -112
- package/src/components/live-preview/LivePreviewPane.tsx +0 -128
- package/src/components/media/focal-point-picker.tsx +0 -66
- package/src/components/media/media-card.tsx +0 -44
- package/src/components/media/media-grid.tsx +0 -32
- package/src/components/media/media-library-dialog.tsx +0 -465
- package/src/components/ui/aspect-ratio.tsx +0 -7
- package/src/components/ui/badge.tsx +0 -36
- package/src/components/ui/button.tsx +0 -56
- package/src/components/ui/calendar.tsx +0 -214
- package/src/components/ui/card.tsx +0 -79
- package/src/components/ui/checkbox.tsx +0 -28
- package/src/components/ui/command.tsx +0 -151
- package/src/components/ui/data-table.tsx +0 -219
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -200
- package/src/components/ui/form.tsx +0 -178
- package/src/components/ui/input.tsx +0 -24
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/page-header.tsx +0 -30
- package/src/components/ui/pagination.tsx +0 -57
- package/src/components/ui/popover.tsx +0 -29
- package/src/components/ui/progress.tsx +0 -26
- package/src/components/ui/radio-group.tsx +0 -42
- package/src/components/ui/render-cell.tsx +0 -110
- package/src/components/ui/scroll-area.tsx +0 -46
- package/src/components/ui/select.tsx +0 -160
- package/src/components/ui/separator.tsx +0 -29
- package/src/components/ui/sheet.tsx +0 -140
- package/src/components/ui/sidebar.tsx +0 -771
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/sonner.tsx +0 -27
- package/src/components/ui/switch.tsx +0 -27
- package/src/components/ui/table.tsx +0 -117
- package/src/components/ui/tabs.tsx +0 -53
- package/src/components/ui/textarea.tsx +0 -22
- package/src/components/ui/toggle.tsx +0 -43
- package/src/components/ui/tooltip.tsx +0 -28
- package/src/hooks/use-mobile.tsx +0 -19
- package/src/hooks/use-preferences.ts +0 -56
- package/src/index.css +0 -111
- package/src/index.tsx +0 -198
- package/src/lib/utils.ts +0 -36
- package/src/main.tsx +0 -10
- package/src/pages/auth/first-user-page.tsx +0 -115
- package/src/pages/auth/login-page.tsx +0 -91
- package/src/pages/collections/edit-page.tsx +0 -280
- package/src/pages/collections/list-page.tsx +0 -343
- package/src/pages/dashboard/dashboard.tsx +0 -150
- package/src/pages/globals/editor-page.tsx +0 -122
- package/src/pages/media/media-page.tsx +0 -564
- package/src/pages/setup/setup-prompt.tsx +0 -181
- package/src/providers/dyrected-provider.tsx +0 -122
- package/src/providers/query-provider.tsx +0 -19
- package/src/types/jexl.d.ts +0 -11
- package/tailwind.config.ts +0 -103
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -12
- package/tsconfig.node.json +0 -25
- package/vite.config.ts +0 -39
- /package/{public → dist}/favicon.svg +0 -0
- /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
|
-
}
|