create-nextblock 0.10.9 → 0.11.2

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 (64) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/actions/interactions.test.ts +301 -0
  3. package/templates/nextblock-template/app/actions/interactions.ts +372 -0
  4. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +4 -4
  5. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +2 -2
  6. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +56 -57
  7. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +1 -1
  8. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +837 -0
  9. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +6 -0
  10. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +4 -0
  11. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -0
  12. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -0
  13. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +18 -13
  14. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -0
  15. package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -0
  16. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +4 -3
  17. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +1 -1
  18. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +3 -5
  19. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +1 -1
  20. package/templates/nextblock-template/app/page.tsx +2 -2
  21. package/templates/nextblock-template/app/product/[slug]/page.tsx +2 -0
  22. package/templates/nextblock-template/components/AppShell.tsx +1 -1
  23. package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -0
  24. package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -0
  25. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +2 -0
  26. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +62 -19
  27. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -19
  28. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +4 -4
  29. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +9 -8
  30. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +38 -9
  31. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +2 -0
  32. package/templates/nextblock-template/lib/onboarding/status.ts +13 -6
  33. package/templates/nextblock-template/lib/setup/actions.ts +3 -1
  34. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +30 -0
  35. package/templates/nextblock-template/lib/updates/check-upstream.ts +44 -7
  36. package/templates/nextblock-template/lib/updates/github-device.ts +206 -0
  37. package/templates/nextblock-template/lib/updates/repo-identity.ts +11 -1
  38. package/templates/nextblock-template/package.json +2 -1
  39. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +2 -4
  40. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +1 -1
  41. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +1 -1
  42. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +1 -1
  43. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  44. package/templates/nextblock-template/lib/ai-block-generation.ts +0 -339
  45. package/templates/nextblock-template/lib/ai-client.ts +0 -247
  46. package/templates/nextblock-template/lib/ai-config.ts +0 -98
  47. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +0 -125
  48. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +0 -363
  49. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +0 -405
  50. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +0 -1228
  51. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +0 -5
  52. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +0 -223
  53. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +0 -2183
  54. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +0 -4807
  55. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +0 -70
  56. package/templates/nextblock-template/lib/ai-key-crypto.ts +0 -132
  57. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +0 -49
  58. package/templates/nextblock-template/lib/ai-model-catalog.ts +0 -41
  59. package/templates/nextblock-template/lib/ai-model-registry.test.ts +0 -231
  60. package/templates/nextblock-template/lib/ai-model-registry.ts +0 -522
  61. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +0 -199
  62. package/templates/nextblock-template/lib/cortex-widget-registry.ts +0 -88
  63. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +0 -237
  64. package/templates/nextblock-template/lib/cortex-widget-schema.ts +0 -393
