@jant/core 0.2.12 → 0.2.13
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/jant.js +3 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +112 -85
- package/dist/auth.d.ts +1 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -1
- package/dist/client.js +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/i18n/context.d.ts.map +1 -1
- package/dist/i18n/context.js +0 -3
- package/dist/i18n/detect.d.ts +0 -11
- package/dist/i18n/detect.d.ts.map +1 -1
- package/dist/i18n/detect.js +1 -52
- package/dist/i18n/i18n.d.ts +4 -14
- package/dist/i18n/i18n.d.ts.map +1 -1
- package/dist/i18n/i18n.js +19 -25
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/middleware.d.ts +2 -5
- package/dist/i18n/middleware.d.ts.map +1 -1
- package/dist/i18n/middleware.js +12 -23
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/image.d.ts.map +1 -1
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/sse.d.ts +45 -17
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +77 -37
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/routes/api/posts.js +0 -1
- package/dist/routes/api/upload.js +3 -1
- package/dist/routes/dash/collections.d.ts.map +1 -1
- package/dist/routes/dash/collections.js +134 -142
- package/dist/routes/dash/index.js +25 -26
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +60 -56
- package/dist/routes/dash/pages.js +64 -66
- package/dist/routes/dash/posts.d.ts.map +1 -1
- package/dist/routes/dash/posts.js +50 -59
- package/dist/routes/dash/redirects.d.ts.map +1 -1
- package/dist/routes/dash/redirects.js +63 -60
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +249 -93
- package/dist/routes/feed/rss.js +6 -4
- package/dist/routes/pages/archive.js +60 -62
- package/dist/routes/pages/collection.js +8 -8
- package/dist/routes/pages/home.js +14 -14
- package/dist/routes/pages/page.js +7 -6
- package/dist/routes/pages/post.js +8 -8
- package/dist/routes/pages/search.js +25 -27
- package/dist/services/collection.d.ts.map +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/post.d.ts.map +1 -1
- package/dist/services/redirect.d.ts.map +1 -1
- package/dist/services/settings.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.d.ts +1 -1
- package/dist/theme/components/ActionButtons.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.js +17 -21
- package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.js +12 -15
- package/dist/theme/components/EmptyState.d.ts.map +1 -1
- package/dist/theme/components/PageForm.d.ts.map +1 -1
- package/dist/theme/components/PageForm.js +58 -56
- package/dist/theme/components/Pagination.d.ts.map +1 -1
- package/dist/theme/components/Pagination.js +22 -25
- package/dist/theme/components/PostForm.d.ts +0 -1
- package/dist/theme/components/PostForm.d.ts.map +1 -1
- package/dist/theme/components/PostForm.js +85 -77
- package/dist/theme/components/PostList.d.ts.map +1 -1
- package/dist/theme/components/PostList.js +17 -17
- package/dist/theme/components/ThreadView.d.ts.map +1 -1
- package/dist/theme/components/ThreadView.js +15 -18
- package/dist/theme/components/TypeBadge.d.ts.map +1 -1
- package/dist/theme/components/TypeBadge.js +20 -20
- package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
- package/dist/theme/components/VisibilityBadge.js +14 -14
- package/dist/theme/components/index.d.ts +1 -1
- package/dist/theme/components/index.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.js +4 -2
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +29 -29
- package/dist/types/lingui-react-macro.d.js +9 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/datastar.js +1606 -0
- package/package.json +5 -2
- package/src/app.tsx +175 -56
- package/src/auth.ts +5 -1
- package/src/client.ts +1 -1
- package/src/db/schema.ts +22 -7
- package/src/i18n/EXAMPLES.md +34 -14
- package/src/i18n/README.md +19 -9
- package/src/i18n/context.tsx +1 -4
- package/src/i18n/detect.ts +1 -67
- package/src/i18n/i18n.ts +15 -19
- package/src/i18n/index.ts +0 -3
- package/src/i18n/middleware.ts +12 -24
- package/src/lib/constants.ts +2 -1
- package/src/lib/image-processor.ts +23 -7
- package/src/lib/image.ts +6 -2
- package/src/lib/schemas.ts +6 -2
- package/src/lib/sse.ts +138 -50
- package/src/middleware/auth.ts +6 -2
- package/src/routes/api/posts.ts +14 -5
- package/src/routes/api/upload.ts +25 -7
- package/src/routes/dash/collections.tsx +162 -70
- package/src/routes/dash/index.tsx +22 -7
- package/src/routes/dash/media.tsx +59 -16
- package/src/routes/dash/pages.tsx +102 -44
- package/src/routes/dash/posts.tsx +87 -54
- package/src/routes/dash/redirects.tsx +74 -26
- package/src/routes/dash/settings.tsx +250 -57
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/pages/archive.tsx +71 -21
- package/src/routes/pages/collection.tsx +21 -6
- package/src/routes/pages/home.tsx +30 -9
- package/src/routes/pages/page.tsx +14 -5
- package/src/routes/pages/post.tsx +21 -7
- package/src/routes/pages/search.tsx +42 -11
- package/src/services/collection.ts +34 -9
- package/src/services/index.ts +4 -1
- package/src/services/media.ts +15 -3
- package/src/services/post.ts +39 -10
- package/src/services/redirect.ts +4 -1
- package/src/services/settings.ts +14 -3
- package/src/theme/components/ActionButtons.tsx +26 -14
- package/src/theme/components/CrudPageHeader.tsx +6 -1
- package/src/theme/components/DangerZone.tsx +19 -13
- package/src/theme/components/EmptyState.tsx +6 -1
- package/src/theme/components/PageForm.tsx +71 -24
- package/src/theme/components/Pagination.tsx +26 -8
- package/src/theme/components/PostForm.tsx +72 -25
- package/src/theme/components/PostList.tsx +16 -5
- package/src/theme/components/ThreadView.tsx +25 -7
- package/src/theme/components/TypeBadge.tsx +13 -4
- package/src/theme/components/VisibilityBadge.tsx +17 -5
- package/src/theme/components/index.ts +4 -1
- package/src/theme/layouts/BaseLayout.tsx +5 -2
- package/src/theme/layouts/DashLayout.tsx +41 -12
- package/src/types/lingui-react-macro.d.ts +34 -0
- package/src/types.ts +16 -2
- package/src/vendor/datastar.js +9 -0
- package/src/vendor/datastar.js.map +7 -0
package/src/i18n/README.md
CHANGED
|
@@ -14,7 +14,7 @@ dashRoute.get("/", async (c) => {
|
|
|
14
14
|
return c.html(
|
|
15
15
|
<I18nProvider c={c}>
|
|
16
16
|
<YourApp />
|
|
17
|
-
</I18nProvider
|
|
17
|
+
</I18nProvider>,
|
|
18
18
|
);
|
|
19
19
|
});
|
|
20
20
|
```
|
|
@@ -77,8 +77,11 @@ function DashboardContent({ postCount }: { postCount: number }) {
|
|
|
77
77
|
{/* 2. With variables */}
|
|
78
78
|
<p>
|
|
79
79
|
{t(
|
|
80
|
-
{
|
|
81
|
-
|
|
80
|
+
{
|
|
81
|
+
message: "You have {count} posts",
|
|
82
|
+
comment: "@context: Post count message",
|
|
83
|
+
},
|
|
84
|
+
{ count: postCount },
|
|
82
85
|
)}
|
|
83
86
|
</p>
|
|
84
87
|
|
|
@@ -102,7 +105,7 @@ dashRoute.get("/", async (c) => {
|
|
|
102
105
|
return c.html(
|
|
103
106
|
<I18nProvider c={c}>
|
|
104
107
|
<DashboardContent postCount={posts.length} />
|
|
105
|
-
</I18nProvider
|
|
108
|
+
</I18nProvider>,
|
|
106
109
|
);
|
|
107
110
|
});
|
|
108
111
|
```
|
|
@@ -122,7 +125,7 @@ dashRoute.get("/", async (c) => {
|
|
|
122
125
|
return c.html(
|
|
123
126
|
<Layout title={i18n._({ message: "Dashboard", comment: "@context: ..." })}>
|
|
124
127
|
<MyComponent c={c} /> {/* Need to pass c prop */}
|
|
125
|
-
</Layout
|
|
128
|
+
</Layout>,
|
|
126
129
|
);
|
|
127
130
|
});
|
|
128
131
|
|
|
@@ -143,7 +146,7 @@ dashRoute.get("/", async (c) => {
|
|
|
143
146
|
<Layout>
|
|
144
147
|
<MyComponent /> {/* No need to pass c prop */}
|
|
145
148
|
</Layout>
|
|
146
|
-
</I18nProvider
|
|
149
|
+
</I18nProvider>,
|
|
147
150
|
);
|
|
148
151
|
});
|
|
149
152
|
|
|
@@ -175,7 +178,7 @@ t({ message: "Dashboard" });
|
|
|
175
178
|
c.html(
|
|
176
179
|
<I18nProvider c={c}>
|
|
177
180
|
<App />
|
|
178
|
-
</I18nProvider
|
|
181
|
+
</I18nProvider>,
|
|
179
182
|
);
|
|
180
183
|
|
|
181
184
|
// ❌ Wrong - useLingui() will throw error
|
|
@@ -204,10 +207,17 @@ dashRoute.get("/", async (c) => {
|
|
|
204
207
|
const { t } = useLingui();
|
|
205
208
|
|
|
206
209
|
// ✅ Correct - values as second parameter
|
|
207
|
-
t(
|
|
210
|
+
t(
|
|
211
|
+
{ message: "Hello {name}", comment: "@context: Greeting" },
|
|
212
|
+
{ name: "Alice" },
|
|
213
|
+
);
|
|
208
214
|
|
|
209
215
|
// ❌ Wrong - values inside first parameter (not supported)
|
|
210
|
-
t({
|
|
216
|
+
t({
|
|
217
|
+
message: "Hello {name}",
|
|
218
|
+
comment: "@context: Greeting",
|
|
219
|
+
values: { name: "Alice" },
|
|
220
|
+
});
|
|
211
221
|
```
|
|
212
222
|
|
|
213
223
|
---
|
package/src/i18n/context.tsx
CHANGED
|
@@ -79,14 +79,11 @@ export function useLingui() {
|
|
|
79
79
|
if (!currentI18n) {
|
|
80
80
|
throw new Error(
|
|
81
81
|
"useLingui() called outside of I18nProvider. " +
|
|
82
|
-
"Make sure your component is wrapped in <I18nProvider c={c}>...</I18nProvider>"
|
|
82
|
+
"Make sure your component is wrapped in <I18nProvider c={c}>...</I18nProvider>",
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// Create translation function that accepts both pre-macro and post-macro formats
|
|
87
86
|
const translate = (descriptor: TranslationDescriptor) => {
|
|
88
|
-
// The macro will add the id, or it's already present
|
|
89
|
-
// At runtime, we pass it to i18n._ which handles the descriptor with values
|
|
90
87
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- currentI18n is checked above
|
|
91
88
|
return currentI18n!._(descriptor as MessageDescriptor);
|
|
92
89
|
};
|
package/src/i18n/detect.ts
CHANGED
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
* Language Detection Utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type
|
|
6
|
-
import { locales, baseLocale, isLocale, type Locale } from "./locales.js";
|
|
7
|
-
|
|
8
|
-
export const LANGUAGE_COOKIE_NAME = "lang";
|
|
5
|
+
import { locales, isLocale, type Locale } from "./locales.js";
|
|
9
6
|
|
|
10
7
|
/**
|
|
11
8
|
* Get display name for a language code
|
|
@@ -35,66 +32,3 @@ export function getSupportedLanguages(): Array<{ code: Locale; name: string }> {
|
|
|
35
32
|
export function isValidLanguage(lang: unknown): lang is Locale {
|
|
36
33
|
return isLocale(lang);
|
|
37
34
|
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parse Accept-Language header and return best matching locale
|
|
41
|
-
*/
|
|
42
|
-
export function parseAcceptLanguage(header: string | null): Locale {
|
|
43
|
-
if (!header) return baseLocale;
|
|
44
|
-
|
|
45
|
-
// Parse "en-US,en;q=0.9,zh-CN;q=0.8" format
|
|
46
|
-
const languages = header
|
|
47
|
-
.split(",")
|
|
48
|
-
.map((part) => {
|
|
49
|
-
const [lang, qPart] = part.trim().split(";");
|
|
50
|
-
const q = qPart ? parseFloat(qPart.replace("q=", "")) : 1;
|
|
51
|
-
return { lang: lang?.trim() ?? "", q };
|
|
52
|
-
})
|
|
53
|
-
.sort((a, b) => b.q - a.q);
|
|
54
|
-
|
|
55
|
-
for (const { lang } of languages) {
|
|
56
|
-
// Direct match
|
|
57
|
-
if (isLocale(lang)) {
|
|
58
|
-
return lang;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Map common variants
|
|
62
|
-
const normalized = lang.toLowerCase();
|
|
63
|
-
if (normalized.startsWith("zh-cn") || normalized.startsWith("zh-hans")) {
|
|
64
|
-
return "zh-Hans";
|
|
65
|
-
}
|
|
66
|
-
if (
|
|
67
|
-
normalized.startsWith("zh-tw") ||
|
|
68
|
-
normalized.startsWith("zh-hk") ||
|
|
69
|
-
normalized.startsWith("zh-hant")
|
|
70
|
-
) {
|
|
71
|
-
return "zh-Hant";
|
|
72
|
-
}
|
|
73
|
-
if (normalized.startsWith("zh")) {
|
|
74
|
-
return "zh-Hans"; // Default Chinese to Simplified
|
|
75
|
-
}
|
|
76
|
-
if (normalized.startsWith("en")) {
|
|
77
|
-
return "en";
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return baseLocale;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Detect user's preferred language from Hono context
|
|
86
|
-
* Priority: Cookie > Accept-Language header > Default
|
|
87
|
-
*/
|
|
88
|
-
export function detectLanguage(c: Context): Locale {
|
|
89
|
-
// 1. Check cookie (using getCookie helper)
|
|
90
|
-
const cookies = c.req.raw.headers.get("Cookie") ?? "";
|
|
91
|
-
const cookieMatch = cookies.match(new RegExp(`${LANGUAGE_COOKIE_NAME}=([^;]+)`));
|
|
92
|
-
const cookieLang = cookieMatch?.[1];
|
|
93
|
-
if (cookieLang && isLocale(cookieLang)) {
|
|
94
|
-
return cookieLang;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 2. Check Accept-Language header
|
|
98
|
-
const acceptLang = c.req.header("Accept-Language") ?? null;
|
|
99
|
-
return parseAcceptLanguage(acceptLang);
|
|
100
|
-
}
|
package/src/i18n/i18n.ts
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* i18n Runtime using @lingui/core
|
|
2
|
+
* i18n Runtime using @lingui/core
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* const { i18n } = bindI18n(c.get("i18n"));
|
|
9
|
-
*
|
|
10
|
-
* // Simple message
|
|
11
|
-
* i18n._(msg({ message: "Hello", comment: "@context: Greeting" }))
|
|
12
|
-
*
|
|
13
|
-
* // With interpolation
|
|
14
|
-
* i18n._(msg({ message: "Welcome, {name}!", comment: "@context: Welcome" }), { name })
|
|
15
|
-
*
|
|
16
|
-
* The msg macro generates hash-based IDs at compile time, which match the compiled catalogs.
|
|
4
|
+
* The SWC Lingui plugin adds hash-based IDs to t() calls when imports come
|
|
5
|
+
* from @lingui/react/macro. The runtimeConfigModule setting rewrites those
|
|
6
|
+
* imports to our custom Hono JSX implementation at build time.
|
|
17
7
|
*/
|
|
18
8
|
|
|
9
|
+
import type { Messages } from "@lingui/core";
|
|
19
10
|
import { I18n } from "@lingui/core";
|
|
20
11
|
import { locales, baseLocale, isLocale, type Locale } from "./locales.js";
|
|
21
12
|
import { messages as messagesEn } from "./locales/en.js";
|
|
@@ -27,6 +18,13 @@ export { locales, baseLocale, isLocale, type Locale };
|
|
|
27
18
|
// Export I18n type for convenience
|
|
28
19
|
export type { I18n };
|
|
29
20
|
|
|
21
|
+
// Pre-compute merged catalogs at module load time (done once, not per request)
|
|
22
|
+
// For non-English locales, merge English as fallback so missing translations
|
|
23
|
+
// fall back to English rather than showing hash IDs.
|
|
24
|
+
const catalogEn: Messages = messagesEn;
|
|
25
|
+
const catalogZhHans: Messages = { ...messagesEn, ...messagesZhHans };
|
|
26
|
+
const catalogZhHant: Messages = { ...messagesEn, ...messagesZhHant };
|
|
27
|
+
|
|
30
28
|
/**
|
|
31
29
|
* Create a new i18n instance for a specific locale.
|
|
32
30
|
* IMPORTANT: In Cloudflare Workers (concurrent environment), we must create
|
|
@@ -35,12 +33,10 @@ export type { I18n };
|
|
|
35
33
|
export function createI18n(locale: Locale): I18n {
|
|
36
34
|
const i18n = new I18n({});
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
i18n.load("
|
|
40
|
-
i18n.load("zh-
|
|
41
|
-
i18n.load("zh-Hant", { ...messagesEn, ...messagesZhHant });
|
|
36
|
+
i18n.load("en", catalogEn);
|
|
37
|
+
i18n.load("zh-Hans", catalogZhHans);
|
|
38
|
+
i18n.load("zh-Hant", catalogZhHant);
|
|
42
39
|
|
|
43
|
-
// Activate locale after loading messages to avoid warnings
|
|
44
40
|
i18n.activate(locale);
|
|
45
41
|
|
|
46
42
|
return i18n;
|
package/src/i18n/index.ts
CHANGED
|
@@ -53,12 +53,9 @@ export { Trans } from "./Trans.js";
|
|
|
53
53
|
|
|
54
54
|
// Language detection utilities
|
|
55
55
|
export {
|
|
56
|
-
detectLanguage,
|
|
57
56
|
isValidLanguage,
|
|
58
|
-
parseAcceptLanguage,
|
|
59
57
|
getLanguageDisplayName,
|
|
60
58
|
getSupportedLanguages,
|
|
61
|
-
LANGUAGE_COOKIE_NAME,
|
|
62
59
|
} from "./detect.js";
|
|
63
60
|
|
|
64
61
|
// Hono middleware
|
package/src/i18n/middleware.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { MiddlewareHandler } from "hono";
|
|
6
6
|
import type { I18n } from "@lingui/core";
|
|
7
|
-
import {
|
|
8
|
-
import { createI18n, isLocale, type Locale } from "./i18n.js";
|
|
7
|
+
import { createI18n, isLocale, baseLocale, type Locale } from "./i18n.js";
|
|
9
8
|
import type { Services } from "../services/index.js";
|
|
10
9
|
|
|
11
10
|
declare module "hono" {
|
|
@@ -19,33 +18,22 @@ declare module "hono" {
|
|
|
19
18
|
* Hono middleware for internationalization.
|
|
20
19
|
* Creates a per-request i18n instance to avoid race conditions in concurrent environments.
|
|
21
20
|
*
|
|
22
|
-
* Language
|
|
23
|
-
*
|
|
24
|
-
* 2. Database SITE_LANGUAGE setting (site default)
|
|
25
|
-
* 3. Accept-Language header
|
|
26
|
-
* 4. Default locale (en)
|
|
21
|
+
* Language is determined by the database SITE_LANGUAGE setting (single source of truth).
|
|
22
|
+
* Falls back to the default locale (en) if not set.
|
|
27
23
|
*/
|
|
28
24
|
export function i18nMiddleware(): MiddlewareHandler {
|
|
29
25
|
return async (c, next) => {
|
|
30
|
-
|
|
31
|
-
let lang = detectLanguage(c);
|
|
26
|
+
let lang: Locale = baseLocale;
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const services = c.get("services") as Services | undefined;
|
|
40
|
-
if (services) {
|
|
41
|
-
try {
|
|
42
|
-
const siteLang = await services.settings.get("SITE_LANGUAGE");
|
|
43
|
-
if (siteLang && isLocale(siteLang)) {
|
|
44
|
-
lang = siteLang;
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
// Ignore errors, fall back to detected language
|
|
28
|
+
const services = c.get("services") as Services | undefined;
|
|
29
|
+
if (services) {
|
|
30
|
+
try {
|
|
31
|
+
const siteLang = await services.settings.get("SITE_LANGUAGE");
|
|
32
|
+
if (siteLang && isLocale(siteLang)) {
|
|
33
|
+
lang = siteLang;
|
|
48
34
|
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore errors, fall back to default locale
|
|
49
37
|
}
|
|
50
38
|
}
|
|
51
39
|
|
package/src/lib/constants.ts
CHANGED
|
@@ -64,4 +64,5 @@ export const ONBOARDING_STATUS = {
|
|
|
64
64
|
COMPLETED: "completed",
|
|
65
65
|
} as const;
|
|
66
66
|
|
|
67
|
-
export type OnboardingStatus =
|
|
67
|
+
export type OnboardingStatus =
|
|
68
|
+
(typeof ONBOARDING_STATUS)[keyof typeof ONBOARDING_STATUS];
|
|
@@ -53,7 +53,10 @@ function readExifOrientation(buffer: ArrayBuffer): number {
|
|
|
53
53
|
const exifOffset = offset + 4;
|
|
54
54
|
|
|
55
55
|
// Check for "Exif\0\0"
|
|
56
|
-
if (
|
|
56
|
+
if (
|
|
57
|
+
view.getUint32(exifOffset) !== 0x45786966 ||
|
|
58
|
+
view.getUint16(exifOffset + 4) !== 0x0000
|
|
59
|
+
) {
|
|
57
60
|
return 1;
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -112,7 +115,7 @@ function calculateDimensions(
|
|
|
112
115
|
width: number,
|
|
113
116
|
height: number,
|
|
114
117
|
maxWidth: number,
|
|
115
|
-
maxHeight: number
|
|
118
|
+
maxHeight: number,
|
|
116
119
|
): { width: number; height: number } {
|
|
117
120
|
if (width <= maxWidth && height <= maxHeight) {
|
|
118
121
|
return { width, height };
|
|
@@ -128,7 +131,10 @@ function calculateDimensions(
|
|
|
128
131
|
/**
|
|
129
132
|
* Process image file
|
|
130
133
|
*/
|
|
131
|
-
async function process(
|
|
134
|
+
async function process(
|
|
135
|
+
file: File,
|
|
136
|
+
options: ProcessOptions = {},
|
|
137
|
+
): Promise<Blob> {
|
|
132
138
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
133
139
|
|
|
134
140
|
// Read file buffer for EXIF
|
|
@@ -145,7 +151,12 @@ async function process(file: File, options: ProcessOptions = {}): Promise<Blob>
|
|
|
145
151
|
const srcHeight = isRotated ? img.width : img.height;
|
|
146
152
|
|
|
147
153
|
// Calculate output size
|
|
148
|
-
const { width, height } = calculateDimensions(
|
|
154
|
+
const { width, height } = calculateDimensions(
|
|
155
|
+
srcWidth,
|
|
156
|
+
srcHeight,
|
|
157
|
+
opts.maxWidth,
|
|
158
|
+
opts.maxHeight,
|
|
159
|
+
);
|
|
149
160
|
|
|
150
161
|
// Create canvas
|
|
151
162
|
const canvas = document.createElement("canvas");
|
|
@@ -184,7 +195,7 @@ async function process(file: File, options: ProcessOptions = {}): Promise<Blob>
|
|
|
184
195
|
}
|
|
185
196
|
},
|
|
186
197
|
opts.mimeType,
|
|
187
|
-
opts.quality
|
|
198
|
+
opts.quality,
|
|
188
199
|
);
|
|
189
200
|
});
|
|
190
201
|
}
|
|
@@ -192,7 +203,10 @@ async function process(file: File, options: ProcessOptions = {}): Promise<Blob>
|
|
|
192
203
|
/**
|
|
193
204
|
* Process file and create a new File object
|
|
194
205
|
*/
|
|
195
|
-
async function processToFile(
|
|
206
|
+
async function processToFile(
|
|
207
|
+
file: File,
|
|
208
|
+
options: ProcessOptions = {},
|
|
209
|
+
): Promise<File> {
|
|
196
210
|
const blob = await process(file, options);
|
|
197
211
|
|
|
198
212
|
// Generate new filename with .webp extension
|
|
@@ -206,5 +220,7 @@ export const ImageProcessor = { process, processToFile };
|
|
|
206
220
|
|
|
207
221
|
// Expose globally for inline scripts
|
|
208
222
|
if (typeof window !== "undefined") {
|
|
209
|
-
(
|
|
223
|
+
(
|
|
224
|
+
window as unknown as { ImageProcessor: typeof ImageProcessor }
|
|
225
|
+
).ImageProcessor = ImageProcessor;
|
|
210
226
|
}
|
package/src/lib/image.ts
CHANGED
|
@@ -51,7 +51,7 @@ export interface ImageOptions {
|
|
|
51
51
|
export function getImageUrl(
|
|
52
52
|
originalUrl: string,
|
|
53
53
|
transformUrl?: string,
|
|
54
|
-
options?: ImageOptions
|
|
54
|
+
options?: ImageOptions,
|
|
55
55
|
): string {
|
|
56
56
|
if (!transformUrl || !options || Object.keys(options).length === 0) {
|
|
57
57
|
return originalUrl;
|
|
@@ -93,7 +93,11 @@ export function getImageUrl(
|
|
|
93
93
|
* // Returns: "https://cdn.example.com/uploads/file.webp"
|
|
94
94
|
* ```
|
|
95
95
|
*/
|
|
96
|
-
export function getMediaUrl(
|
|
96
|
+
export function getMediaUrl(
|
|
97
|
+
mediaId: string,
|
|
98
|
+
r2Key: string,
|
|
99
|
+
r2PublicUrl?: string,
|
|
100
|
+
): string {
|
|
97
101
|
if (r2PublicUrl) {
|
|
98
102
|
return `${r2PublicUrl}/${r2Key}`;
|
|
99
103
|
}
|
package/src/lib/schemas.ts
CHANGED
|
@@ -62,7 +62,11 @@ export const UpdatePostSchema = CreatePostSchema.partial();
|
|
|
62
62
|
* // type is PostType, throws if invalid
|
|
63
63
|
* ```
|
|
64
64
|
*/
|
|
65
|
-
export function parseFormData<T>(
|
|
65
|
+
export function parseFormData<T>(
|
|
66
|
+
formData: FormData,
|
|
67
|
+
key: string,
|
|
68
|
+
schema: z.ZodSchema<T>,
|
|
69
|
+
): T {
|
|
66
70
|
const value = formData.get(key);
|
|
67
71
|
if (value === null) {
|
|
68
72
|
throw new Error(`Missing required field: ${key}`);
|
|
@@ -82,7 +86,7 @@ export function parseFormData<T>(formData: FormData, key: string, schema: z.ZodS
|
|
|
82
86
|
export function parseFormDataOptional<T>(
|
|
83
87
|
formData: FormData,
|
|
84
88
|
key: string,
|
|
85
|
-
schema: z.ZodSchema<T
|
|
89
|
+
schema: z.ZodSchema<T>,
|
|
86
90
|
): T | undefined {
|
|
87
91
|
const value = formData.get(key);
|
|
88
92
|
if (value === null || value === "") {
|