@actuate-media/cms-admin 0.65.0 → 0.67.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 +39 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +7 -4
- 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__/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__/views/dashboard.test.js +7 -3
- package/dist/__tests__/views/dashboard.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/pages-list-view.test.js +77 -1
- package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- 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/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/RowActions.d.ts +19 -0
- package/dist/components/ui/RowActions.d.ts.map +1 -0
- package/dist/components/ui/RowActions.js +16 -0
- package/dist/components/ui/RowActions.js.map +1 -0
- package/dist/components/ui/index.d.ts +4 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/layout/Layout.js +8 -38
- package/dist/layout/Layout.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-service.d.ts +9 -0
- package/dist/lib/pages-service.d.ts.map +1 -1
- package/dist/lib/pages-service.js +14 -0
- package/dist/lib/pages-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/posts-service.d.ts +11 -0
- package/dist/lib/posts-service.d.ts.map +1 -1
- package/dist/lib/posts-service.js +13 -0
- package/dist/lib/posts-service.js.map +1 -1
- package/dist/lib/seo-service.d.ts +3 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +40 -3
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +55 -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 +145 -16
- 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/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +66 -9
- package/dist/views/Posts/PostsListView.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-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/TechnicalTab.d.ts.map +1 -1
- package/dist/views/seo/TechnicalTab.js +15 -0
- package/dist/views/seo/TechnicalTab.js.map +1 -1
- package/package.json +3 -3
- package/src/AdminRoot.tsx +7 -3
- package/src/__tests__/components/inspector-pane.test.tsx +64 -0
- package/src/__tests__/lib/pages-tree.test.ts +197 -0
- package/src/__tests__/views/dashboard.test.tsx +7 -3
- 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/pages-list-view.test.tsx +93 -1
- package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
- package/src/components/ui/InspectorPane.tsx +129 -0
- package/src/components/ui/RowActions.tsx +50 -0
- package/src/components/ui/index.ts +12 -0
- package/src/layout/Header.tsx +2 -2
- package/src/layout/Layout.tsx +8 -80
- package/src/lib/page-editor-service.ts +16 -2
- package/src/lib/pages-service.ts +18 -0
- package/src/lib/pages-tree.ts +145 -0
- package/src/lib/posts-service.ts +26 -0
- package/src/lib/seo-service.ts +3 -0
- package/src/views/Dashboard.tsx +286 -214
- package/src/views/MediaBrowser.tsx +673 -512
- package/src/views/MediaSeoAudit.tsx +320 -0
- package/src/views/Pages/PagesListView.tsx +459 -185
- package/src/views/Pages/PagesTreeTable.tsx +339 -0
- package/src/views/Posts/PostsListView.tsx +135 -10
- package/src/views/ScriptTags.tsx +204 -129
- package/src/views/dashboard/OnboardingBlock.tsx +233 -0
- package/src/views/page-editor/PageSectionEditor.tsx +9 -2
- package/src/views/page-editor/PageSettingsModal.tsx +75 -2
- package/src/views/seo/TechnicalTab.tsx +15 -0
- 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/views/ScriptTagEditor.tsx +0 -353
|
@@ -5,6 +5,7 @@ import { normalizeSlug, validateSlug } from '@actuate-media/cms-core/client'
|
|
|
5
5
|
import { Modal } from '../../components/ui/Modal.js'
|
|
6
6
|
import { Button } from '../../components/ui/Button.js'
|
|
7
7
|
import { SeoCharCount } from '../../components/seo/SeoCharCount.js'
|
|
8
|
+
import { cmsApi } from '../../lib/api.js'
|
|
8
9
|
import type { EditorPage } from '../../lib/page-editor-service.js'
|
|
9
10
|
|
|
10
11
|
const SEO_TITLE_MIN = 30
|
|
@@ -26,7 +27,12 @@ const INPUT =
|
|
|
26
27
|
|
|
27
28
|
const slugify = normalizeSlug
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
interface ParentOption {
|
|
31
|
+
id: string
|
|
32
|
+
title: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Edits page-level fields: title, slug, path, parent page, and SEO. */
|
|
30
36
|
export function PageSettingsModal({
|
|
31
37
|
open,
|
|
32
38
|
page,
|
|
@@ -37,21 +43,52 @@ export function PageSettingsModal({
|
|
|
37
43
|
const [title, setTitle] = useState(page.title)
|
|
38
44
|
const [slug, setSlug] = useState(page.slug)
|
|
39
45
|
const [path, setPath] = useState(page.path)
|
|
46
|
+
const [parentPageId, setParentPageId] = useState(page.parentPageId ?? '')
|
|
40
47
|
const [seoTitle, setSeoTitle] = useState(page.seoTitle)
|
|
41
48
|
const [seoDescription, setSeoDescription] = useState(page.seoDescription)
|
|
42
49
|
// Auto-derive slug/path from title until the user edits slug manually.
|
|
43
50
|
const [slugTouched, setSlugTouched] = useState(Boolean(page.slug))
|
|
51
|
+
const [parentOptions, setParentOptions] = useState<ParentOption[] | null>(null)
|
|
44
52
|
|
|
45
53
|
useEffect(() => {
|
|
46
54
|
if (!open) return
|
|
47
55
|
setTitle(page.title)
|
|
48
56
|
setSlug(page.slug)
|
|
49
57
|
setPath(page.path)
|
|
58
|
+
setParentPageId(page.parentPageId ?? '')
|
|
50
59
|
setSeoTitle(page.seoTitle)
|
|
51
60
|
setSeoDescription(page.seoDescription)
|
|
52
61
|
setSlugTouched(Boolean(page.slug))
|
|
53
62
|
}, [open, page])
|
|
54
63
|
|
|
64
|
+
// Load the parent-page choices lazily the first time the modal opens.
|
|
65
|
+
// The page itself is excluded (a page can't be its own parent); deeper
|
|
66
|
+
// cycle protection lives in the tree builder, which breaks bad chains.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!open || parentOptions !== null) return
|
|
69
|
+
let cancelled = false
|
|
70
|
+
cmsApi<{ docs: Array<{ id: string; title: string | null }> }>(
|
|
71
|
+
'/collections/pages?pageSize=500&sort=updatedAt&order=desc',
|
|
72
|
+
)
|
|
73
|
+
.then((res) => {
|
|
74
|
+
if (cancelled) return
|
|
75
|
+
const docs = res.data?.docs ?? []
|
|
76
|
+
const options = docs
|
|
77
|
+
.filter((d) => d.id !== page.id)
|
|
78
|
+
.map((d) => ({ id: d.id, title: d.title ?? '(untitled)' }))
|
|
79
|
+
.sort((a, b) => a.title.localeCompare(b.title))
|
|
80
|
+
setParentOptions(options)
|
|
81
|
+
})
|
|
82
|
+
.catch(() => {
|
|
83
|
+
// Leave the picker in its loading state note; the rest of the
|
|
84
|
+
// settings form stays fully usable.
|
|
85
|
+
if (!cancelled) setParentOptions([])
|
|
86
|
+
})
|
|
87
|
+
return () => {
|
|
88
|
+
cancelled = true
|
|
89
|
+
}
|
|
90
|
+
}, [open, parentOptions, page.id])
|
|
91
|
+
|
|
55
92
|
function handleTitle(next: string) {
|
|
56
93
|
setTitle(next)
|
|
57
94
|
if (!slugTouched) {
|
|
@@ -65,7 +102,14 @@ export function PageSettingsModal({
|
|
|
65
102
|
|
|
66
103
|
function handleSave() {
|
|
67
104
|
if (slugError) return
|
|
68
|
-
onSave({
|
|
105
|
+
onSave({
|
|
106
|
+
title,
|
|
107
|
+
slug,
|
|
108
|
+
path: path || (slug ? `/${slug}` : '/'),
|
|
109
|
+
parentPageId: parentPageId || null,
|
|
110
|
+
seoTitle,
|
|
111
|
+
seoDescription,
|
|
112
|
+
})
|
|
69
113
|
onClose()
|
|
70
114
|
}
|
|
71
115
|
|
|
@@ -135,6 +179,35 @@ export function PageSettingsModal({
|
|
|
135
179
|
/>
|
|
136
180
|
</div>
|
|
137
181
|
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<label className={LABEL} htmlFor="ps-parent">
|
|
184
|
+
Parent page
|
|
185
|
+
</label>
|
|
186
|
+
<select
|
|
187
|
+
id="ps-parent"
|
|
188
|
+
className={INPUT}
|
|
189
|
+
value={parentPageId}
|
|
190
|
+
disabled={!canEdit || parentOptions === null}
|
|
191
|
+
onChange={(e) => setParentPageId(e.target.value)}
|
|
192
|
+
>
|
|
193
|
+
<option value="">None — top level</option>
|
|
194
|
+
{/* Keep a preselected parent (e.g. from "Add sub-page") valid
|
|
195
|
+
while the options load or if it's missing from the list. */}
|
|
196
|
+
{parentPageId && !(parentOptions ?? []).some((p) => p.id === parentPageId) && (
|
|
197
|
+
<option value={parentPageId}>
|
|
198
|
+
{parentOptions === null ? 'Loading…' : 'Current parent'}
|
|
199
|
+
</option>
|
|
200
|
+
)}
|
|
201
|
+
{(parentOptions ?? []).map((p) => (
|
|
202
|
+
<option key={p.id} value={p.id}>
|
|
203
|
+
{p.title}
|
|
204
|
+
</option>
|
|
205
|
+
))}
|
|
206
|
+
</select>
|
|
207
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
208
|
+
Nests this page under a parent in the Pages tree view.
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
138
211
|
<div>
|
|
139
212
|
<div className="mb-1 flex items-center justify-between">
|
|
140
213
|
<label className={`${LABEL} mb-0`} htmlFor="ps-seo-title">
|
|
@@ -40,6 +40,21 @@ const CRAWL_TOGGLES: { key: keyof CrawlSettings; label: string; help: string }[]
|
|
|
40
40
|
label: 'Include posts in sitemap',
|
|
41
41
|
help: 'Add published posts to the sitemap.',
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
key: 'sitemapEnabled',
|
|
45
|
+
label: 'Enable XML sitemap',
|
|
46
|
+
help: 'Serve the sitemap at /sitemap.xml.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'autoRegenerateSitemapOnPublish',
|
|
50
|
+
label: 'Auto-regenerate sitemap',
|
|
51
|
+
help: 'Rebuild the sitemap automatically when content is published.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'pingSearchEnginesOnPublish',
|
|
55
|
+
label: 'Ping search engines on publish',
|
|
56
|
+
help: 'Notify Google and Bing when the sitemap changes (production only).',
|
|
57
|
+
},
|
|
43
58
|
]
|
|
44
59
|
|
|
45
60
|
function Toggle({
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export interface ScriptTagEditorProps {
|
|
2
|
-
tagId?: string;
|
|
3
|
-
onNavigate?: (path: string) => void;
|
|
4
|
-
}
|
|
5
|
-
export declare function ScriptTagEditor({ tagId, onNavigate }: ScriptTagEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
-
//# sourceMappingURL=ScriptTagEditor.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ScriptTagEditor.d.ts","sourceRoot":"","sources":["../../src/views/ScriptTagEditor.tsx"],"names":[],"mappings":"AASA,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAED,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,oBAAoB,2CAkV1E"}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
4
|
-
import { ArrowLeft, Loader2, AlertTriangle, Trash2, X, Plus } from 'lucide-react';
|
|
5
|
-
import { toast } from 'sonner';
|
|
6
|
-
import { EnabledSwitch } from '../components/EnabledSwitch.js';
|
|
7
|
-
import { useApiData } from '../lib/useApiData.js';
|
|
8
|
-
import { cmsApi } from '../lib/api.js';
|
|
9
|
-
export function ScriptTagEditor({ tagId, onNavigate }) {
|
|
10
|
-
const isNew = !tagId;
|
|
11
|
-
const { data, loading, error } = useApiData(tagId ? `/script-tags` : null);
|
|
12
|
-
const [name, setName] = useState('');
|
|
13
|
-
const [code, setCode] = useState('');
|
|
14
|
-
const [placement, setPlacement] = useState('head');
|
|
15
|
-
const [scope, setScope] = useState('site');
|
|
16
|
-
const [targetPaths, setTargetPaths] = useState([]);
|
|
17
|
-
const [pathInput, setPathInput] = useState('');
|
|
18
|
-
const [priority, setPriority] = useState(100);
|
|
19
|
-
const [enabled, setEnabled] = useState(true);
|
|
20
|
-
const [saving, setSaving] = useState(false);
|
|
21
|
-
const [deleting, setDeleting] = useState(false);
|
|
22
|
-
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
if (data && tagId) {
|
|
25
|
-
const tags = Array.isArray(data) ? data : [];
|
|
26
|
-
const tag = tags.find((t) => t.id === tagId);
|
|
27
|
-
if (tag) {
|
|
28
|
-
setName(tag.name ?? '');
|
|
29
|
-
setCode(tag.code ?? '');
|
|
30
|
-
setPlacement(tag.placement ?? 'head');
|
|
31
|
-
setScope(tag.scope ?? 'site');
|
|
32
|
-
setTargetPaths(tag.targetPaths ?? []);
|
|
33
|
-
setPriority(tag.priority ?? 100);
|
|
34
|
-
setEnabled(tag.enabled ?? true);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}, [data, tagId]);
|
|
38
|
-
const addPath = () => {
|
|
39
|
-
const trimmed = pathInput.trim();
|
|
40
|
-
if (!trimmed)
|
|
41
|
-
return;
|
|
42
|
-
const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
43
|
-
if (!targetPaths.includes(normalized)) {
|
|
44
|
-
setTargetPaths([...targetPaths, normalized]);
|
|
45
|
-
}
|
|
46
|
-
setPathInput('');
|
|
47
|
-
};
|
|
48
|
-
const removePath = (path) => {
|
|
49
|
-
setTargetPaths(targetPaths.filter((p) => p !== path));
|
|
50
|
-
};
|
|
51
|
-
const handleSave = async () => {
|
|
52
|
-
if (!name.trim()) {
|
|
53
|
-
toast.error('Name is required');
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (!code.trim()) {
|
|
57
|
-
toast.error('Code snippet is required');
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (scope !== 'site' && targetPaths.length === 0) {
|
|
61
|
-
toast.error('At least one target path is required for this scope');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
setSaving(true);
|
|
65
|
-
const payload = {
|
|
66
|
-
name: name.trim(),
|
|
67
|
-
code,
|
|
68
|
-
placement,
|
|
69
|
-
scope,
|
|
70
|
-
targetPaths: scope === 'site' ? [] : targetPaths,
|
|
71
|
-
priority,
|
|
72
|
-
enabled,
|
|
73
|
-
};
|
|
74
|
-
const res = isNew
|
|
75
|
-
? await cmsApi('/script-tags', { method: 'POST', body: JSON.stringify(payload) })
|
|
76
|
-
: await cmsApi(`/script-tags/${tagId}`, { method: 'PUT', body: JSON.stringify(payload) });
|
|
77
|
-
setSaving(false);
|
|
78
|
-
if (res.error) {
|
|
79
|
-
toast.error(res.error);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
toast.success(isNew ? 'Tag created' : 'Tag updated');
|
|
83
|
-
onNavigate?.('/script-tags');
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
const handleDelete = async () => {
|
|
87
|
-
if (!tagId)
|
|
88
|
-
return;
|
|
89
|
-
setDeleting(true);
|
|
90
|
-
const res = await cmsApi(`/script-tags/${tagId}`, { method: 'DELETE' });
|
|
91
|
-
setDeleting(false);
|
|
92
|
-
if (res.error) {
|
|
93
|
-
toast.error(res.error);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
toast.success('Tag deleted');
|
|
97
|
-
onNavigate?.('/script-tags');
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
if (loading && !isNew) {
|
|
101
|
-
return (_jsx("div", { className: "flex h-64 items-center justify-center p-3 pr-6 sm:p-4 sm:pr-8", children: _jsx(Loader2, { className: "text-primary h-6 w-6 animate-spin" }) }));
|
|
102
|
-
}
|
|
103
|
-
return (_jsxs("div", { className: "max-w-3xl p-3 pr-6 sm:p-4 sm:pr-8", children: [error && (_jsxs("div", { className: "border-destructive/30 bg-destructive/10 mb-4 flex items-center gap-3 rounded-lg border p-3", children: [_jsx(AlertTriangle, { className: "text-destructive h-5 w-5 shrink-0" }), _jsx("span", { className: "text-destructive flex-1 text-sm", children: error })] })), _jsxs("div", { className: "mb-6", children: [_jsxs("button", { type: "button", onClick: () => onNavigate?.('/script-tags'), className: "text-muted-foreground hover:text-foreground mb-3 flex items-center gap-1.5 text-sm transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to Script Tags"] }), _jsx("h1", { className: "text-foreground", children: isNew ? 'New Script Tag' : 'Edit Script Tag' })] }), _jsx("div", { className: "border-warning/30 bg-warning/10 mb-6 rounded-lg border p-3", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertTriangle, { className: "text-warning mt-0.5 h-4 w-4 shrink-0" }), _jsx("p", { className: "text-warning text-sm", children: "This code will run on public pages matching the scope below. Only add trusted code from verified sources (e.g. Google Analytics, Meta Pixel)." })] }) }), _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "border-border bg-card space-y-4 rounded-lg border p-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-foreground mb-1 block text-sm font-medium", children: "Name" }), _jsx("input", { type: "text", value: name, onChange: (e) => setName(e.target.value), placeholder: "e.g. Google Tag Manager, Facebook Pixel", className: "border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none" })] }), _jsxs("div", { children: [_jsx("label", { className: "text-foreground mb-1 block text-sm font-medium", children: "Code" }), _jsx("textarea", { value: code, onChange: (e) => setCode(e.target.value), placeholder: "Paste your HTML/JavaScript snippet here...", rows: 10, className: "border-input focus:ring-ring w-full rounded-lg border px-3 py-2 font-mono text-sm focus:ring-2 focus:outline-none", spellCheck: false }), _jsxs("p", { className: "text-muted-foreground mt-1 text-xs", children: ["Paste the full code snippet including ", '<script>', " tags."] })] })] }), _jsxs("div", { className: "border-border bg-card space-y-4 rounded-lg border p-4", children: [_jsx("h3", { className: "text-foreground text-sm font-medium", children: "Placement & Scope" }), _jsxs("div", { children: [_jsx("label", { className: "text-foreground mb-1 block text-sm font-medium", children: "Placement" }), _jsxs("select", { value: placement, onChange: (e) => setPlacement(e.target.value), className: "border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none", children: [_jsxs("option", { value: "head", children: ["Head \u2014 inside ", '<head>'] }), _jsxs("option", { value: "body_open", children: ["Body Open \u2014 right after ", '<body>'] }), _jsxs("option", { value: "body_close", children: ["Body Close \u2014 before ", '</body>'] })] }), _jsxs("p", { className: "text-muted-foreground mt-1 text-xs", children: [placement === 'head' && 'Best for analytics scripts, meta tags, and custom CSS.', placement === 'body_open' && 'Best for GTM noscript tags and early-loading scripts.', placement === 'body_close' &&
|
|
104
|
-
'Best for chat widgets, deferred scripts, and tracking pixels.'] })] }), _jsxs("div", { children: [_jsx("label", { className: "text-foreground mb-1 block text-sm font-medium", children: "Scope" }), _jsxs("select", { value: scope, onChange: (e) => setScope(e.target.value), className: "border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none", children: [_jsx("option", { value: "site", children: "Entire Website" }), _jsx("option", { value: "parents", children: "Specific Parent Pages (includes child pages)" }), _jsx("option", { value: "urls", children: "Specific URLs (exact match)" })] })] }), scope !== 'site' && (_jsxs("div", { children: [_jsx("label", { className: "text-foreground mb-1 block text-sm font-medium", children: scope === 'parents' ? 'Parent Paths' : 'URL Paths' }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "text", value: pathInput, onChange: (e) => setPathInput(e.target.value), onKeyDown: (e) => {
|
|
105
|
-
if (e.key === 'Enter') {
|
|
106
|
-
e.preventDefault();
|
|
107
|
-
addPath();
|
|
108
|
-
}
|
|
109
|
-
}, placeholder: "/services", className: "border-input focus:ring-ring flex-1 rounded-lg border px-3 py-2 font-mono text-sm focus:ring-2 focus:outline-none" }), _jsx("button", { type: "button", onClick: addPath, className: "border-input hover:bg-muted rounded-lg border px-3 py-2 text-sm transition-colors", children: _jsx(Plus, { className: "h-4 w-4" }) })] }), scope === 'parents' && (_jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: "This tag will also apply to all child pages under each path." })), targetPaths.length > 0 && (_jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: targetPaths.map((p) => (_jsxs("span", { className: "bg-muted text-foreground inline-flex items-center gap-1 rounded-full px-3 py-1 font-mono text-xs", children: [p, _jsx("button", { type: "button", onClick: () => removePath(p), className: "text-muted-foreground hover:text-muted-foreground ml-0.5", children: _jsx(X, { className: "h-3 w-3" }) })] }, p))) }))] })), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx("label", { className: "text-foreground mb-1 block text-sm font-medium", children: "Priority" }), _jsx("input", { type: "number", value: priority, onChange: (e) => setPriority(parseInt(e.target.value, 10) || 0), min: 0, className: "border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none" }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: "Lower numbers load first (e.g. 1 = first, 100 = default)" })] }), _jsxs("div", { children: [_jsx("label", { className: "text-foreground mb-1 block text-sm font-medium", children: "Enabled" }), _jsx("div", { className: "pt-2", children: _jsx(EnabledSwitch, { checked: enabled, onCheckedChange: setEnabled, "aria-label": enabled ? 'Disable script tag' : 'Enable script tag' }) })] })] })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { children: !isNew && (_jsx(_Fragment, { children: showDeleteConfirm ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-destructive text-sm", children: "Delete this tag?" }), _jsx("button", { type: "button", onClick: handleDelete, disabled: deleting, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded-lg px-3 py-1.5 text-sm disabled:opacity-50", children: deleting ? 'Deleting...' : 'Confirm' }), _jsx("button", { type: "button", onClick: () => setShowDeleteConfirm(false), className: "border-input hover:bg-muted rounded-lg border px-3 py-1.5 text-sm", children: "Cancel" })] })) : (_jsxs("button", { type: "button", onClick: () => setShowDeleteConfirm(true), className: "text-destructive hover:text-destructive flex items-center gap-1.5 text-sm transition-colors", children: [_jsx(Trash2, { className: "h-4 w-4" }), "Delete"] })) })) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("button", { type: "button", onClick: () => onNavigate?.('/script-tags'), className: "border-input text-foreground hover:bg-muted rounded-lg border px-4 py-2 text-sm transition-colors", children: "Cancel" }), _jsx("button", { type: "button", onClick: handleSave, disabled: saving, className: "bg-primary text-primary-foreground hover:bg-primary/90 rounded-lg px-6 py-2 text-sm transition-colors disabled:opacity-50", children: saving ? 'Saving...' : isNew ? 'Create Tag' : 'Save Changes' })] })] })] })] }));
|
|
110
|
-
}
|
|
111
|
-
//# sourceMappingURL=ScriptTagEditor.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ScriptTagEditor.js","sourceRoot":"","sources":["../../src/views/ScriptTagEditor.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AACjF,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAOtC,MAAM,UAAU,eAAe,CAAC,EAAE,KAAK,EAAE,UAAU,EAAwB;IACzE,MAAM,KAAK,GAAG,CAAC,KAAK,CAAA;IACpB,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,UAAU,CAAM,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAE/E,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACpC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACpC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAA;IAClD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAA;IAC1C,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAW,EAAE,CAAC,CAAA;IAC5D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC9C,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAA;IAC7C,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC5C,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC3C,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC/C,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEjE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC;YAClB,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;YAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAA;YACjD,IAAI,GAAG,EAAE,CAAC;gBACR,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;gBACvB,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;gBACvB,YAAY,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM,CAAC,CAAA;gBACrC,QAAQ,CAAC,GAAG,CAAC,KAAK,IAAI,MAAM,CAAC,CAAA;gBAC7B,cAAc,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAA;gBACrC,WAAW,CAAC,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,CAAA;gBAChC,UAAU,CAAC,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,CAAA;YACjC,CAAC;QACH,CAAC;IACH,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;IAEjB,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,EAAE,CAAA;QAChC,IAAI,CAAC,OAAO;YAAE,OAAM;QACpB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAA;QACpE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YACtC,cAAc,CAAC,CAAC,GAAG,WAAW,EAAE,UAAU,CAAC,CAAC,CAAA;QAC9C,CAAC;QACD,YAAY,CAAC,EAAE,CAAC,CAAA;IAClB,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,EAAE;QAClC,cAAc,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAA;IACvD,CAAC,CAAA;IAED,MAAM,UAAU,GAAG,KAAK,IAAI,EAAE;QAC5B,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACjB,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAA;YAC/B,OAAM;QACR,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACjB,KAAK,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAA;YACvC,OAAM;QACR,CAAC;QACD,IAAI,KAAK,KAAK,MAAM,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjD,KAAK,CAAC,KAAK,CAAC,qDAAqD,CAAC,CAAA;YAClE,OAAM;QACR,CAAC;QAED,SAAS,CAAC,IAAI,CAAC,CAAA;QACf,MAAM,OAAO,GAAG;YACd,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;YACjB,IAAI;YACJ,SAAS;YACT,KAAK;YACL,WAAW,EAAE,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW;YAChD,QAAQ;YACR,OAAO;SACR,CAAA;QAED,MAAM,GAAG,GAAG,KAAK;YACf,CAAC,CAAC,MAAM,MAAM,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;YACjF,CAAC,CAAC,MAAM,MAAM,CAAC,gBAAgB,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAE3F,SAAS,CAAC,KAAK,CAAC,CAAA;QAChB,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACxB,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAA;YACpD,UAAU,EAAE,CAAC,cAAc,CAAC,CAAA;QAC9B,CAAC;IACH,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,KAAK,IAAI,EAAE;QAC9B,IAAI,CAAC,KAAK;YAAE,OAAM;QAClB,WAAW,CAAC,IAAI,CAAC,CAAA;QACjB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,gBAAgB,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;QACvE,WAAW,CAAC,KAAK,CAAC,CAAA;QAClB,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACxB,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;YAC5B,UAAU,EAAE,CAAC,cAAc,CAAC,CAAA;QAC9B,CAAC;IACH,CAAC,CAAA;IAED,IAAI,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,CACL,cAAK,SAAS,EAAC,+DAA+D,YAC5E,KAAC,OAAO,IAAC,SAAS,EAAC,mCAAmC,GAAG,GACrD,CACP,CAAA;IACH,CAAC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,mCAAmC,aAC/C,KAAK,IAAI,CACR,eAAK,SAAS,EAAC,4FAA4F,aACzG,KAAC,aAAa,IAAC,SAAS,EAAC,mCAAmC,GAAG,EAC/D,eAAM,SAAS,EAAC,iCAAiC,YAAE,KAAK,GAAQ,IAC5D,CACP,EAED,eAAK,SAAS,EAAC,MAAM,aACnB,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,cAAc,CAAC,EAC3C,SAAS,EAAC,sGAAsG,aAEhH,KAAC,SAAS,IAAC,SAAS,EAAC,SAAS,GAAG,2BAE1B,EACT,aAAI,SAAS,EAAC,iBAAiB,YAAE,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,iBAAiB,GAAM,IAC/E,EAEN,cAAK,SAAS,EAAC,4DAA4D,YACzE,eAAK,SAAS,EAAC,wBAAwB,aACrC,KAAC,aAAa,IAAC,SAAS,EAAC,sCAAsC,GAAG,EAClE,YAAG,SAAS,EAAC,sBAAsB,8JAG/B,IACA,GACF,EAEN,eAAK,SAAS,EAAC,WAAW,aACxB,eAAK,SAAS,EAAC,uDAAuD,aACpE,0BACE,gBAAO,SAAS,EAAC,gDAAgD,qBAAa,EAC9E,gBACE,IAAI,EAAC,MAAM,EACX,KAAK,EAAE,IAAI,EACX,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACxC,WAAW,EAAC,yCAAyC,EACrD,SAAS,EAAC,yGAAyG,GACnH,IACE,EAEN,0BACE,gBAAO,SAAS,EAAC,gDAAgD,qBAAa,EAC9E,mBACE,KAAK,EAAE,IAAI,EACX,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACxC,WAAW,EAAC,4CAA4C,EACxD,IAAI,EAAE,EAAE,EACR,SAAS,EAAC,mHAAmH,EAC7H,UAAU,EAAE,KAAK,GACjB,EACF,aAAG,SAAS,EAAC,oCAAoC,uDACR,UAAU,cAC/C,IACA,IACF,EAEN,eAAK,SAAS,EAAC,uDAAuD,aACpE,aAAI,SAAS,EAAC,qCAAqC,kCAAuB,EAE1E,0BACE,gBAAO,SAAS,EAAC,gDAAgD,0BAAkB,EACnF,kBACE,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAC7C,SAAS,EAAC,yGAAyG,aAEnH,kBAAQ,KAAK,EAAC,MAAM,oCAAgB,QAAQ,IAAU,EACtD,kBAAQ,KAAK,EAAC,WAAW,8CAA0B,QAAQ,IAAU,EACrE,kBAAQ,KAAK,EAAC,YAAY,0CAAsB,SAAS,IAAU,IAC5D,EACT,aAAG,SAAS,EAAC,oCAAoC,aAC9C,SAAS,KAAK,MAAM,IAAI,wDAAwD,EAChF,SAAS,KAAK,WAAW,IAAI,uDAAuD,EACpF,SAAS,KAAK,YAAY;gDACzB,+DAA+D,IAC/D,IACA,EAEN,0BACE,gBAAO,SAAS,EAAC,gDAAgD,sBAAc,EAC/E,kBACE,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,SAAS,EAAC,yGAAyG,aAEnH,iBAAQ,KAAK,EAAC,MAAM,+BAAwB,EAC5C,iBAAQ,KAAK,EAAC,SAAS,6DAAsD,EAC7E,iBAAQ,KAAK,EAAC,MAAM,4CAAqC,IAClD,IACL,EAEL,KAAK,KAAK,MAAM,IAAI,CACnB,0BACE,gBAAO,SAAS,EAAC,gDAAgD,YAC9D,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,GAC7C,EACR,eAAK,SAAS,EAAC,YAAY,aACzB,gBACE,IAAI,EAAC,MAAM,EACX,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAC7C,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;oDACf,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;wDACtB,CAAC,CAAC,cAAc,EAAE,CAAA;wDAClB,OAAO,EAAE,CAAA;oDACX,CAAC;gDACH,CAAC,EACD,WAAW,EAAC,WAAW,EACvB,SAAS,EAAC,mHAAmH,GAC7H,EACF,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,OAAO,EAChB,SAAS,EAAC,mFAAmF,YAE7F,KAAC,IAAI,IAAC,SAAS,EAAC,SAAS,GAAG,GACrB,IACL,EACL,KAAK,KAAK,SAAS,IAAI,CACtB,YAAG,SAAS,EAAC,oCAAoC,6EAE7C,CACL,EACA,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,CACzB,cAAK,SAAS,EAAC,2BAA2B,YACvC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CACtB,gBAEE,SAAS,EAAC,kGAAkG,aAE3G,CAAC,EACF,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,EAC5B,SAAS,EAAC,0DAA0D,YAEpE,KAAC,CAAC,IAAC,SAAS,EAAC,SAAS,GAAG,GAClB,KAVJ,CAAC,CAWD,CACR,CAAC,GACE,CACP,IACG,CACP,EAED,eAAK,SAAS,EAAC,wBAAwB,aACrC,0BACE,gBAAO,SAAS,EAAC,gDAAgD,yBAAiB,EAClF,gBACE,IAAI,EAAC,QAAQ,EACb,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,EAC/D,GAAG,EAAE,CAAC,EACN,SAAS,EAAC,yGAAyG,GACnH,EACF,YAAG,SAAS,EAAC,oCAAoC,yEAE7C,IACA,EACN,0BACE,gBAAO,SAAS,EAAC,gDAAgD,wBAAgB,EACjF,cAAK,SAAS,EAAC,MAAM,YACnB,KAAC,aAAa,IACZ,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,UAAU,gBACf,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,mBAAmB,GAChE,GACE,IACF,IACF,IACF,EAEN,eAAK,SAAS,EAAC,mCAAmC,aAChD,wBACG,CAAC,KAAK,IAAI,CACT,4BACG,iBAAiB,CAAC,CAAC,CAAC,CACnB,eAAK,SAAS,EAAC,yBAAyB,aACtC,eAAM,SAAS,EAAC,0BAA0B,iCAAwB,EAClE,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAC,uHAAuH,YAEhI,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,GAC9B,EACT,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,oBAAoB,CAAC,KAAK,CAAC,EAC1C,SAAS,EAAC,mEAAmE,uBAGtE,IACL,CACP,CAAC,CAAC,CAAC,CACF,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,EACzC,SAAS,EAAC,6FAA6F,aAEvG,KAAC,MAAM,IAAC,SAAS,EAAC,SAAS,GAAG,cAEvB,CACV,GACA,CACJ,GACG,EACN,eAAK,SAAS,EAAC,yBAAyB,aACtC,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,cAAc,CAAC,EAC3C,SAAS,EAAC,mGAAmG,uBAGtG,EACT,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAC,2HAA2H,YAEpI,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,cAAc,GACtD,IACL,IACF,IACF,IACF,CACP,CAAA;AACH,CAAC"}
|
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react'
|
|
4
|
-
import { ArrowLeft, Loader2, AlertTriangle, Trash2, X, Plus } from 'lucide-react'
|
|
5
|
-
import { toast } from 'sonner'
|
|
6
|
-
import { EnabledSwitch } from '../components/EnabledSwitch.js'
|
|
7
|
-
import { useApiData } from '../lib/useApiData.js'
|
|
8
|
-
import { cmsApi } from '../lib/api.js'
|
|
9
|
-
|
|
10
|
-
export interface ScriptTagEditorProps {
|
|
11
|
-
tagId?: string
|
|
12
|
-
onNavigate?: (path: string) => void
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function ScriptTagEditor({ tagId, onNavigate }: ScriptTagEditorProps) {
|
|
16
|
-
const isNew = !tagId
|
|
17
|
-
const { data, loading, error } = useApiData<any>(tagId ? `/script-tags` : null)
|
|
18
|
-
|
|
19
|
-
const [name, setName] = useState('')
|
|
20
|
-
const [code, setCode] = useState('')
|
|
21
|
-
const [placement, setPlacement] = useState('head')
|
|
22
|
-
const [scope, setScope] = useState('site')
|
|
23
|
-
const [targetPaths, setTargetPaths] = useState<string[]>([])
|
|
24
|
-
const [pathInput, setPathInput] = useState('')
|
|
25
|
-
const [priority, setPriority] = useState(100)
|
|
26
|
-
const [enabled, setEnabled] = useState(true)
|
|
27
|
-
const [saving, setSaving] = useState(false)
|
|
28
|
-
const [deleting, setDeleting] = useState(false)
|
|
29
|
-
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (data && tagId) {
|
|
33
|
-
const tags = Array.isArray(data) ? data : []
|
|
34
|
-
const tag = tags.find((t: any) => t.id === tagId)
|
|
35
|
-
if (tag) {
|
|
36
|
-
setName(tag.name ?? '')
|
|
37
|
-
setCode(tag.code ?? '')
|
|
38
|
-
setPlacement(tag.placement ?? 'head')
|
|
39
|
-
setScope(tag.scope ?? 'site')
|
|
40
|
-
setTargetPaths(tag.targetPaths ?? [])
|
|
41
|
-
setPriority(tag.priority ?? 100)
|
|
42
|
-
setEnabled(tag.enabled ?? true)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}, [data, tagId])
|
|
46
|
-
|
|
47
|
-
const addPath = () => {
|
|
48
|
-
const trimmed = pathInput.trim()
|
|
49
|
-
if (!trimmed) return
|
|
50
|
-
const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
|
|
51
|
-
if (!targetPaths.includes(normalized)) {
|
|
52
|
-
setTargetPaths([...targetPaths, normalized])
|
|
53
|
-
}
|
|
54
|
-
setPathInput('')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const removePath = (path: string) => {
|
|
58
|
-
setTargetPaths(targetPaths.filter((p) => p !== path))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const handleSave = async () => {
|
|
62
|
-
if (!name.trim()) {
|
|
63
|
-
toast.error('Name is required')
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
if (!code.trim()) {
|
|
67
|
-
toast.error('Code snippet is required')
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
if (scope !== 'site' && targetPaths.length === 0) {
|
|
71
|
-
toast.error('At least one target path is required for this scope')
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
setSaving(true)
|
|
76
|
-
const payload = {
|
|
77
|
-
name: name.trim(),
|
|
78
|
-
code,
|
|
79
|
-
placement,
|
|
80
|
-
scope,
|
|
81
|
-
targetPaths: scope === 'site' ? [] : targetPaths,
|
|
82
|
-
priority,
|
|
83
|
-
enabled,
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const res = isNew
|
|
87
|
-
? await cmsApi('/script-tags', { method: 'POST', body: JSON.stringify(payload) })
|
|
88
|
-
: await cmsApi(`/script-tags/${tagId}`, { method: 'PUT', body: JSON.stringify(payload) })
|
|
89
|
-
|
|
90
|
-
setSaving(false)
|
|
91
|
-
if (res.error) {
|
|
92
|
-
toast.error(res.error)
|
|
93
|
-
} else {
|
|
94
|
-
toast.success(isNew ? 'Tag created' : 'Tag updated')
|
|
95
|
-
onNavigate?.('/script-tags')
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const handleDelete = async () => {
|
|
100
|
-
if (!tagId) return
|
|
101
|
-
setDeleting(true)
|
|
102
|
-
const res = await cmsApi(`/script-tags/${tagId}`, { method: 'DELETE' })
|
|
103
|
-
setDeleting(false)
|
|
104
|
-
if (res.error) {
|
|
105
|
-
toast.error(res.error)
|
|
106
|
-
} else {
|
|
107
|
-
toast.success('Tag deleted')
|
|
108
|
-
onNavigate?.('/script-tags')
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (loading && !isNew) {
|
|
113
|
-
return (
|
|
114
|
-
<div className="flex h-64 items-center justify-center p-3 pr-6 sm:p-4 sm:pr-8">
|
|
115
|
-
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
|
116
|
-
</div>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<div className="max-w-3xl p-3 pr-6 sm:p-4 sm:pr-8">
|
|
122
|
-
{error && (
|
|
123
|
-
<div className="border-destructive/30 bg-destructive/10 mb-4 flex items-center gap-3 rounded-lg border p-3">
|
|
124
|
-
<AlertTriangle className="text-destructive h-5 w-5 shrink-0" />
|
|
125
|
-
<span className="text-destructive flex-1 text-sm">{error}</span>
|
|
126
|
-
</div>
|
|
127
|
-
)}
|
|
128
|
-
|
|
129
|
-
<div className="mb-6">
|
|
130
|
-
<button
|
|
131
|
-
type="button"
|
|
132
|
-
onClick={() => onNavigate?.('/script-tags')}
|
|
133
|
-
className="text-muted-foreground hover:text-foreground mb-3 flex items-center gap-1.5 text-sm transition-colors"
|
|
134
|
-
>
|
|
135
|
-
<ArrowLeft className="h-4 w-4" />
|
|
136
|
-
Back to Script Tags
|
|
137
|
-
</button>
|
|
138
|
-
<h1 className="text-foreground">{isNew ? 'New Script Tag' : 'Edit Script Tag'}</h1>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<div className="border-warning/30 bg-warning/10 mb-6 rounded-lg border p-3">
|
|
142
|
-
<div className="flex items-start gap-2">
|
|
143
|
-
<AlertTriangle className="text-warning mt-0.5 h-4 w-4 shrink-0" />
|
|
144
|
-
<p className="text-warning text-sm">
|
|
145
|
-
This code will run on public pages matching the scope below. Only add trusted code from
|
|
146
|
-
verified sources (e.g. Google Analytics, Meta Pixel).
|
|
147
|
-
</p>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
<div className="space-y-6">
|
|
152
|
-
<div className="border-border bg-card space-y-4 rounded-lg border p-4">
|
|
153
|
-
<div>
|
|
154
|
-
<label className="text-foreground mb-1 block text-sm font-medium">Name</label>
|
|
155
|
-
<input
|
|
156
|
-
type="text"
|
|
157
|
-
value={name}
|
|
158
|
-
onChange={(e) => setName(e.target.value)}
|
|
159
|
-
placeholder="e.g. Google Tag Manager, Facebook Pixel"
|
|
160
|
-
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
161
|
-
/>
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
<div>
|
|
165
|
-
<label className="text-foreground mb-1 block text-sm font-medium">Code</label>
|
|
166
|
-
<textarea
|
|
167
|
-
value={code}
|
|
168
|
-
onChange={(e) => setCode(e.target.value)}
|
|
169
|
-
placeholder="Paste your HTML/JavaScript snippet here..."
|
|
170
|
-
rows={10}
|
|
171
|
-
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 font-mono text-sm focus:ring-2 focus:outline-none"
|
|
172
|
-
spellCheck={false}
|
|
173
|
-
/>
|
|
174
|
-
<p className="text-muted-foreground mt-1 text-xs">
|
|
175
|
-
Paste the full code snippet including {'<script>'} tags.
|
|
176
|
-
</p>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
|
|
180
|
-
<div className="border-border bg-card space-y-4 rounded-lg border p-4">
|
|
181
|
-
<h3 className="text-foreground text-sm font-medium">Placement & Scope</h3>
|
|
182
|
-
|
|
183
|
-
<div>
|
|
184
|
-
<label className="text-foreground mb-1 block text-sm font-medium">Placement</label>
|
|
185
|
-
<select
|
|
186
|
-
value={placement}
|
|
187
|
-
onChange={(e) => setPlacement(e.target.value)}
|
|
188
|
-
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
189
|
-
>
|
|
190
|
-
<option value="head">Head — inside {'<head>'}</option>
|
|
191
|
-
<option value="body_open">Body Open — right after {'<body>'}</option>
|
|
192
|
-
<option value="body_close">Body Close — before {'</body>'}</option>
|
|
193
|
-
</select>
|
|
194
|
-
<p className="text-muted-foreground mt-1 text-xs">
|
|
195
|
-
{placement === 'head' && 'Best for analytics scripts, meta tags, and custom CSS.'}
|
|
196
|
-
{placement === 'body_open' && 'Best for GTM noscript tags and early-loading scripts.'}
|
|
197
|
-
{placement === 'body_close' &&
|
|
198
|
-
'Best for chat widgets, deferred scripts, and tracking pixels.'}
|
|
199
|
-
</p>
|
|
200
|
-
</div>
|
|
201
|
-
|
|
202
|
-
<div>
|
|
203
|
-
<label className="text-foreground mb-1 block text-sm font-medium">Scope</label>
|
|
204
|
-
<select
|
|
205
|
-
value={scope}
|
|
206
|
-
onChange={(e) => setScope(e.target.value)}
|
|
207
|
-
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
208
|
-
>
|
|
209
|
-
<option value="site">Entire Website</option>
|
|
210
|
-
<option value="parents">Specific Parent Pages (includes child pages)</option>
|
|
211
|
-
<option value="urls">Specific URLs (exact match)</option>
|
|
212
|
-
</select>
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
{scope !== 'site' && (
|
|
216
|
-
<div>
|
|
217
|
-
<label className="text-foreground mb-1 block text-sm font-medium">
|
|
218
|
-
{scope === 'parents' ? 'Parent Paths' : 'URL Paths'}
|
|
219
|
-
</label>
|
|
220
|
-
<div className="flex gap-2">
|
|
221
|
-
<input
|
|
222
|
-
type="text"
|
|
223
|
-
value={pathInput}
|
|
224
|
-
onChange={(e) => setPathInput(e.target.value)}
|
|
225
|
-
onKeyDown={(e) => {
|
|
226
|
-
if (e.key === 'Enter') {
|
|
227
|
-
e.preventDefault()
|
|
228
|
-
addPath()
|
|
229
|
-
}
|
|
230
|
-
}}
|
|
231
|
-
placeholder="/services"
|
|
232
|
-
className="border-input focus:ring-ring flex-1 rounded-lg border px-3 py-2 font-mono text-sm focus:ring-2 focus:outline-none"
|
|
233
|
-
/>
|
|
234
|
-
<button
|
|
235
|
-
type="button"
|
|
236
|
-
onClick={addPath}
|
|
237
|
-
className="border-input hover:bg-muted rounded-lg border px-3 py-2 text-sm transition-colors"
|
|
238
|
-
>
|
|
239
|
-
<Plus className="h-4 w-4" />
|
|
240
|
-
</button>
|
|
241
|
-
</div>
|
|
242
|
-
{scope === 'parents' && (
|
|
243
|
-
<p className="text-muted-foreground mt-1 text-xs">
|
|
244
|
-
This tag will also apply to all child pages under each path.
|
|
245
|
-
</p>
|
|
246
|
-
)}
|
|
247
|
-
{targetPaths.length > 0 && (
|
|
248
|
-
<div className="mt-2 flex flex-wrap gap-2">
|
|
249
|
-
{targetPaths.map((p) => (
|
|
250
|
-
<span
|
|
251
|
-
key={p}
|
|
252
|
-
className="bg-muted text-foreground inline-flex items-center gap-1 rounded-full px-3 py-1 font-mono text-xs"
|
|
253
|
-
>
|
|
254
|
-
{p}
|
|
255
|
-
<button
|
|
256
|
-
type="button"
|
|
257
|
-
onClick={() => removePath(p)}
|
|
258
|
-
className="text-muted-foreground hover:text-muted-foreground ml-0.5"
|
|
259
|
-
>
|
|
260
|
-
<X className="h-3 w-3" />
|
|
261
|
-
</button>
|
|
262
|
-
</span>
|
|
263
|
-
))}
|
|
264
|
-
</div>
|
|
265
|
-
)}
|
|
266
|
-
</div>
|
|
267
|
-
)}
|
|
268
|
-
|
|
269
|
-
<div className="grid grid-cols-2 gap-4">
|
|
270
|
-
<div>
|
|
271
|
-
<label className="text-foreground mb-1 block text-sm font-medium">Priority</label>
|
|
272
|
-
<input
|
|
273
|
-
type="number"
|
|
274
|
-
value={priority}
|
|
275
|
-
onChange={(e) => setPriority(parseInt(e.target.value, 10) || 0)}
|
|
276
|
-
min={0}
|
|
277
|
-
className="border-input focus:ring-ring w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:outline-none"
|
|
278
|
-
/>
|
|
279
|
-
<p className="text-muted-foreground mt-1 text-xs">
|
|
280
|
-
Lower numbers load first (e.g. 1 = first, 100 = default)
|
|
281
|
-
</p>
|
|
282
|
-
</div>
|
|
283
|
-
<div>
|
|
284
|
-
<label className="text-foreground mb-1 block text-sm font-medium">Enabled</label>
|
|
285
|
-
<div className="pt-2">
|
|
286
|
-
<EnabledSwitch
|
|
287
|
-
checked={enabled}
|
|
288
|
-
onCheckedChange={setEnabled}
|
|
289
|
-
aria-label={enabled ? 'Disable script tag' : 'Enable script tag'}
|
|
290
|
-
/>
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
|
|
296
|
-
<div className="flex items-center justify-between">
|
|
297
|
-
<div>
|
|
298
|
-
{!isNew && (
|
|
299
|
-
<>
|
|
300
|
-
{showDeleteConfirm ? (
|
|
301
|
-
<div className="flex items-center gap-2">
|
|
302
|
-
<span className="text-destructive text-sm">Delete this tag?</span>
|
|
303
|
-
<button
|
|
304
|
-
type="button"
|
|
305
|
-
onClick={handleDelete}
|
|
306
|
-
disabled={deleting}
|
|
307
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded-lg px-3 py-1.5 text-sm disabled:opacity-50"
|
|
308
|
-
>
|
|
309
|
-
{deleting ? 'Deleting...' : 'Confirm'}
|
|
310
|
-
</button>
|
|
311
|
-
<button
|
|
312
|
-
type="button"
|
|
313
|
-
onClick={() => setShowDeleteConfirm(false)}
|
|
314
|
-
className="border-input hover:bg-muted rounded-lg border px-3 py-1.5 text-sm"
|
|
315
|
-
>
|
|
316
|
-
Cancel
|
|
317
|
-
</button>
|
|
318
|
-
</div>
|
|
319
|
-
) : (
|
|
320
|
-
<button
|
|
321
|
-
type="button"
|
|
322
|
-
onClick={() => setShowDeleteConfirm(true)}
|
|
323
|
-
className="text-destructive hover:text-destructive flex items-center gap-1.5 text-sm transition-colors"
|
|
324
|
-
>
|
|
325
|
-
<Trash2 className="h-4 w-4" />
|
|
326
|
-
Delete
|
|
327
|
-
</button>
|
|
328
|
-
)}
|
|
329
|
-
</>
|
|
330
|
-
)}
|
|
331
|
-
</div>
|
|
332
|
-
<div className="flex items-center gap-3">
|
|
333
|
-
<button
|
|
334
|
-
type="button"
|
|
335
|
-
onClick={() => onNavigate?.('/script-tags')}
|
|
336
|
-
className="border-input text-foreground hover:bg-muted rounded-lg border px-4 py-2 text-sm transition-colors"
|
|
337
|
-
>
|
|
338
|
-
Cancel
|
|
339
|
-
</button>
|
|
340
|
-
<button
|
|
341
|
-
type="button"
|
|
342
|
-
onClick={handleSave}
|
|
343
|
-
disabled={saving}
|
|
344
|
-
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-lg px-6 py-2 text-sm transition-colors disabled:opacity-50"
|
|
345
|
-
>
|
|
346
|
-
{saving ? 'Saving...' : isNew ? 'Create Tag' : 'Save Changes'}
|
|
347
|
-
</button>
|
|
348
|
-
</div>
|
|
349
|
-
</div>
|
|
350
|
-
</div>
|
|
351
|
-
</div>
|
|
352
|
-
)
|
|
353
|
-
}
|