@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/__tests__/lib/seo-service-errors.test.js +34 -1
  3. package/dist/__tests__/lib/seo-service-errors.test.js.map +1 -1
  4. package/dist/__tests__/lib/seo-service-proposals.test.d.ts +2 -0
  5. package/dist/__tests__/lib/seo-service-proposals.test.d.ts.map +1 -0
  6. package/dist/__tests__/lib/seo-service-proposals.test.js +71 -0
  7. package/dist/__tests__/lib/seo-service-proposals.test.js.map +1 -0
  8. package/dist/__tests__/router/seo-tab-for-path.test.js +1 -0
  9. package/dist/__tests__/router/seo-tab-for-path.test.js.map +1 -1
  10. package/dist/__tests__/views/seo-proposals-tab.render.test.d.ts +2 -0
  11. package/dist/__tests__/views/seo-proposals-tab.render.test.d.ts.map +1 -0
  12. package/dist/__tests__/views/seo-proposals-tab.render.test.js +292 -0
  13. package/dist/__tests__/views/seo-proposals-tab.render.test.js.map +1 -0
  14. package/dist/__tests__/views/seo-settings.render.test.js +73 -6
  15. package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
  16. package/dist/components/seo/ChangePreview.d.ts +6 -0
  17. package/dist/components/seo/ChangePreview.d.ts.map +1 -0
  18. package/dist/components/seo/ChangePreview.js +18 -0
  19. package/dist/components/seo/ChangePreview.js.map +1 -0
  20. package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
  21. package/dist/components/seo/SeoIssueFixPanel.js +1 -14
  22. package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
  23. package/dist/lib/seo-service.d.ts +106 -0
  24. package/dist/lib/seo-service.d.ts.map +1 -1
  25. package/dist/lib/seo-service.js +67 -0
  26. package/dist/lib/seo-service.js.map +1 -1
  27. package/dist/prenta-admin.css +1 -1
  28. package/dist/views/SEO.d.ts +5 -1
  29. package/dist/views/SEO.d.ts.map +1 -1
  30. package/dist/views/SEO.js +28 -9
  31. package/dist/views/SEO.js.map +1 -1
  32. package/dist/views/seo/ProposalsTab.d.ts +7 -0
  33. package/dist/views/seo/ProposalsTab.d.ts.map +1 -0
  34. package/dist/views/seo/ProposalsTab.js +298 -0
  35. package/dist/views/seo/ProposalsTab.js.map +1 -0
  36. package/dist/views/settings/SeoDigestCard.d.ts +9 -0
  37. package/dist/views/settings/SeoDigestCard.d.ts.map +1 -0
  38. package/dist/views/settings/SeoDigestCard.js +77 -0
  39. package/dist/views/settings/SeoDigestCard.js.map +1 -0
  40. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  41. package/dist/views/settings/SeoSettingsTab.js +2 -1
  42. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/__tests__/lib/seo-service-errors.test.ts +38 -0
  45. package/src/__tests__/lib/seo-service-proposals.test.ts +79 -0
  46. package/src/__tests__/router/seo-tab-for-path.test.ts +1 -0
  47. package/src/__tests__/views/seo-proposals-tab.render.test.tsx +370 -0
  48. package/src/__tests__/views/seo-settings.render.test.tsx +104 -5
  49. package/src/components/seo/ChangePreview.tsx +47 -0
  50. package/src/components/seo/SeoIssueFixPanel.tsx +1 -44
  51. package/src/lib/seo-service.ts +168 -0
  52. package/src/views/SEO.tsx +40 -7
  53. package/src/views/seo/ProposalsTab.tsx +681 -0
  54. package/src/views/settings/SeoDigestCard.tsx +164 -0
  55. 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">