create-nextblock 0.0.1
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/bin/create-nextblock.js +997 -0
- package/package.json +25 -0
- package/scripts/sync-template.js +284 -0
- package/templates/nextblock-template/.env.example +37 -0
- package/templates/nextblock-template/.swcrc +30 -0
- package/templates/nextblock-template/README.md +194 -0
- package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
- package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
- package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
- package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
- package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
- package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
- package/templates/nextblock-template/app/actions/email.ts +31 -0
- package/templates/nextblock-template/app/actions/formActions.ts +65 -0
- package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
- package/templates/nextblock-template/app/actions/postActions.ts +80 -0
- package/templates/nextblock-template/app/actions.ts +146 -0
- package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
- package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
- package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
- package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
- package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
- package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
- package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
- package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
- package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
- package/templates/nextblock-template/app/blog/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
- package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
- package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
- package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
- package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
- package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
- package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
- package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
- package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
- package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
- package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
- package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
- package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
- package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
- package/templates/nextblock-template/app/cms/layout.tsx +10 -0
- package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
- package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
- package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
- package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
- package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
- package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
- package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
- package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
- package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
- package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
- package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
- package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
- package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
- package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
- package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
- package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
- package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
- package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
- package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
- package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
- package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
- package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
- package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
- package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
- package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
- package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
- package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
- package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
- package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
- package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
- package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
- package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
- package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
- package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
- package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
- package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
- package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
- package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
- package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
- package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
- package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
- package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
- package/templates/nextblock-template/app/favicon.ico +0 -0
- package/templates/nextblock-template/app/globals.css +401 -0
- package/templates/nextblock-template/app/layout.tsx +191 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
- package/templates/nextblock-template/app/page.tsx +109 -0
- package/templates/nextblock-template/app/providers.tsx +43 -0
- package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
- package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
- package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
- package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
- package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
- package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
- package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
- package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
- package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
- package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
- package/templates/nextblock-template/components/Header.tsx +42 -0
- package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
- package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
- package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
- package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
- package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
- package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
- package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
- package/templates/nextblock-template/components/blocks/types.ts +8 -0
- package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
- package/templates/nextblock-template/components/form-message.tsx +26 -0
- package/templates/nextblock-template/components/header-auth.tsx +71 -0
- package/templates/nextblock-template/components/submit-button.tsx +23 -0
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
- package/templates/nextblock-template/context/AuthContext.tsx +138 -0
- package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
- package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
- package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
- package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
- package/templates/nextblock-template/docs/files-structure.md +426 -0
- package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
- package/templates/nextblock-template/eslint.config.mjs +28 -0
- package/templates/nextblock-template/index.d.ts +5 -0
- package/templates/nextblock-template/lib/blocks/README.md +670 -0
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
- package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
- package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
- package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
- package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
- package/templates/nextblock-template/lib/ui/badge.ts +1 -0
- package/templates/nextblock-template/lib/ui/button.ts +1 -0
- package/templates/nextblock-template/lib/ui/card.ts +1 -0
- package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
- package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
- package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
- package/templates/nextblock-template/lib/ui/input.ts +1 -0
- package/templates/nextblock-template/lib/ui/label.ts +1 -0
- package/templates/nextblock-template/lib/ui/popover.ts +1 -0
- package/templates/nextblock-template/lib/ui/progress.ts +1 -0
- package/templates/nextblock-template/lib/ui/select.ts +1 -0
- package/templates/nextblock-template/lib/ui/separator.ts +1 -0
- package/templates/nextblock-template/lib/ui/table.ts +1 -0
- package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
- package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
- package/templates/nextblock-template/lib/ui/ui.ts +1 -0
- package/templates/nextblock-template/middleware.ts +206 -0
- package/templates/nextblock-template/next-env.d.ts +6 -0
- package/templates/nextblock-template/next.config.js +99 -0
- package/templates/nextblock-template/package.json +52 -0
- package/templates/nextblock-template/postcss.config.js +6 -0
- package/templates/nextblock-template/project.json +7 -0
- package/templates/nextblock-template/public/.gitkeep +0 -0
- package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
- package/templates/nextblock-template/scripts/backup.js +53 -0
- package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
- package/templates/nextblock-template/tailwind.config.ts +19 -0
- package/templates/nextblock-template/tsconfig.json +62 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// context/AuthContext.tsx
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback, useMemo } from 'react';
|
|
5
|
+
import { User, Subscription, SupabaseClient } from '@supabase/supabase-js';
|
|
6
|
+
import { createClient as createSupabaseBrowserClient, getProfileWithRoleClientSide } from '@nextblock-cms/db';
|
|
7
|
+
import { Database } from '@nextblock-cms/db';
|
|
8
|
+
|
|
9
|
+
type Profile = Database['public']['Tables']['profiles']['Row'];
|
|
10
|
+
type UserRole = Database['public']['Enums']['user_role'];
|
|
11
|
+
|
|
12
|
+
interface AuthProviderProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
serverUser: User | null;
|
|
15
|
+
serverProfile: Profile | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface AuthContextType {
|
|
19
|
+
user: User | null;
|
|
20
|
+
profile: Profile | null;
|
|
21
|
+
role: UserRole | null;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
isAdmin: boolean;
|
|
24
|
+
isWriter: boolean;
|
|
25
|
+
isUserRole: boolean;
|
|
26
|
+
supabase: SupabaseClient | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
export const AuthProvider = ({ children, serverUser, serverProfile }: AuthProviderProps) => {
|
|
32
|
+
const [supabase] = useState(() => createSupabaseBrowserClient());
|
|
33
|
+
|
|
34
|
+
const [user, setUser] = useState<User | null>(serverUser);
|
|
35
|
+
const [profile, setProfile] = useState<Profile | null>(serverProfile);
|
|
36
|
+
const [role, setRole] = useState<UserRole | null>(serverProfile?.role ?? null);
|
|
37
|
+
const [isLoading] = useState(false);
|
|
38
|
+
const [authSubscription, setAuthSubscription] = useState<Subscription | null>(null);
|
|
39
|
+
|
|
40
|
+
const handleAuthStateChange = useCallback(async (event: string, session: unknown) => {
|
|
41
|
+
const currentUser = (session && typeof session === 'object' && 'user' in session) ? (session as any).user : null;
|
|
42
|
+
setUser(currentUser);
|
|
43
|
+
|
|
44
|
+
if (currentUser) {
|
|
45
|
+
try {
|
|
46
|
+
const userProfileData = await getProfileWithRoleClientSide(supabase, currentUser.id);
|
|
47
|
+
setProfile(userProfileData);
|
|
48
|
+
setRole(userProfileData?.role ?? null);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error("AuthProvider: Error fetching profile on auth change", e);
|
|
51
|
+
setProfile(null);
|
|
52
|
+
setRole(null);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
setProfile(null);
|
|
56
|
+
setRole(null);
|
|
57
|
+
}
|
|
58
|
+
}, [supabase]);
|
|
59
|
+
|
|
60
|
+
const subscribeToAuth = useCallback(() => {
|
|
61
|
+
if (authSubscription) return; // Already subscribed
|
|
62
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange(handleAuthStateChange);
|
|
63
|
+
setAuthSubscription(subscription);
|
|
64
|
+
}, [supabase, handleAuthStateChange, authSubscription]);
|
|
65
|
+
|
|
66
|
+
const unsubscribeFromAuth = useCallback(() => {
|
|
67
|
+
if (authSubscription) {
|
|
68
|
+
authSubscription.unsubscribe();
|
|
69
|
+
setAuthSubscription(null);
|
|
70
|
+
}
|
|
71
|
+
}, [authSubscription]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
subscribeToAuth();
|
|
75
|
+
|
|
76
|
+
const handleVisibilityChange = () => {
|
|
77
|
+
if (document.visibilityState === 'visible') {
|
|
78
|
+
subscribeToAuth();
|
|
79
|
+
} else {
|
|
80
|
+
unsubscribeFromAuth();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handlePageHide = (event: PageTransitionEvent) => {
|
|
85
|
+
if (!event.persisted) {
|
|
86
|
+
unsubscribeFromAuth(); // This is crucial
|
|
87
|
+
supabase.removeAllChannels();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handlePageShow = (event: PageTransitionEvent) => {
|
|
92
|
+
if (event.persisted) {
|
|
93
|
+
subscribeToAuth();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
98
|
+
window.addEventListener('pagehide', handlePageHide);
|
|
99
|
+
window.addEventListener('pageshow', handlePageShow);
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
103
|
+
window.removeEventListener('pagehide', handlePageHide);
|
|
104
|
+
window.removeEventListener('pageshow', handlePageShow);
|
|
105
|
+
unsubscribeFromAuth();
|
|
106
|
+
supabase.removeAllChannels();
|
|
107
|
+
};
|
|
108
|
+
}, [subscribeToAuth, unsubscribeFromAuth, supabase]);
|
|
109
|
+
|
|
110
|
+
const isAdmin = role === 'ADMIN';
|
|
111
|
+
const isWriter = role === 'WRITER';
|
|
112
|
+
const isUserRole = role === 'USER';
|
|
113
|
+
|
|
114
|
+
const value = useMemo(() => ({
|
|
115
|
+
user,
|
|
116
|
+
profile,
|
|
117
|
+
role,
|
|
118
|
+
isLoading,
|
|
119
|
+
isAdmin,
|
|
120
|
+
isWriter,
|
|
121
|
+
isUserRole,
|
|
122
|
+
supabase,
|
|
123
|
+
}), [user, profile, role, isLoading, isAdmin, isWriter, isUserRole, supabase]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<AuthContext.Provider value={value}>
|
|
127
|
+
{children}
|
|
128
|
+
</AuthContext.Provider>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const useAuth = () => {
|
|
133
|
+
const context = useContext(AuthContext);
|
|
134
|
+
if (context === undefined) {
|
|
135
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
136
|
+
}
|
|
137
|
+
return context;
|
|
138
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface CurrentContent {
|
|
6
|
+
id: string | number | null;
|
|
7
|
+
type: 'page' | 'post' | null;
|
|
8
|
+
slug: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface CurrentContentContextType {
|
|
12
|
+
currentContent: CurrentContent;
|
|
13
|
+
setCurrentContent: (content: CurrentContent) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CurrentContentContext = createContext<CurrentContentContextType | undefined>(undefined);
|
|
17
|
+
|
|
18
|
+
export const CurrentContentProvider = ({ children }: { children: ReactNode }) => {
|
|
19
|
+
const [currentContent, setCurrentContentState] = useState<CurrentContent>({
|
|
20
|
+
id: null,
|
|
21
|
+
type: null,
|
|
22
|
+
slug: null,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const setCurrentContent = (content: CurrentContent) => {
|
|
26
|
+
setCurrentContentState(content);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<CurrentContentContext.Provider value={{ currentContent, setCurrentContent }}>
|
|
31
|
+
{children}
|
|
32
|
+
</CurrentContentContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const useCurrentContent = () => {
|
|
37
|
+
const context = useContext(CurrentContentContext);
|
|
38
|
+
if (context === undefined) {
|
|
39
|
+
throw new Error('useCurrentContent must be used within a CurrentContentProvider');
|
|
40
|
+
}
|
|
41
|
+
return context;
|
|
42
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// context/LanguageContext.tsx
|
|
2
|
+
'use client'; // This directive applies to the rest of the file.
|
|
3
|
+
|
|
4
|
+
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
|
5
|
+
import { Database } from '@nextblock-cms/db';
|
|
6
|
+
import { getActiveLanguagesClientSide } from '@nextblock-cms/db';
|
|
7
|
+
|
|
8
|
+
type Language = Database['public']['Tables']['languages']['Row'];
|
|
9
|
+
import Cookies from 'js-cookie';
|
|
10
|
+
|
|
11
|
+
const LANGUAGE_STORAGE_KEY = 'preferred_locale_storage';
|
|
12
|
+
const LANGUAGE_COOKIE_KEY = 'NEXT_USER_LOCALE';
|
|
13
|
+
const DEFAULT_FALLBACK_LOCALE = 'en'; // Your ultimate fallback
|
|
14
|
+
|
|
15
|
+
interface LanguageContextType {
|
|
16
|
+
currentLocale: string;
|
|
17
|
+
setCurrentLocale: (localeCode: string) => void;
|
|
18
|
+
availableLanguages: Language[];
|
|
19
|
+
defaultLanguage: Language | null;
|
|
20
|
+
isLoadingLanguages: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
|
24
|
+
|
|
25
|
+
interface LanguageProviderProps {
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
serverLocale?: string; // Locale determined on the server (from X-User-Locale header)
|
|
28
|
+
initialAvailableLanguages?: Language[]; // Languages fetched on the server
|
|
29
|
+
initialDefaultLanguage?: Language | null; // Default language determined on the server
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const LanguageProvider = ({
|
|
33
|
+
children,
|
|
34
|
+
serverLocale,
|
|
35
|
+
initialAvailableLanguages,
|
|
36
|
+
initialDefaultLanguage
|
|
37
|
+
}: LanguageProviderProps) => {
|
|
38
|
+
|
|
39
|
+
const [currentLocale, _setCurrentLocale] = useState<string>(() => {
|
|
40
|
+
let initialVal = DEFAULT_FALLBACK_LOCALE;
|
|
41
|
+
if (serverLocale) {
|
|
42
|
+
initialVal = serverLocale;
|
|
43
|
+
} else {
|
|
44
|
+
const cookieVal = Cookies.get(LANGUAGE_COOKIE_KEY);
|
|
45
|
+
if (cookieVal) {
|
|
46
|
+
initialVal = cookieVal;
|
|
47
|
+
} else {
|
|
48
|
+
const storageVal = typeof window !== 'undefined' ? localStorage.getItem(LANGUAGE_STORAGE_KEY) : null;
|
|
49
|
+
if (storageVal) {
|
|
50
|
+
initialVal = storageVal;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return initialVal;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const [availableLanguages, setAvailableLanguages] = useState<Language[]>(initialAvailableLanguages || []);
|
|
58
|
+
const [defaultLanguage, setDefaultLanguage] = useState<Language | null>(initialDefaultLanguage || null);
|
|
59
|
+
// isLoadingLanguages is true if server didn't provide initial languages, or if they were empty.
|
|
60
|
+
const [isLoadingLanguages, setIsLoadingLanguages] = useState(!initialAvailableLanguages || initialAvailableLanguages.length === 0);
|
|
61
|
+
const [clientSelectedLocale, setClientSelectedLocale] = useState<string | null>(null);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
let isMounted = true; // Track mount status
|
|
65
|
+
|
|
66
|
+
const initializeLanguages = async () => {
|
|
67
|
+
setIsLoadingLanguages(true); // Set loading true at the start of initialization
|
|
68
|
+
|
|
69
|
+
let languagesToUse = initialAvailableLanguages;
|
|
70
|
+
let defaultLangToUse = initialDefaultLanguage;
|
|
71
|
+
|
|
72
|
+
// If server didn't provide languages, or they were empty, fetch client-side as a fallback.
|
|
73
|
+
if (!languagesToUse || languagesToUse.length === 0) {
|
|
74
|
+
try {
|
|
75
|
+
const fetchedLangs = await getActiveLanguagesClientSide();
|
|
76
|
+
if (isMounted) { // Check mount status before setting state
|
|
77
|
+
languagesToUse = fetchedLangs;
|
|
78
|
+
setAvailableLanguages(fetchedLangs);
|
|
79
|
+
defaultLangToUse = fetchedLangs.find(lang => lang.is_default) || fetchedLangs[0] || null;
|
|
80
|
+
setDefaultLanguage(defaultLangToUse ?? null);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("LanguageContext: Error fetching languages client-side", error);
|
|
84
|
+
if (isMounted) { // Still ensure we update loading state on error
|
|
85
|
+
languagesToUse = []; // Fallback to empty array
|
|
86
|
+
defaultLangToUse = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!isMounted) return; // Early exit if component unmounted or deps changed
|
|
92
|
+
|
|
93
|
+
// Determine effective locale based on serverLocale, cookies, localStorage, or DB default
|
|
94
|
+
const isValidLang = (lc: string | undefined) => lc && languagesToUse && languagesToUse.some(lang => lang.code === lc);
|
|
95
|
+
const cookieLocale = Cookies.get(LANGUAGE_COOKIE_KEY);
|
|
96
|
+
const storedLocale = typeof window !== 'undefined' ? localStorage.getItem(LANGUAGE_STORAGE_KEY) : null;
|
|
97
|
+
|
|
98
|
+
let effectiveLocale = DEFAULT_FALLBACK_LOCALE; // Start with a fallback
|
|
99
|
+
|
|
100
|
+
if (clientSelectedLocale) {
|
|
101
|
+
// User has made a direct, recent selection. This is the new source of truth for effectiveLocale.
|
|
102
|
+
effectiveLocale = clientSelectedLocale;
|
|
103
|
+
|
|
104
|
+
// If the server has caught up AND this choice is recognized as valid by current availableLanguages,
|
|
105
|
+
// then we can clear clientSelectedLocale. Otherwise, keep it set to ensure it's enforced.
|
|
106
|
+
if (clientSelectedLocale === serverLocale && isValidLang(clientSelectedLocale)) {
|
|
107
|
+
if (isMounted) {
|
|
108
|
+
setClientSelectedLocale(null);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// If clientSelectedLocale is not yet in availableLanguages (isValidLang is false),
|
|
112
|
+
// or serverLocale hasn't caught up, clientSelectedLocale remains set,
|
|
113
|
+
// and effectiveLocale is already correctly set to clientSelectedLocale.
|
|
114
|
+
} else {
|
|
115
|
+
// No recent client selection, use existing prioritization
|
|
116
|
+
effectiveLocale = serverLocale && isValidLang(serverLocale) ? serverLocale
|
|
117
|
+
: (cookieLocale && isValidLang(cookieLocale) ? cookieLocale
|
|
118
|
+
: (storedLocale && isValidLang(storedLocale) ? storedLocale
|
|
119
|
+
: (defaultLangToUse ? defaultLangToUse.code : DEFAULT_FALLBACK_LOCALE)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!isMounted) return; // Early exit
|
|
123
|
+
|
|
124
|
+
if (isMounted) { // Check mount status before final state updates
|
|
125
|
+
_setCurrentLocale(effectiveLocale);
|
|
126
|
+
if (typeof window !== 'undefined') {
|
|
127
|
+
localStorage.setItem(LANGUAGE_STORAGE_KEY, effectiveLocale);
|
|
128
|
+
document.documentElement.lang = effectiveLocale;
|
|
129
|
+
}
|
|
130
|
+
Cookies.set(LANGUAGE_COOKIE_KEY, effectiveLocale, { path: '/', expires: 365, sameSite: 'Lax' });
|
|
131
|
+
setIsLoadingLanguages(false); // Done loading/initializing
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
initializeLanguages();
|
|
136
|
+
|
|
137
|
+
return () => {
|
|
138
|
+
isMounted = false; // Cleanup function to set isMounted to false
|
|
139
|
+
};
|
|
140
|
+
}, [serverLocale, initialAvailableLanguages, initialDefaultLanguage, clientSelectedLocale]);
|
|
141
|
+
|
|
142
|
+
const setCurrentLocaleCallback = useCallback(async (localeCode: string) => { // Add async here
|
|
143
|
+
let localeToSet = DEFAULT_FALLBACK_LOCALE;
|
|
144
|
+
let isValidForRouterRefresh = false;
|
|
145
|
+
|
|
146
|
+
const currentAvailableLangs = availableLanguages; // Use state value which might have been updated by client-side fetch
|
|
147
|
+
|
|
148
|
+
if (currentAvailableLangs.some(lang => lang.code === localeCode)) {
|
|
149
|
+
localeToSet = localeCode;
|
|
150
|
+
isValidForRouterRefresh = true;
|
|
151
|
+
} else if (currentAvailableLangs.length > 0) {
|
|
152
|
+
const dbDefault = currentAvailableLangs.find(lang => lang.is_default);
|
|
153
|
+
localeToSet = dbDefault?.code || currentAvailableLangs[0]?.code || DEFAULT_FALLBACK_LOCALE;
|
|
154
|
+
isValidForRouterRefresh = true;
|
|
155
|
+
} else {
|
|
156
|
+
// No available languages known, still attempt refresh with fallback
|
|
157
|
+
isValidForRouterRefresh = true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_setCurrentLocale(localeToSet);
|
|
161
|
+
if (typeof window !== 'undefined') {
|
|
162
|
+
localStorage.setItem(LANGUAGE_STORAGE_KEY, localeToSet);
|
|
163
|
+
document.documentElement.lang = localeToSet;
|
|
164
|
+
}
|
|
165
|
+
Cookies.set(LANGUAGE_COOKIE_KEY, localeToSet, { path: '/', expires: 365, sameSite: 'Lax' });
|
|
166
|
+
|
|
167
|
+
// The LanguageSwitcher component is responsible for navigation (push or refresh).
|
|
168
|
+
// We only set the clientSelectedLocale here to ensure the main useEffect
|
|
169
|
+
// in LanguageProvider can pick up this explicit choice if needed during initialization
|
|
170
|
+
// or if the serverLocale hasn't caught up yet.
|
|
171
|
+
if (isValidForRouterRefresh) { // This check might still be useful for setClientSelectedLocale
|
|
172
|
+
setClientSelectedLocale(localeToSet);
|
|
173
|
+
}
|
|
174
|
+
// router.refresh(); // REMOVED: LanguageSwitcher will handle navigation/refresh.
|
|
175
|
+
}, [availableLanguages]);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (currentLocale && typeof window !== 'undefined') {
|
|
179
|
+
document.documentElement.lang = currentLocale;
|
|
180
|
+
}
|
|
181
|
+
}, [currentLocale]);
|
|
182
|
+
|
|
183
|
+
const contextValue = React.useMemo(() => {
|
|
184
|
+
return {
|
|
185
|
+
currentLocale: currentLocale,
|
|
186
|
+
setCurrentLocale: setCurrentLocaleCallback,
|
|
187
|
+
availableLanguages,
|
|
188
|
+
defaultLanguage,
|
|
189
|
+
isLoadingLanguages,
|
|
190
|
+
};
|
|
191
|
+
}, [currentLocale, setCurrentLocaleCallback, availableLanguages, defaultLanguage, isLoadingLanguages]);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<LanguageContext.Provider value={contextValue}>
|
|
195
|
+
{children}
|
|
196
|
+
</LanguageContext.Provider>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const useLanguage = () => {
|
|
201
|
+
const context = useContext(LanguageContext);
|
|
202
|
+
if (context === undefined) {
|
|
203
|
+
throw new Error('useLanguage must be used within a LanguageProvider');
|
|
204
|
+
}
|
|
205
|
+
return context;
|
|
206
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# CMS Application: Overview
|
|
2
|
+
|
|
3
|
+
This document provides a high-level overview of the main Next.js application for the CMS, detailing its structure, core modules, and key functionalities. It serves as a guide for understanding how the different parts of the CMS are organized and interact.
|
|
4
|
+
|
|
5
|
+
## 1. CMS Root Layout (`app/cms/layout.tsx`)
|
|
6
|
+
|
|
7
|
+
The file [`app/cms/layout.tsx`](app/cms/layout.tsx) is the foundational component for the entire CMS interface, wrapping all pages under the `/cms` path.
|
|
8
|
+
|
|
9
|
+
### Key Responsibilities:
|
|
10
|
+
|
|
11
|
+
* **Authentication and Access Control**: The layout uses the `useAuth` hook from `AuthContext` to get the current user's status and role (`isAdmin`, `isWriter`). It contains a `useEffect` hook that protects the CMS routes by redirecting unauthenticated users to the sign-in page and users with insufficient permissions to an "unauthorized" page.
|
|
12
|
+
|
|
13
|
+
* **UI Structure**: It renders the primary user interface shell, which consists of:
|
|
14
|
+
* A **responsive sidebar** for navigation, which is permanently visible on desktop screens and can be toggled on mobile.
|
|
15
|
+
* A **main content area** that displays the specific page component for the current route.
|
|
16
|
+
* A **dynamic header** within the main content area that displays the title of the current page.
|
|
17
|
+
|
|
18
|
+
* **Dynamic Page Title**: The header's title is not static. It's dynamically determined by inspecting the current URL's `pathname` using the `usePathname` hook. A series of `if/else if` statements map URL patterns (e.g., `/cms/pages`, `/cms/pages/new`) to user-friendly titles (e.g., "Pages", "New Page").
|
|
19
|
+
|
|
20
|
+
* **User Profile & Logout**: The sidebar includes a section at the bottom displaying the logged-in user's avatar, name, and role, along with a button to sign out.
|
|
21
|
+
|
|
22
|
+
## 2. Core CMS Modules
|
|
23
|
+
|
|
24
|
+
The CMS is organized into several modules, each corresponding to a specific content type or area of management. These modules are located in subdirectories within `app/cms/`. They follow a consistent file structure pattern.
|
|
25
|
+
|
|
26
|
+
### Common File Structure:
|
|
27
|
+
|
|
28
|
+
* `page.tsx`: The main entry point for the module, typically displaying a list or grid of the items (e.g., a table of pages, a grid of media).
|
|
29
|
+
* `[id]/edit/page.tsx`: The page for editing a single, existing item, identified by its ID in the URL.
|
|
30
|
+
* `new/page.tsx`: The page for creating a new item.
|
|
31
|
+
* `actions.ts`: A file containing all the server actions for the module (e.g., `createPage`, `updatePage`, `deletePage`).
|
|
32
|
+
* `components/`: A directory for React components that are specific to this module, such as forms or client-side components that invoke server actions (e.g., `DeletePageButtonClient.tsx`).
|
|
33
|
+
|
|
34
|
+
### Module Breakdown:
|
|
35
|
+
|
|
36
|
+
* **Dashboard (`app/cms/dashboard/`)**: The main landing page after logging into the CMS.
|
|
37
|
+
* `page.tsx`: Displays welcome information and summary statistics.
|
|
38
|
+
|
|
39
|
+
* **Pages (`app/cms/pages/`)**: Manages the static pages of the public-facing website.
|
|
40
|
+
* Key files: `page.tsx`, `[id]/edit/page.tsx`, `new/page.tsx`, `actions.ts`, `components/PageForm.tsx`, `components/DeletePageButtonClient.tsx`.
|
|
41
|
+
|
|
42
|
+
* **Posts (`app/cms/posts/`)**: Manages blog posts or articles.
|
|
43
|
+
* Key files: `page.tsx`, `[id]/edit/page.tsx`, `new/page.tsx`, `actions.ts`, `components/PostForm.tsx`, `components/DeletePostButtonClient.tsx`.
|
|
44
|
+
|
|
45
|
+
* **Media (`app/cms/media/`)**: Manages the website's media library (images, documents, etc.).
|
|
46
|
+
* Key files: `page.tsx`, `[id]/edit/page.tsx`, `actions.ts`, `components/MediaUploadForm.tsx`, `components/MediaGridClient.tsx`, `components/MediaEditForm.tsx`.
|
|
47
|
+
|
|
48
|
+
* **Navigation (`app/cms/navigation/`)**: Manages the site's navigation menus.
|
|
49
|
+
* Key files: `page.tsx`, `[id]/edit/page.tsx`, `new/page.tsx`, `actions.ts`, `components/NavigationMenuDnd.tsx`, `components/NavigationItemForm.tsx`.
|
|
50
|
+
|
|
51
|
+
* **Users (`app/cms/users/`)**: (Admin only) Manages user accounts and roles.
|
|
52
|
+
* Key files: `page.tsx`, `[id]/edit/page.tsx`, `actions.ts`, `components/UserForm.tsx`, `components/DeleteUserButton.tsx`.
|
|
53
|
+
|
|
54
|
+
* **Settings (`app/cms/settings/`)**: Manages site-wide settings. This module contains sub-modules for different settings areas.
|
|
55
|
+
* **Languages (`app/cms/settings/languages/`)**: (Admin only) Manages available content languages.
|
|
56
|
+
* Key files: `page.tsx`, `[id]/edit/page.tsx`, `new/page.tsx`, `actions.ts`, `components/LanguageForm.tsx`.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# CMS Block Editor: Architecture Overview
|
|
2
|
+
|
|
3
|
+
This document provides a comprehensive overview of the block-based content editor's architecture, serving as a technical reference for development.
|
|
4
|
+
|
|
5
|
+
## 1. Core Concepts
|
|
6
|
+
|
|
7
|
+
### Database Schema
|
|
8
|
+
The entire system is powered by a single `blocks` table in Supabase. The key field is `content` (a `JSONB` type), which stores a flexible JSON object containing all data for a specific block. This approach allows for adding new block types and modifying existing ones without requiring database schema migrations.
|
|
9
|
+
|
|
10
|
+
### Block Registry
|
|
11
|
+
The file [`lib/blocks/blockRegistry.ts`](/lib/blocks/blockRegistry.ts) is the **single source of truth** for all block types. It's a central configuration object that maps a block's `block_type` string to its:
|
|
12
|
+
- User-facing `label`.
|
|
13
|
+
- `initialContent` structure.
|
|
14
|
+
- The filename of its corresponding `editorComponent` and `rendererComponent`.
|
|
15
|
+
|
|
16
|
+
Any new block must be defined here first.
|
|
17
|
+
|
|
18
|
+
## 2. Key Architectural Patterns
|
|
19
|
+
|
|
20
|
+
### Editor/Renderer Split
|
|
21
|
+
The architecture maintains a strict separation between the editing interface and the final public-facing output.
|
|
22
|
+
- **Editors** (`app/cms/blocks/editors/*.tsx`): React components that render the forms and controls for modifying a block's `content`.
|
|
23
|
+
- **Renderers** (`components/blocks/renderers/*.tsx`): React components that take a block's `content` and render the final, styled HTML for the public website.
|
|
24
|
+
|
|
25
|
+
### Optimistic UI & Debounced Saves
|
|
26
|
+
To provide a fast and smooth user experience, the editor uses an optimistic UI pattern.
|
|
27
|
+
- All state is centralized in [`app/cms/blocks/components/BlockEditorArea.tsx`](/app/cms/blocks/components/BlockEditorArea.tsx) and managed with React's `useOptimistic` hook.
|
|
28
|
+
- When a user makes a change, the UI updates instantly.
|
|
29
|
+
- The actual save operation is handled by a debounced call to the `updateBlock` server action in [`app/cms/blocks/actions.ts`](/app/cms/blocks/actions.ts).
|
|
30
|
+
- Crucially, `updateBlock` **does not** call `revalidatePath`, preventing disruptive page reloads during editing sessions.
|
|
31
|
+
|
|
32
|
+
## 3. Component Breakdown & Data Flow
|
|
33
|
+
|
|
34
|
+
The editor is a hierarchy of components, each with a specific responsibility.
|
|
35
|
+
|
|
36
|
+
#### `BlockEditorArea.tsx`
|
|
37
|
+
- **Location:** [`app/cms/blocks/components/BlockEditorArea.tsx`](/app/cms/blocks/components/BlockEditorArea.tsx)
|
|
38
|
+
- **Role:** The root component for the editor. It fetches the initial blocks, manages the master `blocks` state (including the optimistic state), and provides the main `DndContext` for top-level drag-and-drop functionality.
|
|
39
|
+
|
|
40
|
+
#### `SortableBlockItem.tsx`
|
|
41
|
+
- **Location:** [`app/cms/blocks/components/SortableBlockItem.tsx`](/app/cms/blocks/components/SortableBlockItem.tsx)
|
|
42
|
+
- **Role:** A wrapper for each individual block that provides the `useSortable` context from `dnd-kit`, making each block draggable. It passes `dragHandleProps` down to its child.
|
|
43
|
+
|
|
44
|
+
#### `EditableBlock.tsx`
|
|
45
|
+
- **Location:** [`app/cms/blocks/components/EditableBlock.tsx`](/app/cms/blocks/components/EditableBlock.tsx)
|
|
46
|
+
- **Role:** The primary controller for a single block. It renders the block's header, including the drag handle and edit/delete buttons.
|
|
47
|
+
- **Core Logic:** This component contains the critical conditional logic for the editing experience:
|
|
48
|
+
- If the `block.block_type` is `section` or `hero`, clicking the edit icon toggles an `isConfigPanelOpen` state, which shows/hides the inline configuration panel.
|
|
49
|
+
- For **all other block types**, clicking the edit icon lazy-loads the appropriate editor component and opens the `BlockEditorModal`.
|
|
50
|
+
|
|
51
|
+
#### `BlockEditorModal.tsx`
|
|
52
|
+
- **Location:** [`app/cms/blocks/components/BlockEditorModal.tsx`](/app/cms/blocks/components/BlockEditorModal.tsx)
|
|
53
|
+
- **Role:** A reusable modal dialog for editing standard blocks.
|
|
54
|
+
- **Functionality:** It receives a lazy-loaded editor component and wraps it in `<React.Suspense>`, showing a loading state *inside* the modal. It manages a temporary state for the block's content and calls the `onContentChange` callback on save. For the Rich Text block, it is styled to be extra tall (`h-[90vh]`).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### Section & Column Components
|
|
59
|
+
|
|
60
|
+
The "Section" block type enables complex multi-column layouts.
|
|
61
|
+
|
|
62
|
+
#### `SectionBlockEditor.tsx`
|
|
63
|
+
- **Location:** [`app/cms/blocks/editors/SectionBlockEditor.tsx`](/app/cms/blocks/editors/SectionBlockEditor.tsx)
|
|
64
|
+
- **Role:** The editor for the `section` block. It conditionally renders the `SectionConfigPanel` based on the `isConfigPanelOpen` prop and contains the `ColumnEditor`s.
|
|
65
|
+
|
|
66
|
+
#### `SectionConfigPanel.tsx` & `BackgroundSelector.tsx`
|
|
67
|
+
- **Location:** [`app/cms/blocks/components/SectionConfigPanel.tsx`](/app/cms/blocks/components/SectionConfigPanel.tsx), [`app/cms/blocks/components/BackgroundSelector.tsx`](/app/cms/blocks/components/BackgroundSelector.tsx)
|
|
68
|
+
- **Role:** These components render the forms for section-level settings (layout, columns, background).
|
|
69
|
+
- **UI Pattern:** The custom inputs (`Minimum Height`, `Image Position`, etc.) use a "disabled save button" pattern. The save icon is disabled by default and only becomes active and green when the user has made a change.
|
|
70
|
+
|
|
71
|
+
#### `ColumnEditor.tsx`
|
|
72
|
+
- **Location:** [`app/cms/blocks/components/ColumnEditor.tsx`](/app/cms/blocks/components/ColumnEditor.tsx)
|
|
73
|
+
- **Role:** Manages the content *within a single column* of a section. It has its own `DndContext` for nested drag-and-drop and uses the same `BlockEditorModal` to edit the blocks it contains.
|