@morphika/andami 0.5.1 → 0.5.2

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 (117) hide show
  1. package/app/admin/assets/page.tsx +6 -6
  2. package/app/admin/database/page.tsx +302 -302
  3. package/app/admin/error.tsx +53 -53
  4. package/app/admin/layout.tsx +320 -320
  5. package/app/admin/navigation/page.tsx +255 -255
  6. package/app/admin/pages/[slug]/page.tsx +6 -6
  7. package/app/admin/pages/page.tsx +11 -11
  8. package/app/admin/projects/page.tsx +14 -14
  9. package/app/admin/setup/page.tsx +1 -1
  10. package/app/admin/styles/page.tsx +1 -1
  11. package/components/admin/MetadataEditor.tsx +6 -6
  12. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  13. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  14. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  15. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  16. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  17. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  18. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  19. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  20. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  21. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  22. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  23. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  24. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  25. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  26. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  27. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  28. package/components/admin/styles/ColorsEditor.tsx +2 -2
  29. package/components/admin/styles/FontsEditor.tsx +6 -6
  30. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  31. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  32. package/components/admin/styles/TypographyEditor.tsx +6 -6
  33. package/components/admin/styles/shared.tsx +68 -68
  34. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  35. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  36. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  37. package/components/builder/BlockCardIcons.tsx +316 -316
  38. package/components/builder/BlockTypePicker.tsx +1 -1
  39. package/components/builder/BubbleIcons.tsx +90 -0
  40. package/components/builder/BuilderCanvas.tsx +2 -0
  41. package/components/builder/CanvasMinimap.tsx +2 -2
  42. package/components/builder/CoverSectionCanvas.tsx +363 -363
  43. package/components/builder/DeviceFrame.tsx +1 -1
  44. package/components/builder/DndWrapper.tsx +3 -3
  45. package/components/builder/InsertionLines.tsx +1 -1
  46. package/components/builder/SectionCardIcons.tsx +421 -320
  47. package/components/builder/SectionEditorBar.tsx +1 -1
  48. package/components/builder/SectionTypePicker.tsx +4 -4
  49. package/components/builder/SectionV2Canvas.tsx +1 -1
  50. package/components/builder/SectionV2Column.tsx +69 -67
  51. package/components/builder/SortableBlock.tsx +93 -73
  52. package/components/builder/SortableRow.tsx +27 -26
  53. package/components/builder/VirtualAssetGrid.tsx +2 -2
  54. package/components/builder/asset-browser/R2BrowserContent.tsx +11 -11
  55. package/components/builder/blockStyles.tsx +192 -185
  56. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  57. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  58. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  59. package/components/builder/color-picker/HueSlider.tsx +124 -124
  60. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  61. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  62. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  63. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  64. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  65. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  66. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  67. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  68. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  69. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  70. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  71. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  72. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  73. package/components/builder/editors/StaggerSettings.tsx +109 -109
  74. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  75. package/components/builder/editors/TextStylePicker.tsx +1 -1
  76. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  77. package/components/builder/editors/index.ts +11 -10
  78. package/components/builder/editors/shared.tsx +6 -6
  79. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  80. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  81. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  82. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  83. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  84. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  85. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  86. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  87. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  88. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  89. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  90. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  91. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  92. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  93. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  94. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  95. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  96. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  97. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  98. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  99. package/lib/animation/enter-types.ts +1 -0
  100. package/lib/animation/hover-effect-presets.ts +210 -210
  101. package/lib/animation/hover-effect-types.ts +1 -0
  102. package/lib/builder/block-registrations.ts +468 -417
  103. package/lib/builder/constants.ts +111 -111
  104. package/lib/builder/store-sections.ts +2 -2
  105. package/lib/builder/types-slices.ts +414 -414
  106. package/lib/builder/types.ts +4 -1
  107. package/lib/config/index.ts +27 -27
  108. package/lib/sanity/types.ts +98 -1
  109. package/lib/version.ts +1 -1
  110. package/package.json +1 -1
  111. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  112. package/sanity/schemas/blocks/index.ts +12 -11
  113. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  114. package/sanity/schemas/index.ts +120 -117
  115. package/styles/admin.css +85 -85
  116. package/styles/animations.css +237 -237
  117. package/styles/base.css +114 -114
