create-nextblock 0.15.8 → 0.15.9

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 (71) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/ToasterProvider.tsx +26 -17
  3. package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -0
  4. package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -0
  5. package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -0
  6. package/templates/nextblock-template/app/actions/email.ts +241 -110
  7. package/templates/nextblock-template/app/actions/formActions.ts +245 -116
  8. package/templates/nextblock-template/app/actions/interactions.ts +489 -396
  9. package/templates/nextblock-template/app/actions/threadActions.ts +166 -0
  10. package/templates/nextblock-template/app/api/checkout/route.ts +162 -146
  11. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +664 -1
  12. package/templates/nextblock-template/app/checkout/page.tsx +57 -52
  13. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +552 -529
  14. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -181
  15. package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -0
  16. package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -0
  17. package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -528
  18. package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -0
  19. package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -0
  20. package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -51
  21. package/templates/nextblock-template/app/cms/layout.tsx +101 -73
  22. package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -0
  23. package/templates/nextblock-template/app/cms/messages/actions.ts +404 -0
  24. package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -0
  25. package/templates/nextblock-template/app/cms/messages/page.tsx +87 -0
  26. package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -0
  27. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -362
  28. package/templates/nextblock-template/app/cms/revisions/service.ts +20 -0
  29. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -185
  30. package/templates/nextblock-template/app/layout.tsx +671 -671
  31. package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -482
  32. package/templates/nextblock-template/app/providers.tsx +96 -96
  33. package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -0
  34. package/templates/nextblock-template/app/thread/[token]/route.ts +57 -0
  35. package/templates/nextblock-template/app/thread/layout.tsx +15 -0
  36. package/templates/nextblock-template/app/thread/page.tsx +98 -0
  37. package/templates/nextblock-template/components/BlockRenderer.tsx +312 -296
  38. package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -0
  39. package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -369
  40. package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -419
  41. package/templates/nextblock-template/components/StaffReplies.tsx +102 -0
  42. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -17
  43. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -19
  44. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -22
  45. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -381
  46. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -92
  47. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -31
  48. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -600
  49. package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -0
  50. package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -0
  51. package/templates/nextblock-template/docs/README.md +42 -41
  52. package/templates/nextblock-template/docs/assets/lighthouse-scores.png +0 -0
  53. package/templates/nextblock-template/lib/blocks/blockColors.test.ts +22 -2
  54. package/templates/nextblock-template/lib/blocks/blockColors.ts +44 -1
  55. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -753
  56. package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -0
  57. package/templates/nextblock-template/lib/cms/payments-reminder.ts +98 -0
  58. package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -0
  59. package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -0
  60. package/templates/nextblock-template/lib/config/email-settings.ts +323 -254
  61. package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -0
  62. package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -0
  63. package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -0
  64. package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -0
  65. package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -0
  66. package/templates/nextblock-template/lib/messages/thread-token.ts +157 -0
  67. package/templates/nextblock-template/lib/messages/threads.ts +579 -0
  68. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +20 -0
  69. package/templates/nextblock-template/lib/site-url.test.ts +89 -0
  70. package/templates/nextblock-template/lib/site-url.ts +102 -48
  71. package/templates/nextblock-template/package.json +1 -1
