@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
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Post Form
|
|
3
|
-
*
|
|
4
|
-
* Server-rendered wrapper that feeds data/labels to `<jant-post-form>`.
|
|
5
|
-
* Provides SSR fallback skeleton while Lit hydrates.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useLingui } from "@lingui/react/macro";
|
|
9
|
-
import type { FC } from "hono/jsx";
|
|
10
|
-
import type { Post, Media, Collection } from "../../../types.js";
|
|
11
|
-
import {
|
|
12
|
-
getMediaUrl,
|
|
13
|
-
getImageUrl,
|
|
14
|
-
getPublicUrlForProvider,
|
|
15
|
-
} from "../../../lib/image.js";
|
|
16
|
-
import { renderCollectionIcon } from "../../../lib/icons.js";
|
|
17
|
-
|
|
18
|
-
export interface PostFormProps {
|
|
19
|
-
post?: Post;
|
|
20
|
-
action: string;
|
|
21
|
-
mediaAttachments?: Media[];
|
|
22
|
-
r2PublicUrl?: string;
|
|
23
|
-
imageTransformUrl?: string;
|
|
24
|
-
s3PublicUrl?: string;
|
|
25
|
-
collections?: Collection[];
|
|
26
|
-
postCollectionIds?: number[];
|
|
27
|
-
cancelHref?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const PostForm: FC<PostFormProps> = ({
|
|
31
|
-
post,
|
|
32
|
-
action,
|
|
33
|
-
mediaAttachments = [],
|
|
34
|
-
r2PublicUrl,
|
|
35
|
-
imageTransformUrl,
|
|
36
|
-
s3PublicUrl,
|
|
37
|
-
collections = [],
|
|
38
|
-
postCollectionIds = [],
|
|
39
|
-
cancelHref,
|
|
40
|
-
}) => {
|
|
41
|
-
const { t } = useLingui();
|
|
42
|
-
const isEdit = Boolean(post);
|
|
43
|
-
|
|
44
|
-
const labels = JSON.stringify({
|
|
45
|
-
formatLabel: t({
|
|
46
|
-
message: "Format",
|
|
47
|
-
comment: "@context: Post form field - post format",
|
|
48
|
-
}),
|
|
49
|
-
noteOption: t({
|
|
50
|
-
message: "Note",
|
|
51
|
-
comment: "@context: Post format option",
|
|
52
|
-
}),
|
|
53
|
-
linkOption: t({
|
|
54
|
-
message: "Link",
|
|
55
|
-
comment: "@context: Post format option",
|
|
56
|
-
}),
|
|
57
|
-
quoteOption: t({
|
|
58
|
-
message: "Quote",
|
|
59
|
-
comment: "@context: Post format option",
|
|
60
|
-
}),
|
|
61
|
-
titleLabel: t({
|
|
62
|
-
message: "Title (optional)",
|
|
63
|
-
comment: "@context: Post form field",
|
|
64
|
-
}),
|
|
65
|
-
titlePlaceholder: t({
|
|
66
|
-
message: "Post title...",
|
|
67
|
-
comment: "@context: Post title placeholder",
|
|
68
|
-
}),
|
|
69
|
-
bodyLabel: t({
|
|
70
|
-
message: "Content",
|
|
71
|
-
comment: "@context: Post form field",
|
|
72
|
-
}),
|
|
73
|
-
bodyPlaceholder: t({
|
|
74
|
-
message: "What's on your mind?",
|
|
75
|
-
comment: "@context: Post content placeholder",
|
|
76
|
-
}),
|
|
77
|
-
urlLabel: t({
|
|
78
|
-
message: "URL (optional)",
|
|
79
|
-
comment: "@context: Post form field - source URL",
|
|
80
|
-
}),
|
|
81
|
-
urlPlaceholder: "https://...",
|
|
82
|
-
quoteTextLabel: t({
|
|
83
|
-
message: "Quote Text",
|
|
84
|
-
comment: "@context: Post form field - quoted text",
|
|
85
|
-
}),
|
|
86
|
-
quoteTextPlaceholder: t({
|
|
87
|
-
message: "The text being quoted...",
|
|
88
|
-
comment: "@context: Quote text placeholder",
|
|
89
|
-
}),
|
|
90
|
-
mediaLabel: t({
|
|
91
|
-
message: "Media",
|
|
92
|
-
comment: "@context: Post form field - media attachments",
|
|
93
|
-
}),
|
|
94
|
-
mediaAddButton: t({
|
|
95
|
-
message: "Add Media",
|
|
96
|
-
comment: "@context: Button to open media picker",
|
|
97
|
-
}),
|
|
98
|
-
mediaRemoveButton: t({
|
|
99
|
-
message: "Remove",
|
|
100
|
-
comment: "@context: Remove media attachment button",
|
|
101
|
-
}),
|
|
102
|
-
mediaEmptyLabel: t({
|
|
103
|
-
message: "No media attached.",
|
|
104
|
-
comment: "@context: Post form media empty state",
|
|
105
|
-
}),
|
|
106
|
-
statusLabel: t({
|
|
107
|
-
message: "Status",
|
|
108
|
-
comment: "@context: Post form field",
|
|
109
|
-
}),
|
|
110
|
-
statusPublished: t({
|
|
111
|
-
message: "Published",
|
|
112
|
-
comment: "@context: Post status option",
|
|
113
|
-
}),
|
|
114
|
-
statusDraft: t({
|
|
115
|
-
message: "Draft",
|
|
116
|
-
comment: "@context: Post status option",
|
|
117
|
-
}),
|
|
118
|
-
visibilityLabel: t({
|
|
119
|
-
message: "Visibility",
|
|
120
|
-
comment: "@context: Post form field - post visibility",
|
|
121
|
-
}),
|
|
122
|
-
visibilityListed: t({
|
|
123
|
-
message: "Listed",
|
|
124
|
-
comment: "@context: Visibility option - appears everywhere",
|
|
125
|
-
}),
|
|
126
|
-
visibilityFeatured: t({
|
|
127
|
-
message: "Featured",
|
|
128
|
-
comment: "@context: Visibility option - highlighted on featured page",
|
|
129
|
-
}),
|
|
130
|
-
visibilityUnlisted: t({
|
|
131
|
-
message: "Unlisted",
|
|
132
|
-
comment: "@context: Visibility option - hidden from feeds",
|
|
133
|
-
}),
|
|
134
|
-
pinnedLabel: t({
|
|
135
|
-
message: "Pinned",
|
|
136
|
-
comment: "@context: Post form checkbox - pin to top",
|
|
137
|
-
}),
|
|
138
|
-
collectionsLabel: t({
|
|
139
|
-
message: "Collections (optional)",
|
|
140
|
-
comment: "@context: Post form field - assign to collections",
|
|
141
|
-
}),
|
|
142
|
-
submitLabel: isEdit
|
|
143
|
-
? t({
|
|
144
|
-
message: "Update",
|
|
145
|
-
comment: "@context: Button to update existing post",
|
|
146
|
-
})
|
|
147
|
-
: t({
|
|
148
|
-
message: "Publish",
|
|
149
|
-
comment: "@context: Button to publish new post",
|
|
150
|
-
}),
|
|
151
|
-
cancelLabel: t({
|
|
152
|
-
message: "Cancel",
|
|
153
|
-
comment: "@context: Button to cancel form",
|
|
154
|
-
}),
|
|
155
|
-
mediaDialogTitle: t({
|
|
156
|
-
message: "Select Media",
|
|
157
|
-
comment: "@context: Media picker dialog title",
|
|
158
|
-
}),
|
|
159
|
-
mediaDialogDone: t({
|
|
160
|
-
message: "Done",
|
|
161
|
-
comment: "@context: Close media picker button",
|
|
162
|
-
}),
|
|
163
|
-
mediaDialogLoading: t({
|
|
164
|
-
message: "Loading...",
|
|
165
|
-
comment: "@context: Loading state for media picker",
|
|
166
|
-
}),
|
|
167
|
-
submitSuccessMessage: isEdit
|
|
168
|
-
? t({
|
|
169
|
-
message: "Post updated.",
|
|
170
|
-
comment: "@context: Toast after editing post",
|
|
171
|
-
})
|
|
172
|
-
: t({
|
|
173
|
-
message: "Post published.",
|
|
174
|
-
comment: "@context: Toast after creating post",
|
|
175
|
-
}),
|
|
176
|
-
submitErrorMessage: t({
|
|
177
|
-
message: "Couldn't save your post. Try again in a moment.",
|
|
178
|
-
comment: "@context: Toast when post save fails",
|
|
179
|
-
}),
|
|
180
|
-
}).replace(/</g, "\\u003c");
|
|
181
|
-
|
|
182
|
-
const initial = JSON.stringify({
|
|
183
|
-
format: post?.format ?? "note",
|
|
184
|
-
title: post?.title ?? "",
|
|
185
|
-
body: post?.body ?? "",
|
|
186
|
-
url: post?.url ?? "",
|
|
187
|
-
quoteText: post?.quoteText ?? "",
|
|
188
|
-
status: post?.status ?? "published",
|
|
189
|
-
visibility: post?.visibility ?? "listed",
|
|
190
|
-
pinned: post?.pinned === 1,
|
|
191
|
-
rating: post?.rating ?? 0,
|
|
192
|
-
collectionIds: postCollectionIds,
|
|
193
|
-
mediaIds: mediaAttachments.map((m) => m.id),
|
|
194
|
-
}).replace(/</g, "\\u003c");
|
|
195
|
-
|
|
196
|
-
const media = JSON.stringify(
|
|
197
|
-
mediaAttachments.map((m) => {
|
|
198
|
-
const pUrl = getPublicUrlForProvider(
|
|
199
|
-
m.provider,
|
|
200
|
-
r2PublicUrl,
|
|
201
|
-
s3PublicUrl,
|
|
202
|
-
);
|
|
203
|
-
const mediaUrl = getMediaUrl(m.storageKey, pUrl);
|
|
204
|
-
const thumbUrl = getImageUrl(mediaUrl, imageTransformUrl, {
|
|
205
|
-
width: 150,
|
|
206
|
-
quality: 80,
|
|
207
|
-
format: "auto",
|
|
208
|
-
fit: "cover",
|
|
209
|
-
});
|
|
210
|
-
return {
|
|
211
|
-
id: m.id,
|
|
212
|
-
thumbUrl,
|
|
213
|
-
alt: m.alt || m.originalName,
|
|
214
|
-
};
|
|
215
|
-
}),
|
|
216
|
-
).replace(/</g, "\\u003c");
|
|
217
|
-
|
|
218
|
-
const collectionOptions = JSON.stringify(
|
|
219
|
-
collections.map((col) => ({
|
|
220
|
-
id: col.id,
|
|
221
|
-
title: col.title,
|
|
222
|
-
icon: col.icon,
|
|
223
|
-
iconHtml: renderCollectionIcon(col.icon, { size: 18 }),
|
|
224
|
-
})),
|
|
225
|
-
).replace(/</g, "\\u003c");
|
|
226
|
-
|
|
227
|
-
const cancel = cancelHref ?? "/dash/posts";
|
|
228
|
-
|
|
229
|
-
return (
|
|
230
|
-
<jant-post-form
|
|
231
|
-
labels={labels}
|
|
232
|
-
initial={initial}
|
|
233
|
-
action={action}
|
|
234
|
-
cancel-href={cancel}
|
|
235
|
-
media={media}
|
|
236
|
-
collections={collectionOptions}
|
|
237
|
-
media-picker-url="/dash/media/picker"
|
|
238
|
-
is-edit={isEdit ? "true" : undefined}
|
|
239
|
-
>
|
|
240
|
-
<div class="flex flex-col gap-4 max-w-2xl">
|
|
241
|
-
<div class="field">
|
|
242
|
-
<div class="label skel-label"></div>
|
|
243
|
-
<div class="input skel-input"></div>
|
|
244
|
-
</div>
|
|
245
|
-
<div class="field">
|
|
246
|
-
<div class="label skel-label"></div>
|
|
247
|
-
<div class="textarea skel-textarea"></div>
|
|
248
|
-
</div>
|
|
249
|
-
<div class="field">
|
|
250
|
-
<div class="label skel-label"></div>
|
|
251
|
-
<div class="input skel-input"></div>
|
|
252
|
-
</div>
|
|
253
|
-
<div class="flex gap-2">
|
|
254
|
-
<div class="btn skel-input min-w-24"></div>
|
|
255
|
-
<div class="btn-outline skel-input min-w-20"></div>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
</jant-post-form>
|
|
259
|
-
);
|
|
260
|
-
};
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dashboard Layout
|
|
3
|
-
*
|
|
4
|
-
* Layout for admin dashboard pages
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { FC, PropsWithChildren } from "hono/jsx";
|
|
8
|
-
import type { Context } from "hono";
|
|
9
|
-
import { useLingui } from "@lingui/react/macro";
|
|
10
|
-
import { BaseLayout, type ToastProps } from "./BaseLayout.js";
|
|
11
|
-
|
|
12
|
-
export interface DashBreadcrumb {
|
|
13
|
-
parent: string;
|
|
14
|
-
parentHref: string;
|
|
15
|
-
current: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface DashLayoutProps {
|
|
19
|
-
c: Context;
|
|
20
|
-
title: string;
|
|
21
|
-
siteName: string;
|
|
22
|
-
siteAvatarUrl?: string;
|
|
23
|
-
currentPath?: string;
|
|
24
|
-
breadcrumb?: DashBreadcrumb;
|
|
25
|
-
toast?: ToastProps;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const AVATAR_COLORS = [
|
|
29
|
-
"#737fab", // slate blue
|
|
30
|
-
"#8d7dab", // muted violet
|
|
31
|
-
"#ab7d8d", // dusty rose
|
|
32
|
-
"#ab917d", // warm taupe
|
|
33
|
-
"#7dab8d", // sage green
|
|
34
|
-
"#7d9bab", // steel blue
|
|
35
|
-
"#9a8d7d", // earth brown
|
|
36
|
-
"#7dabab", // teal grey
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
function hashString(str: string): number {
|
|
40
|
-
let hash = 5381;
|
|
41
|
-
for (let i = 0; i < str.length; i++) {
|
|
42
|
-
hash = (hash * 33) ^ str.charCodeAt(i);
|
|
43
|
-
}
|
|
44
|
-
return Math.abs(hash);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function DashLayoutContent({
|
|
48
|
-
siteName,
|
|
49
|
-
siteAvatarUrl,
|
|
50
|
-
currentPath,
|
|
51
|
-
breadcrumb,
|
|
52
|
-
children,
|
|
53
|
-
}: PropsWithChildren<Omit<DashLayoutProps, "c" | "title">>) {
|
|
54
|
-
const { t } = useLingui();
|
|
55
|
-
|
|
56
|
-
const navClass = (match: RegExp) =>
|
|
57
|
-
`dash-header-link ${currentPath && match.test(currentPath) ? "dash-header-link-active" : ""}`;
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div class="min-h-screen">
|
|
61
|
-
{/* Header */}
|
|
62
|
-
<header class="dash-header">
|
|
63
|
-
<div class="container dash-header-inner">
|
|
64
|
-
<a href="/dash" class="dash-header-avatar-link">
|
|
65
|
-
{siteAvatarUrl ? (
|
|
66
|
-
<img src={siteAvatarUrl} alt="" class="dash-header-avatar" />
|
|
67
|
-
) : (
|
|
68
|
-
<span
|
|
69
|
-
class="dash-header-avatar dash-header-avatar-fallback"
|
|
70
|
-
style={`background-color: ${AVATAR_COLORS[hashString(siteName) % AVATAR_COLORS.length]}`}
|
|
71
|
-
>
|
|
72
|
-
{siteName.charAt(0).toUpperCase()}
|
|
73
|
-
</span>
|
|
74
|
-
)}
|
|
75
|
-
</a>
|
|
76
|
-
<nav class="dash-header-nav">
|
|
77
|
-
<a href="/dash" class={navClass(/^\/dash$/)}>
|
|
78
|
-
{t({
|
|
79
|
-
message: "Dashboard",
|
|
80
|
-
comment: "@context: Dashboard navigation - dashboard home",
|
|
81
|
-
})}
|
|
82
|
-
</a>
|
|
83
|
-
<span class="dash-header-nav-sep" aria-hidden="true">
|
|
84
|
-
·
|
|
85
|
-
</span>
|
|
86
|
-
<a href="/dash/pages" class={navClass(/^\/dash\/pages/)}>
|
|
87
|
-
{t({
|
|
88
|
-
message: "Pages",
|
|
89
|
-
comment: "@context: Dashboard navigation - pages management",
|
|
90
|
-
})}
|
|
91
|
-
</a>
|
|
92
|
-
<span class="dash-header-nav-sep" aria-hidden="true">
|
|
93
|
-
·
|
|
94
|
-
</span>
|
|
95
|
-
<a href="/dash/settings" class={navClass(/^\/dash\/settings/)}>
|
|
96
|
-
{t({
|
|
97
|
-
message: "Settings",
|
|
98
|
-
comment: "@context: Dashboard navigation - site settings",
|
|
99
|
-
})}
|
|
100
|
-
</a>
|
|
101
|
-
</nav>
|
|
102
|
-
|
|
103
|
-
<div class="dash-header-right">
|
|
104
|
-
<a
|
|
105
|
-
href="/"
|
|
106
|
-
class="dash-header-visit"
|
|
107
|
-
target="_blank"
|
|
108
|
-
aria-label={t({
|
|
109
|
-
message: "Visit Blog",
|
|
110
|
-
comment:
|
|
111
|
-
"@context: Dashboard header link to visit the public blog",
|
|
112
|
-
})}
|
|
113
|
-
>
|
|
114
|
-
<span
|
|
115
|
-
class="dash-header-visit-icon"
|
|
116
|
-
data-tooltip={t({
|
|
117
|
-
message: "Visit Blog",
|
|
118
|
-
comment:
|
|
119
|
-
"@context: Dashboard header tooltip for visit blog icon on mobile",
|
|
120
|
-
})}
|
|
121
|
-
data-side="bottom"
|
|
122
|
-
>
|
|
123
|
-
<svg
|
|
124
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
125
|
-
width="16"
|
|
126
|
-
height="16"
|
|
127
|
-
viewBox="0 0 24 24"
|
|
128
|
-
fill="none"
|
|
129
|
-
stroke="currentColor"
|
|
130
|
-
stroke-width="2"
|
|
131
|
-
stroke-linecap="round"
|
|
132
|
-
stroke-linejoin="round"
|
|
133
|
-
>
|
|
134
|
-
<path d="M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6" />
|
|
135
|
-
<path d="m21 3-9 9" />
|
|
136
|
-
<path d="M15 3h6v6" />
|
|
137
|
-
</svg>
|
|
138
|
-
</span>
|
|
139
|
-
<span class="dash-header-visit-text">
|
|
140
|
-
{t({
|
|
141
|
-
message: "Visit Blog",
|
|
142
|
-
comment:
|
|
143
|
-
"@context: Dashboard header link text to visit the public blog",
|
|
144
|
-
})}
|
|
145
|
-
<span class="ml-1" aria-hidden="true">
|
|
146
|
-
{"\u2197"}
|
|
147
|
-
</span>
|
|
148
|
-
</span>
|
|
149
|
-
</a>
|
|
150
|
-
|
|
151
|
-
<div class="dropdown-menu">
|
|
152
|
-
<button
|
|
153
|
-
type="button"
|
|
154
|
-
id="dash-menu-trigger"
|
|
155
|
-
class="dash-header-menu-btn"
|
|
156
|
-
aria-haspopup="menu"
|
|
157
|
-
aria-controls="dash-menu"
|
|
158
|
-
aria-expanded="false"
|
|
159
|
-
aria-label={t({
|
|
160
|
-
message: "Menu",
|
|
161
|
-
comment: "@context: Dashboard header menu button",
|
|
162
|
-
})}
|
|
163
|
-
>
|
|
164
|
-
<svg
|
|
165
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
166
|
-
width="16"
|
|
167
|
-
height="16"
|
|
168
|
-
viewBox="0 0 24 24"
|
|
169
|
-
fill="currentColor"
|
|
170
|
-
>
|
|
171
|
-
<circle cx="5" cy="12" r="2" />
|
|
172
|
-
<circle cx="12" cy="12" r="2" />
|
|
173
|
-
<circle cx="19" cy="12" r="2" />
|
|
174
|
-
</svg>
|
|
175
|
-
</button>
|
|
176
|
-
<div
|
|
177
|
-
id="dash-menu-popover"
|
|
178
|
-
data-popover
|
|
179
|
-
data-align="end"
|
|
180
|
-
aria-hidden="true"
|
|
181
|
-
>
|
|
182
|
-
<div
|
|
183
|
-
role="menu"
|
|
184
|
-
id="dash-menu"
|
|
185
|
-
aria-labelledby="dash-menu-trigger"
|
|
186
|
-
>
|
|
187
|
-
<a href="/signout" role="menuitem">
|
|
188
|
-
{t({
|
|
189
|
-
message: "Sign Out",
|
|
190
|
-
comment: "@context: Dashboard menu item to sign out",
|
|
191
|
-
})}
|
|
192
|
-
</a>
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
198
|
-
</header>
|
|
199
|
-
|
|
200
|
-
{breadcrumb && (
|
|
201
|
-
<div class="container">
|
|
202
|
-
<nav class="dash-breadcrumb">
|
|
203
|
-
<a href={breadcrumb.parentHref} class="dash-breadcrumb-parent">
|
|
204
|
-
{breadcrumb.parent}
|
|
205
|
-
</a>
|
|
206
|
-
<span class="dash-breadcrumb-sep">/</span>
|
|
207
|
-
<span class="dash-breadcrumb-current">{breadcrumb.current}</span>
|
|
208
|
-
</nav>
|
|
209
|
-
</div>
|
|
210
|
-
)}
|
|
211
|
-
|
|
212
|
-
{/* Main */}
|
|
213
|
-
<div class="container py-8">
|
|
214
|
-
<main>{children}</main>
|
|
215
|
-
</div>
|
|
216
|
-
</div>
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export const DashLayout: FC<PropsWithChildren<DashLayoutProps>> = ({
|
|
221
|
-
c,
|
|
222
|
-
title,
|
|
223
|
-
siteName,
|
|
224
|
-
siteAvatarUrl,
|
|
225
|
-
currentPath,
|
|
226
|
-
breadcrumb,
|
|
227
|
-
toast,
|
|
228
|
-
children,
|
|
229
|
-
}) => {
|
|
230
|
-
return (
|
|
231
|
-
<BaseLayout
|
|
232
|
-
title={`${title} - ${siteName}`}
|
|
233
|
-
c={c}
|
|
234
|
-
toast={toast}
|
|
235
|
-
isAuthenticated={true}
|
|
236
|
-
>
|
|
237
|
-
<DashLayoutContent
|
|
238
|
-
siteName={siteName}
|
|
239
|
-
siteAvatarUrl={siteAvatarUrl}
|
|
240
|
-
currentPath={currentPath}
|
|
241
|
-
breadcrumb={breadcrumb}
|
|
242
|
-
>
|
|
243
|
-
{children}
|
|
244
|
-
</DashLayoutContent>
|
|
245
|
-
</BaseLayout>
|
|
246
|
-
);
|
|
247
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single Page (Custom Page)
|
|
3
|
-
*
|
|
4
|
-
* Custom page view — clean centered content.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { FC } from "hono/jsx";
|
|
8
|
-
import type { SinglePageProps } from "../../types.js";
|
|
9
|
-
|
|
10
|
-
export const SinglePage: FC<SinglePageProps> = ({ page }) => {
|
|
11
|
-
return (
|
|
12
|
-
<article class="h-entry py-6" data-page="single-page">
|
|
13
|
-
{page.title && (
|
|
14
|
-
<h1 class="p-name text-2xl font-semibold mb-6">{page.title}</h1>
|
|
15
|
-
)}
|
|
16
|
-
|
|
17
|
-
<div
|
|
18
|
-
class="e-content prose"
|
|
19
|
-
dangerouslySetInnerHTML={{ __html: page.bodyHtml || "" }}
|
|
20
|
-
/>
|
|
21
|
-
</article>
|
|
22
|
-
);
|
|
23
|
-
};
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thread View Component
|
|
3
|
-
*
|
|
4
|
-
* Displays a thread of posts with reply chain visualization
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { FC } from "hono/jsx";
|
|
8
|
-
import { useLingui } from "@lingui/react/macro";
|
|
9
|
-
import type { Post } from "../../types.js";
|
|
10
|
-
import * as sqid from "../../lib/sqid.js";
|
|
11
|
-
import * as time from "../../lib/time.js";
|
|
12
|
-
|
|
13
|
-
export interface ThreadViewProps {
|
|
14
|
-
/** All posts in the thread, ordered by createdAt */
|
|
15
|
-
posts: Post[];
|
|
16
|
-
/** ID of the currently viewed post (to highlight) */
|
|
17
|
-
currentPostId: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const ThreadPost: FC<{
|
|
21
|
-
post: Post;
|
|
22
|
-
isCurrent: boolean;
|
|
23
|
-
isRoot: boolean;
|
|
24
|
-
}> = ({ post, isCurrent, isRoot }) => {
|
|
25
|
-
const { t } = useLingui();
|
|
26
|
-
return (
|
|
27
|
-
<article
|
|
28
|
-
id={`post-${post.id}`}
|
|
29
|
-
class={`h-entry p-4 rounded-lg border ${
|
|
30
|
-
isCurrent
|
|
31
|
-
? "border-primary bg-primary/5 ring-2 ring-primary/20"
|
|
32
|
-
: "border-border hover:border-muted-foreground/30"
|
|
33
|
-
}`}
|
|
34
|
-
>
|
|
35
|
-
{post.title && (
|
|
36
|
-
<h2 class="p-name text-lg font-medium mb-2">
|
|
37
|
-
<a
|
|
38
|
-
href={`${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`}
|
|
39
|
-
class="u-url hover:underline"
|
|
40
|
-
>
|
|
41
|
-
{post.title}
|
|
42
|
-
</a>
|
|
43
|
-
</h2>
|
|
44
|
-
)}
|
|
45
|
-
|
|
46
|
-
<div
|
|
47
|
-
class="e-content prose prose-sm"
|
|
48
|
-
dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
|
|
49
|
-
/>
|
|
50
|
-
|
|
51
|
-
<footer class="mt-3 flex items-center gap-3 text-sm text-muted-foreground">
|
|
52
|
-
<time
|
|
53
|
-
class="dt-published"
|
|
54
|
-
datetime={time.toISOString(post.publishedAt)}
|
|
55
|
-
>
|
|
56
|
-
{time.formatDate(post.publishedAt)}
|
|
57
|
-
</time>
|
|
58
|
-
{isRoot && (
|
|
59
|
-
<span class="text-xs">
|
|
60
|
-
{t({
|
|
61
|
-
message: "Thread start",
|
|
62
|
-
comment: "@context: Thread view indicator - first post in thread",
|
|
63
|
-
})}
|
|
64
|
-
</span>
|
|
65
|
-
)}
|
|
66
|
-
{!isCurrent && (
|
|
67
|
-
<a
|
|
68
|
-
href={`${post.path ? `/${post.path}` : `/p/${sqid.encode(post.id)}`}`}
|
|
69
|
-
class="text-xs hover:underline"
|
|
70
|
-
>
|
|
71
|
-
{t({
|
|
72
|
-
message: "Permalink",
|
|
73
|
-
comment: "@context: Link to individual post in thread",
|
|
74
|
-
})}
|
|
75
|
-
</a>
|
|
76
|
-
)}
|
|
77
|
-
</footer>
|
|
78
|
-
</article>
|
|
79
|
-
);
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
export const ThreadView: FC<ThreadViewProps> = ({ posts, currentPostId }) => {
|
|
83
|
-
const { t } = useLingui();
|
|
84
|
-
if (posts.length === 0) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const rootPost = posts[0];
|
|
89
|
-
const isThread = posts.length > 1;
|
|
90
|
-
|
|
91
|
-
// Single post, no thread
|
|
92
|
-
if (!isThread) {
|
|
93
|
-
return (
|
|
94
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Early return for empty array at line 73 guarantees posts[0] exists
|
|
95
|
-
<ThreadPost post={rootPost!} isCurrent={true} isRoot={false} />
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const threadLabel =
|
|
100
|
-
posts.length === 1
|
|
101
|
-
? t({
|
|
102
|
-
message: "Thread with 1 post",
|
|
103
|
-
comment: "@context: Thread view header - single post",
|
|
104
|
-
})
|
|
105
|
-
: t({
|
|
106
|
-
message: "Thread with {count} posts",
|
|
107
|
-
comment: "@context: Thread view header - multiple posts",
|
|
108
|
-
values: { count: String(posts.length) },
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<div class="thread-view">
|
|
113
|
-
<div class="mb-4 text-sm text-muted-foreground">{threadLabel}</div>
|
|
114
|
-
|
|
115
|
-
<div class="flex flex-col gap-3">
|
|
116
|
-
{posts.map((post, index) => (
|
|
117
|
-
<div key={post.id} class="relative">
|
|
118
|
-
{/* Connection line */}
|
|
119
|
-
{index > 0 && (
|
|
120
|
-
<div class="absolute left-6 -top-3 w-0.5 h-3 bg-border" />
|
|
121
|
-
)}
|
|
122
|
-
{index < posts.length - 1 && (
|
|
123
|
-
<div class="absolute left-6 -bottom-3 w-0.5 h-3 bg-border" />
|
|
124
|
-
)}
|
|
125
|
-
|
|
126
|
-
<ThreadPost
|
|
127
|
-
post={post}
|
|
128
|
-
isCurrent={post.id === currentPostId}
|
|
129
|
-
isRoot={index === 0}
|
|
130
|
-
/>
|
|
131
|
-
</div>
|
|
132
|
-
))}
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
);
|
|
136
|
-
};
|