@jant/core 0.3.24 → 0.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +101 -571
- package/dist/client.js +1 -0
- package/dist/db/schema.js +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +3 -9
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -9
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +48 -3
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +16 -11
- package/dist/lib/schemas.js +34 -3
- package/dist/lib/theme.js +4 -4
- package/dist/lib/timeline.js +24 -48
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +3 -3
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +3 -3
- package/dist/routes/api/search.js +2 -2
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -416
- package/dist/routes/dash/index.js +1 -1
- package/dist/routes/dash/media.js +13 -393
- package/dist/routes/dash/pages.js +112 -86
- package/dist/routes/dash/posts.js +3 -5
- package/dist/routes/dash/redirects.js +20 -14
- package/dist/routes/dash/settings.js +213 -518
- package/dist/routes/feed/rss.js +4 -3
- package/dist/routes/feed/sitemap.js +5 -3
- package/dist/routes/pages/archive.js +3 -6
- package/dist/routes/pages/collection.js +3 -6
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +36 -0
- package/dist/routes/pages/home.js +33 -49
- package/dist/routes/pages/latest.js +45 -0
- package/dist/routes/pages/page.js +29 -32
- package/dist/routes/pages/post.js +3 -6
- package/dist/routes/pages/search.js +3 -6
- package/dist/services/page.js +5 -1
- package/dist/services/post.js +45 -31
- package/dist/services/search.js +1 -1
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/{theme → ui}/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +467 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
- package/dist/{theme/components → ui/dash}/PostList.js +6 -6
- package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
- package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
- package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
- package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +169 -0
- package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
- package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
- package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
- package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
- package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
- package/dist/ui/shared/index.js +5 -0
- package/package.json +1 -9
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/app.tsx +131 -561
- package/src/client.ts +1 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +1 -1
- package/src/i18n/locales/en.po +477 -261
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +477 -261
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +477 -261
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +7 -36
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/schemas.test.ts +60 -19
- package/src/lib/__tests__/timeline.test.ts +45 -81
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +15 -9
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -10
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +73 -4
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +22 -15
- package/src/lib/schemas.ts +47 -6
- package/src/lib/theme.ts +5 -5
- package/src/lib/timeline.ts +28 -57
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +3 -3
- package/src/preset.css +2 -1
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +3 -3
- package/src/routes/api/search.ts +2 -2
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +18 -367
- package/src/routes/dash/index.tsx +1 -1
- package/src/routes/dash/media.tsx +13 -415
- package/src/routes/dash/pages.tsx +131 -98
- package/src/routes/dash/posts.tsx +3 -7
- package/src/routes/dash/redirects.tsx +22 -16
- package/src/routes/dash/settings.tsx +265 -478
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +5 -3
- package/src/routes/feed/sitemap.ts +5 -3
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +2 -6
- package/src/routes/pages/collection.tsx +2 -6
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +44 -0
- package/src/routes/pages/home.tsx +30 -53
- package/src/routes/pages/latest.tsx +59 -0
- package/src/routes/pages/page.tsx +28 -30
- package/src/routes/pages/post.tsx +2 -5
- package/src/routes/pages/search.tsx +2 -6
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post.test.ts +114 -15
- package/src/services/page.ts +13 -1
- package/src/services/post.ts +58 -40
- package/src/services/search.ts +2 -2
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +475 -0
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -774
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/{theme → ui}/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +414 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
- package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
- package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
- package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
- package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
- package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +164 -0
- package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
- package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
- package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
- package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
- package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
- package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -46
- package/dist/routes/dash/navigation.js +0 -289
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
- package/dist/themes/threads/index.js +0 -81
- package/dist/themes/threads/pages/HomePage.js +0 -25
- package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
- package/dist/themes/threads/timeline/TimelineItem.js +0 -36
- package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
- package/dist/themes/threads/timeline/groupByDate.js +0 -22
- package/dist/themes/threads/timeline/timelineMore.js +0 -107
- package/src/lib/__tests__/theme-components.test.ts +0 -105
- package/src/lib/theme-components.ts +0 -65
- package/src/routes/dash/navigation.tsx +0 -317
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
- package/src/themes/threads/index.ts +0 -100
- package/src/themes/threads/style.css +0 -336
- package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
- package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
- package/src/themes/threads/timeline/groupByDate.ts +0 -30
- package/src/themes/threads/timeline/timelineMore.tsx +0 -130
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
package/src/lib/image.ts
CHANGED
|
@@ -102,36 +102,28 @@ export function getPublicUrlForProvider(
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* Generates a media URL
|
|
105
|
+
* Generates a media URL from a storage key.
|
|
106
106
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
107
|
+
* Both proxy and CDN paths use the same structure — only the domain differs.
|
|
108
|
+
* Without a public URL, returns a root-relative path for the local proxy.
|
|
109
|
+
* With a public URL, prefixes that domain.
|
|
109
110
|
*
|
|
110
|
-
* @param
|
|
111
|
-
* @param storageKey - The storage object key (used to build CDN path and extract extension)
|
|
111
|
+
* @param storageKey - The storage object key (e.g. `"media/2025/01/uuid.webp"`)
|
|
112
112
|
* @param publicUrl - Optional public URL base for direct CDN access
|
|
113
113
|
* @returns The public URL for the media file
|
|
114
114
|
*
|
|
115
115
|
* @example
|
|
116
116
|
* ```ts
|
|
117
|
-
* // Without public URL -
|
|
118
|
-
* getMediaUrl("
|
|
119
|
-
* // Returns: "/media/01902a9f-1a2b-7c3d.webp"
|
|
117
|
+
* // Without public URL - local proxy
|
|
118
|
+
* getMediaUrl("media/2025/01/01902a9f-1a2b-7c3d.webp");
|
|
119
|
+
* // Returns: "/media/2025/01/01902a9f-1a2b-7c3d.webp"
|
|
120
120
|
*
|
|
121
|
-
* // With public URL -
|
|
122
|
-
* getMediaUrl("
|
|
121
|
+
* // With public URL - CDN
|
|
122
|
+
* getMediaUrl("media/2025/01/01902a9f-1a2b-7c3d.webp", "https://cdn.example.com");
|
|
123
123
|
* // Returns: "https://cdn.example.com/media/2025/01/01902a9f-1a2b-7c3d.webp"
|
|
124
124
|
* ```
|
|
125
125
|
*/
|
|
126
|
-
export function getMediaUrl(
|
|
127
|
-
|
|
128
|
-
storageKey
|
|
129
|
-
publicUrl?: string,
|
|
130
|
-
): string {
|
|
131
|
-
if (publicUrl) {
|
|
132
|
-
return `${publicUrl.replace(/\/+$/, "")}/${storageKey}`;
|
|
133
|
-
}
|
|
134
|
-
// Extract extension from storage key
|
|
135
|
-
const ext = storageKey.split(".").pop() || "bin";
|
|
136
|
-
return `/media/${mediaId}.${ext}`;
|
|
126
|
+
export function getMediaUrl(storageKey: string, publicUrl?: string): string {
|
|
127
|
+
const base = publicUrl ? publicUrl.replace(/\/+$/, "") : "";
|
|
128
|
+
return `${base}/${storageKey}`;
|
|
137
129
|
}
|
package/src/lib/media-helpers.ts
CHANGED
|
@@ -45,9 +45,9 @@ export function buildMediaMap(
|
|
|
45
45
|
);
|
|
46
46
|
return {
|
|
47
47
|
id: m.id,
|
|
48
|
-
url: getMediaUrl(m.
|
|
48
|
+
url: getMediaUrl(m.storageKey, publicUrl),
|
|
49
49
|
previewUrl: getImageUrl(
|
|
50
|
-
getMediaUrl(m.
|
|
50
|
+
getMediaUrl(m.storageKey, publicUrl),
|
|
51
51
|
imageTransformUrl,
|
|
52
52
|
{ width: 400, quality: 80, format: "auto", fit: "cover" },
|
|
53
53
|
),
|
package/src/lib/nav-reorder.ts
CHANGED
|
@@ -16,7 +16,7 @@ if (list) {
|
|
|
16
16
|
const ids = [...list.querySelectorAll<HTMLElement>("[data-id]")].map(
|
|
17
17
|
(el) => Number(el.dataset.id),
|
|
18
18
|
);
|
|
19
|
-
fetch("/dash/
|
|
19
|
+
fetch("/dash/pages/reorder", {
|
|
20
20
|
method: "POST",
|
|
21
21
|
headers: { "Content-Type": "application/json" },
|
|
22
22
|
body: JSON.stringify({ ids }),
|
package/src/lib/navigation.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Context } from "hono";
|
|
8
|
-
import { getSiteName } from "./config.js";
|
|
9
|
-
import type { NavItemView } from "../types.js";
|
|
8
|
+
import { getSiteName, getHomeDefaultView, getSiteFooter } from "./config.js";
|
|
9
|
+
import type { Collection, NavItemView } from "../types.js";
|
|
10
10
|
import { toNavItemViews } from "./view.js";
|
|
11
|
+
import { getMediaUrl, getPublicUrlForProvider } from "./image.js";
|
|
12
|
+
import { render as renderMarkdown } from "./markdown.js";
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Navigation data needed by SiteLayout
|
|
@@ -16,12 +18,20 @@ export interface NavigationData {
|
|
|
16
18
|
links: NavItemView[];
|
|
17
19
|
currentPath: string;
|
|
18
20
|
siteName: string;
|
|
21
|
+
siteDescription: string;
|
|
22
|
+
isAuthenticated: boolean;
|
|
23
|
+
collections: Collection[];
|
|
24
|
+
homeDefaultView: string;
|
|
25
|
+
siteAvatarUrl?: string;
|
|
26
|
+
showHeaderAvatar?: boolean;
|
|
27
|
+
siteFooterHtml?: string;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
31
|
* Fetch navigation data for public pages.
|
|
23
32
|
*
|
|
24
33
|
* Returns NavItemView[] with pre-computed isActive/isExternal state.
|
|
34
|
+
* Also checks authentication status and loads collections for authenticated users.
|
|
25
35
|
*
|
|
26
36
|
* @param c - Hono context
|
|
27
37
|
* @returns Navigation data for SiteLayout
|
|
@@ -39,7 +49,66 @@ export interface NavigationData {
|
|
|
39
49
|
export async function getNavigationData(c: Context): Promise<NavigationData> {
|
|
40
50
|
const items = await c.var.services.navItems.list();
|
|
41
51
|
const currentPath = new URL(c.req.url).pathname;
|
|
42
|
-
const siteName = await
|
|
52
|
+
const [siteName, homeDefaultView, siteFooter] = await Promise.all([
|
|
53
|
+
getSiteName(c),
|
|
54
|
+
getHomeDefaultView(c),
|
|
55
|
+
getSiteFooter(c),
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
// Only include description if explicitly set (DB or env), not the default
|
|
59
|
+
const dbDescription = await c.var.services.settings.get("SITE_DESCRIPTION");
|
|
60
|
+
const envDescription = c.env.SITE_DESCRIPTION;
|
|
61
|
+
const siteDescription =
|
|
62
|
+
dbDescription || (typeof envDescription === "string" ? envDescription : "");
|
|
63
|
+
|
|
64
|
+
// Resolve avatar URL from storage key
|
|
65
|
+
const avatarKey = await c.var.services.settings.get("SITE_AVATAR");
|
|
66
|
+
const showHeaderAvatar =
|
|
67
|
+
(await c.var.services.settings.get("SHOW_HEADER_AVATAR")) === "true";
|
|
68
|
+
let siteAvatarUrl: string | undefined;
|
|
69
|
+
if (avatarKey) {
|
|
70
|
+
const publicUrl = getPublicUrlForProvider(
|
|
71
|
+
c.env.STORAGE_DRIVER || "r2",
|
|
72
|
+
c.env.R2_PUBLIC_URL,
|
|
73
|
+
c.env.S3_PUBLIC_URL,
|
|
74
|
+
);
|
|
75
|
+
siteAvatarUrl = getMediaUrl(avatarKey, publicUrl);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Render footer markdown
|
|
79
|
+
const siteFooterHtml = siteFooter ? renderMarkdown(siteFooter) : undefined;
|
|
80
|
+
|
|
43
81
|
const links = toNavItemViews(items, currentPath);
|
|
44
|
-
|
|
82
|
+
|
|
83
|
+
// Check auth status for compose button
|
|
84
|
+
let isAuthenticated = false;
|
|
85
|
+
let collections: Collection[] = [];
|
|
86
|
+
if (c.var.auth) {
|
|
87
|
+
try {
|
|
88
|
+
const session = await c.var.auth.api.getSession({
|
|
89
|
+
headers: c.req.raw.headers,
|
|
90
|
+
});
|
|
91
|
+
isAuthenticated = !!session?.user;
|
|
92
|
+
} catch {
|
|
93
|
+
// Not authenticated
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Only load collections when authenticated (for compose dialog)
|
|
98
|
+
if (isAuthenticated) {
|
|
99
|
+
collections = await c.var.services.collections.list();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
links,
|
|
104
|
+
currentPath,
|
|
105
|
+
siteName,
|
|
106
|
+
siteDescription,
|
|
107
|
+
isAuthenticated,
|
|
108
|
+
collections,
|
|
109
|
+
homeDefaultView,
|
|
110
|
+
siteAvatarUrl,
|
|
111
|
+
showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
|
|
112
|
+
siteFooterHtml,
|
|
113
|
+
};
|
|
45
114
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure utility functions for page-based pagination.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Computes which page numbers to display in a numbered pagination control.
|
|
9
|
+
* Always includes: first page, last page, current page, and 1 page on each side of current.
|
|
10
|
+
* Gaps between non-consecutive pages are represented by 0 (ellipsis marker).
|
|
11
|
+
*
|
|
12
|
+
* @param currentPage - The current active page (1-indexed)
|
|
13
|
+
* @param totalPages - Total number of pages
|
|
14
|
+
* @returns Array of page numbers, with 0 representing ellipsis gaps
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* getPageNumbers(1, 5) // [1, 2, 3, 4, 5]
|
|
19
|
+
* getPageNumbers(1, 20) // [1, 2, 0, 20]
|
|
20
|
+
* getPageNumbers(10, 20) // [1, 0, 9, 10, 11, 0, 20]
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function getPageNumbers(
|
|
24
|
+
currentPage: number,
|
|
25
|
+
totalPages: number,
|
|
26
|
+
): number[] {
|
|
27
|
+
if (totalPages <= 7) {
|
|
28
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pages = new Set<number>();
|
|
32
|
+
pages.add(1);
|
|
33
|
+
pages.add(totalPages);
|
|
34
|
+
pages.add(currentPage);
|
|
35
|
+
if (currentPage > 1) pages.add(currentPage - 1);
|
|
36
|
+
if (currentPage < totalPages) pages.add(currentPage + 1);
|
|
37
|
+
|
|
38
|
+
const sorted = [...pages].sort((a, b) => a - b);
|
|
39
|
+
|
|
40
|
+
// Insert 0 for gaps
|
|
41
|
+
const result: number[] = [];
|
|
42
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
43
|
+
if (i > 0 && sorted[i]! - sorted[i - 1]! > 1) {
|
|
44
|
+
result.push(0); // ellipsis marker
|
|
45
|
+
}
|
|
46
|
+
result.push(sorted[i]!);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return result;
|
|
50
|
+
}
|
package/src/lib/render.tsx
CHANGED
|
@@ -3,16 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides a single entry point for rendering public pages with the
|
|
5
5
|
* correct layout stack: BaseLayout > SiteLayout > content.
|
|
6
|
-
*
|
|
7
|
-
* BaseLayout is always the built-in implementation (handles Vite assets,
|
|
8
|
-
* I18nProvider, toast). SiteLayout is resolved from theme components.
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
8
|
import type { Context } from "hono";
|
|
12
9
|
import type { Child } from "hono/jsx";
|
|
13
|
-
import type {
|
|
14
|
-
import { BaseLayout } from "../
|
|
15
|
-
import {
|
|
10
|
+
import type { SiteLayoutProps } from "../types.js";
|
|
11
|
+
import { BaseLayout } from "../ui/layouts/BaseLayout.js";
|
|
12
|
+
import { SiteLayout } from "../ui/layouts/SiteLayout.js";
|
|
16
13
|
import type { NavigationData } from "./navigation.js";
|
|
17
14
|
|
|
18
15
|
export interface RenderPublicPageOptions {
|
|
@@ -29,8 +26,6 @@ export interface RenderPublicPageOptions {
|
|
|
29
26
|
/**
|
|
30
27
|
* Render a public page with the standard layout stack.
|
|
31
28
|
*
|
|
32
|
-
* Always uses the built-in BaseLayout, resolves SiteLayout from theme config.
|
|
33
|
-
*
|
|
34
29
|
* @param c - Hono context
|
|
35
30
|
* @param options - Page rendering options
|
|
36
31
|
* @returns Hono HTML response
|
|
@@ -48,20 +43,32 @@ export interface RenderPublicPageOptions {
|
|
|
48
43
|
export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
49
44
|
const { title, description, navData, content } = options;
|
|
50
45
|
|
|
51
|
-
const components = c.var.config?.theme?.components as
|
|
52
|
-
| ThemeComponents
|
|
53
|
-
| undefined;
|
|
54
|
-
const Layout = components?.SiteLayout ?? DefaultSiteLayout;
|
|
55
|
-
|
|
56
46
|
const layoutProps: SiteLayoutProps = {
|
|
57
47
|
siteName: navData.siteName,
|
|
48
|
+
siteDescription: navData.siteDescription,
|
|
58
49
|
links: navData.links,
|
|
59
50
|
currentPath: navData.currentPath,
|
|
51
|
+
isAuthenticated: navData.isAuthenticated,
|
|
52
|
+
collections: navData.collections,
|
|
53
|
+
homeDefaultView: navData.homeDefaultView,
|
|
54
|
+
siteAvatarUrl: navData.siteAvatarUrl,
|
|
55
|
+
showHeaderAvatar: navData.showHeaderAvatar,
|
|
56
|
+
siteFooterHtml: navData.siteFooterHtml,
|
|
60
57
|
};
|
|
61
58
|
|
|
59
|
+
// Read favicon and noindex from context (set by theme middleware)
|
|
60
|
+
const faviconUrl = c.get("faviconUrl") as string | undefined;
|
|
61
|
+
const noindex = c.get("noindex") as boolean | undefined;
|
|
62
|
+
|
|
62
63
|
return c.html(
|
|
63
|
-
<BaseLayout
|
|
64
|
-
|
|
64
|
+
<BaseLayout
|
|
65
|
+
title={title}
|
|
66
|
+
description={description}
|
|
67
|
+
c={c}
|
|
68
|
+
faviconUrl={faviconUrl}
|
|
69
|
+
noindex={noindex}
|
|
70
|
+
>
|
|
71
|
+
<SiteLayout {...layoutProps}>{content}</SiteLayout>
|
|
65
72
|
</BaseLayout>,
|
|
66
73
|
);
|
|
67
74
|
}
|
package/src/lib/schemas.ts
CHANGED
|
@@ -51,19 +51,20 @@ export const RedirectTypeSchema = z.enum(["301", "302"]);
|
|
|
51
51
|
export const RatingSchema = z.coerce
|
|
52
52
|
.number()
|
|
53
53
|
.int()
|
|
54
|
-
.min(
|
|
54
|
+
.min(0)
|
|
55
55
|
.max(5)
|
|
56
56
|
.optional()
|
|
57
|
-
.or(z.literal("").transform(() => undefined))
|
|
57
|
+
.or(z.literal("").transform(() => undefined))
|
|
58
|
+
.transform((v) => (v === 0 ? undefined : v));
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* API request body schema for creating a post
|
|
61
62
|
*/
|
|
62
63
|
export const CreatePostSchema = z.object({
|
|
63
64
|
format: FormatSchema,
|
|
64
|
-
|
|
65
|
+
path: z
|
|
65
66
|
.string()
|
|
66
|
-
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]
|
|
67
|
+
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/)
|
|
67
68
|
.optional()
|
|
68
69
|
.or(z.literal("").transform(() => undefined)),
|
|
69
70
|
title: z.string().optional(),
|
|
@@ -81,9 +82,10 @@ export const CreatePostSchema = z.object({
|
|
|
81
82
|
collectionId: z.coerce
|
|
82
83
|
.number()
|
|
83
84
|
.int()
|
|
84
|
-
.
|
|
85
|
+
.min(0)
|
|
85
86
|
.optional()
|
|
86
|
-
.or(z.literal("").transform(() => undefined))
|
|
87
|
+
.or(z.literal("").transform(() => undefined))
|
|
88
|
+
.transform((v) => (v === 0 ? undefined : v)),
|
|
87
89
|
replyToId: z.string().optional(), // Sqid format
|
|
88
90
|
publishedAt: z.number().int().positive().optional(),
|
|
89
91
|
mediaIds: z.array(z.string()).max(MAX_MEDIA_ATTACHMENTS).optional(),
|
|
@@ -151,6 +153,45 @@ export const CreateCollectionSchema = z.object({
|
|
|
151
153
|
*/
|
|
152
154
|
export const UpdateCollectionSchema = CreateCollectionSchema.partial();
|
|
153
155
|
|
|
156
|
+
// =============================================================================
|
|
157
|
+
// Auth Schemas
|
|
158
|
+
// =============================================================================
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Setup form validation schema
|
|
162
|
+
*/
|
|
163
|
+
export const SetupSchema = z.object({
|
|
164
|
+
name: z.string().min(1, "Name is required"),
|
|
165
|
+
email: z.string().email("Invalid email address"),
|
|
166
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sign-in form validation schema
|
|
171
|
+
*/
|
|
172
|
+
export const SigninSchema = z.object({
|
|
173
|
+
email: z.string().email("Invalid email address"),
|
|
174
|
+
password: z.string().min(1, "Password is required"),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Password reset form validation schema
|
|
179
|
+
*/
|
|
180
|
+
export const ResetPasswordSchema = z
|
|
181
|
+
.object({
|
|
182
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
183
|
+
confirmPassword: z.string().min(1),
|
|
184
|
+
token: z.string().min(1),
|
|
185
|
+
})
|
|
186
|
+
.refine((d) => d.password === d.confirmPassword, {
|
|
187
|
+
message: "Passwords do not match",
|
|
188
|
+
path: ["confirmPassword"],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// =============================================================================
|
|
192
|
+
// Form Data Helpers
|
|
193
|
+
// =============================================================================
|
|
194
|
+
|
|
154
195
|
/**
|
|
155
196
|
* Form data helper: safely parse a FormData value with a schema
|
|
156
197
|
*
|
package/src/lib/theme.ts
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* Resolves the active color theme and builds CSS for injection into `<head>`.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ColorTheme } from "../
|
|
8
|
-
import { BUILTIN_COLOR_THEMES } from "../
|
|
7
|
+
import type { ColorTheme } from "../ui/color-themes.js";
|
|
8
|
+
import { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
|
|
9
9
|
import type { JantConfig } from "../types.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Get the list of available color themes.
|
|
13
13
|
*
|
|
14
|
-
* Returns `config.
|
|
14
|
+
* Returns `config.colorThemes` if provided, otherwise the built-in list.
|
|
15
15
|
*
|
|
16
16
|
* @param config - The Jant configuration
|
|
17
17
|
* @returns Array of available color themes
|
|
@@ -22,7 +22,7 @@ import type { JantConfig } from "../types.js";
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
export function getAvailableThemes(config: JantConfig): ColorTheme[] {
|
|
25
|
-
return config.
|
|
25
|
+
return config.colorThemes ?? BUILTIN_COLOR_THEMES;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -32,7 +32,7 @@ export function getAvailableThemes(config: JantConfig): ColorTheme[] {
|
|
|
32
32
|
* BaseCoat defaults → selected theme → cssVariables
|
|
33
33
|
*
|
|
34
34
|
* @param theme - The active color theme (undefined = no theme overrides)
|
|
35
|
-
* @param cssVariables - Extra CSS variable overrides from `createApp({
|
|
35
|
+
* @param cssVariables - Extra CSS variable overrides from `createApp({ cssVariables })`
|
|
36
36
|
* @returns CSS string to inject in `<head>`, or empty string if nothing to inject
|
|
37
37
|
*
|
|
38
38
|
* Uses `:root:root` and `:root.dark` selectors for higher specificity than
|
package/src/lib/timeline.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Timeline Data Assembly
|
|
3
3
|
*
|
|
4
4
|
* Shared helper for assembling timeline items with media and thread previews.
|
|
5
|
-
* Used by
|
|
5
|
+
* Used by page rendering with page-based pagination.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Context } from "hono";
|
|
9
|
-
import type { Bindings, TimelineItemView
|
|
9
|
+
import type { Bindings, TimelineItemView } from "../types.js";
|
|
10
10
|
import type { AppVariables } from "../app.js";
|
|
11
11
|
import { buildMediaMap } from "./media-helpers.js";
|
|
12
12
|
import { createMediaContext, toPostView, toPostViews } from "./view.js";
|
|
@@ -20,51 +20,58 @@ const DEFAULT_PAGE_SIZE = 20;
|
|
|
20
20
|
*/
|
|
21
21
|
export interface TimelineResult {
|
|
22
22
|
items: TimelineItemView[];
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
currentPage: number;
|
|
24
|
+
totalPages: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Assembles a page of timeline items with media attachments and thread previews.
|
|
29
29
|
*
|
|
30
|
-
* Fetches posts, batch-loads media, identifies
|
|
31
|
-
* render-ready `TimelineItemView[]` with
|
|
30
|
+
* Fetches posts using offset-based pagination, batch-loads media, identifies
|
|
31
|
+
* threads, and returns render-ready `TimelineItemView[]` with page info.
|
|
32
32
|
*
|
|
33
33
|
* @param c - Hono context (provides services + env)
|
|
34
|
-
* @param options - Optional
|
|
34
|
+
* @param options - Optional page number (1-indexed, defaults to 1)
|
|
35
35
|
* @returns Assembled timeline items with pagination info
|
|
36
36
|
*
|
|
37
37
|
* @example
|
|
38
38
|
* ```ts
|
|
39
|
-
* const { items,
|
|
40
|
-
* const { items,
|
|
39
|
+
* const { items, currentPage, totalPages } = await assembleTimeline(c);
|
|
40
|
+
* const { items, currentPage, totalPages } = await assembleTimeline(c, { page: 2 });
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
43
|
export async function assembleTimeline(
|
|
44
44
|
c: Context<Env>,
|
|
45
|
-
options?: {
|
|
45
|
+
options?: { page?: number },
|
|
46
46
|
): Promise<TimelineResult> {
|
|
47
47
|
const pageSize =
|
|
48
48
|
parseInt(c.env.PAGE_SIZE ?? String(DEFAULT_PAGE_SIZE), 10) ||
|
|
49
49
|
DEFAULT_PAGE_SIZE;
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
const
|
|
51
|
+
const page = Math.max(1, options?.page ?? 1);
|
|
52
|
+
const offset = (page - 1) * pageSize;
|
|
53
|
+
|
|
54
|
+
// Get total count for pagination
|
|
55
|
+
const totalCount = await c.var.services.posts.count({
|
|
53
56
|
status: "published",
|
|
54
57
|
excludeReplies: true,
|
|
55
|
-
limit: pageSize + 1,
|
|
56
|
-
cursor: options?.cursor,
|
|
57
58
|
});
|
|
59
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
+
// Fetch posts for the current page
|
|
62
|
+
const posts = await c.var.services.posts.list({
|
|
63
|
+
status: "published",
|
|
64
|
+
excludeReplies: true,
|
|
65
|
+
limit: pageSize,
|
|
66
|
+
offset,
|
|
67
|
+
});
|
|
61
68
|
|
|
62
|
-
if (
|
|
63
|
-
return { items: [],
|
|
69
|
+
if (posts.length === 0) {
|
|
70
|
+
return { items: [], currentPage: page, totalPages };
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
// Batch load media attachments
|
|
67
|
-
const postIds =
|
|
74
|
+
const postIds = posts.map((p) => p.id);
|
|
68
75
|
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
69
76
|
const mediaCtx = createMediaContext(c);
|
|
70
77
|
const mediaMap = buildMediaMap(
|
|
@@ -102,7 +109,7 @@ export async function assembleTimeline(
|
|
|
102
109
|
: new Map();
|
|
103
110
|
|
|
104
111
|
// Assemble timeline items with View Models
|
|
105
|
-
const items: TimelineItemView[] =
|
|
112
|
+
const items: TimelineItemView[] = posts.map((post) => {
|
|
106
113
|
const postView = toPostView(
|
|
107
114
|
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
108
115
|
mediaCtx,
|
|
@@ -130,41 +137,5 @@ export async function assembleTimeline(
|
|
|
130
137
|
return { post: postView };
|
|
131
138
|
});
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
const lastPost = displayPosts[displayPosts.length - 1];
|
|
135
|
-
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
136
|
-
|
|
137
|
-
return { items, hasMore, nextCursor };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Groups timeline items by their publication date (YYYY-MM-DD).
|
|
142
|
-
*
|
|
143
|
-
* @param items - Timeline items to group
|
|
144
|
-
* @returns Array of date groups, each containing items published on the same day
|
|
145
|
-
*
|
|
146
|
-
* @example
|
|
147
|
-
* ```ts
|
|
148
|
-
* const groups = groupByDate(items);
|
|
149
|
-
* // [{ dateKey: "2024-02-01", label: "Feb 1, 2024", items: [...] }, ...]
|
|
150
|
-
* ```
|
|
151
|
-
*/
|
|
152
|
-
export function groupByDate(items: TimelineItemView[]): DateGroup[] {
|
|
153
|
-
const groups: DateGroup[] = [];
|
|
154
|
-
let current: DateGroup | null = null;
|
|
155
|
-
|
|
156
|
-
for (const item of items) {
|
|
157
|
-
const dateKey = item.post.publishedAt.slice(0, 10);
|
|
158
|
-
if (!current || current.dateKey !== dateKey) {
|
|
159
|
-
current = {
|
|
160
|
-
dateKey,
|
|
161
|
-
label: item.post.publishedAtFormatted,
|
|
162
|
-
items: [],
|
|
163
|
-
};
|
|
164
|
-
groups.push(current);
|
|
165
|
-
}
|
|
166
|
-
current.items.push(item);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return groups;
|
|
140
|
+
return { items, currentPage: page, totalPages };
|
|
170
141
|
}
|