@actuate-media/cms-admin 0.9.0 → 0.11.0
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/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +8 -5
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/layout/primitives.test.d.ts +2 -0
- package/dist/__tests__/layout/primitives.test.d.ts.map +1 -0
- package/dist/__tests__/layout/primitives.test.js +34 -0
- package/dist/__tests__/layout/primitives.test.js.map +1 -0
- package/dist/__tests__/lib/cv.test.d.ts +2 -0
- package/dist/__tests__/lib/cv.test.d.ts.map +1 -0
- package/dist/__tests__/lib/cv.test.js +66 -0
- package/dist/__tests__/lib/cv.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/assets/actuate-logo.d.ts +36 -0
- package/dist/assets/actuate-logo.d.ts.map +1 -0
- package/dist/assets/actuate-logo.js +15 -0
- package/dist/assets/actuate-logo.js.map +1 -0
- package/dist/components/Breadcrumbs.js +2 -2
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/ContentOverviewChart.js +3 -3
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/FocalPointPicker.js +2 -2
- package/dist/components/FolderTree.js +20 -20
- package/dist/components/LivePreview.js +3 -3
- package/dist/components/LocaleSwitcher.js +1 -1
- package/dist/components/MediaPickerModal.js +4 -4
- package/dist/components/PresenceIndicator.js +1 -1
- package/dist/components/SEOConfigPanel.d.ts +2 -0
- package/dist/components/SEOConfigPanel.d.ts.map +1 -0
- package/dist/components/SEOConfigPanel.js +174 -0
- package/dist/components/SEOConfigPanel.js.map +1 -0
- package/dist/components/SEOPanel.js +9 -9
- package/dist/components/SEOPerformance.js +2 -2
- package/dist/components/SchedulePublishDialog.d.ts +18 -0
- package/dist/components/SchedulePublishDialog.d.ts.map +1 -0
- package/dist/components/SchedulePublishDialog.js +106 -0
- package/dist/components/SchedulePublishDialog.js.map +1 -0
- package/dist/components/SharePreviewLinkDialog.d.ts +17 -0
- package/dist/components/SharePreviewLinkDialog.d.ts.map +1 -0
- package/dist/components/SharePreviewLinkDialog.js +83 -0
- package/dist/components/SharePreviewLinkDialog.js.map +1 -0
- package/dist/components/TipTapEditor.js +5 -5
- package/dist/components/VersionHistory.js +2 -2
- package/dist/components/ui/Badge.d.ts +33 -3
- package/dist/components/ui/Badge.d.ts.map +1 -1
- package/dist/components/ui/Badge.js +42 -8
- package/dist/components/ui/Badge.js.map +1 -1
- package/dist/components/ui/Button.d.ts +19 -8
- package/dist/components/ui/Button.d.ts.map +1 -1
- package/dist/components/ui/Button.js +35 -14
- package/dist/components/ui/Button.js.map +1 -1
- package/dist/components/ui/Card.d.ts +26 -0
- package/dist/components/ui/Card.d.ts.map +1 -0
- package/dist/components/ui/Card.js +45 -0
- package/dist/components/ui/Card.js.map +1 -0
- package/dist/components/ui/DataTable.js +1 -1
- package/dist/components/ui/Input.d.ts +15 -0
- package/dist/components/ui/Input.d.ts.map +1 -0
- package/dist/components/ui/Input.js +23 -0
- package/dist/components/ui/Input.js.map +1 -0
- package/dist/components/ui/SearchInput.js +1 -1
- package/dist/components/ui/Select.d.ts +16 -0
- package/dist/components/ui/Select.d.ts.map +1 -0
- package/dist/components/ui/Select.js +25 -0
- package/dist/components/ui/Select.js.map +1 -0
- package/dist/components/ui/Toast.js +1 -1
- package/dist/components/ui/index.d.ts +10 -4
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +5 -2
- package/dist/components/ui/index.js.map +1 -1
- package/dist/fields/BlockBuilderField.js +3 -3
- package/dist/fields/DateField.js +1 -1
- package/dist/fields/RelationshipField.js +3 -3
- package/dist/fields/TextField.js +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Layout.d.ts +14 -0
- package/dist/layout/Layout.d.ts.map +1 -1
- package/dist/layout/Layout.js +17 -11
- package/dist/layout/Layout.js.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +21 -11
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/layout/primitives/AdminShell.d.ts +43 -0
- package/dist/layout/primitives/AdminShell.d.ts.map +1 -0
- package/dist/layout/primitives/AdminShell.js +51 -0
- package/dist/layout/primitives/AdminShell.js.map +1 -0
- package/dist/layout/primitives/Box.d.ts +19 -0
- package/dist/layout/primitives/Box.d.ts.map +1 -0
- package/dist/layout/primitives/Box.js +12 -0
- package/dist/layout/primitives/Box.js.map +1 -0
- package/dist/layout/primitives/Cluster.d.ts +27 -0
- package/dist/layout/primitives/Cluster.d.ts.map +1 -0
- package/dist/layout/primitives/Cluster.js +37 -0
- package/dist/layout/primitives/Cluster.js.map +1 -0
- package/dist/layout/primitives/Grid.d.ts +45 -0
- package/dist/layout/primitives/Grid.d.ts.map +1 -0
- package/dist/layout/primitives/Grid.js +59 -0
- package/dist/layout/primitives/Grid.js.map +1 -0
- package/dist/layout/primitives/PageContainer.d.ts +36 -0
- package/dist/layout/primitives/PageContainer.d.ts.map +1 -0
- package/dist/layout/primitives/PageContainer.js +41 -0
- package/dist/layout/primitives/PageContainer.js.map +1 -0
- package/dist/layout/primitives/Split.d.ts +34 -0
- package/dist/layout/primitives/Split.d.ts.map +1 -0
- package/dist/layout/primitives/Split.js +27 -0
- package/dist/layout/primitives/Split.js.map +1 -0
- package/dist/layout/primitives/Stack.d.ts +23 -0
- package/dist/layout/primitives/Stack.d.ts.map +1 -0
- package/dist/layout/primitives/Stack.js +34 -0
- package/dist/layout/primitives/Stack.js.map +1 -0
- package/dist/layout/primitives/index.d.ts +30 -0
- package/dist/layout/primitives/index.d.ts.map +1 -0
- package/dist/layout/primitives/index.js +22 -0
- package/dist/layout/primitives/index.js.map +1 -0
- package/dist/layout/primitives/tokens.d.ts +48 -0
- package/dist/layout/primitives/tokens.d.ts.map +1 -0
- package/dist/layout/primitives/tokens.js +54 -0
- package/dist/layout/primitives/tokens.js.map +1 -0
- package/dist/lib/cv.d.ts +53 -0
- package/dist/lib/cv.d.ts.map +1 -0
- package/dist/lib/cv.js +39 -0
- package/dist/lib/cv.js.map +1 -0
- package/dist/views/ApiKeys.d.ts.map +1 -1
- package/dist/views/ApiKeys.js +13 -11
- package/dist/views/ApiKeys.js.map +1 -1
- package/dist/views/CollectionList.js +8 -8
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +333 -78
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +17 -5
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/ForgotPassword.js +2 -2
- package/dist/views/FormEditor.js +5 -5
- package/dist/views/FormSubmissions.js +6 -6
- package/dist/views/Forms.js +2 -2
- package/dist/views/Login.d.ts +16 -1
- package/dist/views/Login.d.ts.map +1 -1
- package/dist/views/Login.js +17 -7
- package/dist/views/Login.js.map +1 -1
- package/dist/views/MediaBrowser.js +16 -16
- package/dist/views/PageEditor.js +2 -2
- package/dist/views/Pages.js +10 -10
- package/dist/views/PostEditor.js +2 -2
- package/dist/views/Posts.js +4 -4
- package/dist/views/Redirects.js +4 -4
- package/dist/views/ResetPassword.js +2 -2
- package/dist/views/SEO.js +6 -6
- package/dist/views/ScriptTagEditor.js +4 -4
- package/dist/views/ScriptTags.js +2 -2
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +9 -8
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/SetupWizard.js +2 -2
- package/dist/views/Users.js +4 -4
- package/dist/views/page-builder/AIBlockAssist.js +1 -1
- package/dist/views/page-builder/AIGenerateDialog.js +10 -10
- package/dist/views/page-builder/BlockEditor.js +10 -10
- package/dist/views/page-builder/BlockPicker.js +4 -4
- package/dist/views/page-builder/BottomBar.js +1 -1
- package/dist/views/page-builder/BuilderToolbar.js +2 -2
- package/dist/views/page-builder/ContextPanel.js +2 -2
- package/dist/views/page-builder/DesignScore.js +9 -9
- package/dist/views/page-builder/NodeSettings.js +8 -8
- package/dist/views/page-builder/PageBuilder.js +3 -3
- package/dist/views/page-builder/PageSettings.js +1 -1
- package/dist/views/page-builder/PageTemplates.js +2 -2
- package/dist/views/page-builder/SEOPanel.js +13 -13
- package/dist/views/page-builder/SavedSections.js +5 -5
- package/dist/views/page-builder/TemplatePicker.js +2 -2
- package/dist/views/page-builder/block-renderers/CTAPreview.js +5 -5
- package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/CodePreview.js +1 -1
- package/dist/views/page-builder/block-renderers/FAQPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/FallbackPreview.js +1 -1
- package/dist/views/page-builder/block-renderers/FormPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +5 -5
- package/dist/views/page-builder/block-renderers/HeroPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/ImagePreview.js +3 -3
- package/dist/views/page-builder/block-renderers/TextPreview.js +3 -3
- package/dist/views/page-builder/block-renderers/VideoPreview.js +4 -4
- package/dist/views/page-builder/canvas/BlockRenderer.js +1 -1
- package/dist/views/page-builder/canvas/BuilderCanvas.js +3 -3
- package/dist/views/page-builder/canvas/ColumnRenderer.js +2 -2
- package/dist/views/page-builder/canvas/ContainerRenderer.js +2 -2
- package/dist/views/page-builder/canvas/RowRenderer.js +2 -2
- package/dist/views/page-builder/canvas/SectionRenderer.js +2 -2
- package/package.json +6 -2
- package/src/AdminRoot.tsx +21 -11
- package/src/__tests__/layout/primitives.test.ts +37 -0
- package/src/__tests__/lib/cv.test.ts +74 -0
- package/src/assets/actuate-logo.tsx +72 -0
- package/src/components/Breadcrumbs.tsx +6 -6
- package/src/components/CommandPalette.tsx +34 -34
- package/src/components/ContentOverviewChart.tsx +3 -3
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/components/FocalPointPicker.tsx +4 -4
- package/src/components/FolderTree.tsx +38 -38
- package/src/components/LivePreview.tsx +16 -16
- package/src/components/LocaleSwitcher.tsx +7 -7
- package/src/components/MediaPickerModal.tsx +21 -21
- package/src/components/PresenceIndicator.tsx +2 -2
- package/src/components/SEOConfigPanel.tsx +582 -0
- package/src/components/SEOPanel.tsx +46 -46
- package/src/components/SEOPerformance.tsx +21 -21
- package/src/components/SchedulePublishDialog.tsx +241 -0
- package/src/components/SharePreviewLinkDialog.tsx +227 -0
- package/src/components/TipTapEditor.tsx +33 -33
- package/src/components/VersionHistory.tsx +16 -16
- package/src/components/ui/Badge.tsx +66 -14
- package/src/components/ui/Button.tsx +70 -33
- package/src/components/ui/Card.tsx +101 -0
- package/src/components/ui/DataTable.tsx +1 -1
- package/src/components/ui/Input.tsx +35 -0
- package/src/components/ui/SearchInput.tsx +4 -4
- package/src/components/ui/Select.tsx +56 -0
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/index.ts +18 -4
- package/src/fields/BlockBuilderField.tsx +3 -3
- package/src/fields/DateField.tsx +1 -1
- package/src/fields/RelationshipField.tsx +10 -10
- package/src/fields/TextField.tsx +1 -1
- package/src/index.ts +32 -0
- package/src/layout/Header.tsx +28 -28
- package/src/layout/Layout.tsx +39 -46
- package/src/layout/Sidebar.tsx +37 -64
- package/src/layout/primitives/AdminShell.tsx +118 -0
- package/src/layout/primitives/Box.tsx +30 -0
- package/src/layout/primitives/Cluster.tsx +74 -0
- package/src/layout/primitives/Grid.tsx +120 -0
- package/src/layout/primitives/PageContainer.tsx +96 -0
- package/src/layout/primitives/Split.tsx +73 -0
- package/src/layout/primitives/Stack.tsx +67 -0
- package/src/layout/primitives/index.ts +36 -0
- package/src/layout/primitives/tokens.ts +76 -0
- package/src/lib/cv.ts +96 -0
- package/src/styles/build-input.css +1 -1
- package/src/views/ApiKeys.tsx +57 -57
- package/src/views/CollectionList.tsx +30 -30
- package/src/views/Dashboard.tsx +737 -186
- package/src/views/DocumentEdit.tsx +90 -10
- package/src/views/ForgotPassword.tsx +18 -18
- package/src/views/FormEditor.tsx +75 -75
- package/src/views/FormSubmissions.tsx +76 -76
- package/src/views/Forms.tsx +27 -27
- package/src/views/Login.tsx +65 -25
- package/src/views/MediaBrowser.tsx +127 -127
- package/src/views/PageEditor.tsx +25 -25
- package/src/views/Pages.tsx +59 -59
- package/src/views/PostEditor.tsx +37 -37
- package/src/views/Posts.tsx +48 -48
- package/src/views/Redirects.tsx +21 -21
- package/src/views/ResetPassword.tsx +28 -28
- package/src/views/SEO.tsx +144 -144
- package/src/views/ScriptTagEditor.tsx +24 -24
- package/src/views/ScriptTags.tsx +10 -10
- package/src/views/Settings.tsx +88 -80
- package/src/views/SetupWizard.tsx +28 -28
- package/src/views/Users.tsx +20 -20
- package/src/views/page-builder/AIBlockAssist.tsx +1 -1
- package/src/views/page-builder/AIGenerateDialog.tsx +63 -63
- package/src/views/page-builder/BlockEditor.tsx +26 -26
- package/src/views/page-builder/BlockPicker.tsx +22 -22
- package/src/views/page-builder/BottomBar.tsx +8 -8
- package/src/views/page-builder/BuilderToolbar.tsx +17 -17
- package/src/views/page-builder/ContextPanel.tsx +3 -3
- package/src/views/page-builder/DesignScore.tsx +21 -21
- package/src/views/page-builder/NodeSettings.tsx +27 -27
- package/src/views/page-builder/PageBuilder.tsx +11 -11
- package/src/views/page-builder/PageSettings.tsx +4 -4
- package/src/views/page-builder/PageTemplates.tsx +18 -18
- package/src/views/page-builder/SEOPanel.tsx +53 -53
- package/src/views/page-builder/SavedSections.tsx +37 -37
- package/src/views/page-builder/TemplatePicker.tsx +17 -17
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +13 -13
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +5 -5
- package/src/views/page-builder/block-renderers/CodePreview.tsx +6 -6
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +13 -13
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +3 -3
- package/src/views/page-builder/block-renderers/FormPreview.tsx +20 -20
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +8 -8
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +16 -16
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +4 -4
- package/src/views/page-builder/block-renderers/TextPreview.tsx +14 -14
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +12 -12
- package/src/views/page-builder/canvas/BlockRenderer.tsx +4 -4
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +6 -6
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +3 -3
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +2 -2
- package/src/views/page-builder/canvas/RowRenderer.tsx +2 -2
- package/src/views/page-builder/canvas/SectionRenderer.tsx +2 -2
|
@@ -438,10 +438,10 @@ function Section({
|
|
|
438
438
|
badge?: React.ReactNode
|
|
439
439
|
}) {
|
|
440
440
|
return (
|
|
441
|
-
<div className="border border-[var(--border)]
|
|
441
|
+
<div className="overflow-hidden rounded-lg border border-[var(--border)]">
|
|
442
442
|
<button
|
|
443
443
|
onClick={() => onToggle(id)}
|
|
444
|
-
className="flex items-center justify-between
|
|
444
|
+
className="flex w-full items-center justify-between px-4 py-2.5 text-left text-sm font-medium text-[var(--foreground)] transition-colors hover:bg-[var(--muted)]"
|
|
445
445
|
>
|
|
446
446
|
<span className="flex items-center gap-2">
|
|
447
447
|
{icon}
|
|
@@ -454,7 +454,7 @@ function Section({
|
|
|
454
454
|
<ChevronDown className="h-4 w-4 text-[var(--muted-foreground)]" />
|
|
455
455
|
)}
|
|
456
456
|
</button>
|
|
457
|
-
{expanded && <div className="px-4 pb-4
|
|
457
|
+
{expanded && <div className="px-4 pt-1 pb-4">{children}</div>}
|
|
458
458
|
</div>
|
|
459
459
|
)
|
|
460
460
|
}
|
|
@@ -472,9 +472,9 @@ function ToggleSwitch({
|
|
|
472
472
|
}) {
|
|
473
473
|
return (
|
|
474
474
|
<div className="flex items-center justify-between gap-3">
|
|
475
|
-
<div className="
|
|
475
|
+
<div className="min-w-0 flex-1">
|
|
476
476
|
<label className="text-sm font-medium text-[var(--foreground)]">{label}</label>
|
|
477
|
-
<p className="text-xs text-[var(--muted-foreground)]
|
|
477
|
+
<p className="mt-0.5 text-xs text-[var(--muted-foreground)]">{description}</p>
|
|
478
478
|
</div>
|
|
479
479
|
<button
|
|
480
480
|
type="button"
|
|
@@ -484,7 +484,7 @@ function ToggleSwitch({
|
|
|
484
484
|
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition-colors ${checked ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]'}`}
|
|
485
485
|
>
|
|
486
486
|
<span
|
|
487
|
-
className={`pointer-events-none block h-5 w-5 rounded-full bg-white shadow-sm transition-transform
|
|
487
|
+
className={`pointer-events-none mt-0.5 block h-5 w-5 rounded-full bg-white shadow-sm transition-transform ${checked ? 'translate-x-[22px]' : 'translate-x-0.5'}`}
|
|
488
488
|
/>
|
|
489
489
|
</button>
|
|
490
490
|
</div>
|
|
@@ -510,18 +510,18 @@ function InputField({
|
|
|
510
510
|
}) {
|
|
511
511
|
return (
|
|
512
512
|
<div>
|
|
513
|
-
<label className="block text-xs font-medium text-[var(--muted-foreground)]
|
|
513
|
+
<label className="mb-1 block text-xs font-medium text-[var(--muted-foreground)]">
|
|
514
514
|
{label}
|
|
515
515
|
</label>
|
|
516
516
|
<input
|
|
517
517
|
type={type}
|
|
518
518
|
value={value}
|
|
519
519
|
onChange={(e) => onChange(e.target.value)}
|
|
520
|
-
className="w-full
|
|
520
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-1.5 text-sm text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:ring-2 focus:ring-[var(--primary)] focus:outline-none"
|
|
521
521
|
placeholder={placeholder}
|
|
522
522
|
/>
|
|
523
523
|
{charCount !== undefined && charTarget && (
|
|
524
|
-
<p className="
|
|
524
|
+
<p className="mt-1 text-xs text-[var(--muted-foreground)]">
|
|
525
525
|
{charCount} chars {charTarget}
|
|
526
526
|
</p>
|
|
527
527
|
)}
|
|
@@ -548,18 +548,18 @@ function TextareaField({
|
|
|
548
548
|
}) {
|
|
549
549
|
return (
|
|
550
550
|
<div>
|
|
551
|
-
<label className="block text-xs font-medium text-[var(--muted-foreground)]
|
|
551
|
+
<label className="mb-1 block text-xs font-medium text-[var(--muted-foreground)]">
|
|
552
552
|
{label}
|
|
553
553
|
</label>
|
|
554
554
|
<textarea
|
|
555
555
|
value={value}
|
|
556
556
|
onChange={(e) => onChange(e.target.value)}
|
|
557
557
|
rows={rows}
|
|
558
|
-
className="w-full
|
|
558
|
+
className="w-full resize-none rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-1.5 text-sm text-[var(--foreground)] placeholder:text-[var(--muted-foreground)] focus:ring-2 focus:ring-[var(--primary)] focus:outline-none"
|
|
559
559
|
placeholder={placeholder}
|
|
560
560
|
/>
|
|
561
561
|
{charCount !== undefined && charTarget && (
|
|
562
|
-
<p className="
|
|
562
|
+
<p className="mt-1 text-xs text-[var(--muted-foreground)]">
|
|
563
563
|
{charCount} chars {charTarget}
|
|
564
564
|
</p>
|
|
565
565
|
)}
|
|
@@ -622,24 +622,24 @@ export function SEOPanel({
|
|
|
622
622
|
return (
|
|
623
623
|
<div className="rounded-lg border border-[var(--border)] bg-[var(--card)]">
|
|
624
624
|
{/* Header */}
|
|
625
|
-
<div className="flex items-center justify-between
|
|
626
|
-
<h3 className="font-semibold text-[var(--foreground)]
|
|
627
|
-
<Search className="
|
|
625
|
+
<div className="flex items-center justify-between border-b border-[var(--border)] px-4 py-3">
|
|
626
|
+
<h3 className="flex items-center gap-2 text-sm font-semibold text-[var(--foreground)]">
|
|
627
|
+
<Search className="h-4 w-4" />
|
|
628
628
|
SEO
|
|
629
629
|
</h3>
|
|
630
630
|
<button
|
|
631
631
|
onClick={() => update({ metaTitle: title, ogTitle: '', ogDescription: '' })}
|
|
632
|
-
className="text-xs text-[var(--primary)] hover:opacity-80
|
|
632
|
+
className="flex items-center gap-1 text-xs text-[var(--primary)] hover:opacity-80"
|
|
633
633
|
>
|
|
634
|
-
<RefreshCw className="
|
|
634
|
+
<RefreshCw className="h-3 w-3" />
|
|
635
635
|
Reset
|
|
636
636
|
</button>
|
|
637
637
|
</div>
|
|
638
638
|
|
|
639
639
|
{/* Score */}
|
|
640
|
-
<div className="flex items-center gap-4
|
|
640
|
+
<div className="flex items-center gap-4 border-b border-[var(--border)] px-4 py-3">
|
|
641
641
|
<ScoreRing score={score} />
|
|
642
|
-
<div className="text-xs text-[var(--muted-foreground)]
|
|
642
|
+
<div className="space-y-0.5 text-xs text-[var(--muted-foreground)]">
|
|
643
643
|
<div className="flex items-center gap-1.5">
|
|
644
644
|
<CheckCircle2 className="h-3 w-3 text-green-500" /> {goodCount} passed
|
|
645
645
|
</div>
|
|
@@ -653,7 +653,7 @@ export function SEOPanel({
|
|
|
653
653
|
</div>
|
|
654
654
|
|
|
655
655
|
{/* Sections */}
|
|
656
|
-
<div className="
|
|
656
|
+
<div className="space-y-2 p-3">
|
|
657
657
|
{/* SEO Analysis */}
|
|
658
658
|
<Section
|
|
659
659
|
id="analysis"
|
|
@@ -672,7 +672,7 @@ export function SEOPanel({
|
|
|
672
672
|
<div key={c.id} className="flex items-start gap-2 py-1">
|
|
673
673
|
<StatusDot status={c.status} />
|
|
674
674
|
<div className="min-w-0">
|
|
675
|
-
<p className="text-sm text-[var(--foreground)]
|
|
675
|
+
<p className="text-sm leading-snug text-[var(--foreground)]">{c.label}</p>
|
|
676
676
|
<p className="text-xs text-[var(--muted-foreground)]">{c.detail}</p>
|
|
677
677
|
</div>
|
|
678
678
|
</div>
|
|
@@ -719,8 +719,8 @@ export function SEOPanel({
|
|
|
719
719
|
<span
|
|
720
720
|
className={
|
|
721
721
|
readability.passiveEstimate > 15
|
|
722
|
-
? 'text-amber-500
|
|
723
|
-
: 'text-green-500
|
|
722
|
+
? 'font-medium text-amber-500'
|
|
723
|
+
: 'font-medium text-green-500'
|
|
724
724
|
}
|
|
725
725
|
>
|
|
726
726
|
{readability.passiveEstimate}%
|
|
@@ -781,7 +781,7 @@ export function SEOPanel({
|
|
|
781
781
|
<div>
|
|
782
782
|
<label
|
|
783
783
|
id="robots-policy-label"
|
|
784
|
-
className="mb-1 block text-xs font-medium
|
|
784
|
+
className="text-muted-foreground mb-1 block text-xs font-medium"
|
|
785
785
|
>
|
|
786
786
|
Robots Policy
|
|
787
787
|
</label>
|
|
@@ -794,21 +794,21 @@ export function SEOPanel({
|
|
|
794
794
|
>
|
|
795
795
|
<Select.Trigger
|
|
796
796
|
aria-labelledby="robots-policy-label"
|
|
797
|
-
className="flex w-full items-center justify-between rounded-lg border
|
|
797
|
+
className="border-border bg-background text-foreground focus:ring-primary flex w-full items-center justify-between rounded-lg border px-3 py-1.5 text-sm focus:ring-2 focus:outline-none"
|
|
798
798
|
>
|
|
799
799
|
<Select.Value />
|
|
800
800
|
<Select.Icon>
|
|
801
|
-
<ChevronDown className="h-4 w-4
|
|
801
|
+
<ChevronDown className="text-muted-foreground h-4 w-4" />
|
|
802
802
|
</Select.Icon>
|
|
803
803
|
</Select.Trigger>
|
|
804
804
|
<Select.Portal>
|
|
805
|
-
<Select.Content className="z-50 overflow-hidden rounded-lg border
|
|
805
|
+
<Select.Content className="border-border bg-card z-50 overflow-hidden rounded-lg border shadow-md">
|
|
806
806
|
<Select.Viewport className="p-1">
|
|
807
807
|
{ROBOTS_POLICY_OPTIONS.map((option) => (
|
|
808
808
|
<Select.Item
|
|
809
809
|
key={option.value}
|
|
810
810
|
value={option.value}
|
|
811
|
-
className="relative flex cursor-pointer
|
|
811
|
+
className="text-foreground hover:bg-muted focus:bg-muted relative flex cursor-pointer items-center rounded-md py-1.5 pr-3 pl-8 text-sm outline-none select-none"
|
|
812
812
|
>
|
|
813
813
|
<Select.ItemIndicator className="absolute left-2 inline-flex items-center">
|
|
814
814
|
<Check className="h-4 w-4" />
|
|
@@ -820,7 +820,7 @@ export function SEOPanel({
|
|
|
820
820
|
</Select.Content>
|
|
821
821
|
</Select.Portal>
|
|
822
822
|
</Select.Root>
|
|
823
|
-
<p className="mt-1 text-xs
|
|
823
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
824
824
|
Use inheritance for most pages. Override only when a page needs different
|
|
825
825
|
index/follow behavior.
|
|
826
826
|
</p>
|
|
@@ -836,14 +836,14 @@ export function SEOPanel({
|
|
|
836
836
|
expanded={expandedSections.includes('preview')}
|
|
837
837
|
onToggle={toggleSection}
|
|
838
838
|
>
|
|
839
|
-
<div className="rounded-lg border border-[var(--border)]
|
|
840
|
-
<div className="text-sm text-blue-600 hover:underline
|
|
839
|
+
<div className="rounded-lg border border-[var(--border)] bg-[var(--background)] p-3">
|
|
840
|
+
<div className="line-clamp-1 cursor-pointer text-sm text-blue-600 hover:underline">
|
|
841
841
|
{metaTitle || title || 'Page Title'}
|
|
842
842
|
</div>
|
|
843
|
-
<div className="text-xs text-green-700
|
|
843
|
+
<div className="mt-1 truncate text-xs text-green-700">
|
|
844
844
|
{siteUrl}/{slug}
|
|
845
845
|
</div>
|
|
846
|
-
<div className="text-sm text-[var(--muted-foreground)]
|
|
846
|
+
<div className="mt-1 line-clamp-2 text-sm text-[var(--muted-foreground)]">
|
|
847
847
|
{metaDesc ||
|
|
848
848
|
'Add a meta description to see how this page will appear in search results.'}
|
|
849
849
|
</div>
|
|
@@ -880,8 +880,8 @@ export function SEOPanel({
|
|
|
880
880
|
type="url"
|
|
881
881
|
/>
|
|
882
882
|
|
|
883
|
-
<div className="border-t border-[var(--border)] pt-3
|
|
884
|
-
<p className="text-xs font-medium text-[var(--muted-foreground)]
|
|
883
|
+
<div className="mt-3 border-t border-[var(--border)] pt-3">
|
|
884
|
+
<p className="mb-2 text-xs font-medium text-[var(--muted-foreground)]">
|
|
885
885
|
Twitter / X Overrides
|
|
886
886
|
</p>
|
|
887
887
|
<div className="space-y-3">
|
|
@@ -910,31 +910,31 @@ export function SEOPanel({
|
|
|
910
910
|
|
|
911
911
|
{/* Social preview card */}
|
|
912
912
|
<div className="mt-3">
|
|
913
|
-
<p className="text-xs font-medium text-[var(--muted-foreground)]
|
|
913
|
+
<p className="mb-2 text-xs font-medium text-[var(--muted-foreground)]">
|
|
914
914
|
Social Preview
|
|
915
915
|
</p>
|
|
916
|
-
<div className="rounded-lg border border-[var(--border)]
|
|
916
|
+
<div className="overflow-hidden rounded-lg border border-[var(--border)] bg-[var(--muted)]">
|
|
917
917
|
{seoData.ogImage ? (
|
|
918
|
-
<div className="aspect-video
|
|
918
|
+
<div className="flex aspect-video items-center justify-center overflow-hidden bg-[var(--muted)]">
|
|
919
919
|
<img
|
|
920
920
|
src={seoData.ogImage}
|
|
921
921
|
alt="OG preview"
|
|
922
|
-
className="
|
|
922
|
+
className="h-full w-full object-cover"
|
|
923
923
|
/>
|
|
924
924
|
</div>
|
|
925
925
|
) : (
|
|
926
|
-
<div className="aspect-video bg-[var(--muted)]
|
|
926
|
+
<div className="flex aspect-video items-center justify-center bg-[var(--muted)] text-sm text-[var(--muted-foreground)]">
|
|
927
927
|
No OG image set
|
|
928
928
|
</div>
|
|
929
929
|
)}
|
|
930
930
|
<div className="p-3">
|
|
931
|
-
<div className="text-sm font-medium text-[var(--foreground)]
|
|
931
|
+
<div className="line-clamp-1 text-sm font-medium text-[var(--foreground)]">
|
|
932
932
|
{displayTitle}
|
|
933
933
|
</div>
|
|
934
|
-
<div className="text-xs text-[var(--muted-foreground)]
|
|
934
|
+
<div className="mt-1 line-clamp-2 text-xs text-[var(--muted-foreground)]">
|
|
935
935
|
{displayDesc.slice(0, 100)}
|
|
936
936
|
</div>
|
|
937
|
-
<div className="text-xs text-[var(--muted-foreground)]
|
|
937
|
+
<div className="mt-1 truncate text-xs text-[var(--muted-foreground)]">
|
|
938
938
|
{siteUrl}
|
|
939
939
|
</div>
|
|
940
940
|
</div>
|
|
@@ -959,19 +959,19 @@ export function SEOPanel({
|
|
|
959
959
|
onChange={(v) => update({ isCornerstone: v })}
|
|
960
960
|
/>
|
|
961
961
|
{seoData.isCornerstone && (
|
|
962
|
-
<div className="flex items-center gap-1.5
|
|
962
|
+
<div className="flex items-center gap-1.5 rounded-md bg-amber-50 px-2.5 py-1.5 text-xs text-amber-600">
|
|
963
963
|
<Star className="h-3.5 w-3.5" />
|
|
964
964
|
Cornerstone content is held to stricter SEO standards
|
|
965
965
|
</div>
|
|
966
966
|
)}
|
|
967
967
|
<div>
|
|
968
|
-
<label className="block text-xs font-medium text-[var(--muted-foreground)]
|
|
968
|
+
<label className="mb-1 block text-xs font-medium text-[var(--muted-foreground)]">
|
|
969
969
|
Schema Type
|
|
970
970
|
</label>
|
|
971
971
|
<select
|
|
972
972
|
value={seoData.schemaType ?? 'Article'}
|
|
973
973
|
onChange={(e) => update({ schemaType: e.target.value })}
|
|
974
|
-
className="w-full
|
|
974
|
+
className="w-full rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-1.5 text-sm text-[var(--foreground)] focus:ring-2 focus:ring-[var(--primary)] focus:outline-none"
|
|
975
975
|
>
|
|
976
976
|
{SCHEMA_TYPES.map((t) => (
|
|
977
977
|
<option key={t} value={t}>
|
|
@@ -27,7 +27,7 @@ function ScoreBadge({ score }: { score: number }) {
|
|
|
27
27
|
const bg = score >= 80 ? 'bg-green-50' : score >= 60 ? 'bg-amber-50' : 'bg-red-50'
|
|
28
28
|
return (
|
|
29
29
|
<span
|
|
30
|
-
className={`inline-flex items-center justify-center
|
|
30
|
+
className={`inline-flex h-9 w-9 items-center justify-center rounded-full text-sm font-semibold ${color} ${bg}`}
|
|
31
31
|
>
|
|
32
32
|
{score}
|
|
33
33
|
</span>
|
|
@@ -53,10 +53,10 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
|
|
|
53
53
|
const visible = topContent.slice(page * perPage, (page + 1) * perPage)
|
|
54
54
|
|
|
55
55
|
return (
|
|
56
|
-
<div className="
|
|
57
|
-
<div className="
|
|
56
|
+
<div className="rounded-lg border border-gray-200 bg-white">
|
|
57
|
+
<div className="flex items-center justify-between border-b border-gray-200 p-4">
|
|
58
58
|
<div className="flex items-center gap-2">
|
|
59
|
-
<Search className="
|
|
59
|
+
<Search className="h-4 w-4 text-gray-500" />
|
|
60
60
|
<h2 className="text-sm font-semibold text-gray-900">SEO Performance</h2>
|
|
61
61
|
</div>
|
|
62
62
|
{totalIssues > 0 && (
|
|
@@ -66,25 +66,25 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
|
|
|
66
66
|
)}
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
|
-
<div className="grid grid-cols-1
|
|
70
|
-
<div className="lg:col-span-7
|
|
71
|
-
<h3 className="text-sm font-medium text-gray-700
|
|
69
|
+
<div className="grid grid-cols-1 divide-y divide-gray-200 lg:grid-cols-12 lg:divide-x lg:divide-y-0">
|
|
70
|
+
<div className="p-4 lg:col-span-7">
|
|
71
|
+
<h3 className="mb-3 text-sm font-medium text-gray-700">Top Performing Content</h3>
|
|
72
72
|
<div className="space-y-2">
|
|
73
73
|
{visible.map((item) => (
|
|
74
74
|
<div key={item.id} className="flex items-center justify-between py-2">
|
|
75
|
-
<div className="
|
|
76
|
-
<p className="text-sm font-medium text-gray-900
|
|
75
|
+
<div className="mr-3 min-w-0 flex-1">
|
|
76
|
+
<p className="truncate text-sm font-medium text-gray-900">{item.title}</p>
|
|
77
77
|
<p className="text-xs text-gray-500 capitalize">{item.collection}</p>
|
|
78
78
|
</div>
|
|
79
79
|
<ScoreBadge score={item.score} />
|
|
80
80
|
</div>
|
|
81
81
|
))}
|
|
82
82
|
{visible.length === 0 && (
|
|
83
|
-
<p className="text-sm text-gray-400
|
|
83
|
+
<p className="py-4 text-center text-sm text-gray-400">No published content yet</p>
|
|
84
84
|
)}
|
|
85
85
|
</div>
|
|
86
86
|
{topContent.length > perPage && (
|
|
87
|
-
<div className="flex items-center justify-between
|
|
87
|
+
<div className="mt-3 flex items-center justify-between border-t border-gray-100 pt-3 text-xs text-gray-500">
|
|
88
88
|
<span>
|
|
89
89
|
Showing {page * perPage + 1}-{Math.min((page + 1) * perPage, topContent.length)} of{' '}
|
|
90
90
|
{topContent.length}
|
|
@@ -93,9 +93,9 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
|
|
|
93
93
|
<button
|
|
94
94
|
onClick={() => setPage(Math.max(0, page - 1))}
|
|
95
95
|
disabled={page === 0}
|
|
96
|
-
className="p-1
|
|
96
|
+
className="rounded p-1 hover:bg-gray-100 disabled:opacity-30"
|
|
97
97
|
>
|
|
98
|
-
<ChevronLeft className="
|
|
98
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
99
99
|
</button>
|
|
100
100
|
<span>
|
|
101
101
|
Page {page + 1} of {totalPages}
|
|
@@ -103,21 +103,21 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
|
|
|
103
103
|
<button
|
|
104
104
|
onClick={() => setPage(Math.min(totalPages - 1, page + 1))}
|
|
105
105
|
disabled={page >= totalPages - 1}
|
|
106
|
-
className="p-1
|
|
106
|
+
className="rounded p-1 hover:bg-gray-100 disabled:opacity-30"
|
|
107
107
|
>
|
|
108
|
-
<ChevronRight className="
|
|
108
|
+
<ChevronRight className="h-3.5 w-3.5" />
|
|
109
109
|
</button>
|
|
110
110
|
</div>
|
|
111
111
|
</div>
|
|
112
112
|
)}
|
|
113
113
|
</div>
|
|
114
114
|
|
|
115
|
-
<div className="lg:col-span-5
|
|
116
|
-
<h3 className="text-sm font-medium text-gray-700
|
|
115
|
+
<div className="p-4 lg:col-span-5">
|
|
116
|
+
<h3 className="mb-3 text-sm font-medium text-gray-700">SEO Issues</h3>
|
|
117
117
|
<div className="space-y-3">
|
|
118
118
|
<div className="flex items-center justify-between">
|
|
119
119
|
<div className="flex items-center gap-2">
|
|
120
|
-
<FileWarning className="
|
|
120
|
+
<FileWarning className="h-4 w-4 text-red-400" />
|
|
121
121
|
<span className="text-sm text-gray-700">Missing meta descriptions</span>
|
|
122
122
|
</div>
|
|
123
123
|
<span className="text-sm font-semibold text-gray-900">
|
|
@@ -126,7 +126,7 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
|
|
|
126
126
|
</div>
|
|
127
127
|
<div className="flex items-center justify-between">
|
|
128
128
|
<div className="flex items-center gap-2">
|
|
129
|
-
<LinkIcon className="
|
|
129
|
+
<LinkIcon className="h-4 w-4 text-red-400" />
|
|
130
130
|
<span className="text-sm text-gray-700">Broken internal links</span>
|
|
131
131
|
</div>
|
|
132
132
|
<span className="text-sm font-semibold text-gray-900">
|
|
@@ -135,7 +135,7 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
|
|
|
135
135
|
</div>
|
|
136
136
|
<div className="flex items-center justify-between">
|
|
137
137
|
<div className="flex items-center gap-2">
|
|
138
|
-
<ImageOff className="
|
|
138
|
+
<ImageOff className="h-4 w-4 text-red-400" />
|
|
139
139
|
<span className="text-sm text-gray-700">Missing alt text</span>
|
|
140
140
|
</div>
|
|
141
141
|
<span className="text-sm font-semibold text-gray-900">{issues.missingAltText}</span>
|
|
@@ -144,7 +144,7 @@ export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
|
|
|
144
144
|
{totalIssues > 0 && (
|
|
145
145
|
<button
|
|
146
146
|
onClick={() => onNavigate?.('/seo')}
|
|
147
|
-
className="mt-4 text-sm text-blue-600 hover:text-blue-700
|
|
147
|
+
className="mt-4 text-sm font-medium text-blue-600 hover:text-blue-700"
|
|
148
148
|
>
|
|
149
149
|
View All Issues
|
|
150
150
|
</button>
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { Calendar, Clock, Loader2, X } from 'lucide-react'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import { cmsApi } from '../lib/api.js'
|
|
7
|
+
|
|
8
|
+
export interface SchedulePublishDialogProps {
|
|
9
|
+
collectionSlug: string
|
|
10
|
+
documentId: string
|
|
11
|
+
/** Existing scheduled publish time, if any. */
|
|
12
|
+
scheduledAt?: string | null
|
|
13
|
+
/** Existing scheduled unpublish time, if any. */
|
|
14
|
+
scheduledUnpublishAt?: string | null
|
|
15
|
+
open: boolean
|
|
16
|
+
onClose: () => void
|
|
17
|
+
/** Called with the updated schedule fields after a successful save. */
|
|
18
|
+
onScheduled?: (next: {
|
|
19
|
+
status: string
|
|
20
|
+
scheduledAt: string | null
|
|
21
|
+
scheduledUnpublishAt: string | null
|
|
22
|
+
}) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert a Date or ISO string to the `YYYY-MM-DDTHH:mm` shape that the
|
|
27
|
+
* `<input type="datetime-local">` element expects, in the user's local
|
|
28
|
+
* timezone (the input is timezone-naive but we present + parse it as local).
|
|
29
|
+
*/
|
|
30
|
+
function toLocalDateTimeInput(value: string | Date | null | undefined): string {
|
|
31
|
+
if (!value) return ''
|
|
32
|
+
const d = typeof value === 'string' ? new Date(value) : value
|
|
33
|
+
if (Number.isNaN(d.getTime())) return ''
|
|
34
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
35
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Round "now + 1 hour" to the next quarter-hour for a friendly default. */
|
|
39
|
+
function defaultScheduleTime(): string {
|
|
40
|
+
const d = new Date(Date.now() + 60 * 60 * 1000)
|
|
41
|
+
d.setMinutes(Math.ceil(d.getMinutes() / 15) * 15, 0, 0)
|
|
42
|
+
return toLocalDateTimeInput(d)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function SchedulePublishDialog({
|
|
46
|
+
collectionSlug,
|
|
47
|
+
documentId,
|
|
48
|
+
scheduledAt,
|
|
49
|
+
scheduledUnpublishAt,
|
|
50
|
+
open,
|
|
51
|
+
onClose,
|
|
52
|
+
onScheduled,
|
|
53
|
+
}: SchedulePublishDialogProps) {
|
|
54
|
+
const [publishAt, setPublishAt] = useState('')
|
|
55
|
+
const [unpublishAt, setUnpublishAt] = useState('')
|
|
56
|
+
const [includeUnpublish, setIncludeUnpublish] = useState(false)
|
|
57
|
+
const [saving, setSaving] = useState(false)
|
|
58
|
+
const [cancelling, setCancelling] = useState(false)
|
|
59
|
+
|
|
60
|
+
const hasExistingSchedule = Boolean(scheduledAt || scheduledUnpublishAt)
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!open) return
|
|
64
|
+
setPublishAt(scheduledAt ? toLocalDateTimeInput(scheduledAt) : defaultScheduleTime())
|
|
65
|
+
setUnpublishAt(scheduledUnpublishAt ? toLocalDateTimeInput(scheduledUnpublishAt) : '')
|
|
66
|
+
setIncludeUnpublish(Boolean(scheduledUnpublishAt))
|
|
67
|
+
}, [open, scheduledAt, scheduledUnpublishAt])
|
|
68
|
+
|
|
69
|
+
async function handleSave() {
|
|
70
|
+
if (!publishAt && !(includeUnpublish && unpublishAt)) {
|
|
71
|
+
toast.error('Pick a publish or unpublish time')
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// datetime-local is parsed by the browser as local time; new Date() on
|
|
76
|
+
// that string respects the user's timezone and serialises to a UTC
|
|
77
|
+
// ISO string the server can compare against `Date.now()` cleanly.
|
|
78
|
+
const publishDate = publishAt ? new Date(publishAt) : null
|
|
79
|
+
const unpublishDate = includeUnpublish && unpublishAt ? new Date(unpublishAt) : null
|
|
80
|
+
|
|
81
|
+
if (publishDate && publishDate.getTime() <= Date.now()) {
|
|
82
|
+
toast.error('Publish time must be in the future')
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
if (unpublishDate && unpublishDate.getTime() <= Date.now()) {
|
|
86
|
+
toast.error('Unpublish time must be in the future')
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if (publishDate && unpublishDate && unpublishDate <= publishDate) {
|
|
90
|
+
toast.error('Unpublish time must be after publish time')
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setSaving(true)
|
|
95
|
+
const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}/schedule`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
scheduledAt: publishDate ? publishDate.toISOString() : null,
|
|
99
|
+
scheduledUnpublishAt: unpublishDate ? unpublishDate.toISOString() : null,
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
setSaving(false)
|
|
103
|
+
if (res.error) {
|
|
104
|
+
toast.error(res.error)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
toast.success(publishDate ? 'Publish scheduled' : 'Unpublish scheduled')
|
|
108
|
+
onScheduled?.({
|
|
109
|
+
status: res.data?.status ?? 'SCHEDULED',
|
|
110
|
+
scheduledAt: res.data?.scheduledAt ?? (publishDate ? publishDate.toISOString() : null),
|
|
111
|
+
scheduledUnpublishAt:
|
|
112
|
+
res.data?.scheduledUnpublishAt ?? (unpublishDate ? unpublishDate.toISOString() : null),
|
|
113
|
+
})
|
|
114
|
+
onClose()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function handleCancelSchedule() {
|
|
118
|
+
setCancelling(true)
|
|
119
|
+
const res = await cmsApi<any>(`/collections/${collectionSlug}/${documentId}/schedule`, {
|
|
120
|
+
method: 'DELETE',
|
|
121
|
+
})
|
|
122
|
+
setCancelling(false)
|
|
123
|
+
if (res.error) {
|
|
124
|
+
toast.error(res.error)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
toast.success('Schedule cancelled')
|
|
128
|
+
onScheduled?.({
|
|
129
|
+
status: res.data?.status ?? 'DRAFT',
|
|
130
|
+
scheduledAt: null,
|
|
131
|
+
scheduledUnpublishAt: null,
|
|
132
|
+
})
|
|
133
|
+
onClose()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!open) return null
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
140
|
+
<div className="fixed inset-0 bg-black/40" onClick={onClose} />
|
|
141
|
+
<div className="relative w-full max-w-md rounded-lg bg-white shadow-xl">
|
|
142
|
+
<div className="flex items-center justify-between border-b border-gray-200 px-4 py-3">
|
|
143
|
+
<div className="flex items-center gap-2">
|
|
144
|
+
<Calendar className="h-5 w-5 text-gray-600" />
|
|
145
|
+
<h2 className="text-lg font-semibold text-gray-900">Schedule publishing</h2>
|
|
146
|
+
</div>
|
|
147
|
+
<button
|
|
148
|
+
onClick={onClose}
|
|
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
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
<p className="text-xs text-gray-500">
|
|
169
|
+
The document will move from DRAFT to PUBLISHED at this time (your local timezone).
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<label className="flex items-center gap-2 text-sm text-gray-700">
|
|
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 && (
|
|
184
|
+
<div className="space-y-1.5">
|
|
185
|
+
<label className="text-sm font-medium text-gray-700">Unpublish at</label>
|
|
186
|
+
<div className="relative">
|
|
187
|
+
<Clock className="pointer-events-none absolute top-2.5 left-2.5 h-4 w-4 text-gray-400" />
|
|
188
|
+
<input
|
|
189
|
+
type="datetime-local"
|
|
190
|
+
value={unpublishAt}
|
|
191
|
+
onChange={(e) => setUnpublishAt(e.target.value)}
|
|
192
|
+
min={publishAt || toLocalDateTimeInput(new Date(Date.now() + 60 * 1000))}
|
|
193
|
+
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"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</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
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|