@actuate-media/cms-admin 0.49.0 → 0.49.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/CHANGELOG.md +13 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +10 -11
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/modal.render.test.d.ts +2 -0
- package/dist/__tests__/components/modal.render.test.d.ts.map +1 -0
- package/dist/__tests__/components/modal.render.test.js +37 -0
- package/dist/__tests__/components/modal.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/MediaPickerModal.d.ts +1 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.js +12 -11
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/components/SchedulePublishDialog.d.ts +1 -1
- package/dist/components/SchedulePublishDialog.d.ts.map +1 -1
- package/dist/components/SchedulePublishDialog.js +6 -4
- package/dist/components/SchedulePublishDialog.js.map +1 -1
- package/dist/components/SharePreviewLinkDialog.d.ts +1 -1
- package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -1
- package/dist/components/SharePreviewLinkDialog.js +10 -9
- package/dist/components/SharePreviewLinkDialog.js.map +1 -1
- package/dist/components/VersionHistory.d.ts +1 -1
- package/dist/components/VersionHistory.d.ts.map +1 -1
- package/dist/components/VersionHistory.js +14 -14
- package/dist/components/VersionHistory.js.map +1 -1
- package/dist/components/ui/Modal.d.ts +17 -1
- package/dist/components/ui/Modal.d.ts.map +1 -1
- package/dist/components/ui/Modal.js +16 -15
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/fields/BlockBuilderField.d.ts.map +1 -1
- package/dist/fields/BlockBuilderField.js +6 -4
- package/dist/fields/BlockBuilderField.js.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.d.ts +1 -1
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -1
- package/dist/views/page-builder/AIGenerateDialog.js +23 -23
- package/dist/views/page-builder/AIGenerateDialog.js.map +1 -1
- package/package.json +1 -1
- package/src/AdminRoot.tsx +35 -30
- package/src/__tests__/components/modal.render.test.tsx +57 -0
- package/src/components/MediaPickerModal.tsx +143 -119
- package/src/components/SchedulePublishDialog.tsx +118 -93
- package/src/components/SharePreviewLinkDialog.tsx +115 -93
- package/src/components/VersionHistory.tsx +98 -82
- package/src/components/ui/Modal.tsx +48 -39
- package/src/fields/BlockBuilderField.tsx +26 -19
- package/src/views/page-builder/AIGenerateDialog.tsx +347 -335
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useRef } from 'react'
|
|
3
|
+
import { useId, useState, useRef } from 'react'
|
|
4
|
+
import * as Dialog from '@radix-ui/react-dialog'
|
|
4
5
|
import { X, Upload, Search, ImageIcon, Loader2 } from 'lucide-react'
|
|
5
6
|
import { toast } from 'sonner'
|
|
6
7
|
import { cmsApi } from '../lib/api.js'
|
|
8
|
+
import { adminPortalContainer } from '../lib/portal-container.js'
|
|
7
9
|
import { useApiData } from '../lib/useApiData.js'
|
|
8
10
|
|
|
9
11
|
interface MediaItem {
|
|
@@ -60,6 +62,7 @@ export function MediaPickerModal({
|
|
|
60
62
|
const [search, setSearch] = useState('')
|
|
61
63
|
const [uploading, setUploading] = useState(false)
|
|
62
64
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
65
|
+
const searchId = useId()
|
|
63
66
|
|
|
64
67
|
const { data, loading, refetch } = useApiData<{ data: { items: MediaItem[] } }>(
|
|
65
68
|
open ? `/media?pageSize=50${search ? `&search=${encodeURIComponent(search)}` : ''}` : null,
|
|
@@ -119,132 +122,153 @@ export function MediaPickerModal({
|
|
|
119
122
|
onClose()
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
if (!open) return null
|
|
123
|
-
|
|
124
125
|
return (
|
|
125
|
-
<
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
: 'text-gray-500 hover:text-gray-700'
|
|
146
|
-
}`}
|
|
147
|
-
>
|
|
148
|
-
Media Library
|
|
149
|
-
</button>
|
|
150
|
-
<button
|
|
151
|
-
onClick={() => setTab('upload')}
|
|
152
|
-
className={`flex-1 px-4 py-2.5 text-sm font-medium transition-colors ${
|
|
153
|
-
tab === 'upload'
|
|
154
|
-
? 'border-b-2 border-blue-600 text-blue-600'
|
|
155
|
-
: 'text-gray-500 hover:text-gray-700'
|
|
156
|
-
}`}
|
|
157
|
-
>
|
|
158
|
-
Upload New
|
|
159
|
-
</button>
|
|
126
|
+
<Dialog.Root open={open} onOpenChange={(next) => !next && onClose()}>
|
|
127
|
+
<Dialog.Portal container={adminPortalContainer()}>
|
|
128
|
+
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/40" />
|
|
129
|
+
<Dialog.Content
|
|
130
|
+
aria-describedby={undefined}
|
|
131
|
+
className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-50 mx-4 flex max-h-[80vh] w-[calc(100%-2rem)] max-w-2xl -translate-x-1/2 -translate-y-1/2 flex-col rounded-xl border shadow-2xl focus:outline-none"
|
|
132
|
+
>
|
|
133
|
+
<div className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
134
|
+
<Dialog.Title className="text-foreground text-lg font-medium">
|
|
135
|
+
Insert Image
|
|
136
|
+
</Dialog.Title>
|
|
137
|
+
<Dialog.Close asChild>
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
aria-label="Close media picker"
|
|
141
|
+
className="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring rounded-lg p-1.5 transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
142
|
+
>
|
|
143
|
+
<X className="h-5 w-5" aria-hidden />
|
|
144
|
+
</button>
|
|
145
|
+
</Dialog.Close>
|
|
160
146
|
</div>
|
|
161
|
-
)}
|
|
162
147
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
148
|
+
{!libraryOnly && (
|
|
149
|
+
<div className="border-border flex border-b" role="tablist" aria-label="Image source">
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
role="tab"
|
|
153
|
+
aria-selected={tab === 'library'}
|
|
154
|
+
onClick={() => setTab('library')}
|
|
155
|
+
className={`flex-1 px-4 py-2.5 text-sm font-medium transition-colors ${
|
|
156
|
+
tab === 'library'
|
|
157
|
+
? 'border-primary text-primary border-b-2'
|
|
158
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
159
|
+
}`}
|
|
160
|
+
>
|
|
161
|
+
Media Library
|
|
162
|
+
</button>
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
role="tab"
|
|
166
|
+
aria-selected={tab === 'upload'}
|
|
167
|
+
onClick={() => setTab('upload')}
|
|
168
|
+
className={`flex-1 px-4 py-2.5 text-sm font-medium transition-colors ${
|
|
169
|
+
tab === 'upload'
|
|
170
|
+
? 'border-primary text-primary border-b-2'
|
|
171
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
172
|
+
}`}
|
|
173
|
+
>
|
|
174
|
+
Upload New
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
176
178
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
180
|
+
{libraryOnly || tab === 'library' ? (
|
|
181
|
+
<>
|
|
182
|
+
<div className="relative mb-4">
|
|
183
|
+
<Search
|
|
184
|
+
className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2"
|
|
185
|
+
aria-hidden
|
|
186
|
+
/>
|
|
187
|
+
<input
|
|
188
|
+
id={searchId}
|
|
189
|
+
type="text"
|
|
190
|
+
value={search}
|
|
191
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
192
|
+
placeholder="Search images..."
|
|
193
|
+
aria-label="Search images"
|
|
194
|
+
className="border-input bg-background text-foreground focus:ring-ring w-full rounded-lg border py-2 pr-3 pl-9 text-sm focus:ring-2 focus:outline-none"
|
|
195
|
+
/>
|
|
180
196
|
</div>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
197
|
+
|
|
198
|
+
{loading ? (
|
|
199
|
+
<div className="flex items-center justify-center py-12">
|
|
200
|
+
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
|
|
201
|
+
</div>
|
|
202
|
+
) : imageItems.length === 0 ? (
|
|
203
|
+
<div className="text-muted-foreground py-12 text-center text-sm">
|
|
204
|
+
No images found. Try uploading one.
|
|
205
|
+
</div>
|
|
206
|
+
) : (
|
|
207
|
+
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4">
|
|
208
|
+
{imageItems.map((item: MediaItem) => {
|
|
209
|
+
const src = item.url || item.storageKey
|
|
210
|
+
return (
|
|
211
|
+
<button
|
|
212
|
+
key={item.id}
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={() => handleSelectItem(item)}
|
|
215
|
+
title={item.filename}
|
|
216
|
+
className="group border-border bg-muted hover:border-primary relative aspect-square overflow-hidden rounded-lg border-2 transition-colors"
|
|
217
|
+
>
|
|
218
|
+
{src ? (
|
|
219
|
+
<img
|
|
220
|
+
src={src}
|
|
221
|
+
alt={item.filename}
|
|
222
|
+
loading="lazy"
|
|
223
|
+
className="h-full w-full object-contain"
|
|
224
|
+
/>
|
|
225
|
+
) : (
|
|
226
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
227
|
+
<ImageIcon className="text-muted-foreground h-8 w-8" aria-hidden />
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
<div className="absolute inset-x-0 bottom-0 bg-black/60 p-1.5 opacity-0 transition-opacity group-hover:opacity-100">
|
|
231
|
+
<p className="truncate text-xs text-white">{item.filename}</p>
|
|
206
232
|
</div>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
233
|
+
</button>
|
|
234
|
+
)
|
|
235
|
+
})}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</>
|
|
239
|
+
) : (
|
|
240
|
+
<div className="flex flex-col items-center justify-center py-12">
|
|
241
|
+
<input
|
|
242
|
+
ref={fileInputRef}
|
|
243
|
+
type="file"
|
|
244
|
+
accept={accept ?? 'image/*'}
|
|
245
|
+
className="hidden"
|
|
246
|
+
onChange={(e) => handleUpload(e.target.files)}
|
|
247
|
+
/>
|
|
248
|
+
<div className="bg-primary/10 mb-4 flex h-16 w-16 items-center justify-center rounded-full">
|
|
249
|
+
{uploading ? (
|
|
250
|
+
<Loader2 className="text-primary h-8 w-8 animate-spin" aria-hidden />
|
|
251
|
+
) : (
|
|
252
|
+
<Upload className="text-primary h-8 w-8" aria-hidden />
|
|
253
|
+
)}
|
|
214
254
|
</div>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-blue-50">
|
|
227
|
-
{uploading ? (
|
|
228
|
-
<Loader2 className="h-8 w-8 animate-spin text-blue-600" />
|
|
229
|
-
) : (
|
|
230
|
-
<Upload className="h-8 w-8 text-blue-600" />
|
|
255
|
+
<p className="text-muted-foreground mb-4 text-sm">
|
|
256
|
+
{uploading ? 'Uploading...' : 'Select an image file to upload'}
|
|
257
|
+
</p>
|
|
258
|
+
{!uploading && (
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
onClick={() => fileInputRef.current?.click()}
|
|
262
|
+
className="bg-primary text-primary-foreground rounded-lg px-4 py-2 text-sm transition-opacity hover:opacity-90"
|
|
263
|
+
>
|
|
264
|
+
Choose File
|
|
265
|
+
</button>
|
|
231
266
|
)}
|
|
232
267
|
</div>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
onClick={() => fileInputRef.current?.click()}
|
|
239
|
-
className="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
|
|
240
|
-
>
|
|
241
|
-
Choose File
|
|
242
|
-
</button>
|
|
243
|
-
)}
|
|
244
|
-
</div>
|
|
245
|
-
)}
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
</Dialog.Content>
|
|
271
|
+
</Dialog.Portal>
|
|
272
|
+
</Dialog.Root>
|
|
249
273
|
)
|
|
250
274
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
3
|
+
import { useEffect, useId, useState } from 'react'
|
|
4
|
+
import * as Dialog from '@radix-ui/react-dialog'
|
|
4
5
|
import { Calendar, Clock, Loader2, X } from 'lucide-react'
|
|
5
6
|
import { toast } from 'sonner'
|
|
6
7
|
import { cmsApi } from '../lib/api.js'
|
|
8
|
+
import { adminPortalContainer } from '../lib/portal-container.js'
|
|
7
9
|
|
|
8
10
|
export interface SchedulePublishDialogProps {
|
|
9
11
|
collectionSlug: string
|
|
@@ -56,6 +58,8 @@ export function SchedulePublishDialog({
|
|
|
56
58
|
const [includeUnpublish, setIncludeUnpublish] = useState(false)
|
|
57
59
|
const [saving, setSaving] = useState(false)
|
|
58
60
|
const [cancelling, setCancelling] = useState(false)
|
|
61
|
+
const publishAtId = useId()
|
|
62
|
+
const unpublishAtId = useId()
|
|
59
63
|
|
|
60
64
|
const hasExistingSchedule = Boolean(scheduledAt || scheduledUnpublishAt)
|
|
61
65
|
|
|
@@ -133,109 +137,130 @@ export function SchedulePublishDialog({
|
|
|
133
137
|
onClose()
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
if (!open) return null
|
|
137
|
-
|
|
138
140
|
return (
|
|
139
|
-
<
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
<
|
|
143
|
-
<div className="flex items-center
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
className="rounded-lg p-1.5 transition-colors hover:bg-gray-100"
|
|
150
|
-
>
|
|
151
|
-
<X className="h-5 w-5 text-gray-500" />
|
|
152
|
-
</button>
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
<div className="space-y-4 px-4 py-4">
|
|
156
|
-
<div className="space-y-1.5">
|
|
157
|
-
<label className="text-sm font-medium text-gray-700">Publish at</label>
|
|
158
|
-
<div className="relative">
|
|
159
|
-
<Clock className="pointer-events-none absolute top-2.5 left-2.5 h-4 w-4 text-gray-400" />
|
|
160
|
-
<input
|
|
161
|
-
type="datetime-local"
|
|
162
|
-
value={publishAt}
|
|
163
|
-
onChange={(e) => setPublishAt(e.target.value)}
|
|
164
|
-
min={toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
|
|
165
|
-
className="w-full rounded-md border border-gray-300 bg-white py-2 pr-3 pl-9 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
|
166
|
-
/>
|
|
141
|
+
<Dialog.Root open={open} onOpenChange={(next) => !next && onClose()}>
|
|
142
|
+
<Dialog.Portal container={adminPortalContainer()}>
|
|
143
|
+
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/40" />
|
|
144
|
+
<Dialog.Content className="bg-card text-card-foreground border-border fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border shadow-xl focus:outline-none">
|
|
145
|
+
<div className="border-border flex items-center justify-between border-b px-4 py-3">
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
<Calendar className="text-muted-foreground h-5 w-5" aria-hidden />
|
|
148
|
+
<Dialog.Title className="text-foreground text-lg font-medium">
|
|
149
|
+
Schedule publishing
|
|
150
|
+
</Dialog.Title>
|
|
167
151
|
</div>
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
152
|
+
<Dialog.Close asChild>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
aria-label="Close dialog"
|
|
156
|
+
className="text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring rounded-lg p-1.5 transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
157
|
+
>
|
|
158
|
+
<X className="h-5 w-5" aria-hidden />
|
|
159
|
+
</button>
|
|
160
|
+
</Dialog.Close>
|
|
171
161
|
</div>
|
|
172
162
|
|
|
173
|
-
<
|
|
174
|
-
<input
|
|
175
|
-
type="checkbox"
|
|
176
|
-
checked={includeUnpublish}
|
|
177
|
-
onChange={(e) => setIncludeUnpublish(e.target.checked)}
|
|
178
|
-
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
179
|
-
/>
|
|
180
|
-
Also schedule an unpublish
|
|
181
|
-
</label>
|
|
182
|
-
|
|
183
|
-
{includeUnpublish && (
|
|
163
|
+
<div className="space-y-4 px-4 py-4">
|
|
184
164
|
<div className="space-y-1.5">
|
|
185
|
-
<label className="text-sm font-medium
|
|
165
|
+
<label htmlFor={publishAtId} className="text-foreground text-sm font-medium">
|
|
166
|
+
Publish at
|
|
167
|
+
</label>
|
|
186
168
|
<div className="relative">
|
|
187
|
-
<Clock
|
|
169
|
+
<Clock
|
|
170
|
+
className="text-muted-foreground pointer-events-none absolute top-2.5 left-2.5 h-4 w-4"
|
|
171
|
+
aria-hidden
|
|
172
|
+
/>
|
|
188
173
|
<input
|
|
174
|
+
id={publishAtId}
|
|
189
175
|
type="datetime-local"
|
|
190
|
-
value={
|
|
191
|
-
onChange={(e) =>
|
|
192
|
-
min={
|
|
193
|
-
className="
|
|
176
|
+
value={publishAt}
|
|
177
|
+
onChange={(e) => setPublishAt(e.target.value)}
|
|
178
|
+
min={toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
|
|
179
|
+
className="border-input bg-background text-foreground focus:border-ring focus:ring-ring w-full rounded-md border py-2 pr-3 pl-9 text-sm focus:ring-1 focus:outline-none"
|
|
194
180
|
/>
|
|
195
181
|
</div>
|
|
182
|
+
<p className="text-muted-foreground text-xs">
|
|
183
|
+
The document will move from DRAFT to PUBLISHED at this time (your local timezone).
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<label className="text-foreground flex items-center gap-2 text-sm">
|
|
188
|
+
<input
|
|
189
|
+
type="checkbox"
|
|
190
|
+
checked={includeUnpublish}
|
|
191
|
+
onChange={(e) => setIncludeUnpublish(e.target.checked)}
|
|
192
|
+
className="border-input text-primary focus:ring-ring h-4 w-4 rounded"
|
|
193
|
+
/>
|
|
194
|
+
Also schedule an unpublish
|
|
195
|
+
</label>
|
|
196
|
+
|
|
197
|
+
{includeUnpublish && (
|
|
198
|
+
<div className="space-y-1.5">
|
|
199
|
+
<label htmlFor={unpublishAtId} className="text-foreground text-sm font-medium">
|
|
200
|
+
Unpublish at
|
|
201
|
+
</label>
|
|
202
|
+
<div className="relative">
|
|
203
|
+
<Clock
|
|
204
|
+
className="text-muted-foreground pointer-events-none absolute top-2.5 left-2.5 h-4 w-4"
|
|
205
|
+
aria-hidden
|
|
206
|
+
/>
|
|
207
|
+
<input
|
|
208
|
+
id={unpublishAtId}
|
|
209
|
+
type="datetime-local"
|
|
210
|
+
value={unpublishAt}
|
|
211
|
+
onChange={(e) => setUnpublishAt(e.target.value)}
|
|
212
|
+
min={publishAt || toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
|
|
213
|
+
className="border-input bg-background text-foreground focus:border-ring focus:ring-ring w-full rounded-md border py-2 pr-3 pl-9 text-sm focus:ring-1 focus:outline-none"
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div className="border-border flex items-center justify-between gap-2 border-t px-4 py-3">
|
|
221
|
+
{hasExistingSchedule ? (
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
onClick={handleCancelSchedule}
|
|
225
|
+
disabled={cancelling || saving}
|
|
226
|
+
className="text-destructive hover:bg-destructive/10 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium disabled:opacity-50"
|
|
227
|
+
>
|
|
228
|
+
{cancelling ? (
|
|
229
|
+
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
|
|
230
|
+
) : (
|
|
231
|
+
<X className="h-4 w-4" aria-hidden />
|
|
232
|
+
)}
|
|
233
|
+
Cancel schedule
|
|
234
|
+
</button>
|
|
235
|
+
) : (
|
|
236
|
+
<span />
|
|
237
|
+
)}
|
|
238
|
+
<div className="flex items-center gap-2">
|
|
239
|
+
<Dialog.Close asChild>
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
className="text-foreground hover:bg-accent rounded-md px-3 py-1.5 text-sm"
|
|
243
|
+
>
|
|
244
|
+
Close
|
|
245
|
+
</button>
|
|
246
|
+
</Dialog.Close>
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={handleSave}
|
|
250
|
+
disabled={saving || cancelling}
|
|
251
|
+
className="bg-primary text-primary-foreground inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium hover:opacity-90 disabled:opacity-50"
|
|
252
|
+
>
|
|
253
|
+
{saving ? (
|
|
254
|
+
<Loader2 className="h-4 w-4 animate-spin" aria-hidden />
|
|
255
|
+
) : (
|
|
256
|
+
<Calendar className="h-4 w-4" aria-hidden />
|
|
257
|
+
)}
|
|
258
|
+
{hasExistingSchedule ? 'Reschedule' : 'Schedule'}
|
|
259
|
+
</button>
|
|
196
260
|
</div>
|
|
197
|
-
)}
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
<div className="flex items-center justify-between gap-2 border-t border-gray-200 px-4 py-3">
|
|
201
|
-
{hasExistingSchedule ? (
|
|
202
|
-
<button
|
|
203
|
-
onClick={handleCancelSchedule}
|
|
204
|
-
disabled={cancelling || saving}
|
|
205
|
-
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-red-700 hover:bg-red-50 disabled:opacity-50"
|
|
206
|
-
>
|
|
207
|
-
{cancelling ? (
|
|
208
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
209
|
-
) : (
|
|
210
|
-
<X className="h-4 w-4" />
|
|
211
|
-
)}
|
|
212
|
-
Cancel schedule
|
|
213
|
-
</button>
|
|
214
|
-
) : (
|
|
215
|
-
<span />
|
|
216
|
-
)}
|
|
217
|
-
<div className="flex items-center gap-2">
|
|
218
|
-
<button
|
|
219
|
-
onClick={onClose}
|
|
220
|
-
className="rounded-md px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-100"
|
|
221
|
-
>
|
|
222
|
-
Close
|
|
223
|
-
</button>
|
|
224
|
-
<button
|
|
225
|
-
onClick={handleSave}
|
|
226
|
-
disabled={saving || cancelling}
|
|
227
|
-
className="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
228
|
-
>
|
|
229
|
-
{saving ? (
|
|
230
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
231
|
-
) : (
|
|
232
|
-
<Calendar className="h-4 w-4" />
|
|
233
|
-
)}
|
|
234
|
-
{hasExistingSchedule ? 'Reschedule' : 'Schedule'}
|
|
235
|
-
</button>
|
|
236
261
|
</div>
|
|
237
|
-
</
|
|
238
|
-
</
|
|
239
|
-
</
|
|
262
|
+
</Dialog.Content>
|
|
263
|
+
</Dialog.Portal>
|
|
264
|
+
</Dialog.Root>
|
|
240
265
|
)
|
|
241
266
|
}
|