@ecomcx/ecomweb-theme-sdk 1.27.0 → 1.29.0
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.d.ts +153 -4
- package/dist/api.js +60 -3
- package/dist/components.js +9 -4
- package/dist/types.d.ts +36 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +19 -0
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PostContentDoc, CommentPage, Menu, Post, ResolveResult, Site, Term, WidgetInstance } from "./types.js";
|
|
1
|
+
import type { Author, PostContentDoc, CommentPage, Media, Menu, Post, ResolveResult, Site, Term, WidgetInstance } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Client gọi Public API của CMS (dùng next-fetch-kit: chạy được cả SSR lẫn CSR,
|
|
4
4
|
* tự gộp request trùng trong cùng một lần render, timeout + retry khi lỗi mạng).
|
|
@@ -32,6 +32,37 @@ export declare const UNLOCK_HEADER = "X-CMS-Unlock";
|
|
|
32
32
|
/** unlock: giá trị cookie cms-unlock — có thì không dùng cache (nội dung riêng của người đã nhập mật khẩu). */
|
|
33
33
|
export declare const resolvePath: (path: string, query?: Record<string, string | string[] | undefined>, noStore?: boolean, unlock?: string) => Promise<ResolveResult>;
|
|
34
34
|
export declare const getPreview: (token: string) => Promise<ResolveResult>;
|
|
35
|
+
/** Một điều kiện lọc theo taxonomy: bài phải khớp (hoặc không khớp) các term này. */
|
|
36
|
+
export interface TaxQueryClause {
|
|
37
|
+
taxonomy: string;
|
|
38
|
+
terms: string[];
|
|
39
|
+
/** Mặc định "IN". "NOT IN" = loại bài thuộc các term này. */
|
|
40
|
+
operator?: "IN" | "NOT IN";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Một điều kiện lọc theo trường tuỳ biến (post.meta). Giá trị so sánh phải đúng kiểu JSON (số là số).
|
|
44
|
+
*
|
|
45
|
+
* So bằng (`=`, `!=`, `IN`, `NOT IN`, `EXISTS`, `NOT EXISTS`) chạy trên chỉ mục GIN sẵn có — dùng được
|
|
46
|
+
* với mọi khoá meta.
|
|
47
|
+
*
|
|
48
|
+
* So khoảng (`>`, `>=`, `<`, `<=`, `BETWEEN`, `LIKE`) **chỉ dùng được với khoá đã khai báo
|
|
49
|
+
* `filterable`** (backend tạo chỉ mục B-tree cho khoá đó lúc khởi động); khoá chưa khai báo trả 422.
|
|
50
|
+
* Với khoá kiểu số, giá trị phải là số JSON (chuỗi số cũng được, core tự chuyển) — nếu không phép so
|
|
51
|
+
* sẽ so chuỗi ("100" < "20").
|
|
52
|
+
*/
|
|
53
|
+
export interface MetaQueryClause {
|
|
54
|
+
key: string;
|
|
55
|
+
compare: "=" | "!=" | "IN" | "NOT IN" | "EXISTS" | "NOT EXISTS" | ">" | ">=" | "<" | "<=" | "BETWEEN" | "LIKE";
|
|
56
|
+
/** Bỏ trống với EXISTS / NOT EXISTS. Với LIKE: mẫu có `%` / `_`. */
|
|
57
|
+
value?: string | number | boolean | (string | number | boolean)[];
|
|
58
|
+
/** Cận trên của BETWEEN (bắt buộc với BETWEEN). */
|
|
59
|
+
value2?: string | number;
|
|
60
|
+
}
|
|
61
|
+
export interface MetaQuery {
|
|
62
|
+
/** Mặc định "AND". */
|
|
63
|
+
relation?: "AND" | "OR";
|
|
64
|
+
clauses: MetaQueryClause[];
|
|
65
|
+
}
|
|
35
66
|
export interface PostQuery {
|
|
36
67
|
/** Mặc định: ngôn ngữ của request hiện tại. */
|
|
37
68
|
locale?: string;
|
|
@@ -40,6 +71,14 @@ export interface PostQuery {
|
|
|
40
71
|
term?: string;
|
|
41
72
|
authorId?: number;
|
|
42
73
|
search?: string;
|
|
74
|
+
/** Lọc nhiều post type cùng lúc (bài viết + trang + job...). Gộp với `type`. */
|
|
75
|
+
types?: string[];
|
|
76
|
+
/** Lọc nhiều taxonomy cùng lúc — thay cho taxonomy/term khi cần AND/OR nhiều điều kiện. */
|
|
77
|
+
taxQuery?: TaxQueryClause[];
|
|
78
|
+
/** Lọc theo trường tuỳ biến; chạy trong cơ sở dữ liệu trên chỉ mục GIN của posts.meta. */
|
|
79
|
+
metaQuery?: MetaQuery;
|
|
80
|
+
/** Chỉ lấy bài thuộc ngữ cảnh này (plugin tự lọc thêm qua filter posts.query). */
|
|
81
|
+
context?: string;
|
|
43
82
|
include?: number[];
|
|
44
83
|
exclude?: number[];
|
|
45
84
|
parentId?: number;
|
|
@@ -47,10 +86,26 @@ export interface PostQuery {
|
|
|
47
86
|
sticky?: boolean;
|
|
48
87
|
/** Kèm nội dung đầy đủ (post.content). Mặc định không — danh sách chỉ cần tiêu đề, sapo, ảnh, giúp trang nhẹ và nhanh. */
|
|
49
88
|
withContent?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* `relevance` = theo độ khớp với `search` (trang tìm kiếm dùng mặc định); `rand` = ngẫu nhiên (kết quả vẫn được
|
|
91
|
+
* cache, đổi khi cache hết hạn hoặc nội dung đổi).
|
|
92
|
+
*/
|
|
50
93
|
page?: number;
|
|
51
94
|
perPage?: number;
|
|
52
|
-
orderBy?: "date" | "title" | "modified" | "menu_order" | "comment_count";
|
|
95
|
+
orderBy?: "date" | "title" | "modified" | "menu_order" | "comment_count" | "id" | "slug" | "rand" | "relevance";
|
|
53
96
|
order?: "asc" | "desc";
|
|
97
|
+
/** Ngày đăng từ (gồm cả mốc), ISO-8601 hoặc Date — vd sự kiện / tin tức từ đầu năm học. */
|
|
98
|
+
after?: string | Date;
|
|
99
|
+
/** Ngày đăng trước (không gồm mốc). */
|
|
100
|
+
before?: string | Date;
|
|
101
|
+
/** Năm đăng (UTC); kèm `month` 1–12 để lấy một tháng — trang lưu trữ. */
|
|
102
|
+
year?: number;
|
|
103
|
+
month?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Sắp xếp theo một khoá meta đã khai báo `filterable` (vd `"gia"`, `"job_deadline"`) — thay cho `orderBy`.
|
|
106
|
+
* Khoá chưa khai báo trả 422 (không có chỉ mục thì sắp xếp phải quét toàn bảng).
|
|
107
|
+
*/
|
|
108
|
+
orderByMeta?: string;
|
|
54
109
|
}
|
|
55
110
|
export declare const getPosts: ({ withContent, ...q }?: PostQuery) => Promise<{
|
|
56
111
|
items: Post[];
|
|
@@ -58,6 +113,95 @@ export declare const getPosts: ({ withContent, ...q }?: PostQuery) => Promise<{
|
|
|
58
113
|
page: number;
|
|
59
114
|
totalPages: number;
|
|
60
115
|
}>;
|
|
116
|
+
/** Một trang của danh sách công khai (term, tác giả, media). */
|
|
117
|
+
export interface ListPage<T> {
|
|
118
|
+
items: T[];
|
|
119
|
+
total: number;
|
|
120
|
+
page: number;
|
|
121
|
+
totalPages: number;
|
|
122
|
+
}
|
|
123
|
+
/** Một giá trị facet: slug term (facet phân loại) hoặc giá trị meta, kèm số bài. */
|
|
124
|
+
export interface FacetValue {
|
|
125
|
+
value: string;
|
|
126
|
+
count: number;
|
|
127
|
+
termId?: number | null;
|
|
128
|
+
name?: string | null;
|
|
129
|
+
parentId?: number | null;
|
|
130
|
+
}
|
|
131
|
+
export interface FacetResult {
|
|
132
|
+
total: number;
|
|
133
|
+
facets: Record<string, FacetValue[]>;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Đếm số bài theo từng giá trị (bộ lọc có số đếm: "Khoa CNTT (12)") trên đúng tập bài mà {@link getPosts} trả về với
|
|
137
|
+
* cùng điều kiện. `facets`: tên phân loại công khai ("category") và/hoặc `"meta:khoá"` (khoá khai báo `filterable`).
|
|
138
|
+
* Tối đa 5 facet × 100 giá trị, đếm trong PostgreSQL.
|
|
139
|
+
* @example const { facets } = await getFacets(["khoa", "meta:hoc-ky"], { type: "khoa-hoc", taxQuery })
|
|
140
|
+
*/
|
|
141
|
+
export declare function getFacets(facets: string[], query?: Omit<PostQuery, "page" | "perPage" | "withContent" | "orderBy" | "order" | "orderByMeta">): Promise<FacetResult>;
|
|
142
|
+
export interface TermQuery {
|
|
143
|
+
taxonomy: string;
|
|
144
|
+
locale?: string;
|
|
145
|
+
/** Tìm theo tên, không phân biệt dấu. */
|
|
146
|
+
search?: string;
|
|
147
|
+
/** Chỉ term con trực tiếp của term này; 0 = chỉ term gốc. */
|
|
148
|
+
parent?: number;
|
|
149
|
+
include?: number[];
|
|
150
|
+
exclude?: number[];
|
|
151
|
+
hideEmpty?: boolean;
|
|
152
|
+
/** Chỉ term có meta khoá này (và đúng giá trị nếu có metaValue). */
|
|
153
|
+
metaKey?: string;
|
|
154
|
+
metaValue?: string;
|
|
155
|
+
orderBy?: "name" | "slug" | "count" | "id";
|
|
156
|
+
order?: "asc" | "desc";
|
|
157
|
+
/** perPage 1–500 (mặc định 100). */
|
|
158
|
+
page?: number;
|
|
159
|
+
perPage?: number;
|
|
160
|
+
/** Tham số thêm cho plugin (filter terms.query đọc trong Query). */
|
|
161
|
+
params?: Record<string, string>;
|
|
162
|
+
}
|
|
163
|
+
/** Danh sách term có phân trang, lọc theo cha / từ khoá / meta, sắp xếp — bản đầy đủ của {@link getTerms}. */
|
|
164
|
+
export declare const queryTerms: ({ params, ...q }: TermQuery) => Promise<ListPage<Term>>;
|
|
165
|
+
export interface AuthorQuery {
|
|
166
|
+
locale?: string;
|
|
167
|
+
/** Tìm theo tên hiển thị, không phân biệt dấu. */
|
|
168
|
+
search?: string;
|
|
169
|
+
include?: number[];
|
|
170
|
+
exclude?: number[];
|
|
171
|
+
/** Lọc theo meta công khai (khoá khai báo `UserMeta` Public), vd metaKey "khoa". Khoá không công khai → 422. */
|
|
172
|
+
metaKey?: string;
|
|
173
|
+
metaValue?: string;
|
|
174
|
+
orderBy?: "name" | "post_count" | "id";
|
|
175
|
+
order?: "asc" | "desc";
|
|
176
|
+
/** perPage 1–100 (mặc định 20). */
|
|
177
|
+
page?: number;
|
|
178
|
+
perPage?: number;
|
|
179
|
+
/** Tham số thêm cho plugin (filter authors.query), vd plugin danh bạ thêm vai trò "giang-vien". */
|
|
180
|
+
params?: Record<string, string>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Danh sách tác giả công khai (trang "Đội ngũ", danh bạ): mặc định thành viên có bài đã đăng; plugin mở rộng qua
|
|
184
|
+
* filter authors.query. Chỉ có tên, slug, ảnh, tiểu sử, meta công khai — không bao giờ có email.
|
|
185
|
+
*/
|
|
186
|
+
export declare const getAuthors: ({ params, ...q }?: AuthorQuery) => Promise<ListPage<Author>>;
|
|
187
|
+
export interface MediaQuery {
|
|
188
|
+
/** Tìm theo tiêu đề, alt, chú thích, tên file — không phân biệt dấu. */
|
|
189
|
+
search?: string;
|
|
190
|
+
/** Tiền tố kiểu file: "image/", "video/", "application/pdf". */
|
|
191
|
+
mime?: string;
|
|
192
|
+
folder?: number;
|
|
193
|
+
include?: number[];
|
|
194
|
+
exclude?: number[];
|
|
195
|
+
orderBy?: "date" | "title" | "id" | "size";
|
|
196
|
+
order?: "asc" | "desc";
|
|
197
|
+
/** perPage 1–100 (mặc định 20). */
|
|
198
|
+
page?: number;
|
|
199
|
+
perPage?: number;
|
|
200
|
+
/** Tham số thêm cho plugin (filter media.query). */
|
|
201
|
+
params?: Record<string, string>;
|
|
202
|
+
}
|
|
203
|
+
/** Danh sách media (thư viện ảnh, tài liệu tải về) — plugin giới hạn / mở rộng qua filter media.query. */
|
|
204
|
+
export declare const getMedia: ({ params, ...q }?: MediaQuery) => Promise<ListPage<Media>>;
|
|
61
205
|
export declare const getPost: (type: string, slug: string, locale?: string) => Promise<Post | null>;
|
|
62
206
|
export declare const getTerms: (taxonomy: string, hideEmpty?: boolean, locale?: string) => Promise<Term[]>;
|
|
63
207
|
/** Cookie giữ token bản nháp của Trình dựng (website đang ở chế độ xem trước). */
|
|
@@ -73,8 +217,13 @@ export declare const getPattern: (id: number) => Promise<PostContentDoc | null>;
|
|
|
73
217
|
export declare const getMenu: (location: string, locale?: string) => Promise<Menu | null>;
|
|
74
218
|
export declare const getMenuById: (id: number) => Promise<Menu | null>;
|
|
75
219
|
export declare const getWidgets: (area: string, locale?: string) => Promise<WidgetInstance[]>;
|
|
76
|
-
/**
|
|
77
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Bình luận đã duyệt theo trang (số mỗi trang, thứ tự: Cài đặt › Thảo luận).
|
|
222
|
+
* Bài có mật khẩu (core >= 1.15): CMS chỉ trả bình luận khi có token mở khoá — unlock = giá trị cookie cms-unlock (không cache).
|
|
223
|
+
*/
|
|
224
|
+
export declare const getComments: (postId: number, page?: number, unlock?: string) => Promise<CommentPage>;
|
|
225
|
+
/** Cookie mở khoá bài có mật khẩu của request hiện tại (server component); ngoài request → undefined. */
|
|
226
|
+
export declare function requestUnlock(): Promise<string | undefined>;
|
|
78
227
|
export declare const getArchives: (locale?: string) => Promise<{
|
|
79
228
|
year: number;
|
|
80
229
|
month: number;
|
package/dist/api.js
CHANGED
|
@@ -116,8 +116,50 @@ export const UNLOCK_HEADER = "X-CMS-Unlock";
|
|
|
116
116
|
/** unlock: giá trị cookie cms-unlock — có thì không dùng cache (nội dung riêng của người đã nhập mật khẩu). */
|
|
117
117
|
export const resolvePath = (path, query = {}, noStore = false, unlock) => cmsFetch("/resolve", { tags: ["posts"], noStore: noStore || !!unlock, query: clean({ ...query, path }), headers: unlock ? { [UNLOCK_HEADER]: unlock } : undefined });
|
|
118
118
|
export const getPreview = (token) => cmsFetch("/preview", { noStore: true, query: { token } });
|
|
119
|
-
export const getPosts = async ({ withContent, ...q } = {}) =>
|
|
119
|
+
export const getPosts = async ({ withContent, ...q } = {}) => {
|
|
120
|
+
// Bóc tách TRONG thân hàm, không phải trong tham số: chữ ký đã công bố của getPosts nằm trong baseline
|
|
121
|
+
// hợp đồng (api/theme-sdk.txt) nên thêm tên vào tham số là đổi một dòng đã ký (CI coi là phá hợp đồng).
|
|
122
|
+
const { taxQuery, metaQuery, ...rest } = q;
|
|
123
|
+
return cmsFetch("/posts", { tags: ["posts"], query: clean({
|
|
124
|
+
...postQueryParams(rest),
|
|
125
|
+
// Bộ lọc phức tạp đi dạng JSON — core phân tích và áp thẳng vào câu SQL (không lọc bằng bộ nhớ).
|
|
126
|
+
taxQuery: taxQuery?.length ? JSON.stringify(taxQuery) : undefined,
|
|
127
|
+
metaQuery: metaQuery?.clauses?.length ? JSON.stringify(metaQuery) : undefined,
|
|
128
|
+
content: withContent ?? false,
|
|
129
|
+
locale: rest.locale ?? (await getRequestLocale()),
|
|
130
|
+
}) });
|
|
131
|
+
};
|
|
120
132
|
const loc = async (locale) => locale ?? (await getRequestLocale());
|
|
133
|
+
/** Tham số đơn giản của PostQuery → query string (Date → ISO). */
|
|
134
|
+
function postQueryParams(q) {
|
|
135
|
+
const iso = (d) => (d instanceof Date ? d.toISOString() : d);
|
|
136
|
+
return { ...q, after: iso(q.after), before: iso(q.before) };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Đếm số bài theo từng giá trị (bộ lọc có số đếm: "Khoa CNTT (12)") trên đúng tập bài mà {@link getPosts} trả về với
|
|
140
|
+
* cùng điều kiện. `facets`: tên phân loại công khai ("category") và/hoặc `"meta:khoá"` (khoá khai báo `filterable`).
|
|
141
|
+
* Tối đa 5 facet × 100 giá trị, đếm trong PostgreSQL.
|
|
142
|
+
* @example const { facets } = await getFacets(["khoa", "meta:hoc-ky"], { type: "khoa-hoc", taxQuery })
|
|
143
|
+
*/
|
|
144
|
+
export async function getFacets(facets, query = {}) {
|
|
145
|
+
const { taxQuery, metaQuery, ...rest } = query;
|
|
146
|
+
return cmsFetch("/posts/facets", { tags: ["posts"], query: clean({
|
|
147
|
+
...postQueryParams(rest),
|
|
148
|
+
facets: facets.join(","),
|
|
149
|
+
taxQuery: taxQuery?.length ? JSON.stringify(taxQuery) : undefined,
|
|
150
|
+
metaQuery: metaQuery?.clauses?.length ? JSON.stringify(metaQuery) : undefined,
|
|
151
|
+
locale: rest.locale ?? (await getRequestLocale()),
|
|
152
|
+
}) });
|
|
153
|
+
}
|
|
154
|
+
/** Danh sách term có phân trang, lọc theo cha / từ khoá / meta, sắp xếp — bản đầy đủ của {@link getTerms}. */
|
|
155
|
+
export const queryTerms = async ({ params, ...q }) => cmsFetch("/terms", { tags: ["terms"], query: clean({ ...params, ...q, locale: await loc(q.locale) }) });
|
|
156
|
+
/**
|
|
157
|
+
* Danh sách tác giả công khai (trang "Đội ngũ", danh bạ): mặc định thành viên có bài đã đăng; plugin mở rộng qua
|
|
158
|
+
* filter authors.query. Chỉ có tên, slug, ảnh, tiểu sử, meta công khai — không bao giờ có email.
|
|
159
|
+
*/
|
|
160
|
+
export const getAuthors = async ({ params, ...q } = {}) => cmsFetch("/authors", { tags: ["authors"], query: clean({ ...params, ...q, locale: await loc(q.locale) }) });
|
|
161
|
+
/** Danh sách media (thư viện ảnh, tài liệu tải về) — plugin giới hạn / mở rộng qua filter media.query. */
|
|
162
|
+
export const getMedia = async ({ params, ...q } = {}) => cmsFetch("/media", { tags: ["media"], query: clean({ ...params, ...q }) });
|
|
121
163
|
export const getPost = async (type, slug, locale) => cmsFetch(`/posts/${type}/${encodeURIComponent(slug)}`, { tags: ["posts"], query: clean({ locale: await loc(locale) }) }).catch(() => null);
|
|
122
164
|
export const getTerms = async (taxonomy, hideEmpty = true, locale) => cmsFetch(`/terms/${taxonomy}`, { tags: ["terms"], query: clean({ hideEmpty, locale: await loc(locale) }) });
|
|
123
165
|
/** Cookie giữ token bản nháp của Trình dựng (website đang ở chế độ xem trước). */
|
|
@@ -143,7 +185,22 @@ export const getPattern = (id) => cmsFetch(`/patterns/${id}`, { tags: ["patterns
|
|
|
143
185
|
export const getMenu = async (location, locale) => cmsFetch(`/menus/${location}`, { tags: ["menus"], query: clean({ locale: await loc(locale) }) });
|
|
144
186
|
export const getMenuById = (id) => cmsFetch(`/menus/id/${id}`, { tags: ["menus"] });
|
|
145
187
|
export const getWidgets = async (area, locale) => cmsFetch(`/widgets/${area}`, { tags: ["widgets"], query: clean({ locale: await loc(locale) }) });
|
|
146
|
-
/**
|
|
147
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Bình luận đã duyệt theo trang (số mỗi trang, thứ tự: Cài đặt › Thảo luận).
|
|
190
|
+
* Bài có mật khẩu (core >= 1.15): CMS chỉ trả bình luận khi có token mở khoá — unlock = giá trị cookie cms-unlock (không cache).
|
|
191
|
+
*/
|
|
192
|
+
export const getComments = (postId, page = 1, unlock) => cmsFetch(`/comments/${postId}`, {
|
|
193
|
+
tags: [`comments:${postId}`], noStore: !!unlock, query: page > 1 ? { page } : undefined, headers: unlock ? { [UNLOCK_HEADER]: unlock } : undefined,
|
|
194
|
+
});
|
|
195
|
+
/** Cookie mở khoá bài có mật khẩu của request hiện tại (server component); ngoài request → undefined. */
|
|
196
|
+
export async function requestUnlock() {
|
|
197
|
+
try {
|
|
198
|
+
const { cookies } = await import("next/headers");
|
|
199
|
+
return (await cookies()).get(UNLOCK_COOKIE)?.value;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
148
205
|
export const getArchives = async (locale) => cmsFetch("/archives", { tags: ["posts"], query: clean({ locale: await loc(locale) }) });
|
|
149
206
|
export const getLanguages = () => cmsFetch("/languages", { tags: ["site"] });
|
package/dist/components.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { getArchives, getComments, getMenu, getMenuById, getPosts, getSite, getTerms, getWidgets } from "./api.js";
|
|
2
|
+
import { getArchives, getComments, getMenu, getMenuById, getPosts, getSite, getTerms, getWidgets, requestUnlock } from "./api.js";
|
|
3
3
|
import { findWidget, getSlotItems } from "./runtime.js";
|
|
4
|
-
import { cx, formatDate, pageLink } from "./utils.js";
|
|
4
|
+
import { cx, formatDate, pageLink, pageLinkWithQuery } from "./utils.js";
|
|
5
5
|
import { Img } from "./image.js";
|
|
6
6
|
import { CommentForm, CommentList, CommentsMore, ExtensionBoundary, LanguageSwitcherClient, Link, SearchFormClient } from "./client.js";
|
|
7
7
|
import { getT } from "./i18n.js";
|
|
@@ -102,7 +102,11 @@ export async function Pagination({ data, prevLabel, nextLabel }) {
|
|
|
102
102
|
else if (pages[pages.length - 1] !== "…")
|
|
103
103
|
pages.push("…");
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
// Danh sách có bộ lọc (?phong-ban=…, ?muc-luong=…) → giữ nguyên bộ lọc khi chuyển trang
|
|
106
|
+
const hasFilters = Object.keys(data.query ?? {}).some((k) => k !== "s" && k !== "page");
|
|
107
|
+
const link = (p) => hasFilters
|
|
108
|
+
? pageLinkWithQuery(data.basePath, p, data.query)
|
|
109
|
+
: pageLink(data.basePath, p, data.kind === "search" ? data.search : null);
|
|
106
110
|
return (_jsxs("nav", { className: "pagination", "aria-label": t("pagination.label"), children: [data.page > 1 && _jsx(Link, { className: "prev", href: link(data.page - 1), children: prevLabel }), pages.map((p, i) => p === "…" ? _jsx("span", { className: "dots", children: "\u2026" }, `e${i}`)
|
|
107
111
|
: p === data.page ? _jsx("span", { className: "current", "aria-current": "page", children: p }, p)
|
|
108
112
|
: _jsx(Link, { href: link(p), children: p }, p)), data.page < data.totalPages && _jsx(Link, { className: "next", href: link(data.page + 1), children: nextLabel })] }));
|
|
@@ -130,7 +134,8 @@ export async function Comments({ post, title }) {
|
|
|
130
134
|
return null;
|
|
131
135
|
if (post.passwordProtected && !post.content)
|
|
132
136
|
return null; // bài có mật khẩu: ẩn bình luận tới khi mở khoá
|
|
133
|
-
const
|
|
137
|
+
const unlock = post.passwordProtected ? await requestUnlock() : undefined;
|
|
138
|
+
const [site, page, t] = await Promise.all([getSite(), getComments(post.id, 1, unlock), getT()]);
|
|
134
139
|
const comments = page?.items ?? [];
|
|
135
140
|
const dateSite = { dateFormat: site.dateFormat, timezone: site.timezone, locale: site.locale };
|
|
136
141
|
title ??= t("comments.title");
|
package/dist/types.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface Author {
|
|
|
11
11
|
slug: string;
|
|
12
12
|
avatarUrl?: string | null;
|
|
13
13
|
bio?: string | null;
|
|
14
|
+
/** Meta công khai của thành viên (plugin khai báo `UserMeta` Public), vd `{ khoa: "cntt" }`. */
|
|
15
|
+
meta?: JsonObject;
|
|
16
|
+
/** Số bài đã đăng — chỉ có trong danh sách {@link getAuthors}. */
|
|
17
|
+
postCount?: number | null;
|
|
18
|
+
/** Dữ liệu plugin thêm qua filter api.author.response. */
|
|
19
|
+
extra?: JsonObject;
|
|
14
20
|
}
|
|
15
21
|
export interface MediaSize {
|
|
16
22
|
url: string;
|
|
@@ -34,6 +40,8 @@ export interface Media {
|
|
|
34
40
|
/** Điểm lấy nét 0–1 (object-position). */
|
|
35
41
|
focalX?: number | null;
|
|
36
42
|
focalY?: number | null;
|
|
43
|
+
title?: string;
|
|
44
|
+
createdAt?: string;
|
|
37
45
|
}
|
|
38
46
|
export interface Term {
|
|
39
47
|
id: number;
|
|
@@ -214,6 +222,12 @@ export interface ResolveResult {
|
|
|
214
222
|
} | null;
|
|
215
223
|
/** Không cho máy tìm kiếm index. */
|
|
216
224
|
noIndex?: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Tham số truy vấn của URL (searchParams) của request hiện tại — server render đọc trực tiếp để
|
|
227
|
+
* lọc danh sách ngay trên máy chủ. Dùng cho bộ lọc người đọc chọn: `getPosts({ metaQuery: … })`
|
|
228
|
+
* rồi truyền `data.query` vào để giữ bộ lọc khi chuyển trang (xem `pageLinkWithQuery`).
|
|
229
|
+
*/
|
|
230
|
+
query?: Record<string, string>;
|
|
217
231
|
}
|
|
218
232
|
/** Props mà mọi template của theme nhận được. */
|
|
219
233
|
export interface TemplateProps {
|
|
@@ -279,6 +293,20 @@ export interface ThemeManifest {
|
|
|
279
293
|
}>;
|
|
280
294
|
/** Nguồn ngoài website theme cần nạp (CSP): { "style-src": ["https://fonts.googleapis.com"], "font-src": ["https://fonts.gstatic.com"] }. */
|
|
281
295
|
csp?: Partial<Record<"script-src" | "style-src" | "img-src" | "font-src" | "connect-src" | "frame-src" | "media-src" | "worker-src" | "form-action", string[]>>;
|
|
296
|
+
/**
|
|
297
|
+
* Kiểu nội dung của theme (CMS ≥ 1.15, như plugin.json — giống functions.php của WordPress):
|
|
298
|
+
* { "su-kien": { "label": "Sự kiện", "rewriteSlug": "su-kien", "taxonomies": ["khoa"] } }.
|
|
299
|
+
* Đăng ký khi theme (hoặc theme con) đang dùng; không đè kiểu của core/plugin.
|
|
300
|
+
*/
|
|
301
|
+
postTypes?: Record<string, Record<string, JsonValue>>;
|
|
302
|
+
/** Phân loại của theme: { "khoa": { "label": "Khoa", "hierarchical": true, "postTypes": ["su-kien"] } }. */
|
|
303
|
+
taxonomies?: Record<string, Record<string, JsonValue>>;
|
|
304
|
+
/** Meta bài của theme: { "ngay-bat-dau": { "filterable": true, "indexType": "date" } } — lọc/sắp xếp/facet được. */
|
|
305
|
+
postMeta?: Record<string, Record<string, JsonValue>>;
|
|
306
|
+
/** Meta thành viên: { "hoc-vi": { "public": true } } — hiện trong hồ sơ tác giả (Author.meta). */
|
|
307
|
+
userMeta?: Record<string, {
|
|
308
|
+
public?: boolean;
|
|
309
|
+
}>;
|
|
282
310
|
[key: string]: unknown;
|
|
283
311
|
}
|
|
284
312
|
type Loader<T> = () => Promise<{
|
|
@@ -429,6 +457,14 @@ export interface FilterMap {
|
|
|
429
457
|
"sitemap.entries": (entries: SitemapEntry[], ctx: {
|
|
430
458
|
site: Site;
|
|
431
459
|
}) => SitemapEntry[] | Promise<SitemapEntry[]>;
|
|
460
|
+
/**
|
|
461
|
+
* Đổi thứ tự / thêm tên template trước khi theme chọn (core ≥ 1.15) — vd plugin "Sự kiện" thêm "plugin-su-kien" lên đầu
|
|
462
|
+
* cho kiểu nội dung của nó và khai báo template đó trong `definePlugin({ templates })`. Theme có template trùng tên vẫn thắng.
|
|
463
|
+
*/
|
|
464
|
+
"template.hierarchy": (templates: string[], ctx: {
|
|
465
|
+
site: Site;
|
|
466
|
+
data: ResolveResult;
|
|
467
|
+
}) => string[] | Promise<string[]>;
|
|
432
468
|
/** Thêm/bớt class của thẻ <body> (vd "logged-in", "has-promo-bar") */
|
|
433
469
|
"body.class": (classes: string[], ctx: {
|
|
434
470
|
site: Site;
|
package/dist/utils.d.ts
CHANGED
|
@@ -5,4 +5,11 @@ export declare function formatDate(value: string | null | undefined, site?: Pick
|
|
|
5
5
|
}): string;
|
|
6
6
|
export declare function setting<T = string>(settings: Record<string, unknown>, key: string, fallback: T): T;
|
|
7
7
|
export declare function pageLink(basePath: string, page: number, search?: string | null): string;
|
|
8
|
+
/**
|
|
9
|
+
* Link của một trang trong danh sách CÓ bộ lọc: giữ nguyên mọi tham số truy vấn hiện có (phòng ban,
|
|
10
|
+
* mức lương, từ khoá…) và chỉ đổi số trang. Truyền `data.query` của trang vào.
|
|
11
|
+
*
|
|
12
|
+
* <Link href={pageLinkWithQuery(data.basePath, 2, data.query)}>2</Link>
|
|
13
|
+
*/
|
|
14
|
+
export declare function pageLinkWithQuery(basePath: string, page: number, query?: Record<string, string> | null): string;
|
|
8
15
|
export declare function cx(...classes: Array<string | false | null | undefined>): string;
|
package/dist/utils.js
CHANGED
|
@@ -24,6 +24,25 @@ export function pageLink(basePath, page, search) {
|
|
|
24
24
|
const path = page <= 1 ? base || "/" : `${base}/page/${page}`;
|
|
25
25
|
return search ? `${path}${path.includes("?") ? "&" : "?"}s=${encodeURIComponent(search)}` : path;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Link của một trang trong danh sách CÓ bộ lọc: giữ nguyên mọi tham số truy vấn hiện có (phòng ban,
|
|
29
|
+
* mức lương, từ khoá…) và chỉ đổi số trang. Truyền `data.query` của trang vào.
|
|
30
|
+
*
|
|
31
|
+
* <Link href={pageLinkWithQuery(data.basePath, 2, data.query)}>2</Link>
|
|
32
|
+
*/
|
|
33
|
+
export function pageLinkWithQuery(basePath, page, query) {
|
|
34
|
+
const base = basePath === "/" ? "" : basePath.replace(/\/$/, "");
|
|
35
|
+
const path = page <= 1 ? base || "/" : `${base}/page/${page}`;
|
|
36
|
+
const params = new URLSearchParams();
|
|
37
|
+
for (const [k, v] of Object.entries(query ?? {})) {
|
|
38
|
+
// "page" do đường dẫn quyết định; tham số rỗng không cần giữ
|
|
39
|
+
if (k === "page" || v === "" || v === undefined || v === null)
|
|
40
|
+
continue;
|
|
41
|
+
params.set(k, v);
|
|
42
|
+
}
|
|
43
|
+
const qs = params.toString();
|
|
44
|
+
return qs ? `${path}${path.includes("?") ? "&" : "?"}${qs}` : path;
|
|
45
|
+
}
|
|
27
46
|
export function cx(...classes) {
|
|
28
47
|
return classes.filter(Boolean).join(" ");
|
|
29
48
|
}
|