@jant/core 0.3.25 → 0.3.27
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/app.js +70 -563
- package/dist/auth.js +3 -0
- package/dist/client.js +1 -0
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -10
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/navigation.js +23 -3
- package/dist/lib/render.js +10 -1
- package/dist/lib/schemas.js +31 -0
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +1 -1
- package/dist/routes/api/posts.js +1 -1
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/dash/collections.js +23 -415
- package/dist/routes/dash/media.js +12 -392
- package/dist/routes/dash/pages.js +7 -330
- package/dist/routes/dash/redirects.js +18 -12
- package/dist/routes/dash/settings.js +198 -577
- package/dist/routes/feed/rss.js +2 -1
- package/dist/routes/feed/sitemap.js +4 -2
- package/dist/routes/pages/featured.js +5 -1
- package/dist/routes/pages/home.js +26 -1
- package/dist/routes/pages/latest.js +45 -0
- package/dist/services/post.js +30 -50
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/ui/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +36 -21
- package/dist/ui/dash/PageForm.js +21 -15
- package/dist/ui/dash/PostForm.js +22 -16
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/ui/layouts/BaseLayout.js +24 -2
- package/dist/ui/layouts/SiteLayout.js +47 -19
- package/package.json +1 -1
- package/src/app.tsx +95 -553
- package/src/auth.ts +4 -1
- package/src/client.ts +1 -0
- package/src/i18n/locales/en.po +240 -175
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +240 -175
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +240 -175
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +2 -2
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -11
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/navigation.ts +33 -2
- package/src/lib/render.tsx +15 -1
- package/src/lib/schemas.ts +39 -0
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +1 -1
- package/src/routes/api/posts.ts +1 -1
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +17 -366
- package/src/routes/dash/media.tsx +12 -414
- package/src/routes/dash/pages.tsx +8 -348
- package/src/routes/dash/redirects.tsx +20 -14
- package/src/routes/dash/settings.tsx +243 -534
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +3 -1
- package/src/routes/feed/sitemap.ts +4 -2
- package/src/routes/pages/featured.tsx +7 -1
- package/src/routes/pages/home.tsx +25 -2
- package/src/routes/pages/latest.tsx +59 -0
- package/src/services/post.ts +34 -66
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +1 -1
- package/src/styles/ui.css +24 -40
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -644
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/ui/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +40 -21
- package/src/ui/dash/PageForm.tsx +25 -19
- package/src/ui/dash/PostForm.tsx +26 -20
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/ui/layouts/BaseLayout.tsx +17 -0
- package/src/ui/layouts/SiteLayout.tsx +45 -31
package/src/ui/color-themes.ts
CHANGED
|
@@ -118,9 +118,40 @@ function defineTheme(opts: {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
|
|
121
|
+
defineTheme({
|
|
122
|
+
id: "halloween",
|
|
123
|
+
name: "Halloween",
|
|
124
|
+
preview: {
|
|
125
|
+
lightBg: "#f9f2e3",
|
|
126
|
+
lightText: "#352200",
|
|
127
|
+
lightLink: "#b84400",
|
|
128
|
+
darkBg: "#1e1000",
|
|
129
|
+
darkText: "#dfc390",
|
|
130
|
+
darkLink: "#ff8c00",
|
|
131
|
+
},
|
|
132
|
+
light: {
|
|
133
|
+
bg: "oklch(0.97 0.015 75)",
|
|
134
|
+
fg: "oklch(0.25 0.04 55)",
|
|
135
|
+
primary: "oklch(0.47 0.17 50)",
|
|
136
|
+
primaryFg: "oklch(0.98 0.01 75)",
|
|
137
|
+
muted: "oklch(0.93 0.02 75)",
|
|
138
|
+
mutedFg: "oklch(0.5 0.025 55)",
|
|
139
|
+
border: "oklch(0.88 0.025 75)",
|
|
140
|
+
},
|
|
141
|
+
dark: {
|
|
142
|
+
bg: "oklch(0.16 0.03 50)",
|
|
143
|
+
fg: "oklch(0.85 0.025 75)",
|
|
144
|
+
primary: "oklch(0.72 0.19 55)",
|
|
145
|
+
primaryFg: "oklch(0.14 0.03 50)",
|
|
146
|
+
muted: "oklch(0.22 0.025 50)",
|
|
147
|
+
mutedFg: "oklch(0.62 0.02 75)",
|
|
148
|
+
border: "oklch(0.28 0.025 50)",
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
|
|
121
152
|
{
|
|
122
153
|
id: "default",
|
|
123
|
-
name: "
|
|
154
|
+
name: "Panda",
|
|
124
155
|
light: {},
|
|
125
156
|
dark: {},
|
|
126
157
|
preview: {
|
|
@@ -226,37 +257,6 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
|
|
|
226
257
|
},
|
|
227
258
|
}),
|
|
228
259
|
|
|
229
|
-
defineTheme({
|
|
230
|
-
id: "halloween",
|
|
231
|
-
name: "Halloween",
|
|
232
|
-
preview: {
|
|
233
|
-
lightBg: "#f9f2e3",
|
|
234
|
-
lightText: "#352200",
|
|
235
|
-
lightLink: "#cc5500",
|
|
236
|
-
darkBg: "#1e1000",
|
|
237
|
-
darkText: "#dfc390",
|
|
238
|
-
darkLink: "#ff8c00",
|
|
239
|
-
},
|
|
240
|
-
light: {
|
|
241
|
-
bg: "oklch(0.97 0.015 75)",
|
|
242
|
-
fg: "oklch(0.25 0.04 55)",
|
|
243
|
-
primary: "oklch(0.6 0.2 50)",
|
|
244
|
-
primaryFg: "oklch(0.98 0.01 75)",
|
|
245
|
-
muted: "oklch(0.93 0.02 75)",
|
|
246
|
-
mutedFg: "oklch(0.5 0.025 55)",
|
|
247
|
-
border: "oklch(0.88 0.025 75)",
|
|
248
|
-
},
|
|
249
|
-
dark: {
|
|
250
|
-
bg: "oklch(0.16 0.03 50)",
|
|
251
|
-
fg: "oklch(0.85 0.025 75)",
|
|
252
|
-
primary: "oklch(0.72 0.19 55)",
|
|
253
|
-
primaryFg: "oklch(0.14 0.03 50)",
|
|
254
|
-
muted: "oklch(0.22 0.025 50)",
|
|
255
|
-
mutedFg: "oklch(0.62 0.02 75)",
|
|
256
|
-
border: "oklch(0.28 0.025 50)",
|
|
257
|
-
},
|
|
258
|
-
}),
|
|
259
|
-
|
|
260
260
|
defineTheme({
|
|
261
261
|
id: "notepad",
|
|
262
262
|
name: "Notepad",
|
|
@@ -294,7 +294,7 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
|
|
|
294
294
|
preview: {
|
|
295
295
|
lightBg: "#f7eef5",
|
|
296
296
|
lightText: "#2e1e2c",
|
|
297
|
-
lightLink: "#
|
|
297
|
+
lightLink: "#7a30a8",
|
|
298
298
|
darkBg: "#1d1428",
|
|
299
299
|
darkText: "#d4c2d0",
|
|
300
300
|
darkLink: "#c080fc",
|
|
@@ -302,7 +302,7 @@ export const BUILTIN_COLOR_THEMES: ColorTheme[] = [
|
|
|
302
302
|
light: {
|
|
303
303
|
bg: "oklch(0.97 0.012 325)",
|
|
304
304
|
fg: "oklch(0.25 0.02 310)",
|
|
305
|
-
primary: "oklch(0.
|
|
305
|
+
primary: "oklch(0.45 0.2 300)",
|
|
306
306
|
primaryFg: "oklch(0.98 0.008 325)",
|
|
307
307
|
muted: "oklch(0.93 0.016 325)",
|
|
308
308
|
mutedFg: "oklch(0.52 0.015 310)",
|
|
@@ -319,34 +319,53 @@ export const ComposeDialog: FC<ComposeDialogProps> = ({ collections }) => {
|
|
|
319
319
|
<button
|
|
320
320
|
type="button"
|
|
321
321
|
class="btn-outline text-sm"
|
|
322
|
-
data-attr
|
|
322
|
+
data-attr:disabled="$_composeLoading"
|
|
323
323
|
data-on:click="$status = 'draft'; document.querySelector('#compose-dialog form').requestSubmit()"
|
|
324
324
|
>
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
325
|
+
<svg
|
|
326
|
+
data-show="$_composeLoading"
|
|
327
|
+
style="display:none"
|
|
328
|
+
class="animate-spin size-4"
|
|
329
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
330
|
+
viewBox="0 0 24 24"
|
|
331
|
+
fill="none"
|
|
332
|
+
stroke="currentColor"
|
|
333
|
+
stroke-width="2"
|
|
334
|
+
stroke-linecap="round"
|
|
335
|
+
stroke-linejoin="round"
|
|
336
|
+
role="status"
|
|
337
|
+
>
|
|
338
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
339
|
+
</svg>
|
|
340
|
+
{t({
|
|
341
|
+
message: "Draft",
|
|
342
|
+
comment: "@context: Compose button - save as draft",
|
|
343
|
+
})}
|
|
332
344
|
</button>
|
|
333
345
|
<button
|
|
334
346
|
type="submit"
|
|
335
347
|
class="btn text-sm"
|
|
336
|
-
data-attr
|
|
348
|
+
data-attr:disabled="$_composeLoading"
|
|
337
349
|
>
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
+
<svg
|
|
351
|
+
data-show="$_composeLoading"
|
|
352
|
+
style="display:none"
|
|
353
|
+
class="animate-spin size-4"
|
|
354
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
355
|
+
viewBox="0 0 24 24"
|
|
356
|
+
fill="none"
|
|
357
|
+
stroke="currentColor"
|
|
358
|
+
stroke-width="2"
|
|
359
|
+
stroke-linecap="round"
|
|
360
|
+
stroke-linejoin="round"
|
|
361
|
+
role="status"
|
|
362
|
+
>
|
|
363
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
364
|
+
</svg>
|
|
365
|
+
{t({
|
|
366
|
+
message: "Post",
|
|
367
|
+
comment: "@context: Compose button - publish post",
|
|
368
|
+
})}
|
|
350
369
|
</button>
|
|
351
370
|
</div>
|
|
352
371
|
</div>
|
package/src/ui/dash/PageForm.tsx
CHANGED
|
@@ -147,25 +147,31 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
147
147
|
|
|
148
148
|
{/* Submit */}
|
|
149
149
|
<div class="flex gap-2">
|
|
150
|
-
<button type="submit" class="btn" data-attr
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
150
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
151
|
+
<svg
|
|
152
|
+
data-show="$_loading"
|
|
153
|
+
style="display:none"
|
|
154
|
+
class="animate-spin size-4"
|
|
155
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
156
|
+
viewBox="0 0 24 24"
|
|
157
|
+
fill="none"
|
|
158
|
+
stroke="currentColor"
|
|
159
|
+
stroke-width="2"
|
|
160
|
+
stroke-linecap="round"
|
|
161
|
+
stroke-linejoin="round"
|
|
162
|
+
role="status"
|
|
163
|
+
>
|
|
164
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
165
|
+
</svg>
|
|
166
|
+
{isEdit
|
|
167
|
+
? t({
|
|
168
|
+
message: "Update Page",
|
|
169
|
+
comment: "@context: Button to update existing page",
|
|
170
|
+
})
|
|
171
|
+
: t({
|
|
172
|
+
message: "Create Page",
|
|
173
|
+
comment: "@context: Button to create new page",
|
|
174
|
+
})}
|
|
169
175
|
</button>
|
|
170
176
|
<a href={cancelUrl} class="btn-outline">
|
|
171
177
|
{t({
|
package/src/ui/dash/PostForm.tsx
CHANGED
|
@@ -168,7 +168,7 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
168
168
|
r2PublicUrl,
|
|
169
169
|
s3PublicUrl,
|
|
170
170
|
);
|
|
171
|
-
const mUrl = getMediaUrl(m.
|
|
171
|
+
const mUrl = getMediaUrl(m.storageKey, pUrl);
|
|
172
172
|
const thumbUrl = getImageUrl(mUrl, imageTransformUrl, {
|
|
173
173
|
width: 150,
|
|
174
174
|
quality: 80,
|
|
@@ -288,25 +288,31 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
288
288
|
|
|
289
289
|
{/* Submit */}
|
|
290
290
|
<div class="flex gap-2">
|
|
291
|
-
<button type="submit" class="btn" data-attr
|
|
292
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
291
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
292
|
+
<svg
|
|
293
|
+
data-show="$_loading"
|
|
294
|
+
style="display:none"
|
|
295
|
+
class="animate-spin size-4"
|
|
296
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
297
|
+
viewBox="0 0 24 24"
|
|
298
|
+
fill="none"
|
|
299
|
+
stroke="currentColor"
|
|
300
|
+
stroke-width="2"
|
|
301
|
+
stroke-linecap="round"
|
|
302
|
+
stroke-linejoin="round"
|
|
303
|
+
role="status"
|
|
304
|
+
>
|
|
305
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
306
|
+
</svg>
|
|
307
|
+
{isEdit
|
|
308
|
+
? t({
|
|
309
|
+
message: "Update",
|
|
310
|
+
comment: "@context: Button to update existing post",
|
|
311
|
+
})
|
|
312
|
+
: t({
|
|
313
|
+
message: "Publish",
|
|
314
|
+
comment: "@context: Button to publish new post",
|
|
315
|
+
})}
|
|
310
316
|
</button>
|
|
311
317
|
<a href="/dash/posts" class="btn-outline">
|
|
312
318
|
{t({ message: "Cancel", comment: "@context: Button to cancel form" })}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared collection form (new + edit)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import type { Collection } from "../../../types.js";
|
|
7
|
+
|
|
8
|
+
export function CollectionForm({
|
|
9
|
+
collection,
|
|
10
|
+
isEdit,
|
|
11
|
+
}: {
|
|
12
|
+
collection?: Collection;
|
|
13
|
+
isEdit?: boolean;
|
|
14
|
+
}) {
|
|
15
|
+
const { t } = useLingui();
|
|
16
|
+
|
|
17
|
+
const signals = JSON.stringify({
|
|
18
|
+
title: collection?.title ?? "",
|
|
19
|
+
slug: collection?.slug ?? "",
|
|
20
|
+
description: collection?.description ?? "",
|
|
21
|
+
}).replace(/</g, "\\u003c");
|
|
22
|
+
|
|
23
|
+
const action = isEdit
|
|
24
|
+
? `/dash/collections/${collection?.id}`
|
|
25
|
+
: "/dash/collections";
|
|
26
|
+
|
|
27
|
+
const heading = isEdit
|
|
28
|
+
? t({ message: "Edit Collection", comment: "@context: Page heading" })
|
|
29
|
+
: t({ message: "New Collection", comment: "@context: Page heading" });
|
|
30
|
+
|
|
31
|
+
const submitLabel = isEdit
|
|
32
|
+
? t({
|
|
33
|
+
message: "Update Collection",
|
|
34
|
+
comment: "@context: Button to save collection changes",
|
|
35
|
+
})
|
|
36
|
+
: t({
|
|
37
|
+
message: "Create Collection",
|
|
38
|
+
comment: "@context: Button to save new collection",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const cancelHref = isEdit
|
|
42
|
+
? `/dash/collections/${collection?.id}`
|
|
43
|
+
: "/dash/collections";
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<h1 class="text-2xl font-semibold mb-6">{heading}</h1>
|
|
48
|
+
|
|
49
|
+
<form
|
|
50
|
+
data-signals={signals}
|
|
51
|
+
data-on:submit__prevent={`@post('${action}')`}
|
|
52
|
+
data-indicator="_loading"
|
|
53
|
+
class="flex flex-col gap-4 max-w-lg"
|
|
54
|
+
>
|
|
55
|
+
<div class="field">
|
|
56
|
+
<label class="label">
|
|
57
|
+
{t({
|
|
58
|
+
message: "Title",
|
|
59
|
+
comment: "@context: Collection form field",
|
|
60
|
+
})}
|
|
61
|
+
</label>
|
|
62
|
+
<input
|
|
63
|
+
type="text"
|
|
64
|
+
data-bind="title"
|
|
65
|
+
class="input"
|
|
66
|
+
required
|
|
67
|
+
placeholder={
|
|
68
|
+
isEdit
|
|
69
|
+
? undefined
|
|
70
|
+
: t({
|
|
71
|
+
message: "My Collection",
|
|
72
|
+
comment: "@context: Collection title placeholder",
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="field">
|
|
79
|
+
<label class="label">
|
|
80
|
+
{t({ message: "Slug", comment: "@context: Collection form field" })}
|
|
81
|
+
</label>
|
|
82
|
+
<input
|
|
83
|
+
type="text"
|
|
84
|
+
data-bind="slug"
|
|
85
|
+
class="input"
|
|
86
|
+
required
|
|
87
|
+
pattern="[a-z0-9-]+"
|
|
88
|
+
placeholder={isEdit ? undefined : "my-collection"}
|
|
89
|
+
/>
|
|
90
|
+
{!isEdit && (
|
|
91
|
+
<p class="text-xs text-muted-foreground mt-1">
|
|
92
|
+
{t({
|
|
93
|
+
message: "URL-safe identifier (lowercase, numbers, hyphens)",
|
|
94
|
+
comment: "@context: Collection path help text",
|
|
95
|
+
})}
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="field">
|
|
101
|
+
<label class="label">
|
|
102
|
+
{t({
|
|
103
|
+
message: "Description (optional)",
|
|
104
|
+
comment: "@context: Collection form field",
|
|
105
|
+
})}
|
|
106
|
+
</label>
|
|
107
|
+
<textarea
|
|
108
|
+
data-bind="description"
|
|
109
|
+
class="textarea"
|
|
110
|
+
rows={3}
|
|
111
|
+
placeholder={
|
|
112
|
+
isEdit
|
|
113
|
+
? undefined
|
|
114
|
+
: t({
|
|
115
|
+
message: "What's this collection about?",
|
|
116
|
+
comment: "@context: Collection description placeholder",
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
>
|
|
120
|
+
{collection?.description ?? ""}
|
|
121
|
+
</textarea>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="flex gap-2">
|
|
125
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
126
|
+
<svg
|
|
127
|
+
data-show="$_loading"
|
|
128
|
+
style="display:none"
|
|
129
|
+
class="animate-spin size-4"
|
|
130
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
131
|
+
viewBox="0 0 24 24"
|
|
132
|
+
fill="none"
|
|
133
|
+
stroke="currentColor"
|
|
134
|
+
stroke-width="2"
|
|
135
|
+
stroke-linecap="round"
|
|
136
|
+
stroke-linejoin="round"
|
|
137
|
+
role="status"
|
|
138
|
+
>
|
|
139
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
140
|
+
</svg>
|
|
141
|
+
{submitLabel}
|
|
142
|
+
</button>
|
|
143
|
+
<a href={cancelHref} class="btn-outline">
|
|
144
|
+
{t({
|
|
145
|
+
message: "Cancel",
|
|
146
|
+
comment: "@context: Button to cancel form",
|
|
147
|
+
})}
|
|
148
|
+
</a>
|
|
149
|
+
</div>
|
|
150
|
+
</form>
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections list view
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import type { Collection } from "../../../types.js";
|
|
7
|
+
import {
|
|
8
|
+
EmptyState,
|
|
9
|
+
ListItemRow,
|
|
10
|
+
ActionButtons,
|
|
11
|
+
CrudPageHeader,
|
|
12
|
+
} from "../index.js";
|
|
13
|
+
|
|
14
|
+
export function CollectionsListContent({
|
|
15
|
+
collections,
|
|
16
|
+
}: {
|
|
17
|
+
collections: Collection[];
|
|
18
|
+
}) {
|
|
19
|
+
const { t } = useLingui();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<CrudPageHeader
|
|
24
|
+
title={t({
|
|
25
|
+
message: "Collections",
|
|
26
|
+
comment: "@context: Dashboard heading",
|
|
27
|
+
})}
|
|
28
|
+
ctaLabel={t({
|
|
29
|
+
message: "New Collection",
|
|
30
|
+
comment: "@context: Button to create new collection",
|
|
31
|
+
})}
|
|
32
|
+
ctaHref="/dash/collections/new"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
{collections.length === 0 ? (
|
|
36
|
+
<EmptyState
|
|
37
|
+
message={t({
|
|
38
|
+
message: "No collections yet.",
|
|
39
|
+
comment: "@context: Empty state message",
|
|
40
|
+
})}
|
|
41
|
+
ctaText={t({
|
|
42
|
+
message: "New Collection",
|
|
43
|
+
comment: "@context: Button to create new collection",
|
|
44
|
+
})}
|
|
45
|
+
ctaHref="/dash/collections/new"
|
|
46
|
+
/>
|
|
47
|
+
) : (
|
|
48
|
+
<div class="flex flex-col divide-y">
|
|
49
|
+
{collections.map((col) => (
|
|
50
|
+
<ListItemRow
|
|
51
|
+
key={col.id}
|
|
52
|
+
actions={
|
|
53
|
+
<ActionButtons
|
|
54
|
+
editHref={`/dash/collections/${col.id}/edit`}
|
|
55
|
+
editLabel={t({
|
|
56
|
+
message: "Edit",
|
|
57
|
+
comment: "@context: Button to edit collection",
|
|
58
|
+
})}
|
|
59
|
+
viewHref={`/c/${col.slug}`}
|
|
60
|
+
viewLabel={t({
|
|
61
|
+
message: "View",
|
|
62
|
+
comment: "@context: Button to view collection",
|
|
63
|
+
})}
|
|
64
|
+
/>
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
<a
|
|
68
|
+
href={`/dash/collections/${col.id}`}
|
|
69
|
+
class="font-medium hover:underline"
|
|
70
|
+
>
|
|
71
|
+
{col.title}
|
|
72
|
+
</a>
|
|
73
|
+
<p class="text-sm text-muted-foreground">/{col.slug}</p>
|
|
74
|
+
{col.description && (
|
|
75
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
76
|
+
{col.description}
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
</ListItemRow>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single collection detail view
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import type { Collection, PostView } from "../../../types.js";
|
|
7
|
+
import { ActionButtons } from "../index.js";
|
|
8
|
+
import { encode } from "../../../lib/sqid.js";
|
|
9
|
+
|
|
10
|
+
export function ViewCollectionContent({
|
|
11
|
+
collection,
|
|
12
|
+
posts,
|
|
13
|
+
}: {
|
|
14
|
+
collection: Collection;
|
|
15
|
+
posts: PostView[];
|
|
16
|
+
}) {
|
|
17
|
+
const { t } = useLingui();
|
|
18
|
+
const postsHeader = t({
|
|
19
|
+
message: "Posts in Collection ({count})",
|
|
20
|
+
comment: "@context: Collection posts section heading",
|
|
21
|
+
values: { count: String(posts.length) },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<div class="flex items-center justify-between mb-6">
|
|
27
|
+
<div>
|
|
28
|
+
<h1 class="text-2xl font-semibold">{collection.title}</h1>
|
|
29
|
+
<p class="text-sm text-muted-foreground">/{collection.slug}</p>
|
|
30
|
+
</div>
|
|
31
|
+
<ActionButtons
|
|
32
|
+
editHref={`/dash/collections/${collection.id}/edit`}
|
|
33
|
+
editLabel={t({
|
|
34
|
+
message: "Edit",
|
|
35
|
+
comment: "@context: Button to edit collection",
|
|
36
|
+
})}
|
|
37
|
+
viewHref={`/c/${collection.slug}`}
|
|
38
|
+
viewLabel={t({
|
|
39
|
+
message: "View",
|
|
40
|
+
comment: "@context: Button to view collection",
|
|
41
|
+
})}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{collection.description && (
|
|
46
|
+
<p class="text-muted-foreground mb-6">{collection.description}</p>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div class="card">
|
|
50
|
+
<header>
|
|
51
|
+
<h2>{postsHeader}</h2>
|
|
52
|
+
</header>
|
|
53
|
+
<section>
|
|
54
|
+
{posts.length === 0 ? (
|
|
55
|
+
<p class="text-muted-foreground">
|
|
56
|
+
{t({
|
|
57
|
+
message: "No posts in this collection.",
|
|
58
|
+
comment: "@context: Empty state message",
|
|
59
|
+
})}
|
|
60
|
+
</p>
|
|
61
|
+
) : (
|
|
62
|
+
<div class="flex flex-col divide-y">
|
|
63
|
+
{posts.map((post) => (
|
|
64
|
+
<div key={post.id} class="py-3 flex items-center gap-4">
|
|
65
|
+
<div class="flex-1 min-w-0">
|
|
66
|
+
<a
|
|
67
|
+
href={`/dash/posts/${encode(post.id)}`}
|
|
68
|
+
class="font-medium hover:underline"
|
|
69
|
+
>
|
|
70
|
+
{post.title ||
|
|
71
|
+
post.excerpt?.slice(0, 50) ||
|
|
72
|
+
`Post #${post.id}`}
|
|
73
|
+
</a>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</section>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="mt-6">
|
|
83
|
+
<a href="/dash/collections" class="text-sm hover:underline">
|
|
84
|
+
{t({
|
|
85
|
+
message: "\u2190 Back to Collections",
|
|
86
|
+
comment: "@context: Navigation link",
|
|
87
|
+
})}
|
|
88
|
+
</a>
|
|
89
|
+
</div>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|