create-nextblock 0.16.2 → 0.16.3
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/api/cron/reset-sandbox/route.ts +4 -4
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +1827 -189
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +40 -0
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -0
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -227
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +2 -0
- package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +7 -1
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -194
- package/templates/nextblock-template/components/seo/PageSeoAuditSection.tsx +8 -1
- package/templates/nextblock-template/lib/seo/page-audit-context.tsx +18 -1
- package/templates/nextblock-template/lib/seo/page-document.test.ts +34 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +52 -1
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +192 -177
- package/templates/nextblock-template/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Badge } from "@nextblock-cms/ui";
|
|
3
|
+
|
|
4
|
+
interface SeoScoreBadgeProps {
|
|
5
|
+
score: number;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function SeoScoreBadge({ score, className = "" }: SeoScoreBadgeProps) {
|
|
10
|
+
const normalizedScore = Math.max(0, Math.min(100, Math.round(score)));
|
|
11
|
+
|
|
12
|
+
let colorClass = "";
|
|
13
|
+
let dotClass = "";
|
|
14
|
+
let title = "";
|
|
15
|
+
|
|
16
|
+
if (normalizedScore >= 90) {
|
|
17
|
+
colorClass = "bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-800/60";
|
|
18
|
+
dotClass = "bg-emerald-500";
|
|
19
|
+
title = `SEO Score: ${normalizedScore}% — Optimal`;
|
|
20
|
+
} else if (normalizedScore >= 70) {
|
|
21
|
+
colorClass = "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-800/60";
|
|
22
|
+
dotClass = "bg-amber-500";
|
|
23
|
+
title = `SEO Score: ${normalizedScore}% — Good (Minor warnings)`;
|
|
24
|
+
} else {
|
|
25
|
+
colorClass = "bg-rose-100 text-rose-800 border-rose-200 dark:bg-rose-950/50 dark:text-rose-300 dark:border-rose-800/60";
|
|
26
|
+
dotClass = "bg-rose-500";
|
|
27
|
+
title = `SEO Score: ${normalizedScore}% — Needs improvement`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Badge
|
|
32
|
+
variant="outline"
|
|
33
|
+
title={title}
|
|
34
|
+
className={`inline-flex items-center px-2 py-0.5 text-xs font-semibold rounded-full border transition-colors ${colorClass} ${className}`}
|
|
35
|
+
>
|
|
36
|
+
<span className={`w-1.5 h-1.5 rounded-full mr-1.5 shrink-0 ${dotClass}`} />
|
|
37
|
+
{normalizedScore}%
|
|
38
|
+
</Badge>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +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
|
+
}
|