create-nextblock 0.16.4 → 0.17.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/CLAUDE.md +22 -0
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- package/package.json +1 -1
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/CLAUDE.md +26 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +270 -205
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +8096 -7198
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +11 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/lib/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -99
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +42 -12
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
- package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
|
@@ -1,251 +1,251 @@
|
|
|
1
|
-
// app/cms/posts/page.tsx
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { Button } from "@nextblock-cms/ui";
|
|
6
|
-
import {
|
|
7
|
-
Table,
|
|
8
|
-
TableBody,
|
|
9
|
-
TableCell,
|
|
10
|
-
TableHead,
|
|
11
|
-
TableHeader,
|
|
12
|
-
TableRow,
|
|
13
|
-
} from "@nextblock-cms/ui";
|
|
14
|
-
import { Badge } from "@nextblock-cms/ui";
|
|
15
|
-
import { Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
16
|
-
import { MoreHorizontal, PlusCircle, Edit3, PenTool } from "lucide-react";
|
|
17
|
-
import {
|
|
18
|
-
DropdownMenu,
|
|
19
|
-
DropdownMenuContent,
|
|
20
|
-
DropdownMenuItem,
|
|
21
|
-
DropdownMenuButtonTrigger,
|
|
22
|
-
DropdownMenuSeparator,
|
|
23
|
-
} from "@nextblock-cms/ui";
|
|
24
|
-
import type { Database } from "@nextblock-cms/db";
|
|
25
|
-
import { getActiveLanguagesServerSide } from "@nextblock-cms/db/server";
|
|
26
|
-
import { resolveMediaUrl } from "../../../lib/media/resolveMediaUrl";
|
|
27
|
-
import { auditSeo } from "@nextblock-cms/utils/seo";
|
|
28
|
-
import { buildPageSeoDocument } from "../../../lib/seo/page-document";
|
|
29
|
-
|
|
30
|
-
type Post = Database['public']['Tables']['posts']['Row'] & { feature_image_url?: string | null };
|
|
31
|
-
import LanguageFilterSelect from "../components/LanguageFilterSelect";
|
|
32
|
-
import DeletePostButtonClient from "./components/DeletePostButtonClient";
|
|
33
|
-
import { ContentTransferControls } from "../import-export/ContentTransferControls";
|
|
34
|
-
import VisibilityBadge from "../components/VisibilityBadge";
|
|
35
|
-
import SeoScoreBadge from "../components/SeoScoreBadge";
|
|
36
|
-
import TablePagination from "../components/TablePagination";
|
|
37
|
-
|
|
38
|
-
async function getPostsWithDetails(
|
|
39
|
-
filterLanguageId?: number,
|
|
40
|
-
pageNumber: number = 1,
|
|
41
|
-
pageSize: number = 25
|
|
42
|
-
): Promise<{
|
|
43
|
-
items: { post: Post; languageCode: string; seoScore: number }[];
|
|
44
|
-
totalCount: number;
|
|
45
|
-
}> {
|
|
46
|
-
const supabase = createClient();
|
|
47
|
-
const languages = await getActiveLanguagesServerSide();
|
|
48
|
-
const langMap = new Map(languages.map(l => [l.id, l.code]));
|
|
49
|
-
|
|
50
|
-
let query = supabase
|
|
51
|
-
.from("posts")
|
|
52
|
-
.select("*, languages!inner(code), media ( object_key ), blocks(id, block_type, content, order)", { count: "exact" })
|
|
53
|
-
.order("created_at", { ascending: false });
|
|
54
|
-
|
|
55
|
-
if (filterLanguageId) {
|
|
56
|
-
query = query.eq("language_id", filterLanguageId);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const from = (pageNumber - 1) * pageSize;
|
|
60
|
-
const to = from + pageSize - 1;
|
|
61
|
-
|
|
62
|
-
const { data: postsData, count, error } = await query.range(from, to);
|
|
63
|
-
|
|
64
|
-
if (error) {
|
|
65
|
-
console.error("Error fetching posts:", error);
|
|
66
|
-
return { items: [], totalCount: 0 };
|
|
67
|
-
}
|
|
68
|
-
if (!postsData) return { items: [], totalCount: 0 };
|
|
69
|
-
|
|
70
|
-
const items = postsData.map(p => {
|
|
71
|
-
const langInfo = p.languages as unknown as { code: string } | null;
|
|
72
|
-
const rawBlocks = (p.blocks || []) as unknown as Parameters<typeof buildPageSeoDocument>[0];
|
|
73
|
-
const doc = buildPageSeoDocument(rawBlocks, {
|
|
74
|
-
documentTitle: p.title,
|
|
75
|
-
documentType: "post",
|
|
76
|
-
});
|
|
77
|
-
const auditResult = auditSeo({
|
|
78
|
-
document: doc,
|
|
79
|
-
metaTitle: p.meta_title ?? undefined,
|
|
80
|
-
metaDescription: p.meta_description ?? undefined,
|
|
81
|
-
scope: "page",
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const langCode = langInfo?.code || langMap.get(p.language_id) || "N/A";
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
post: { ...p, feature_image_url: resolveMediaUrl(p.media?.object_key) } as Post,
|
|
88
|
-
languageCode: String(langCode).toUpperCase(),
|
|
89
|
-
seoScore: auditResult.score,
|
|
90
|
-
};
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
return { items, totalCount: count ?? items.length };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface CmsPostsListPageProps {
|
|
97
|
-
searchParams?: Promise<{
|
|
98
|
-
lang?: string;
|
|
99
|
-
success?: string;
|
|
100
|
-
page?: string;
|
|
101
|
-
pageSize?: string;
|
|
102
|
-
}>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
|
|
106
|
-
const searchParams = await props.searchParams;
|
|
107
|
-
const allLanguages = await getActiveLanguagesServerSide();
|
|
108
|
-
const selectedLangId = searchParams?.lang ? parseInt(searchParams.lang, 10) : undefined;
|
|
109
|
-
const isValidLangId = selectedLangId ? allLanguages.some(l => l.id === selectedLangId) : true;
|
|
110
|
-
const filterLangId = isValidLangId ? selectedLangId : undefined;
|
|
111
|
-
|
|
112
|
-
const pageNumber = searchParams?.page
|
|
113
|
-
? Math.max(1, parseInt(searchParams.page, 10) || 1)
|
|
114
|
-
: 1;
|
|
115
|
-
const pageSize = searchParams?.pageSize
|
|
116
|
-
? Math.max(1, parseInt(searchParams.pageSize, 10) || 25)
|
|
117
|
-
: 25;
|
|
118
|
-
|
|
119
|
-
const { items: postsWithDetails, totalCount } = await getPostsWithDetails(
|
|
120
|
-
filterLangId,
|
|
121
|
-
pageNumber,
|
|
122
|
-
pageSize
|
|
123
|
-
);
|
|
124
|
-
const successMessage = searchParams?.success;
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<div className="w-full">
|
|
128
|
-
<div className="flex justify-between items-center mb-6 flex-wrap gap-4">
|
|
129
|
-
<h1 className="text-2xl font-semibold">Manage Posts</h1>
|
|
130
|
-
<div className="flex items-center gap-3">
|
|
131
|
-
<ContentTransferControls
|
|
132
|
-
contentType="posts"
|
|
133
|
-
label="Posts"
|
|
134
|
-
languageId={filterLangId}
|
|
135
|
-
hasContent={postsWithDetails.length > 0}
|
|
136
|
-
/>
|
|
137
|
-
<LanguageFilterSelect
|
|
138
|
-
allLanguages={allLanguages}
|
|
139
|
-
currentFilterLangId={filterLangId}
|
|
140
|
-
basePath="/cms/posts"
|
|
141
|
-
/>
|
|
142
|
-
<Button variant="default" asChild>
|
|
143
|
-
<Link href="/cms/posts/new">
|
|
144
|
-
<PlusCircle className="mr-2 h-4 w-4" /> Create New Post
|
|
145
|
-
</Link>
|
|
146
|
-
</Button>
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
|
|
150
|
-
{successMessage && (
|
|
151
|
-
<Alert variant="success" className="mb-4">
|
|
152
|
-
<AlertDescription>
|
|
153
|
-
{decodeURIComponent(successMessage)}
|
|
154
|
-
</AlertDescription>
|
|
155
|
-
</Alert>
|
|
156
|
-
)}
|
|
157
|
-
|
|
158
|
-
{totalCount === 0 ? (
|
|
159
|
-
<div className="text-center py-10 border rounded-lg dark:border-slate-700">
|
|
160
|
-
<PenTool className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
161
|
-
<h3 className="mt-2 text-sm font-medium text-foreground">
|
|
162
|
-
{filterLangId
|
|
163
|
-
? "No posts found for the selected language."
|
|
164
|
-
: "No posts found."}
|
|
165
|
-
</h3>
|
|
166
|
-
<p className="mt-1 text-sm text-muted-foreground">
|
|
167
|
-
Get started by creating a new post.
|
|
168
|
-
</p>
|
|
169
|
-
<div className="mt-6">
|
|
170
|
-
<Button asChild>
|
|
171
|
-
<Link href="/cms/posts/new">
|
|
172
|
-
<PlusCircle className="mr-2 h-4 w-4" /> Create Post
|
|
173
|
-
</Link>
|
|
174
|
-
</Button>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
) : (
|
|
178
|
-
<div className="rounded-lg border overflow-hidden dark:border-slate-700">
|
|
179
|
-
<Table>
|
|
180
|
-
<TableHeader>
|
|
181
|
-
<TableRow className="dark:border-slate-700">
|
|
182
|
-
<TableHead className="w-[280px] sm:w-[350px]">Title</TableHead>
|
|
183
|
-
<TableHead>Status</TableHead>
|
|
184
|
-
<TableHead>Language</TableHead>
|
|
185
|
-
<TableHead className="hidden md:table-cell">Slug</TableHead>
|
|
186
|
-
<TableHead>SEO</TableHead>
|
|
187
|
-
<TableHead className="hidden lg:table-cell">Published At</TableHead>
|
|
188
|
-
<TableHead className="text-right w-[80px]">Actions</TableHead>
|
|
189
|
-
</TableRow>
|
|
190
|
-
</TableHeader>
|
|
191
|
-
<TableBody>
|
|
192
|
-
{postsWithDetails.map(({ post, languageCode, seoScore }) => (
|
|
193
|
-
<TableRow key={post.id} className="dark:border-slate-700">
|
|
194
|
-
<TableCell className="font-medium">
|
|
195
|
-
<Link
|
|
196
|
-
href={`/cms/posts/${post.id}/edit`}
|
|
197
|
-
className="flex items-center cursor-pointer"
|
|
198
|
-
>
|
|
199
|
-
<Edit3 className="mr-2 h-4 w-4" />
|
|
200
|
-
{post.title}
|
|
201
|
-
</Link>
|
|
202
|
-
</TableCell>
|
|
203
|
-
<TableCell>
|
|
204
|
-
<VisibilityBadge
|
|
205
|
-
type="post"
|
|
206
|
-
status={post.status}
|
|
207
|
-
publishedAt={post.published_at}
|
|
208
|
-
/>
|
|
209
|
-
</TableCell>
|
|
210
|
-
<TableCell><Badge variant="outline" className="dark:border-slate-600">{languageCode}</Badge></TableCell>
|
|
211
|
-
<TableCell className="text-muted-foreground text-xs hidden md:table-cell">/article/{post.slug}</TableCell>
|
|
212
|
-
<TableCell>
|
|
213
|
-
<SeoScoreBadge score={seoScore} />
|
|
214
|
-
</TableCell>
|
|
215
|
-
<TableCell className="hidden lg:table-cell text-xs text-muted-foreground">
|
|
216
|
-
{post.published_at ? new Date(post.published_at).toLocaleDateString() : "Not yet"}
|
|
217
|
-
</TableCell>
|
|
218
|
-
<TableCell className="text-right">
|
|
219
|
-
<DropdownMenu>
|
|
220
|
-
<DropdownMenuButtonTrigger id={`post-trigger-${post.id}`}>
|
|
221
|
-
<MoreHorizontal className="h-4 w-4" />
|
|
222
|
-
<span className="sr-only">Post actions for {post.title}</span>
|
|
223
|
-
</DropdownMenuButtonTrigger>
|
|
224
|
-
<DropdownMenuContent align="end">
|
|
225
|
-
<DropdownMenuItem asChild>
|
|
226
|
-
<Link href={`/cms/posts/${post.id}/edit`} className="flex items-center cursor-pointer">
|
|
227
|
-
<Edit3 className="mr-2 h-4 w-4" /> Edit
|
|
228
|
-
</Link>
|
|
229
|
-
</DropdownMenuItem>
|
|
230
|
-
<DropdownMenuSeparator />
|
|
231
|
-
<DeletePostButtonClient postId={post.id} />
|
|
232
|
-
</DropdownMenuContent>
|
|
233
|
-
</DropdownMenu>
|
|
234
|
-
</TableCell>
|
|
235
|
-
</TableRow>
|
|
236
|
-
))}
|
|
237
|
-
</TableBody>
|
|
238
|
-
</Table>
|
|
239
|
-
|
|
240
|
-
<TablePagination
|
|
241
|
-
currentPage={pageNumber}
|
|
242
|
-
pageSize={pageSize}
|
|
243
|
-
totalCount={totalCount}
|
|
244
|
-
basePath="/cms/posts"
|
|
245
|
-
itemLabel="posts"
|
|
246
|
-
/>
|
|
247
|
-
</div>
|
|
248
|
-
)}
|
|
249
|
-
</div>
|
|
250
|
-
);
|
|
251
|
-
}
|
|
1
|
+
// app/cms/posts/page.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { Button } from "@nextblock-cms/ui";
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from "@nextblock-cms/ui";
|
|
14
|
+
import { Badge } from "@nextblock-cms/ui";
|
|
15
|
+
import { Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
16
|
+
import { MoreHorizontal, PlusCircle, Edit3, PenTool } from "lucide-react";
|
|
17
|
+
import {
|
|
18
|
+
DropdownMenu,
|
|
19
|
+
DropdownMenuContent,
|
|
20
|
+
DropdownMenuItem,
|
|
21
|
+
DropdownMenuButtonTrigger,
|
|
22
|
+
DropdownMenuSeparator,
|
|
23
|
+
} from "@nextblock-cms/ui";
|
|
24
|
+
import type { Database } from "@nextblock-cms/db";
|
|
25
|
+
import { getActiveLanguagesServerSide } from "@nextblock-cms/db/server";
|
|
26
|
+
import { resolveMediaUrl } from "../../../lib/media/resolveMediaUrl";
|
|
27
|
+
import { auditSeo } from "@nextblock-cms/utils/seo";
|
|
28
|
+
import { buildPageSeoDocument } from "../../../lib/seo/page-document";
|
|
29
|
+
|
|
30
|
+
type Post = Database['public']['Tables']['posts']['Row'] & { feature_image_url?: string | null };
|
|
31
|
+
import LanguageFilterSelect from "../components/LanguageFilterSelect";
|
|
32
|
+
import DeletePostButtonClient from "./components/DeletePostButtonClient";
|
|
33
|
+
import { ContentTransferControls } from "../import-export/ContentTransferControls";
|
|
34
|
+
import VisibilityBadge from "../components/VisibilityBadge";
|
|
35
|
+
import SeoScoreBadge from "../components/SeoScoreBadge";
|
|
36
|
+
import TablePagination from "../components/TablePagination";
|
|
37
|
+
|
|
38
|
+
async function getPostsWithDetails(
|
|
39
|
+
filterLanguageId?: number,
|
|
40
|
+
pageNumber: number = 1,
|
|
41
|
+
pageSize: number = 25
|
|
42
|
+
): Promise<{
|
|
43
|
+
items: { post: Post; languageCode: string; seoScore: number }[];
|
|
44
|
+
totalCount: number;
|
|
45
|
+
}> {
|
|
46
|
+
const supabase = createClient();
|
|
47
|
+
const languages = await getActiveLanguagesServerSide();
|
|
48
|
+
const langMap = new Map(languages.map(l => [l.id, l.code]));
|
|
49
|
+
|
|
50
|
+
let query = supabase
|
|
51
|
+
.from("posts")
|
|
52
|
+
.select("*, languages!inner(code), media ( object_key ), blocks(id, block_type, content, order)", { count: "exact" })
|
|
53
|
+
.order("created_at", { ascending: false });
|
|
54
|
+
|
|
55
|
+
if (filterLanguageId) {
|
|
56
|
+
query = query.eq("language_id", filterLanguageId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const from = (pageNumber - 1) * pageSize;
|
|
60
|
+
const to = from + pageSize - 1;
|
|
61
|
+
|
|
62
|
+
const { data: postsData, count, error } = await query.range(from, to);
|
|
63
|
+
|
|
64
|
+
if (error) {
|
|
65
|
+
console.error("Error fetching posts:", error);
|
|
66
|
+
return { items: [], totalCount: 0 };
|
|
67
|
+
}
|
|
68
|
+
if (!postsData) return { items: [], totalCount: 0 };
|
|
69
|
+
|
|
70
|
+
const items = postsData.map(p => {
|
|
71
|
+
const langInfo = p.languages as unknown as { code: string } | null;
|
|
72
|
+
const rawBlocks = (p.blocks || []) as unknown as Parameters<typeof buildPageSeoDocument>[0];
|
|
73
|
+
const doc = buildPageSeoDocument(rawBlocks, {
|
|
74
|
+
documentTitle: p.title,
|
|
75
|
+
documentType: "post",
|
|
76
|
+
});
|
|
77
|
+
const auditResult = auditSeo({
|
|
78
|
+
document: doc,
|
|
79
|
+
metaTitle: p.meta_title ?? undefined,
|
|
80
|
+
metaDescription: p.meta_description ?? undefined,
|
|
81
|
+
scope: "page",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const langCode = langInfo?.code || langMap.get(p.language_id) || "N/A";
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
post: { ...p, feature_image_url: resolveMediaUrl(p.media?.object_key) } as Post,
|
|
88
|
+
languageCode: String(langCode).toUpperCase(),
|
|
89
|
+
seoScore: auditResult.score,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return { items, totalCount: count ?? items.length };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface CmsPostsListPageProps {
|
|
97
|
+
searchParams?: Promise<{
|
|
98
|
+
lang?: string;
|
|
99
|
+
success?: string;
|
|
100
|
+
page?: string;
|
|
101
|
+
pageSize?: string;
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default async function CmsPostsListPage(props: CmsPostsListPageProps) {
|
|
106
|
+
const searchParams = await props.searchParams;
|
|
107
|
+
const allLanguages = await getActiveLanguagesServerSide();
|
|
108
|
+
const selectedLangId = searchParams?.lang ? parseInt(searchParams.lang, 10) : undefined;
|
|
109
|
+
const isValidLangId = selectedLangId ? allLanguages.some(l => l.id === selectedLangId) : true;
|
|
110
|
+
const filterLangId = isValidLangId ? selectedLangId : undefined;
|
|
111
|
+
|
|
112
|
+
const pageNumber = searchParams?.page
|
|
113
|
+
? Math.max(1, parseInt(searchParams.page, 10) || 1)
|
|
114
|
+
: 1;
|
|
115
|
+
const pageSize = searchParams?.pageSize
|
|
116
|
+
? Math.max(1, parseInt(searchParams.pageSize, 10) || 25)
|
|
117
|
+
: 25;
|
|
118
|
+
|
|
119
|
+
const { items: postsWithDetails, totalCount } = await getPostsWithDetails(
|
|
120
|
+
filterLangId,
|
|
121
|
+
pageNumber,
|
|
122
|
+
pageSize
|
|
123
|
+
);
|
|
124
|
+
const successMessage = searchParams?.success;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className="w-full">
|
|
128
|
+
<div className="flex justify-between items-center mb-6 flex-wrap gap-4">
|
|
129
|
+
<h1 className="text-2xl font-semibold">Manage Posts</h1>
|
|
130
|
+
<div className="flex items-center gap-3">
|
|
131
|
+
<ContentTransferControls
|
|
132
|
+
contentType="posts"
|
|
133
|
+
label="Posts"
|
|
134
|
+
languageId={filterLangId}
|
|
135
|
+
hasContent={postsWithDetails.length > 0}
|
|
136
|
+
/>
|
|
137
|
+
<LanguageFilterSelect
|
|
138
|
+
allLanguages={allLanguages}
|
|
139
|
+
currentFilterLangId={filterLangId}
|
|
140
|
+
basePath="/cms/posts"
|
|
141
|
+
/>
|
|
142
|
+
<Button variant="default" asChild>
|
|
143
|
+
<Link href="/cms/posts/new">
|
|
144
|
+
<PlusCircle className="mr-2 h-4 w-4" /> Create New Post
|
|
145
|
+
</Link>
|
|
146
|
+
</Button>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{successMessage && (
|
|
151
|
+
<Alert variant="success" className="mb-4">
|
|
152
|
+
<AlertDescription>
|
|
153
|
+
{decodeURIComponent(successMessage)}
|
|
154
|
+
</AlertDescription>
|
|
155
|
+
</Alert>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{totalCount === 0 ? (
|
|
159
|
+
<div className="text-center py-10 border rounded-lg dark:border-slate-700">
|
|
160
|
+
<PenTool className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
161
|
+
<h3 className="mt-2 text-sm font-medium text-foreground">
|
|
162
|
+
{filterLangId
|
|
163
|
+
? "No posts found for the selected language."
|
|
164
|
+
: "No posts found."}
|
|
165
|
+
</h3>
|
|
166
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
167
|
+
Get started by creating a new post.
|
|
168
|
+
</p>
|
|
169
|
+
<div className="mt-6">
|
|
170
|
+
<Button asChild>
|
|
171
|
+
<Link href="/cms/posts/new">
|
|
172
|
+
<PlusCircle className="mr-2 h-4 w-4" /> Create Post
|
|
173
|
+
</Link>
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
) : (
|
|
178
|
+
<div className="rounded-lg border overflow-hidden dark:border-slate-700">
|
|
179
|
+
<Table>
|
|
180
|
+
<TableHeader>
|
|
181
|
+
<TableRow className="dark:border-slate-700">
|
|
182
|
+
<TableHead className="w-[280px] sm:w-[350px]">Title</TableHead>
|
|
183
|
+
<TableHead>Status</TableHead>
|
|
184
|
+
<TableHead>Language</TableHead>
|
|
185
|
+
<TableHead className="hidden md:table-cell">Slug</TableHead>
|
|
186
|
+
<TableHead>SEO</TableHead>
|
|
187
|
+
<TableHead className="hidden lg:table-cell">Published At</TableHead>
|
|
188
|
+
<TableHead className="text-right w-[80px]">Actions</TableHead>
|
|
189
|
+
</TableRow>
|
|
190
|
+
</TableHeader>
|
|
191
|
+
<TableBody>
|
|
192
|
+
{postsWithDetails.map(({ post, languageCode, seoScore }) => (
|
|
193
|
+
<TableRow key={post.id} className="dark:border-slate-700">
|
|
194
|
+
<TableCell className="font-medium">
|
|
195
|
+
<Link
|
|
196
|
+
href={`/cms/posts/${post.id}/edit`}
|
|
197
|
+
className="flex items-center cursor-pointer"
|
|
198
|
+
>
|
|
199
|
+
<Edit3 className="mr-2 h-4 w-4" />
|
|
200
|
+
{post.title}
|
|
201
|
+
</Link>
|
|
202
|
+
</TableCell>
|
|
203
|
+
<TableCell>
|
|
204
|
+
<VisibilityBadge
|
|
205
|
+
type="post"
|
|
206
|
+
status={post.status}
|
|
207
|
+
publishedAt={post.published_at}
|
|
208
|
+
/>
|
|
209
|
+
</TableCell>
|
|
210
|
+
<TableCell><Badge variant="outline" className="dark:border-slate-600">{languageCode}</Badge></TableCell>
|
|
211
|
+
<TableCell className="text-muted-foreground text-xs hidden md:table-cell">/article/{post.slug}</TableCell>
|
|
212
|
+
<TableCell>
|
|
213
|
+
<SeoScoreBadge score={seoScore} />
|
|
214
|
+
</TableCell>
|
|
215
|
+
<TableCell className="hidden lg:table-cell text-xs text-muted-foreground">
|
|
216
|
+
{post.published_at ? new Date(post.published_at).toLocaleDateString() : "Not yet"}
|
|
217
|
+
</TableCell>
|
|
218
|
+
<TableCell className="text-right">
|
|
219
|
+
<DropdownMenu>
|
|
220
|
+
<DropdownMenuButtonTrigger id={`post-trigger-${post.id}`}>
|
|
221
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
222
|
+
<span className="sr-only">Post actions for {post.title}</span>
|
|
223
|
+
</DropdownMenuButtonTrigger>
|
|
224
|
+
<DropdownMenuContent align="end">
|
|
225
|
+
<DropdownMenuItem asChild>
|
|
226
|
+
<Link href={`/cms/posts/${post.id}/edit`} className="flex items-center cursor-pointer">
|
|
227
|
+
<Edit3 className="mr-2 h-4 w-4" /> Edit
|
|
228
|
+
</Link>
|
|
229
|
+
</DropdownMenuItem>
|
|
230
|
+
<DropdownMenuSeparator />
|
|
231
|
+
<DeletePostButtonClient postId={post.id} />
|
|
232
|
+
</DropdownMenuContent>
|
|
233
|
+
</DropdownMenu>
|
|
234
|
+
</TableCell>
|
|
235
|
+
</TableRow>
|
|
236
|
+
))}
|
|
237
|
+
</TableBody>
|
|
238
|
+
</Table>
|
|
239
|
+
|
|
240
|
+
<TablePagination
|
|
241
|
+
currentPage={pageNumber}
|
|
242
|
+
pageSize={pageSize}
|
|
243
|
+
totalCount={totalCount}
|
|
244
|
+
basePath="/cms/posts"
|
|
245
|
+
itemLabel="posts"
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { redirect } from 'next/navigation';
|
|
2
|
-
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
3
|
-
import { CategoryManagementPage as CategoryManagementPageUI } from '@nextblock-cms/ecommerce/server';
|
|
4
|
-
|
|
5
|
-
export default async function CategoryManagementPage() {
|
|
6
|
-
const isOnline = await verifyPackageOnline('ecommerce');
|
|
7
|
-
if (!isOnline) {
|
|
8
|
-
redirect('/cms/settings/packages');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
return <CategoryManagementPageUI />;
|
|
12
|
-
}
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
3
|
+
import { CategoryManagementPage as CategoryManagementPageUI } from '@nextblock-cms/ecommerce/server';
|
|
4
|
+
|
|
5
|
+
export default async function CategoryManagementPage() {
|
|
6
|
+
const isOnline = await verifyPackageOnline('ecommerce');
|
|
7
|
+
if (!isOnline) {
|
|
8
|
+
redirect('/cms/settings/packages');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return <CategoryManagementPageUI />;
|
|
12
|
+
}
|