@actuate-media/cms-admin 0.66.0 → 0.68.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/CHANGELOG.md +37 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +36 -9
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/components/inspector-pane.test.d.ts +2 -0
- package/dist/__tests__/components/inspector-pane.test.d.ts.map +1 -0
- package/dist/__tests__/components/inspector-pane.test.js +31 -0
- package/dist/__tests__/components/inspector-pane.test.js.map +1 -0
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts +2 -0
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts.map +1 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js +64 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts +2 -0
- package/dist/__tests__/lib/pages-tree.test.d.ts.map +1 -0
- package/dist/__tests__/lib/pages-tree.test.js +173 -0
- package/dist/__tests__/lib/pages-tree.test.js.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.js +88 -0
- package/dist/__tests__/lib/post-types-service.test.js.map +1 -0
- package/dist/__tests__/views/collection-list-nav.render.test.js +43 -5
- package/dist/__tests__/views/collection-list-nav.render.test.js.map +1 -1
- package/dist/__tests__/views/dashboard.test.js +7 -3
- package/dist/__tests__/views/dashboard.test.js.map +1 -1
- package/dist/__tests__/views/forms-list.render.test.js +25 -6
- package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts +2 -0
- package/dist/__tests__/views/media-seo-audit.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js +102 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts +2 -0
- package/dist/__tests__/views/onboarding-block.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/onboarding-block.render.test.js +146 -0
- package/dist/__tests__/views/onboarding-block.render.test.js.map +1 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js +121 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.js +105 -0
- package/dist/__tests__/views/page-templates.render.test.js.map +1 -0
- package/dist/__tests__/views/pages-list-view.test.js +77 -1
- package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
- package/dist/__tests__/views/post-type-editor.render.test.d.ts +2 -0
- package/dist/__tests__/views/post-type-editor.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/post-type-editor.render.test.js +102 -0
- package/dist/__tests__/views/post-type-editor.render.test.js.map +1 -0
- package/dist/__tests__/views/security-settings.render.test.js +85 -4
- package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js +117 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/collections/EntryDetailPane.d.ts +10 -0
- package/dist/components/collections/EntryDetailPane.d.ts.map +1 -0
- package/dist/components/collections/EntryDetailPane.js +92 -0
- package/dist/components/collections/EntryDetailPane.js.map +1 -0
- package/dist/components/forms/FieldsPanel.js +1 -1
- package/dist/components/forms/FieldsPanel.js.map +1 -1
- package/dist/components/forms/FormSchemaPane.d.ts +8 -0
- package/dist/components/forms/FormSchemaPane.d.ts.map +1 -0
- package/dist/components/forms/FormSchemaPane.js +95 -0
- package/dist/components/forms/FormSchemaPane.js.map +1 -0
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts +26 -0
- package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js +160 -0
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.d.ts +9 -0
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -0
- package/dist/components/seo/SeoEditorPane.js +175 -0
- package/dist/components/seo/SeoEditorPane.js.map +1 -0
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/components/ui/Button.d.ts +1 -1
- package/dist/components/ui/InspectorPane.d.ts +51 -0
- package/dist/components/ui/InspectorPane.d.ts.map +1 -0
- package/dist/components/ui/InspectorPane.js +42 -0
- package/dist/components/ui/InspectorPane.js.map +1 -0
- package/dist/components/ui/StatusBadge.d.ts +1 -1
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/hooks/useIdleLogout.d.ts +18 -0
- package/dist/hooks/useIdleLogout.d.ts.map +1 -0
- package/dist/hooks/useIdleLogout.js +66 -0
- package/dist/hooks/useIdleLogout.js.map +1 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +7 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +11 -2
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +8 -2
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/pages-tree.d.ts +44 -0
- package/dist/lib/pages-tree.d.ts.map +1 -0
- package/dist/lib/pages-tree.js +106 -0
- package/dist/lib/pages-tree.js.map +1 -0
- package/dist/lib/post-types-service.d.ts +44 -0
- package/dist/lib/post-types-service.d.ts.map +1 -0
- package/dist/lib/post-types-service.js +83 -0
- package/dist/lib/post-types-service.js.map +1 -0
- package/dist/views/CollectionList.d.ts.map +1 -1
- package/dist/views/CollectionList.js +23 -4
- package/dist/views/CollectionList.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +31 -2
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js +25 -21
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +28 -2
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/MediaSeoAudit.d.ts +25 -0
- package/dist/views/MediaSeoAudit.d.ts.map +1 -0
- package/dist/views/MediaSeoAudit.js +97 -0
- package/dist/views/MediaSeoAudit.js.map +1 -0
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +90 -10
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages/PagesTreeTable.d.ts +35 -0
- package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -0
- package/dist/views/Pages/PagesTreeTable.js +28 -0
- package/dist/views/Pages/PagesTreeTable.js.map +1 -0
- package/dist/views/Posts/PostTypeEditor.d.ts +16 -0
- package/dist/views/Posts/PostTypeEditor.d.ts.map +1 -0
- package/dist/views/Posts/PostTypeEditor.js +159 -0
- package/dist/views/Posts/PostTypeEditor.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +3 -2
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
- package/dist/views/Posts/PostTypesView.js +1 -1
- package/dist/views/Posts/PostTypesView.js.map +1 -1
- package/dist/views/ScriptTags.d.ts +5 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +32 -7
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/dashboard/OnboardingBlock.d.ts +29 -0
- package/dist/views/dashboard/OnboardingBlock.d.ts.map +1 -0
- package/dist/views/dashboard/OnboardingBlock.js +71 -0
- package/dist/views/dashboard/OnboardingBlock.js.map +1 -0
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts +20 -0
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +1 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js +105 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js.map +1 -0
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +51 -2
- package/dist/views/page-builder/PageBuilder.js.map +1 -1
- package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
- package/dist/views/page-builder/PageTemplates.js +113 -6
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts +7 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +3 -3
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
- package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
- package/dist/views/page-editor/PageSettingsModal.js +42 -3
- package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
- package/dist/views/seo/ContentTab.d.ts.map +1 -1
- package/dist/views/seo/ContentTab.js +22 -6
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/settings/AccessProtectionCard.d.ts +5 -1
- package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -1
- package/dist/views/settings/AccessProtectionCard.js +13 -10
- package/dist/views/settings/AccessProtectionCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +3 -3
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/dist/views/settings/SessionPolicyCard.d.ts +1 -2
- package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -1
- package/dist/views/settings/SessionPolicyCard.js +12 -5
- package/dist/views/settings/SessionPolicyCard.js.map +1 -1
- package/dist/views/settings/useSecuritySettings.d.ts +13 -1
- package/dist/views/settings/useSecuritySettings.d.ts.map +1 -1
- package/dist/views/settings/useSecuritySettings.js +47 -4
- package/dist/views/settings/useSecuritySettings.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +43 -10
- package/src/__tests__/components/inspector-pane.test.tsx +64 -0
- package/src/__tests__/hooks/useIdleLogout.test.tsx +86 -0
- package/src/__tests__/lib/pages-tree.test.ts +197 -0
- package/src/__tests__/lib/post-types-service.test.ts +112 -0
- package/src/__tests__/views/collection-list-nav.render.test.tsx +51 -5
- package/src/__tests__/views/dashboard.test.tsx +7 -3
- package/src/__tests__/views/forms-list.render.test.tsx +34 -6
- package/src/__tests__/views/media-seo-audit.render.test.tsx +117 -0
- package/src/__tests__/views/onboarding-block.render.test.tsx +193 -0
- package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +138 -0
- package/src/__tests__/views/page-templates.render.test.tsx +142 -0
- package/src/__tests__/views/pages-list-view.test.tsx +93 -1
- package/src/__tests__/views/post-type-editor.render.test.tsx +116 -0
- package/src/__tests__/views/security-settings.render.test.tsx +111 -4
- package/src/__tests__/views/seo-content-pane.render.test.tsx +142 -0
- package/src/components/collections/EntryDetailPane.tsx +182 -0
- package/src/components/forms/FieldsPanel.tsx +3 -1
- package/src/components/forms/FormSchemaPane.tsx +192 -0
- package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
- package/src/components/seo/SeoEditorPane.tsx +533 -0
- package/src/components/ui/InspectorPane.tsx +129 -0
- package/src/components/ui/index.ts +10 -0
- package/src/hooks/useIdleLogout.ts +77 -0
- package/src/lib/api.ts +8 -2
- package/src/lib/page-editor-service.ts +16 -2
- package/src/lib/pages-tree.ts +145 -0
- package/src/lib/post-types-service.ts +129 -0
- package/src/views/CollectionList.tsx +158 -110
- package/src/views/Dashboard.tsx +238 -196
- package/src/views/Forms.tsx +154 -126
- package/src/views/MediaBrowser.tsx +641 -511
- package/src/views/MediaSeoAudit.tsx +320 -0
- package/src/views/Pages/PagesListView.tsx +352 -183
- package/src/views/Pages/PagesTreeTable.tsx +339 -0
- package/src/views/Posts/PostTypeEditor.tsx +568 -0
- package/src/views/Posts/PostTypesView.tsx +5 -4
- package/src/views/ScriptTags.tsx +204 -129
- package/src/views/dashboard/OnboardingBlock.tsx +233 -0
- package/src/views/page-builder/BuilderSectionsPanel.tsx +304 -0
- package/src/views/page-builder/PageBuilder.tsx +69 -2
- package/src/views/page-builder/PageTemplates.tsx +340 -57
- package/src/views/page-editor/PageSectionEditor.tsx +9 -2
- package/src/views/page-editor/PageSettingsModal.tsx +75 -2
- package/src/views/seo/ContentTab.tsx +113 -80
- package/src/views/settings/AccessProtectionCard.tsx +83 -53
- package/src/views/settings/SecurityTab.tsx +23 -2
- package/src/views/settings/SessionPolicyCard.tsx +45 -9
- package/src/views/settings/useSecuritySettings.ts +78 -22
- package/dist/components/forms/FormSchemaDrawer.d.ts +0 -9
- package/dist/components/forms/FormSchemaDrawer.d.ts.map +0 -1
- package/dist/components/forms/FormSchemaDrawer.js +0 -99
- package/dist/components/forms/FormSchemaDrawer.js.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.d.ts +0 -8
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.js +0 -172
- package/dist/components/seo/SeoEditorDrawer.js.map +0 -1
- package/dist/views/ScriptTagEditor.d.ts +0 -6
- package/dist/views/ScriptTagEditor.d.ts.map +0 -1
- package/dist/views/ScriptTagEditor.js +0 -111
- package/dist/views/ScriptTagEditor.js.map +0 -1
- package/src/components/forms/FormSchemaDrawer.tsx +0 -192
- package/src/components/seo/SeoEditorDrawer.tsx +0 -529
- package/src/views/ScriptTagEditor.tsx +0 -353
|
@@ -32,6 +32,7 @@ import { cmsApi } from '../lib/api.js'
|
|
|
32
32
|
import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
|
|
33
33
|
import { StatusBadge } from '../components/ui/StatusBadge.js'
|
|
34
34
|
import { StatPill } from '../components/ui/StatPill.js'
|
|
35
|
+
import { MediaSeoAudit, type MediaSeoAuditItem } from './MediaSeoAudit.js'
|
|
35
36
|
|
|
36
37
|
interface MediaItem {
|
|
37
38
|
id: number | string
|
|
@@ -45,6 +46,11 @@ interface MediaItem {
|
|
|
45
46
|
format?: string
|
|
46
47
|
altTag?: string
|
|
47
48
|
title?: string
|
|
49
|
+
caption?: string
|
|
50
|
+
description?: string
|
|
51
|
+
focusKeyword?: string
|
|
52
|
+
/** Derived server-side by cms-core's `calculateMediaSeoScore()`. */
|
|
53
|
+
seoScore?: number
|
|
48
54
|
usedOn?: { page: string; path: string }[]
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -119,6 +125,7 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
119
125
|
'/media?pageSize=1&folderId=none',
|
|
120
126
|
)
|
|
121
127
|
|
|
128
|
+
const [activeTab, setActiveTab] = useState<'library' | 'audit'>('library')
|
|
122
129
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
|
123
130
|
const [searchQuery, setSearchQuery] = useState('')
|
|
124
131
|
const [filterType, setFilterType] = useState('all')
|
|
@@ -129,6 +136,9 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
129
136
|
const [editAlt, setEditAlt] = useState('')
|
|
130
137
|
const [editTitle, setEditTitle] = useState('')
|
|
131
138
|
const [editFilename, setEditFilename] = useState('')
|
|
139
|
+
const [editCaption, setEditCaption] = useState('')
|
|
140
|
+
const [editDescription, setEditDescription] = useState('')
|
|
141
|
+
const [editFocusKeyword, setEditFocusKeyword] = useState('')
|
|
132
142
|
const [saving, setSaving] = useState(false)
|
|
133
143
|
const [aiGenerating, setAiGenerating] = useState<string | null>(null)
|
|
134
144
|
const [uploading, setUploading] = useState(false)
|
|
@@ -180,6 +190,22 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
180
190
|
setEditAlt(item.altTag ?? '')
|
|
181
191
|
setEditTitle(item.title ?? '')
|
|
182
192
|
setEditFilename(item.name)
|
|
193
|
+
setEditCaption(item.caption ?? '')
|
|
194
|
+
setEditDescription(item.description ?? '')
|
|
195
|
+
setEditFocusKeyword(item.focusKeyword ?? '')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Count of items missing alt OR title — drives the SEO Audit tab badge.
|
|
199
|
+
const auditAttentionCount = useMemo(
|
|
200
|
+
() => mediaItems.filter((item) => !item.altTag?.trim() || !item.title?.trim()).length,
|
|
201
|
+
[mediaItems],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const handleAuditEdit = (auditItem: MediaSeoAuditItem) => {
|
|
205
|
+
const item = mediaItems.find((m) => m.id === auditItem.id)
|
|
206
|
+
if (!item) return
|
|
207
|
+
setActiveTab('library')
|
|
208
|
+
openDetail(item)
|
|
183
209
|
}
|
|
184
210
|
|
|
185
211
|
const closeDetail = () => {
|
|
@@ -208,6 +234,9 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
208
234
|
alt: editAlt,
|
|
209
235
|
title: editTitle,
|
|
210
236
|
filename: editFilename,
|
|
237
|
+
caption: editCaption,
|
|
238
|
+
description: editDescription,
|
|
239
|
+
focusKeyword: editFocusKeyword,
|
|
211
240
|
}),
|
|
212
241
|
})
|
|
213
242
|
setSaving(false)
|
|
@@ -471,585 +500,686 @@ export function MediaBrowser({ onNavigate }: MediaBrowserProps) {
|
|
|
471
500
|
</div>
|
|
472
501
|
</div>
|
|
473
502
|
|
|
474
|
-
<div
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
503
|
+
<div
|
|
504
|
+
role="tablist"
|
|
505
|
+
aria-label="Media views"
|
|
506
|
+
className="mb-4 flex border-b border-[var(--bdr)]"
|
|
507
|
+
>
|
|
508
|
+
<button
|
|
509
|
+
type="button"
|
|
510
|
+
role="tab"
|
|
511
|
+
aria-selected={activeTab === 'library'}
|
|
512
|
+
onClick={() => setActiveTab('library')}
|
|
513
|
+
className={`-mb-px flex items-center gap-1.5 border-b-2 px-4 py-2.5 text-[13px] font-medium transition-colors focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none ${
|
|
514
|
+
activeTab === 'library'
|
|
515
|
+
? 'border-[var(--acc)] text-[var(--acc)]'
|
|
516
|
+
: 'border-transparent text-[var(--muted)] hover:text-[var(--txt)]'
|
|
517
|
+
}`}
|
|
518
|
+
>
|
|
519
|
+
<ImageIcon size={14} aria-hidden />
|
|
520
|
+
Library
|
|
521
|
+
</button>
|
|
522
|
+
<button
|
|
523
|
+
type="button"
|
|
524
|
+
role="tab"
|
|
525
|
+
aria-selected={activeTab === 'audit'}
|
|
526
|
+
onClick={() => setActiveTab('audit')}
|
|
527
|
+
className={`-mb-px flex items-center gap-1.5 border-b-2 px-4 py-2.5 text-[13px] font-medium transition-colors focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:outline-none ${
|
|
528
|
+
activeTab === 'audit'
|
|
529
|
+
? 'border-[var(--acc)] text-[var(--acc)]'
|
|
530
|
+
: 'border-transparent text-[var(--muted)] hover:text-[var(--txt)]'
|
|
531
|
+
}`}
|
|
532
|
+
>
|
|
533
|
+
<Search size={14} aria-hidden />
|
|
534
|
+
SEO Audit
|
|
535
|
+
<span
|
|
536
|
+
className={`rounded-[4px] px-[5px] py-px text-[10px] font-semibold ${
|
|
537
|
+
auditAttentionCount === 0
|
|
538
|
+
? 'bg-[var(--suc-l)] text-[var(--suc)]'
|
|
539
|
+
: 'bg-[var(--err-l)] text-[var(--err)]'
|
|
540
|
+
}`}
|
|
541
|
+
aria-label={`${auditAttentionCount} items need attention`}
|
|
542
|
+
>
|
|
543
|
+
{auditAttentionCount}
|
|
544
|
+
</span>
|
|
545
|
+
</button>
|
|
546
|
+
</div>
|
|
490
547
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
className="border-input focus:ring-ring rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
509
|
-
>
|
|
510
|
-
<option value="all">All Types</option>
|
|
511
|
-
<option value="image">Images</option>
|
|
512
|
-
<option value="video">Videos</option>
|
|
513
|
-
<option value="document">Documents</option>
|
|
514
|
-
</select>
|
|
515
|
-
</div>
|
|
516
|
-
<div className="bg-muted flex items-center gap-1 rounded-lg p-1">
|
|
517
|
-
<button
|
|
518
|
-
onClick={() => setViewMode('grid')}
|
|
519
|
-
className={`rounded p-1.5 transition-colors ${viewMode === 'grid' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
520
|
-
>
|
|
521
|
-
<Grid3x3 className="h-4 w-4" />
|
|
522
|
-
</button>
|
|
523
|
-
<button
|
|
524
|
-
onClick={() => setViewMode('list')}
|
|
525
|
-
className={`rounded p-1.5 transition-colors ${viewMode === 'list' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
526
|
-
>
|
|
527
|
-
<List className="h-4 w-4" />
|
|
528
|
-
</button>
|
|
529
|
-
</div>
|
|
548
|
+
{activeTab === 'audit' ? (
|
|
549
|
+
<MediaSeoAudit items={mediaItems} onEdit={handleAuditEdit} />
|
|
550
|
+
) : (
|
|
551
|
+
<div className="flex min-h-0 flex-1 gap-4 overflow-hidden">
|
|
552
|
+
{sidebarOpen && (
|
|
553
|
+
<div className="border-border bg-card flex w-56 shrink-0 flex-col overflow-hidden rounded-lg border">
|
|
554
|
+
<FolderTree
|
|
555
|
+
scope="media"
|
|
556
|
+
selected={folderSel}
|
|
557
|
+
onSelect={(sel) => {
|
|
558
|
+
setFolderSel(sel)
|
|
559
|
+
setSelectedMedia([])
|
|
560
|
+
}}
|
|
561
|
+
totalCount={allData.data?.total}
|
|
562
|
+
uncategorizedCount={uncatData.data?.total}
|
|
563
|
+
onDropItem={handleDropItem}
|
|
564
|
+
/>
|
|
530
565
|
</div>
|
|
531
|
-
|
|
566
|
+
)}
|
|
532
567
|
|
|
533
|
-
|
|
534
|
-
<div className="border-
|
|
535
|
-
<div className="flex items-center justify-between">
|
|
536
|
-
<
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
568
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
569
|
+
<div className="border-border bg-card mb-4 rounded-lg border">
|
|
570
|
+
<div className="flex items-center justify-between p-3">
|
|
571
|
+
<div className="flex flex-1 items-center gap-3">
|
|
572
|
+
<div className="relative max-w-md flex-1">
|
|
573
|
+
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
|
|
574
|
+
<input
|
|
575
|
+
type="text"
|
|
576
|
+
placeholder="Search media..."
|
|
577
|
+
value={searchQuery}
|
|
578
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
579
|
+
className="border-input focus:ring-ring w-full rounded-lg border py-2 pr-3 pl-9 text-sm focus:ring-2 focus:outline-none"
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
<select
|
|
583
|
+
value={filterType}
|
|
584
|
+
onChange={(e) => setFilterType(e.target.value)}
|
|
585
|
+
className="border-input focus:ring-ring rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
586
|
+
>
|
|
587
|
+
<option value="all">All Types</option>
|
|
588
|
+
<option value="image">Images</option>
|
|
589
|
+
<option value="video">Videos</option>
|
|
590
|
+
<option value="document">Documents</option>
|
|
591
|
+
</select>
|
|
592
|
+
</div>
|
|
593
|
+
<div className="bg-muted flex items-center gap-1 rounded-lg p-1">
|
|
540
594
|
<button
|
|
541
|
-
onClick={
|
|
542
|
-
|
|
543
|
-
setSelectedMedia([])
|
|
544
|
-
}}
|
|
545
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded-lg px-3 py-1.5 text-sm transition-colors"
|
|
595
|
+
onClick={() => setViewMode('grid')}
|
|
596
|
+
className={`rounded p-1.5 transition-colors ${viewMode === 'grid' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
546
597
|
>
|
|
547
|
-
|
|
598
|
+
<Grid3x3 className="h-4 w-4" />
|
|
548
599
|
</button>
|
|
549
600
|
<button
|
|
550
|
-
onClick={() =>
|
|
551
|
-
className=
|
|
601
|
+
onClick={() => setViewMode('list')}
|
|
602
|
+
className={`rounded p-1.5 transition-colors ${viewMode === 'list' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
552
603
|
>
|
|
553
|
-
|
|
604
|
+
<List className="h-4 w-4" />
|
|
554
605
|
</button>
|
|
555
606
|
</div>
|
|
556
607
|
</div>
|
|
557
608
|
</div>
|
|
558
|
-
)}
|
|
559
609
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
610
|
+
{selectedMedia.length > 0 && (
|
|
611
|
+
<div className="border-primary/30 bg-primary/10 mb-4 rounded-lg border p-3">
|
|
612
|
+
<div className="flex items-center justify-between">
|
|
613
|
+
<span className="text-primary text-sm">
|
|
614
|
+
{selectedMedia.length} file{selectedMedia.length !== 1 ? 's' : ''} selected
|
|
615
|
+
</span>
|
|
616
|
+
<div className="flex items-center gap-2">
|
|
617
|
+
<button
|
|
618
|
+
onClick={async () => {
|
|
619
|
+
for (const id of selectedMedia) await deleteMedia(id)
|
|
620
|
+
setSelectedMedia([])
|
|
621
|
+
}}
|
|
622
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded-lg px-3 py-1.5 text-sm transition-colors"
|
|
623
|
+
>
|
|
624
|
+
Delete Selected
|
|
625
|
+
</button>
|
|
626
|
+
<button
|
|
627
|
+
onClick={() => setSelectedMedia([])}
|
|
628
|
+
className="border-input bg-card hover:bg-muted rounded-lg border px-3 py-1.5 text-sm transition-colors"
|
|
629
|
+
>
|
|
630
|
+
Cancel
|
|
631
|
+
</button>
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
564
634
|
</div>
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
className=
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
)
|
|
597
|
-
.
|
|
598
|
-
.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
>
|
|
622
|
-
<
|
|
635
|
+
)}
|
|
636
|
+
|
|
637
|
+
{filteredAndSorted.length === 0 && !loading ? (
|
|
638
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
639
|
+
<div className="bg-muted mb-4 flex h-12 w-12 items-center justify-center rounded-full">
|
|
640
|
+
<ImageIcon className="text-muted-foreground h-6 w-6" />
|
|
641
|
+
</div>
|
|
642
|
+
<h3 className="text-foreground mb-1 text-sm font-medium">
|
|
643
|
+
{folderSel.type === 'smart' && folderSel.smart === 'uncategorized'
|
|
644
|
+
? 'No uncategorized media'
|
|
645
|
+
: folderSel.type === 'folder'
|
|
646
|
+
? 'No media in this folder'
|
|
647
|
+
: 'No media yet'}
|
|
648
|
+
</h3>
|
|
649
|
+
<p className="text-muted-foreground text-sm">
|
|
650
|
+
Upload your first file to get started.
|
|
651
|
+
</p>
|
|
652
|
+
</div>
|
|
653
|
+
) : (
|
|
654
|
+
<div className="flex min-h-0 flex-1 gap-4 overflow-hidden">
|
|
655
|
+
<div
|
|
656
|
+
className={`border-border bg-card overflow-hidden rounded-lg border transition-all duration-200 ${panelOpen ? 'min-w-0 flex-1' : 'w-full'}`}
|
|
657
|
+
>
|
|
658
|
+
{viewMode === 'grid' ? (
|
|
659
|
+
<div
|
|
660
|
+
className={`grid h-full auto-rows-min content-start gap-3 overflow-y-auto p-2 sm:gap-4 sm:p-3 ${
|
|
661
|
+
panelOpen
|
|
662
|
+
? 'grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'
|
|
663
|
+
: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
|
|
664
|
+
}`}
|
|
665
|
+
>
|
|
666
|
+
{filteredAndSorted.map((item) => {
|
|
667
|
+
const isActive = activeItem?.id === item.id
|
|
668
|
+
const hasIssues = !item.altTag || !item.title || item.usedOn?.length === 0
|
|
669
|
+
const typeLabel = (
|
|
670
|
+
item.name.includes('.')
|
|
671
|
+
? item.name.split('.').pop()!
|
|
672
|
+
: item.format || item.type || ''
|
|
673
|
+
)
|
|
674
|
+
.replace(/^.*\//, '')
|
|
675
|
+
.toUpperCase()
|
|
676
|
+
return (
|
|
677
|
+
<div
|
|
678
|
+
key={item.id}
|
|
679
|
+
className={`group relative flex cursor-pointer flex-col overflow-hidden rounded-lg border-2 transition-all ${
|
|
680
|
+
isActive
|
|
681
|
+
? 'border-primary ring-primary/30 ring-2'
|
|
682
|
+
: selectedMedia.includes(item.id)
|
|
683
|
+
? 'border-primary/60 ring-primary/20 ring-1'
|
|
684
|
+
: 'border-border hover:border-input'
|
|
685
|
+
}`}
|
|
686
|
+
onClick={() => openDetail(item)}
|
|
687
|
+
draggable
|
|
688
|
+
onDragStart={(e) => handleDragStart(e, item.id)}
|
|
689
|
+
>
|
|
690
|
+
<div className="relative aspect-square w-full overflow-hidden">
|
|
691
|
+
<div className="bg-muted flex h-full w-full items-center justify-center">
|
|
692
|
+
<MediaPreview item={item} />
|
|
623
693
|
</div>
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
694
|
+
{hasIssues && (
|
|
695
|
+
<div
|
|
696
|
+
className="bg-warning absolute top-1.5 left-1.5 flex h-5 w-5 items-center justify-center rounded-full"
|
|
697
|
+
title="Needs attention"
|
|
698
|
+
>
|
|
699
|
+
<AlertTriangle className="text-warning-foreground h-3 w-3" />
|
|
700
|
+
</div>
|
|
701
|
+
)}
|
|
629
702
|
<div
|
|
630
|
-
className=
|
|
631
|
-
|
|
632
|
-
? 'border-primary bg-primary'
|
|
633
|
-
: 'border-input bg-card/80'
|
|
634
|
-
}`}
|
|
703
|
+
className="absolute top-1.5 right-1.5 opacity-0 transition-opacity group-hover:opacity-100"
|
|
704
|
+
onClick={(e) => handleCheckbox(e, item.id)}
|
|
635
705
|
>
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
706
|
+
<div
|
|
707
|
+
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition-colors ${
|
|
708
|
+
selectedMedia.includes(item.id)
|
|
709
|
+
? 'border-primary bg-primary'
|
|
710
|
+
: 'border-input bg-card/80'
|
|
711
|
+
}`}
|
|
712
|
+
>
|
|
713
|
+
{selectedMedia.includes(item.id) && (
|
|
714
|
+
<svg
|
|
715
|
+
className="text-primary-foreground h-3 w-3"
|
|
716
|
+
fill="none"
|
|
717
|
+
viewBox="0 0 12 12"
|
|
718
|
+
>
|
|
719
|
+
<path
|
|
720
|
+
d="M10 3L4.5 8.5L2 6"
|
|
721
|
+
stroke="currentColor"
|
|
722
|
+
strokeWidth="2"
|
|
723
|
+
strokeLinecap="round"
|
|
724
|
+
strokeLinejoin="round"
|
|
725
|
+
/>
|
|
726
|
+
</svg>
|
|
727
|
+
)}
|
|
728
|
+
</div>
|
|
651
729
|
</div>
|
|
652
730
|
</div>
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
</
|
|
731
|
+
<div className="border-border bg-card flex items-center gap-2 border-t px-2 py-1.5">
|
|
732
|
+
<span
|
|
733
|
+
className={`mt-0.5 h-2 w-2 shrink-0 self-start rounded-full ${
|
|
734
|
+
hasIssues ? 'bg-warning' : 'bg-success'
|
|
735
|
+
}`}
|
|
736
|
+
title={hasIssues ? 'Needs attention' : 'SEO complete'}
|
|
737
|
+
/>
|
|
738
|
+
<div className="min-w-0 flex-1">
|
|
739
|
+
<p className="text-foreground truncate text-xs font-medium">
|
|
740
|
+
{item.name}
|
|
741
|
+
</p>
|
|
742
|
+
<p className="text-muted-foreground truncate text-xs">
|
|
743
|
+
{[typeLabel, item.size].filter(Boolean).join(' · ')}
|
|
744
|
+
</p>
|
|
745
|
+
</div>
|
|
668
746
|
</div>
|
|
669
747
|
</div>
|
|
670
|
-
|
|
671
|
-
)
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
</
|
|
706
|
-
</
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
748
|
+
)
|
|
749
|
+
})}
|
|
750
|
+
</div>
|
|
751
|
+
) : (
|
|
752
|
+
<div className="h-full overflow-y-auto">
|
|
753
|
+
<table className="w-full">
|
|
754
|
+
<thead className="border-border bg-muted sticky top-0 border-b">
|
|
755
|
+
<tr>
|
|
756
|
+
<th className="w-8 px-3 py-2 text-left">
|
|
757
|
+
<input
|
|
758
|
+
type="checkbox"
|
|
759
|
+
checked={
|
|
760
|
+
selectedMedia.length === filteredAndSorted.length &&
|
|
761
|
+
filteredAndSorted.length > 0
|
|
762
|
+
}
|
|
763
|
+
onChange={handleSelectAll}
|
|
764
|
+
className="border-input rounded"
|
|
765
|
+
/>
|
|
766
|
+
</th>
|
|
767
|
+
<th className="w-6 px-1 py-2"></th>
|
|
768
|
+
<th className="px-3 py-2 text-left">
|
|
769
|
+
<SortHeader label="Name" sortKey="name" />
|
|
770
|
+
</th>
|
|
771
|
+
<th className="px-3 py-2 text-left">
|
|
772
|
+
<SortHeader label="Type" sortKey="type" />
|
|
773
|
+
</th>
|
|
774
|
+
<th className="px-3 py-2 text-left">
|
|
775
|
+
<SortHeader label="Size" sortKey="size" />
|
|
776
|
+
</th>
|
|
777
|
+
<th className="px-3 py-2 text-left">
|
|
778
|
+
<SortHeader label="Uploaded" sortKey="date" />
|
|
779
|
+
</th>
|
|
780
|
+
<th className="text-foreground px-3 py-2 text-left text-xs font-medium">
|
|
781
|
+
Status
|
|
782
|
+
</th>
|
|
783
|
+
</tr>
|
|
784
|
+
</thead>
|
|
785
|
+
<tbody className="divide-border divide-y">
|
|
786
|
+
{filteredAndSorted.map((item) => {
|
|
787
|
+
const isActive = activeItem?.id === item.id
|
|
788
|
+
const hasIssues =
|
|
789
|
+
!item.altTag || !item.title || item.usedOn?.length === 0
|
|
790
|
+
return (
|
|
791
|
+
<tr
|
|
792
|
+
key={item.id}
|
|
793
|
+
className={`cursor-pointer transition-colors ${isActive ? 'bg-primary/10' : 'hover:bg-muted'}`}
|
|
794
|
+
onClick={() => openDetail(item)}
|
|
795
|
+
draggable
|
|
796
|
+
onDragStart={(e) => handleDragStart(e, item.id)}
|
|
797
|
+
>
|
|
798
|
+
<td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
|
|
799
|
+
<input
|
|
800
|
+
type="checkbox"
|
|
801
|
+
checked={selectedMedia.includes(item.id)}
|
|
802
|
+
onChange={() =>
|
|
803
|
+
handleCheckbox(
|
|
804
|
+
{ stopPropagation: () => {} } as React.MouseEvent,
|
|
805
|
+
item.id,
|
|
806
|
+
)
|
|
807
|
+
}
|
|
808
|
+
className="border-input rounded"
|
|
809
|
+
/>
|
|
810
|
+
</td>
|
|
811
|
+
<td className="cursor-grab px-1 py-2">
|
|
812
|
+
<GripVertical className="text-muted-foreground h-4 w-4" />
|
|
813
|
+
</td>
|
|
814
|
+
<td className="px-3 py-2">
|
|
815
|
+
<div className="flex items-center gap-3">
|
|
816
|
+
<div className="bg-muted flex h-10 w-10 items-center justify-center overflow-hidden rounded">
|
|
817
|
+
<MediaPreview item={item} />
|
|
818
|
+
</div>
|
|
819
|
+
<span className="text-foreground text-sm font-medium">
|
|
820
|
+
{item.name}
|
|
821
|
+
</span>
|
|
740
822
|
</div>
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
</
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
</table>
|
|
767
|
-
</div>
|
|
768
|
-
)}
|
|
769
|
-
</div>
|
|
823
|
+
</td>
|
|
824
|
+
<td className="text-muted-foreground px-3 py-2 text-sm">
|
|
825
|
+
{item.format ?? item.type}
|
|
826
|
+
</td>
|
|
827
|
+
<td className="text-muted-foreground px-3 py-2 text-sm">
|
|
828
|
+
{item.size}
|
|
829
|
+
</td>
|
|
830
|
+
<td className="text-muted-foreground px-3 py-2 text-sm">
|
|
831
|
+
{item.date}
|
|
832
|
+
</td>
|
|
833
|
+
<td className="px-3 py-2">
|
|
834
|
+
{hasIssues ? (
|
|
835
|
+
<StatusBadge tone="warning">Needs attention</StatusBadge>
|
|
836
|
+
) : (
|
|
837
|
+
<StatusBadge tone="success">Complete</StatusBadge>
|
|
838
|
+
)}
|
|
839
|
+
</td>
|
|
840
|
+
</tr>
|
|
841
|
+
)
|
|
842
|
+
})}
|
|
843
|
+
</tbody>
|
|
844
|
+
</table>
|
|
845
|
+
</div>
|
|
846
|
+
)}
|
|
847
|
+
</div>
|
|
770
848
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
849
|
+
{panelOpen && activeItem && (
|
|
850
|
+
<div className="border-border bg-card flex w-80 shrink-0 flex-col overflow-y-auto rounded-lg border lg:w-96">
|
|
851
|
+
<div className="border-border bg-card sticky top-0 z-10 flex items-center justify-between border-b p-4">
|
|
852
|
+
<h3 className="text-foreground truncate text-sm font-medium">
|
|
853
|
+
{activeItem.name}
|
|
854
|
+
</h3>
|
|
855
|
+
<button
|
|
856
|
+
onClick={closeDetail}
|
|
857
|
+
className="hover:bg-muted rounded p-1 transition-colors"
|
|
858
|
+
aria-label="Close panel"
|
|
859
|
+
>
|
|
860
|
+
<X className="text-muted-foreground h-4 w-4" />
|
|
861
|
+
</button>
|
|
862
|
+
</div>
|
|
785
863
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
864
|
+
<div className="flex-1 overflow-y-auto">
|
|
865
|
+
<div className="border-border border-b p-4">
|
|
866
|
+
<div className="bg-muted flex aspect-video items-center justify-center overflow-hidden rounded-lg">
|
|
867
|
+
{isImageMedia(activeItem) ? (
|
|
868
|
+
<MediaPreview item={activeItem} />
|
|
869
|
+
) : (
|
|
870
|
+
<ImageIcon className="text-muted-foreground h-12 w-12" />
|
|
871
|
+
)}
|
|
872
|
+
</div>
|
|
794
873
|
</div>
|
|
795
|
-
</div>
|
|
796
874
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
875
|
+
{issues.length > 0 && (
|
|
876
|
+
<div className="border-warning/30 bg-warning/10 mx-4 mt-4 rounded-lg border p-3">
|
|
877
|
+
<div className="flex items-start gap-2">
|
|
878
|
+
<AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" />
|
|
879
|
+
<div>
|
|
880
|
+
<p className="text-warning mb-1 text-xs font-medium">
|
|
881
|
+
{issues.length} issue{issues.length !== 1 ? 's' : ''} found
|
|
882
|
+
</p>
|
|
883
|
+
<ul className="space-y-0.5">
|
|
884
|
+
{issues.map((issue, i) => (
|
|
885
|
+
<li key={i} className="text-warning text-xs">
|
|
886
|
+
• {issue}
|
|
887
|
+
</li>
|
|
888
|
+
))}
|
|
889
|
+
</ul>
|
|
890
|
+
</div>
|
|
891
|
+
</div>
|
|
892
|
+
<button
|
|
893
|
+
type="button"
|
|
894
|
+
onClick={async () => {
|
|
895
|
+
if (!activeItem.altTag) await handleAiGenerate('alt')
|
|
896
|
+
if (!activeItem.title) await handleAiGenerate('title')
|
|
897
|
+
if (activeItem.sizeBytes > 2000000) await handleAiGenerate('optimize')
|
|
898
|
+
}}
|
|
899
|
+
className="bg-warning text-warning-foreground hover:bg-warning/90 mt-2 flex w-full items-center justify-center gap-1.5 rounded-lg px-3 py-1.5 text-xs transition-colors"
|
|
900
|
+
>
|
|
901
|
+
<Sparkles className="h-3.5 w-3.5" />
|
|
902
|
+
AI Fix All Issues
|
|
903
|
+
</button>
|
|
904
|
+
</div>
|
|
905
|
+
)}
|
|
906
|
+
|
|
907
|
+
<div className="border-border space-y-3 border-b p-4">
|
|
908
|
+
<h4 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
|
909
|
+
File Information
|
|
910
|
+
</h4>
|
|
911
|
+
<div className="grid grid-cols-2 gap-3">
|
|
801
912
|
<div>
|
|
802
|
-
<
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
913
|
+
<div className="text-muted-foreground mb-0.5 text-xs">Format</div>
|
|
914
|
+
<div className="text-foreground text-sm">
|
|
915
|
+
{activeItem.format ?? 'Unknown'}
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
<div>
|
|
919
|
+
<div className="text-muted-foreground mb-0.5 text-xs">File Size</div>
|
|
920
|
+
<div className="text-foreground flex items-center gap-1 text-sm">
|
|
921
|
+
{activeItem.size}
|
|
922
|
+
{activeItem.sizeBytes > 2000000 && (
|
|
923
|
+
<span className="text-warning text-xs">(large)</span>
|
|
924
|
+
)}
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
<div>
|
|
928
|
+
<div className="text-muted-foreground mb-0.5 text-xs">Dimensions</div>
|
|
929
|
+
<div className="text-foreground text-sm">
|
|
930
|
+
{activeItem.dimensions ?? '—'}
|
|
931
|
+
</div>
|
|
932
|
+
</div>
|
|
933
|
+
<div>
|
|
934
|
+
<div className="text-muted-foreground mb-0.5 text-xs">Uploaded</div>
|
|
935
|
+
<div className="text-foreground text-sm">{activeItem.date}</div>
|
|
812
936
|
</div>
|
|
813
937
|
</div>
|
|
814
|
-
<button
|
|
815
|
-
type="button"
|
|
816
|
-
onClick={async () => {
|
|
817
|
-
if (!activeItem.altTag) await handleAiGenerate('alt')
|
|
818
|
-
if (!activeItem.title) await handleAiGenerate('title')
|
|
819
|
-
if (activeItem.sizeBytes > 2000000) await handleAiGenerate('optimize')
|
|
820
|
-
}}
|
|
821
|
-
className="bg-warning text-warning-foreground hover:bg-warning/90 mt-2 flex w-full items-center justify-center gap-1.5 rounded-lg px-3 py-1.5 text-xs transition-colors"
|
|
822
|
-
>
|
|
823
|
-
<Sparkles className="h-3.5 w-3.5" />
|
|
824
|
-
AI Fix All Issues
|
|
825
|
-
</button>
|
|
826
|
-
</div>
|
|
827
|
-
)}
|
|
828
938
|
|
|
829
|
-
<div className="border-border space-y-3 border-b p-4">
|
|
830
|
-
<h4 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
|
831
|
-
File Information
|
|
832
|
-
</h4>
|
|
833
|
-
<div className="grid grid-cols-2 gap-3">
|
|
834
939
|
<div>
|
|
835
|
-
<div className="text-muted-foreground mb-
|
|
836
|
-
<div className="
|
|
837
|
-
|
|
940
|
+
<div className="text-muted-foreground mb-1 text-xs">URL</div>
|
|
941
|
+
<div className="flex items-center gap-1">
|
|
942
|
+
<code className="border-border bg-muted text-foreground flex-1 truncate rounded border px-2 py-1.5 text-xs">
|
|
943
|
+
{activeItem.url}
|
|
944
|
+
</code>
|
|
945
|
+
<button
|
|
946
|
+
onClick={handleCopyUrl}
|
|
947
|
+
className="hover:bg-muted shrink-0 rounded p-1.5 transition-colors"
|
|
948
|
+
title="Copy URL"
|
|
949
|
+
>
|
|
950
|
+
<Copy className="text-muted-foreground h-3.5 w-3.5" />
|
|
951
|
+
</button>
|
|
838
952
|
</div>
|
|
839
953
|
</div>
|
|
954
|
+
</div>
|
|
955
|
+
|
|
956
|
+
<div className="border-border space-y-4 border-b p-4">
|
|
957
|
+
<h4 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
|
958
|
+
SEO & Accessibility
|
|
959
|
+
</h4>
|
|
960
|
+
|
|
840
961
|
<div>
|
|
841
|
-
<div className="
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
962
|
+
<div className="mb-1 flex items-center justify-between">
|
|
963
|
+
<label className="text-foreground text-sm font-medium">Alt Tag</label>
|
|
964
|
+
<button
|
|
965
|
+
type="button"
|
|
966
|
+
onClick={() => handleAiGenerate('alt')}
|
|
967
|
+
disabled={aiGenerating === 'alt'}
|
|
968
|
+
className="text-primary hover:text-primary/80 flex items-center gap-1 text-xs transition-colors disabled:opacity-50"
|
|
969
|
+
>
|
|
970
|
+
<Bot className="h-3.5 w-3.5" />
|
|
971
|
+
{aiGenerating === 'alt' ? 'Generating...' : 'AI Generate'}
|
|
972
|
+
</button>
|
|
847
973
|
</div>
|
|
974
|
+
<textarea
|
|
975
|
+
value={editAlt}
|
|
976
|
+
onChange={(e) => setEditAlt(e.target.value)}
|
|
977
|
+
placeholder="Describe this image for accessibility..."
|
|
978
|
+
rows={2}
|
|
979
|
+
className="border-input focus:ring-ring w-full resize-none rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
980
|
+
/>
|
|
981
|
+
{!editAlt && (
|
|
982
|
+
<p className="text-destructive mt-1 text-xs">
|
|
983
|
+
Required for accessibility and SEO
|
|
984
|
+
</p>
|
|
985
|
+
)}
|
|
848
986
|
</div>
|
|
987
|
+
|
|
849
988
|
<div>
|
|
850
|
-
<div className="
|
|
851
|
-
|
|
852
|
-
|
|
989
|
+
<div className="mb-1 flex items-center justify-between">
|
|
990
|
+
<label className="text-foreground text-sm font-medium">Title</label>
|
|
991
|
+
<button
|
|
992
|
+
type="button"
|
|
993
|
+
onClick={() => handleAiGenerate('title')}
|
|
994
|
+
disabled={aiGenerating === 'title'}
|
|
995
|
+
className="text-primary hover:text-primary/80 flex items-center gap-1 text-xs transition-colors disabled:opacity-50"
|
|
996
|
+
>
|
|
997
|
+
<Bot className="h-3.5 w-3.5" />
|
|
998
|
+
{aiGenerating === 'title' ? 'Generating...' : 'AI Generate'}
|
|
999
|
+
</button>
|
|
853
1000
|
</div>
|
|
1001
|
+
<input
|
|
1002
|
+
type="text"
|
|
1003
|
+
value={editTitle}
|
|
1004
|
+
onChange={(e) => setEditTitle(e.target.value)}
|
|
1005
|
+
placeholder="Image title..."
|
|
1006
|
+
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
1007
|
+
/>
|
|
854
1008
|
</div>
|
|
1009
|
+
|
|
855
1010
|
<div>
|
|
856
|
-
<
|
|
857
|
-
|
|
1011
|
+
<label
|
|
1012
|
+
htmlFor="media-detail-caption"
|
|
1013
|
+
className="text-foreground mb-1 block text-sm font-medium"
|
|
1014
|
+
>
|
|
1015
|
+
Caption
|
|
1016
|
+
</label>
|
|
1017
|
+
<input
|
|
1018
|
+
id="media-detail-caption"
|
|
1019
|
+
type="text"
|
|
1020
|
+
value={editCaption}
|
|
1021
|
+
onChange={(e) => setEditCaption(e.target.value)}
|
|
1022
|
+
placeholder="Short caption shown alongside the asset..."
|
|
1023
|
+
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
1024
|
+
/>
|
|
858
1025
|
</div>
|
|
859
|
-
</div>
|
|
860
1026
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
{activeItem.url}
|
|
866
|
-
</code>
|
|
867
|
-
<button
|
|
868
|
-
onClick={handleCopyUrl}
|
|
869
|
-
className="hover:bg-muted shrink-0 rounded p-1.5 transition-colors"
|
|
870
|
-
title="Copy URL"
|
|
1027
|
+
<div>
|
|
1028
|
+
<label
|
|
1029
|
+
htmlFor="media-detail-description"
|
|
1030
|
+
className="text-foreground mb-1 block text-sm font-medium"
|
|
871
1031
|
>
|
|
872
|
-
|
|
873
|
-
</
|
|
1032
|
+
Description
|
|
1033
|
+
</label>
|
|
1034
|
+
<textarea
|
|
1035
|
+
id="media-detail-description"
|
|
1036
|
+
value={editDescription}
|
|
1037
|
+
onChange={(e) => setEditDescription(e.target.value)}
|
|
1038
|
+
placeholder="Longer description for search and reuse..."
|
|
1039
|
+
rows={2}
|
|
1040
|
+
className="border-input focus:ring-ring w-full resize-none rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
1041
|
+
/>
|
|
874
1042
|
</div>
|
|
875
|
-
</div>
|
|
876
|
-
</div>
|
|
877
|
-
|
|
878
|
-
<div className="border-border space-y-4 border-b p-4">
|
|
879
|
-
<h4 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
|
880
|
-
SEO & Accessibility
|
|
881
|
-
</h4>
|
|
882
1043
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
type="button"
|
|
888
|
-
onClick={() => handleAiGenerate('alt')}
|
|
889
|
-
disabled={aiGenerating === 'alt'}
|
|
890
|
-
className="text-primary hover:text-primary/80 flex items-center gap-1 text-xs transition-colors disabled:opacity-50"
|
|
1044
|
+
<div>
|
|
1045
|
+
<label
|
|
1046
|
+
htmlFor="media-detail-focus-keyword"
|
|
1047
|
+
className="text-foreground mb-1 block text-sm font-medium"
|
|
891
1048
|
>
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1049
|
+
Focus Keyword
|
|
1050
|
+
</label>
|
|
1051
|
+
<input
|
|
1052
|
+
id="media-detail-focus-keyword"
|
|
1053
|
+
type="text"
|
|
1054
|
+
value={editFocusKeyword}
|
|
1055
|
+
onChange={(e) => setEditFocusKeyword(e.target.value)}
|
|
1056
|
+
placeholder="Primary keyword this asset targets..."
|
|
1057
|
+
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
1058
|
+
/>
|
|
895
1059
|
</div>
|
|
896
|
-
<textarea
|
|
897
|
-
value={editAlt}
|
|
898
|
-
onChange={(e) => setEditAlt(e.target.value)}
|
|
899
|
-
placeholder="Describe this image for accessibility..."
|
|
900
|
-
rows={2}
|
|
901
|
-
className="border-input focus:ring-ring w-full resize-none rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
902
|
-
/>
|
|
903
|
-
{!editAlt && (
|
|
904
|
-
<p className="text-destructive mt-1 text-xs">
|
|
905
|
-
Required for accessibility and SEO
|
|
906
|
-
</p>
|
|
907
|
-
)}
|
|
908
|
-
</div>
|
|
909
1060
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
{aiGenerating === 'title' ? 'Generating...' : 'AI Generate'}
|
|
921
|
-
</button>
|
|
1061
|
+
<div>
|
|
1062
|
+
<label className="text-foreground mb-1 block text-sm font-medium">
|
|
1063
|
+
File Name
|
|
1064
|
+
</label>
|
|
1065
|
+
<input
|
|
1066
|
+
type="text"
|
|
1067
|
+
value={editFilename}
|
|
1068
|
+
onChange={(e) => setEditFilename(e.target.value)}
|
|
1069
|
+
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
1070
|
+
/>
|
|
922
1071
|
</div>
|
|
923
|
-
<input
|
|
924
|
-
type="text"
|
|
925
|
-
value={editTitle}
|
|
926
|
-
onChange={(e) => setEditTitle(e.target.value)}
|
|
927
|
-
placeholder="Image title..."
|
|
928
|
-
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
929
|
-
/>
|
|
930
1072
|
</div>
|
|
931
1073
|
|
|
932
|
-
<div>
|
|
933
|
-
<
|
|
934
|
-
|
|
935
|
-
</
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1074
|
+
<div className="border-border border-b p-4">
|
|
1075
|
+
<h4 className="text-muted-foreground mb-3 text-xs font-medium tracking-wide uppercase">
|
|
1076
|
+
Used On {activeItem.usedOn && `(${activeItem.usedOn.length})`}
|
|
1077
|
+
</h4>
|
|
1078
|
+
{activeItem.usedOn && activeItem.usedOn.length > 0 ? (
|
|
1079
|
+
<div className="space-y-2">
|
|
1080
|
+
{activeItem.usedOn.map((usage, i) => (
|
|
1081
|
+
<button
|
|
1082
|
+
key={i}
|
|
1083
|
+
type="button"
|
|
1084
|
+
onClick={() => onNavigate?.(usage.path)}
|
|
1085
|
+
className="border-border hover:bg-muted flex w-full items-center gap-2 rounded-lg border p-2 text-left transition-colors"
|
|
1086
|
+
>
|
|
1087
|
+
<Link2 className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
1088
|
+
<span className="text-foreground flex-1 truncate text-sm">
|
|
1089
|
+
{usage.page}
|
|
1090
|
+
</span>
|
|
1091
|
+
<ExternalLink className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
|
|
1092
|
+
</button>
|
|
1093
|
+
))}
|
|
1094
|
+
</div>
|
|
1095
|
+
) : (
|
|
1096
|
+
<div className="border-warning/30 bg-warning/10 rounded-lg border p-3">
|
|
1097
|
+
<div className="flex items-start gap-2">
|
|
1098
|
+
<AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" />
|
|
1099
|
+
<div>
|
|
1100
|
+
<p className="text-warning text-xs font-medium">Orphaned media</p>
|
|
1101
|
+
<p className="text-warning mt-0.5 text-xs">
|
|
1102
|
+
This file isn't used on any page. Consider deleting it to
|
|
1103
|
+
save storage.
|
|
1104
|
+
</p>
|
|
1105
|
+
</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
</div>
|
|
1108
|
+
)}
|
|
942
1109
|
</div>
|
|
943
|
-
</div>
|
|
944
1110
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
{
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
))}
|
|
965
|
-
</div>
|
|
966
|
-
) : (
|
|
967
|
-
<div className="border-warning/30 bg-warning/10 rounded-lg border p-3">
|
|
968
|
-
<div className="flex items-start gap-2">
|
|
969
|
-
<AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" />
|
|
970
|
-
<div>
|
|
971
|
-
<p className="text-warning text-xs font-medium">Orphaned media</p>
|
|
972
|
-
<p className="text-warning mt-0.5 text-xs">
|
|
973
|
-
This file isn't used on any page. Consider deleting it to save
|
|
974
|
-
storage.
|
|
975
|
-
</p>
|
|
1111
|
+
<div className="space-y-3 p-4">
|
|
1112
|
+
<h4 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
|
1113
|
+
AI Optimization
|
|
1114
|
+
</h4>
|
|
1115
|
+
<button
|
|
1116
|
+
type="button"
|
|
1117
|
+
onClick={() => handleAiGenerate('optimize')}
|
|
1118
|
+
disabled={aiGenerating === 'optimize'}
|
|
1119
|
+
className="border-primary/30 bg-primary/10 hover:bg-primary/15 flex w-full items-center gap-2 rounded-lg border p-3 text-left transition-colors disabled:opacity-50"
|
|
1120
|
+
>
|
|
1121
|
+
<Sparkles
|
|
1122
|
+
className={`text-primary h-5 w-5 shrink-0 ${aiGenerating === 'optimize' ? 'animate-spin' : ''}`}
|
|
1123
|
+
/>
|
|
1124
|
+
<div className="flex-1">
|
|
1125
|
+
<div className="text-primary text-sm font-medium">
|
|
1126
|
+
{aiGenerating === 'optimize' ? 'Optimizing...' : 'Optimize Image'}
|
|
1127
|
+
</div>
|
|
1128
|
+
<div className="text-primary mt-0.5 text-xs">
|
|
1129
|
+
Compress and convert to modern format (WebP/AVIF)
|
|
976
1130
|
</div>
|
|
977
1131
|
</div>
|
|
978
|
-
</
|
|
979
|
-
|
|
1132
|
+
</button>
|
|
1133
|
+
<button
|
|
1134
|
+
type="button"
|
|
1135
|
+
className="border-border hover:bg-muted flex w-full items-center gap-2 rounded-lg border p-3 text-left transition-colors"
|
|
1136
|
+
>
|
|
1137
|
+
<Bot className="text-muted-foreground h-5 w-5 shrink-0" />
|
|
1138
|
+
<div className="flex-1">
|
|
1139
|
+
<div className="text-foreground text-sm font-medium">
|
|
1140
|
+
AI Content Analysis
|
|
1141
|
+
</div>
|
|
1142
|
+
<div className="text-muted-foreground mt-0.5 text-xs">
|
|
1143
|
+
Detect objects, faces, text, and suggest categories
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
</button>
|
|
1147
|
+
</div>
|
|
980
1148
|
</div>
|
|
981
1149
|
|
|
982
|
-
<div className="
|
|
983
|
-
<h4 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
|
984
|
-
AI Optimization
|
|
985
|
-
</h4>
|
|
1150
|
+
<div className="border-border bg-card sticky bottom-0 flex items-center gap-2 border-t p-4">
|
|
986
1151
|
<button
|
|
987
1152
|
type="button"
|
|
988
|
-
onClick={
|
|
989
|
-
disabled={
|
|
990
|
-
className="
|
|
1153
|
+
onClick={handleSaveDetails}
|
|
1154
|
+
disabled={saving}
|
|
1155
|
+
className="bg-primary text-primary-foreground hover:bg-primary/90 flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm transition-colors disabled:opacity-50"
|
|
991
1156
|
>
|
|
992
|
-
<
|
|
993
|
-
|
|
994
|
-
/>
|
|
995
|
-
<div className="flex-1">
|
|
996
|
-
<div className="text-primary text-sm font-medium">
|
|
997
|
-
{aiGenerating === 'optimize' ? 'Optimizing...' : 'Optimize Image'}
|
|
998
|
-
</div>
|
|
999
|
-
<div className="text-primary mt-0.5 text-xs">
|
|
1000
|
-
Compress and convert to modern format (WebP/AVIF)
|
|
1001
|
-
</div>
|
|
1002
|
-
</div>
|
|
1157
|
+
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
1158
|
+
{saving ? 'Saving...' : 'Save Changes'}
|
|
1003
1159
|
</button>
|
|
1004
1160
|
<button
|
|
1005
1161
|
type="button"
|
|
1006
|
-
className="
|
|
1162
|
+
className="hover:bg-muted rounded-lg p-2 transition-colors"
|
|
1163
|
+
title="Download"
|
|
1007
1164
|
>
|
|
1008
|
-
<
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1165
|
+
<Download className="text-muted-foreground h-4 w-4" />
|
|
1166
|
+
</button>
|
|
1167
|
+
<button
|
|
1168
|
+
type="button"
|
|
1169
|
+
onClick={() => deleteMedia(activeItem.id)}
|
|
1170
|
+
className="hover:bg-muted rounded-lg p-2 transition-colors"
|
|
1171
|
+
title="Delete"
|
|
1172
|
+
>
|
|
1173
|
+
<Trash2 className="text-destructive h-4 w-4" />
|
|
1017
1174
|
</button>
|
|
1018
1175
|
</div>
|
|
1019
1176
|
</div>
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
onClick={handleSaveDetails}
|
|
1025
|
-
disabled={saving}
|
|
1026
|
-
className="bg-primary text-primary-foreground hover:bg-primary/90 flex flex-1 items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm transition-colors disabled:opacity-50"
|
|
1027
|
-
>
|
|
1028
|
-
{saving && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
1029
|
-
{saving ? 'Saving...' : 'Save Changes'}
|
|
1030
|
-
</button>
|
|
1031
|
-
<button
|
|
1032
|
-
type="button"
|
|
1033
|
-
className="hover:bg-muted rounded-lg p-2 transition-colors"
|
|
1034
|
-
title="Download"
|
|
1035
|
-
>
|
|
1036
|
-
<Download className="text-muted-foreground h-4 w-4" />
|
|
1037
|
-
</button>
|
|
1038
|
-
<button
|
|
1039
|
-
type="button"
|
|
1040
|
-
onClick={() => deleteMedia(activeItem.id)}
|
|
1041
|
-
className="hover:bg-muted rounded-lg p-2 transition-colors"
|
|
1042
|
-
title="Delete"
|
|
1043
|
-
>
|
|
1044
|
-
<Trash2 className="text-destructive h-4 w-4" />
|
|
1045
|
-
</button>
|
|
1046
|
-
</div>
|
|
1047
|
-
</div>
|
|
1048
|
-
)}
|
|
1049
|
-
</div>
|
|
1050
|
-
)}
|
|
1177
|
+
)}
|
|
1178
|
+
</div>
|
|
1179
|
+
)}
|
|
1180
|
+
</div>
|
|
1051
1181
|
</div>
|
|
1052
|
-
|
|
1182
|
+
)}
|
|
1053
1183
|
</div>
|
|
1054
1184
|
)
|
|
1055
1185
|
}
|