@jant/core 0.3.35 → 0.3.36
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/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-8Dj-5CLW.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +3109 -2294
- package/dist/index.js +3026 -2778
- package/package.json +13 -4
- package/src/__tests__/helpers/app.ts +1 -1
- package/src/__tests__/helpers/db.ts +6 -0
- package/src/app.tsx +1 -5
- package/src/{lib → client}/avatar-upload.ts +1 -1
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
- package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +45 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/{ui → client}/components/compose-types.ts +3 -1
- package/src/{ui → client}/components/jant-collection-form.ts +301 -182
- package/src/client/components/jant-collection-sidebar.ts +801 -0
- package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
- package/src/client/components/jant-compose-editor.ts +1249 -0
- package/src/client/components/jant-compose-fullscreen.ts +338 -0
- package/src/client/components/jant-media-lightbox.ts +257 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
- package/src/{ui → client}/components/jant-post-form.ts +57 -8
- package/src/{ui → client}/components/jant-settings-general.ts +2 -2
- package/src/{ui → client}/components/nav-manager-types.ts +3 -0
- package/src/{ui → client}/components/post-form-template.ts +35 -31
- package/src/{ui → client}/components/post-form-types.ts +7 -3
- package/src/{lib → client}/compose-bridge.ts +9 -7
- package/src/client/lazy-slugify.ts +51 -0
- package/src/{lib → client}/media-upload.ts +16 -3
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/client/page-slug-bridge.ts +42 -0
- package/src/{lib → client}/post-form-bridge.ts +2 -2
- package/src/{lib → client}/settings-bridge.ts +3 -3
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +40 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +60 -0
- package/src/client/tiptap/image-node.ts +488 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +140 -0
- package/src/client/tiptap/slash-commands.ts +328 -0
- package/src/{types → client/types}/sortablejs.d.ts +1 -1
- package/src/client.ts +24 -17
- package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
- package/src/db/schema.ts +6 -1
- package/src/i18n/locales/en.po +641 -215
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +642 -204
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +642 -204
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +9 -6
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +9 -9
- package/src/lib/emoji-catalog.ts +146 -0
- package/src/lib/feed.ts +1 -1
- package/src/lib/media-helpers.ts +10 -9
- package/src/lib/render.tsx +4 -3
- package/src/lib/resolve-config.ts +8 -1
- package/src/lib/schemas.ts +2 -3
- package/src/lib/summary.ts +92 -0
- package/src/lib/timeline.ts +2 -0
- package/src/lib/tiptap-render.ts +196 -0
- package/src/lib/upload.ts +97 -9
- package/src/lib/url.ts +7 -23
- package/src/lib/view.ts +33 -19
- package/src/middleware/error-handler.ts +3 -3
- package/src/preset.css +38 -0
- package/src/routes/api/collections.ts +20 -3
- package/src/routes/api/posts.ts +48 -33
- package/src/routes/api/upload.ts +7 -5
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +26 -11
- package/src/routes/auth/signin.tsx +10 -7
- package/src/routes/compose.tsx +20 -11
- package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
- package/src/routes/dash/index.tsx +7 -1
- package/src/routes/dash/media.tsx +3 -0
- package/src/routes/dash/pages.tsx +8 -2
- package/src/routes/dash/posts.tsx +6 -2
- package/src/routes/dash/redirects.tsx +15 -9
- package/src/routes/dash/settings.tsx +336 -32
- package/src/routes/feed/__tests__/rss.test.ts +7 -7
- package/src/routes/feed/rss.ts +8 -6
- package/src/routes/pages/__tests__/featured.test.ts +6 -7
- package/src/routes/pages/archive.tsx +11 -7
- package/src/routes/pages/collection.tsx +32 -15
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +1 -1
- package/src/routes/pages/home.tsx +1 -1
- package/src/services/__tests__/post.test.ts +124 -33
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/page.ts +16 -3
- package/src/services/post.ts +96 -37
- package/src/services/search.ts +4 -2
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +240 -60
- package/src/styles/tokens.css +10 -0
- package/src/styles/ui.css +1157 -81
- package/src/types/bindings.ts +5 -0
- package/src/types/config.ts +23 -1
- package/src/types/constants.ts +3 -0
- package/src/types/entities.ts +9 -2
- package/src/types/operations.ts +9 -3
- package/src/types/props.ts +3 -3
- package/src/types/views.ts +3 -2
- package/src/ui/compose/ComposeDialog.tsx +24 -7
- package/src/ui/dash/PageForm.tsx +2 -0
- package/src/ui/dash/PostList.tsx +5 -5
- package/src/ui/dash/StatusBadge.tsx +13 -5
- package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
- package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
- package/src/ui/dash/media/MediaListContent.tsx +9 -4
- package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
- package/src/ui/dash/pages/PagesContent.tsx +2 -1
- package/src/ui/dash/posts/PostForm.tsx +19 -7
- package/src/ui/dash/settings/AccountContent.tsx +133 -138
- package/src/ui/dash/settings/AvatarContent.tsx +70 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
- package/src/ui/layouts/DashLayout.tsx +157 -75
- package/src/ui/layouts/SiteLayout.tsx +13 -13
- package/src/ui/pages/ArchivePage.tsx +10 -7
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/SearchPage.tsx +1 -1
- package/src/ui/shared/CollectionsSidebar.tsx +228 -3
- package/src/ui/shared/MediaGallery.tsx +179 -41
- package/src/lib/collections-reorder.ts +0 -28
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
- /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
- /package/src/{ui → client}/components/settings-types.ts +0 -0
- /package/src/{lib → client}/image-processor.ts +0 -0
- /package/src/{lib → client}/toast.ts +0 -0
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collection Page
|
|
3
3
|
*
|
|
4
|
-
* Collection header with icon and
|
|
4
|
+
* Collection header with icon and timeline feed of posts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
8
|
import { useLingui } from "@lingui/react/macro";
|
|
9
9
|
import type { CollectionPageProps } from "../../types.js";
|
|
10
10
|
import { renderCollectionIcon } from "../../lib/icons.js";
|
|
11
|
+
import { TimelineFeed } from "../feed/TimelineFeed.js";
|
|
11
12
|
|
|
12
13
|
export const CollectionPage: FC<CollectionPageProps> = ({
|
|
13
14
|
collection,
|
|
14
|
-
|
|
15
|
+
items,
|
|
15
16
|
}) => {
|
|
16
17
|
const { t } = useLingui();
|
|
17
18
|
const iconHtml = renderCollectionIcon(collection.icon, { size: 28 });
|
|
@@ -34,45 +35,15 @@ export const CollectionPage: FC<CollectionPageProps> = ({
|
|
|
34
35
|
</header>
|
|
35
36
|
|
|
36
37
|
<main>
|
|
37
|
-
{
|
|
38
|
+
{items.length === 0 ? (
|
|
38
39
|
<p class="text-muted-foreground">
|
|
39
40
|
{t({
|
|
40
|
-
message: "
|
|
41
|
+
message: "This collection is empty. Add posts from the editor.",
|
|
41
42
|
comment: "@context: Empty state message",
|
|
42
43
|
})}
|
|
43
44
|
</p>
|
|
44
45
|
) : (
|
|
45
|
-
<
|
|
46
|
-
{posts.map((post) => (
|
|
47
|
-
<article
|
|
48
|
-
key={post.id}
|
|
49
|
-
class="h-entry py-4"
|
|
50
|
-
data-post
|
|
51
|
-
data-format={post.format}
|
|
52
|
-
>
|
|
53
|
-
{post.title && (
|
|
54
|
-
<h2 class="p-name text-lg font-medium mb-2">
|
|
55
|
-
<a href={post.permalink} class="u-url hover:underline">
|
|
56
|
-
{post.title}
|
|
57
|
-
</a>
|
|
58
|
-
</h2>
|
|
59
|
-
)}
|
|
60
|
-
<div
|
|
61
|
-
class="e-content prose prose-sm"
|
|
62
|
-
data-post-body
|
|
63
|
-
dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
|
|
64
|
-
/>
|
|
65
|
-
<footer
|
|
66
|
-
class="mt-2 text-sm text-muted-foreground"
|
|
67
|
-
data-post-meta
|
|
68
|
-
>
|
|
69
|
-
<time class="dt-published" datetime={post.publishedAt}>
|
|
70
|
-
{post.publishedAtFormatted}
|
|
71
|
-
</time>
|
|
72
|
-
</footer>
|
|
73
|
-
</article>
|
|
74
|
-
))}
|
|
75
|
-
</div>
|
|
46
|
+
<TimelineFeed items={items} />
|
|
76
47
|
)}
|
|
77
48
|
</main>
|
|
78
49
|
</div>
|
|
@@ -27,7 +27,8 @@ export const CollectionsPage: FC<CollectionsPageProps> = ({ collections }) => {
|
|
|
27
27
|
{collections.length === 0 ? (
|
|
28
28
|
<p class="text-muted-foreground">
|
|
29
29
|
{t({
|
|
30
|
-
message:
|
|
30
|
+
message:
|
|
31
|
+
"No collections yet. Start one to organize posts by topic.",
|
|
31
32
|
comment: "@context: Empty state message on collections page",
|
|
32
33
|
})}
|
|
33
34
|
</p>
|
|
@@ -18,7 +18,8 @@ export const FeaturedPage: FC<FeaturedPageProps> = ({ items }) => {
|
|
|
18
18
|
{items.length === 0 ? (
|
|
19
19
|
<p class="text-muted-foreground">
|
|
20
20
|
{t({
|
|
21
|
-
message:
|
|
21
|
+
message:
|
|
22
|
+
"No featured posts. Mark a post as featured to highlight it here.",
|
|
22
23
|
comment: "@context: Empty state message on featured page",
|
|
23
24
|
})}
|
|
24
25
|
</p>
|
|
@@ -62,7 +62,7 @@ export const SearchPage: FC<SearchPageProps> = ({
|
|
|
62
62
|
<p class="text-sm text-muted-foreground mb-4">
|
|
63
63
|
{results.length === 0
|
|
64
64
|
? t({
|
|
65
|
-
message: "No results
|
|
65
|
+
message: "No results. Try different keywords.",
|
|
66
66
|
comment: "@context: Search empty results",
|
|
67
67
|
})
|
|
68
68
|
: results.length === 1
|
|
@@ -2,25 +2,74 @@
|
|
|
2
2
|
* Collections Sidebar
|
|
3
3
|
*
|
|
4
4
|
* Shared sidebar navigation for public collection pages.
|
|
5
|
-
*
|
|
5
|
+
* - Anonymous users: static nav with collections and dividers
|
|
6
|
+
* - Authenticated users: interactive Lit component with CRUD, reorder, divider management
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { FC } from "hono/jsx";
|
|
9
10
|
import { useLingui } from "@lingui/react/macro";
|
|
10
|
-
import type { Collection } from "../../types.js";
|
|
11
|
+
import type { Collection, CollectionDivider } from "../../types.js";
|
|
11
12
|
import { renderCollectionIcon } from "../../lib/icons.js";
|
|
12
13
|
|
|
14
|
+
const escapeJson = (data: unknown) =>
|
|
15
|
+
JSON.stringify(data).replace(/</g, "\\u003c");
|
|
16
|
+
|
|
13
17
|
export interface CollectionsSidebarProps {
|
|
14
18
|
collections: Collection[];
|
|
19
|
+
dividers: CollectionDivider[];
|
|
15
20
|
activeSlug?: string;
|
|
21
|
+
isAuthenticated?: boolean;
|
|
22
|
+
postCounts?: Map<number, number>;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export const CollectionsSidebar: FC<CollectionsSidebarProps> = ({
|
|
19
26
|
collections,
|
|
27
|
+
dividers,
|
|
20
28
|
activeSlug,
|
|
29
|
+
isAuthenticated,
|
|
30
|
+
postCounts,
|
|
21
31
|
}) => {
|
|
32
|
+
if (isAuthenticated) {
|
|
33
|
+
return (
|
|
34
|
+
<AuthenticatedSidebar
|
|
35
|
+
collections={collections}
|
|
36
|
+
dividers={dividers}
|
|
37
|
+
activeSlug={activeSlug}
|
|
38
|
+
postCounts={postCounts}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<AnonymousSidebar
|
|
45
|
+
collections={collections}
|
|
46
|
+
dividers={dividers}
|
|
47
|
+
activeSlug={activeSlug}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Anonymous: static HTML nav
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const AnonymousSidebar: FC<{
|
|
57
|
+
collections: Collection[];
|
|
58
|
+
dividers: CollectionDivider[];
|
|
59
|
+
activeSlug?: string;
|
|
60
|
+
}> = ({ collections, dividers, activeSlug }) => {
|
|
22
61
|
const { t } = useLingui();
|
|
23
62
|
|
|
63
|
+
// Interleave collections and dividers by position
|
|
64
|
+
type Item =
|
|
65
|
+
| { kind: "collection"; data: Collection }
|
|
66
|
+
| { kind: "divider"; data: CollectionDivider };
|
|
67
|
+
|
|
68
|
+
const items: Item[] = [
|
|
69
|
+
...collections.map((c) => ({ kind: "collection" as const, data: c })),
|
|
70
|
+
...dividers.map((d) => ({ kind: "divider" as const, data: d })),
|
|
71
|
+
].sort((a, b) => a.data.position - b.data.position);
|
|
72
|
+
|
|
24
73
|
return (
|
|
25
74
|
<nav class="flex flex-col gap-1 pt-6">
|
|
26
75
|
<h2 class="px-3 pb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
@@ -29,7 +78,15 @@ export const CollectionsSidebar: FC<CollectionsSidebarProps> = ({
|
|
|
29
78
|
comment: "@context: Sidebar heading for collections nav",
|
|
30
79
|
})}
|
|
31
80
|
</h2>
|
|
32
|
-
{
|
|
81
|
+
{items.map((item) => {
|
|
82
|
+
if (item.kind === "divider") {
|
|
83
|
+
return (
|
|
84
|
+
<div key={`d-${item.data.id}`} class="px-3 py-1">
|
|
85
|
+
<hr class="border-border" />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const col = item.data;
|
|
33
90
|
const isActive = col.slug === activeSlug;
|
|
34
91
|
return (
|
|
35
92
|
<a
|
|
@@ -57,3 +114,171 @@ export const CollectionsSidebar: FC<CollectionsSidebarProps> = ({
|
|
|
57
114
|
</nav>
|
|
58
115
|
);
|
|
59
116
|
};
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Authenticated: Lit component shell
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const AuthenticatedSidebar: FC<{
|
|
123
|
+
collections: Collection[];
|
|
124
|
+
dividers: CollectionDivider[];
|
|
125
|
+
activeSlug?: string;
|
|
126
|
+
postCounts?: Map<number, number>;
|
|
127
|
+
}> = ({ collections, dividers, activeSlug, postCounts }) => {
|
|
128
|
+
const { t } = useLingui();
|
|
129
|
+
|
|
130
|
+
const sidebarCollections = collections.map((col) => ({
|
|
131
|
+
id: col.id,
|
|
132
|
+
slug: col.slug,
|
|
133
|
+
title: col.title,
|
|
134
|
+
description: col.description,
|
|
135
|
+
icon: col.icon,
|
|
136
|
+
sortOrder: col.sortOrder,
|
|
137
|
+
position: col.position,
|
|
138
|
+
postCount: postCounts?.get(col.id) ?? 0,
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
const labels = {
|
|
142
|
+
collections: t({
|
|
143
|
+
message: "Collections",
|
|
144
|
+
comment: "@context: Sidebar heading for collections nav",
|
|
145
|
+
}),
|
|
146
|
+
reorder: t({
|
|
147
|
+
message: "Reorder",
|
|
148
|
+
comment: "@context: Menu action to reorder collections",
|
|
149
|
+
}),
|
|
150
|
+
done: t({
|
|
151
|
+
message: "Done",
|
|
152
|
+
comment: "@context: Button to exit reorder mode",
|
|
153
|
+
}),
|
|
154
|
+
addDivider: t({
|
|
155
|
+
message: "Add Divider",
|
|
156
|
+
comment: "@context: Menu action to add a divider",
|
|
157
|
+
}),
|
|
158
|
+
newCollection: t({
|
|
159
|
+
message: "New Collection",
|
|
160
|
+
comment: "@context: Tooltip/aria for add collection button",
|
|
161
|
+
}),
|
|
162
|
+
edit: t({
|
|
163
|
+
message: "Edit",
|
|
164
|
+
comment: "@context: Per-collection edit action",
|
|
165
|
+
}),
|
|
166
|
+
deleteDivider: t({
|
|
167
|
+
message: "Remove Divider",
|
|
168
|
+
comment: "@context: Tooltip for divider delete button",
|
|
169
|
+
}),
|
|
170
|
+
moreActions: t({
|
|
171
|
+
message: "More actions",
|
|
172
|
+
comment: "@context: Aria-label for more button",
|
|
173
|
+
}),
|
|
174
|
+
deleteCollection: t({
|
|
175
|
+
message: "Delete",
|
|
176
|
+
comment: "@context: Delete collection action",
|
|
177
|
+
}),
|
|
178
|
+
confirmDelete: t({
|
|
179
|
+
message:
|
|
180
|
+
"Delete this collection permanently? Posts inside won't be removed.",
|
|
181
|
+
comment: "@context: Confirm dialog for deleting a collection",
|
|
182
|
+
}),
|
|
183
|
+
orderSaved: t({
|
|
184
|
+
message: "Order saved",
|
|
185
|
+
comment: "@context: Toast after reordering collections",
|
|
186
|
+
}),
|
|
187
|
+
saved: t({
|
|
188
|
+
message: "Saved",
|
|
189
|
+
comment: "@context: Toast after saving a collection",
|
|
190
|
+
}),
|
|
191
|
+
saveFailed: t({
|
|
192
|
+
message: "Couldn't save. Try again in a moment.",
|
|
193
|
+
comment: "@context: Toast when save fails",
|
|
194
|
+
}),
|
|
195
|
+
deleted: t({
|
|
196
|
+
message: "Deleted",
|
|
197
|
+
comment: "@context: Toast after deleting a collection",
|
|
198
|
+
}),
|
|
199
|
+
formLabels: {
|
|
200
|
+
titleLabel: t({
|
|
201
|
+
message: "Title",
|
|
202
|
+
comment: "@context: Collection form field",
|
|
203
|
+
}),
|
|
204
|
+
titlePlaceholder: t({
|
|
205
|
+
message: "My Collection",
|
|
206
|
+
comment: "@context: Collection title placeholder",
|
|
207
|
+
}),
|
|
208
|
+
slugLabel: t({
|
|
209
|
+
message: "Slug",
|
|
210
|
+
comment: "@context: Collection form field",
|
|
211
|
+
}),
|
|
212
|
+
slugHelp: t({
|
|
213
|
+
message:
|
|
214
|
+
"URL-safe identifier (lowercase, numbers, hyphens). For CJK titles, slug will be auto-generated on the server.",
|
|
215
|
+
comment: "@context: Collection path help text",
|
|
216
|
+
}),
|
|
217
|
+
descriptionLabel: t({
|
|
218
|
+
message: "Description (optional)",
|
|
219
|
+
comment: "@context: Collection form field",
|
|
220
|
+
}),
|
|
221
|
+
descriptionPlaceholder: t({
|
|
222
|
+
message: "What's this collection about?",
|
|
223
|
+
comment: "@context: Collection description placeholder",
|
|
224
|
+
}),
|
|
225
|
+
removeIcon: t({
|
|
226
|
+
message: "Remove",
|
|
227
|
+
comment: "@context: Button to remove icon",
|
|
228
|
+
}),
|
|
229
|
+
iconsTab: t({
|
|
230
|
+
message: "Icons",
|
|
231
|
+
comment: "@context: Icon picker tab label",
|
|
232
|
+
}),
|
|
233
|
+
emojisTab: t({
|
|
234
|
+
message: "Emojis",
|
|
235
|
+
comment: "@context: Emoji picker tab label",
|
|
236
|
+
}),
|
|
237
|
+
searchIconsPlaceholder: t({
|
|
238
|
+
message: "Search icons...",
|
|
239
|
+
comment: "@context: Icon picker search placeholder",
|
|
240
|
+
}),
|
|
241
|
+
searchEmojisPlaceholder: t({
|
|
242
|
+
message: "Search emojis...",
|
|
243
|
+
comment: "@context: Emoji picker search placeholder",
|
|
244
|
+
}),
|
|
245
|
+
sortOrderLabel: t({
|
|
246
|
+
message: "Sort Order",
|
|
247
|
+
comment: "@context: Collection form field",
|
|
248
|
+
}),
|
|
249
|
+
sortNewest: t({
|
|
250
|
+
message: "Newest first",
|
|
251
|
+
comment: "@context: Collection sort order option",
|
|
252
|
+
}),
|
|
253
|
+
sortOldest: t({
|
|
254
|
+
message: "Oldest first",
|
|
255
|
+
comment: "@context: Collection sort order option",
|
|
256
|
+
}),
|
|
257
|
+
sortRatingDesc: t({
|
|
258
|
+
message: "Highest rated",
|
|
259
|
+
comment: "@context: Collection sort order option",
|
|
260
|
+
}),
|
|
261
|
+
sortRatingAsc: t({
|
|
262
|
+
message: "Lowest rated",
|
|
263
|
+
comment: "@context: Collection sort order option",
|
|
264
|
+
}),
|
|
265
|
+
submitLabel: t({
|
|
266
|
+
message: "Save",
|
|
267
|
+
comment: "@context: Button to save collection",
|
|
268
|
+
}),
|
|
269
|
+
cancelLabel: t({
|
|
270
|
+
message: "Cancel",
|
|
271
|
+
comment: "@context: Button to cancel form",
|
|
272
|
+
}),
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<jant-collection-sidebar
|
|
278
|
+
collections={escapeJson(sidebarCollections)}
|
|
279
|
+
dividers={escapeJson(dividers)}
|
|
280
|
+
labels={escapeJson(labels)}
|
|
281
|
+
active-slug={activeSlug ?? ""}
|
|
282
|
+
/>
|
|
283
|
+
);
|
|
284
|
+
};
|
|
@@ -1,59 +1,197 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Media Gallery Component
|
|
3
3
|
*
|
|
4
|
-
* Renders media attachments in a horizontal scrollable row
|
|
5
|
-
*
|
|
4
|
+
* Renders media attachments: images in a horizontal scrollable row
|
|
5
|
+
* (with lightbox support), videos inline with play overlay, audio
|
|
6
|
+
* as compact player cards, and PDFs as file cards linking to the file.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { FC } from "hono/jsx";
|
|
9
10
|
import type { MediaView } from "../../types.js";
|
|
11
|
+
import { getMediaCategory } from "../../lib/upload.js";
|
|
10
12
|
|
|
11
13
|
export interface MediaGalleryProps {
|
|
12
14
|
attachments: MediaView[];
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
function formatSize(bytes: number): string {
|
|
18
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
19
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
20
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
export const MediaGallery: FC<MediaGalleryProps> = ({ attachments }) => {
|
|
16
|
-
|
|
17
|
-
if (images.length === 0) return null;
|
|
24
|
+
if (attachments.length === 0) return null;
|
|
18
25
|
|
|
19
|
-
const
|
|
26
|
+
const images = attachments.filter(
|
|
27
|
+
(a) => getMediaCategory(a.mimeType) === "image",
|
|
28
|
+
);
|
|
29
|
+
const videos = attachments.filter(
|
|
30
|
+
(a) => getMediaCategory(a.mimeType) === "video",
|
|
31
|
+
);
|
|
32
|
+
const audios = attachments.filter(
|
|
33
|
+
(a) => getMediaCategory(a.mimeType) === "audio",
|
|
34
|
+
);
|
|
35
|
+
const documents = attachments.filter(
|
|
36
|
+
(a) => getMediaCategory(a.mimeType) === "document",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Build lightbox group from images + videos
|
|
40
|
+
const lightboxItems = [
|
|
41
|
+
...images.map((img) => ({
|
|
42
|
+
url: img.url,
|
|
43
|
+
alt: img.altText || "",
|
|
44
|
+
width: img.width,
|
|
45
|
+
height: img.height,
|
|
46
|
+
})),
|
|
47
|
+
...videos.map((v) => ({
|
|
48
|
+
url: v.url,
|
|
49
|
+
alt: v.altText || "",
|
|
50
|
+
width: v.width,
|
|
51
|
+
height: v.height,
|
|
52
|
+
mimeType: v.mimeType,
|
|
53
|
+
})),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const hasVisualMedia = images.length > 0 || videos.length > 0;
|
|
57
|
+
const singleVisual = images.length + videos.length === 1;
|
|
20
58
|
|
|
21
59
|
return (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
<>
|
|
61
|
+
{/* Images + Videos gallery */}
|
|
62
|
+
{hasVisualMedia && (
|
|
63
|
+
<div
|
|
64
|
+
data-post-media
|
|
65
|
+
data-lightbox-group={JSON.stringify(lightboxItems)}
|
|
66
|
+
class={`mt-3 flex gap-2 ${singleVisual ? "" : "overflow-x-auto scroll-smooth snap-x snap-mandatory"}`}
|
|
67
|
+
style={
|
|
68
|
+
singleVisual
|
|
69
|
+
? undefined
|
|
70
|
+
: "scrollbar-width: none; -ms-overflow-style: none;"
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
{images.map((img, index) => {
|
|
74
|
+
const aspectRatio =
|
|
75
|
+
img.width && img.height ? img.width / img.height : 4 / 3;
|
|
76
|
+
const itemWidth = singleVisual
|
|
77
|
+
? undefined
|
|
78
|
+
: `${Math.round(320 * Math.min(Math.max(aspectRatio, 0.6), 1.6))}px`;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<a
|
|
82
|
+
key={img.id}
|
|
83
|
+
href={img.url}
|
|
84
|
+
data-lightbox-index={index}
|
|
85
|
+
class={`${singleVisual ? "" : "shrink-0 snap-start"} block rounded-lg overflow-hidden`}
|
|
86
|
+
style={
|
|
87
|
+
singleVisual
|
|
88
|
+
? undefined
|
|
89
|
+
: { width: itemWidth, maxWidth: "85%" }
|
|
90
|
+
}
|
|
91
|
+
>
|
|
92
|
+
<img
|
|
93
|
+
src={img.thumbnailUrl}
|
|
94
|
+
alt={img.altText || ""}
|
|
95
|
+
class={
|
|
96
|
+
singleVisual
|
|
97
|
+
? "rounded-lg max-w-full max-h-96 h-auto object-contain"
|
|
98
|
+
: "h-80 w-full object-cover"
|
|
99
|
+
}
|
|
100
|
+
loading="lazy"
|
|
101
|
+
/>
|
|
102
|
+
</a>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
{videos.map((v, vIdx) => {
|
|
106
|
+
const lightboxIndex = images.length + vIdx;
|
|
107
|
+
return (
|
|
108
|
+
<a
|
|
109
|
+
key={v.id}
|
|
110
|
+
href={v.url}
|
|
111
|
+
data-lightbox-index={lightboxIndex}
|
|
112
|
+
class={`${singleVisual ? "" : "shrink-0 snap-start"} media-video-wrap`}
|
|
113
|
+
style={
|
|
114
|
+
singleVisual ? undefined : { width: "320px", maxWidth: "85%" }
|
|
115
|
+
}
|
|
116
|
+
>
|
|
117
|
+
<video
|
|
118
|
+
src={v.url}
|
|
119
|
+
preload="metadata"
|
|
120
|
+
muted
|
|
121
|
+
playsinline
|
|
122
|
+
class={singleVisual ? "max-h-96" : "h-80 w-full object-cover"}
|
|
123
|
+
/>
|
|
124
|
+
<div class="media-video-play-overlay">
|
|
125
|
+
<svg viewBox="0 0 24 24" fill="white">
|
|
126
|
+
<path d="M8 5v14l11-7z" />
|
|
127
|
+
</svg>
|
|
128
|
+
</div>
|
|
129
|
+
</a>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* Audio cards */}
|
|
136
|
+
{audios.map((a) => (
|
|
137
|
+
<div key={a.id} class="media-audio-card">
|
|
138
|
+
<div class="media-audio-icon">
|
|
139
|
+
<svg
|
|
140
|
+
width="20"
|
|
141
|
+
height="20"
|
|
142
|
+
viewBox="0 0 24 24"
|
|
143
|
+
fill="none"
|
|
144
|
+
stroke="currentColor"
|
|
145
|
+
stroke-width="1.5"
|
|
146
|
+
stroke-linecap="round"
|
|
147
|
+
stroke-linejoin="round"
|
|
148
|
+
>
|
|
149
|
+
<path d="M9 18V5l12-2v13" />
|
|
150
|
+
<circle cx="6" cy="18" r="3" />
|
|
151
|
+
<circle cx="18" cy="16" r="3" />
|
|
152
|
+
</svg>
|
|
153
|
+
</div>
|
|
154
|
+
{a.altText && <span class="media-audio-name">{a.altText}</span>}
|
|
155
|
+
<div class="media-audio-player">
|
|
156
|
+
<audio controls preload="metadata">
|
|
157
|
+
<source src={a.url} type={a.mimeType} />
|
|
158
|
+
</audio>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
))}
|
|
162
|
+
|
|
163
|
+
{/* PDF cards */}
|
|
164
|
+
{documents.map((d) => (
|
|
165
|
+
<a
|
|
166
|
+
key={d.id}
|
|
167
|
+
href={d.url}
|
|
168
|
+
target="_blank"
|
|
169
|
+
rel="noopener noreferrer"
|
|
170
|
+
class="media-pdf-card"
|
|
171
|
+
>
|
|
172
|
+
<div class="media-pdf-icon">
|
|
173
|
+
<svg
|
|
174
|
+
width="20"
|
|
175
|
+
height="20"
|
|
176
|
+
viewBox="0 0 24 24"
|
|
177
|
+
fill="none"
|
|
178
|
+
stroke="currentColor"
|
|
179
|
+
stroke-width="1.5"
|
|
180
|
+
stroke-linecap="round"
|
|
181
|
+
stroke-linejoin="round"
|
|
182
|
+
>
|
|
183
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
184
|
+
<polyline points="14 2 14 8 20 8" />
|
|
185
|
+
<line x1="16" y1="13" x2="8" y2="13" />
|
|
186
|
+
<line x1="16" y1="17" x2="8" y2="17" />
|
|
187
|
+
</svg>
|
|
188
|
+
</div>
|
|
189
|
+
<span class="media-pdf-name">{d.altText || "PDF"}</span>
|
|
190
|
+
{d.size != null && (
|
|
191
|
+
<span class="media-pdf-size">{formatSize(d.size)}</span>
|
|
192
|
+
)}
|
|
193
|
+
</a>
|
|
194
|
+
))}
|
|
195
|
+
</>
|
|
58
196
|
);
|
|
59
197
|
};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Collection Reorder
|
|
3
|
-
*
|
|
4
|
-
* Initializes SortableJS on the collections list in the dashboard.
|
|
5
|
-
* Auto-detects the list element and only activates when present.
|
|
6
|
-
* Sends prefixed string IDs (e.g. "c-1", "d-2") to support mixed
|
|
7
|
-
* collections and dividers in a unified sort order.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import Sortable from "sortablejs";
|
|
11
|
-
|
|
12
|
-
const list = document.getElementById("collections-list");
|
|
13
|
-
if (list) {
|
|
14
|
-
Sortable.create(list, {
|
|
15
|
-
animation: 150,
|
|
16
|
-
handle: "[data-id]",
|
|
17
|
-
onEnd() {
|
|
18
|
-
const items = [...list.querySelectorAll<HTMLElement>("[data-id]")]
|
|
19
|
-
.map((el) => el.dataset.id)
|
|
20
|
-
.filter((id): id is string => id !== undefined);
|
|
21
|
-
fetch("/dash/collections/reorder", {
|
|
22
|
-
method: "POST",
|
|
23
|
-
headers: { "Content-Type": "application/json" },
|
|
24
|
-
body: JSON.stringify({ items }),
|
|
25
|
-
});
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
}
|