create-nextblock 0.16.2 → 0.16.4
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 +2002 -189
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -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/app/cms/products/ProductFormClientShell.tsx +19 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +14 -2
- 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 +68 -0
- package/templates/nextblock-template/lib/seo/page-document.ts +4 -412
- package/templates/nextblock-template/lib/seo/robots-txt.ts +5 -4
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +202 -177
- package/templates/nextblock-template/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -1,227 +1,273 @@
|
|
|
1
|
-
// app/cms/pages/page.tsx
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { Button } from "@nextblock-cms/ui";
|
|
6
|
-
import {
|
|
7
|
-
Table,
|
|
8
|
-
TableBody,
|
|
9
|
-
TableCell,
|
|
10
|
-
TableHead,
|
|
11
|
-
TableHeader,
|
|
12
|
-
TableRow,
|
|
13
|
-
} from "@nextblock-cms/ui";
|
|
14
|
-
import { Badge } from "@nextblock-cms/ui";
|
|
15
|
-
import { Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
16
|
-
import {
|
|
17
|
-
MoreHorizontal,
|
|
18
|
-
PlusCircle,
|
|
19
|
-
Edit3,
|
|
20
|
-
FileText,
|
|
21
|
-
} from "lucide-react";
|
|
22
|
-
import {
|
|
23
|
-
DropdownMenu,
|
|
24
|
-
DropdownMenuContent,
|
|
25
|
-
DropdownMenuItem,
|
|
26
|
-
DropdownMenuButtonTrigger,
|
|
27
|
-
DropdownMenuSeparator,
|
|
28
|
-
} from "@nextblock-cms/ui";
|
|
29
|
-
|
|
30
|
-
import
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import
|
|
36
|
-
import
|
|
37
|
-
import
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
1
|
+
// app/cms/pages/page.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { createClient } from "@nextblock-cms/db/server";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { Button } from "@nextblock-cms/ui";
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from "@nextblock-cms/ui";
|
|
14
|
+
import { Badge } from "@nextblock-cms/ui";
|
|
15
|
+
import { Alert, AlertDescription } from "@nextblock-cms/ui";
|
|
16
|
+
import {
|
|
17
|
+
MoreHorizontal,
|
|
18
|
+
PlusCircle,
|
|
19
|
+
Edit3,
|
|
20
|
+
FileText,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
import {
|
|
23
|
+
DropdownMenu,
|
|
24
|
+
DropdownMenuContent,
|
|
25
|
+
DropdownMenuItem,
|
|
26
|
+
DropdownMenuButtonTrigger,
|
|
27
|
+
DropdownMenuSeparator,
|
|
28
|
+
} from "@nextblock-cms/ui";
|
|
29
|
+
import type { Database } from "@nextblock-cms/db";
|
|
30
|
+
import { getActiveLanguagesServerSide } from "@nextblock-cms/db/server";
|
|
31
|
+
import { auditSeo } from "@nextblock-cms/utils/seo";
|
|
32
|
+
import { buildPageSeoDocument } from "../../../lib/seo/page-document";
|
|
33
|
+
|
|
34
|
+
type Page = Database["public"]["Tables"]["pages"]["Row"];
|
|
35
|
+
import LanguageFilterSelect from "../components/LanguageFilterSelect";
|
|
36
|
+
import DeletePageButtonClient from "./components/DeletePageButtonClient";
|
|
37
|
+
import { ContentTransferControls } from "../import-export/ContentTransferControls";
|
|
38
|
+
import VisibilityBadge from "../components/VisibilityBadge";
|
|
39
|
+
import SeoScoreBadge from "../components/SeoScoreBadge";
|
|
40
|
+
import TablePagination from "../components/TablePagination";
|
|
41
|
+
|
|
42
|
+
async function getPagesWithDetails(
|
|
43
|
+
filterLanguageId?: number,
|
|
44
|
+
pageNumber: number = 1,
|
|
45
|
+
pageSize: number = 25
|
|
46
|
+
): Promise<{
|
|
47
|
+
items: { page: Page; languageCode: string; seoScore: number }[];
|
|
48
|
+
totalCount: number;
|
|
49
|
+
}> {
|
|
50
|
+
const supabase = createClient();
|
|
51
|
+
const languages = await getActiveLanguagesServerSide();
|
|
52
|
+
const langMap = new Map(languages.map((l) => [l.id, l.code]));
|
|
53
|
+
|
|
54
|
+
let query = supabase
|
|
55
|
+
.from("pages")
|
|
56
|
+
.select("*, languages!inner(code), blocks(id, block_type, content, order)", { count: "exact" })
|
|
57
|
+
.order("created_at", { ascending: false });
|
|
58
|
+
|
|
59
|
+
if (filterLanguageId) {
|
|
60
|
+
query = query.eq("language_id", filterLanguageId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const from = (pageNumber - 1) * pageSize;
|
|
64
|
+
const to = from + pageSize - 1;
|
|
65
|
+
|
|
66
|
+
const { data: pagesData, count, error } = await query.range(from, to);
|
|
67
|
+
|
|
68
|
+
if (error) {
|
|
69
|
+
console.error("Error fetching pages:", error);
|
|
70
|
+
return { items: [], totalCount: 0 };
|
|
71
|
+
}
|
|
72
|
+
if (!pagesData) return { items: [], totalCount: 0 };
|
|
73
|
+
|
|
74
|
+
const items = pagesData.map((p) => {
|
|
75
|
+
const langInfo = p.languages as unknown as { code: string } | null;
|
|
76
|
+
const rawBlocks = (p.blocks || []) as unknown as Parameters<typeof buildPageSeoDocument>[0];
|
|
77
|
+
const doc = buildPageSeoDocument(rawBlocks);
|
|
78
|
+
const auditResult = auditSeo({
|
|
79
|
+
document: doc,
|
|
80
|
+
metaTitle: p.meta_title ?? undefined,
|
|
81
|
+
metaDescription: p.meta_description ?? undefined,
|
|
82
|
+
scope: "page",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const langCode = langInfo?.code || langMap.get(p.language_id) || "N/A";
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
page: p as unknown as Page,
|
|
89
|
+
languageCode: String(langCode).toUpperCase(),
|
|
90
|
+
seoScore: auditResult.score,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return { items, totalCount: count ?? items.length };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface CmsPagesListPageProps {
|
|
98
|
+
searchParams?: Promise<{
|
|
99
|
+
lang?: string;
|
|
100
|
+
success?: string;
|
|
101
|
+
page?: string;
|
|
102
|
+
pageSize?: string;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default async function CmsPagesListPage(props: CmsPagesListPageProps) {
|
|
107
|
+
const searchParams = await props.searchParams;
|
|
108
|
+
const allLanguages = await getActiveLanguagesServerSide();
|
|
109
|
+
const selectedLangId = searchParams?.lang
|
|
110
|
+
? parseInt(searchParams.lang, 10)
|
|
111
|
+
: undefined;
|
|
112
|
+
|
|
113
|
+
const isValidLangId = selectedLangId
|
|
114
|
+
? allLanguages.some((l) => l.id === selectedLangId)
|
|
115
|
+
: true;
|
|
116
|
+
const filterLangId = isValidLangId ? selectedLangId : undefined;
|
|
117
|
+
|
|
118
|
+
const pageNumber = searchParams?.page
|
|
119
|
+
? Math.max(1, parseInt(searchParams.page, 10) || 1)
|
|
120
|
+
: 1;
|
|
121
|
+
const pageSize = searchParams?.pageSize
|
|
122
|
+
? Math.max(1, parseInt(searchParams.pageSize, 10) || 25)
|
|
123
|
+
: 25;
|
|
124
|
+
|
|
125
|
+
const { items: pagesWithDetails, totalCount } = await getPagesWithDetails(
|
|
126
|
+
filterLangId,
|
|
127
|
+
pageNumber,
|
|
128
|
+
pageSize
|
|
129
|
+
);
|
|
130
|
+
const successMessage = searchParams?.success;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="w-full">
|
|
134
|
+
<div className="flex justify-between items-center mb-6 flex-wrap gap-4">
|
|
135
|
+
<h1 className="text-2xl font-semibold">Manage Pages</h1>
|
|
136
|
+
<div className="flex items-center gap-3">
|
|
137
|
+
<ContentTransferControls
|
|
138
|
+
contentType="pages"
|
|
139
|
+
label="Pages"
|
|
140
|
+
languageId={filterLangId}
|
|
141
|
+
hasContent={pagesWithDetails.length > 0}
|
|
142
|
+
/>
|
|
143
|
+
<LanguageFilterSelect
|
|
144
|
+
allLanguages={allLanguages}
|
|
145
|
+
currentFilterLangId={filterLangId}
|
|
146
|
+
basePath="/cms/pages"
|
|
147
|
+
/>
|
|
148
|
+
<Button variant="default" asChild>
|
|
149
|
+
<Link href="/cms/pages/new">
|
|
150
|
+
<PlusCircle className="mr-2 h-4 w-4" /> Create New Page
|
|
151
|
+
</Link>
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{successMessage && (
|
|
157
|
+
<Alert variant="success" className="mb-4">
|
|
158
|
+
<AlertDescription>
|
|
159
|
+
{decodeURIComponent(successMessage)}
|
|
160
|
+
</AlertDescription>
|
|
161
|
+
</Alert>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{totalCount === 0 ? (
|
|
165
|
+
<div className="text-center py-10 border rounded-lg dark:border-slate-700">
|
|
166
|
+
<FileText className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
167
|
+
<h3 className="mt-2 text-sm font-medium text-foreground">
|
|
168
|
+
{filterLangId
|
|
169
|
+
? "No pages found for the selected language."
|
|
170
|
+
: "No pages found."}
|
|
171
|
+
</h3>
|
|
172
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
173
|
+
Get started by creating a new page.
|
|
174
|
+
</p>
|
|
175
|
+
<div className="mt-6">
|
|
176
|
+
<Button asChild>
|
|
177
|
+
<Link href="/cms/pages/new">
|
|
178
|
+
<PlusCircle className="mr-2 h-4 w-4" /> Create Page
|
|
179
|
+
</Link>
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
) : (
|
|
184
|
+
<div className="rounded-lg border overflow-hidden dark:border-slate-700">
|
|
185
|
+
<Table>
|
|
186
|
+
<TableHeader>
|
|
187
|
+
<TableRow className="dark:border-slate-700">
|
|
188
|
+
<TableHead className="w-[280px] sm:w-[350px]">Title</TableHead>
|
|
189
|
+
<TableHead>Status</TableHead>
|
|
190
|
+
<TableHead>Language</TableHead>
|
|
191
|
+
<TableHead className="hidden md:table-cell">Slug</TableHead>
|
|
192
|
+
<TableHead>SEO</TableHead>
|
|
193
|
+
<TableHead className="hidden lg:table-cell">
|
|
194
|
+
Last Updated
|
|
195
|
+
</TableHead>
|
|
196
|
+
<TableHead className="text-right w-[80px]">Actions</TableHead>
|
|
197
|
+
</TableRow>
|
|
198
|
+
</TableHeader>
|
|
199
|
+
<TableBody>
|
|
200
|
+
{pagesWithDetails.map(({ page, languageCode, seoScore }) => (
|
|
201
|
+
<TableRow key={page.id} className="dark:border-slate-700">
|
|
202
|
+
<TableCell className="font-medium">
|
|
203
|
+
<Link
|
|
204
|
+
href={`/cms/pages/${page.id}/edit`}
|
|
205
|
+
className="flex items-center cursor-pointer"
|
|
206
|
+
>
|
|
207
|
+
<Edit3 className="mr-2 h-4 w-4" />
|
|
208
|
+
{page.title}
|
|
209
|
+
</Link>
|
|
210
|
+
</TableCell>
|
|
211
|
+
<TableCell>
|
|
212
|
+
<VisibilityBadge
|
|
213
|
+
type="page"
|
|
214
|
+
status={page.status}
|
|
215
|
+
publishedAt={page.published_at}
|
|
216
|
+
/>
|
|
217
|
+
</TableCell>
|
|
218
|
+
<TableCell>
|
|
219
|
+
<Badge variant="outline" className="dark:border-slate-600">
|
|
220
|
+
{languageCode}
|
|
221
|
+
</Badge>
|
|
222
|
+
</TableCell>
|
|
223
|
+
<TableCell className="text-muted-foreground text-xs hidden md:table-cell">
|
|
224
|
+
/{page.slug}
|
|
225
|
+
</TableCell>
|
|
226
|
+
<TableCell>
|
|
227
|
+
<SeoScoreBadge score={seoScore} />
|
|
228
|
+
</TableCell>
|
|
229
|
+
<TableCell className="hidden lg:table-cell text-xs text-muted-foreground">
|
|
230
|
+
{new Date(page.updated_at).toLocaleDateString()}
|
|
231
|
+
</TableCell>
|
|
232
|
+
<TableCell className="text-right">
|
|
233
|
+
<DropdownMenu>
|
|
234
|
+
<DropdownMenuButtonTrigger id={`page-trigger-${page.id}`}>
|
|
235
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
236
|
+
<span className="sr-only">
|
|
237
|
+
Page actions for {page.title}
|
|
238
|
+
</span>
|
|
239
|
+
</DropdownMenuButtonTrigger>
|
|
240
|
+
<DropdownMenuContent align="end">
|
|
241
|
+
<DropdownMenuItem asChild>
|
|
242
|
+
<Link
|
|
243
|
+
href={`/cms/pages/${page.id}/edit`}
|
|
244
|
+
className="flex items-center cursor-pointer"
|
|
245
|
+
>
|
|
246
|
+
<Edit3 className="mr-2 h-4 w-4" /> Edit
|
|
247
|
+
</Link>
|
|
248
|
+
</DropdownMenuItem>
|
|
249
|
+
<DropdownMenuSeparator />
|
|
250
|
+
<DeletePageButtonClient
|
|
251
|
+
pageId={page.id}
|
|
252
|
+
pageTitle={page.title}
|
|
253
|
+
/>
|
|
254
|
+
</DropdownMenuContent>
|
|
255
|
+
</DropdownMenu>
|
|
256
|
+
</TableCell>
|
|
257
|
+
</TableRow>
|
|
258
|
+
))}
|
|
259
|
+
</TableBody>
|
|
260
|
+
</Table>
|
|
261
|
+
|
|
262
|
+
<TablePagination
|
|
263
|
+
currentPage={pageNumber}
|
|
264
|
+
pageSize={pageSize}
|
|
265
|
+
totalCount={totalCount}
|
|
266
|
+
basePath="/cms/pages"
|
|
267
|
+
itemLabel="pages"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
@@ -189,6 +189,8 @@ export default async function EditPostPage(props: { params: Promise<{ id: string
|
|
|
189
189
|
paint instead of an empty document.
|
|
190
190
|
*/}
|
|
191
191
|
<PageSeoProvider
|
|
192
|
+
documentTitle={postWithBlocks.title}
|
|
193
|
+
documentType="post"
|
|
192
194
|
initialBlocks={postWithBlocks.blocks}
|
|
193
195
|
initialMetaDescription={postWithBlocks.meta_description}
|
|
194
196
|
initialMetaTitle={postWithBlocks.meta_title}
|