@actuate-media/cms-admin 0.4.0 → 0.7.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/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +35 -0
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.js +1 -0
- package/dist/components/Breadcrumbs.js.map +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/ErrorBoundary.js.map +1 -1
- package/dist/hooks/useBuilderState.d.ts +49 -0
- package/dist/hooks/useBuilderState.d.ts.map +1 -0
- package/dist/hooks/useBuilderState.js +238 -0
- package/dist/hooks/useBuilderState.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +2 -2
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/views/ForgotPassword.d.ts +5 -0
- package/dist/views/ForgotPassword.d.ts.map +1 -0
- package/dist/views/ForgotPassword.js +41 -0
- package/dist/views/ForgotPassword.js.map +1 -0
- package/dist/views/ResetPassword.d.ts +6 -0
- package/dist/views/ResetPassword.d.ts.map +1 -0
- package/dist/views/ResetPassword.js +46 -0
- package/dist/views/ResetPassword.js.map +1 -0
- package/dist/views/ScriptTagEditor.d.ts +6 -0
- package/dist/views/ScriptTagEditor.d.ts.map +1 -0
- package/dist/views/ScriptTagEditor.js +109 -0
- package/dist/views/ScriptTagEditor.js.map +1 -0
- package/dist/views/ScriptTags.d.ts +5 -0
- package/dist/views/ScriptTags.d.ts.map +1 -0
- package/dist/views/ScriptTags.js +54 -0
- package/dist/views/ScriptTags.js.map +1 -0
- package/dist/views/page-builder/AIBlockAssist.d.ts +9 -0
- package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
- package/dist/views/page-builder/AIBlockAssist.js +40 -0
- package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
- package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
- package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
- package/dist/views/page-builder/AIGenerateDialog.js +170 -0
- package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
- package/dist/views/page-builder/BlockEditor.d.ts +11 -0
- package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
- package/dist/views/page-builder/BlockEditor.js +67 -0
- package/dist/views/page-builder/BlockEditor.js.map +1 -0
- package/dist/views/page-builder/BlockPicker.d.ts +7 -0
- package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
- package/dist/views/page-builder/BlockPicker.js +102 -0
- package/dist/views/page-builder/BlockPicker.js.map +1 -0
- package/dist/views/page-builder/BottomBar.d.ts +9 -0
- package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
- package/dist/views/page-builder/BottomBar.js +13 -0
- package/dist/views/page-builder/BottomBar.js.map +1 -0
- package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
- package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
- package/dist/views/page-builder/BuilderToolbar.js +18 -0
- package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
- package/dist/views/page-builder/ContextPanel.d.ts +20 -0
- package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
- package/dist/views/page-builder/ContextPanel.js +40 -0
- package/dist/views/page-builder/ContextPanel.js.map +1 -0
- package/dist/views/page-builder/DesignScore.d.ts +6 -0
- package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
- package/dist/views/page-builder/DesignScore.js +93 -0
- package/dist/views/page-builder/DesignScore.js.map +1 -0
- package/dist/views/page-builder/NodeSettings.d.ts +12 -0
- package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
- package/dist/views/page-builder/NodeSettings.js +80 -0
- package/dist/views/page-builder/NodeSettings.js.map +1 -0
- package/dist/views/page-builder/PageBuilder.d.ts +8 -0
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
- package/dist/views/page-builder/PageBuilder.js +126 -0
- package/dist/views/page-builder/PageBuilder.js.map +1 -0
- package/dist/views/page-builder/PageSettings.d.ts +7 -0
- package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
- package/dist/views/page-builder/PageSettings.js +27 -0
- package/dist/views/page-builder/PageSettings.js.map +1 -0
- package/dist/views/page-builder/SEOPanel.d.ts +10 -0
- package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
- package/dist/views/page-builder/SEOPanel.js +105 -0
- package/dist/views/page-builder/SEOPanel.js.map +1 -0
- package/dist/views/page-builder/SavedSections.d.ts +6 -0
- package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
- package/dist/views/page-builder/SavedSections.js +145 -0
- package/dist/views/page-builder/SavedSections.js.map +1 -0
- package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
- package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
- package/dist/views/page-builder/TemplatePicker.js +68 -0
- package/dist/views/page-builder/TemplatePicker.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
- package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
- package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
- package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
- package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
- package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
- package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
- package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
- package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
- package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
- package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
- package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
- package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
- package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
- package/dist/views/page-builder/block-renderers/index.js +25 -0
- package/dist/views/page-builder/block-renderers/index.js.map +1 -0
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
- package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
- package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
- package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
- package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
- package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
- package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
- package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
- package/dist/views/page-builder/canvas/index.d.ts +3 -0
- package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
- package/dist/views/page-builder/canvas/index.js +2 -0
- package/dist/views/page-builder/canvas/index.js.map +1 -0
- package/package.json +7 -4
- package/src/AdminRoot.tsx +41 -0
- package/src/components/Breadcrumbs.tsx +1 -0
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/hooks/useBuilderState.ts +328 -0
- package/src/index.ts +8 -0
- package/src/layout/Sidebar.tsx +7 -0
- package/src/views/ForgotPassword.tsx +136 -0
- package/src/views/ResetPassword.tsx +192 -0
- package/src/views/ScriptTagEditor.tsx +361 -0
- package/src/views/ScriptTags.tsx +174 -0
- package/src/views/page-builder/AIBlockAssist.tsx +68 -0
- package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
- package/src/views/page-builder/BlockEditor.tsx +352 -0
- package/src/views/page-builder/BlockPicker.tsx +338 -0
- package/src/views/page-builder/BottomBar.tsx +64 -0
- package/src/views/page-builder/BuilderToolbar.tsx +218 -0
- package/src/views/page-builder/ContextPanel.tsx +145 -0
- package/src/views/page-builder/DesignScore.tsx +258 -0
- package/src/views/page-builder/NodeSettings.tsx +515 -0
- package/src/views/page-builder/PageBuilder.tsx +288 -0
- package/src/views/page-builder/PageSettings.tsx +161 -0
- package/src/views/page-builder/SEOPanel.tsx +485 -0
- package/src/views/page-builder/SavedSections.tsx +486 -0
- package/src/views/page-builder/TemplatePicker.tsx +201 -0
- package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
- package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
- package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
- package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
- package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
- package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
- package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
- package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
- package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
- package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
- package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
- package/src/views/page-builder/block-renderers/index.ts +34 -0
- package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
- package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
- package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
- package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
- package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
- package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
- package/src/views/page-builder/canvas/index.ts +2 -0
package/src/AdminRoot.tsx
CHANGED
|
@@ -13,12 +13,18 @@ import { FormEditor } from './views/FormEditor.js';
|
|
|
13
13
|
import { FormSubmissions } from './views/FormSubmissions.js';
|
|
14
14
|
import { Users } from './views/Users.js';
|
|
15
15
|
import { SEO } from './views/SEO.js';
|
|
16
|
+
import { ScriptTags } from './views/ScriptTags.js';
|
|
17
|
+
import { ScriptTagEditor } from './views/ScriptTagEditor.js';
|
|
16
18
|
import { SetupWizard } from './views/SetupWizard.js';
|
|
17
19
|
import { Login } from './views/Login.js';
|
|
20
|
+
import { ForgotPassword } from './views/ForgotPassword.js';
|
|
21
|
+
import { ResetPassword } from './views/ResetPassword.js';
|
|
18
22
|
import { ErrorBoundary } from './components/ErrorBoundary.js';
|
|
19
23
|
import { ThemeProvider } from './components/ThemeProvider.js';
|
|
20
24
|
import { LocaleProvider } from './components/LocaleProvider.js';
|
|
21
25
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js';
|
|
26
|
+
import { PageBuilder } from './views/page-builder/PageBuilder.js';
|
|
27
|
+
import { SavedSections } from './views/page-builder/SavedSections.js';
|
|
22
28
|
|
|
23
29
|
export interface AdminRootProps {
|
|
24
30
|
config: any;
|
|
@@ -63,6 +69,16 @@ function AdminShell({ config, session, basePath = '/admin', initialPath = '/', s
|
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
if (!session && !setupRequired) {
|
|
72
|
+
if (matchRoute('/forgot-password')) {
|
|
73
|
+
return <ForgotPassword onNavigate={navigate} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const resetMatch = matchRoute('/reset-password');
|
|
77
|
+
if (resetMatch) {
|
|
78
|
+
const params = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
|
|
79
|
+
return <ResetPassword onNavigate={navigate} token={params.get('token')} />;
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
if (onLogin) {
|
|
67
83
|
return <Login onLogin={onLogin} onNavigate={navigate} captchaConfig={captchaConfig} />;
|
|
68
84
|
}
|
|
@@ -85,6 +101,16 @@ function AdminShell({ config, session, basePath = '/admin', initialPath = '/', s
|
|
|
85
101
|
return <Dashboard config={config} session={session} onNavigate={navigate} />;
|
|
86
102
|
}
|
|
87
103
|
|
|
104
|
+
const pageBuilderEdit = matchRoute('/page-builder/:id');
|
|
105
|
+
if (pageBuilderEdit?.id) {
|
|
106
|
+
return <PageBuilder documentId={pageBuilderEdit.id} collectionSlug="pages" config={config} onNavigate={navigate} />;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const pageBuilderNew = matchRoute('/page-builder/new');
|
|
110
|
+
if (pageBuilderNew) {
|
|
111
|
+
return <PageBuilder collectionSlug="pages" config={config} onNavigate={navigate} />;
|
|
112
|
+
}
|
|
113
|
+
|
|
88
114
|
for (const slug of collectionSlugs) {
|
|
89
115
|
const newMatch = matchRoute(`/${slug}/new`);
|
|
90
116
|
if (newMatch) {
|
|
@@ -146,6 +172,21 @@ function AdminShell({ config, session, basePath = '/admin', initialPath = '/', s
|
|
|
146
172
|
return <SEO onNavigate={navigate} initialTab="pages" />;
|
|
147
173
|
}
|
|
148
174
|
|
|
175
|
+
if (matchRoute('/script-tags/new')) {
|
|
176
|
+
return <ScriptTagEditor onNavigate={navigate} />;
|
|
177
|
+
}
|
|
178
|
+
const scriptTagEdit = matchRoute('/script-tags/:id');
|
|
179
|
+
if (scriptTagEdit?.id) {
|
|
180
|
+
return <ScriptTagEditor tagId={scriptTagEdit.id} onNavigate={navigate} />;
|
|
181
|
+
}
|
|
182
|
+
if (matchRoute('/script-tags')) {
|
|
183
|
+
return <ScriptTags onNavigate={navigate} />;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (matchRoute('/saved-sections')) {
|
|
187
|
+
return <SavedSections onNavigate={navigate} config={config} />;
|
|
188
|
+
}
|
|
189
|
+
|
|
149
190
|
if (matchRoute('/users')) {
|
|
150
191
|
return <Users onNavigate={navigate} />;
|
|
151
192
|
}
|
|
@@ -32,13 +32,13 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
32
32
|
return (
|
|
33
33
|
<div className="flex min-h-[200px] items-center justify-center p-6">
|
|
34
34
|
<div className="text-center">
|
|
35
|
-
<h2 className="text-lg font-
|
|
36
|
-
<p className="text-sm text-
|
|
35
|
+
<h2 className="text-lg font-medium text-foreground mb-2">Something went wrong</h2>
|
|
36
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
37
37
|
{this.state.error?.message ?? 'An unexpected error occurred'}
|
|
38
38
|
</p>
|
|
39
39
|
<button
|
|
40
40
|
onClick={() => this.setState({ hasError: false, error: null })}
|
|
41
|
-
className="px-4 py-2 text-sm font-medium text-
|
|
41
|
+
className="px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-md hover:bg-primary/90 transition-colors"
|
|
42
42
|
>
|
|
43
43
|
Try Again
|
|
44
44
|
</button>
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef, useMemo } from 'react';
|
|
4
|
+
import type {
|
|
5
|
+
PageNode,
|
|
6
|
+
BuilderNode,
|
|
7
|
+
BlockNode,
|
|
8
|
+
SectionNode,
|
|
9
|
+
ColumnNode,
|
|
10
|
+
} from '@actuate-media/cms-core';
|
|
11
|
+
import {
|
|
12
|
+
addNode,
|
|
13
|
+
removeNode,
|
|
14
|
+
moveNode,
|
|
15
|
+
updateNodeSettings,
|
|
16
|
+
updateBlockData,
|
|
17
|
+
findNode,
|
|
18
|
+
findParent,
|
|
19
|
+
createEmptyPage,
|
|
20
|
+
createSection,
|
|
21
|
+
createContainer,
|
|
22
|
+
createRow,
|
|
23
|
+
createColumn,
|
|
24
|
+
createBlock,
|
|
25
|
+
hasChildren,
|
|
26
|
+
} from '@actuate-media/cms-core';
|
|
27
|
+
|
|
28
|
+
export type DeviceMode = 'desktop' | 'tablet' | 'mobile';
|
|
29
|
+
export type PanelTab = 'block' | 'node' | 'page' | 'seo' | 'design';
|
|
30
|
+
|
|
31
|
+
export interface PageSettings {
|
|
32
|
+
title: string;
|
|
33
|
+
slug: string;
|
|
34
|
+
template?: string;
|
|
35
|
+
metaTitle?: string;
|
|
36
|
+
metaDescription?: string;
|
|
37
|
+
ogImage?: string;
|
|
38
|
+
focusKeyphrase?: string;
|
|
39
|
+
schemaType?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BuilderState {
|
|
43
|
+
tree: PageNode;
|
|
44
|
+
selectedNodeId: string | null;
|
|
45
|
+
selectedNode: BuilderNode | null;
|
|
46
|
+
deviceMode: DeviceMode;
|
|
47
|
+
activeTab: PanelTab;
|
|
48
|
+
pageSettings: PageSettings;
|
|
49
|
+
dirty: boolean;
|
|
50
|
+
canUndo: boolean;
|
|
51
|
+
canRedo: boolean;
|
|
52
|
+
showGridOverlay: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BuilderActions {
|
|
56
|
+
selectNode: (id: string | null) => void;
|
|
57
|
+
setDeviceMode: (mode: DeviceMode) => void;
|
|
58
|
+
setActiveTab: (tab: PanelTab) => void;
|
|
59
|
+
setPageSettings: (settings: Partial<PageSettings>) => void;
|
|
60
|
+
setShowGridOverlay: (show: boolean) => void;
|
|
61
|
+
|
|
62
|
+
addSection: () => void;
|
|
63
|
+
addRowToSection: (sectionId: string) => void;
|
|
64
|
+
addBlockToColumn: (columnId: string, blockType: string, variant?: string) => void;
|
|
65
|
+
addNodeAtId: (parentId: string, node: BuilderNode, index?: number) => void;
|
|
66
|
+
removeNodeById: (id: string) => void;
|
|
67
|
+
moveNodeById: (id: string, newParentId: string, index?: number) => void;
|
|
68
|
+
updateSettings: (id: string, settings: Record<string, unknown>) => void;
|
|
69
|
+
updateBlock: (id: string, data: Record<string, unknown>) => void;
|
|
70
|
+
duplicateNode: (id: string) => void;
|
|
71
|
+
moveNodeUp: (id: string) => void;
|
|
72
|
+
moveNodeDown: (id: string) => void;
|
|
73
|
+
|
|
74
|
+
undo: () => void;
|
|
75
|
+
redo: () => void;
|
|
76
|
+
markClean: () => void;
|
|
77
|
+
replaceTree: (tree: PageNode) => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const MAX_HISTORY = 50;
|
|
81
|
+
|
|
82
|
+
function deepClone<T>(obj: T): T {
|
|
83
|
+
return JSON.parse(JSON.stringify(obj));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function regenerateIds(node: BuilderNode): BuilderNode {
|
|
87
|
+
const cloned = deepClone(node);
|
|
88
|
+
let counter = 0;
|
|
89
|
+
function walk(n: any) {
|
|
90
|
+
n.id = `node_${Date.now().toString(36)}_${(counter++).toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
91
|
+
if (Array.isArray(n.children)) {
|
|
92
|
+
for (const child of n.children) walk(child);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
walk(cloned);
|
|
96
|
+
return cloned;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function useBuilderState(
|
|
100
|
+
initialTree?: PageNode,
|
|
101
|
+
initialPageSettings?: Partial<PageSettings>,
|
|
102
|
+
): BuilderState & BuilderActions {
|
|
103
|
+
const [tree, setTree] = useState<PageNode>(() => initialTree ?? createEmptyPage());
|
|
104
|
+
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
|
105
|
+
const [deviceMode, setDeviceMode] = useState<DeviceMode>('desktop');
|
|
106
|
+
const [activeTab, setActiveTab] = useState<PanelTab>('page');
|
|
107
|
+
const [showGridOverlay, setShowGridOverlay] = useState(false);
|
|
108
|
+
const [pageSettings, setPageSettingsState] = useState<PageSettings>({
|
|
109
|
+
title: '',
|
|
110
|
+
slug: '',
|
|
111
|
+
...initialPageSettings,
|
|
112
|
+
});
|
|
113
|
+
const [dirty, setDirty] = useState(false);
|
|
114
|
+
|
|
115
|
+
const undoStack = useRef<PageNode[]>([]);
|
|
116
|
+
const redoStack = useRef<PageNode[]>([]);
|
|
117
|
+
|
|
118
|
+
const pushHistory = useCallback((currentTree: PageNode) => {
|
|
119
|
+
undoStack.current = [...undoStack.current.slice(-MAX_HISTORY + 1), currentTree];
|
|
120
|
+
redoStack.current = [];
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
124
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
125
|
+
|
|
126
|
+
const applyTreeChange = useCallback((fn: (prev: PageNode) => PageNode) => {
|
|
127
|
+
setTree((prev) => {
|
|
128
|
+
pushHistory(prev);
|
|
129
|
+
const next = fn(prev);
|
|
130
|
+
setDirty(true);
|
|
131
|
+
setCanUndo(true);
|
|
132
|
+
setCanRedo(false);
|
|
133
|
+
return next;
|
|
134
|
+
});
|
|
135
|
+
}, [pushHistory]);
|
|
136
|
+
|
|
137
|
+
const selectedNode = useMemo(() => {
|
|
138
|
+
if (!selectedNodeId) return null;
|
|
139
|
+
return findNode(tree, selectedNodeId) ?? null;
|
|
140
|
+
}, [tree, selectedNodeId]);
|
|
141
|
+
|
|
142
|
+
const selectNode = useCallback((id: string | null) => {
|
|
143
|
+
setSelectedNodeId(id);
|
|
144
|
+
if (id) {
|
|
145
|
+
const node = findNode(tree, id);
|
|
146
|
+
if (node) {
|
|
147
|
+
if (node.type === 'block') setActiveTab('block');
|
|
148
|
+
else if (node.type === 'page') setActiveTab('page');
|
|
149
|
+
else setActiveTab('node');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}, [tree]);
|
|
153
|
+
|
|
154
|
+
const setPageSettings = useCallback((settings: Partial<PageSettings>) => {
|
|
155
|
+
setPageSettingsState((prev) => ({ ...prev, ...settings }));
|
|
156
|
+
setDirty(true);
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
const addSection = useCallback(() => {
|
|
160
|
+
applyTreeChange((prev) => {
|
|
161
|
+
const section = createSection({ paddingTop: '64px', paddingBottom: '64px' });
|
|
162
|
+
const container = createContainer();
|
|
163
|
+
const col = createColumn(12);
|
|
164
|
+
const row = createRow([col]);
|
|
165
|
+
(container as any).children = [row];
|
|
166
|
+
(section as any).children = [container];
|
|
167
|
+
return addNode(prev, prev.id, section as BuilderNode);
|
|
168
|
+
});
|
|
169
|
+
}, [applyTreeChange]);
|
|
170
|
+
|
|
171
|
+
const addRowToSection = useCallback((sectionId: string) => {
|
|
172
|
+
applyTreeChange((prev) => {
|
|
173
|
+
const section = findNode(prev, sectionId);
|
|
174
|
+
if (!section || !hasChildren(section)) return prev;
|
|
175
|
+
const containers = (section as SectionNode).children.filter((c) => c.type === 'container');
|
|
176
|
+
const targetId = containers.length > 0 ? containers[containers.length - 1]!.id : sectionId;
|
|
177
|
+
const col = createColumn(12);
|
|
178
|
+
const row = createRow([col]);
|
|
179
|
+
return addNode(prev, targetId, row as BuilderNode);
|
|
180
|
+
});
|
|
181
|
+
}, [applyTreeChange]);
|
|
182
|
+
|
|
183
|
+
const addBlockToColumn = useCallback((columnId: string, blockType: string, variant?: string) => {
|
|
184
|
+
applyTreeChange((prev) => {
|
|
185
|
+
const block = createBlock(blockType, variant);
|
|
186
|
+
return addNode(prev, columnId, block as BuilderNode);
|
|
187
|
+
});
|
|
188
|
+
}, [applyTreeChange]);
|
|
189
|
+
|
|
190
|
+
const addNodeAtId = useCallback((parentId: string, node: BuilderNode, index?: number) => {
|
|
191
|
+
applyTreeChange((prev) => addNode(prev, parentId, node, index));
|
|
192
|
+
}, [applyTreeChange]);
|
|
193
|
+
|
|
194
|
+
const removeNodeById = useCallback((id: string) => {
|
|
195
|
+
applyTreeChange((prev) => {
|
|
196
|
+
const result = removeNode(prev, id);
|
|
197
|
+
if (selectedNodeId === id) setSelectedNodeId(null);
|
|
198
|
+
return result;
|
|
199
|
+
});
|
|
200
|
+
}, [applyTreeChange, selectedNodeId]);
|
|
201
|
+
|
|
202
|
+
const moveNodeById = useCallback((id: string, newParentId: string, index?: number) => {
|
|
203
|
+
applyTreeChange((prev) => moveNode(prev, id, newParentId, index));
|
|
204
|
+
}, [applyTreeChange]);
|
|
205
|
+
|
|
206
|
+
const updateSettings = useCallback((id: string, settings: Record<string, unknown>) => {
|
|
207
|
+
applyTreeChange((prev) => updateNodeSettings(prev, id, settings));
|
|
208
|
+
}, [applyTreeChange]);
|
|
209
|
+
|
|
210
|
+
const updateBlock = useCallback((id: string, data: Record<string, unknown>) => {
|
|
211
|
+
applyTreeChange((prev) => updateBlockData(prev, id, data));
|
|
212
|
+
}, [applyTreeChange]);
|
|
213
|
+
|
|
214
|
+
const duplicateNode = useCallback((id: string) => {
|
|
215
|
+
applyTreeChange((prev) => {
|
|
216
|
+
const node = findNode(prev, id);
|
|
217
|
+
if (!node) return prev;
|
|
218
|
+
const parent = findParent(prev, id);
|
|
219
|
+
if (!parent) return prev;
|
|
220
|
+
const siblings = (parent as any).children as BuilderNode[];
|
|
221
|
+
const idx = siblings.findIndex((c) => c.id === id);
|
|
222
|
+
const cloned = regenerateIds(node);
|
|
223
|
+
return addNode(prev, parent.id, cloned, idx + 1);
|
|
224
|
+
});
|
|
225
|
+
}, [applyTreeChange]);
|
|
226
|
+
|
|
227
|
+
const moveNodeUp = useCallback((id: string) => {
|
|
228
|
+
applyTreeChange((prev) => {
|
|
229
|
+
const parent = findParent(prev, id);
|
|
230
|
+
if (!parent) return prev;
|
|
231
|
+
const siblings = (parent as any).children as BuilderNode[];
|
|
232
|
+
const idx = siblings.findIndex((c) => c.id === id);
|
|
233
|
+
if (idx <= 0) return prev;
|
|
234
|
+
const withoutNode = removeNode(prev, id);
|
|
235
|
+
const node = findNode(prev, id);
|
|
236
|
+
if (!node) return prev;
|
|
237
|
+
return addNode(withoutNode, parent.id, deepClone(node), idx - 1);
|
|
238
|
+
});
|
|
239
|
+
}, [applyTreeChange]);
|
|
240
|
+
|
|
241
|
+
const moveNodeDown = useCallback((id: string) => {
|
|
242
|
+
applyTreeChange((prev) => {
|
|
243
|
+
const parent = findParent(prev, id);
|
|
244
|
+
if (!parent) return prev;
|
|
245
|
+
const siblings = (parent as any).children as BuilderNode[];
|
|
246
|
+
const idx = siblings.findIndex((c) => c.id === id);
|
|
247
|
+
if (idx >= siblings.length - 1) return prev;
|
|
248
|
+
const withoutNode = removeNode(prev, id);
|
|
249
|
+
const node = findNode(prev, id);
|
|
250
|
+
if (!node) return prev;
|
|
251
|
+
return addNode(withoutNode, parent.id, deepClone(node), idx + 1);
|
|
252
|
+
});
|
|
253
|
+
}, [applyTreeChange]);
|
|
254
|
+
|
|
255
|
+
const undo = useCallback(() => {
|
|
256
|
+
if (undoStack.current.length === 0) return;
|
|
257
|
+
const prev = undoStack.current[undoStack.current.length - 1]!;
|
|
258
|
+
undoStack.current = undoStack.current.slice(0, -1);
|
|
259
|
+
setTree((current) => {
|
|
260
|
+
redoStack.current = [...redoStack.current, current];
|
|
261
|
+
setCanUndo(undoStack.current.length > 0);
|
|
262
|
+
setCanRedo(true);
|
|
263
|
+
return prev;
|
|
264
|
+
});
|
|
265
|
+
}, []);
|
|
266
|
+
|
|
267
|
+
const redo = useCallback(() => {
|
|
268
|
+
if (redoStack.current.length === 0) return;
|
|
269
|
+
const next = redoStack.current[redoStack.current.length - 1]!;
|
|
270
|
+
redoStack.current = redoStack.current.slice(0, -1);
|
|
271
|
+
setTree((current) => {
|
|
272
|
+
undoStack.current = [...undoStack.current, current];
|
|
273
|
+
setCanRedo(redoStack.current.length > 0);
|
|
274
|
+
setCanUndo(true);
|
|
275
|
+
return next;
|
|
276
|
+
});
|
|
277
|
+
}, []);
|
|
278
|
+
|
|
279
|
+
const markClean = useCallback(() => {
|
|
280
|
+
setDirty(false);
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
const replaceTree = useCallback((newTree: PageNode) => {
|
|
284
|
+
setTree((prev) => {
|
|
285
|
+
pushHistory(prev);
|
|
286
|
+
setDirty(true);
|
|
287
|
+
setCanUndo(true);
|
|
288
|
+
setCanRedo(false);
|
|
289
|
+
return newTree;
|
|
290
|
+
});
|
|
291
|
+
}, [pushHistory]);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
tree,
|
|
295
|
+
selectedNodeId,
|
|
296
|
+
selectedNode,
|
|
297
|
+
deviceMode,
|
|
298
|
+
activeTab,
|
|
299
|
+
pageSettings,
|
|
300
|
+
dirty,
|
|
301
|
+
canUndo,
|
|
302
|
+
canRedo,
|
|
303
|
+
showGridOverlay,
|
|
304
|
+
|
|
305
|
+
selectNode,
|
|
306
|
+
setDeviceMode,
|
|
307
|
+
setActiveTab,
|
|
308
|
+
setPageSettings,
|
|
309
|
+
setShowGridOverlay,
|
|
310
|
+
|
|
311
|
+
addSection,
|
|
312
|
+
addRowToSection,
|
|
313
|
+
addBlockToColumn,
|
|
314
|
+
addNodeAtId,
|
|
315
|
+
removeNodeById,
|
|
316
|
+
moveNodeById,
|
|
317
|
+
updateSettings,
|
|
318
|
+
updateBlock,
|
|
319
|
+
duplicateNode,
|
|
320
|
+
moveNodeUp,
|
|
321
|
+
moveNodeDown,
|
|
322
|
+
|
|
323
|
+
undo,
|
|
324
|
+
redo,
|
|
325
|
+
markClean,
|
|
326
|
+
replaceTree,
|
|
327
|
+
};
|
|
328
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -26,9 +26,17 @@ export { SetupWizard } from './views/SetupWizard.js';
|
|
|
26
26
|
export type { SetupWizardProps } from './views/SetupWizard.js';
|
|
27
27
|
export { Login } from './views/Login.js';
|
|
28
28
|
export type { LoginProps, CaptchaConfig } from './views/Login.js';
|
|
29
|
+
export { ForgotPassword } from './views/ForgotPassword.js';
|
|
30
|
+
export type { ForgotPasswordProps } from './views/ForgotPassword.js';
|
|
31
|
+
export { ResetPassword } from './views/ResetPassword.js';
|
|
32
|
+
export type { ResetPasswordProps } from './views/ResetPassword.js';
|
|
29
33
|
export { CollectionList } from './views/CollectionList.js';
|
|
30
34
|
export { DocumentEdit } from './views/DocumentEdit.js';
|
|
31
35
|
|
|
36
|
+
export { PageBuilder } from './views/page-builder/PageBuilder.js';
|
|
37
|
+
export { useBuilderState } from './hooks/useBuilderState.js';
|
|
38
|
+
export type { BuilderState, BuilderActions, DeviceMode, PanelTab, PageSettings as BuilderPageSettings } from './hooks/useBuilderState.js';
|
|
39
|
+
|
|
32
40
|
export { Breadcrumbs } from './components/Breadcrumbs.js';
|
|
33
41
|
export { CommandPalette } from './components/CommandPalette.js';
|
|
34
42
|
export { ErrorBoundary } from './components/ErrorBoundary.js';
|
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
PanelTop,
|
|
20
20
|
PanelBottom,
|
|
21
21
|
Layers,
|
|
22
|
+
Code2,
|
|
23
|
+
LayoutTemplate,
|
|
24
|
+
Library,
|
|
22
25
|
} from 'lucide-react';
|
|
23
26
|
import type { LucideIcon } from 'lucide-react';
|
|
24
27
|
|
|
@@ -253,9 +256,13 @@ function buildNavItems(config: any): NavItem[] {
|
|
|
253
256
|
}
|
|
254
257
|
|
|
255
258
|
items.push(
|
|
259
|
+
{ path: '/page-builder/new', label: 'Page Builder', icon: LayoutTemplate, group: 'Pages' },
|
|
260
|
+
{ path: '/saved-sections', label: 'Saved Sections', icon: Library, group: 'Pages' },
|
|
261
|
+
{ path: '/page-templates', label: 'Templates', icon: Layers, group: 'Pages' },
|
|
256
262
|
{ path: '/media', label: 'Media', icon: Image },
|
|
257
263
|
{ path: '/forms', label: 'Forms', icon: ClipboardList },
|
|
258
264
|
{ path: '/seo', label: 'SEO', icon: SearchIcon },
|
|
265
|
+
{ path: '/script-tags', label: 'Script Tags', icon: Code2 },
|
|
259
266
|
{ path: '/users', label: 'Users', icon: Users },
|
|
260
267
|
{ path: '/settings', label: 'Settings', icon: Settings },
|
|
261
268
|
);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, type FormEvent } from 'react';
|
|
4
|
+
import { Shield, ArrowLeft, Loader2, CheckCircle2, AlertTriangle } from 'lucide-react';
|
|
5
|
+
import { cmsApi } from '../lib/api.js';
|
|
6
|
+
|
|
7
|
+
export interface ForgotPasswordProps {
|
|
8
|
+
onNavigate: (path: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ForgotPassword({ onNavigate }: ForgotPasswordProps) {
|
|
12
|
+
const [email, setEmail] = useState('');
|
|
13
|
+
const [submitting, setSubmitting] = useState(false);
|
|
14
|
+
const [sent, setSent] = useState(false);
|
|
15
|
+
const [error, setError] = useState('');
|
|
16
|
+
|
|
17
|
+
const canSubmit = email.trim() && !submitting;
|
|
18
|
+
|
|
19
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
if (!canSubmit) return;
|
|
22
|
+
|
|
23
|
+
setError('');
|
|
24
|
+
setSubmitting(true);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = await cmsApi('/auth/forgot-password', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
body: JSON.stringify({ email: email.trim() }),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (result.error && result.status === 429) {
|
|
33
|
+
setError('Too many requests. Please try again later.');
|
|
34
|
+
} else {
|
|
35
|
+
setSent(true);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
setError('An unexpected error occurred. Please try again.');
|
|
39
|
+
} finally {
|
|
40
|
+
setSubmitting(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
|
|
46
|
+
<div className="w-full max-w-md">
|
|
47
|
+
<div className="text-center mb-8">
|
|
48
|
+
<div className="mx-auto mb-4 w-14 h-14 bg-blue-600 rounded-xl flex items-center justify-center">
|
|
49
|
+
<Shield className="w-7 h-7 text-white" />
|
|
50
|
+
</div>
|
|
51
|
+
<h1 className="text-2xl font-bold text-gray-900">Reset Password</h1>
|
|
52
|
+
<p className="text-gray-600 mt-2">
|
|
53
|
+
{sent
|
|
54
|
+
? 'Check your inbox for a reset link'
|
|
55
|
+
: "Enter your email and we'll send you a reset link"}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm space-y-5">
|
|
60
|
+
{sent ? (
|
|
61
|
+
<div className="text-center space-y-4">
|
|
62
|
+
<div className="mx-auto w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
|
|
63
|
+
<CheckCircle2 className="w-6 h-6 text-green-600" />
|
|
64
|
+
</div>
|
|
65
|
+
<p className="text-sm text-gray-600">
|
|
66
|
+
If an account exists for <strong>{email}</strong>, you will receive a password reset email shortly.
|
|
67
|
+
</p>
|
|
68
|
+
<p className="text-xs text-gray-500">
|
|
69
|
+
The link expires in 1 hour. Check your spam folder if you don't see it.
|
|
70
|
+
</p>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={() => onNavigate('/login')}
|
|
74
|
+
className="w-full py-2.5 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
|
75
|
+
>
|
|
76
|
+
Back to Sign In
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
) : (
|
|
80
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
81
|
+
{error && (
|
|
82
|
+
<div className="flex items-start gap-3 p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
83
|
+
<AlertTriangle className="w-5 h-5 text-red-600 mt-0.5 shrink-0" />
|
|
84
|
+
<p className="text-sm text-red-800">{error}</p>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<div>
|
|
89
|
+
<label htmlFor="forgot-email" className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
90
|
+
Email Address
|
|
91
|
+
</label>
|
|
92
|
+
<input
|
|
93
|
+
id="forgot-email"
|
|
94
|
+
type="email"
|
|
95
|
+
value={email}
|
|
96
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
97
|
+
placeholder="admin@example.com"
|
|
98
|
+
className="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
99
|
+
required
|
|
100
|
+
autoFocus
|
|
101
|
+
autoComplete="email"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<button
|
|
106
|
+
type="submit"
|
|
107
|
+
disabled={!canSubmit}
|
|
108
|
+
className="w-full py-2.5 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
109
|
+
>
|
|
110
|
+
{submitting ? (
|
|
111
|
+
<>
|
|
112
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
113
|
+
Sending...
|
|
114
|
+
</>
|
|
115
|
+
) : (
|
|
116
|
+
'Send Reset Link'
|
|
117
|
+
)}
|
|
118
|
+
</button>
|
|
119
|
+
</form>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{!sent && (
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={() => onNavigate('/login')}
|
|
126
|
+
className="w-full flex items-center justify-center gap-2 text-sm text-gray-600 hover:text-gray-800 transition-colors"
|
|
127
|
+
>
|
|
128
|
+
<ArrowLeft className="w-4 h-4" />
|
|
129
|
+
Back to Sign In
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|