@@ -0,0 +1,369 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect, useTransition, useOptimistic } from "react";
4
+ import { createClient } from "@nextblock-cms/db";
5
+ import { Button } from "@nextblock-cms/ui";
6
+ import { Textarea } from "@nextblock-cms/ui";
7
+ import { Avatar, AvatarFallback, AvatarImage } from "@nextblock-cms/ui";
8
+ import { submitInteraction, toggleReaction } from "../app/actions/interactions";
9
+ import { cn, useTranslations } from "@nextblock-cms/utils";
10
+ import { MessageSquare, ThumbsUp, Loader2, PenTool } from "lucide-react";
11
+
12
+ interface PostCommentsSectionProps {
13
+ postId: number;
14
+ }
15
+
16
+ export default function PostCommentsSection({ postId }: PostCommentsSectionProps) {
17
+ const { t, lang } = useTranslations();
18
+ const [comments, setComments] = useState<any[]>([]);
19
+ const [loading, setLoading] = useState(true);
20
+ const [page, setPage] = useState(0);
21
+ const [hasMore, setHasMore] = useState(true);
22
+ const [user, setUser] = useState<any>(null);
23
+
24
+ // Form states
25
+ const [isFormOpen, setIsFormOpen] = useState(false);
26
+ const [content, setContent] = useState("");
27
+ const [submitting, setSubmitting] = useState(false);
28
+ const [error, setError] = useState<string | null>(null);
29
+ const [success, setSuccess] = useState<string | null>(null);
30
+
31
+ // Liked interactions tracking
32
+ const [likedIds, setLikedIds] = useState<string[]>([]);
33
+ const [, startTransition] = useTransition();
34
+
35
+ // Optimistic comments list
36
+ const [optimisticComments, setOptimisticComments] = useOptimistic(
37
+ comments,
38
+ (state, { commentId, hasReacted, count }: { commentId: string; hasReacted: boolean; count: number }) =>
39
+ state.map((c) => {
40
+ if (c.id === commentId) {
41
+ const reactions = { ...((c.reactions as Record<string, number>) || {}) };
42
+ reactions.likes = count;
43
+ return { ...c, reactions, tempHasReacted: hasReacted };
44
+ }
45
+ return c;
46
+ })
47
+ );
48
+
49
+ useEffect(() => {
50
+ // 1. Fetch user
51
+ const supabase = createClient();
52
+ supabase.auth.getUser().then(({ data: { user } }) => {
53
+ setUser(user);
54
+ });
55
+
56
+ // 2. Read liked interactions from cookies
57
+ const match = document.cookie.match(/reacted_interactions=([^;]+)/);
58
+ if (match) {
59
+ try {
60
+ setLikedIds(JSON.parse(decodeURIComponent(match[1])));
61
+ } catch (err) {
62
+ console.warn("Failed to parse liked interactions cookie:", err);
63
+ }
64
+ }
65
+
66
+ // 3. Load initial comments
67
+ fetchComments(0, true);
68
+ }, [postId]);
69
+
70
+ const fetchComments = async (pageNum: number, isInitial = false) => {
71
+ setLoading(true);
72
+ const supabase = createClient();
73
+ const limit = 5;
74
+ const start = pageNum * limit;
75
+ const end = start + limit - 1;
76
+
77
+ try {
78
+ const { data, error: dbError } = await supabase
79
+ .from("cms_interactions" as any)
80
+ .select("*, profiles(full_name, avatar_url, github_username)")
81
+ .eq("post_id", postId)
82
+ .eq("type", "comment")
83
+ .eq("status", "approved")
84
+ .order("created_at", { ascending: false })
85
+ .range(start, end);
86
+
87
+ if (dbError) throw dbError;
88
+
89
+ if (data) {
90
+ if (isInitial) {
91
+ setComments(data);
92
+ setPage(0);
93
+ } else {
94
+ setComments((prev) => [...prev, ...data]);
95
+ setPage(pageNum);
96
+ }
97
+ setHasMore(data.length === limit);
98
+ }
99
+ } catch (err) {
100
+ console.error("Error fetching comments:", err);
101
+ } finally {
102
+ setLoading(false);
103
+ }
104
+ };
105
+
106
+ const handleLoadMore = () => {
107
+ fetchComments(page + 1);
108
+ };
109
+
110
+ const handleLike = async (commentId: string) => {
111
+ const isLiked = likedIds.includes(commentId);
112
+ const comment = comments.find((c) => c.id === commentId);
113
+ if (!comment) return;
114
+
115
+ const currentLikes = (comment.reactions as Record<string, number>)?.likes || 0;
116
+ const nextCount = isLiked ? Math.max(0, currentLikes - 1) : currentLikes + 1;
117
+
118
+ startTransition(async () => {
119
+ // Apply optimistic update
120
+ setOptimisticComments({ commentId, hasReacted: !isLiked, count: nextCount });
121
+
122
+ const res = await toggleReaction(commentId);
123
+ if (res.success) {
124
+ // Update liked cookie state locally
125
+ if (isLiked) {
126
+ setLikedIds((prev) => prev.filter((id) => id !== commentId));
127
+ } else {
128
+ setLikedIds((prev) => [...prev, commentId]);
129
+ }
130
+ // Sync actual state
131
+ setComments((prev) =>
132
+ prev.map((c) => {
133
+ if (c.id === commentId) {
134
+ const reactions = { ...((c.reactions as Record<string, number>) || {}) };
135
+ reactions.likes = res.count ?? nextCount;
136
+ return { ...c, reactions };
137
+ }
138
+ return c;
139
+ })
140
+ );
141
+ }
142
+ });
143
+ };
144
+
145
+ const handleSubmitComment = async (e: React.FormEvent) => {
146
+ e.preventDefault();
147
+ if (!user) {
148
+ setError(t("comments.login_to_write"));
149
+ return;
150
+ }
151
+
152
+ if (content.trim().length < 5) {
153
+ setError(lang === "fr" ? "Votre commentaire doit faire au moins 5 caractères." : "Your comment must be at least 5 characters long.");
154
+ return;
155
+ }
156
+
157
+ setSubmitting(true);
158
+ setError(null);
159
+ setSuccess(null);
160
+
161
+ const res = await submitInteraction({
162
+ type: "comment",
163
+ content: content.trim(),
164
+ postId,
165
+ });
166
+
167
+ setSubmitting(false);
168
+
169
+ if (res.error) {
170
+ setError(res.error);
171
+ } else {
172
+ setSuccess(t("comments.success_pending"));
173
+ setContent("");
174
+ // Close form after a brief delay
175
+ setTimeout(() => {
176
+ setIsFormOpen(false);
177
+ setSuccess(null);
178
+ }, 3000);
179
+ }
180
+ };
181
+
182
+ const getInitials = (profile: any) => {
183
+ if (profile?.full_name) {
184
+ return profile.full_name
185
+ .split(" ")
186
+ .map((n: string) => n[0])
187
+ .join("")
188
+ .toUpperCase()
189
+ .slice(0, 2);
190
+ }
191
+ return "U";
192
+ };
193
+
194
+ return (
195
+ <div className="space-y-8">
196
+ {/* Header */}
197
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between border-b pb-6 gap-4">
198
+ <div>
199
+ <h2 className="text-2xl font-bold tracking-tight text-foreground flex items-center gap-2">
200
+ <MessageSquare className="h-6 w-6 text-primary" />
201
+ {t("comments.discussion")}
202
+ </h2>
203
+ <p className="text-sm text-muted-foreground mt-1">
204
+ {t("comments.join_conversation")}
205
+ </p>
206
+ </div>
207
+
208
+ {user ? (
209
+ <Button
210
+ onClick={() => setIsFormOpen(!isFormOpen)}
211
+ className="flex items-center gap-2 transition-all"
212
+ variant={isFormOpen ? "outline" : "default"}
213
+ >
214
+ <PenTool className="h-4 w-4" />
215
+ {isFormOpen ? t("comments.cancel_comment") : t("comments.write_comment")}
216
+ </Button>
217
+ ) : (
218
+ <p className="text-sm text-muted-foreground italic">
219
+ {t("comments.login_to_write")}
220
+ </p>
221
+ )}
222
+ </div>
223
+
224
+ {/* Submission Form */}
225
+ {isFormOpen && (
226
+ <form
227
+ onSubmit={handleSubmitComment}
228
+ className="bg-card/50 border border-border/80 rounded-2xl p-6 shadow-sm space-y-4 animate-in fade-in slide-in-from-top-4 duration-300"
229
+ >
230
+ <h3 className="text-lg font-semibold text-foreground">{t("comments.join_discussion")}</h3>
231
+
232
+ {/* Text Area */}
233
+ <div className="space-y-1.5">
234
+ <label
235
+ htmlFor="comment-content"
236
+ className="text-xs font-semibold text-muted-foreground uppercase tracking-wider block"
237
+ >
238
+ {t("comments.your_message")}
239
+ </label>
240
+ <Textarea
241
+ id="comment-content"
242
+ placeholder={t("comments.message_placeholder")}
243
+ value={content}
244
+ onChange={(e) => setContent(e.target.value)}
245
+ className="min-h-[120px] focus:ring-1 focus:ring-primary"
246
+ disabled={submitting}
247
+ required
248
+ />
249
+ </div>
250
+
251
+ {error && <div className="text-sm font-semibold text-destructive">{error}</div>}
252
+ {success && <div className="text-sm font-semibold text-emerald-600">{success}</div>}
253
+
254
+ <div className="flex justify-end gap-3 pt-2">
255
+ <Button
256
+ type="button"
257
+ variant="ghost"
258
+ onClick={() => setIsFormOpen(false)}
259
+ disabled={submitting}
260
+ >
261
+ {t("comments.cancel")}
262
+ </Button>
263
+ <Button type="submit" disabled={submitting} className="min-w-[120px]">
264
+ {submitting ? (
265
+ <>
266
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
267
+ {t("comments.submitting")}
268
+ </>
269
+ ) : (
270
+ t("comments.post_comment")
271
+ )}
272
+ </Button>
273
+ </div>
274
+ </form>
275
+ )}
276
+
277
+ {/* Comments List */}
278
+ <div className="space-y-6">
279
+ {optimisticComments.length > 0 ? (
280
+ optimisticComments.map((comment) => {
281
+ const hasLiked = likedIds.includes(comment.id) || comment.tempHasReacted;
282
+ const likeCount = (comment.reactions as Record<string, number>)?.likes || 0;
283
+ const commenterName = comment.profiles?.full_name || comment.profiles?.github_username || "Anonymous";
284
+ const dateStr = new Date(comment.created_at).toLocaleDateString(lang, {
285
+ year: "numeric",
286
+ month: "long",
287
+ day: "numeric",
288
+ });
289
+
290
+ return (
291
+ <div
292
+ key={comment.id}
293
+ className="bg-card border border-border/60 rounded-2xl p-5 shadow-sm space-y-4 hover:border-border/100 transition-all duration-300"
294
+ >
295
+ {/* Commenter Header */}
296
+ <div className="flex items-center justify-between gap-4">
297
+ <div className="flex items-center gap-3">
298
+ <Avatar className="h-10 w-10 border">
299
+ <AvatarImage src={comment.profiles?.avatar_url || undefined} alt={commenterName} />
300
+ <AvatarFallback className="bg-primary/5 text-primary text-xs font-semibold">
301
+ {getInitials(comment.profiles)}
302
+ </AvatarFallback>
303
+ </Avatar>
304
+ <div>
305
+ <h4 className="text-sm font-semibold text-foreground leading-none">
306
+ {commenterName}
307
+ </h4>
308
+ <span className="text-[10px] text-muted-foreground mt-1.5 block" suppressHydrationWarning>
309
+ {dateStr}
310
+ </span>
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ {/* Comment Content */}
316
+ <p className="text-sm text-slate-600 dark:text-slate-350 leading-relaxed whitespace-pre-line text-left">
317
+ {comment.content}
318
+ </p>
319
+
320
+ {/* Reaction Actions */}
321
+ <div className="flex items-center pt-2">
322
+ <button
323
+ onClick={() => handleLike(comment.id)}
324
+ className={cn(
325
+ "flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-full border transition-all active:scale-95",
326
+ hasLiked
327
+ ? "bg-primary/5 border-primary/20 text-primary"
328
+ : "bg-background border-border text-muted-foreground hover:text-foreground hover:border-border/100"
329
+ )}
330
+ >
331
+ <ThumbsUp className={cn("h-3.5 w-3.5", hasLiked && "fill-current")} />
332
+ <span>{t("comments.like")}</span>
333
+ {likeCount > 0 && <span className="font-semibold ml-0.5">{likeCount}</span>}
334
+ </button>
335
+ </div>
336
+ </div>
337
+ );
338
+ })
339
+ ) : (
340
+ !loading && (
341
+ <div className="text-center py-12 border border-dashed rounded-2xl bg-slate-50/50 dark:bg-slate-900/10">
342
+ <MessageSquare className="mx-auto h-8 w-8 text-slate-300 dark:text-slate-700" />
343
+ <h3 className="mt-4 text-sm font-semibold text-foreground">{t("comments.no_comments")}</h3>
344
+ <p className="mt-1 text-xs text-muted-foreground">
345
+ {t("comments.be_the_first")}
346
+ </p>
347
+ </div>
348
+ )
349
+ )}
350
+
351
+ {/* Loading Spinner */}
352
+ {loading && (
353
+ <div className="flex justify-center py-6">
354
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
355
+ </div>
356
+ )}
357
+
358
+ {/* Load More */}
359
+ {hasMore && !loading && (
360
+ <div className="flex justify-center pt-2">
361
+ <Button variant="outline" size="sm" onClick={handleLoadMore}>
362
+ {t("comments.load_more")}
363
+ </Button>
364
+ </div>
365
+ )}
366
+ </div>
367
+ </div>
368
+ );
369
+ }