@jant/core 0.3.26 → 0.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/client.css +1 -0
- package/dist/client/client.js +31561 -0
- package/dist/index.js +15209 -15
- package/package.json +21 -15
- package/src/__tests__/helpers/app.ts +19 -3
- package/src/__tests__/helpers/db.ts +44 -0
- package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
- package/src/app.tsx +112 -173
- package/src/auth.ts +4 -1
- package/src/client.ts +13 -0
- package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
- package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
- package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
- package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
- package/src/db/schema.ts +24 -4
- package/src/i18n/locales/en.po +810 -385
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +733 -522
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +733 -522
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +7 -11
- package/src/index.ts +1 -1
- package/src/lib/__tests__/icons.test.ts +178 -0
- package/src/lib/__tests__/resolve-config.test.ts +184 -0
- package/src/lib/__tests__/schemas.test.ts +12 -6
- package/src/lib/__tests__/theme.test.ts +62 -0
- package/src/lib/__tests__/timezones.test.ts +1 -1
- package/src/lib/__tests__/url.test.ts +12 -0
- package/src/lib/__tests__/view.test.ts +1 -5
- package/src/lib/avatar-upload.ts +18 -10
- package/src/lib/collection-form-bridge.ts +52 -0
- package/src/lib/collections-reorder.ts +28 -0
- package/src/lib/compose-bridge.ts +251 -0
- package/src/lib/errors.ts +116 -0
- package/src/lib/excerpt.ts +1 -1
- package/src/lib/favicon.ts +3 -5
- package/src/lib/html.ts +22 -0
- package/src/lib/icon-catalog.ts +181 -0
- package/src/lib/icons.ts +202 -0
- package/src/lib/navigation.ts +18 -33
- package/src/lib/pagination.ts +3 -2
- package/src/lib/post-form-bridge.ts +136 -0
- package/src/lib/render.tsx +11 -4
- package/src/lib/resolve-config.ts +157 -0
- package/src/lib/schemas.ts +76 -12
- package/src/lib/settings-bridge.ts +139 -0
- package/src/lib/storage.ts +37 -16
- package/src/lib/theme.ts +5 -7
- package/src/lib/timeline.ts +4 -8
- package/src/lib/toast.ts +134 -0
- package/src/lib/upload.ts +71 -0
- package/src/lib/url.ts +9 -1
- package/src/lib/version.ts +16 -0
- package/src/lib/view.ts +9 -10
- package/src/middleware/__tests__/auth.test.ts +6 -28
- package/src/middleware/__tests__/onboarding.test.ts +1 -1
- package/src/middleware/auth.ts +6 -12
- package/src/middleware/config.ts +51 -0
- package/src/middleware/error-handler.ts +56 -0
- package/src/middleware/onboarding.ts +1 -1
- package/src/preset.css +6 -0
- package/src/routes/__tests__/compose.test.ts +104 -17
- package/src/routes/api/__tests__/collections.test.ts +93 -2
- package/src/routes/api/__tests__/posts.test.ts +2 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/collections.ts +64 -68
- package/src/routes/api/nav-items.ts +21 -59
- package/src/routes/api/pages.ts +18 -46
- package/src/routes/api/posts.ts +64 -86
- package/src/routes/api/search.ts +6 -4
- package/src/routes/api/settings.ts +8 -24
- package/src/routes/api/upload.ts +55 -53
- package/src/routes/auth/__tests__/setup.test.ts +118 -0
- package/src/routes/auth/reset.tsx +17 -66
- package/src/routes/auth/setup.tsx +67 -11
- package/src/routes/auth/signin.tsx +44 -8
- package/src/routes/compose.tsx +194 -0
- package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
- package/src/routes/dash/__tests__/pages.test.ts +2 -2
- package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
- package/src/routes/dash/appearance.tsx +173 -0
- package/src/routes/dash/collections.tsx +80 -14
- package/src/routes/dash/index.tsx +12 -14
- package/src/routes/dash/media.tsx +46 -49
- package/src/routes/dash/pages.tsx +85 -37
- package/src/routes/dash/posts.tsx +60 -23
- package/src/routes/dash/redirects.tsx +43 -33
- package/src/routes/dash/settings.tsx +234 -214
- package/src/routes/feed/__tests__/rss.test.ts +7 -3
- package/src/routes/feed/rss.ts +11 -16
- package/src/routes/feed/sitemap.ts +15 -9
- package/src/routes/pages/__tests__/collections.test.ts +9 -8
- package/src/routes/pages/archive.tsx +2 -2
- package/src/routes/pages/collection.tsx +76 -9
- package/src/routes/pages/collections.tsx +3 -1
- package/src/routes/pages/featured.tsx +2 -2
- package/src/routes/pages/home.tsx +3 -3
- package/src/routes/pages/latest.tsx +2 -2
- package/src/routes/pages/page.tsx +2 -2
- package/src/routes/pages/post.tsx +2 -2
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +324 -34
- package/src/services/__tests__/media.test.ts +1 -1
- package/src/services/__tests__/page.test.ts +116 -1
- package/src/services/auth.ts +88 -0
- package/src/services/collection.ts +169 -30
- package/src/services/index.ts +8 -3
- package/src/services/media.ts +39 -12
- package/src/services/navigation.ts +17 -5
- package/src/services/page.ts +24 -4
- package/src/services/post.ts +87 -19
- package/src/services/search.ts +0 -1
- package/src/services/settings.ts +21 -13
- package/src/style.css +3 -0
- package/src/styles/components.css +42 -1
- package/src/styles/tokens.css +4 -0
- package/src/styles/ui.css +902 -73
- package/src/types/app-context.ts +25 -0
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +60 -23
- package/src/types/entities.ts +12 -2
- package/src/types/lingui-react-macro.d.ts +3 -3
- package/src/types/operations.ts +2 -4
- package/src/types/views.ts +1 -3
- package/src/ui/__tests__/font-themes.test.ts +27 -8
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
- package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
- package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
- package/src/ui/components/collection-types.ts +45 -0
- package/src/ui/components/compose-types.ts +75 -0
- package/src/ui/components/jant-collection-form.ts +512 -0
- package/src/ui/components/jant-compose-dialog.ts +494 -0
- package/src/ui/components/jant-compose-editor.ts +799 -0
- package/src/ui/components/jant-post-form.ts +290 -0
- package/src/ui/components/jant-settings-avatar.ts +231 -0
- package/src/ui/components/jant-settings-general.ts +436 -0
- package/src/ui/components/post-form-template.ts +260 -0
- package/src/ui/components/post-form-types.ts +87 -0
- package/src/ui/components/settings-types.ts +62 -0
- package/src/ui/compose/ComposeDialog.tsx +141 -385
- package/src/ui/compose/ComposePrompt.tsx +3 -3
- package/src/ui/dash/PostList.tsx +55 -61
- package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
- package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
- package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
- package/src/ui/dash/collections/CollectionForm.tsx +130 -117
- package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
- package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
- package/src/ui/dash/index.ts +1 -1
- package/src/ui/dash/posts/PostForm.tsx +248 -0
- package/src/ui/dash/settings/AccountContent.tsx +69 -80
- package/src/ui/dash/settings/GeneralContent.tsx +159 -478
- package/src/ui/dash/settings/SettingsNav.tsx +4 -4
- package/src/ui/font-themes.ts +115 -32
- package/src/ui/layouts/BaseLayout.tsx +49 -19
- package/src/ui/layouts/DashLayout.tsx +14 -9
- package/src/ui/layouts/SiteLayout.tsx +38 -23
- package/src/ui/pages/CollectionPage.tsx +12 -2
- package/src/ui/pages/CollectionsPage.tsx +27 -27
- package/src/ui/pages/HomePage.tsx +15 -6
- package/src/ui/pages/SearchPage.tsx +1 -2
- package/src/ui/shared/CollectionsSidebar.tsx +59 -0
- package/src/ui/shared/Pagination.tsx +2 -2
- package/dist/app.js +0 -265
- package/dist/auth.js +0 -36
- package/dist/client.js +0 -13
- package/dist/db/index.js +0 -10
- package/dist/db/schema.js +0 -224
- package/dist/i18n/Trans.js +0 -24
- package/dist/i18n/context.js +0 -58
- package/dist/i18n/detect.js +0 -26
- package/dist/i18n/i18n.js +0 -49
- package/dist/i18n/index.js +0 -44
- package/dist/i18n/locales/en.js +0 -1
- package/dist/i18n/locales/zh-Hans.js +0 -1
- package/dist/i18n/locales/zh-Hant.js +0 -1
- package/dist/i18n/locales.js +0 -13
- package/dist/i18n/middleware.js +0 -30
- package/dist/lib/avatar-upload.js +0 -134
- package/dist/lib/config.js +0 -143
- package/dist/lib/constants.js +0 -50
- package/dist/lib/excerpt.js +0 -76
- package/dist/lib/favicon.js +0 -102
- package/dist/lib/feed.js +0 -123
- package/dist/lib/image-processor.js +0 -187
- package/dist/lib/image.js +0 -97
- package/dist/lib/index.js +0 -7
- package/dist/lib/markdown.js +0 -83
- package/dist/lib/media-helpers.js +0 -49
- package/dist/lib/media-upload.js +0 -104
- package/dist/lib/nav-reorder.js +0 -27
- package/dist/lib/navigation.js +0 -79
- package/dist/lib/pagination.js +0 -44
- package/dist/lib/render.js +0 -53
- package/dist/lib/schemas.js +0 -174
- package/dist/lib/sqid.js +0 -72
- package/dist/lib/sse.js +0 -218
- package/dist/lib/storage.js +0 -164
- package/dist/lib/theme.js +0 -65
- package/dist/lib/time.js +0 -159
- package/dist/lib/timeline.js +0 -95
- package/dist/lib/timezones.js +0 -388
- package/dist/lib/url.js +0 -89
- package/dist/lib/view.js +0 -217
- package/dist/middleware/auth.js +0 -52
- package/dist/middleware/onboarding.js +0 -41
- package/dist/routes/api/collections.js +0 -124
- package/dist/routes/api/nav-items.js +0 -104
- package/dist/routes/api/pages.js +0 -91
- package/dist/routes/api/posts.js +0 -218
- package/dist/routes/api/search.js +0 -48
- package/dist/routes/api/settings.js +0 -68
- package/dist/routes/api/upload.js +0 -246
- package/dist/routes/auth/reset.js +0 -221
- package/dist/routes/auth/setup.js +0 -194
- package/dist/routes/auth/signin.js +0 -176
- package/dist/routes/compose.js +0 -48
- package/dist/routes/dash/collections.js +0 -115
- package/dist/routes/dash/index.js +0 -118
- package/dist/routes/dash/media.js +0 -106
- package/dist/routes/dash/pages.js +0 -294
- package/dist/routes/dash/posts.js +0 -244
- package/dist/routes/dash/redirects.js +0 -257
- package/dist/routes/dash/settings.js +0 -379
- package/dist/routes/feed/rss.js +0 -62
- package/dist/routes/feed/sitemap.js +0 -49
- package/dist/routes/pages/archive.js +0 -62
- package/dist/routes/pages/collection.js +0 -34
- package/dist/routes/pages/collections.js +0 -28
- package/dist/routes/pages/featured.js +0 -36
- package/dist/routes/pages/home.js +0 -64
- package/dist/routes/pages/latest.js +0 -45
- package/dist/routes/pages/page.js +0 -68
- package/dist/routes/pages/post.js +0 -44
- package/dist/routes/pages/search.js +0 -54
- package/dist/services/collection.js +0 -109
- package/dist/services/index.js +0 -24
- package/dist/services/media.js +0 -117
- package/dist/services/navigation.js +0 -91
- package/dist/services/page.js +0 -84
- package/dist/services/post.js +0 -229
- package/dist/services/redirect.js +0 -48
- package/dist/services/search.js +0 -67
- package/dist/services/settings.js +0 -68
- package/dist/types/bindings.js +0 -3
- package/dist/types/config.js +0 -147
- package/dist/types/constants.js +0 -27
- package/dist/types/entities.js +0 -3
- package/dist/types/lingui-react-macro.d.js +0 -9
- package/dist/types/operations.js +0 -3
- package/dist/types/props.js +0 -3
- package/dist/types/sortablejs.d.js +0 -5
- package/dist/types/views.js +0 -5
- package/dist/types.js +0 -11
- package/dist/ui/color-themes.js +0 -268
- package/dist/ui/compose/ComposeDialog.js +0 -467
- package/dist/ui/compose/ComposePrompt.js +0 -55
- package/dist/ui/dash/ActionButtons.js +0 -46
- package/dist/ui/dash/CrudPageHeader.js +0 -22
- package/dist/ui/dash/DangerZone.js +0 -36
- package/dist/ui/dash/FormatBadge.js +0 -27
- package/dist/ui/dash/ListItemRow.js +0 -21
- package/dist/ui/dash/PageForm.js +0 -195
- package/dist/ui/dash/PostForm.js +0 -395
- package/dist/ui/dash/PostList.js +0 -83
- package/dist/ui/dash/StatusBadge.js +0 -46
- package/dist/ui/dash/collections/CollectionForm.js +0 -152
- package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
- package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
- package/dist/ui/dash/index.js +0 -10
- package/dist/ui/dash/media/MediaListContent.js +0 -166
- package/dist/ui/dash/media/ViewMediaContent.js +0 -212
- package/dist/ui/dash/pages/LinkFormContent.js +0 -130
- package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
- package/dist/ui/dash/settings/AccountContent.js +0 -209
- package/dist/ui/dash/settings/AppearanceContent.js +0 -259
- package/dist/ui/dash/settings/GeneralContent.js +0 -536
- package/dist/ui/dash/settings/SettingsNav.js +0 -41
- package/dist/ui/feed/LinkCard.js +0 -72
- package/dist/ui/feed/NoteCard.js +0 -58
- package/dist/ui/feed/QuoteCard.js +0 -63
- package/dist/ui/feed/ThreadPreview.js +0 -48
- package/dist/ui/feed/TimelineFeed.js +0 -41
- package/dist/ui/feed/TimelineItem.js +0 -27
- package/dist/ui/font-themes.js +0 -36
- package/dist/ui/layouts/BaseLayout.js +0 -153
- package/dist/ui/layouts/DashLayout.js +0 -141
- package/dist/ui/layouts/SiteLayout.js +0 -169
- package/dist/ui/pages/ArchivePage.js +0 -143
- package/dist/ui/pages/CollectionPage.js +0 -70
- package/dist/ui/pages/CollectionsPage.js +0 -76
- package/dist/ui/pages/FeaturedPage.js +0 -24
- package/dist/ui/pages/HomePage.js +0 -24
- package/dist/ui/pages/PostPage.js +0 -55
- package/dist/ui/pages/SearchPage.js +0 -122
- package/dist/ui/pages/SinglePage.js +0 -23
- package/dist/ui/shared/EmptyState.js +0 -27
- package/dist/ui/shared/MediaGallery.js +0 -35
- package/dist/ui/shared/Pagination.js +0 -195
- package/dist/ui/shared/ThreadView.js +0 -108
- package/dist/ui/shared/index.js +0 -5
- package/dist/vendor/datastar.js +0 -1606
- package/src/lib/__tests__/config.test.ts +0 -192
- package/src/lib/config.ts +0 -167
- package/src/routes/compose.ts +0 -63
- package/src/ui/dash/PostForm.tsx +0 -360
- package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
package/src/lib/toast.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast Utility
|
|
3
|
+
*
|
|
4
|
+
* Shared showToast() for all client-side bridge modules.
|
|
5
|
+
* Appends a temporary notification to `#toast-container`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const TOAST_ICONS = {
|
|
9
|
+
success:
|
|
10
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>',
|
|
11
|
+
error:
|
|
12
|
+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg>',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Show a toast notification.
|
|
17
|
+
*
|
|
18
|
+
* @param message - Text to display
|
|
19
|
+
* @param type - Visual style: "success" (default) or "error"
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* showToast("Saved successfully.");
|
|
23
|
+
* showToast("Something went wrong", "error");
|
|
24
|
+
*/
|
|
25
|
+
export function showToast(
|
|
26
|
+
message: string,
|
|
27
|
+
type: "success" | "error" = "success",
|
|
28
|
+
): void {
|
|
29
|
+
if (!message) return;
|
|
30
|
+
|
|
31
|
+
const container = document.getElementById("toast-container");
|
|
32
|
+
if (!container) return;
|
|
33
|
+
|
|
34
|
+
const toast = document.createElement("div");
|
|
35
|
+
toast.className = `toast toast-${type}`;
|
|
36
|
+
toast.innerHTML = `${TOAST_ICONS[type]}<span>${message}</span>`;
|
|
37
|
+
container.appendChild(toast);
|
|
38
|
+
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
toast.classList.add("toast-out");
|
|
41
|
+
toast.addEventListener("animationend", () => toast.remove());
|
|
42
|
+
}, 3000);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Show a persistent toast that stays until explicitly dismissed.
|
|
47
|
+
*
|
|
48
|
+
* @param id - Unique identifier for updating/dismissing later
|
|
49
|
+
* @param message - Text to display
|
|
50
|
+
* @param type - Visual style: "success" (default) or "error"
|
|
51
|
+
* @returns The toast element
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* showPersistentToast("upload", "Uploading...");
|
|
55
|
+
*/
|
|
56
|
+
export function showPersistentToast(
|
|
57
|
+
id: string,
|
|
58
|
+
message: string,
|
|
59
|
+
type: "success" | "error" = "success",
|
|
60
|
+
): HTMLElement | null {
|
|
61
|
+
const container = document.getElementById("toast-container");
|
|
62
|
+
if (!container) return null;
|
|
63
|
+
|
|
64
|
+
const toast = document.createElement("div");
|
|
65
|
+
toast.className = `toast toast-${type}`;
|
|
66
|
+
toast.id = `toast-${id}`;
|
|
67
|
+
toast.innerHTML = `${TOAST_ICONS[type]}<span>${message}</span>`;
|
|
68
|
+
container.appendChild(toast);
|
|
69
|
+
|
|
70
|
+
return toast;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Update the message of an existing persistent toast.
|
|
75
|
+
*
|
|
76
|
+
* @param id - The toast identifier
|
|
77
|
+
* @param message - New message text
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* updateToast("upload", "Almost done...");
|
|
81
|
+
*/
|
|
82
|
+
export function updateToast(id: string, message: string): void {
|
|
83
|
+
const toast = document.getElementById(`toast-${id}`);
|
|
84
|
+
if (!toast) return;
|
|
85
|
+
|
|
86
|
+
const span = toast.querySelector("span");
|
|
87
|
+
if (span) span.textContent = message;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Dismiss a persistent toast with fadeout animation.
|
|
92
|
+
*
|
|
93
|
+
* @param id - The toast identifier
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* dismissToast("upload");
|
|
97
|
+
*/
|
|
98
|
+
export function dismissToast(id: string): void {
|
|
99
|
+
const toast = document.getElementById(`toast-${id}`);
|
|
100
|
+
if (!toast) return;
|
|
101
|
+
|
|
102
|
+
toast.classList.add("toast-out");
|
|
103
|
+
toast.addEventListener("animationend", () => toast.remove());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Replace a persistent toast with an auto-dismissing one.
|
|
108
|
+
*
|
|
109
|
+
* @param id - The toast identifier
|
|
110
|
+
* @param message - New message text
|
|
111
|
+
* @param type - Visual style: "success" (default) or "error"
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* replaceWithAutoClose("upload", "Published!", "success");
|
|
115
|
+
*/
|
|
116
|
+
export function replaceWithAutoClose(
|
|
117
|
+
id: string,
|
|
118
|
+
message: string,
|
|
119
|
+
type: "success" | "error" = "success",
|
|
120
|
+
): void {
|
|
121
|
+
const toast = document.getElementById(`toast-${id}`);
|
|
122
|
+
if (!toast) {
|
|
123
|
+
showToast(message, type);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
toast.className = `toast toast-${type}`;
|
|
128
|
+
toast.innerHTML = `${TOAST_ICONS[type]}<span>${message}</span>`;
|
|
129
|
+
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
toast.classList.add("toast-out");
|
|
132
|
+
toast.addEventListener("animationend", () => toast.remove());
|
|
133
|
+
}, 3000);
|
|
134
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared file validation and storage key generation for upload routes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { uuidv7 } from "uuidv7";
|
|
8
|
+
|
|
9
|
+
/** MIME types allowed for upload */
|
|
10
|
+
const ALLOWED_UPLOAD_TYPES = [
|
|
11
|
+
"image/jpeg",
|
|
12
|
+
"image/png",
|
|
13
|
+
"image/gif",
|
|
14
|
+
"image/webp",
|
|
15
|
+
"image/svg+xml",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
/** Maximum file size in bytes (10MB) */
|
|
19
|
+
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates an uploaded file's type and size.
|
|
23
|
+
*
|
|
24
|
+
* @param file - The uploaded File object
|
|
25
|
+
* @returns null if valid, error message string if invalid
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const error = validateUploadFile(file);
|
|
29
|
+
* if (error) return dsToast(error, "error");
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function validateUploadFile(file: File): string | null {
|
|
33
|
+
if (
|
|
34
|
+
!ALLOWED_UPLOAD_TYPES.includes(
|
|
35
|
+
file.type as (typeof ALLOWED_UPLOAD_TYPES)[number],
|
|
36
|
+
)
|
|
37
|
+
) {
|
|
38
|
+
return "File type not allowed.";
|
|
39
|
+
}
|
|
40
|
+
if (file.size > MAX_UPLOAD_SIZE) {
|
|
41
|
+
return "File too large (max 10MB).";
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generates a unique storage key for an uploaded file.
|
|
48
|
+
* Format: `media/YYYY/MM/uuid.ext`
|
|
49
|
+
*
|
|
50
|
+
* @param originalFilename - Original filename to extract extension from
|
|
51
|
+
* @returns Object with generated id, filename, and storageKey
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const { id, filename, storageKey } = generateStorageKey("photo.jpg");
|
|
55
|
+
* // { id: "0192...", filename: "0192....jpg", storageKey: "media/2025/01/0192....jpg" }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function generateStorageKey(originalFilename: string): {
|
|
59
|
+
id: string;
|
|
60
|
+
filename: string;
|
|
61
|
+
storageKey: string;
|
|
62
|
+
} {
|
|
63
|
+
const ext = originalFilename.split(".").pop() || "bin";
|
|
64
|
+
const id = uuidv7();
|
|
65
|
+
const date = new Date();
|
|
66
|
+
const year = date.getUTCFullYear();
|
|
67
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
68
|
+
const filename = `${id}.${ext}`;
|
|
69
|
+
const storageKey = `media/${year}/${month}/${filename}`;
|
|
70
|
+
return { id, filename, storageKey };
|
|
71
|
+
}
|
package/src/lib/url.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* URL Utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { pinyin } from "pinyin-pro";
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Extracts the hostname (domain) from a URL string.
|
|
7
9
|
*
|
|
@@ -98,7 +100,13 @@ export function isFullUrl(str: string): boolean {
|
|
|
98
100
|
* ```
|
|
99
101
|
*/
|
|
100
102
|
export function slugify(text: string): string {
|
|
101
|
-
|
|
103
|
+
// Replace CJK characters with their pinyin equivalents, preserving non-CJK text
|
|
104
|
+
const converted = text.replace(
|
|
105
|
+
/[\u4e00-\u9fff\u3400-\u4dbf]+/g,
|
|
106
|
+
(match) => ` ${pinyin(match, { toneType: "none", separator: " " })} `,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return converted
|
|
102
110
|
.toLowerCase()
|
|
103
111
|
.trim()
|
|
104
112
|
.replace(/[^\w\s-]/g, "")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version and environment detection
|
|
3
|
+
*
|
|
4
|
+
* In Vite dev, `__JANT_DEV__` is replaced with `true` via Vite's `define` config.
|
|
5
|
+
* In production (wrangler/esbuild), the typeof check evaluates to false safely.
|
|
6
|
+
*
|
|
7
|
+
* `__JANT_VERSION__` is replaced by Vite's `define` during both dev and lib build.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
declare const __JANT_DEV__: boolean | undefined;
|
|
11
|
+
declare const __JANT_VERSION__: string;
|
|
12
|
+
|
|
13
|
+
export const IS_VITE_DEV =
|
|
14
|
+
typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
|
|
15
|
+
|
|
16
|
+
export const CORE_VERSION = __JANT_VERSION__;
|
package/src/lib/view.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Theme components receive only View types -- no lib/ imports needed.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Context } from "hono";
|
|
9
8
|
import type {
|
|
10
9
|
Post,
|
|
11
10
|
PostWithMedia,
|
|
@@ -22,6 +21,7 @@ import type {
|
|
|
22
21
|
Format,
|
|
23
22
|
Status,
|
|
24
23
|
NavItemType,
|
|
24
|
+
AppConfig,
|
|
25
25
|
} from "../types.js";
|
|
26
26
|
import { encode } from "./sqid.js";
|
|
27
27
|
import {
|
|
@@ -38,7 +38,7 @@ import { getHtmlExcerpt } from "./excerpt.js";
|
|
|
38
38
|
// =============================================================================
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Central media config -- extracted once per request from
|
|
41
|
+
* Central media config -- extracted once per request from appConfig.
|
|
42
42
|
*/
|
|
43
43
|
export interface MediaContext {
|
|
44
44
|
r2PublicUrl?: string;
|
|
@@ -47,16 +47,16 @@ export interface MediaContext {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Creates a MediaContext from
|
|
50
|
+
* Creates a MediaContext from AppConfig.
|
|
51
51
|
*
|
|
52
|
-
* @param
|
|
53
|
-
* @returns MediaContext with
|
|
52
|
+
* @param appConfig - Resolved app configuration
|
|
53
|
+
* @returns MediaContext with URL values
|
|
54
54
|
*/
|
|
55
|
-
export function createMediaContext(
|
|
55
|
+
export function createMediaContext(appConfig: AppConfig): MediaContext {
|
|
56
56
|
return {
|
|
57
|
-
r2PublicUrl:
|
|
58
|
-
imageTransformUrl:
|
|
59
|
-
s3PublicUrl:
|
|
57
|
+
r2PublicUrl: appConfig.r2PublicUrl || undefined,
|
|
58
|
+
imageTransformUrl: appConfig.imageTransformUrl || undefined,
|
|
59
|
+
s3PublicUrl: appConfig.s3PublicUrl || undefined,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -154,7 +154,6 @@ export function toPostView(post: PostWithMedia, _ctx: MediaContext): PostView {
|
|
|
154
154
|
featured: post.featured === 1,
|
|
155
155
|
pinned: post.pinned === 1,
|
|
156
156
|
rating: post.rating ?? undefined,
|
|
157
|
-
collectionId: post.collectionId ?? undefined,
|
|
158
157
|
publishedAt: toISOString(post.publishedAt),
|
|
159
158
|
publishedAtFormatted: formatDate(post.publishedAt),
|
|
160
159
|
publishedAtTime: formatTime(post.publishedAt),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { requireAuth, requireAuthApi } from "../auth.js";
|
|
4
|
+
import { errorHandler } from "../error-handler.js";
|
|
4
5
|
import type { Bindings } from "../../types.js";
|
|
5
|
-
import type { AppVariables } from "../../app.js";
|
|
6
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
6
7
|
|
|
7
8
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
8
9
|
|
|
@@ -59,23 +60,12 @@ describe("requireAuth", () => {
|
|
|
59
60
|
expect(res.status).toBe(302);
|
|
60
61
|
expect(res.headers.get("Location")).toBe("/login");
|
|
61
62
|
});
|
|
62
|
-
|
|
63
|
-
it("redirects when auth is not configured", async () => {
|
|
64
|
-
const app = new Hono<Env>();
|
|
65
|
-
app.use("*", async (c, next) => {
|
|
66
|
-
// auth not set (undefined)
|
|
67
|
-
await next();
|
|
68
|
-
});
|
|
69
|
-
app.get("/dash", requireAuth(), (c) => c.text("Dashboard"));
|
|
70
|
-
|
|
71
|
-
const res = await app.request("/dash", { redirect: "manual" });
|
|
72
|
-
expect(res.status).toBe(302);
|
|
73
|
-
});
|
|
74
63
|
});
|
|
75
64
|
|
|
76
65
|
describe("requireAuthApi", () => {
|
|
77
66
|
it("allows authenticated requests", async () => {
|
|
78
67
|
const app = new Hono<Env>();
|
|
68
|
+
app.onError(errorHandler);
|
|
79
69
|
app.use("*", async (c, next) => {
|
|
80
70
|
c.set("auth", createMockAuth(true));
|
|
81
71
|
await next();
|
|
@@ -91,6 +81,7 @@ describe("requireAuthApi", () => {
|
|
|
91
81
|
|
|
92
82
|
it("returns 401 for unauthenticated requests", async () => {
|
|
93
83
|
const app = new Hono<Env>();
|
|
84
|
+
app.onError(errorHandler);
|
|
94
85
|
app.use("*", async (c, next) => {
|
|
95
86
|
c.set("auth", createMockAuth(false));
|
|
96
87
|
await next();
|
|
@@ -102,25 +93,12 @@ describe("requireAuthApi", () => {
|
|
|
102
93
|
|
|
103
94
|
const body = await res.json();
|
|
104
95
|
expect(body.error).toBe("Unauthorized");
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
it("returns 500 when auth is not configured", async () => {
|
|
108
|
-
const app = new Hono<Env>();
|
|
109
|
-
app.use("*", async (c, next) => {
|
|
110
|
-
// auth not set (undefined)
|
|
111
|
-
await next();
|
|
112
|
-
});
|
|
113
|
-
app.get("/api/data", requireAuthApi(), (c) => c.json({ data: "secret" }));
|
|
114
|
-
|
|
115
|
-
const res = await app.request("/api/data");
|
|
116
|
-
expect(res.status).toBe(500);
|
|
117
|
-
|
|
118
|
-
const body = await res.json();
|
|
119
|
-
expect(body.error).toBe("Authentication not configured");
|
|
96
|
+
expect(body.code).toBe("UNAUTHORIZED");
|
|
120
97
|
});
|
|
121
98
|
|
|
122
99
|
it("returns 401 when getSession throws", async () => {
|
|
123
100
|
const app = new Hono<Env>();
|
|
101
|
+
app.onError(errorHandler);
|
|
124
102
|
app.use("*", async (c, next) => {
|
|
125
103
|
c.set("auth", {
|
|
126
104
|
api: {
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from "vitest";
|
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { requireOnboarding, resetOnboardingCache } from "../onboarding.js";
|
|
4
4
|
import type { Bindings } from "../../types.js";
|
|
5
|
-
import type { AppVariables } from "../../app.js";
|
|
5
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
6
6
|
|
|
7
7
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
8
8
|
|
package/src/middleware/auth.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import type { MiddlewareHandler } from "hono";
|
|
8
8
|
import type { Bindings } from "../types.js";
|
|
9
|
-
import type { AppVariables } from "../app.js";
|
|
9
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
10
|
+
import { DomainError, UnauthorizedError } from "../lib/errors.js";
|
|
10
11
|
|
|
11
12
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
12
13
|
|
|
@@ -16,10 +17,6 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
16
17
|
*/
|
|
17
18
|
export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
|
|
18
19
|
return async (c, next) => {
|
|
19
|
-
if (!c.var.auth) {
|
|
20
|
-
return c.redirect(redirectTo);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
try {
|
|
24
21
|
const session = await c.var.auth.api.getSession({
|
|
25
22
|
headers: c.req.raw.headers,
|
|
@@ -42,22 +39,19 @@ export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
|
|
|
42
39
|
*/
|
|
43
40
|
export function requireAuthApi(): MiddlewareHandler<Env> {
|
|
44
41
|
return async (c, next) => {
|
|
45
|
-
if (!c.var.auth) {
|
|
46
|
-
return c.json({ error: "Authentication not configured" }, 500);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
42
|
try {
|
|
50
43
|
const session = await c.var.auth.api.getSession({
|
|
51
44
|
headers: c.req.raw.headers,
|
|
52
45
|
});
|
|
53
46
|
|
|
54
47
|
if (!session?.user) {
|
|
55
|
-
|
|
48
|
+
throw new UnauthorizedError();
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
await next();
|
|
59
|
-
} catch {
|
|
60
|
-
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err instanceof DomainError) throw err;
|
|
54
|
+
throw new UnauthorizedError();
|
|
61
55
|
}
|
|
62
56
|
};
|
|
63
57
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Middleware
|
|
3
|
+
*
|
|
4
|
+
* Loads settings from DB, resolves app config and theme.
|
|
5
|
+
* Apply only to route groups that need config/theme data —
|
|
6
|
+
* skip for /health, /media/*, /favicon.ico, /api/auth/*, etc.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MiddlewareHandler } from "hono";
|
|
10
|
+
import type { Bindings } from "../types.js";
|
|
11
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
12
|
+
import { resolveConfig } from "../lib/resolve-config.js";
|
|
13
|
+
import { buildThemeStyle } from "../lib/theme.js";
|
|
14
|
+
import { BUILTIN_COLOR_THEMES } from "../ui/color-themes.js";
|
|
15
|
+
import { BUILTIN_FONT_THEMES } from "../ui/font-themes.js";
|
|
16
|
+
|
|
17
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Middleware that loads settings, resolves app config, and builds theme CSS.
|
|
21
|
+
*
|
|
22
|
+
* Sets `allSettings`, `appConfig`, and `themeStyle` on the Hono context.
|
|
23
|
+
*/
|
|
24
|
+
export function withConfig(): MiddlewareHandler<Env> {
|
|
25
|
+
return async (c, next) => {
|
|
26
|
+
const allSettings = await c.var.services.settings.getAll();
|
|
27
|
+
c.set("allSettings", allSettings);
|
|
28
|
+
const appConfig = resolveConfig(c.env, allSettings);
|
|
29
|
+
c.set("appConfig", appConfig);
|
|
30
|
+
|
|
31
|
+
// Resolve active color theme
|
|
32
|
+
const activeTheme = BUILTIN_COLOR_THEMES.find(
|
|
33
|
+
(t) => t.id === (appConfig.themeId || appConfig.defaultThemeId),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Build font override CSS variables
|
|
37
|
+
const fontTheme = appConfig.fontThemeId
|
|
38
|
+
? BUILTIN_FONT_THEMES.find((f) => f.id === appConfig.fontThemeId)
|
|
39
|
+
: undefined;
|
|
40
|
+
const fontOverrides: Record<string, string> = {};
|
|
41
|
+
if (fontTheme) {
|
|
42
|
+
fontOverrides["--font-body"] = fontTheme.bodyFontFamily;
|
|
43
|
+
fontOverrides["--font-heading"] = fontTheme.headingFontFamily;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const themeStyle = buildThemeStyle(activeTheme, fontOverrides);
|
|
47
|
+
c.set("themeStyle", themeStyle);
|
|
48
|
+
|
|
49
|
+
await next();
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Error Handler
|
|
3
|
+
*
|
|
4
|
+
* Maps DomainError subclasses to HTTP responses.
|
|
5
|
+
* API routes receive JSON; page routes fall through to Hono defaults.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ErrorHandler } from "hono";
|
|
9
|
+
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
10
|
+
import type { Bindings } from "../types.js";
|
|
11
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
12
|
+
import { DomainError, NotFoundError, ValidationError } from "../lib/errors.js";
|
|
13
|
+
import { dsToast } from "../lib/sse.js";
|
|
14
|
+
|
|
15
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
16
|
+
|
|
17
|
+
export const errorHandler: ErrorHandler<Env> = (err, c) => {
|
|
18
|
+
// API routes: always return JSON
|
|
19
|
+
if (c.req.path.startsWith("/api/")) {
|
|
20
|
+
if (err instanceof DomainError) {
|
|
21
|
+
const body: Record<string, unknown> = {
|
|
22
|
+
error: err.message,
|
|
23
|
+
code: err.code,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (err instanceof ValidationError && err.details) {
|
|
27
|
+
body.details = err.details;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return c.json(body, err.statusCode as ContentfulStatusCode);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Unknown API error
|
|
34
|
+
// eslint-disable-next-line no-console -- Server error logging is intentional
|
|
35
|
+
console.error("[Jant] Unhandled error:", err);
|
|
36
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Datastar requests: return toast
|
|
40
|
+
if (c.req.header("datastar-request")) {
|
|
41
|
+
if (err instanceof DomainError) {
|
|
42
|
+
return dsToast(err.message, "error");
|
|
43
|
+
}
|
|
44
|
+
// eslint-disable-next-line no-console -- Server error logging is intentional
|
|
45
|
+
console.error("[Jant] Unhandled error:", err);
|
|
46
|
+
return dsToast("An unexpected error occurred", "error");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Non-API routes: map NotFoundError to Hono's built-in 404
|
|
50
|
+
if (err instanceof NotFoundError) {
|
|
51
|
+
return c.notFound();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Everything else: re-throw for Hono's default handling
|
|
55
|
+
throw err;
|
|
56
|
+
};
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { MiddlewareHandler } from "hono";
|
|
11
11
|
import type { Bindings } from "../types.js";
|
|
12
|
-
import type { AppVariables } from "../app.js";
|
|
12
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
13
13
|
|
|
14
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
15
|
|
package/src/preset.css
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
@theme {
|
|
17
17
|
--radius-default: 0.5rem;
|
|
18
18
|
--color-success: var(--success);
|
|
19
|
+
--default-font-family: var(--font-body);
|
|
20
|
+
--default-mono-font-family: var(--font-mono);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
:root {
|
|
@@ -67,4 +69,8 @@
|
|
|
67
69
|
a:hover {
|
|
68
70
|
text-decoration-color: currentColor;
|
|
69
71
|
}
|
|
72
|
+
|
|
73
|
+
:where(h1, h2, h3, h4, h5, h6) {
|
|
74
|
+
font-family: var(--font-heading);
|
|
75
|
+
}
|
|
70
76
|
}
|