create-nextblock 0.16.4 → 0.17.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.
Files changed (203) hide show
  1. package/CLAUDE.md +22 -0
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/package.json +1 -1
  8. package/templates/nextblock-template/.browserslistrc +11 -11
  9. package/templates/nextblock-template/.swcrc +30 -30
  10. package/templates/nextblock-template/CLAUDE.md +26 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
  14. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  15. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  16. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  17. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  18. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  19. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
  20. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  21. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  22. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  23. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +272 -205
  24. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +10473 -8467
  25. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  26. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  27. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  28. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  29. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  30. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
  31. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
  32. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
  33. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
  34. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
  36. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  37. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  38. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  39. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
  40. package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
  41. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  42. package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
  43. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  44. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  45. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  46. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
  47. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
  48. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  49. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  50. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  51. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  52. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  53. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  54. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  55. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
  56. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  57. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  58. package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
  59. package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
  60. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  61. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  62. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  63. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  64. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  65. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  66. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  67. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  68. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  69. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
  70. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  71. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  72. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  73. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  74. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  75. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  76. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  77. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  78. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
  79. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
  80. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  81. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  82. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  83. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
  84. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
  85. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
  86. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
  87. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
  88. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
  89. package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
  90. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
  91. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
  92. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  93. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  94. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
  95. package/templates/nextblock-template/components/Header.tsx +38 -38
  96. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  97. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  98. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  99. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
  100. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
  101. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
  102. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
  103. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
  104. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
  105. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
  106. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
  107. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
  108. package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
  109. package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
  110. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  111. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  112. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  113. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  114. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
  115. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  116. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  117. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  118. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  119. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  120. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  121. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +1 -1
  122. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +402 -282
  123. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +15 -8
  124. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  125. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +23 -4
  126. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  127. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +4 -1
  128. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +8 -0
  129. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +2 -2
  130. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +95 -104
  131. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  132. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  133. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  134. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  135. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  136. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  137. package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
  138. package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
  139. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  140. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  141. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  142. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  143. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  144. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  145. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  146. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  147. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  148. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  149. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  150. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  151. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  152. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  153. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  154. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  155. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  156. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  157. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
  158. package/templates/nextblock-template/lib/media/youtube.ts +99 -99
  159. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  160. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  161. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  162. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  163. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  164. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  165. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  166. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  167. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  168. package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
  169. package/templates/nextblock-template/lib/seo/redirect-store.ts +3 -2
  170. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  171. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  172. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  173. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +15 -175
  174. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  175. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  176. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  177. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  178. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  179. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  180. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  181. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  182. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
  183. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
  184. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
  185. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
  186. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
  187. package/templates/nextblock-template/lib/zod-config.ts +5 -5
  188. package/templates/nextblock-template/next-env.d.ts +1 -0
  189. package/templates/nextblock-template/package.json +1 -1
  190. package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
  191. package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
  192. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  193. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
  194. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  195. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  196. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  197. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  198. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  199. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  200. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  201. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  202. package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
  203. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
