@ampless/admin 1.0.0-alpha.82 → 1.0.0-alpha.84
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/dist/api/index.d.ts +58 -2
- package/dist/api/index.js +213 -1
- package/dist/{chunk-TBNETGQL.js → chunk-7NQERXNY.js} +1 -1
- package/dist/{chunk-7AGYC7JB.js → chunk-BRTC5O3E.js} +15 -4
- package/dist/{chunk-MPZ3GVZC.js → chunk-CFIOUXVE.js} +16 -6
- package/dist/{chunk-IR4EB5C3.js → chunk-GGHN2RKQ.js} +1 -1
- package/dist/{chunk-DDAGBC4N.js → chunk-HN3UFHP7.js} +1 -1
- package/dist/chunk-J6R356H3.js +183 -0
- package/dist/{chunk-DUSEHGSV.js → chunk-K44QF3MV.js} +1 -1
- package/dist/{chunk-HOJJIABL.js → chunk-MKWYNK4J.js} +2 -2
- package/dist/{chunk-7TAT4AHW.js → chunk-OFPNBZKP.js} +36 -2
- package/dist/{chunk-PHV2FQFV.js → chunk-SMI7Y77B.js} +1 -1
- package/dist/{chunk-DSCNWIBN.js → chunk-SQ6LFPXH.js} +2 -2
- package/dist/{chunk-4QTNVVKD.js → chunk-V6JNTCPS.js} +2 -2
- package/dist/{chunk-NI4JSVXL.js → chunk-W4VHP2JW.js} +1 -1
- package/dist/{chunk-AB2VWBDC.js → chunk-WXIYV2PO.js} +45 -3
- package/dist/{chunk-OJTHDE7F.js → chunk-YU5MPQXB.js} +2 -2
- package/dist/components/admin-dashboard.js +3 -3
- package/dist/components/edit-post-view.js +5 -5
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +6 -6
- package/dist/components/login-view.js +3 -3
- package/dist/components/mcp-tokens-view.js +3 -3
- package/dist/components/media-view.js +5 -5
- package/dist/components/new-post-view.js +5 -5
- package/dist/components/plugin-settings-form.js +3 -3
- package/dist/components/posts-list-view.js +3 -3
- package/dist/components/users-list-view.js +3 -3
- package/dist/{i18n-DBNV_Fgf.d.ts → i18n-DA-aakRX.d.ts} +52 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/pages/index.d.ts +1 -1
- package/dist/pages/index.js +15 -15
- package/package.json +4 -4
- package/dist/chunk-LNX3I35F.js +0 -67
package/dist/api/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { NextRequest } from 'next/server';
|
|
|
2
2
|
import { Admin } from '../index.js';
|
|
3
3
|
import 'ampless';
|
|
4
4
|
import '@ampless/runtime';
|
|
5
|
-
import '../i18n-
|
|
5
|
+
import '../i18n-DA-aakRX.js';
|
|
6
6
|
import '@aws-amplify/adapter-nextjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -34,4 +34,60 @@ declare function createMediaProxyRoute(admin: Admin): {
|
|
|
34
34
|
}) => Promise<Response>;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Build the `POST /admin/preview` Route Handler.
|
|
39
|
+
*
|
|
40
|
+
* Client-side `<PostForm>` / `<PostHistoryPanel>` POST a draft Post to this
|
|
41
|
+
* endpoint while the preview tab is open; we render the body + page-level
|
|
42
|
+
* scripts via `ampless.renderBody` / `publicPostScriptsForPage` and return
|
|
43
|
+
* a **complete HTML document** that the admin shows in an iframe using
|
|
44
|
+
* `srcDoc`.
|
|
45
|
+
*
|
|
46
|
+
* ## Why a complete document (not a bare fragment)?
|
|
47
|
+
*
|
|
48
|
+
* An iframe loaded via `srcDoc` does NOT inherit the parent page's CSS.
|
|
49
|
+
* Classes on the iframe *element* (e.g. `prose prose-neutral`) style only
|
|
50
|
+
* the element's box model, not its content document. To get typography inside
|
|
51
|
+
* the preview we must inline the styles directly into the document we return.
|
|
52
|
+
* We inject two `<style>` blocks:
|
|
53
|
+
*
|
|
54
|
+
* 1. **Theme CSS vars**: `renderThemeCss(cssVars)` — the same `:root {…}`
|
|
55
|
+
* block the public root layout emits, so the live theme's colour tokens
|
|
56
|
+
* flow into the preview.
|
|
57
|
+
* 2. **`PREVIEW_BASE_CSS`**: a self-contained typography approximation
|
|
58
|
+
* (h1-h6, p, ul/ol/li, a, code, pre, blockquote, img, table, hr, dark
|
|
59
|
+
* mode). This is *not* the actual live theme stylesheet — Next.js App
|
|
60
|
+
* Router emits hashed CSS filenames that are unknowable from a Route
|
|
61
|
+
* Handler — but it's enough for a "readable preview".
|
|
62
|
+
*
|
|
63
|
+
* ## Why a Route Handler (not a Server Action)?
|
|
64
|
+
*
|
|
65
|
+
* Next.js 15+ refuses to compile Client Components that reach
|
|
66
|
+
* `react-dom/server` through a `'use server'` module, because the build
|
|
67
|
+
* traces the import graph from Client Components through Server Action
|
|
68
|
+
* modules. Putting this rendering behind a Route Handler decouples it from
|
|
69
|
+
* that graph entirely — the form fetches a plain HTTP endpoint and the
|
|
70
|
+
* bundler never walks from `<PostForm>` into here. The endpoint also gets an
|
|
71
|
+
* explicit `admin.isEditor()` gate so a future change to the `(admin)`
|
|
72
|
+
* route-group middleware can't silently turn preview into a content-leak
|
|
73
|
+
* vector for unpublished drafts.
|
|
74
|
+
*
|
|
75
|
+
* ## Dynamic import of `react-dom/server`
|
|
76
|
+
*
|
|
77
|
+
* The `react-dom/server` import itself is loaded via dynamic `import()` inside
|
|
78
|
+
* the handler. Next.js 16's Turbopack flags any top-level static
|
|
79
|
+
* `import 'react-dom/server'` reached from the app router build (Route
|
|
80
|
+
* Handlers included), so we deliberately defer the resolution to request time
|
|
81
|
+
* — the module is still pulled from the same Node.js subpath that `next start`
|
|
82
|
+
* ships, just not visible to the build-time import-graph walker.
|
|
83
|
+
*
|
|
84
|
+
* ## Auth
|
|
85
|
+
*
|
|
86
|
+
* Locked to authenticated editors. Anonymous + reader access is 403. This
|
|
87
|
+
* matches the rest of `/admin/**`, which is gated by the `(admin)` route
|
|
88
|
+
* group + middleware, but we add an explicit check here as defence-in-depth
|
|
89
|
+
* against the middleware gate being misconfigured.
|
|
90
|
+
*/
|
|
91
|
+
declare function createPreviewRouteHandler(admin: Admin): (req: Request) => Promise<Response>;
|
|
92
|
+
|
|
93
|
+
export { createMediaProxyRoute, createPreviewRouteHandler };
|
package/dist/api/index.js
CHANGED
|
@@ -55,6 +55,218 @@ async function signMediaUrl({
|
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
// src/api/preview-route.tsx
|
|
60
|
+
import { renderThemeCss } from "@ampless/runtime";
|
|
61
|
+
import { Fragment, jsxs } from "react/jsx-runtime";
|
|
62
|
+
var PREVIEW_BASE_CSS = `
|
|
63
|
+
/* ampless admin preview \u2014 typography approximation */
|
|
64
|
+
*,
|
|
65
|
+
*::before,
|
|
66
|
+
*::after {
|
|
67
|
+
box-sizing: border-box;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
body {
|
|
71
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
72
|
+
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
|
73
|
+
font-size: 1rem;
|
|
74
|
+
line-height: 1.75;
|
|
75
|
+
color: #1a202c;
|
|
76
|
+
background: #fff;
|
|
77
|
+
margin: 0;
|
|
78
|
+
padding: 1.5rem;
|
|
79
|
+
max-width: 65ch;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main {
|
|
83
|
+
width: 100%;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Headings */
|
|
87
|
+
h1, h2, h3, h4, h5, h6 {
|
|
88
|
+
font-weight: 700;
|
|
89
|
+
line-height: 1.25;
|
|
90
|
+
margin-top: 1.5em;
|
|
91
|
+
margin-bottom: 0.5em;
|
|
92
|
+
}
|
|
93
|
+
h1 { font-size: 2.25em; margin-top: 0; }
|
|
94
|
+
h2 { font-size: 1.5em; }
|
|
95
|
+
h3 { font-size: 1.25em; }
|
|
96
|
+
h4 { font-size: 1em; }
|
|
97
|
+
h5 { font-size: 0.875em; }
|
|
98
|
+
h6 { font-size: 0.875em; font-weight: 600; color: #4a5568; }
|
|
99
|
+
|
|
100
|
+
/* Paragraph */
|
|
101
|
+
p {
|
|
102
|
+
margin-top: 1em;
|
|
103
|
+
margin-bottom: 1em;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Lists */
|
|
107
|
+
ul, ol {
|
|
108
|
+
padding-left: 1.625em;
|
|
109
|
+
margin-top: 1em;
|
|
110
|
+
margin-bottom: 1em;
|
|
111
|
+
}
|
|
112
|
+
ul { list-style-type: disc; }
|
|
113
|
+
ol { list-style-type: decimal; }
|
|
114
|
+
li {
|
|
115
|
+
margin-top: 0.25em;
|
|
116
|
+
margin-bottom: 0.25em;
|
|
117
|
+
}
|
|
118
|
+
li > ul, li > ol {
|
|
119
|
+
margin-top: 0.25em;
|
|
120
|
+
margin-bottom: 0.25em;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Links */
|
|
124
|
+
a {
|
|
125
|
+
color: #2563eb;
|
|
126
|
+
text-decoration: underline;
|
|
127
|
+
}
|
|
128
|
+
a:hover {
|
|
129
|
+
color: #1d4ed8;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Inline code */
|
|
133
|
+
code {
|
|
134
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
135
|
+
"Liberation Mono", "Courier New", monospace;
|
|
136
|
+
font-size: 0.875em;
|
|
137
|
+
background: #f1f5f9;
|
|
138
|
+
padding: 0.125em 0.375em;
|
|
139
|
+
border-radius: 0.25em;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Code block */
|
|
143
|
+
pre {
|
|
144
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
145
|
+
"Liberation Mono", "Courier New", monospace;
|
|
146
|
+
font-size: 0.875em;
|
|
147
|
+
background: #1e293b;
|
|
148
|
+
color: #e2e8f0;
|
|
149
|
+
padding: 1em 1.25em;
|
|
150
|
+
border-radius: 0.5em;
|
|
151
|
+
overflow-x: auto;
|
|
152
|
+
margin-top: 1.25em;
|
|
153
|
+
margin-bottom: 1.25em;
|
|
154
|
+
line-height: 1.7;
|
|
155
|
+
}
|
|
156
|
+
pre code {
|
|
157
|
+
background: transparent;
|
|
158
|
+
padding: 0;
|
|
159
|
+
font-size: inherit;
|
|
160
|
+
color: inherit;
|
|
161
|
+
border-radius: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Blockquote */
|
|
165
|
+
blockquote {
|
|
166
|
+
border-left: 0.25rem solid #cbd5e1;
|
|
167
|
+
padding-left: 1em;
|
|
168
|
+
margin-left: 0;
|
|
169
|
+
margin-right: 0;
|
|
170
|
+
color: #64748b;
|
|
171
|
+
font-style: italic;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Horizontal rule */
|
|
175
|
+
hr {
|
|
176
|
+
border: none;
|
|
177
|
+
border-top: 1px solid #e2e8f0;
|
|
178
|
+
margin: 2em 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Images */
|
|
182
|
+
img {
|
|
183
|
+
max-width: 100%;
|
|
184
|
+
height: auto;
|
|
185
|
+
border-radius: 0.375em;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Tables */
|
|
189
|
+
table {
|
|
190
|
+
width: 100%;
|
|
191
|
+
border-collapse: collapse;
|
|
192
|
+
font-size: 0.875em;
|
|
193
|
+
margin-top: 1.25em;
|
|
194
|
+
margin-bottom: 1.25em;
|
|
195
|
+
}
|
|
196
|
+
th {
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
text-align: left;
|
|
199
|
+
padding: 0.5em 0.75em;
|
|
200
|
+
border-bottom: 2px solid #e2e8f0;
|
|
201
|
+
}
|
|
202
|
+
td {
|
|
203
|
+
padding: 0.5em 0.75em;
|
|
204
|
+
border-bottom: 1px solid #e2e8f0;
|
|
205
|
+
}
|
|
206
|
+
tr:last-child td {
|
|
207
|
+
border-bottom: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Strong / em */
|
|
211
|
+
strong { font-weight: 700; }
|
|
212
|
+
em { font-style: italic; }
|
|
213
|
+
|
|
214
|
+
/* ------------------------------------------------------------------ */
|
|
215
|
+
/* Dark mode */
|
|
216
|
+
/* ------------------------------------------------------------------ */
|
|
217
|
+
@media (prefers-color-scheme: dark) {
|
|
218
|
+
body {
|
|
219
|
+
color: #e2e8f0;
|
|
220
|
+
background: #0f172a;
|
|
221
|
+
}
|
|
222
|
+
h6 { color: #94a3b8; }
|
|
223
|
+
a { color: #60a5fa; }
|
|
224
|
+
a:hover { color: #93c5fd; }
|
|
225
|
+
code { background: #1e293b; color: #e2e8f0; }
|
|
226
|
+
pre { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; }
|
|
227
|
+
blockquote { border-left-color: #475569; color: #94a3b8; }
|
|
228
|
+
hr { border-top-color: #334155; }
|
|
229
|
+
th { border-bottom-color: #334155; }
|
|
230
|
+
td { border-bottom-color: #1e293b; }
|
|
231
|
+
}
|
|
232
|
+
`.trim();
|
|
233
|
+
function createPreviewRouteHandler(admin) {
|
|
234
|
+
return async function POST(req) {
|
|
235
|
+
const session = await admin.getServerSession();
|
|
236
|
+
if (!admin.isEditor(session)) {
|
|
237
|
+
return new Response("Forbidden", { status: 403 });
|
|
238
|
+
}
|
|
239
|
+
let draft;
|
|
240
|
+
try {
|
|
241
|
+
draft = await req.json();
|
|
242
|
+
} catch {
|
|
243
|
+
return new Response("Bad Request", { status: 400 });
|
|
244
|
+
}
|
|
245
|
+
const ampless = await admin.getAmpless();
|
|
246
|
+
const node = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
247
|
+
await ampless.renderBody(draft),
|
|
248
|
+
await ampless.publicPostScriptsForPage([draft])
|
|
249
|
+
] });
|
|
250
|
+
const { renderToStaticMarkup } = await import("react-dom/server");
|
|
251
|
+
const fragment = renderToStaticMarkup(node);
|
|
252
|
+
let themeCssBlock = "";
|
|
253
|
+
try {
|
|
254
|
+
const themeConfig = await admin.loadThemeConfig();
|
|
255
|
+
const css = renderThemeCss(themeConfig.cssVars);
|
|
256
|
+
if (css) themeCssBlock = css;
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.warn("[ampless admin] preview theme CSS unavailable:", err);
|
|
259
|
+
}
|
|
260
|
+
const html = `<!doctype html><html><head><meta charset="utf-8">` + (themeCssBlock ? `<style>${themeCssBlock}</style>` : "") + `<style>${PREVIEW_BASE_CSS}</style></head><body class="ampless-preview"><main>${fragment}</main></body></html>`;
|
|
261
|
+
return new Response(html, {
|
|
262
|
+
headers: {
|
|
263
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
264
|
+
"Cache-Control": "no-store"
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
}
|
|
58
269
|
export {
|
|
59
|
-
createMediaProxyRoute
|
|
270
|
+
createMediaProxyRoute,
|
|
271
|
+
createPreviewRouteHandler
|
|
60
272
|
};
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
getAdminCmsConfig,
|
|
5
5
|
getMediaProcessingDefaults,
|
|
6
6
|
uploadProcessedImage
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HN3UFHP7.js";
|
|
8
8
|
import {
|
|
9
9
|
getAdminEditorExtensions,
|
|
10
10
|
getAdminTiptapNodeHtml,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "./chunk-2ITWLRYF.js";
|
|
16
16
|
import {
|
|
17
17
|
useT
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-K44QF3MV.js";
|
|
19
19
|
|
|
20
20
|
// src/components/media-picker.tsx
|
|
21
21
|
import { useEffect, useRef, useState } from "react";
|
|
@@ -839,13 +839,17 @@ function PostHistoryPanel({
|
|
|
839
839
|
// this site. A stricter sandbox (= separate-origin preview
|
|
840
840
|
// route + CSP / COEP / COOP) is parked for v2.0+ if/when
|
|
841
841
|
// a real plugin marketplace lands.
|
|
842
|
+
// No `prose` classes here: classes on the iframe ELEMENT
|
|
843
|
+
// don't style its srcDoc contents. Typography comes from
|
|
844
|
+
// the full document the preview route now returns
|
|
845
|
+
// (PREVIEW_BASE_CSS + theme vars in createPreviewRouteHandler).
|
|
842
846
|
/* @__PURE__ */ jsx6(
|
|
843
847
|
"iframe",
|
|
844
848
|
{
|
|
845
849
|
title: "revision-preview",
|
|
846
850
|
srcDoc: previewHtml,
|
|
847
851
|
sandbox: "allow-scripts allow-same-origin",
|
|
848
|
-
className: "
|
|
852
|
+
className: "min-h-[300px] w-full rounded-md border"
|
|
849
853
|
}
|
|
850
854
|
)
|
|
851
855
|
)
|
|
@@ -1738,13 +1742,20 @@ function PostForm({ post, previewEndpoint = "/admin/preview" }) {
|
|
|
1738
1742
|
// trusted editors of this site. A stricter sandbox (= separate-
|
|
1739
1743
|
// origin preview route + CSP / COEP / COOP) is parked for
|
|
1740
1744
|
// v2.0+ if/when a real plugin marketplace lands.
|
|
1745
|
+
//
|
|
1746
|
+
// Typography note: `prose prose-neutral dark:prose-invert
|
|
1747
|
+
// max-w-none` were removed from the iframe element. Those classes
|
|
1748
|
+
// style the iframe *element* border-box only, not its content
|
|
1749
|
+
// document — an iframe srcDoc does not inherit the parent page's
|
|
1750
|
+
// CSS. Typography is now applied inside the document returned by
|
|
1751
|
+
// createPreviewRouteHandler (PREVIEW_BASE_CSS + theme cssVars).
|
|
1741
1752
|
/* @__PURE__ */ jsx8(
|
|
1742
1753
|
"iframe",
|
|
1743
1754
|
{
|
|
1744
1755
|
title: "post-preview",
|
|
1745
1756
|
srcDoc: previewHtml,
|
|
1746
1757
|
sandbox: "allow-scripts allow-same-origin",
|
|
1747
|
-
className: "
|
|
1758
|
+
className: "min-h-[400px] w-full rounded-md border"
|
|
1748
1759
|
}
|
|
1749
1760
|
)
|
|
1750
1761
|
),
|
|
@@ -1,43 +1,53 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import {
|
|
3
3
|
useT
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-K44QF3MV.js";
|
|
5
5
|
|
|
6
6
|
// src/components/admin-dashboard.tsx
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
import Link from "next/link";
|
|
9
|
-
import {
|
|
9
|
+
import { listPostSummaries } from "ampless";
|
|
10
10
|
import { Button, Card, CardContent, CardHeader, CardTitle } from "@ampless/runtime/ui";
|
|
11
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
12
|
function AdminDashboard() {
|
|
13
13
|
const t = useT();
|
|
14
14
|
const [posts, setPosts] = useState([]);
|
|
15
15
|
const [loading, setLoading] = useState(true);
|
|
16
|
+
const [loadError, setLoadError] = useState(null);
|
|
16
17
|
useEffect(() => {
|
|
17
|
-
|
|
18
|
+
listPostSummaries({ status: "all" }).then(setPosts).catch((err) => {
|
|
19
|
+
console.error("[admin-dashboard] listPostSummaries failed:", err);
|
|
20
|
+
setLoadError(err instanceof Error ? err.message : String(err));
|
|
21
|
+
}).finally(() => setLoading(false));
|
|
18
22
|
}, []);
|
|
19
23
|
const published = posts.filter((p) => p.status === "published").length;
|
|
20
24
|
const drafts = posts.filter((p) => p.status === "draft").length;
|
|
25
|
+
const countPending = loading || Boolean(loadError);
|
|
21
26
|
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl p-4 md:p-8", children: [
|
|
22
27
|
/* @__PURE__ */ jsxs("div", { className: "mb-6 flex flex-wrap items-center justify-between gap-3 md:mb-8", children: [
|
|
23
28
|
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold md:text-3xl", children: t("dashboard.title") }),
|
|
24
29
|
/* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/admin/posts/new", children: t("dashboard.newPost") }) })
|
|
25
30
|
] }),
|
|
31
|
+
loadError && /* @__PURE__ */ jsxs("p", { className: "mb-4 text-sm text-destructive", children: [
|
|
32
|
+
t("posts.list.loadError"),
|
|
33
|
+
": ",
|
|
34
|
+
loadError
|
|
35
|
+
] }),
|
|
26
36
|
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3", children: [
|
|
27
37
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
28
38
|
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { children: t("dashboard.totalPosts") }) }),
|
|
29
39
|
/* @__PURE__ */ jsxs(CardContent, { children: [
|
|
30
|
-
/* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children:
|
|
40
|
+
/* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: countPending ? "-" : posts.length }),
|
|
31
41
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("dashboard.totalLabel") })
|
|
32
42
|
] })
|
|
33
43
|
] }),
|
|
34
44
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
35
45
|
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { children: t("dashboard.published") }) }),
|
|
36
|
-
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children:
|
|
46
|
+
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: countPending ? "-" : published }) })
|
|
37
47
|
] }),
|
|
38
48
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
39
49
|
/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { children: t("dashboard.drafts") }) }),
|
|
40
|
-
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children:
|
|
50
|
+
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-3xl font-bold", children: countPending ? "-" : drafts }) })
|
|
41
51
|
] })
|
|
42
52
|
] })
|
|
43
53
|
] });
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import {
|
|
3
|
+
useT
|
|
4
|
+
} from "./chunk-K44QF3MV.js";
|
|
5
|
+
|
|
6
|
+
// src/components/posts-list-view.tsx
|
|
7
|
+
import { useEffect, useMemo, useState } from "react";
|
|
8
|
+
import Link from "next/link";
|
|
9
|
+
import { formatDate, listPostSummaries } from "ampless";
|
|
10
|
+
import {
|
|
11
|
+
Button,
|
|
12
|
+
Input,
|
|
13
|
+
Table,
|
|
14
|
+
TableBody,
|
|
15
|
+
TableCell,
|
|
16
|
+
TableHead,
|
|
17
|
+
TableHeader,
|
|
18
|
+
TableRow
|
|
19
|
+
} from "@ampless/runtime/ui";
|
|
20
|
+
import {
|
|
21
|
+
collectTags,
|
|
22
|
+
filterSortPostSummaries
|
|
23
|
+
} from "ampless";
|
|
24
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
25
|
+
var PAGE_SIZE = 100;
|
|
26
|
+
function PostsList() {
|
|
27
|
+
const t = useT();
|
|
28
|
+
const [posts, setPosts] = useState([]);
|
|
29
|
+
const [loading, setLoading] = useState(true);
|
|
30
|
+
const [loadError, setLoadError] = useState(null);
|
|
31
|
+
const [query, setQuery] = useState("");
|
|
32
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
33
|
+
const [tagFilter, setTagFilter] = useState("");
|
|
34
|
+
const [sort, setSort] = useState("updated-desc");
|
|
35
|
+
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
listPostSummaries({ status: "all" }).then(setPosts).catch((err) => {
|
|
38
|
+
console.error("[posts-list-view] listPostSummaries failed:", err);
|
|
39
|
+
setLoadError(err instanceof Error ? err.message : String(err));
|
|
40
|
+
}).finally(() => setLoading(false));
|
|
41
|
+
}, []);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setVisibleCount(PAGE_SIZE);
|
|
44
|
+
}, [query, statusFilter, tagFilter, sort]);
|
|
45
|
+
const tagCounts = useMemo(() => collectTags(posts), [posts]);
|
|
46
|
+
const filteredPosts = useMemo(
|
|
47
|
+
() => filterSortPostSummaries(posts, {
|
|
48
|
+
query,
|
|
49
|
+
status: statusFilter,
|
|
50
|
+
tag: tagFilter,
|
|
51
|
+
sort
|
|
52
|
+
}),
|
|
53
|
+
[posts, query, statusFilter, tagFilter, sort]
|
|
54
|
+
);
|
|
55
|
+
const visiblePosts = filteredPosts.slice(0, visibleCount);
|
|
56
|
+
const hasMore = visibleCount < filteredPosts.length;
|
|
57
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl p-4 md:p-8", children: [
|
|
58
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-6 flex flex-wrap items-center justify-between gap-3 md:mb-8", children: [
|
|
59
|
+
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold md:text-3xl", children: t("posts.list.title") }),
|
|
60
|
+
/* @__PURE__ */ jsx(Button, { asChild: true, children: /* @__PURE__ */ jsx(Link, { href: "/admin/posts/new", children: t("posts.list.newButton") }) })
|
|
61
|
+
] }),
|
|
62
|
+
loading ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("common.loading") }) : loadError ? /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive", children: [
|
|
63
|
+
t("posts.list.loadError"),
|
|
64
|
+
": ",
|
|
65
|
+
loadError
|
|
66
|
+
] }) : posts.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-md border p-12 text-center", children: [
|
|
67
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("posts.list.empty") }),
|
|
68
|
+
/* @__PURE__ */ jsx(Button, { asChild: true, className: "mt-4", children: /* @__PURE__ */ jsx(Link, { href: "/admin/posts/new", children: t("posts.list.createFirst") }) })
|
|
69
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
70
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
71
|
+
/* @__PURE__ */ jsx(
|
|
72
|
+
Input,
|
|
73
|
+
{
|
|
74
|
+
className: "min-w-56 max-w-sm",
|
|
75
|
+
value: query,
|
|
76
|
+
onChange: (e) => setQuery(e.target.value),
|
|
77
|
+
placeholder: t("posts.list.searchPlaceholder")
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
/* @__PURE__ */ jsxs(
|
|
81
|
+
"select",
|
|
82
|
+
{
|
|
83
|
+
className: "h-9 rounded-md border bg-background px-2 py-1.5 text-sm",
|
|
84
|
+
value: statusFilter,
|
|
85
|
+
"aria-label": t("common.status"),
|
|
86
|
+
onChange: (e) => setStatusFilter(e.target.value),
|
|
87
|
+
children: [
|
|
88
|
+
/* @__PURE__ */ jsx("option", { value: "all", children: t("posts.list.filterAll") }),
|
|
89
|
+
/* @__PURE__ */ jsx("option", { value: "published", children: t("posts.list.filterPublished") }),
|
|
90
|
+
/* @__PURE__ */ jsx("option", { value: "draft", children: t("posts.list.filterDraft") })
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
),
|
|
94
|
+
/* @__PURE__ */ jsxs(
|
|
95
|
+
"select",
|
|
96
|
+
{
|
|
97
|
+
className: "h-9 rounded-md border bg-background px-2 py-1.5 text-sm",
|
|
98
|
+
value: tagFilter,
|
|
99
|
+
"aria-label": t("common.tags"),
|
|
100
|
+
onChange: (e) => setTagFilter(e.target.value),
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t("posts.list.filterAllTags") }),
|
|
103
|
+
[...tagCounts.entries()].map(([tag, count]) => /* @__PURE__ */ jsxs("option", { value: tag, children: [
|
|
104
|
+
tag,
|
|
105
|
+
" (",
|
|
106
|
+
count,
|
|
107
|
+
")"
|
|
108
|
+
] }, tag))
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
),
|
|
112
|
+
/* @__PURE__ */ jsxs(
|
|
113
|
+
"select",
|
|
114
|
+
{
|
|
115
|
+
className: "h-9 rounded-md border bg-background px-2 py-1.5 text-sm",
|
|
116
|
+
value: sort,
|
|
117
|
+
"aria-label": t("posts.list.sortLabel"),
|
|
118
|
+
onChange: (e) => setSort(e.target.value),
|
|
119
|
+
children: [
|
|
120
|
+
/* @__PURE__ */ jsx("option", { value: "updated-desc", children: t("posts.list.sortUpdatedDesc") }),
|
|
121
|
+
/* @__PURE__ */ jsx("option", { value: "updated-asc", children: t("posts.list.sortUpdatedAsc") }),
|
|
122
|
+
/* @__PURE__ */ jsx("option", { value: "published-desc", children: t("posts.list.sortPublishedDesc") }),
|
|
123
|
+
/* @__PURE__ */ jsx("option", { value: "published-asc", children: t("posts.list.sortPublishedAsc") }),
|
|
124
|
+
/* @__PURE__ */ jsx("option", { value: "title-asc", children: t("posts.list.sortTitleAsc") }),
|
|
125
|
+
/* @__PURE__ */ jsx("option", { value: "title-desc", children: t("posts.list.sortTitleDesc") })
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
),
|
|
129
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: t("posts.list.resultCount", {
|
|
130
|
+
shown: filteredPosts.length,
|
|
131
|
+
total: posts.length
|
|
132
|
+
}) })
|
|
133
|
+
] }),
|
|
134
|
+
filteredPosts.length === 0 ? /* @__PURE__ */ jsx("p", { className: "rounded-md border p-8 text-center text-muted-foreground", children: t("posts.list.noMatches") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
135
|
+
/* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-md border", children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
136
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
137
|
+
/* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnTitle") }),
|
|
138
|
+
/* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnStatus") }),
|
|
139
|
+
/* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnSlug") }),
|
|
140
|
+
/* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnPublished") }),
|
|
141
|
+
/* @__PURE__ */ jsx(TableHead, { children: t("posts.list.columnUpdated") })
|
|
142
|
+
] }) }),
|
|
143
|
+
/* @__PURE__ */ jsx(TableBody, { children: visiblePosts.map((post) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
144
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
|
|
145
|
+
Link,
|
|
146
|
+
{
|
|
147
|
+
href: `/admin/posts/${post.postId}`,
|
|
148
|
+
className: "font-medium hover:underline",
|
|
149
|
+
children: post.title
|
|
150
|
+
}
|
|
151
|
+
) }),
|
|
152
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
|
|
153
|
+
"span",
|
|
154
|
+
{
|
|
155
|
+
className: post.status === "published" ? "inline-block rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700" : "inline-block rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700",
|
|
156
|
+
children: t(`common.${post.status}`)
|
|
157
|
+
}
|
|
158
|
+
) }),
|
|
159
|
+
/* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs text-muted-foreground", children: post.slug }),
|
|
160
|
+
/* @__PURE__ */ jsx(TableCell, { className: "whitespace-nowrap text-sm text-muted-foreground", children: formatPostDate(post.publishedAt) }),
|
|
161
|
+
/* @__PURE__ */ jsx(TableCell, { className: "whitespace-nowrap text-sm text-muted-foreground", children: formatPostDate(post.updatedAt) })
|
|
162
|
+
] }, post.postId)) })
|
|
163
|
+
] }) }),
|
|
164
|
+
hasMore && /* @__PURE__ */ jsx(
|
|
165
|
+
Button,
|
|
166
|
+
{
|
|
167
|
+
type: "button",
|
|
168
|
+
variant: "outline",
|
|
169
|
+
onClick: () => setVisibleCount((n) => n + PAGE_SIZE),
|
|
170
|
+
children: t("posts.list.showMore")
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
] })
|
|
174
|
+
] })
|
|
175
|
+
] });
|
|
176
|
+
}
|
|
177
|
+
function formatPostDate(value) {
|
|
178
|
+
return value ? formatDate(value) || "-" : "-";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export {
|
|
182
|
+
PostsList
|
|
183
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import {
|
|
3
3
|
MediaUploader
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-SQ6LFPXH.js";
|
|
5
5
|
import {
|
|
6
6
|
useT
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-K44QF3MV.js";
|
|
8
8
|
|
|
9
9
|
// src/components/media-view.tsx
|
|
10
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|