@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,564 +0,0 @@
1
- import * as React from "react"
2
- import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
3
- import { toast } from "sonner"
4
- import { useDyrected } from "../../providers/dyrected-provider"
5
- import { Button } from "../../components/ui/button"
6
- import { Input } from "../../components/ui/input"
7
- import { cn, getMediaUrl } from "../../lib/utils"
8
- import {
9
- Card,
10
- CardContent,
11
- CardHeader,
12
- } from "../../components/ui/card"
13
- import {
14
- Dialog,
15
- DialogContent,
16
- DialogHeader,
17
- DialogTitle,
18
- DialogTrigger,
19
- } from "../../components/ui/dialog"
20
- import { ScrollArea } from "../../components/ui/scroll-area"
21
- import { AspectRatio } from "../../components/ui/aspect-ratio"
22
- import {
23
- Upload,
24
- Search,
25
- FileIcon,
26
- Trash2,
27
- ExternalLink,
28
- Image as ImageIcon,
29
- Copy,
30
- Info
31
- } from "lucide-react"
32
- import { useDropzone } from "react-dropzone"
33
- import { Progress } from "../../components/ui/progress"
34
- import {
35
- Sheet,
36
- SheetContent,
37
- SheetHeader,
38
- SheetTitle,
39
- } from "../../components/ui/sheet"
40
- import { Separator } from "../../components/ui/separator"
41
- // import { FocalPointPicker } from "../../components/media/focal-point-picker"
42
- import { Blurhash } from "react-blurhash"
43
-
44
- export function MediaPage({ collectionSlug, schema }: { collectionSlug?: string, schema?: any }) {
45
- const { client } = useDyrected()
46
- const queryClient = useQueryClient()
47
- const [search, setSearch] = React.useState("")
48
- const [isUploadOpen, setIsUploadOpen] = React.useState(false)
49
- const [selectedItem, setSelectedItem] = React.useState<any>(null)
50
-
51
- const { data: mediaResponse, isLoading } = useQuery({
52
- queryKey: ["media", collectionSlug, search],
53
- queryFn: () => client!.listMedia({ where: search ? { filename: { contains: search } } : undefined }, collectionSlug).then(r => r.docs),
54
- enabled: !!client,
55
- })
56
-
57
- const deleteMutation = useMutation({
58
- mutationFn: (id: string) => client!.deleteMedia(id, collectionSlug),
59
- onSuccess: () => {
60
- queryClient.invalidateQueries({ queryKey: ["media"] })
61
- toast.success("Asset deleted successfully")
62
- },
63
- onError: (error: any) => {
64
- toast.error("Failed to delete asset", {
65
- description: error.message
66
- })
67
- }
68
- })
69
-
70
- const updateMutation = useMutation({
71
- mutationFn: (args: { id: string, data: any }) => client!.update(collectionSlug || "media", args.id, args.data),
72
- onSuccess: (data) => {
73
- queryClient.invalidateQueries({ queryKey: ["media"] })
74
- setSelectedItem(data)
75
- toast.success("Asset details updated")
76
- },
77
- onError: (error: any) => {
78
- toast.error("Failed to update asset", {
79
- description: error.message
80
- })
81
- }
82
- })
83
-
84
- const onDrop = React.useCallback((acceptedFiles: File[]) => {
85
- if (acceptedFiles.length > 0) {
86
- setIsUploadOpen(true)
87
- }
88
- }, [])
89
-
90
- const { getRootProps, getInputProps, isDragActive } = useDropzone({
91
- onDrop,
92
- noClick: true, // Only trigger on drop, not on background click
93
- })
94
-
95
- return (
96
- <div {...getRootProps()} className="dy-min-h-full dy-space-y-8 dy-animate-in dy-relative">
97
- <input {...getInputProps()} />
98
-
99
- {isDragActive && (
100
- <div className="dy-absolute dy-inset-0 dy-z-50 dy-bg-primary/10 dy-backdrop-blur-[2px] dy-border-4 dy-border-dashed dy-border-primary dy-rounded-2xl dy-flex dy-items-center dy-justify-center dy-pointer-events-none">
101
- <div className="dy-bg-white dy-p-8 dy-rounded-2xl dy-shadow-2xl dy-flex dy-flex-col dy-items-center dy-gap-4">
102
- <div className="dy-h-16 dy-w-16 dy-rounded-full dy-bg-primary/10 dy-flex dy-items-center dy-justify-center">
103
- <Upload className="dy-h-8 dy-w-8 dy-text-primary dy-animate-bounce" />
104
- </div>
105
- <p className="dy-text-xl dy-font-bold">Drop to upload assets</p>
106
- </div>
107
- </div>
108
- )}
109
- <div className="dy-flex dy-items-end dy-justify-between dy-border-b dy-border-border/50 dy-pb-6">
110
- <div>
111
- <div className="dy-flex dy-items-center dy-gap-2 dy-mb-1">
112
- <ImageIcon className="dy-h-5 dy-w-5 dy-text-primary" />
113
- <h1 className="dy-text-3xl dy-font-bold dy-tracking-tight dy-text-foreground">
114
- {schema?.labels?.plural ?? schema?.label ?? "Media Library"}
115
- </h1>
116
- </div>
117
- <p className="dy-text-sm dy-text-muted-foreground">
118
- Manage your images, documents, and other assets for this site.
119
- </p>
120
- </div>
121
- <Dialog open={isUploadOpen} onOpenChange={setIsUploadOpen}>
122
- <DialogTrigger asChild>
123
- <Button className="dy-h-10 dy-px-4 dy-rounded-lg dy-bg-primary hover:dy-bg-primary/90 dy-shadow-md dy-transition-all active:dy-scale-95">
124
- <Upload className="dy-mr-2 dy-h-4 dy-w-4" />
125
- Upload Assets
126
- </Button>
127
- </DialogTrigger>
128
- <DialogContent className="sm:dy-max-w-[600px] dy-rounded-2xl dy-overflow-hidden dy-border-none dy-shadow-2xl">
129
- <DialogHeader className="dy-pb-4 dy-border-b dy-border-border/40">
130
- <DialogTitle className="dy-text-xl dy-font-bold">Upload Media Assets</DialogTitle>
131
- </DialogHeader>
132
- <FileUploader
133
- collectionSlug={collectionSlug}
134
- onComplete={() => {
135
- setIsUploadOpen(false)
136
- queryClient.invalidateQueries({ queryKey: ["media", collectionSlug] })
137
- }}
138
- />
139
- </DialogContent>
140
- </Dialog>
141
- </div>
142
-
143
- <div className="dy-flex dy-items-center dy-gap-4">
144
- <div className="dy-relative dy-flex-1 dy-max-w-sm">
145
- <Search className="dy-absolute dy-left-3 dy-top-1/2 dy--translate-y-1/2 dy-h-4 dy-w-4 dy-text-muted-foreground/60" />
146
- <Input
147
- placeholder="Search assets by filename..."
148
- className="dy-pl-10 dy-h-11 dy-bg-white dy-border-border/60 dy-rounded-xl dy-shadow-sm focus-visible:dy-ring-primary/20"
149
- value={search}
150
- onChange={(e) => setSearch(e.target.value)}
151
- />
152
- </div>
153
- </div>
154
-
155
- <ScrollArea className="dy-h-[calc(100vh-320px)] dy-pr-4">
156
- {isLoading ? (
157
- <div className="dy-flex dy-h-60 dy-items-center dy-justify-center">
158
- <div className="dy-animate-spin dy-rounded-full dy-border-4 dy-border-primary/20 dy-border-t-primary dy-h-10 dy-w-10"></div>
159
- </div>
160
- ) : mediaResponse?.length === 0 ? (
161
- <div className="dy-flex dy-h-80 dy-flex-col dy-items-center dy-justify-center dy-rounded-2xl dy-border-2 dy-border-dashed dy-border-border/60 dy-bg-muted/5 dy-text-center dy-animate-in">
162
- <div className="dy-h-16 dy-w-16 dy-rounded-2xl dy-bg-muted/40 dy-flex dy-items-center dy-justify-center dy-mb-4">
163
- <FileIcon className="dy-h-8 dy-w-8 dy-text-muted-foreground/50" />
164
- </div>
165
- <h3 className="dy-text-lg dy-font-bold dy-text-foreground">No assets found</h3>
166
- <p className="dy-text-sm dy-text-muted-foreground dy-max-w-xs dy-mx-auto">
167
- Your media library is empty. Upload some files to start building your content.
168
- </p>
169
- </div>
170
- ) : (
171
- <div className="dy-grid dy-grid-cols-2 md:dy-grid-cols-3 lg:dy-grid-cols-4 xl:dy-grid-cols-5 2xl:dy-grid-cols-6 dy-gap-6 dy-pb-8">
172
- {mediaResponse?.map((item) => (
173
- <MediaCard
174
- key={item.id}
175
- item={item}
176
- baseUrl={client!.getBaseUrl()}
177
- onDelete={() => deleteMutation.mutate(item.id)}
178
- onClick={() => setSelectedItem(item)}
179
- isSelected={selectedItem?.id === item.id}
180
- />
181
- ))}
182
- </div>
183
- )}
184
- </ScrollArea>
185
-
186
- <MediaSidebar
187
- item={selectedItem}
188
- onClose={() => setSelectedItem(null)}
189
- baseUrl={client!.getBaseUrl()}
190
- onUpdate={(data) => updateMutation.mutate({ id: selectedItem.id, data })}
191
- />
192
- </div>
193
- )
194
- }
195
-
196
- function MediaCard({ item, baseUrl, onDelete, onClick, isSelected }: {
197
- item: any,
198
- baseUrl: string,
199
- onDelete: () => void,
200
- onClick: () => void,
201
- isSelected: boolean
202
- }) {
203
- const isImage = item.mimeType?.startsWith("image/")
204
- const url = getMediaUrl(item, baseUrl)
205
-
206
- return (
207
- <Card
208
- className={cn(
209
- "dy-overflow-hidden dy-group dy-relative dy-border-border/40 dy-bg-white dy-shadow-sm hover:dy-shadow-xl dy-transition-all dy-duration-300 dy-rounded-xl dy-cursor-pointer",
210
- isSelected && "dy-ring-2 dy-ring-primary dy-ring-offset-2 dy-shadow-lg dy-scale-[0.98]"
211
- )}
212
- onClick={onClick}
213
- >
214
- <CardHeader className="dy-p-0 dy-border-b dy-border-border/10">
215
- <AspectRatio ratio={1 / 1} className="dy-bg-muted/30 dy-overflow-hidden dy-relative">
216
- {isImage ? (
217
- <>
218
- {item.blurhash && (
219
- <div className="dy-absolute dy-inset-0 dy-z-0">
220
- <Blurhash
221
- hash={item.blurhash}
222
- width="100%"
223
- height="100%"
224
- resolutionX={32}
225
- resolutionY={32}
226
- punch={1}
227
- />
228
- </div>
229
- )}
230
- <img
231
- src={url}
232
- alt={item.filename}
233
- className="dy-object-cover dy-w-full dy-h-full dy-transition-transform dy-duration-500 dy-group-hover:dy-scale-110 dy-relative dy-z-10"
234
- loading="lazy"
235
- />
236
- </>
237
- ) : (
238
- <div className="dy-flex dy-items-center dy-justify-center dy-h-full dy-bg-primary/5">
239
- <FileIcon className="dy-h-10 dy-w-10 dy-text-primary/40" />
240
- </div>
241
- )}
242
- </AspectRatio>
243
- </CardHeader>
244
- <CardContent className="dy-p-3 dy-bg-white">
245
- <p className="dy-text-[11px] dy-font-bold dy-truncate dy-text-foreground/90 dy-mb-0.5" title={item.filename}>
246
- {item.filename}
247
- </p>
248
- <div className="dy-flex dy-items-center dy-justify-between">
249
- <p className="dy-text-[9px] dy-text-muted-foreground dy-font-bold dy-uppercase dy-tracking-wider">
250
- {item.mimeType?.split("/")[1] || "file"}
251
- </p>
252
- <p className="dy-text-[9px] dy-text-muted-foreground dy-font-medium">
253
- {((item.filesize || item.size || 0) / 1024).toFixed(1)} KB
254
- </p>
255
- </div>
256
- </CardContent>
257
- <div className="dy-absolute dy-top-2 dy-right-2 dy-flex dy-gap-2 dy-opacity-0 dy-group-hover:dy-opacity-100 dy-transition-opacity">
258
- <Button
259
- size="icon"
260
- variant="destructive"
261
- className="dy-h-7 dy-w-7 dy-rounded-lg dy-shadow-lg"
262
- onClick={(e) => {
263
- e.stopPropagation()
264
- if (confirm("Are you sure you want to delete this file?")) {
265
- onDelete()
266
- }
267
- }}
268
- >
269
- <Trash2 className="dy-h-3.5 dy-w-3.5" />
270
- </Button>
271
- </div>
272
- </Card>
273
- )
274
- }
275
-
276
- function MediaSidebar({ item, onClose, baseUrl, onUpdate }: {
277
- item: any,
278
- onClose: () => void,
279
- baseUrl: string,
280
- onUpdate: (data: any) => void
281
- }) {
282
- const [formData, setFormData] = React.useState<any>({})
283
- const [isSaving, setIsSaving] = React.useState(false)
284
-
285
- React.useEffect(() => {
286
- if (item) {
287
- setFormData({
288
- alt: item.alt || "",
289
- caption: item.caption || "",
290
- filename: item.filename || "",
291
- })
292
- }
293
- }, [item])
294
-
295
- if (!item) return null
296
-
297
- const isImage = item.mimeType?.startsWith("image/")
298
- const url = getMediaUrl(item, baseUrl)
299
-
300
- const handleSave = async () => {
301
- setIsSaving(true)
302
- try {
303
- await onUpdate(formData)
304
- } finally {
305
- setIsSaving(false)
306
- }
307
- }
308
-
309
- const hasChanges =
310
- formData.alt !== (item.alt || "") ||
311
- formData.caption !== (item.caption || "") ||
312
- formData.filename !== (item.filename || "")
313
-
314
- return (
315
- <Sheet open={!!item} onOpenChange={onClose}>
316
- <SheetContent className="sm:dy-max-w-md dy-p-0 dy-flex dy-flex-col dy-h-full dy-border-l dy-border-border/40 dy-bg-white dy-shadow-2xl">
317
- <SheetHeader className="dy-p-6 dy-border-b dy-border-border/40 dy-bg-white">
318
- <SheetTitle className="dy-flex dy-items-center dy-gap-2">
319
- <Info className="dy-h-5 dy-w-5 dy-text-primary" />
320
- File Details
321
- </SheetTitle>
322
- </SheetHeader>
323
-
324
- <ScrollArea className="dy-flex-1 dy-bg-white">
325
- <div className="dy-p-6 dy-space-y-8">
326
- <div className="dy-rounded-xl dy-overflow-hidden dy-border dy-border-border/40 dy-bg-muted/10 dy-relative dy-shadow-inner">
327
- <AspectRatio ratio={16 / 9}>
328
- {isImage ? (
329
- <>
330
- {item.blurhash && (
331
- <div className="dy-absolute dy-inset-0 dy-z-0">
332
- <Blurhash
333
- hash={item.blurhash}
334
- width="100%"
335
- height="100%"
336
- resolutionX={32}
337
- resolutionY={32}
338
- punch={1}
339
- />
340
- </div>
341
- )}
342
- <img src={url} alt={item.filename} className="dy-object-contain dy-w-full dy-h-full dy-bg-checkered dy-relative dy-z-10" />
343
- </>
344
- ) : (
345
- <div className="dy-flex dy-items-center dy-justify-center dy-h-full">
346
- <FileIcon className="dy-h-16 dy-w-16 dy-text-muted-foreground/30" />
347
- </div>
348
- )}
349
- </AspectRatio>
350
- </div>
351
-
352
- <div className="dy-space-y-6">
353
- <div className="dy-space-y-4">
354
- <div className="dy-space-y-2">
355
- <label className="dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-widest dy-text-muted-foreground/80">Filename</label>
356
- <Input
357
- value={formData.filename}
358
- onChange={(e) => setFormData({ ...formData, filename: e.target.value })}
359
- className="dy-h-10 dy-rounded-lg dy-bg-white dy-border-border/60 focus:dy-ring-1 focus:dy-ring-primary/20"
360
- />
361
- </div>
362
- <div className="dy-space-y-2">
363
- <label className="dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-widest dy-text-muted-foreground/80">Alt Text</label>
364
- <Input
365
- value={formData.alt}
366
- onChange={(e) => setFormData({ ...formData, alt: e.target.value })}
367
- placeholder="Describe the image for accessibility..."
368
- className="dy-h-10 dy-rounded-lg dy-bg-white dy-border-border/60 focus:dy-ring-1 focus:dy-ring-primary/20"
369
- />
370
- </div>
371
- <div className="dy-space-y-2">
372
- <label className="dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-widest dy-text-muted-foreground/80">Caption</label>
373
- <textarea
374
- value={formData.caption}
375
- onChange={(e) => setFormData({ ...formData, caption: e.target.value })}
376
- placeholder="Add a caption..."
377
- className="dy-flex dy-min-h-[80px] dy-w-full dy-rounded-lg dy-border dy-border-border/60 dy-bg-white dy-px-3 dy-py-2 dy-text-sm dy-ring-offset-background placeholder:dy-text-muted-foreground focus-visible:dy-outline-none focus-visible:dy-ring-1 focus-visible:dy-ring-primary/20 disabled:dy-cursor-not-allowed disabled:dy-opacity-50"
378
- />
379
- </div>
380
- </div>
381
-
382
- <Separator className="dy-bg-border/40" />
383
-
384
- <div className="dy-grid dy-grid-cols-2 dy-gap-6">
385
- <DetailItem label="File ID" value={item.id} copyable />
386
- <DetailItem label="Size" value={`${((item.filesize || item.size || 0) / 1024).toFixed(1)} KB`} />
387
- <DetailItem label="Type" value={item.mimeType || "Unknown"} />
388
- <DetailItem label="Dimensions" value={item.width ? `${item.width}x${item.height}` : "N/A"} />
389
- </div>
390
-
391
- <DetailItem label="URL" value={url} copyable />
392
- <DetailItem label="Created At" value={item?.createdAt ? new Date(item?.createdAt).toLocaleString() : "N/A"} />
393
- </div>
394
-
395
- {/* {isImage && (
396
- <div className="dy-space-y-4">
397
- <Separator className="dy-bg-border/40" />
398
- <div className="dy-space-y-4">
399
- <label className="dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-widest dy-text-muted-foreground/80">Focal Point</label>
400
- <FocalPointPicker
401
- url={url}
402
- value={item.focalPoint}
403
- onChange={(fp) => {
404
- onUpdate({ focalPoint: fp })
405
- }}
406
- />
407
- </div>
408
- </div>
409
- )} */}
410
- </div>
411
- </ScrollArea>
412
-
413
- <div className="dy-p-6 dy-border-t dy-border-border/40 dy-bg-muted/5 dy-space-y-3">
414
- {hasChanges && (
415
- <Button
416
- className="dy-w-full dy-h-12 dy-rounded-xl dy-font-bold dy-bg-primary dy-text-white dy-shadow-lg dy-shadow-primary/20 dy-animate-in dy-fade-in dy-slide-in-from-bottom-2"
417
- onClick={handleSave}
418
- disabled={isSaving}
419
- >
420
- {isSaving ? "Saving..." : "Save Changes"}
421
- </Button>
422
- )}
423
- <Button className="dy-w-full dy-h-11 dy-rounded-xl dy-font-bold dy-gap-2 dy-bg-white" variant="outline" asChild>
424
- <a href={url} target="_blank" rel="noreferrer">
425
- <ExternalLink className="dy-h-4 dy-w-4" />
426
- Open Original
427
- </a>
428
- </Button>
429
- </div>
430
- </SheetContent>
431
- </Sheet>
432
- )
433
- }
434
-
435
- function DetailItem({ label, value, copyable }: {
436
- label: string,
437
- value: string,
438
- copyable?: boolean
439
- }) {
440
- return (
441
- <div className="dy-space-y-1.5">
442
- <label className="dy-text-[10px] dy-font-bold dy-uppercase dy-tracking-widest dy-text-muted-foreground/80">{label}</label>
443
- <div className="dy-flex dy-items-center dy-gap-2 dy-group">
444
- <p className="dy-text-sm dy-font-medium dy-text-foreground dy-truncate dy-flex-1">{value}</p>
445
- {copyable && (
446
- <Button
447
- size="icon"
448
- variant="ghost"
449
- className="dy-h-7 dy-w-7 dy-text-muted-foreground hover:dy-text-primary dy-transition-colors"
450
- onClick={() => navigator.clipboard.writeText(value)}
451
- >
452
- <Copy className="dy-h-3.5 dy-w-3.5" />
453
- </Button>
454
- )}
455
- </div>
456
- </div>
457
- )
458
- }
459
-
460
- function FileUploader({ collectionSlug, onComplete }: { collectionSlug?: string, onComplete: () => void }) {
461
- const { client } = useDyrected()
462
- const [files, setFiles] = React.useState<File[]>([])
463
- const [uploading, setUploading] = React.useState(false)
464
- const [progress, setProgress] = React.useState(0)
465
-
466
- const onDrop = React.useCallback((acceptedFiles: File[]) => {
467
- setFiles(prev => [...prev, ...acceptedFiles])
468
- }, [])
469
-
470
- const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
471
-
472
- const handleUpload = async () => {
473
- if (files.length === 0) return
474
- setUploading(true)
475
- setProgress(0)
476
-
477
- try {
478
- for (let i = 0; i < files.length; i++) {
479
- await client!.uploadMedia(files[i], collectionSlug)
480
- setProgress(((i + 1) / files.length) * 100)
481
- }
482
- onComplete()
483
- toast.success(`${files.length} assets uploaded successfully`)
484
- } catch (error: any) {
485
- console.error("Upload failed", error)
486
- toast.error("Failed to upload assets", {
487
- description: error.message
488
- })
489
- } finally {
490
- setUploading(false)
491
- }
492
- }
493
-
494
- return (
495
- <div className="dy-space-y-6 dy-py-6 dy-px-4">
496
- <div
497
- {...getRootProps()}
498
- className={`border-2 border-dashed rounded-2xl p-12 text-center cursor-pointer transition-all duration-300 ${isDragActive
499
- ? "border-primary bg-primary/5 scale-[0.98]"
500
- : "border-muted-foreground/20 hover:border-primary/40 hover:bg-muted/5"
501
- }`}
502
- >
503
- <input {...getInputProps()} />
504
- <div className="dy-h-16 dy-w-16 dy-rounded-2xl dy-bg-primary/10 dy-flex dy-items-center dy-justify-center dy-mx-auto dy-mb-4">
505
- <Upload className="dy-h-8 dy-w-8 dy-text-primary" />
506
- </div>
507
- <p className="dy-text-xl dy-font-bold dy-text-foreground">Drag & drop assets</p>
508
- <p className="dy-text-sm dy-text-muted-foreground dy-mt-1">or click to browse your files</p>
509
- </div>
510
-
511
- {files.length > 0 && (
512
- <div className="dy-space-y-4 dy-animate-in dy-fade-in dy-slide-in-from-bottom-4">
513
- <div className="dy-flex dy-items-center dy-justify-between">
514
- <p className="dy-text-sm dy-font-bold dy-text-foreground">{files.length} assets selected</p>
515
- <Button variant="ghost" size="sm" onClick={() => setFiles([])} disabled={uploading} className="dy-text-xs dy-h-8">
516
- Clear All
517
- </Button>
518
- </div>
519
-
520
- <div className="dy-max-h-[240px] dy-overflow-auto dy-space-y-2 dy-pr-2 dy-custom-scrollbar">
521
- {files.map((file, idx) => (
522
- <div key={idx} className="dy-flex dy-items-center dy-justify-between dy-p-3 dy-bg-muted/30 dy-border dy-border-border/40 dy-rounded-xl dy-text-sm dy-group dy-transition-colors hover:dy-bg-muted/50">
523
- <div className="dy-flex dy-items-center dy-gap-3 dy-truncate">
524
- <div className="dy-h-8 dy-w-8 dy-rounded-lg dy-bg-white dy-border dy-border-border/60 dy-flex dy-items-center dy-justify-center dy-flex-shrink-0">
525
- <FileIcon className="dy-h-4 dy-w-4 dy-text-muted-foreground" />
526
- </div>
527
- <span className="dy-truncate dy-font-medium dy-text-foreground/80">{file.name}</span>
528
- </div>
529
- <span className="dy-text-muted-foreground dy-text-[10px] dy-font-bold dy-bg-white dy-px-2 dy-py-1 dy-rounded dy-border dy-border-border/40 dy-ml-4">
530
- {(file.size / 1024).toFixed(1)} KB
531
- </span>
532
- </div>
533
- ))}
534
- </div>
535
-
536
- {uploading && (
537
- <div className="dy-space-y-2 dy-pt-2">
538
- <div className="dy-flex dy-justify-between dy-text-[11px] dy-font-bold dy-uppercase dy-tracking-wider dy-text-muted-foreground">
539
- <span>Uploading...</span>
540
- <span>{Math.round(progress)}%</span>
541
- </div>
542
- <Progress value={progress} className="dy-h-2 dy-rounded-full" />
543
- </div>
544
- )}
545
-
546
- <div className="dy-flex dy-justify-end dy-pt-4 dy-border-t dy-border-border/40">
547
- <Button
548
- onClick={handleUpload}
549
- disabled={uploading || files.length === 0}
550
- className="dy-w-full dy-h-12 dy-rounded-xl dy-bg-primary hover:dy-bg-primary/90 dy-text-white dy-font-bold dy-shadow-lg dy-shadow-primary/20 dy-transition-all active:dy-scale-[0.98]"
551
- >
552
- {uploading ? (
553
- <span className="dy-flex dy-items-center dy-gap-2">
554
- <div className="dy-h-4 dy-w-4 dy-border-2 dy-border-white/30 dy-border-t-white dy-rounded-full dy-animate-spin" />
555
- Uploading Assets...
556
- </span>
557
- ) : `Upload ${files.length} Assets`}
558
- </Button>
559
- </div>
560
- </div>
561
- )}
562
- </div>
563
- )
564
- }