@jant/core 0.3.25 → 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 +67 -562
- package/dist/client.js +1 -0
- 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/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -10
- 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/navigation.js +23 -3
- package/dist/lib/render.js +10 -1
- package/dist/lib/schemas.js +31 -0
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +1 -1
- package/dist/routes/api/posts.js +1 -1
- 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/dash/collections.js +23 -415
- package/dist/routes/dash/media.js +12 -392
- package/dist/routes/dash/pages.js +7 -330
- package/dist/routes/dash/redirects.js +18 -12
- package/dist/routes/dash/settings.js +198 -577
- package/dist/routes/feed/rss.js +2 -1
- package/dist/routes/feed/sitemap.js +4 -2
- package/dist/routes/pages/featured.js +5 -1
- package/dist/routes/pages/home.js +26 -1
- package/dist/routes/pages/latest.js +45 -0
- package/dist/services/post.js +30 -50
- 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/ui/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +36 -21
- package/dist/ui/dash/PageForm.js +21 -15
- package/dist/ui/dash/PostForm.js +22 -16
- 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/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/ui/font-themes.js +36 -0
- package/dist/ui/layouts/BaseLayout.js +24 -2
- package/dist/ui/layouts/SiteLayout.js +47 -19
- package/package.json +1 -1
- package/src/app.tsx +93 -553
- package/src/client.ts +1 -0
- package/src/i18n/locales/en.po +240 -175
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +240 -175
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +240 -175
- package/src/i18n/locales/zh-Hant.ts +1 -1
- 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__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +2 -2
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -11
- 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/navigation.ts +33 -2
- package/src/lib/render.tsx +15 -1
- package/src/lib/schemas.ts +39 -0
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +1 -1
- package/src/routes/api/posts.ts +1 -1
- 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/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +17 -366
- package/src/routes/dash/media.tsx +12 -414
- package/src/routes/dash/pages.tsx +8 -348
- package/src/routes/dash/redirects.tsx +20 -14
- package/src/routes/dash/settings.tsx +243 -534
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +3 -1
- package/src/routes/feed/sitemap.ts +4 -2
- package/src/routes/pages/featured.tsx +7 -1
- package/src/routes/pages/home.tsx +25 -2
- package/src/routes/pages/latest.tsx +59 -0
- package/src/services/post.ts +34 -66
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +1 -1
- package/src/styles/ui.css +24 -40
- 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 -644
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/ui/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +40 -21
- package/src/ui/dash/PageForm.tsx +25 -19
- package/src/ui/dash/PostForm.tsx +26 -20
- 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/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/ui/font-themes.ts +54 -0
- package/src/ui/layouts/BaseLayout.tsx +17 -0
- package/src/ui/layouts/SiteLayout.tsx +45 -31
package/src/lib/config.ts
CHANGED
|
@@ -118,3 +118,50 @@ export async function getSiteDescription(c: Context): Promise<string> {
|
|
|
118
118
|
export async function getSiteLanguage(c: Context): Promise<string> {
|
|
119
119
|
return getConfig(c, "SITE_LANGUAGE");
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get home default view with fallback chain: DB > ENV > Default
|
|
124
|
+
*
|
|
125
|
+
* @param c - Hono context
|
|
126
|
+
* @returns Home default view ("latest" or "featured")
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const view = await getHomeDefaultView(c);
|
|
131
|
+
* // Returns: (DB: HOME_DEFAULT_VIEW) ?? "latest"
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export async function getHomeDefaultView(c: Context): Promise<string> {
|
|
135
|
+
return getConfig(c, "HOME_DEFAULT_VIEW");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get timezone with fallback chain: DB > ENV > Default
|
|
140
|
+
*
|
|
141
|
+
* @param c - Hono context
|
|
142
|
+
* @returns Timezone string (e.g. "Beijing", "UTC")
|
|
143
|
+
*/
|
|
144
|
+
export async function getTimeZone(c: Context): Promise<string> {
|
|
145
|
+
return getConfig(c, "TIME_ZONE");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get site footer markdown with fallback chain: DB > ENV > Default
|
|
150
|
+
*
|
|
151
|
+
* @param c - Hono context
|
|
152
|
+
* @returns Footer markdown string (empty string if not set)
|
|
153
|
+
*/
|
|
154
|
+
export async function getSiteFooter(c: Context): Promise<string> {
|
|
155
|
+
return getConfig(c, "SITE_FOOTER");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if search engine indexing is disabled
|
|
160
|
+
*
|
|
161
|
+
* @param c - Hono context
|
|
162
|
+
* @returns true if NOINDEX is set to "true"
|
|
163
|
+
*/
|
|
164
|
+
export async function isNoIndex(c: Context): Promise<boolean> {
|
|
165
|
+
const value = await getConfig(c, "NOINDEX");
|
|
166
|
+
return value === "true";
|
|
167
|
+
}
|
package/src/lib/constants.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export const RESERVED_PATHS = [
|
|
9
9
|
"featured",
|
|
10
|
+
"latest",
|
|
10
11
|
"collections",
|
|
11
12
|
"signin",
|
|
12
13
|
"signout",
|
|
@@ -42,19 +43,26 @@ export function isReservedPath(path: string): boolean {
|
|
|
42
43
|
export const DEFAULT_PAGE_SIZE = 100;
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
|
-
* Settings keys (
|
|
46
|
+
* Settings keys - derived from CONFIG_FIELDS (Single Source of Truth)
|
|
47
|
+
*
|
|
48
|
+
* Only non-envOnly fields and internal fields are stored in DB settings.
|
|
49
|
+
* Environment-only fields (SITE_URL, AUTH_SECRET, etc.) are never in the DB.
|
|
46
50
|
*/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
import { CONFIG_FIELDS, type ConfigKey } from "../types.js";
|
|
52
|
+
|
|
53
|
+
type SettingsFieldKey = {
|
|
54
|
+
[K in ConfigKey]: (typeof CONFIG_FIELDS)[K] extends { envOnly: false }
|
|
55
|
+
? K
|
|
56
|
+
: never;
|
|
57
|
+
}[ConfigKey];
|
|
58
|
+
|
|
59
|
+
export const SETTINGS_KEYS = Object.fromEntries(
|
|
60
|
+
Object.entries(CONFIG_FIELDS)
|
|
61
|
+
.filter(([, field]) => !field.envOnly || "internal" in field)
|
|
62
|
+
.map(([key]) => [key, key]),
|
|
63
|
+
) as { [K in SettingsFieldKey]: K };
|
|
56
64
|
|
|
57
|
-
export type SettingsKey =
|
|
65
|
+
export type SettingsKey = SettingsFieldKey;
|
|
58
66
|
|
|
59
67
|
/**
|
|
60
68
|
* Onboarding status values
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Favicon Utilities
|
|
3
|
+
*
|
|
4
|
+
* Sizes and ICO encoding for generated favicon variants.
|
|
5
|
+
* Favicon data is stored as base64 in the settings table (not R2)
|
|
6
|
+
* since the files are tiny and accessed on every page load.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Favicon variant sizes (width x height in pixels)
|
|
11
|
+
*/
|
|
12
|
+
export const FAVICON_SIZES = {
|
|
13
|
+
ICO_16: 16,
|
|
14
|
+
ICO_32: 32,
|
|
15
|
+
APPLE_TOUCH: 180,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Encode PNG images into an ICO file.
|
|
20
|
+
*
|
|
21
|
+
* ICO format (with PNG payloads):
|
|
22
|
+
* - Header: 6 bytes (reserved=0, type=1, count=N)
|
|
23
|
+
* - Directory: 16 bytes per entry (width, height, colors, reserved, planes, bpp, size, offset)
|
|
24
|
+
* - Data: raw PNG bytes for each entry
|
|
25
|
+
*
|
|
26
|
+
* @param entries - Array of { size, png } where png is an ArrayBuffer of PNG data
|
|
27
|
+
* @returns ICO file as a Blob
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const ico = encodeIco([
|
|
32
|
+
* { size: 16, png: png16ArrayBuffer },
|
|
33
|
+
* { size: 32, png: png32ArrayBuffer },
|
|
34
|
+
* ]);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function encodeIco(
|
|
38
|
+
entries: { size: number; png: ArrayBuffer }[],
|
|
39
|
+
): Blob {
|
|
40
|
+
const headerSize = 6;
|
|
41
|
+
const dirEntrySize = 16;
|
|
42
|
+
const dirSize = entries.length * dirEntrySize;
|
|
43
|
+
|
|
44
|
+
let dataOffset = headerSize + dirSize;
|
|
45
|
+
|
|
46
|
+
// Build header + directory
|
|
47
|
+
const header = new ArrayBuffer(headerSize + dirSize);
|
|
48
|
+
const view = new DataView(header);
|
|
49
|
+
|
|
50
|
+
// ICO header
|
|
51
|
+
view.setUint16(0, 0, true); // reserved
|
|
52
|
+
view.setUint16(2, 1, true); // type = icon
|
|
53
|
+
view.setUint16(4, entries.length, true); // count
|
|
54
|
+
|
|
55
|
+
const pngBuffers: ArrayBuffer[] = [];
|
|
56
|
+
for (let i = 0; i < entries.length; i++) {
|
|
57
|
+
const entry = entries[i]!;
|
|
58
|
+
const offset = headerSize + i * dirEntrySize;
|
|
59
|
+
|
|
60
|
+
// Width/height: 0 means 256
|
|
61
|
+
view.setUint8(offset + 0, entry.size < 256 ? entry.size : 0);
|
|
62
|
+
view.setUint8(offset + 1, entry.size < 256 ? entry.size : 0);
|
|
63
|
+
view.setUint8(offset + 2, 0); // color count (0 for >256 colors)
|
|
64
|
+
view.setUint8(offset + 3, 0); // reserved
|
|
65
|
+
view.setUint16(offset + 4, 1, true); // color planes
|
|
66
|
+
view.setUint16(offset + 6, 32, true); // bits per pixel
|
|
67
|
+
view.setUint32(offset + 8, entry.png.byteLength, true); // image size
|
|
68
|
+
view.setUint32(offset + 12, dataOffset, true); // image offset
|
|
69
|
+
|
|
70
|
+
dataOffset += entry.png.byteLength;
|
|
71
|
+
pngBuffers.push(entry.png);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new Blob([header, ...pngBuffers], { type: "image/x-icon" });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Convert an ArrayBuffer to a base64 string.
|
|
79
|
+
*
|
|
80
|
+
* @param buffer - The ArrayBuffer to encode
|
|
81
|
+
* @returns base64-encoded string
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const b64 = arrayBufferToBase64(await blob.arrayBuffer());
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
89
|
+
const bytes = new Uint8Array(buffer);
|
|
90
|
+
let binary = "";
|
|
91
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
92
|
+
binary += String.fromCharCode(bytes[i]!);
|
|
93
|
+
}
|
|
94
|
+
return btoa(binary);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Convert a base64 string to a Uint8Array.
|
|
99
|
+
*
|
|
100
|
+
* @param base64 - The base64 string to decode
|
|
101
|
+
* @returns decoded Uint8Array
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const bytes = base64ToUint8Array(storedBase64);
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function base64ToUint8Array(base64: string): Uint8Array {
|
|
109
|
+
const binary = atob(base64);
|
|
110
|
+
const bytes = new Uint8Array(binary.length);
|
|
111
|
+
for (let i = 0; i < binary.length; i++) {
|
|
112
|
+
bytes[i] = binary.charCodeAt(i);
|
|
113
|
+
}
|
|
114
|
+
return bytes;
|
|
115
|
+
}
|
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/navigation.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Context } from "hono";
|
|
8
|
-
import { getSiteName } from "./config.js";
|
|
8
|
+
import { getSiteName, getHomeDefaultView, getSiteFooter } from "./config.js";
|
|
9
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
|
|
@@ -19,6 +21,10 @@ export interface NavigationData {
|
|
|
19
21
|
siteDescription: string;
|
|
20
22
|
isAuthenticated: boolean;
|
|
21
23
|
collections: Collection[];
|
|
24
|
+
homeDefaultView: string;
|
|
25
|
+
siteAvatarUrl?: string;
|
|
26
|
+
showHeaderAvatar?: boolean;
|
|
27
|
+
siteFooterHtml?: string;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
@@ -43,7 +49,11 @@ export interface NavigationData {
|
|
|
43
49
|
export async function getNavigationData(c: Context): Promise<NavigationData> {
|
|
44
50
|
const items = await c.var.services.navItems.list();
|
|
45
51
|
const currentPath = new URL(c.req.url).pathname;
|
|
46
|
-
const siteName = await
|
|
52
|
+
const [siteName, homeDefaultView, siteFooter] = await Promise.all([
|
|
53
|
+
getSiteName(c),
|
|
54
|
+
getHomeDefaultView(c),
|
|
55
|
+
getSiteFooter(c),
|
|
56
|
+
]);
|
|
47
57
|
|
|
48
58
|
// Only include description if explicitly set (DB or env), not the default
|
|
49
59
|
const dbDescription = await c.var.services.settings.get("SITE_DESCRIPTION");
|
|
@@ -51,6 +61,23 @@ export async function getNavigationData(c: Context): Promise<NavigationData> {
|
|
|
51
61
|
const siteDescription =
|
|
52
62
|
dbDescription || (typeof envDescription === "string" ? envDescription : "");
|
|
53
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
|
+
|
|
54
81
|
const links = toNavItemViews(items, currentPath);
|
|
55
82
|
|
|
56
83
|
// Check auth status for compose button
|
|
@@ -79,5 +106,9 @@ export async function getNavigationData(c: Context): Promise<NavigationData> {
|
|
|
79
106
|
siteDescription,
|
|
80
107
|
isAuthenticated,
|
|
81
108
|
collections,
|
|
109
|
+
homeDefaultView,
|
|
110
|
+
siteAvatarUrl,
|
|
111
|
+
showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
|
|
112
|
+
siteFooterHtml,
|
|
82
113
|
};
|
|
83
114
|
}
|
package/src/lib/render.tsx
CHANGED
|
@@ -50,10 +50,24 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
50
50
|
currentPath: navData.currentPath,
|
|
51
51
|
isAuthenticated: navData.isAuthenticated,
|
|
52
52
|
collections: navData.collections,
|
|
53
|
+
homeDefaultView: navData.homeDefaultView,
|
|
54
|
+
siteAvatarUrl: navData.siteAvatarUrl,
|
|
55
|
+
showHeaderAvatar: navData.showHeaderAvatar,
|
|
56
|
+
siteFooterHtml: navData.siteFooterHtml,
|
|
53
57
|
};
|
|
54
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
|
+
|
|
55
63
|
return c.html(
|
|
56
|
-
<BaseLayout
|
|
64
|
+
<BaseLayout
|
|
65
|
+
title={title}
|
|
66
|
+
description={description}
|
|
67
|
+
c={c}
|
|
68
|
+
faviconUrl={faviconUrl}
|
|
69
|
+
noindex={noindex}
|
|
70
|
+
>
|
|
57
71
|
<SiteLayout {...layoutProps}>{content}</SiteLayout>
|
|
58
72
|
</BaseLayout>,
|
|
59
73
|
);
|
package/src/lib/schemas.ts
CHANGED
|
@@ -153,6 +153,45 @@ export const CreateCollectionSchema = z.object({
|
|
|
153
153
|
*/
|
|
154
154
|
export const UpdateCollectionSchema = CreateCollectionSchema.partial();
|
|
155
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
|
+
|
|
156
195
|
/**
|
|
157
196
|
* Form data helper: safely parse a FormData value with a schema
|
|
158
197
|
*
|