@@ -1,136 +1,136 @@
1
- "use client";
2
-
3
- import React from "react";
4
- import { useRouter, useSearchParams } from "next/navigation";
5
- import {
6
- Button,
7
- Select,
8
- SelectContent,
9
- SelectItem,
10
- SelectTrigger,
11
- SelectValue,
12
- } from "@nextblock-cms/ui";
13
- import { ChevronLeft, ChevronRight } from "lucide-react";
14
-
15
- interface TablePaginationProps {
16
- currentPage: number;
17
- pageSize: number;
18
- totalCount: number;
19
- basePath: string;
20
- itemLabel?: string;
21
- pageSizeOptions?: number[];
22
- }
23
-
24
- export default function TablePagination({
25
- currentPage,
26
- pageSize,
27
- totalCount,
28
- basePath,
29
- itemLabel = "items",
30
- pageSizeOptions = [10, 25, 50, 100],
31
- }: TablePaginationProps) {
32
- const router = useRouter();
33
- const searchParams = useSearchParams();
34
-
35
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
36
- const from = totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1;
37
- const to = Math.min(totalCount, currentPage * pageSize);
38
-
39
- const navigateTo = (newPage: number, newPageSize?: number) => {
40
- const current = new URLSearchParams(Array.from(searchParams.entries()));
41
-
42
- if (newPage > 1) {
43
- current.set("page", newPage.toString());
44
- } else {
45
- current.delete("page");
46
- }
47
-
48
- const effectivePageSize = newPageSize ?? pageSize;
49
- if (effectivePageSize !== 25) {
50
- current.set("pageSize", effectivePageSize.toString());
51
- } else {
52
- current.delete("pageSize");
53
- }
54
-
55
- const query = current.toString();
56
- router.push(`${basePath}${query ? `?${query}` : ""}`);
57
- };
58
-
59
- const handlePageSizeChange = (val: string) => {
60
- const nextSize = parseInt(val, 10);
61
- if (!Number.isNaN(nextSize) && nextSize > 0) {
62
- // Reset to page 1 when changing page size
63
- navigateTo(1, nextSize);
64
- }
65
- };
66
-
67
- return (
68
- <div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-4 py-3 bg-muted/20 border-t border-slate-200 dark:border-slate-700 text-xs sm:text-sm text-muted-foreground">
69
- {/* Item summary */}
70
- <div className="flex items-center gap-1">
71
- <span>Showing</span>
72
- <span className="font-medium text-foreground">{from}</span>
73
- <span>to</span>
74
- <span className="font-medium text-foreground">{to}</span>
75
- <span>of</span>
76
- <span className="font-medium text-foreground">{totalCount}</span>
77
- <span>{itemLabel}</span>
78
- </div>
79
-
80
- {/* Controls: Page size + Pagination buttons */}
81
- <div className="flex items-center gap-4 sm:gap-6 flex-wrap">
82
- {/* Rows per page selector */}
83
- <div className="flex items-center gap-2">
84
- <span className="text-xs">Rows per page:</span>
85
- <Select
86
- value={pageSize.toString()}
87
- onValueChange={handlePageSizeChange}
88
- >
89
- <SelectTrigger className="h-8 w-[72px] text-xs">
90
- <SelectValue />
91
- </SelectTrigger>
92
- <SelectContent>
93
- {pageSizeOptions.map((opt) => (
94
- <SelectItem key={opt} value={opt.toString()} className="text-xs">
95
- {opt}
96
- </SelectItem>
97
- ))}
98
- </SelectContent>
99
- </Select>
100
- </div>
101
-
102
- {/* Page counter */}
103
- <div className="flex items-center gap-1 text-xs">
104
- <span>Page</span>
105
- <span className="font-medium text-foreground">{currentPage}</span>
106
- <span>of</span>
107
- <span className="font-medium text-foreground">{totalPages}</span>
108
- </div>
109
-
110
- {/* Prev / Next buttons */}
111
- <div className="flex items-center gap-1">
112
- <Button
113
- variant="outline"
114
- size="sm"
115
- className="h-8 w-8 p-0"
116
- disabled={currentPage <= 1}
117
- onClick={() => navigateTo(currentPage - 1)}
118
- aria-label="Previous page"
119
- >
120
- <ChevronLeft className="h-4 w-4" />
121
- </Button>
122
- <Button
123
- variant="outline"
124
- size="sm"
125
- className="h-8 w-8 p-0"
126
- disabled={currentPage >= totalPages}
127
- onClick={() => navigateTo(currentPage + 1)}
128
- aria-label="Next page"
129
- >
130
- <ChevronRight className="h-4 w-4" />
131
- </Button>
132
- </div>
133
- </div>
134
- </div>
135
- );
136
- }
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
+ import {
6
+ Button,
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "@nextblock-cms/ui";
13
+ import { ChevronLeft, ChevronRight } from "lucide-react";
14
+
15
+ interface TablePaginationProps {
16
+ currentPage: number;
17
+ pageSize: number;
18
+ totalCount: number;
19
+ basePath: string;
20
+ itemLabel?: string;
21
+ pageSizeOptions?: number[];
22
+ }
23
+
24
+ export default function TablePagination({
25
+ currentPage,
26
+ pageSize,
27
+ totalCount,
28
+ basePath,
29
+ itemLabel = "items",
30
+ pageSizeOptions = [10, 25, 50, 100],
31
+ }: TablePaginationProps) {
32
+ const router = useRouter();
33
+ const searchParams = useSearchParams();
34
+
35
+ const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
36
+ const from = totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1;
37
+ const to = Math.min(totalCount, currentPage * pageSize);
38
+
39
+ const navigateTo = (newPage: number, newPageSize?: number) => {
40
+ const current = new URLSearchParams(Array.from(searchParams.entries()));
41
+
42
+ if (newPage > 1) {
43
+ current.set("page", newPage.toString());
44
+ } else {
45
+ current.delete("page");
46
+ }
47
+
48
+ const effectivePageSize = newPageSize ?? pageSize;
49
+ if (effectivePageSize !== 25) {
50
+ current.set("pageSize", effectivePageSize.toString());
51
+ } else {
52
+ current.delete("pageSize");
53
+ }
54
+
55
+ const query = current.toString();
56
+ router.push(`${basePath}${query ? `?${query}` : ""}`);
57
+ };
58
+
59
+ const handlePageSizeChange = (val: string) => {
60
+ const nextSize = parseInt(val, 10);
61
+ if (!Number.isNaN(nextSize) && nextSize > 0) {
62
+ // Reset to page 1 when changing page size
63
+ navigateTo(1, nextSize);
64
+ }
65
+ };
66
+
67
+ return (
68
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-4 py-3 bg-muted/20 border-t border-slate-200 dark:border-slate-700 text-xs sm:text-sm text-muted-foreground">
69
+ {/* Item summary */}
70
+ <div className="flex items-center gap-1">
71
+ <span>Showing</span>
72
+ <span className="font-medium text-foreground">{from}</span>
73
+ <span>to</span>
74
+ <span className="font-medium text-foreground">{to}</span>
75
+ <span>of</span>
76
+ <span className="font-medium text-foreground">{totalCount}</span>
77
+ <span>{itemLabel}</span>
78
+ </div>
79
+
80
+ {/* Controls: Page size + Pagination buttons */}
81
+ <div className="flex items-center gap-4 sm:gap-6 flex-wrap">
82
+ {/* Rows per page selector */}
83
+ <div className="flex items-center gap-2">
84
+ <span className="text-xs">Rows per page:</span>
85
+ <Select
86
+ value={pageSize.toString()}
87
+ onValueChange={handlePageSizeChange}
88
+ >
89
+ <SelectTrigger className="h-8 w-[72px] text-xs">
90
+ <SelectValue />
91
+ </SelectTrigger>
92
+ <SelectContent>
93
+ {pageSizeOptions.map((opt) => (
94
+ <SelectItem key={opt} value={opt.toString()} className="text-xs">
95
+ {opt}
96
+ </SelectItem>
97
+ ))}
98
+ </SelectContent>
99
+ </Select>
100
+ </div>
101
+
102
+ {/* Page counter */}
103
+ <div className="flex items-center gap-1 text-xs">
104
+ <span>Page</span>
105
+ <span className="font-medium text-foreground">{currentPage}</span>
106
+ <span>of</span>
107
+ <span className="font-medium text-foreground">{totalPages}</span>
108
+ </div>
109
+
110
+ {/* Prev / Next buttons */}
111
+ <div className="flex items-center gap-1">
112
+ <Button
113
+ variant="outline"
114
+ size="sm"
115
+ className="h-8 w-8 p-0"
116
+ disabled={currentPage <= 1}
117
+ onClick={() => navigateTo(currentPage - 1)}
118
+ aria-label="Previous page"
119
+ >
120
+ <ChevronLeft className="h-4 w-4" />
121
+ </Button>
122
+ <Button
123
+ variant="outline"
124
+ size="sm"
125
+ className="h-8 w-8 p-0"
126
+ disabled={currentPage >= totalPages}
127
+ onClick={() => navigateTo(currentPage + 1)}
128
+ aria-label="Next page"
129
+ >
130
+ <ChevronRight className="h-4 w-4" />
131
+ </Button>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
@@ -1,45 +1,45 @@
1
- 'use client';
2
-
3
- import Link from 'next/link';
4
- import { useState } from 'react';
5
- import { Button } from '@nextblock-cms/ui';
6
- import { ShieldAlert, X } from 'lucide-react';
7
-
8
- /**
9
- * In-app reminder for staff (ADMIN/WRITER) who have not enrolled a second factor,
10
- * shown when the "Encourage staff to enable 2FA" policy is on. Dismissible for the
11
- * session (state lives in the persistent CMS layout, so it survives client-side
12
- * navigation but reappears on the next full load until 2FA is set up).
13
- */
14
- export default function TwoFactorReminderBanner() {
15
- const [dismissed, setDismissed] = useState(false);
16
- if (dismissed) return null;
17
-
18
- return (
19
- <div className="mb-6 flex items-start gap-3 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-amber-900 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-200">
20
- <ShieldAlert className="mt-0.5 h-5 w-5 shrink-0" />
21
- <div className="min-w-0 flex-1">
22
- <p className="text-sm font-medium">
23
- Your account doesn&rsquo;t have two-factor authentication enabled.
24
- </p>
25
- <p className="text-xs opacity-90">
26
- Protect your CMS account by adding a second factor (authenticator app or email code).
27
- </p>
28
- </div>
29
- <div className="flex shrink-0 items-center gap-2">
30
- <Button asChild size="sm" variant="outline" className="border-amber-400">
31
- <Link href="/cms/settings/security">Set up 2FA</Link>
32
- </Button>
33
- <Button
34
- variant="ghost"
35
- size="icon"
36
- onClick={() => setDismissed(true)}
37
- aria-label="Dismiss for now"
38
- className="h-8 w-8"
39
- >
40
- <X className="h-4 w-4" />
41
- </Button>
42
- </div>
43
- </div>
44
- );
45
- }
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useState } from 'react';
5
+ import { Button } from '@nextblock-cms/ui';
6
+ import { ShieldAlert, X } from 'lucide-react';
7
+
8
+ /**
9
+ * In-app reminder for staff (ADMIN/WRITER) who have not enrolled a second factor,
10
+ * shown when the "Encourage staff to enable 2FA" policy is on. Dismissible for the
11
+ * session (state lives in the persistent CMS layout, so it survives client-side
12
+ * navigation but reappears on the next full load until 2FA is set up).
13
+ */
14
+ export default function TwoFactorReminderBanner() {
15
+ const [dismissed, setDismissed] = useState(false);
16
+ if (dismissed) return null;
17
+
18
+ return (
19
+ <div className="mb-6 flex items-start gap-3 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-amber-900 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-200">
20
+ <ShieldAlert className="mt-0.5 h-5 w-5 shrink-0" />
21
+ <div className="min-w-0 flex-1">
22
+ <p className="text-sm font-medium">
23
+ Your account doesn&rsquo;t have two-factor authentication enabled.
24
+ </p>
25
+ <p className="text-xs opacity-90">
26
+ Protect your CMS account by adding a second factor (authenticator app or email code).
27
+ </p>
28
+ </div>
29
+ <div className="flex shrink-0 items-center gap-2">
30
+ <Button asChild size="sm" variant="outline" className="border-amber-400">
31
+ <Link href="/cms/settings/security">Set up 2FA</Link>
32
+ </Button>
33
+ <Button
34
+ variant="ghost"
35
+ size="icon"
36
+ onClick={() => setDismissed(true)}
37
+ aria-label="Dismiss for now"
38
+ className="h-8 w-8"
39
+ >
40
+ <X className="h-4 w-4" />
41
+ </Button>
42
+ </div>
43
+ </div>
44
+ );
45
+ }
@@ -1,102 +1,102 @@
1
- 'use server';
2
-
3
- import { cookies } from 'next/headers';
4
- import { revalidatePath } from 'next/cache';
5
- import { createClient } from '@nextblock-cms/db/server';
6
- import {
7
- startDeviceFlow,
8
- pollDeviceFlowOnce,
9
- installSyncWorkflow,
10
- } from '../../../lib/updates/github-device';
11
- import { markSyncWorkflowInstalled } from '../../../lib/updates/check-upstream';
12
-
13
- const DEVICE_COOKIE = 'nb_gh_device';
14
-
15
- /** ADMIN gate, mirroring the rest of the CMS server entry points. */
16
- async function isAdmin(): Promise<boolean> {
17
- const supabase = createClient();
18
- const {
19
- data: { user },
20
- } = await supabase.auth.getUser();
21
- if (!user) return false;
22
- const { data: profile, error } = await supabase
23
- .from('profiles')
24
- .select('role')
25
- .eq('id', user.id)
26
- .single();
27
- return !error && profile?.role === 'ADMIN';
28
- }
29
-
30
- export interface StartConnectResult {
31
- ok: boolean;
32
- userCode?: string;
33
- verificationUri?: string;
34
- interval?: number;
35
- expiresIn?: number;
36
- error?: string;
37
- }
38
-
39
- /** Begin the device flow; stash the device code in an httpOnly cookie for polling. */
40
- export async function startGithubConnect(): Promise<StartConnectResult> {
41
- if (!(await isAdmin())) return { ok: false, error: 'Administrator role required.' };
42
-
43
- try {
44
- const flow = await startDeviceFlow();
45
- const store = await cookies();
46
- store.set(DEVICE_COOKIE, flow.deviceCode, {
47
- httpOnly: true,
48
- secure: process.env.NODE_ENV === 'production',
49
- sameSite: 'lax',
50
- path: '/cms',
51
- maxAge: flow.expiresIn,
52
- });
53
- return {
54
- ok: true,
55
- userCode: flow.userCode,
56
- verificationUri: flow.verificationUri,
57
- interval: flow.interval,
58
- expiresIn: flow.expiresIn,
59
- };
60
- } catch (caught) {
61
- return { ok: false, error: caught instanceof Error ? caught.message : 'Could not start GitHub connect.' };
62
- }
63
- }
64
-
65
- export type PollConnectResult =
66
- | { status: 'installed'; htmlUrl?: string }
67
- | { status: 'pending'; slowDown?: boolean }
68
- | { status: 'error'; error: string };
69
-
70
- /** Poll once; on authorization, install the workflow and clear the device cookie. */
71
- export async function pollGithubConnect(): Promise<PollConnectResult> {
72
- if (!(await isAdmin())) return { status: 'error', error: 'Administrator role required.' };
73
-
74
- const store = await cookies();
75
- const deviceCode = store.get(DEVICE_COOKIE)?.value;
76
- if (!deviceCode) {
77
- return { status: 'error', error: 'Connect session expired — start again.' };
78
- }
79
-
80
- const poll = await pollDeviceFlowOnce(deviceCode);
81
-
82
- if (poll.status === 'pending') return { status: 'pending', slowDown: poll.slowDown };
83
-
84
- if (poll.status === 'authorized') {
85
- const install = await installSyncWorkflow(poll.token);
86
- store.delete({ name: DEVICE_COOKIE, path: '/cms' });
87
- if (install.ok) {
88
- // We just installed the workflow — flip the onboarding state now instead of waiting
89
- // for the throttled background poll / GitHub's registration lag.
90
- await markSyncWorkflowInstalled();
91
- revalidatePath('/cms', 'layout');
92
- return { status: 'installed', htmlUrl: install.htmlUrl };
93
- }
94
- return { status: 'error', error: install.error ?? 'Could not install the workflow.' };
95
- }
96
-
97
- // expired / denied / error — clear the session.
98
- store.delete({ name: DEVICE_COOKIE, path: '/cms' });
99
- if (poll.status === 'expired') return { status: 'error', error: 'Authorization timed out — start again.' };
100
- if (poll.status === 'denied') return { status: 'error', error: 'Authorization was declined.' };
101
- return { status: 'error', error: poll.error };
102
- }
1
+ 'use server';
2
+
3
+ import { cookies } from 'next/headers';
4
+ import { revalidatePath } from 'next/cache';
5
+ import { createClient } from '@nextblock-cms/db/server';
6
+ import {
7
+ startDeviceFlow,
8
+ pollDeviceFlowOnce,
9
+ installSyncWorkflow,
10
+ } from '../../../lib/updates/github-device';
11
+ import { markSyncWorkflowInstalled } from '../../../lib/updates/check-upstream';
12
+
13
+ const DEVICE_COOKIE = 'nb_gh_device';
14
+
15
+ /** ADMIN gate, mirroring the rest of the CMS server entry points. */
16
+ async function isAdmin(): Promise<boolean> {
17
+ const supabase = createClient();
18
+ const {
19
+ data: { user },
20
+ } = await supabase.auth.getUser();
21
+ if (!user) return false;
22
+ const { data: profile, error } = await supabase
23
+ .from('profiles')
24
+ .select('role')
25
+ .eq('id', user.id)
26
+ .single();
27
+ return !error && profile?.role === 'ADMIN';
28
+ }
29
+
30
+ export interface StartConnectResult {
31
+ ok: boolean;
32
+ userCode?: string;
33
+ verificationUri?: string;
34
+ interval?: number;
35
+ expiresIn?: number;
36
+ error?: string;
37
+ }
38
+
39
+ /** Begin the device flow; stash the device code in an httpOnly cookie for polling. */
40
+ export async function startGithubConnect(): Promise<StartConnectResult> {
41
+ if (!(await isAdmin())) return { ok: false, error: 'Administrator role required.' };
42
+
43
+ try {
44
+ const flow = await startDeviceFlow();
45
+ const store = await cookies();
46
+ store.set(DEVICE_COOKIE, flow.deviceCode, {
47
+ httpOnly: true,
48
+ secure: process.env.NODE_ENV === 'production',
49
+ sameSite: 'lax',
50
+ path: '/cms',
51
+ maxAge: flow.expiresIn,
52
+ });
53
+ return {
54
+ ok: true,
55
+ userCode: flow.userCode,
56
+ verificationUri: flow.verificationUri,
57
+ interval: flow.interval,
58
+ expiresIn: flow.expiresIn,
59
+ };
60
+ } catch (caught) {
61
+ return { ok: false, error: caught instanceof Error ? caught.message : 'Could not start GitHub connect.' };
62
+ }
63
+ }
64
+
65
+ export type PollConnectResult =
66
+ | { status: 'installed'; htmlUrl?: string }
67
+ | { status: 'pending'; slowDown?: boolean }
68
+ | { status: 'error'; error: string };
69
+
70
+ /** Poll once; on authorization, install the workflow and clear the device cookie. */
71
+ export async function pollGithubConnect(): Promise<PollConnectResult> {
72
+ if (!(await isAdmin())) return { status: 'error', error: 'Administrator role required.' };
73
+
74
+ const store = await cookies();
75
+ const deviceCode = store.get(DEVICE_COOKIE)?.value;
76
+ if (!deviceCode) {
77
+ return { status: 'error', error: 'Connect session expired — start again.' };
78
+ }
79
+
80
+ const poll = await pollDeviceFlowOnce(deviceCode);
81
+
82
+ if (poll.status === 'pending') return { status: 'pending', slowDown: poll.slowDown };
83
+
84
+ if (poll.status === 'authorized') {
85
+ const install = await installSyncWorkflow(poll.token);
86
+ store.delete({ name: DEVICE_COOKIE, path: '/cms' });
87
+ if (install.ok) {
88
+ // We just installed the workflow — flip the onboarding state now instead of waiting
89
+ // for the throttled background poll / GitHub's registration lag.
90
+ await markSyncWorkflowInstalled();
91
+ revalidatePath('/cms', 'layout');
92
+ return { status: 'installed', htmlUrl: install.htmlUrl };
93
+ }
94
+ return { status: 'error', error: install.error ?? 'Could not install the workflow.' };
95
+ }
96
+
97
+ // expired / denied / error — clear the session.
98
+ store.delete({ name: DEVICE_COOKIE, path: '/cms' });
99
+ if (poll.status === 'expired') return { status: 'error', error: 'Authorization timed out — start again.' };
100
+ if (poll.status === 'denied') return { status: 'error', error: 'Authorization was declined.' };
101
+ return { status: 'error', error: poll.error };
102
+ }
@@ -1,31 +1,31 @@
1
- 'use server';
2
-
3
- import { revalidatePath } from 'next/cache';
4
- import { createClient } from '@nextblock-cms/db/server';
5
-
6
- /**
7
- * Mark a system alert resolved (dismiss it from the dashboard banner). Runs as the
8
- * signed-in user; the system_alerts UPDATE RLS policy restricts this to ADMINs, so a
9
- * non-admin caller simply updates zero rows. Inserts are service-role only.
10
- */
11
- export async function resolveSystemAlert(
12
- id: string,
13
- ): Promise<{ ok: boolean; error?: string }> {
14
- if (!id) return { ok: false, error: 'Missing alert id.' };
15
-
16
- const supabase = createClient();
17
- const {
18
- data: { user },
19
- } = await supabase.auth.getUser();
20
- if (!user) return { ok: false, error: 'Not authenticated.' };
21
-
22
- const { error } = await supabase
23
- .from('system_alerts')
24
- .update({ is_resolved: true, resolved_at: new Date().toISOString() })
25
- .eq('id', id);
26
-
27
- if (error) return { ok: false, error: error.message };
28
-
29
- revalidatePath('/cms', 'layout');
30
- return { ok: true };
31
- }
1
+ 'use server';
2
+
3
+ import { revalidatePath } from 'next/cache';
4
+ import { createClient } from '@nextblock-cms/db/server';
5
+
6
+ /**
7
+ * Mark a system alert resolved (dismiss it from the dashboard banner). Runs as the
8
+ * signed-in user; the system_alerts UPDATE RLS policy restricts this to ADMINs, so a
9
+ * non-admin caller simply updates zero rows. Inserts are service-role only.
10
+ */
11
+ export async function resolveSystemAlert(
12
+ id: string,
13
+ ): Promise<{ ok: boolean; error?: string }> {
14
+ if (!id) return { ok: false, error: 'Missing alert id.' };
15
+
16
+ const supabase = createClient();
17
+ const {
18
+ data: { user },
19
+ } = await supabase.auth.getUser();
20
+ if (!user) return { ok: false, error: 'Not authenticated.' };
21
+
22
+ const { error } = await supabase
23
+ .from('system_alerts')
24
+ .update({ is_resolved: true, resolved_at: new Date().toISOString() })
25
+ .eq('id', id);
26
+
27
+ if (error) return { ok: false, error: error.message };
28
+
29
+ revalidatePath('/cms', 'layout');
30
+ return { ok: true };
31
+ }