@jant/core 0.3.36 → 0.3.38
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/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -145
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/schemas.test.ts +181 -63
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +51 -42
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FC } from "hono/jsx";
|
|
2
|
+
import { useLingui } from "@lingui/react/macro";
|
|
3
|
+
import type { Collection } from "../../types.js";
|
|
4
|
+
import { ComposeForm } from "../compose/ComposeDialog.js";
|
|
5
|
+
|
|
6
|
+
export interface ComposePageProps {
|
|
7
|
+
collections?: Collection[];
|
|
8
|
+
uploadMaxFileSize?: number;
|
|
9
|
+
closeHref?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ComposePage: FC<ComposePageProps> = ({
|
|
13
|
+
collections,
|
|
14
|
+
uploadMaxFileSize,
|
|
15
|
+
closeHref = "/",
|
|
16
|
+
}) => {
|
|
17
|
+
const { t } = useLingui();
|
|
18
|
+
const backLabel = t({
|
|
19
|
+
message: "Back",
|
|
20
|
+
comment: "@context: Link back from the new post page",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section class="compose-page" data-page="compose">
|
|
25
|
+
<div class="compose-page-shell">
|
|
26
|
+
<div class="compose-page-intro">
|
|
27
|
+
<div class="compose-page-intro-row">
|
|
28
|
+
<h1 class="compose-page-title">
|
|
29
|
+
{t({
|
|
30
|
+
message: "New post",
|
|
31
|
+
comment: "@context: Page title for the new post page",
|
|
32
|
+
})}
|
|
33
|
+
</h1>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
class="compose-page-back-link"
|
|
37
|
+
aria-label={backLabel}
|
|
38
|
+
data-on:click="el.closest('.compose-page-shell')?.querySelector('jant-compose-dialog')?.requestCloseAndLeave()"
|
|
39
|
+
>
|
|
40
|
+
<span>{`← ${backLabel}`}</span>
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<ComposeForm
|
|
45
|
+
collections={collections}
|
|
46
|
+
uploadMaxFileSize={uploadMaxFileSize}
|
|
47
|
+
pageMode
|
|
48
|
+
closeHref={closeHref}
|
|
49
|
+
autoRestoreDraft
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -2,55 +2,40 @@
|
|
|
2
2
|
* Single Post Page
|
|
3
3
|
*
|
|
4
4
|
* Single post view — clean, no card border, with divider footer.
|
|
5
|
+
* When `threadPosts` is provided, renders the full thread with the current
|
|
6
|
+
* post highlighted and scroll-targeted.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import type { FC } from "hono/jsx";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import { MediaGallery } from "../shared/MediaGallery.js";
|
|
11
|
-
|
|
12
|
-
export const PostPage: FC<PostPageProps> = ({ post }) => {
|
|
13
|
-
const { t } = useLingui();
|
|
10
|
+
import type { PostPageProps, PostView } from "../../types.js";
|
|
11
|
+
import { TimelineItemFromPost } from "../feed/TimelineItem.js";
|
|
14
12
|
|
|
13
|
+
const ThreadDetail: FC<{ post: PostView; threadPosts: PostView[] }> = ({
|
|
14
|
+
post,
|
|
15
|
+
threadPosts,
|
|
16
|
+
}) => {
|
|
15
17
|
return (
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/>
|
|
32
|
-
)}
|
|
33
|
-
|
|
34
|
-
{post.media.length > 0 && (
|
|
35
|
-
<div class="mt-4" data-post-media>
|
|
36
|
-
<MediaGallery attachments={post.media} />
|
|
37
|
-
</div>
|
|
38
|
-
)}
|
|
39
|
-
|
|
40
|
-
<footer
|
|
41
|
-
class="mt-6 pt-4 border-t text-sm text-muted-foreground"
|
|
42
|
-
data-post-meta
|
|
43
|
-
>
|
|
44
|
-
<time class="dt-published" datetime={post.publishedAt}>
|
|
45
|
-
{post.publishedAtFormatted}
|
|
46
|
-
</time>
|
|
47
|
-
<a href={post.permalink} class="u-url ml-4">
|
|
48
|
-
{t({
|
|
49
|
-
message: "Permalink",
|
|
50
|
-
comment: "@context: Link to permanent URL of post",
|
|
51
|
-
})}
|
|
52
|
-
</a>
|
|
53
|
-
</footer>
|
|
54
|
-
</article>
|
|
18
|
+
<div class="thread-group thread-group-detail" data-page="post">
|
|
19
|
+
{threadPosts.map((tp) => {
|
|
20
|
+
const isCurrent = tp.id === post.id;
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
key={tp.id}
|
|
24
|
+
id={`post-${tp.id}`}
|
|
25
|
+
class={`thread-item thread-detail-item${isCurrent ? " thread-item-current" : ""}`}
|
|
26
|
+
{...(isCurrent ? { "data-post-current": "" } : {})}
|
|
27
|
+
>
|
|
28
|
+
<TimelineItemFromPost post={tp} />
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
})}
|
|
32
|
+
</div>
|
|
55
33
|
);
|
|
56
34
|
};
|
|
35
|
+
|
|
36
|
+
export const PostPage: FC<PostPageProps> = ({ post, threadPosts }) => {
|
|
37
|
+
if (threadPosts && threadPosts.length > 1) {
|
|
38
|
+
return <ThreadDetail post={post} threadPosts={threadPosts} />;
|
|
39
|
+
}
|
|
40
|
+
return <TimelineItemFromPost post={post} mode="detail" />;
|
|
41
|
+
};
|
|
@@ -1,14 +1,184 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Search Page
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Dedicated search result UI — compact per-type cards, not full timeline cards.
|
|
5
|
+
* Each card shows only what's relevant: title/domain/quote + FTS snippet.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { FC } from "hono/jsx";
|
|
8
9
|
import { useLingui } from "@lingui/react/macro";
|
|
9
|
-
import type { SearchPageProps } from "../../types.js";
|
|
10
|
+
import type { SearchPageProps, SearchResultView } from "../../types.js";
|
|
10
11
|
import { PagePagination } from "../shared/Pagination.js";
|
|
11
12
|
|
|
13
|
+
// External link icon (shared by LinkCard)
|
|
14
|
+
const ExternalLinkIcon = () => (
|
|
15
|
+
<svg
|
|
16
|
+
class="size-3 shrink-0"
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
fill="none"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
stroke-width="2"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
>
|
|
23
|
+
<path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const SearchResultCard: FC<{ result: SearchResultView }> = ({ result }) => {
|
|
28
|
+
const { post, snippet, titleHighlighted, quoteHighlighted } = result;
|
|
29
|
+
|
|
30
|
+
// Extract domain for link posts
|
|
31
|
+
let domain: string | undefined;
|
|
32
|
+
if (post.format === "link" && post.url) {
|
|
33
|
+
try {
|
|
34
|
+
domain = new URL(post.url).hostname.replace(/^www\./, "");
|
|
35
|
+
} catch {
|
|
36
|
+
// Invalid URL, skip
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const footer = (
|
|
41
|
+
<footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
|
|
42
|
+
<span class="badge-outline">{post.format}</span>
|
|
43
|
+
<a href={post.permalink} class="hover:underline">
|
|
44
|
+
<time datetime={post.publishedAt}>{post.publishedAtFormatted}</time>
|
|
45
|
+
</a>
|
|
46
|
+
</footer>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// ── Link ──────────────────────────────────────────────────────────────────
|
|
50
|
+
if (post.format === "link") {
|
|
51
|
+
return (
|
|
52
|
+
<article data-post data-format="link">
|
|
53
|
+
{domain && (
|
|
54
|
+
<div class="flex items-center gap-1 text-xs text-muted-foreground mb-1">
|
|
55
|
+
<ExternalLinkIcon />
|
|
56
|
+
<span>{domain}</span>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
{(titleHighlighted ?? post.title) && (
|
|
60
|
+
<h3 class="font-semibold text-base mb-1">
|
|
61
|
+
{titleHighlighted ? (
|
|
62
|
+
<a
|
|
63
|
+
href={post.url || post.permalink}
|
|
64
|
+
target={post.url ? "_blank" : undefined}
|
|
65
|
+
rel={post.url ? "noopener noreferrer" : undefined}
|
|
66
|
+
class="hover:underline"
|
|
67
|
+
dangerouslySetInnerHTML={{ __html: titleHighlighted }}
|
|
68
|
+
/>
|
|
69
|
+
) : (
|
|
70
|
+
<a
|
|
71
|
+
href={post.url || post.permalink}
|
|
72
|
+
target={post.url ? "_blank" : undefined}
|
|
73
|
+
rel={post.url ? "noopener noreferrer" : undefined}
|
|
74
|
+
class="hover:underline"
|
|
75
|
+
>
|
|
76
|
+
{post.title}
|
|
77
|
+
</a>
|
|
78
|
+
)}
|
|
79
|
+
</h3>
|
|
80
|
+
)}
|
|
81
|
+
{snippet && (
|
|
82
|
+
<p
|
|
83
|
+
class="search-snippet"
|
|
84
|
+
dangerouslySetInnerHTML={{ __html: snippet }}
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
{footer}
|
|
88
|
+
</article>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Quote ─────────────────────────────────────────────────────────────────
|
|
93
|
+
if (post.format === "quote") {
|
|
94
|
+
return (
|
|
95
|
+
<article data-post data-format="quote">
|
|
96
|
+
{quoteHighlighted && (
|
|
97
|
+
<blockquote class="feed-quote mb-1">
|
|
98
|
+
<p
|
|
99
|
+
class="text-sm"
|
|
100
|
+
dangerouslySetInnerHTML={{ __html: quoteHighlighted }}
|
|
101
|
+
/>
|
|
102
|
+
{post.title && (
|
|
103
|
+
<footer class="text-xs text-muted-foreground mt-1">
|
|
104
|
+
{post.url ? (
|
|
105
|
+
<a
|
|
106
|
+
href={post.url}
|
|
107
|
+
target="_blank"
|
|
108
|
+
rel="noopener noreferrer"
|
|
109
|
+
class="hover:underline"
|
|
110
|
+
>
|
|
111
|
+
— {post.title}
|
|
112
|
+
</a>
|
|
113
|
+
) : (
|
|
114
|
+
<span>— {post.title}</span>
|
|
115
|
+
)}
|
|
116
|
+
</footer>
|
|
117
|
+
)}
|
|
118
|
+
</blockquote>
|
|
119
|
+
)}
|
|
120
|
+
{snippet && (
|
|
121
|
+
<p
|
|
122
|
+
class="search-snippet"
|
|
123
|
+
dangerouslySetInnerHTML={{ __html: snippet }}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
{footer}
|
|
127
|
+
</article>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Note with title (article) ─────────────────────────────────────────────
|
|
132
|
+
if (post.title) {
|
|
133
|
+
return (
|
|
134
|
+
<article data-post data-format="note">
|
|
135
|
+
<h3 class="font-semibold text-base mb-1">
|
|
136
|
+
{titleHighlighted ? (
|
|
137
|
+
<a
|
|
138
|
+
href={post.permalink}
|
|
139
|
+
class="hover:underline"
|
|
140
|
+
dangerouslySetInnerHTML={{ __html: titleHighlighted }}
|
|
141
|
+
/>
|
|
142
|
+
) : (
|
|
143
|
+
<a href={post.permalink} class="hover:underline">
|
|
144
|
+
{post.title}
|
|
145
|
+
</a>
|
|
146
|
+
)}
|
|
147
|
+
</h3>
|
|
148
|
+
{snippet && (
|
|
149
|
+
<p
|
|
150
|
+
class="search-snippet"
|
|
151
|
+
dangerouslySetInnerHTML={{ __html: snippet }}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
{footer}
|
|
155
|
+
</article>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Untitled note ─────────────────────────────────────────────────────────
|
|
160
|
+
return (
|
|
161
|
+
<article data-post data-format="note">
|
|
162
|
+
{snippet ? (
|
|
163
|
+
<a href={post.permalink} class="block hover:opacity-80">
|
|
164
|
+
<p
|
|
165
|
+
class="search-snippet"
|
|
166
|
+
dangerouslySetInnerHTML={{ __html: snippet }}
|
|
167
|
+
/>
|
|
168
|
+
</a>
|
|
169
|
+
) : (
|
|
170
|
+
<a
|
|
171
|
+
href={post.permalink}
|
|
172
|
+
class="block text-sm text-muted-foreground hover:underline"
|
|
173
|
+
>
|
|
174
|
+
{post.publishedAtFormatted}
|
|
175
|
+
</a>
|
|
176
|
+
)}
|
|
177
|
+
{footer}
|
|
178
|
+
</article>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
12
182
|
export const SearchPage: FC<SearchPageProps> = ({
|
|
13
183
|
query,
|
|
14
184
|
results,
|
|
@@ -17,14 +187,12 @@ export const SearchPage: FC<SearchPageProps> = ({
|
|
|
17
187
|
page,
|
|
18
188
|
}) => {
|
|
19
189
|
const { t } = useLingui();
|
|
20
|
-
const searchTitle = t({
|
|
21
|
-
message: "Search",
|
|
22
|
-
comment: "@context: Search page title",
|
|
23
|
-
});
|
|
24
190
|
|
|
25
191
|
return (
|
|
26
192
|
<div class="py-6" data-page="search">
|
|
27
|
-
<h1 class="text-2xl font-semibold mb-6">
|
|
193
|
+
<h1 class="text-2xl font-semibold mb-6">
|
|
194
|
+
{t({ message: "Search", comment: "@context: Search page title" })}
|
|
195
|
+
</h1>
|
|
28
196
|
|
|
29
197
|
{/* Search form */}
|
|
30
198
|
<form method="get" action="/search" class="mb-8">
|
|
@@ -78,36 +246,12 @@ export const SearchPage: FC<SearchPageProps> = ({
|
|
|
78
246
|
|
|
79
247
|
{results.length > 0 && (
|
|
80
248
|
<>
|
|
81
|
-
<div class="
|
|
82
|
-
{results.map((result) => (
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
data-format={result.post.format}
|
|
88
|
-
>
|
|
89
|
-
<a href={result.post.permalink} class="block">
|
|
90
|
-
<h2 class="font-medium hover:underline">
|
|
91
|
-
{result.post.title ||
|
|
92
|
-
result.post.excerpt?.slice(0, 60) ||
|
|
93
|
-
"Post #" + result.post.id}
|
|
94
|
-
</h2>
|
|
95
|
-
|
|
96
|
-
{result.snippet && (
|
|
97
|
-
<p
|
|
98
|
-
class="text-sm text-muted-foreground mt-2 line-clamp-2"
|
|
99
|
-
dangerouslySetInnerHTML={{ __html: result.snippet }}
|
|
100
|
-
/>
|
|
101
|
-
)}
|
|
102
|
-
|
|
103
|
-
<footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
|
|
104
|
-
<span class="badge-outline">{result.post.format}</span>
|
|
105
|
-
<time datetime={result.post.publishedAt}>
|
|
106
|
-
{result.post.publishedAtFormatted}
|
|
107
|
-
</time>
|
|
108
|
-
</footer>
|
|
109
|
-
</a>
|
|
110
|
-
</article>
|
|
249
|
+
<div class="flex flex-col">
|
|
250
|
+
{results.map((result, i) => (
|
|
251
|
+
<div key={result.post.id}>
|
|
252
|
+
{i > 0 && <hr class="feed-divider" />}
|
|
253
|
+
<SearchResultCard result={result} />
|
|
254
|
+
</div>
|
|
111
255
|
))}
|
|
112
256
|
</div>
|
|
113
257
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Breadcrumb Component
|
|
3
|
+
*
|
|
4
|
+
* Reuses the existing dash-breadcrumb CSS classes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { FC } from "hono/jsx";
|
|
8
|
+
|
|
9
|
+
export interface AdminBreadcrumbProps {
|
|
10
|
+
parent: string;
|
|
11
|
+
parentHref: string;
|
|
12
|
+
current: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AdminBreadcrumb: FC<AdminBreadcrumbProps> = ({
|
|
16
|
+
parent,
|
|
17
|
+
parentHref,
|
|
18
|
+
current,
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<nav class="dash-breadcrumb mb-6">
|
|
22
|
+
<a href={parentHref} class="dash-breadcrumb-parent">
|
|
23
|
+
{parent}
|
|
24
|
+
</a>
|
|
25
|
+
<span class="dash-breadcrumb-sep">/</span>
|
|
26
|
+
<span class="dash-breadcrumb-current">{current}</span>
|
|
27
|
+
</nav>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Collections Sidebar
|
|
3
3
|
*
|
|
4
4
|
* Shared sidebar navigation for public collection pages.
|
|
5
|
-
* - Anonymous users: static nav with collections and dividers
|
|
5
|
+
* - Anonymous users: static nav with collections and dividers from sidebar items
|
|
6
6
|
* - Authenticated users: interactive Lit component with CRUD, reorder, divider management
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { FC } from "hono/jsx";
|
|
10
10
|
import { useLingui } from "@lingui/react/macro";
|
|
11
|
-
import type { Collection,
|
|
11
|
+
import type { Collection, SidebarItem } from "../../types.js";
|
|
12
12
|
import { renderCollectionIcon } from "../../lib/icons.js";
|
|
13
13
|
|
|
14
14
|
const escapeJson = (data: unknown) =>
|
|
@@ -16,15 +16,15 @@ const escapeJson = (data: unknown) =>
|
|
|
16
16
|
|
|
17
17
|
export interface CollectionsSidebarProps {
|
|
18
18
|
collections: Collection[];
|
|
19
|
-
|
|
19
|
+
sidebarItems: SidebarItem[];
|
|
20
20
|
activeSlug?: string;
|
|
21
21
|
isAuthenticated?: boolean;
|
|
22
|
-
postCounts?: Map<
|
|
22
|
+
postCounts?: Map<string, number>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export const CollectionsSidebar: FC<CollectionsSidebarProps> = ({
|
|
26
26
|
collections,
|
|
27
|
-
|
|
27
|
+
sidebarItems,
|
|
28
28
|
activeSlug,
|
|
29
29
|
isAuthenticated,
|
|
30
30
|
postCounts,
|
|
@@ -33,7 +33,7 @@ export const CollectionsSidebar: FC<CollectionsSidebarProps> = ({
|
|
|
33
33
|
return (
|
|
34
34
|
<AuthenticatedSidebar
|
|
35
35
|
collections={collections}
|
|
36
|
-
|
|
36
|
+
sidebarItems={sidebarItems}
|
|
37
37
|
activeSlug={activeSlug}
|
|
38
38
|
postCounts={postCounts}
|
|
39
39
|
/>
|
|
@@ -43,7 +43,7 @@ export const CollectionsSidebar: FC<CollectionsSidebarProps> = ({
|
|
|
43
43
|
return (
|
|
44
44
|
<AnonymousSidebar
|
|
45
45
|
collections={collections}
|
|
46
|
-
|
|
46
|
+
sidebarItems={sidebarItems}
|
|
47
47
|
activeSlug={activeSlug}
|
|
48
48
|
/>
|
|
49
49
|
);
|
|
@@ -55,20 +55,13 @@ export const CollectionsSidebar: FC<CollectionsSidebarProps> = ({
|
|
|
55
55
|
|
|
56
56
|
const AnonymousSidebar: FC<{
|
|
57
57
|
collections: Collection[];
|
|
58
|
-
|
|
58
|
+
sidebarItems: SidebarItem[];
|
|
59
59
|
activeSlug?: string;
|
|
60
|
-
}> = ({ collections,
|
|
60
|
+
}> = ({ collections, sidebarItems, activeSlug }) => {
|
|
61
61
|
const { t } = useLingui();
|
|
62
62
|
|
|
63
|
-
//
|
|
64
|
-
|
|
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);
|
|
63
|
+
// Build collection lookup
|
|
64
|
+
const collectionMap = new Map(collections.map((c) => [c.id, c]));
|
|
72
65
|
|
|
73
66
|
return (
|
|
74
67
|
<nav class="flex flex-col gap-1 pt-6">
|
|
@@ -78,19 +71,22 @@ const AnonymousSidebar: FC<{
|
|
|
78
71
|
comment: "@context: Sidebar heading for collections nav",
|
|
79
72
|
})}
|
|
80
73
|
</h2>
|
|
81
|
-
{
|
|
82
|
-
if (item.
|
|
74
|
+
{sidebarItems.map((item) => {
|
|
75
|
+
if (item.type === "divider") {
|
|
83
76
|
return (
|
|
84
|
-
<div key={
|
|
77
|
+
<div key={item.id} class="px-3 py-1">
|
|
85
78
|
<hr class="border-border" />
|
|
86
79
|
</div>
|
|
87
80
|
);
|
|
88
81
|
}
|
|
89
|
-
const col = item.
|
|
82
|
+
const col = item.collectionId
|
|
83
|
+
? collectionMap.get(item.collectionId)
|
|
84
|
+
: undefined;
|
|
85
|
+
if (!col) return null;
|
|
90
86
|
const isActive = col.slug === activeSlug;
|
|
91
87
|
return (
|
|
92
88
|
<a
|
|
93
|
-
key={
|
|
89
|
+
key={item.id}
|
|
94
90
|
href={`/c/${col.slug}`}
|
|
95
91
|
class={`flex items-center gap-2.5 px-3 py-2 text-sm rounded-md truncate ${
|
|
96
92
|
isActive
|
|
@@ -121,21 +117,36 @@ const AnonymousSidebar: FC<{
|
|
|
121
117
|
|
|
122
118
|
const AuthenticatedSidebar: FC<{
|
|
123
119
|
collections: Collection[];
|
|
124
|
-
|
|
120
|
+
sidebarItems: SidebarItem[];
|
|
125
121
|
activeSlug?: string;
|
|
126
|
-
postCounts?: Map<
|
|
127
|
-
}> = ({ collections,
|
|
122
|
+
postCounts?: Map<string, number>;
|
|
123
|
+
}> = ({ collections, sidebarItems, activeSlug, postCounts }) => {
|
|
128
124
|
const { t } = useLingui();
|
|
129
125
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
// Build collection lookup for enriching sidebar items
|
|
127
|
+
const collectionMap = new Map(
|
|
128
|
+
collections.map((col) => [
|
|
129
|
+
col.id,
|
|
130
|
+
{
|
|
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
|
+
postCount: postCounts?.get(col.id) ?? 0,
|
|
138
|
+
},
|
|
139
|
+
]),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const clientSidebarItems = sidebarItems.map((item) => ({
|
|
143
|
+
id: item.id,
|
|
144
|
+
type: item.type,
|
|
145
|
+
collectionId: item.collectionId,
|
|
146
|
+
position: item.position,
|
|
147
|
+
collection: item.collectionId
|
|
148
|
+
? collectionMap.get(item.collectionId)
|
|
149
|
+
: undefined,
|
|
139
150
|
}));
|
|
140
151
|
|
|
141
152
|
const labels = {
|
|
@@ -275,8 +286,7 @@ const AuthenticatedSidebar: FC<{
|
|
|
275
286
|
|
|
276
287
|
return (
|
|
277
288
|
<jant-collection-sidebar
|
|
278
|
-
|
|
279
|
-
dividers={escapeJson(dividers)}
|
|
289
|
+
sidebar-items={escapeJson(clientSidebarItems)}
|
|
280
290
|
labels={escapeJson(labels)}
|
|
281
291
|
active-slug={activeSlug ?? ""}
|
|
282
292
|
/>
|