@jant/core 0.3.35 → 0.3.37
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/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- 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/{lib → client}/avatar-upload.ts +4 -3
- 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/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -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 +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- 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 -140
- 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 +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- 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__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- 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__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- 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 +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- 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 +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- 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/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -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 +57 -31
- 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 +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- 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 +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- 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 +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- 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 +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- 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 +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- 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 +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- 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 +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- 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/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/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- 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/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- 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 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- 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/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Icon Picker Grid
|
|
3
|
-
*
|
|
4
|
-
* HTML fragment returned by GET /dash/collections/icons.
|
|
5
|
-
* Renders a grid of icon buttons organized by category.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { FC } from "hono/jsx";
|
|
9
|
-
import { ICON_CATALOG } from "../../../lib/icon-catalog.js";
|
|
10
|
-
import { getIconSvg } from "../../../lib/icons.js";
|
|
11
|
-
|
|
12
|
-
export const IconPickerGrid: FC = () => {
|
|
13
|
-
return (
|
|
14
|
-
<div class="flex flex-col gap-4">
|
|
15
|
-
{Object.entries(ICON_CATALOG).map(([category, names]) => (
|
|
16
|
-
<div key={category} data-category={category}>
|
|
17
|
-
<h3 class="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
|
18
|
-
{category}
|
|
19
|
-
</h3>
|
|
20
|
-
<div class="grid grid-cols-8 gap-1">
|
|
21
|
-
{names.map((name) => {
|
|
22
|
-
const svg = getIconSvg(name);
|
|
23
|
-
if (!svg) return null;
|
|
24
|
-
return (
|
|
25
|
-
<button
|
|
26
|
-
key={name}
|
|
27
|
-
type="button"
|
|
28
|
-
class="flex items-center justify-center w-9 h-9 rounded-md hover:bg-accent transition-colors"
|
|
29
|
-
data-icon-name={name}
|
|
30
|
-
data-icon-svg={svg}
|
|
31
|
-
title={name}
|
|
32
|
-
data-on:click={`$iconName = el.dataset.iconName; $iconSvg = el.dataset.iconSvg; $icon = JSON.stringify({ name: $iconName, svg: $iconSvg, color: $iconColor }); const p = document.getElementById('icon-preview'); if (p) p.innerHTML = el.dataset.iconSvg; document.getElementById('icon-picker-dialog')?.close()`}
|
|
33
|
-
>
|
|
34
|
-
<span
|
|
35
|
-
class="w-5 h-5 flex items-center justify-center"
|
|
36
|
-
dangerouslySetInnerHTML={{
|
|
37
|
-
__html: svg
|
|
38
|
-
.replace(/width="24"/, 'width="20"')
|
|
39
|
-
.replace(/height="24"/, 'height="20"'),
|
|
40
|
-
}}
|
|
41
|
-
/>
|
|
42
|
-
</button>
|
|
43
|
-
);
|
|
44
|
-
})}
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
))}
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
|
-
};
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single collection detail view
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useLingui } from "@lingui/react/macro";
|
|
6
|
-
import type { Collection, PostView } from "../../../types.js";
|
|
7
|
-
import { ActionButtons } from "../index.js";
|
|
8
|
-
import { encode } from "../../../lib/sqid.js";
|
|
9
|
-
import { renderCollectionIcon } from "../../../lib/icons.js";
|
|
10
|
-
|
|
11
|
-
export function ViewCollectionContent({
|
|
12
|
-
collection,
|
|
13
|
-
posts,
|
|
14
|
-
}: {
|
|
15
|
-
collection: Collection;
|
|
16
|
-
posts: PostView[];
|
|
17
|
-
}) {
|
|
18
|
-
const { t } = useLingui();
|
|
19
|
-
const count = String(posts.length);
|
|
20
|
-
const postsHeader = t({
|
|
21
|
-
message: `Posts in Collection (${count})`,
|
|
22
|
-
comment: "@context: Collection posts section heading",
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<>
|
|
27
|
-
<div class="flex items-center justify-between mb-6">
|
|
28
|
-
<div>
|
|
29
|
-
<h1 class="text-2xl font-semibold flex items-center gap-2">
|
|
30
|
-
{collection.icon && (
|
|
31
|
-
<span
|
|
32
|
-
class="shrink-0"
|
|
33
|
-
dangerouslySetInnerHTML={{
|
|
34
|
-
__html: renderCollectionIcon(collection.icon, { size: 24 }),
|
|
35
|
-
}}
|
|
36
|
-
/>
|
|
37
|
-
)}
|
|
38
|
-
{collection.title}
|
|
39
|
-
</h1>
|
|
40
|
-
<p class="text-sm text-muted-foreground">/{collection.slug}</p>
|
|
41
|
-
</div>
|
|
42
|
-
<ActionButtons
|
|
43
|
-
editHref={`/dash/collections/${collection.id}/edit`}
|
|
44
|
-
editLabel={t({
|
|
45
|
-
message: "Edit",
|
|
46
|
-
comment: "@context: Button to edit collection",
|
|
47
|
-
})}
|
|
48
|
-
viewHref={`/c/${collection.slug}`}
|
|
49
|
-
viewLabel={t({
|
|
50
|
-
message: "View",
|
|
51
|
-
comment: "@context: Button to view collection",
|
|
52
|
-
})}
|
|
53
|
-
/>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
{collection.description && (
|
|
57
|
-
<p class="text-muted-foreground mb-6">{collection.description}</p>
|
|
58
|
-
)}
|
|
59
|
-
|
|
60
|
-
<div class="card">
|
|
61
|
-
<header>
|
|
62
|
-
<h2>{postsHeader}</h2>
|
|
63
|
-
</header>
|
|
64
|
-
<section>
|
|
65
|
-
{posts.length === 0 ? (
|
|
66
|
-
<p class="text-muted-foreground">
|
|
67
|
-
{t({
|
|
68
|
-
message: "No posts in this collection.",
|
|
69
|
-
comment: "@context: Empty state message",
|
|
70
|
-
})}
|
|
71
|
-
</p>
|
|
72
|
-
) : (
|
|
73
|
-
<div class="flex flex-col divide-y">
|
|
74
|
-
{posts.map((post) => (
|
|
75
|
-
<div key={post.id} class="py-3 flex items-center gap-4">
|
|
76
|
-
<div class="flex-1 min-w-0">
|
|
77
|
-
<a
|
|
78
|
-
href={`/dash/posts/${encode(post.id)}`}
|
|
79
|
-
class="font-medium hover:underline"
|
|
80
|
-
>
|
|
81
|
-
{post.title ||
|
|
82
|
-
post.excerpt?.slice(0, 50) ||
|
|
83
|
-
`Post #${post.id}`}
|
|
84
|
-
</a>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
)}
|
|
90
|
-
</section>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
<div class="mt-6">
|
|
94
|
-
<a href="/dash/collections" class="text-sm hover:underline">
|
|
95
|
-
{t({
|
|
96
|
-
message: "\u2190 Back to Collections",
|
|
97
|
-
comment: "@context: Navigation link",
|
|
98
|
-
})}
|
|
99
|
-
</a>
|
|
100
|
-
</div>
|
|
101
|
-
</>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Media grid list with upload UI
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useLingui } from "@lingui/react/macro";
|
|
6
|
-
import type { Media } from "../../../types.js";
|
|
7
|
-
import { EmptyState } from "../index.js";
|
|
8
|
-
import {
|
|
9
|
-
getMediaUrl,
|
|
10
|
-
getImageUrl,
|
|
11
|
-
getPublicUrlForProvider,
|
|
12
|
-
} from "../../../lib/image.js";
|
|
13
|
-
|
|
14
|
-
function formatSize(bytes: number): string {
|
|
15
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
16
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
17
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function MediaCard({
|
|
21
|
-
media,
|
|
22
|
-
r2PublicUrl,
|
|
23
|
-
imageTransformUrl,
|
|
24
|
-
s3PublicUrl,
|
|
25
|
-
}: {
|
|
26
|
-
media: Media;
|
|
27
|
-
r2PublicUrl?: string;
|
|
28
|
-
imageTransformUrl?: string;
|
|
29
|
-
s3PublicUrl?: string;
|
|
30
|
-
}) {
|
|
31
|
-
const publicUrl = getPublicUrlForProvider(
|
|
32
|
-
media.provider,
|
|
33
|
-
r2PublicUrl,
|
|
34
|
-
s3PublicUrl,
|
|
35
|
-
);
|
|
36
|
-
const fullUrl = getMediaUrl(media.storageKey, publicUrl);
|
|
37
|
-
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
38
|
-
width: 300,
|
|
39
|
-
quality: 80,
|
|
40
|
-
format: "auto",
|
|
41
|
-
fit: "cover",
|
|
42
|
-
});
|
|
43
|
-
const isImage = media.mimeType.startsWith("image/");
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<div class="group relative" data-media-id={media.id}>
|
|
47
|
-
{isImage ? (
|
|
48
|
-
<button
|
|
49
|
-
type="button"
|
|
50
|
-
class="block w-full aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary cursor-pointer"
|
|
51
|
-
onclick={`document.getElementById('lightbox-img').src = '${fullUrl}'; document.getElementById('lightbox').showModal()`}
|
|
52
|
-
>
|
|
53
|
-
<img
|
|
54
|
-
src={thumbnailUrl}
|
|
55
|
-
alt={media.alt || media.originalName}
|
|
56
|
-
class="w-full h-full object-cover"
|
|
57
|
-
loading="lazy"
|
|
58
|
-
/>
|
|
59
|
-
</button>
|
|
60
|
-
) : (
|
|
61
|
-
<a
|
|
62
|
-
href={`/dash/media/${media.id}`}
|
|
63
|
-
class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
|
|
64
|
-
>
|
|
65
|
-
<div class="w-full h-full flex items-center justify-center text-muted-foreground">
|
|
66
|
-
<span class="text-xs">{media.mimeType}</span>
|
|
67
|
-
</div>
|
|
68
|
-
</a>
|
|
69
|
-
)}
|
|
70
|
-
<a
|
|
71
|
-
href={`/dash/media/${media.id}`}
|
|
72
|
-
class="block mt-2 text-xs truncate hover:underline"
|
|
73
|
-
title={media.originalName}
|
|
74
|
-
>
|
|
75
|
-
{media.originalName}
|
|
76
|
-
</a>
|
|
77
|
-
<div class="text-xs text-muted-foreground">{formatSize(media.size)}</div>
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function MediaListContent({
|
|
83
|
-
mediaList,
|
|
84
|
-
r2PublicUrl,
|
|
85
|
-
imageTransformUrl,
|
|
86
|
-
s3PublicUrl,
|
|
87
|
-
}: {
|
|
88
|
-
mediaList: Media[];
|
|
89
|
-
r2PublicUrl?: string;
|
|
90
|
-
imageTransformUrl?: string;
|
|
91
|
-
s3PublicUrl?: string;
|
|
92
|
-
}) {
|
|
93
|
-
const { t } = useLingui();
|
|
94
|
-
|
|
95
|
-
const processingText = t({
|
|
96
|
-
message: "Processing...",
|
|
97
|
-
comment: "@context: Upload status - processing",
|
|
98
|
-
});
|
|
99
|
-
const uploadingText = t({
|
|
100
|
-
message: "Uploading...",
|
|
101
|
-
comment: "@context: Upload status - uploading",
|
|
102
|
-
});
|
|
103
|
-
const uploadText = t({
|
|
104
|
-
message: "Upload",
|
|
105
|
-
comment: "@context: Button to upload media file",
|
|
106
|
-
});
|
|
107
|
-
const errorText = t({
|
|
108
|
-
message: "Upload failed. Please try again.",
|
|
109
|
-
comment: "@context: Upload error message",
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<>
|
|
114
|
-
{/* Hidden form for Datastar-driven upload */}
|
|
115
|
-
<form
|
|
116
|
-
id="upload-form"
|
|
117
|
-
class="hidden"
|
|
118
|
-
enctype="multipart/form-data"
|
|
119
|
-
data-on:submit__prevent="@post('/api/upload', {contentType: 'form'})"
|
|
120
|
-
>
|
|
121
|
-
<input id="upload-file-input" type="file" name="file" />
|
|
122
|
-
</form>
|
|
123
|
-
|
|
124
|
-
{/* Header */}
|
|
125
|
-
<div class="flex items-center justify-between mb-6">
|
|
126
|
-
<h1 class="text-2xl font-semibold">
|
|
127
|
-
{t({ message: "Media", comment: "@context: Media main heading" })}
|
|
128
|
-
</h1>
|
|
129
|
-
<label class="btn cursor-pointer">
|
|
130
|
-
<span>{uploadText}</span>
|
|
131
|
-
<input
|
|
132
|
-
type="file"
|
|
133
|
-
class="hidden"
|
|
134
|
-
accept="image/*"
|
|
135
|
-
data-media-upload
|
|
136
|
-
data-text-processing={processingText}
|
|
137
|
-
data-text-uploading={uploadingText}
|
|
138
|
-
data-text-error={errorText}
|
|
139
|
-
/>
|
|
140
|
-
</label>
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
{/* Upload instructions */}
|
|
144
|
-
<div class="card mb-6">
|
|
145
|
-
<section class="text-sm text-muted-foreground">
|
|
146
|
-
<p>
|
|
147
|
-
{t({
|
|
148
|
-
message:
|
|
149
|
-
"Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.",
|
|
150
|
-
comment:
|
|
151
|
-
"@context: Media upload instructions - auto optimization",
|
|
152
|
-
})}
|
|
153
|
-
</p>
|
|
154
|
-
</section>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
{/* Media grid or empty state */}
|
|
158
|
-
<div id="media-content">
|
|
159
|
-
{mediaList.length === 0 ? (
|
|
160
|
-
<div id="empty-state">
|
|
161
|
-
<EmptyState
|
|
162
|
-
message={t({
|
|
163
|
-
message: "No media uploaded yet.",
|
|
164
|
-
comment: "@context: Empty state message when no media exists",
|
|
165
|
-
})}
|
|
166
|
-
/>
|
|
167
|
-
</div>
|
|
168
|
-
) : (
|
|
169
|
-
<div
|
|
170
|
-
id="media-grid"
|
|
171
|
-
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"
|
|
172
|
-
>
|
|
173
|
-
{mediaList.map((m) => (
|
|
174
|
-
<MediaCard
|
|
175
|
-
key={m.id}
|
|
176
|
-
media={m}
|
|
177
|
-
r2PublicUrl={r2PublicUrl}
|
|
178
|
-
imageTransformUrl={imageTransformUrl}
|
|
179
|
-
s3PublicUrl={s3PublicUrl}
|
|
180
|
-
/>
|
|
181
|
-
))}
|
|
182
|
-
</div>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
{/* Lightbox */}
|
|
187
|
-
<dialog
|
|
188
|
-
id="lightbox"
|
|
189
|
-
class="p-0 m-auto bg-transparent backdrop:bg-black/80"
|
|
190
|
-
onclick="event.target === this && this.close()"
|
|
191
|
-
>
|
|
192
|
-
<img
|
|
193
|
-
id="lightbox-img"
|
|
194
|
-
src=""
|
|
195
|
-
alt=""
|
|
196
|
-
class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
|
197
|
-
/>
|
|
198
|
-
</dialog>
|
|
199
|
-
</>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single media detail view
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useLingui } from "@lingui/react/macro";
|
|
6
|
-
import type { Media } from "../../../types.js";
|
|
7
|
-
import { DangerZone } from "../index.js";
|
|
8
|
-
import * as time from "../../../lib/time.js";
|
|
9
|
-
import {
|
|
10
|
-
getMediaUrl,
|
|
11
|
-
getImageUrl,
|
|
12
|
-
getPublicUrlForProvider,
|
|
13
|
-
} from "../../../lib/image.js";
|
|
14
|
-
|
|
15
|
-
function formatSize(bytes: number): string {
|
|
16
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
17
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
18
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function ViewMediaContent({
|
|
22
|
-
media,
|
|
23
|
-
r2PublicUrl,
|
|
24
|
-
imageTransformUrl,
|
|
25
|
-
s3PublicUrl,
|
|
26
|
-
}: {
|
|
27
|
-
media: Media;
|
|
28
|
-
r2PublicUrl?: string;
|
|
29
|
-
imageTransformUrl?: string;
|
|
30
|
-
s3PublicUrl?: string;
|
|
31
|
-
}) {
|
|
32
|
-
const { t } = useLingui();
|
|
33
|
-
const publicUrl = getPublicUrlForProvider(
|
|
34
|
-
media.provider,
|
|
35
|
-
r2PublicUrl,
|
|
36
|
-
s3PublicUrl,
|
|
37
|
-
);
|
|
38
|
-
const url = getMediaUrl(media.storageKey, publicUrl);
|
|
39
|
-
const thumbnailUrl = getImageUrl(url, imageTransformUrl, {
|
|
40
|
-
width: 600,
|
|
41
|
-
quality: 85,
|
|
42
|
-
format: "auto",
|
|
43
|
-
});
|
|
44
|
-
const isImage = media.mimeType.startsWith("image/");
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<>
|
|
48
|
-
<div class="flex items-center justify-between mb-6">
|
|
49
|
-
<div>
|
|
50
|
-
<h1 class="text-2xl font-semibold">{media.originalName}</h1>
|
|
51
|
-
<p class="text-muted-foreground mt-1">
|
|
52
|
-
{formatSize(media.size)} · {media.mimeType} ·{" "}
|
|
53
|
-
{time.formatDate(media.createdAt)}
|
|
54
|
-
</p>
|
|
55
|
-
</div>
|
|
56
|
-
<a href="/dash/media" class="btn-outline">
|
|
57
|
-
{t({
|
|
58
|
-
message: "Back",
|
|
59
|
-
comment: "@context: Button to go back to media list",
|
|
60
|
-
})}
|
|
61
|
-
</a>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<div class="grid gap-6 md:grid-cols-2">
|
|
65
|
-
{/* Preview */}
|
|
66
|
-
<div class="card">
|
|
67
|
-
<header>
|
|
68
|
-
<h2>
|
|
69
|
-
{t({
|
|
70
|
-
message: "Preview",
|
|
71
|
-
comment: "@context: Media detail section - preview",
|
|
72
|
-
})}
|
|
73
|
-
</h2>
|
|
74
|
-
</header>
|
|
75
|
-
<section>
|
|
76
|
-
{isImage ? (
|
|
77
|
-
<>
|
|
78
|
-
<button
|
|
79
|
-
type="button"
|
|
80
|
-
class="cursor-pointer"
|
|
81
|
-
onclick={`document.getElementById('lightbox-img').src = '${url}'; document.getElementById('lightbox').showModal()`}
|
|
82
|
-
>
|
|
83
|
-
<img
|
|
84
|
-
src={thumbnailUrl}
|
|
85
|
-
alt={media.alt || media.originalName}
|
|
86
|
-
class="max-w-full rounded-lg hover:opacity-90 transition-opacity"
|
|
87
|
-
/>
|
|
88
|
-
</button>
|
|
89
|
-
<p class="text-xs text-muted-foreground mt-2">
|
|
90
|
-
{t({
|
|
91
|
-
message: "Click image to view full size",
|
|
92
|
-
comment: "@context: Hint to click image for lightbox",
|
|
93
|
-
})}
|
|
94
|
-
</p>
|
|
95
|
-
</>
|
|
96
|
-
) : (
|
|
97
|
-
<div class="aspect-video bg-muted rounded-lg flex items-center justify-center text-muted-foreground">
|
|
98
|
-
<span>{media.mimeType}</span>
|
|
99
|
-
</div>
|
|
100
|
-
)}
|
|
101
|
-
</section>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
{/* Details */}
|
|
105
|
-
<div class="space-y-6">
|
|
106
|
-
<div class="card">
|
|
107
|
-
<header>
|
|
108
|
-
<h2>
|
|
109
|
-
{t({
|
|
110
|
-
message: "URL",
|
|
111
|
-
comment: "@context: Media detail section - URL",
|
|
112
|
-
})}
|
|
113
|
-
</h2>
|
|
114
|
-
</header>
|
|
115
|
-
<section>
|
|
116
|
-
<div class="flex items-center gap-2">
|
|
117
|
-
<input
|
|
118
|
-
type="text"
|
|
119
|
-
class="input flex-1 font-mono text-sm"
|
|
120
|
-
value={url}
|
|
121
|
-
readonly
|
|
122
|
-
/>
|
|
123
|
-
<button
|
|
124
|
-
type="button"
|
|
125
|
-
class="btn-outline"
|
|
126
|
-
onclick={`navigator.clipboard.writeText('${url}')`}
|
|
127
|
-
>
|
|
128
|
-
{t({
|
|
129
|
-
message: "Copy",
|
|
130
|
-
comment: "@context: Button to copy URL to clipboard",
|
|
131
|
-
})}
|
|
132
|
-
</button>
|
|
133
|
-
</div>
|
|
134
|
-
<p class="text-xs text-muted-foreground mt-2">
|
|
135
|
-
{t({
|
|
136
|
-
message: "Use this URL to embed the media in your posts.",
|
|
137
|
-
comment: "@context: Media URL helper text",
|
|
138
|
-
})}
|
|
139
|
-
</p>
|
|
140
|
-
</section>
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
<div class="card">
|
|
144
|
-
<header>
|
|
145
|
-
<h2>
|
|
146
|
-
{t({
|
|
147
|
-
message: "Markdown",
|
|
148
|
-
comment: "@context: Media detail section - Markdown snippet",
|
|
149
|
-
})}
|
|
150
|
-
</h2>
|
|
151
|
-
</header>
|
|
152
|
-
<section>
|
|
153
|
-
<div class="flex items-center gap-2">
|
|
154
|
-
<input
|
|
155
|
-
type="text"
|
|
156
|
-
class="input flex-1 font-mono text-sm"
|
|
157
|
-
value={``}
|
|
158
|
-
readonly
|
|
159
|
-
/>
|
|
160
|
-
<button
|
|
161
|
-
type="button"
|
|
162
|
-
class="btn-outline"
|
|
163
|
-
onclick={`navigator.clipboard.writeText('')`}
|
|
164
|
-
>
|
|
165
|
-
{t({
|
|
166
|
-
message: "Copy",
|
|
167
|
-
comment: "@context: Button to copy Markdown to clipboard",
|
|
168
|
-
})}
|
|
169
|
-
</button>
|
|
170
|
-
</div>
|
|
171
|
-
</section>
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
{/* Delete */}
|
|
175
|
-
<DangerZone
|
|
176
|
-
actionLabel={t({
|
|
177
|
-
message: "Delete Media",
|
|
178
|
-
comment: "@context: Button to delete media",
|
|
179
|
-
})}
|
|
180
|
-
formAction={`/dash/media/${media.id}/delete`}
|
|
181
|
-
confirmMessage="Are you sure you want to delete this media?"
|
|
182
|
-
description={t({
|
|
183
|
-
message:
|
|
184
|
-
"Deleting this media will remove it permanently from storage.",
|
|
185
|
-
comment: "@context: Warning message before deleting media",
|
|
186
|
-
})}
|
|
187
|
-
/>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{/* Lightbox */}
|
|
192
|
-
{isImage && (
|
|
193
|
-
<dialog
|
|
194
|
-
id="lightbox"
|
|
195
|
-
class="p-0 m-auto bg-transparent backdrop:bg-black/80"
|
|
196
|
-
onclick="event.target === this && this.close()"
|
|
197
|
-
>
|
|
198
|
-
<img
|
|
199
|
-
id="lightbox-img"
|
|
200
|
-
src=""
|
|
201
|
-
alt=""
|
|
202
|
-
class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
|
203
|
-
/>
|
|
204
|
-
</dialog>
|
|
205
|
-
)}
|
|
206
|
-
</>
|
|
207
|
-
);
|
|
208
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pages list — page CRUD only
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useLingui } from "@lingui/react/macro";
|
|
6
|
-
import type { Page } from "../../../types.js";
|
|
7
|
-
import { ListItemRow, ActionButtons, CrudPageHeader } from "../index.js";
|
|
8
|
-
|
|
9
|
-
export function PagesContent({ pages }: { pages: Page[] }) {
|
|
10
|
-
const { t } = useLingui();
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<>
|
|
14
|
-
<CrudPageHeader
|
|
15
|
-
title={t({
|
|
16
|
-
message: "Pages",
|
|
17
|
-
comment: "@context: Pages main heading",
|
|
18
|
-
})}
|
|
19
|
-
>
|
|
20
|
-
<a href="/dash/pages/new" class="btn">
|
|
21
|
-
{t({
|
|
22
|
-
message: "New Page",
|
|
23
|
-
comment: "@context: Button to create new page",
|
|
24
|
-
})}
|
|
25
|
-
</a>
|
|
26
|
-
</CrudPageHeader>
|
|
27
|
-
|
|
28
|
-
{pages.length === 0 ? (
|
|
29
|
-
<p class="text-sm text-muted-foreground py-4">
|
|
30
|
-
{t({
|
|
31
|
-
message: "No pages yet. Create your first page to get started.",
|
|
32
|
-
comment: "@context: Empty state for pages list",
|
|
33
|
-
})}
|
|
34
|
-
</p>
|
|
35
|
-
) : (
|
|
36
|
-
<div class="flex flex-col divide-y">
|
|
37
|
-
{pages.map((page) => (
|
|
38
|
-
<ListItemRow
|
|
39
|
-
key={page.id}
|
|
40
|
-
actions={
|
|
41
|
-
<ActionButtons
|
|
42
|
-
editHref={`/dash/pages/${page.id}/edit`}
|
|
43
|
-
editLabel={t({
|
|
44
|
-
message: "Edit",
|
|
45
|
-
comment: "@context: Button to edit page",
|
|
46
|
-
})}
|
|
47
|
-
viewHref={
|
|
48
|
-
page.status !== "draft" ? `/${page.slug}` : undefined
|
|
49
|
-
}
|
|
50
|
-
viewLabel={t({
|
|
51
|
-
message: "View",
|
|
52
|
-
comment: "@context: Button to view page on public site",
|
|
53
|
-
})}
|
|
54
|
-
/>
|
|
55
|
-
}
|
|
56
|
-
>
|
|
57
|
-
<a
|
|
58
|
-
href={`/dash/pages/${page.id}`}
|
|
59
|
-
class="font-medium hover:underline"
|
|
60
|
-
>
|
|
61
|
-
{page.title ||
|
|
62
|
-
t({
|
|
63
|
-
message: "Untitled",
|
|
64
|
-
comment: "@context: Default title for untitled page",
|
|
65
|
-
})}
|
|
66
|
-
</a>
|
|
67
|
-
<p class="text-sm text-muted-foreground mt-1">/{page.slug}</p>
|
|
68
|
-
</ListItemRow>
|
|
69
|
-
))}
|
|
70
|
-
</div>
|
|
71
|
-
)}
|
|
72
|
-
</>
|
|
73
|
-
);
|
|
74
|
-
}
|