@@ -1,302 +1,302 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useCallback } from "react";
4
-
5
- // ── Types ──
6
-
7
- interface SanityStats {
8
- totalDocuments: number;
9
- pages: number;
10
- publishedPages: number;
11
- draftPages: number;
12
- projects: number;
13
- siteSettings: number;
14
- siteStyles: number;
15
- assetRegistry: number;
16
- datasets: string[];
17
- }
18
-
19
- interface SanityStatus {
20
- connected: boolean;
21
- projectId: string;
22
- dataset: string;
23
- hasWriteToken: boolean;
24
- stats: SanityStats | null;
25
- error?: string;
26
- apiVersion: string;
27
- }
28
-
29
- // ── Stat Card ──
30
-
31
- function StatCard({ label, value, sub }: { label: string; value: number | string; sub?: string }) {
32
- return (
33
- <div className="bg-white rounded-xl border border-neutral-200 p-4">
34
- <p className="text-[11px] uppercase tracking-wider text-neutral-400 mb-1">{label}</p>
35
- <p className="text-2xl font-semibold text-neutral-900">{value}</p>
36
- {sub && <p className="text-xs text-neutral-400 mt-0.5">{sub}</p>}
37
- </div>
38
- );
39
- }
40
-
41
- // ── Config Field (read-only display) ──
42
-
43
- function ConfigField({ label, value, masked }: { label: string; value: string; masked?: boolean }) {
44
- const [revealed, setRevealed] = useState(false);
45
- const display = masked && !revealed ? "••••••••••••" : value || "—";
46
-
47
- return (
48
- <div className="flex items-center justify-between py-3 border-b border-neutral-100 last:border-0">
49
- <span className="text-sm text-neutral-500">{label}</span>
50
- <div className="flex items-center gap-2">
51
- <code className="text-sm text-neutral-800 bg-neutral-50 px-2 py-0.5 rounded font-mono">{display}</code>
52
- {masked && value && (
53
- <button
54
- onClick={() => setRevealed(!revealed)}
55
- className="text-xs text-[#076bff] hover:underline"
56
- >
57
- {revealed ? "Hide" : "Show"}
58
- </button>
59
- )}
60
- </div>
61
- </div>
62
- );
63
- }
64
-
65
- // ── Document Type Row ──
66
-
67
- function DocTypeRow({ label, count, icon }: { label: string; count: number; icon: React.ReactNode }) {
68
- return (
69
- <div className="flex items-center justify-between py-2.5 border-b border-neutral-100 last:border-0">
70
- <div className="flex items-center gap-2.5">
71
- <span className="text-neutral-400">{icon}</span>
72
- <span className="text-sm text-neutral-700">{label}</span>
73
- </div>
74
- <span className="text-sm font-medium text-neutral-900 bg-neutral-100 rounded-full px-2.5 py-0.5 min-w-[32px] text-center">
75
- {count}
76
- </span>
77
- </div>
78
- );
79
- }
80
-
81
- // ── Main Page ──
82
-
83
- export default function AdminDatabasePage() {
84
- const [status, setStatus] = useState<SanityStatus | null>(null);
85
- const [loading, setLoading] = useState(true);
86
- const [testing, setTesting] = useState(false);
87
-
88
- const fetchStatus = useCallback(async () => {
89
- try {
90
- const res = await fetch("/api/admin/database");
91
- if (res.ok) {
92
- const data = await res.json();
93
- setStatus(data);
94
- }
95
- } catch {
96
- setStatus(null);
97
- } finally {
98
- setLoading(false);
99
- }
100
- }, []);
101
-
102
- useEffect(() => {
103
- fetchStatus();
104
- }, [fetchStatus]);
105
-
106
- const handleTestConnection = async () => {
107
- setTesting(true);
108
- setLoading(false);
109
- try {
110
- const res = await fetch("/api/admin/database");
111
- if (res.ok) {
112
- const data = await res.json();
113
- setStatus(data);
114
- }
115
- } catch {
116
- // handled by status state
117
- } finally {
118
- setTesting(false);
119
- }
120
- };
121
-
122
- // ── Loading ──
123
- if (loading) {
124
- return (
125
- <div className="flex items-center justify-center py-20">
126
- <span className="text-sm text-neutral-400 animate-pulse">Connecting to database...</span>
127
- </div>
128
- );
129
- }
130
-
131
- const stats = status?.stats;
132
- const isConnected = status?.connected === true;
133
-
134
- return (
135
- <div className="space-y-8 max-w-4xl">
136
- {/* Header */}
137
- <div className="flex items-center justify-between">
138
- <div>
139
- <h1 className="text-2xl font-semibold text-neutral-900">Database</h1>
140
- <p className="text-sm text-neutral-400 mt-1">Sanity CMS connection and data overview</p>
141
- </div>
142
- <button
143
- onClick={handleTestConnection}
144
- disabled={testing}
145
- className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border border-neutral-200 bg-white text-neutral-700 hover:bg-neutral-50 transition-colors disabled:opacity-50"
146
- >
147
- {testing ? (
148
- <svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
149
- <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
150
- </svg>
151
- ) : (
152
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
153
- <polyline points="23 4 23 10 17 10" />
154
- <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
155
- </svg>
156
- )}
157
- {testing ? "Testing..." : "Test Connection"}
158
- </button>
159
- </div>
160
-
161
- {/* Connection status banner */}
162
- <div className={`flex items-center gap-3 p-4 rounded-xl border ${
163
- isConnected
164
- ? "border-green-200 bg-green-50"
165
- : "border-red-200 bg-red-50"
166
- }`}>
167
- <div className={`w-2.5 h-2.5 rounded-full ${isConnected ? "bg-green-500" : "bg-red-500"}`} />
168
- <div className="flex-1">
169
- <p className={`text-sm font-medium ${isConnected ? "text-green-800" : "text-red-800"}`}>
170
- {isConnected ? "Connected to Sanity" : "Connection Failed"}
171
- </p>
172
- {status?.error && (
173
- <p className="text-xs text-red-600 mt-0.5">{status.error}</p>
174
- )}
175
- </div>
176
- {isConnected && (
177
- <span className="text-xs text-green-600 bg-green-100 px-2 py-0.5 rounded-full">
178
- API v{status?.apiVersion}
179
- </span>
180
- )}
181
- </div>
182
-
183
- {/* Stats grid */}
184
- {stats && (
185
- <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
186
- <StatCard label="Total Documents" value={stats.totalDocuments} />
187
- <StatCard label="Pages" value={stats.pages} sub={`${stats.publishedPages} published, ${stats.draftPages} draft`} />
188
- <StatCard label="Projects" value={stats.projects} />
189
- <StatCard label="Dataset" value={status?.dataset || "—"} />
190
- </div>
191
- )}
192
-
193
- {/* Two-column layout: Config + Schema */}
194
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
195
-
196
- {/* Connection Configuration */}
197
- <div className="bg-white rounded-xl border border-neutral-200 overflow-hidden">
198
- <div className="px-5 py-4 border-b border-neutral-100">
199
- <h2 className="text-sm font-semibold text-neutral-900">Connection Configuration</h2>
200
- <p className="text-xs text-neutral-400 mt-0.5">Values from environment variables — edit in .env.local or hosting dashboard</p>
201
- </div>
202
- <div className="px-5 py-2">
203
- <ConfigField label="Project ID" value={status?.projectId || ""} />
204
- <ConfigField label="Dataset" value={status?.dataset || ""} />
205
- <ConfigField label="API Version" value={status?.apiVersion || ""} />
206
- <ConfigField label="Write Token" value={status?.hasWriteToken ? "Configured" : "Not set"} />
207
- <ConfigField label="CDN" value="Disabled (real-time reads)" />
208
- </div>
209
- </div>
210
-
211
- {/* Document Types */}
212
- {stats && (
213
- <div className="bg-white rounded-xl border border-neutral-200 overflow-hidden">
214
- <div className="px-5 py-4 border-b border-neutral-100">
215
- <h2 className="text-sm font-semibold text-neutral-900">Schema Overview</h2>
216
- <p className="text-xs text-neutral-400 mt-0.5">Document types and counts in this dataset</p>
217
- </div>
218
- <div className="px-5 py-2">
219
- <DocTypeRow
220
- label="Pages"
221
- count={stats.pages}
222
- icon={
223
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><polyline points="14 2 14 8 20 8" /></svg>
224
- }
225
- />
226
- <DocTypeRow
227
- label="Projects"
228
- count={stats.projects}
229
- icon={
230
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="2" y="2" width="20" height="20" rx="2.18" /><line x1="7" y1="2" x2="7" y2="22" /><line x1="17" y1="2" x2="17" y2="22" /><line x1="2" y1="12" x2="22" y2="12" /></svg>
231
- }
232
- />
233
- <DocTypeRow
234
- label="Site Settings"
235
- count={stats.siteSettings}
236
- icon={
237
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.38.17.62.55.68.95" /></svg>
238
- }
239
- />
240
- <DocTypeRow
241
- label="Site Styles"
242
- count={stats.siteStyles}
243
- icon={
244
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" /><circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" /><circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" /><circle cx="6.5" cy="12" r="0.5" fill="currentColor" /><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2Z" /></svg>
245
- }
246
- />
247
- <DocTypeRow
248
- label="Asset Registry"
249
- count={stats.assetRegistry}
250
- icon={
251
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3" /><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5" /></svg>
252
- }
253
- />
254
- </div>
255
- </div>
256
- )}
257
- </div>
258
-
259
- {/* Sanity Studio link */}
260
- <div className="bg-white rounded-xl border border-neutral-200 p-5">
261
- <div className="flex items-center justify-between">
262
- <div>
263
- <h2 className="text-sm font-semibold text-neutral-900">Sanity Studio</h2>
264
- <p className="text-xs text-neutral-400 mt-0.5">Access the raw Sanity Studio for advanced data editing</p>
265
- </div>
266
- <a
267
- href="/studio"
268
- target="_blank"
269
- rel="noopener noreferrer"
270
- className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-neutral-900 text-white hover:bg-neutral-800 transition-colors"
271
- >
272
- Open Studio
273
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
274
- <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
275
- <polyline points="15 3 21 3 21 9" />
276
- <line x1="10" y1="14" x2="21" y2="3" />
277
- </svg>
278
- </a>
279
- </div>
280
- </div>
281
-
282
- {/* Env guide */}
283
- <div className="bg-neutral-50 rounded-xl border border-neutral-200 p-5">
284
- <h2 className="text-sm font-semibold text-neutral-700 mb-3">Environment Variables Reference</h2>
285
- <div className="space-y-2 font-mono text-xs text-neutral-500">
286
- <div className="flex gap-3">
287
- <span className="text-neutral-400 w-64 shrink-0">NEXT_PUBLIC_SANITY_PROJECT_ID</span>
288
- <span className="text-neutral-600">Sanity project ID (required)</span>
289
- </div>
290
- <div className="flex gap-3">
291
- <span className="text-neutral-400 w-64 shrink-0">NEXT_PUBLIC_SANITY_DATASET</span>
292
- <span className="text-neutral-600">Dataset name (default: &quot;production&quot;)</span>
293
- </div>
294
- <div className="flex gap-3">
295
- <span className="text-neutral-400 w-64 shrink-0">SANITY_API_TOKEN</span>
296
- <span className="text-neutral-600">Write token for mutations (server-only)</span>
297
- </div>
298
- </div>
299
- </div>
300
- </div>
301
- );
302
- }
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+
5
+ // ── Types ──
6
+
7
+ interface SanityStats {
8
+ totalDocuments: number;
9
+ pages: number;
10
+ publishedPages: number;
11
+ draftPages: number;
12
+ projects: number;
13
+ siteSettings: number;
14
+ siteStyles: number;
15
+ assetRegistry: number;
16
+ datasets: string[];
17
+ }
18
+
19
+ interface SanityStatus {
20
+ connected: boolean;
21
+ projectId: string;
22
+ dataset: string;
23
+ hasWriteToken: boolean;
24
+ stats: SanityStats | null;
25
+ error?: string;
26
+ apiVersion: string;
27
+ }
28
+
29
+ // ── Stat Card ──
30
+
31
+ function StatCard({ label, value, sub }: { label: string; value: number | string; sub?: string }) {
32
+ return (
33
+ <div className="bg-white rounded-xl border border-neutral-200 p-4">
34
+ <p className="text-[11px] uppercase tracking-wider text-neutral-400 mb-1">{label}</p>
35
+ <p className="text-2xl font-semibold text-neutral-900">{value}</p>
36
+ {sub && <p className="text-xs text-neutral-400 mt-0.5">{sub}</p>}
37
+ </div>
38
+ );
39
+ }
40
+
41
+ // ── Config Field (read-only display) ──
42
+
43
+ function ConfigField({ label, value, masked }: { label: string; value: string; masked?: boolean }) {
44
+ const [revealed, setRevealed] = useState(false);
45
+ const display = masked && !revealed ? "••••••••••••" : value || "—";
46
+
47
+ return (
48
+ <div className="flex items-center justify-between py-3 border-b border-neutral-100 last:border-0">
49
+ <span className="text-sm text-neutral-500">{label}</span>
50
+ <div className="flex items-center gap-2">
51
+ <code className="text-sm text-neutral-800 bg-neutral-50 px-2 py-0.5 rounded font-mono">{display}</code>
52
+ {masked && value && (
53
+ <button
54
+ onClick={() => setRevealed(!revealed)}
55
+ className="text-xs text-[#3580f9] hover:underline"
56
+ >
57
+ {revealed ? "Hide" : "Show"}
58
+ </button>
59
+ )}
60
+ </div>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ // ── Document Type Row ──
66
+
67
+ function DocTypeRow({ label, count, icon }: { label: string; count: number; icon: React.ReactNode }) {
68
+ return (
69
+ <div className="flex items-center justify-between py-2.5 border-b border-neutral-100 last:border-0">
70
+ <div className="flex items-center gap-2.5">
71
+ <span className="text-neutral-400">{icon}</span>
72
+ <span className="text-sm text-neutral-700">{label}</span>
73
+ </div>
74
+ <span className="text-sm font-medium text-neutral-900 bg-neutral-100 rounded-full px-2.5 py-0.5 min-w-[32px] text-center">
75
+ {count}
76
+ </span>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ // ── Main Page ──
82
+
83
+ export default function AdminDatabasePage() {
84
+ const [status, setStatus] = useState<SanityStatus | null>(null);
85
+ const [loading, setLoading] = useState(true);
86
+ const [testing, setTesting] = useState(false);
87
+
88
+ const fetchStatus = useCallback(async () => {
89
+ try {
90
+ const res = await fetch("/api/admin/database");
91
+ if (res.ok) {
92
+ const data = await res.json();
93
+ setStatus(data);
94
+ }
95
+ } catch {
96
+ setStatus(null);
97
+ } finally {
98
+ setLoading(false);
99
+ }
100
+ }, []);
101
+
102
+ useEffect(() => {
103
+ fetchStatus();
104
+ }, [fetchStatus]);
105
+
106
+ const handleTestConnection = async () => {
107
+ setTesting(true);
108
+ setLoading(false);
109
+ try {
110
+ const res = await fetch("/api/admin/database");
111
+ if (res.ok) {
112
+ const data = await res.json();
113
+ setStatus(data);
114
+ }
115
+ } catch {
116
+ // handled by status state
117
+ } finally {
118
+ setTesting(false);
119
+ }
120
+ };
121
+
122
+ // ── Loading ──
123
+ if (loading) {
124
+ return (
125
+ <div className="flex items-center justify-center py-20">
126
+ <span className="text-sm text-neutral-400 animate-pulse">Connecting to database...</span>
127
+ </div>
128
+ );
129
+ }
130
+
131
+ const stats = status?.stats;
132
+ const isConnected = status?.connected === true;
133
+
134
+ return (
135
+ <div className="space-y-8 max-w-4xl">
136
+ {/* Header */}
137
+ <div className="flex items-center justify-between">
138
+ <div>
139
+ <h1 className="text-2xl font-semibold text-neutral-900">Database</h1>
140
+ <p className="text-sm text-neutral-400 mt-1">Sanity CMS connection and data overview</p>
141
+ </div>
142
+ <button
143
+ onClick={handleTestConnection}
144
+ disabled={testing}
145
+ className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg border border-neutral-200 bg-white text-neutral-700 hover:bg-neutral-50 transition-colors disabled:opacity-50"
146
+ >
147
+ {testing ? (
148
+ <svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
149
+ <path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
150
+ </svg>
151
+ ) : (
152
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
153
+ <polyline points="23 4 23 10 17 10" />
154
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
155
+ </svg>
156
+ )}
157
+ {testing ? "Testing..." : "Test Connection"}
158
+ </button>
159
+ </div>
160
+
161
+ {/* Connection status banner */}
162
+ <div className={`flex items-center gap-3 p-4 rounded-xl border ${
163
+ isConnected
164
+ ? "border-green-200 bg-green-50"
165
+ : "border-red-200 bg-red-50"
166
+ }`}>
167
+ <div className={`w-2.5 h-2.5 rounded-full ${isConnected ? "bg-green-500" : "bg-red-500"}`} />
168
+ <div className="flex-1">
169
+ <p className={`text-sm font-medium ${isConnected ? "text-green-800" : "text-red-800"}`}>
170
+ {isConnected ? "Connected to Sanity" : "Connection Failed"}
171
+ </p>
172
+ {status?.error && (
173
+ <p className="text-xs text-red-600 mt-0.5">{status.error}</p>
174
+ )}
175
+ </div>
176
+ {isConnected && (
177
+ <span className="text-xs text-green-600 bg-green-100 px-2 py-0.5 rounded-full">
178
+ API v{status?.apiVersion}
179
+ </span>
180
+ )}
181
+ </div>
182
+
183
+ {/* Stats grid */}
184
+ {stats && (
185
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
186
+ <StatCard label="Total Documents" value={stats.totalDocuments} />
187
+ <StatCard label="Pages" value={stats.pages} sub={`${stats.publishedPages} published, ${stats.draftPages} draft`} />
188
+ <StatCard label="Projects" value={stats.projects} />
189
+ <StatCard label="Dataset" value={status?.dataset || "—"} />
190
+ </div>
191
+ )}
192
+
193
+ {/* Two-column layout: Config + Schema */}
194
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
195
+
196
+ {/* Connection Configuration */}
197
+ <div className="bg-white rounded-xl border border-neutral-200 overflow-hidden">
198
+ <div className="px-5 py-4 border-b border-neutral-100">
199
+ <h2 className="text-sm font-semibold text-neutral-900">Connection Configuration</h2>
200
+ <p className="text-xs text-neutral-400 mt-0.5">Values from environment variables — edit in .env.local or hosting dashboard</p>
201
+ </div>
202
+ <div className="px-5 py-2">
203
+ <ConfigField label="Project ID" value={status?.projectId || ""} />
204
+ <ConfigField label="Dataset" value={status?.dataset || ""} />
205
+ <ConfigField label="API Version" value={status?.apiVersion || ""} />
206
+ <ConfigField label="Write Token" value={status?.hasWriteToken ? "Configured" : "Not set"} />
207
+ <ConfigField label="CDN" value="Disabled (real-time reads)" />
208
+ </div>
209
+ </div>
210
+
211
+ {/* Document Types */}
212
+ {stats && (
213
+ <div className="bg-white rounded-xl border border-neutral-200 overflow-hidden">
214
+ <div className="px-5 py-4 border-b border-neutral-100">
215
+ <h2 className="text-sm font-semibold text-neutral-900">Schema Overview</h2>
216
+ <p className="text-xs text-neutral-400 mt-0.5">Document types and counts in this dataset</p>
217
+ </div>
218
+ <div className="px-5 py-2">
219
+ <DocTypeRow
220
+ label="Pages"
221
+ count={stats.pages}
222
+ icon={
223
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><polyline points="14 2 14 8 20 8" /></svg>
224
+ }
225
+ />
226
+ <DocTypeRow
227
+ label="Projects"
228
+ count={stats.projects}
229
+ icon={
230
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="2" y="2" width="20" height="20" rx="2.18" /><line x1="7" y1="2" x2="7" y2="22" /><line x1="17" y1="2" x2="17" y2="22" /><line x1="2" y1="12" x2="22" y2="12" /></svg>
231
+ }
232
+ />
233
+ <DocTypeRow
234
+ label="Site Settings"
235
+ count={stats.siteSettings}
236
+ icon={
237
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.38.17.62.55.68.95" /></svg>
238
+ }
239
+ />
240
+ <DocTypeRow
241
+ label="Site Styles"
242
+ count={stats.siteStyles}
243
+ icon={
244
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" /><circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" /><circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" /><circle cx="6.5" cy="12" r="0.5" fill="currentColor" /><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2Z" /></svg>
245
+ }
246
+ />
247
+ <DocTypeRow
248
+ label="Asset Registry"
249
+ count={stats.assetRegistry}
250
+ icon={
251
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3" /><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5" /></svg>
252
+ }
253
+ />
254
+ </div>
255
+ </div>
256
+ )}
257
+ </div>
258
+
259
+ {/* Sanity Studio link */}
260
+ <div className="bg-white rounded-xl border border-neutral-200 p-5">
261
+ <div className="flex items-center justify-between">
262
+ <div>
263
+ <h2 className="text-sm font-semibold text-neutral-900">Sanity Studio</h2>
264
+ <p className="text-xs text-neutral-400 mt-0.5">Access the raw Sanity Studio for advanced data editing</p>
265
+ </div>
266
+ <a
267
+ href="/studio"
268
+ target="_blank"
269
+ rel="noopener noreferrer"
270
+ className="flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-neutral-900 text-white hover:bg-neutral-800 transition-colors"
271
+ >
272
+ Open Studio
273
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
274
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
275
+ <polyline points="15 3 21 3 21 9" />
276
+ <line x1="10" y1="14" x2="21" y2="3" />
277
+ </svg>
278
+ </a>
279
+ </div>
280
+ </div>
281
+
282
+ {/* Env guide */}
283
+ <div className="bg-neutral-50 rounded-xl border border-neutral-200 p-5">
284
+ <h2 className="text-sm font-semibold text-neutral-700 mb-3">Environment Variables Reference</h2>
285
+ <div className="space-y-2 font-mono text-xs text-neutral-500">
286
+ <div className="flex gap-3">
287
+ <span className="text-neutral-400 w-64 shrink-0">NEXT_PUBLIC_SANITY_PROJECT_ID</span>
288
+ <span className="text-neutral-600">Sanity project ID (required)</span>
289
+ </div>
290
+ <div className="flex gap-3">
291
+ <span className="text-neutral-400 w-64 shrink-0">NEXT_PUBLIC_SANITY_DATASET</span>
292
+ <span className="text-neutral-600">Dataset name (default: &quot;production&quot;)</span>
293
+ </div>
294
+ <div className="flex gap-3">
295
+ <span className="text-neutral-400 w-64 shrink-0">SANITY_API_TOKEN</span>
296
+ <span className="text-neutral-600">Write token for mutations (server-only)</span>
297
+ </div>
298
+ </div>
299
+ </div>
300
+ </div>
301
+ );
302
+ }