@prenta/admin 1.12.0 → 1.14.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 +43 -0
- package/dist/__tests__/lib/seo-service-errors.test.js +34 -1
- package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -1
- package/dist/__tests__/lib/seo-service-proposals.test.d.ts +2 -0
- package/dist/__tests__/lib/seo-service-proposals.test.d.ts.map +1 -0
- package/dist/__tests__/lib/seo-service-proposals.test.js +71 -0
- package/dist/__tests__/lib/seo-service-proposals.test.js.map +1 -0
- package/dist/__tests__/router/seo-tab-for-path.test.js +1 -0
- package/dist/__tests__/router/seo-tab-for-path.test.js.map +1 -1
- package/dist/__tests__/views/seo-proposals-tab.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.js +292 -0
- package/dist/__tests__/views/seo-proposals-tab.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-settings.render.test.js +73 -6
- package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
- package/dist/components/seo/ChangePreview.d.ts +6 -0
- package/dist/components/seo/ChangePreview.d.ts.map +1 -0
- package/dist/components/seo/ChangePreview.js +18 -0
- package/dist/components/seo/ChangePreview.js.map +1 -0
- package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
- package/dist/components/seo/SeoIssueFixPanel.js +1 -14
- package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
- package/dist/lib/seo-service.d.ts +106 -0
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +67 -0
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/SEO.d.ts +5 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +28 -9
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/seo/ProposalsTab.d.ts +7 -0
- package/dist/views/seo/ProposalsTab.d.ts.map +1 -0
- package/dist/views/seo/ProposalsTab.js +298 -0
- package/dist/views/seo/ProposalsTab.js.map +1 -0
- package/dist/views/settings/SeoDigestCard.d.ts +9 -0
- package/dist/views/settings/SeoDigestCard.d.ts.map +1 -0
- package/dist/views/settings/SeoDigestCard.js +77 -0
- package/dist/views/settings/SeoDigestCard.js.map +1 -0
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
- package/dist/views/settings/SeoSettingsTab.js +2 -1
- package/dist/views/settings/SeoSettingsTab.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/lib/seo-service-errors.test.ts +38 -0
- package/src/__tests__/lib/seo-service-proposals.test.ts +79 -0
- package/src/__tests__/router/seo-tab-for-path.test.ts +1 -0
- package/src/__tests__/views/seo-proposals-tab.render.test.tsx +370 -0
- package/src/__tests__/views/seo-settings.render.test.tsx +104 -5
- package/src/components/seo/ChangePreview.tsx +47 -0
- package/src/components/seo/SeoIssueFixPanel.tsx +1 -44
- package/src/lib/seo-service.ts +168 -0
- package/src/views/SEO.tsx +40 -7
- package/src/views/seo/ProposalsTab.tsx +681 -0
- package/src/views/settings/SeoDigestCard.tsx +164 -0
- package/src/views/settings/SeoSettingsTab.tsx +2 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useId, useState } from 'react'
|
|
4
|
+
import { Loader2, Send } from 'lucide-react'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import {
|
|
7
|
+
fetchDigestSettings,
|
|
8
|
+
sendTestDigest,
|
|
9
|
+
updateDigestSettings,
|
|
10
|
+
type SeoDigestSettings,
|
|
11
|
+
} from '../../lib/seo-service.js'
|
|
12
|
+
import { SettingsCard, SettingsToggleRow } from './components.js'
|
|
13
|
+
|
|
14
|
+
const INPUT_CLASS =
|
|
15
|
+
'w-full rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Settings → SEO "Weekly SEO digest" card: opt-in toggle, explicit recipient
|
|
19
|
+
* list, last-sent status, and a rate-limited test send. The digest itself is
|
|
20
|
+
* sent by the Monday /cron/seo-scan tick after the persisted audit.
|
|
21
|
+
*/
|
|
22
|
+
export function SeoDigestCard({ canEdit }: { canEdit: boolean }) {
|
|
23
|
+
const recipientsId = useId()
|
|
24
|
+
const [settings, setSettings] = useState<SeoDigestSettings | null>(null)
|
|
25
|
+
const [loading, setLoading] = useState(true)
|
|
26
|
+
const [recipientsText, setRecipientsText] = useState('')
|
|
27
|
+
const [saving, setSaving] = useState(false)
|
|
28
|
+
const [sendingTest, setSendingTest] = useState(false)
|
|
29
|
+
|
|
30
|
+
const load = useCallback(async () => {
|
|
31
|
+
setLoading(true)
|
|
32
|
+
const s = await fetchDigestSettings()
|
|
33
|
+
if (s) {
|
|
34
|
+
setSettings(s)
|
|
35
|
+
setRecipientsText(s.recipients.join(', '))
|
|
36
|
+
}
|
|
37
|
+
setLoading(false)
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
void load()
|
|
42
|
+
}, [load])
|
|
43
|
+
|
|
44
|
+
async function handleToggle(enabled: boolean) {
|
|
45
|
+
const res = await updateDigestSettings({ enabled })
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
toast.error(res.error ?? 'Failed to update digest settings')
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
toast.success(enabled ? 'Weekly digest enabled' : 'Weekly digest disabled')
|
|
51
|
+
await load()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function handleSaveRecipients() {
|
|
55
|
+
setSaving(true)
|
|
56
|
+
const recipients = recipientsText
|
|
57
|
+
.split(',')
|
|
58
|
+
.map((r) => r.trim())
|
|
59
|
+
.filter((r) => r.length > 0)
|
|
60
|
+
const res = await updateDigestSettings({ recipients })
|
|
61
|
+
setSaving(false)
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
toast.error(res.error ?? 'Failed to save recipients')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
toast.success('Digest recipients saved')
|
|
67
|
+
await load()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function handleTestSend() {
|
|
71
|
+
setSendingTest(true)
|
|
72
|
+
const res = await sendTestDigest()
|
|
73
|
+
setSendingTest(false)
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
toast.error(res.error ?? 'Test digest failed')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
toast.success(`Test digest sent to ${res.to ?? 'your email'}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (loading) {
|
|
82
|
+
return (
|
|
83
|
+
<SettingsCard title="Weekly SEO digest">
|
|
84
|
+
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
|
85
|
+
<Loader2 size={16} className="motion-safe:animate-spin" aria-hidden="true" />
|
|
86
|
+
Loading…
|
|
87
|
+
</div>
|
|
88
|
+
</SettingsCard>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
if (!settings) {
|
|
92
|
+
return (
|
|
93
|
+
<SettingsCard title="Weekly SEO digest">
|
|
94
|
+
<p className="text-destructive text-sm">Failed to load digest settings.</p>
|
|
95
|
+
</SettingsCard>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<SettingsCard
|
|
101
|
+
title="Weekly SEO digest"
|
|
102
|
+
description="A Monday email with the site score, search performance, quick wins, new issues, and pending proposals. Sent by the weekly audit cron."
|
|
103
|
+
>
|
|
104
|
+
<div className="flex flex-col gap-4">
|
|
105
|
+
<SettingsToggleRow
|
|
106
|
+
label="Send weekly digest"
|
|
107
|
+
description="Requires at least one recipient below."
|
|
108
|
+
checked={settings.enabled}
|
|
109
|
+
onChange={handleToggle}
|
|
110
|
+
disabled={!canEdit}
|
|
111
|
+
/>
|
|
112
|
+
<div>
|
|
113
|
+
<label
|
|
114
|
+
htmlFor={recipientsId}
|
|
115
|
+
className="text-foreground mb-1 flex items-center gap-2 text-sm font-medium"
|
|
116
|
+
>
|
|
117
|
+
Recipients
|
|
118
|
+
</label>
|
|
119
|
+
<input
|
|
120
|
+
id={recipientsId}
|
|
121
|
+
type="text"
|
|
122
|
+
className={INPUT_CLASS}
|
|
123
|
+
placeholder="owner@example.com, seo@example.com"
|
|
124
|
+
value={recipientsText}
|
|
125
|
+
onChange={(e) => setRecipientsText(e.target.value)}
|
|
126
|
+
disabled={!canEdit}
|
|
127
|
+
/>
|
|
128
|
+
<p className="text-muted-foreground mt-1 text-sm">Comma-separated, up to 10 addresses.</p>
|
|
129
|
+
</div>
|
|
130
|
+
<div className="flex items-center gap-3">
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
className="bg-primary text-primary-foreground rounded-md px-4 py-2 text-base font-medium disabled:cursor-not-allowed disabled:opacity-60"
|
|
134
|
+
onClick={() => void handleSaveRecipients()}
|
|
135
|
+
disabled={!canEdit || saving}
|
|
136
|
+
>
|
|
137
|
+
{saving ? 'Saving…' : 'Save recipients'}
|
|
138
|
+
</button>
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
className="border-border text-foreground inline-flex items-center gap-2 rounded-md border px-4 py-2 text-base font-medium disabled:cursor-not-allowed disabled:opacity-60"
|
|
142
|
+
onClick={() => void handleTestSend()}
|
|
143
|
+
disabled={!canEdit || sendingTest}
|
|
144
|
+
>
|
|
145
|
+
{sendingTest ? (
|
|
146
|
+
<Loader2 size={16} className="motion-safe:animate-spin" aria-hidden="true" />
|
|
147
|
+
) : (
|
|
148
|
+
<Send size={16} aria-hidden="true" />
|
|
149
|
+
)}
|
|
150
|
+
Send test digest
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
<p className="text-muted-foreground text-sm">
|
|
154
|
+
{settings.lastSentAt
|
|
155
|
+
? `Last sent ${new Date(settings.lastSentAt).toLocaleString()}`
|
|
156
|
+
: 'Never sent'}
|
|
157
|
+
{settings.lastError ? (
|
|
158
|
+
<span className="text-destructive"> · {settings.lastError}</span>
|
|
159
|
+
) : null}
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
</SettingsCard>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -17,6 +17,7 @@ import { siteUrlMatchesAdminOrigin } from '../../lib/preview-link.js'
|
|
|
17
17
|
import { MediaPickerModal } from '../../components/MediaPickerModal.js'
|
|
18
18
|
import { SEOConfigPanel } from '../../components/SEOConfigPanel.js'
|
|
19
19
|
import { SearchDataCard } from './SearchDataCard.js'
|
|
20
|
+
import { SeoDigestCard } from './SeoDigestCard.js'
|
|
20
21
|
import {
|
|
21
22
|
ConfirmDangerousSettingDialog,
|
|
22
23
|
SettingsCard,
|
|
@@ -151,6 +152,7 @@ export function SeoSettingsTab({ onNavigate, role }: SeoSettingsTabProps) {
|
|
|
151
152
|
<OrganizationCard seo={seo} canEdit={canEdit} />
|
|
152
153
|
<SearchEngineVerificationCard seo={seo} canEdit={canEdit} />
|
|
153
154
|
<SearchDataCard canEdit={canEdit} />
|
|
155
|
+
<SeoDigestCard canEdit={canEdit} />
|
|
154
156
|
<SitemapDefaultsCard seo={seo} canEdit={canEdit} onNavigate={onNavigate} />
|
|
155
157
|
|
|
156
158
|
<details className="border-border bg-card group rounded-lg border">
|