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.
- package/package.json +1 -1
- package/templates/nextblock-template/app/ToasterProvider.tsx +26 -17
- package/templates/nextblock-template/app/actions/contactSellerActions.test.ts +280 -0
- package/templates/nextblock-template/app/actions/contactSellerActions.ts +222 -0
- package/templates/nextblock-template/app/actions/email-retry.test.ts +62 -0
- package/templates/nextblock-template/app/actions/email.ts +241 -110
- package/templates/nextblock-template/app/actions/formActions.ts +245 -116
- package/templates/nextblock-template/app/actions/interactions.ts +489 -396
- package/templates/nextblock-template/app/actions/threadActions.ts +166 -0
- package/templates/nextblock-template/app/api/checkout/route.ts +162 -146
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +664 -1
- package/templates/nextblock-template/app/checkout/page.tsx +57 -52
- package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +552 -529
- package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +304 -181
- package/templates/nextblock-template/app/cms/components/ContactReminderBanner.tsx +75 -0
- package/templates/nextblock-template/app/cms/components/PaymentsReminderBanner.tsx +58 -0
- package/templates/nextblock-template/app/cms/components/VisibilityControl.tsx +542 -528
- package/templates/nextblock-template/app/cms/inquiries/actions.ts +66 -0
- package/templates/nextblock-template/app/cms/inquiries/page.tsx +12 -0
- package/templates/nextblock-template/app/cms/interactions/page.tsx +12 -51
- package/templates/nextblock-template/app/cms/layout.tsx +101 -73
- package/templates/nextblock-template/app/cms/messages/MessagesClient.tsx +661 -0
- package/templates/nextblock-template/app/cms/messages/actions.ts +404 -0
- package/templates/nextblock-template/app/cms/messages/loadInbox.ts +333 -0
- package/templates/nextblock-template/app/cms/messages/page.tsx +87 -0
- package/templates/nextblock-template/app/cms/messages/require-admin.ts +37 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +370 -362
- package/templates/nextblock-template/app/cms/revisions/service.ts +20 -0
- package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +227 -185
- package/templates/nextblock-template/app/layout.tsx +671 -671
- package/templates/nextblock-template/app/product/[slug]/page.tsx +502 -482
- package/templates/nextblock-template/app/providers.tsx +96 -96
- package/templates/nextblock-template/app/thread/ThreadView.tsx +164 -0
- package/templates/nextblock-template/app/thread/[token]/route.ts +57 -0
- package/templates/nextblock-template/app/thread/layout.tsx +15 -0
- package/templates/nextblock-template/app/thread/page.tsx +98 -0
- package/templates/nextblock-template/components/BlockRenderer.tsx +312 -296
- package/templates/nextblock-template/components/ContactSellerSection.tsx +188 -0
- package/templates/nextblock-template/components/PostCommentsSection.tsx +378 -369
- package/templates/nextblock-template/components/ProductReviewsSection.tsx +426 -419
- package/templates/nextblock-template/components/StaffReplies.tsx +102 -0
- package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +18 -17
- package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +20 -19
- package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +25 -22
- package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +385 -381
- package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +157 -92
- package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +34 -31
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +612 -600
- package/templates/nextblock-template/components/commerce/PaymentReadinessBoundary.tsx +32 -0
- package/templates/nextblock-template/docs/14-MESSAGES-INBOX.md +309 -0
- package/templates/nextblock-template/docs/README.md +42 -41
- package/templates/nextblock-template/docs/assets/lighthouse-scores.png +0 -0
- package/templates/nextblock-template/lib/blocks/blockColors.test.ts +22 -2
- package/templates/nextblock-template/lib/blocks/blockColors.ts +44 -1
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +761 -753
- package/templates/nextblock-template/lib/cms/contact-reminder.ts +64 -0
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +98 -0
- package/templates/nextblock-template/lib/cms/unread-messages.ts +42 -0
- package/templates/nextblock-template/lib/commerce/seller-contact.ts +162 -0
- package/templates/nextblock-template/lib/config/email-settings.ts +323 -254
- package/templates/nextblock-template/lib/config/email-tls.test.ts +57 -0
- package/templates/nextblock-template/lib/email/placeholder-address.test.ts +59 -0
- package/templates/nextblock-template/lib/email/placeholder-address.ts +39 -0
- package/templates/nextblock-template/lib/messages/thread-reference.test.ts +70 -0
- package/templates/nextblock-template/lib/messages/thread-token.test.ts +93 -0
- package/templates/nextblock-template/lib/messages/thread-token.ts +157 -0
- package/templates/nextblock-template/lib/messages/threads.ts +579 -0
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +20 -0
- package/templates/nextblock-template/lib/site-url.test.ts +89 -0
- package/templates/nextblock-template/lib/site-url.ts +102 -48
- 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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
}
|