@@ -0,0 +1,66 @@
1
+ 'use server';
2
+
3
+ import { revalidatePath } from 'next/cache';
4
+ import { createClient } from '@nextblock-cms/db/server';
5
+
6
+ import { STORE_CONTACT_SETTINGS_KEY } from '../../../lib/commerce/seller-contact';
7
+
8
+ async function assertAdmin() {
9
+ const supabase = createClient();
10
+ const {
11
+ data: { user },
12
+ } = await supabase.auth.getUser();
13
+ if (!user) throw new Error('Unauthorized');
14
+
15
+ const { data: profile } = await supabase
16
+ .from('profiles')
17
+ .select('role')
18
+ .eq('id', user.id)
19
+ .single();
20
+ if (!profile || profile.role !== 'ADMIN') throw new Error('Forbidden');
21
+ }
22
+
23
+ export interface InquiryActionState {
24
+ success: boolean;
25
+ message: string;
26
+ }
27
+
28
+ /**
29
+ * Set where storefront enquiries are emailed. Stored in the public settings row — it is
30
+ * a business address, not a credential — but never rendered to the storefront: the
31
+ * public form posts a product id and the server resolves the recipient.
32
+ */
33
+ export async function saveStoreContactEmail(
34
+ _prevState: unknown,
35
+ formData: FormData
36
+ ): Promise<InquiryActionState> {
37
+ try {
38
+ await assertAdmin();
39
+
40
+ const contactEmail = String(formData.get('contact_email') ?? '').trim();
41
+ if (contactEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contactEmail)) {
42
+ return { success: false, message: "That doesn't look like a valid email address." };
43
+ }
44
+
45
+ const supabase = createClient();
46
+ const { error } = await supabase
47
+ .from('site_settings')
48
+ .upsert({ key: STORE_CONTACT_SETTINGS_KEY, value: { contactEmail } });
49
+
50
+ if (error) {
51
+ console.error('Error saving store contact email:', error.message);
52
+ return { success: false, message: 'Could not save the contact address.' };
53
+ }
54
+
55
+ revalidatePath('/cms/inquiries');
56
+ return {
57
+ success: true,
58
+ message: contactEmail
59
+ ? 'Enquiries will now be sent to that address.'
60
+ : 'Cleared — enquiries will fall back to your invoice or support address.',
61
+ };
62
+ } catch (error) {
63
+ console.error('saveStoreContactEmail failed:', error);
64
+ return { success: false, message: 'You do not have permission to change this.' };
65
+ }
66
+ }
@@ -0,0 +1,12 @@
1
+ import { redirect } from 'next/navigation';
2
+
3
+ /**
4
+ * Product enquiries are a tab of the unified Messages inbox now.
5
+ *
6
+ * Kept as a redirect rather than deleted: this path was linked from the CMS nav and may
7
+ * still be bookmarked, and any stale client bundle will keep routing here until it
8
+ * reloads.
9
+ */
10
+ export default function InquiriesRedirect() {
11
+ redirect('/cms/messages?source=product_inquiry');
12
+ }
@@ -1,51 +1,12 @@
1
- import { redirect } from "next/navigation";
2
- import { createClient, getProfileWithRoleServerSide } from "@nextblock-cms/db/server";
3
- import InteractionsModerationClient from "./InteractionsModerationClient";
4
-
5
- export const dynamic = "force-dynamic";
6
-
7
- export default async function InteractionsPage() {
8
- const supabase = createClient();
9
-
10
- // 1. Authenticate user
11
- const { data: { user } } = await supabase.auth.getUser();
12
- if (!user) {
13
- redirect("/login?redirect_to=/cms/interactions");
14
- }
15
-
16
- // 2. Authorize user (requires ADMIN or WRITER roles to access CMS)
17
- const profile = await getProfileWithRoleServerSide(user.id);
18
- if (!profile || (profile.role !== "ADMIN" && profile.role !== "WRITER")) {
19
- redirect("/cms/dashboard");
20
- }
21
-
22
- // 3. Fetch initial interactions (reviews and comments)
23
- const { data: interactions, error } = await supabase
24
- .from("cms_interactions" as any)
25
- .select(`
26
- id,
27
- type,
28
- status,
29
- content,
30
- rating,
31
- reactions,
32
- created_at,
33
- product_id,
34
- post_id,
35
- profiles(full_name, avatar_url),
36
- products(title, slug),
37
- posts(title, slug)
38
- `)
39
- .order("created_at", { ascending: false });
40
-
41
- if (error) {
42
- console.error("Error fetching initial interactions:", error);
43
- }
44
-
45
- return (
46
- <InteractionsModerationClient
47
- initialInteractions={interactions || []}
48
- isAdmin={profile.role === "ADMIN"}
49
- />
50
- );
51
- }
1
+ import { redirect } from 'next/navigation';
2
+
3
+ /**
4
+ * Reviews and comments are tabs of the unified Messages inbox now.
5
+ *
6
+ * Kept as a redirect rather than deleted: the "new interaction" notification email
7
+ * builds a call-to-action link straight to this path, so old mail in an admin's inbox
8
+ * must keep working.
9
+ */
10
+ export default function InteractionsRedirect() {
11
+ redirect('/cms/messages?source=review');
12
+ }
@@ -1,73 +1,101 @@
1
- import 'katex/dist/katex.min.css';
2
- import { redirect } from 'next/navigation';
3
- import { after } from 'next/server';
4
- import CmsClientLayout from "./CmsClientLayout";
5
- import { verifyPackageOnline, createClient } from '@nextblock-cms/db/server';
6
- import { evaluateTwoFactor, getStaffTwoFactorReminder } from '../../lib/auth/twoFactor';
7
- import { maybeRefreshUpstreamStatus } from '../../lib/updates/check-upstream';
8
- import type { SystemAlertItem } from './components/SystemAlertsBanner';
9
-
10
- /**
11
- * Unresolved system alerts for the dashboard banner. Runs as the signed-in user, so the
12
- * system_alerts SELECT RLS policy returns rows only for ADMINs (WRITERs get an empty
13
- * list). Best-effort: any failure (e.g. the table not yet migrated) yields no banner.
14
- */
15
- async function getUnresolvedSystemAlerts(): Promise<SystemAlertItem[]> {
16
- try {
17
- const supabase = createClient();
18
- const { data, error } = await supabase
19
- .from('system_alerts')
20
- .select('id, alert_type, title, message, metadata')
21
- .eq('is_resolved', false)
22
- .in('alert_type', ['merge_conflict', 'runtime_update_available'])
23
- .order('created_at', { ascending: false })
24
- .limit(20);
25
- if (error || !data) return [];
26
- return data.map((a) => ({
27
- id: a.id,
28
- alert_type: a.alert_type,
29
- title: a.title,
30
- message: a.message,
31
- metadata: (a.metadata ?? null) as Record<string, unknown> | null,
32
- }));
33
- } catch {
34
- return [];
35
- }
36
- }
37
-
38
- export default async function CmsLayout({
39
- children,
40
- }: {
41
- children: React.ReactNode;
42
- }) {
43
- // Enforce any outstanding second factor before rendering the CMS. This guards
44
- // direct navigation to /cms/* with an aal1 (password-only) session.
45
- const twoFactor = await evaluateTwoFactor();
46
- if (twoFactor.status === 'totp_required' || twoFactor.status === 'email_required') {
47
- redirect('/two-factor?redirect_to=/cms/dashboard');
48
- }
49
-
50
- const [isEcommerceActive, isCortexAiActive, showTwoFactorReminder, systemAlerts] =
51
- await Promise.all([
52
- verifyPackageOnline('ecommerce'),
53
- verifyPackageOnline('cortex-ai'),
54
- getStaffTwoFactorReminder(),
55
- getUnresolvedSystemAlerts(),
56
- ]);
57
-
58
- // After the response, refresh upstream update/conflict status in the background
59
- // (throttled to ~6h, see maybeRefreshUpstreamStatus). This keeps the banner current
60
- // without a cron so it works on Vercel Hobby (limited crons) and self-hosted alike.
61
- after(() => maybeRefreshUpstreamStatus());
62
-
63
- return (
64
- <CmsClientLayout
65
- isCortexAiActive={isCortexAiActive}
66
- isEcommerceActive={isEcommerceActive}
67
- showTwoFactorReminder={showTwoFactorReminder}
68
- systemAlerts={systemAlerts}
69
- >
70
- {children}
71
- </CmsClientLayout>
72
- );
73
- }
1
+ import 'katex/dist/katex.min.css';
2
+ import { redirect } from 'next/navigation';
3
+ import { after } from 'next/server';
4
+ import CmsClientLayout from "./CmsClientLayout";
5
+ import { verifyPackageOnline, createClient } from '@nextblock-cms/db/server';
6
+ import { evaluateTwoFactor, getStaffTwoFactorReminder } from '../../lib/auth/twoFactor';
7
+ import { maybeRefreshUpstreamStatus } from '../../lib/updates/check-upstream';
8
+ import { getPaymentsReminder } from '../../lib/cms/payments-reminder';
9
+ import { getUnreadMessageCount } from '../../lib/cms/unread-messages';
10
+ import { getContactReminder } from '../../lib/cms/contact-reminder';
11
+ import type { SystemAlertItem } from './components/SystemAlertsBanner';
12
+
13
+ /**
14
+ * Unresolved system alerts for the dashboard banner. Runs as the signed-in user, so the
15
+ * system_alerts SELECT RLS policy returns rows only for ADMINs (WRITERs get an empty
16
+ * list). Best-effort: any failure (e.g. the table not yet migrated) yields no banner.
17
+ */
18
+ async function getUnresolvedSystemAlerts(): Promise<SystemAlertItem[]> {
19
+ try {
20
+ const supabase = createClient();
21
+ const { data, error } = await supabase
22
+ .from('system_alerts')
23
+ .select('id, alert_type, title, message, metadata')
24
+ .eq('is_resolved', false)
25
+ .in('alert_type', ['merge_conflict', 'runtime_update_available'])
26
+ .order('created_at', { ascending: false })
27
+ .limit(20);
28
+ if (error || !data) return [];
29
+ return data.map((a) => ({
30
+ id: a.id,
31
+ alert_type: a.alert_type,
32
+ title: a.title,
33
+ message: a.message,
34
+ metadata: (a.metadata ?? null) as Record<string, unknown> | null,
35
+ }));
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ export default async function CmsLayout({
42
+ children,
43
+ }: {
44
+ children: React.ReactNode;
45
+ }) {
46
+ // Enforce any outstanding second factor before rendering the CMS. This guards
47
+ // direct navigation to /cms/* with an aal1 (password-only) session.
48
+ const twoFactor = await evaluateTwoFactor();
49
+ if (twoFactor.status === 'totp_required' || twoFactor.status === 'email_required') {
50
+ redirect('/two-factor?redirect_to=/cms/dashboard');
51
+ }
52
+
53
+ const supabaseForRole = createClient();
54
+ const {
55
+ data: { user: cmsUser },
56
+ } = await supabaseForRole.auth.getUser();
57
+ const { data: cmsProfile } = cmsUser
58
+ ? await supabaseForRole.from('profiles').select('role').eq('id', cmsUser.id).maybeSingle()
59
+ : { data: null };
60
+ const isAdmin = cmsProfile?.role === 'ADMIN';
61
+
62
+ const [
63
+ isEcommerceActive,
64
+ isCortexAiActive,
65
+ showTwoFactorReminder,
66
+ systemAlerts,
67
+ paymentsReminder,
68
+ messagesUnread,
69
+ contactReminder,
70
+ ] =
71
+ await Promise.all([
72
+ verifyPackageOnline('ecommerce'),
73
+ verifyPackageOnline('cortex-ai'),
74
+ getStaffTwoFactorReminder(),
75
+ getUnresolvedSystemAlerts(),
76
+ // Re-checks ecommerce activation itself; verifyPackageOnline is unstable_cache'd
77
+ // for 60s, so the second call costs nothing.
78
+ getPaymentsReminder(),
79
+ getUnreadMessageCount(isAdmin),
80
+ isAdmin ? getContactReminder() : Promise.resolve(null),
81
+ ]);
82
+
83
+ // After the response, refresh upstream update/conflict status in the background
84
+ // (throttled to ~6h, see maybeRefreshUpstreamStatus). This keeps the banner current
85
+ // without a cron — so it works on Vercel Hobby (limited crons) and self-hosted alike.
86
+ after(() => maybeRefreshUpstreamStatus());
87
+
88
+ return (
89
+ <CmsClientLayout
90
+ isCortexAiActive={isCortexAiActive}
91
+ isEcommerceActive={isEcommerceActive}
92
+ showTwoFactorReminder={showTwoFactorReminder}
93
+ systemAlerts={systemAlerts}
94
+ paymentsReminder={paymentsReminder}
95
+ messagesUnread={messagesUnread}
96
+ contactReminder={contactReminder}
97
+ >
98
+ {children}
99
+ </CmsClientLayout>
100
+ );
101
+ }