@jant/core 0.3.34 → 0.3.36
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/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-8Dj-5CLW.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +3109 -2294
- package/dist/index.js +3327 -3031
- package/package.json +13 -4
- package/src/__tests__/helpers/app.ts +1 -1
- package/src/__tests__/helpers/db.ts +6 -0
- package/src/app.tsx +1 -5
- package/src/{lib → client}/avatar-upload.ts +1 -1
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
- package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +45 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/{ui → client}/components/compose-types.ts +3 -1
- package/src/{ui → client}/components/jant-collection-form.ts +301 -182
- package/src/client/components/jant-collection-sidebar.ts +801 -0
- package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
- package/src/client/components/jant-compose-editor.ts +1249 -0
- package/src/client/components/jant-compose-fullscreen.ts +338 -0
- package/src/client/components/jant-media-lightbox.ts +257 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
- package/src/{ui → client}/components/jant-post-form.ts +57 -8
- package/src/{ui → client}/components/jant-settings-general.ts +2 -2
- package/src/{ui → client}/components/nav-manager-types.ts +3 -0
- package/src/{ui → client}/components/post-form-template.ts +35 -31
- package/src/{ui → client}/components/post-form-types.ts +7 -3
- package/src/{lib → client}/compose-bridge.ts +9 -7
- package/src/client/lazy-slugify.ts +51 -0
- package/src/{lib → client}/media-upload.ts +16 -3
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/client/page-slug-bridge.ts +42 -0
- package/src/{lib → client}/post-form-bridge.ts +2 -2
- package/src/{lib → client}/settings-bridge.ts +3 -3
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +40 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +60 -0
- package/src/client/tiptap/image-node.ts +488 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +140 -0
- package/src/client/tiptap/slash-commands.ts +328 -0
- package/src/{types → client/types}/sortablejs.d.ts +1 -1
- package/src/client.ts +24 -17
- package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
- package/src/db/schema.ts +6 -1
- package/src/i18n/locales/en.po +641 -215
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +642 -204
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +642 -204
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +9 -6
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +9 -9
- package/src/lib/emoji-catalog.ts +146 -0
- package/src/lib/feed.ts +1 -1
- package/src/lib/media-helpers.ts +10 -9
- package/src/lib/render.tsx +4 -3
- package/src/lib/resolve-config.ts +8 -1
- package/src/lib/schemas.ts +2 -3
- package/src/lib/summary.ts +92 -0
- package/src/lib/timeline.ts +2 -0
- package/src/lib/tiptap-render.ts +196 -0
- package/src/lib/upload.ts +97 -9
- package/src/lib/url.ts +7 -23
- package/src/lib/view.ts +33 -19
- package/src/middleware/error-handler.ts +3 -3
- package/src/preset.css +38 -0
- package/src/routes/api/collections.ts +20 -3
- package/src/routes/api/posts.ts +48 -33
- package/src/routes/api/upload.ts +7 -5
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +26 -11
- package/src/routes/auth/signin.tsx +10 -7
- package/src/routes/compose.tsx +20 -11
- package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
- package/src/routes/dash/index.tsx +7 -1
- package/src/routes/dash/media.tsx +3 -0
- package/src/routes/dash/pages.tsx +8 -2
- package/src/routes/dash/posts.tsx +6 -2
- package/src/routes/dash/redirects.tsx +15 -9
- package/src/routes/dash/settings.tsx +336 -32
- package/src/routes/feed/__tests__/rss.test.ts +245 -6
- package/src/routes/feed/rss.ts +70 -6
- package/src/routes/pages/__tests__/featured.test.ts +6 -7
- package/src/routes/pages/archive.tsx +11 -7
- package/src/routes/pages/collection.tsx +32 -15
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +1 -1
- package/src/routes/pages/home.tsx +1 -1
- package/src/services/__tests__/post.test.ts +124 -33
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/page.ts +16 -3
- package/src/services/post.ts +96 -37
- package/src/services/search.ts +4 -2
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +240 -60
- package/src/styles/tokens.css +10 -0
- package/src/styles/ui.css +1157 -81
- package/src/types/bindings.ts +5 -0
- package/src/types/config.ts +23 -1
- package/src/types/constants.ts +3 -0
- package/src/types/entities.ts +9 -2
- package/src/types/operations.ts +9 -3
- package/src/types/props.ts +3 -3
- package/src/types/views.ts +3 -2
- package/src/ui/compose/ComposeDialog.tsx +24 -7
- package/src/ui/dash/PageForm.tsx +2 -0
- package/src/ui/dash/PostList.tsx +5 -5
- package/src/ui/dash/StatusBadge.tsx +13 -5
- package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
- package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
- package/src/ui/dash/media/MediaListContent.tsx +9 -4
- package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
- package/src/ui/dash/pages/PagesContent.tsx +2 -1
- package/src/ui/dash/posts/PostForm.tsx +19 -7
- package/src/ui/dash/settings/AccountContent.tsx +133 -138
- package/src/ui/dash/settings/AvatarContent.tsx +70 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
- package/src/ui/layouts/DashLayout.tsx +157 -75
- package/src/ui/layouts/SiteLayout.tsx +13 -13
- package/src/ui/pages/ArchivePage.tsx +10 -7
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/SearchPage.tsx +1 -1
- package/src/ui/shared/CollectionsSidebar.tsx +228 -3
- package/src/ui/shared/MediaGallery.tsx +179 -41
- package/src/lib/collections-reorder.ts +0 -28
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
- /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
- /package/src/{ui → client}/components/settings-types.ts +0 -0
- /package/src/{lib → client}/image-processor.ts +0 -0
- /package/src/{lib → client}/toast.ts +0 -0
package/src/routes/api/upload.ts
CHANGED
|
@@ -138,7 +138,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
138
138
|
if (!storage) {
|
|
139
139
|
const errorText = i18n._(
|
|
140
140
|
msg({
|
|
141
|
-
message: "
|
|
141
|
+
message: "File storage isn't set up. Check your server config.",
|
|
142
142
|
comment: "@context: Error when file storage is not set up",
|
|
143
143
|
}),
|
|
144
144
|
);
|
|
@@ -154,7 +154,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
154
154
|
if (!file) {
|
|
155
155
|
const errorText = i18n._(
|
|
156
156
|
msg({
|
|
157
|
-
message: "No file
|
|
157
|
+
message: "No file selected. Choose a file to upload.",
|
|
158
158
|
comment: "@context: Error when no file was selected for upload",
|
|
159
159
|
}),
|
|
160
160
|
);
|
|
@@ -165,7 +165,9 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// Validate file type and size
|
|
168
|
-
const uploadError = validateUploadFile(file
|
|
168
|
+
const uploadError = validateUploadFile(file, {
|
|
169
|
+
maxFileSizeMB: c.var.appConfig.uploadMaxFileSize,
|
|
170
|
+
});
|
|
169
171
|
if (uploadError) {
|
|
170
172
|
if (wantsSSE(c)) {
|
|
171
173
|
return sseUploadError(c, uploadError);
|
|
@@ -215,7 +217,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
215
217
|
await stream.toast(
|
|
216
218
|
i18n._(
|
|
217
219
|
msg({
|
|
218
|
-
message: "
|
|
220
|
+
message: "File uploaded.",
|
|
219
221
|
comment: "@context: Toast after successful file upload",
|
|
220
222
|
}),
|
|
221
223
|
),
|
|
@@ -243,7 +245,7 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
243
245
|
|
|
244
246
|
const errorText = i18n._(
|
|
245
247
|
msg({
|
|
246
|
-
message: "Upload
|
|
248
|
+
message: "Upload didn't go through. Try again in a moment.",
|
|
247
249
|
comment: "@context: Error when file upload fails",
|
|
248
250
|
}),
|
|
249
251
|
);
|
|
@@ -37,7 +37,7 @@ const ResetContent: FC<{ token: string }> = ({ token }) => {
|
|
|
37
37
|
</h2>
|
|
38
38
|
<p>
|
|
39
39
|
{t({
|
|
40
|
-
message: "
|
|
40
|
+
message: "Choose a new password.",
|
|
41
41
|
comment: "@context: Password reset page description",
|
|
42
42
|
})}
|
|
43
43
|
</p>
|
|
@@ -118,7 +118,7 @@ const ResetErrorContent: FC = () => {
|
|
|
118
118
|
<header>
|
|
119
119
|
<h2>
|
|
120
120
|
{t({
|
|
121
|
-
message: "
|
|
121
|
+
message: "This Link Has Expired",
|
|
122
122
|
comment: "@context: Password reset error heading",
|
|
123
123
|
})}
|
|
124
124
|
</h2>
|
|
@@ -127,7 +127,7 @@ const ResetErrorContent: FC = () => {
|
|
|
127
127
|
<p class="text-muted-foreground">
|
|
128
128
|
{t({
|
|
129
129
|
message:
|
|
130
|
-
"This
|
|
130
|
+
"This reset link is no longer valid. Request a new one to continue.",
|
|
131
131
|
comment: "@context: Password reset error description",
|
|
132
132
|
})}
|
|
133
133
|
</p>
|
|
@@ -175,7 +175,8 @@ resetRoutes.post("/reset", async (c) => {
|
|
|
175
175
|
parsed.error.issues[0]?.message ??
|
|
176
176
|
i18n._(
|
|
177
177
|
msg({
|
|
178
|
-
message:
|
|
178
|
+
message:
|
|
179
|
+
"Something doesn't look right. Check the form and try again.",
|
|
179
180
|
comment:
|
|
180
181
|
"@context: Fallback validation error for password reset form",
|
|
181
182
|
}),
|
|
@@ -146,7 +146,8 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
146
146
|
parsed.error.issues[0]?.message ??
|
|
147
147
|
i18n._(
|
|
148
148
|
msg({
|
|
149
|
-
message:
|
|
149
|
+
message:
|
|
150
|
+
"Something doesn't look right. Check the form and try again.",
|
|
150
151
|
comment: "@context: Fallback validation error for setup form",
|
|
151
152
|
}),
|
|
152
153
|
);
|
|
@@ -159,7 +160,7 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
159
160
|
return dsToast(
|
|
160
161
|
i18n._(
|
|
161
162
|
msg({
|
|
162
|
-
message: "
|
|
163
|
+
message: "Auth secret is missing. Check your environment variables.",
|
|
163
164
|
comment:
|
|
164
165
|
"@context: Error toast when authentication secret is missing from server config",
|
|
165
166
|
}),
|
|
@@ -177,7 +178,8 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
177
178
|
return dsToast(
|
|
178
179
|
i18n._(
|
|
179
180
|
msg({
|
|
180
|
-
message:
|
|
181
|
+
message:
|
|
182
|
+
"Couldn't create your account. Check the details and try again.",
|
|
181
183
|
comment: "@context: Error toast when account creation fails",
|
|
182
184
|
}),
|
|
183
185
|
),
|
|
@@ -195,19 +197,13 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
// Seed default navigation items (order: Collections, About, Archive, RSS, Dashboard)
|
|
198
201
|
await c.var.services.navItems.create({
|
|
199
202
|
type: "link",
|
|
200
203
|
label: "Collections",
|
|
201
204
|
url: "/c",
|
|
202
205
|
});
|
|
203
|
-
// Seed default navigation items
|
|
204
|
-
await c.var.services.navItems.create({
|
|
205
|
-
type: "link",
|
|
206
|
-
label: "Archive",
|
|
207
|
-
url: "/archive",
|
|
208
|
-
});
|
|
209
206
|
|
|
210
|
-
// Seed default About page
|
|
211
207
|
const aboutPage = await c.var.services.pages.create({
|
|
212
208
|
slug: "about",
|
|
213
209
|
title: "About",
|
|
@@ -228,6 +224,24 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
228
224
|
pageId: aboutPage.id,
|
|
229
225
|
});
|
|
230
226
|
|
|
227
|
+
await c.var.services.navItems.create({
|
|
228
|
+
type: "link",
|
|
229
|
+
label: "Archive",
|
|
230
|
+
url: "/archive",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await c.var.services.navItems.create({
|
|
234
|
+
type: "system",
|
|
235
|
+
label: "RSS",
|
|
236
|
+
url: "/feed",
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await c.var.services.navItems.create({
|
|
240
|
+
type: "system",
|
|
241
|
+
label: "Dashboard",
|
|
242
|
+
url: "/dash",
|
|
243
|
+
});
|
|
244
|
+
|
|
231
245
|
return dsRedirect("/signin?setup");
|
|
232
246
|
} catch (err) {
|
|
233
247
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
@@ -235,7 +249,8 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
235
249
|
return dsToast(
|
|
236
250
|
i18n._(
|
|
237
251
|
msg({
|
|
238
|
-
message:
|
|
252
|
+
message:
|
|
253
|
+
"Couldn't create your account. Check the details and try again.",
|
|
239
254
|
comment: "@context: Error toast when account creation fails",
|
|
240
255
|
}),
|
|
241
256
|
),
|
|
@@ -40,7 +40,8 @@ const SigninContent: FC<{
|
|
|
40
40
|
{demoEmail && demoPassword && (
|
|
41
41
|
<p class="text-muted-foreground text-sm mb-4">
|
|
42
42
|
{t({
|
|
43
|
-
message:
|
|
43
|
+
message:
|
|
44
|
+
"Demo credentials are pre-filled — hit Sign In to continue.",
|
|
44
45
|
comment:
|
|
45
46
|
"@context: Hint shown on signin page when demo credentials are pre-filled",
|
|
46
47
|
})}
|
|
@@ -110,9 +111,9 @@ signinRoutes.get("/signin", async (c) => {
|
|
|
110
111
|
const isReset = c.req.query("reset") !== undefined;
|
|
111
112
|
let toast: { message: string } | undefined;
|
|
112
113
|
if (isSetup) {
|
|
113
|
-
toast = { message: "Account created
|
|
114
|
+
toast = { message: "Account created. Sign in to get started." };
|
|
114
115
|
} else if (isReset) {
|
|
115
|
-
toast = { message: "Password reset
|
|
116
|
+
toast = { message: "Password reset. Sign in with your new password." };
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
return c.html(
|
|
@@ -132,7 +133,7 @@ signinRoutes.post("/signin", async (c) => {
|
|
|
132
133
|
return dsToast(
|
|
133
134
|
i18n._(
|
|
134
135
|
msg({
|
|
135
|
-
message: "
|
|
136
|
+
message: "Authentication isn't set up. Check your server config.",
|
|
136
137
|
comment:
|
|
137
138
|
"@context: Error toast when authentication system is unavailable",
|
|
138
139
|
}),
|
|
@@ -149,7 +150,8 @@ signinRoutes.post("/signin", async (c) => {
|
|
|
149
150
|
parsed.error.issues[0]?.message ??
|
|
150
151
|
i18n._(
|
|
151
152
|
msg({
|
|
152
|
-
message:
|
|
153
|
+
message:
|
|
154
|
+
"Something doesn't look right. Check the form and try again.",
|
|
153
155
|
comment: "@context: Fallback validation error for sign-in form",
|
|
154
156
|
}),
|
|
155
157
|
);
|
|
@@ -165,12 +167,13 @@ signinRoutes.post("/signin", async (c) => {
|
|
|
165
167
|
headers: c.req.raw.headers,
|
|
166
168
|
});
|
|
167
169
|
|
|
168
|
-
return dsRedirect("/
|
|
170
|
+
return dsRedirect("/", { headers });
|
|
169
171
|
} catch {
|
|
170
172
|
return dsToast(
|
|
171
173
|
i18n._(
|
|
172
174
|
msg({
|
|
173
|
-
message:
|
|
175
|
+
message:
|
|
176
|
+
"Wrong email or password. Check your credentials and try again.",
|
|
174
177
|
comment: "@context: Error toast when sign-in credentials are wrong",
|
|
175
178
|
}),
|
|
176
179
|
),
|
package/src/routes/compose.tsx
CHANGED
|
@@ -94,7 +94,8 @@ composeRoutes.post("/", async (c) => {
|
|
|
94
94
|
result.error.issues[0]?.message ??
|
|
95
95
|
i18n._(
|
|
96
96
|
msg({
|
|
97
|
-
message:
|
|
97
|
+
message:
|
|
98
|
+
"Something doesn't look right. Check the form and try again.",
|
|
98
99
|
comment: "@context: Fallback validation error for compose form",
|
|
99
100
|
}),
|
|
100
101
|
);
|
|
@@ -121,16 +122,24 @@ composeRoutes.post("/", async (c) => {
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
const post = await c.var.services.posts.create(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
const post = await c.var.services.posts.create(
|
|
126
|
+
{
|
|
127
|
+
format: data.format,
|
|
128
|
+
title: data.title || undefined,
|
|
129
|
+
body: data.body || undefined,
|
|
130
|
+
status: data.status ?? "published",
|
|
131
|
+
url: data.url || undefined,
|
|
132
|
+
quoteText: data.quoteText || undefined,
|
|
133
|
+
rating: data.rating || undefined,
|
|
134
|
+
collectionIds: data.collectionIds?.length
|
|
135
|
+
? data.collectionIds
|
|
136
|
+
: undefined,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
maxParagraphs: c.var.appConfig.summaryMaxParagraphs,
|
|
140
|
+
maxChars: c.var.appConfig.summaryMaxChars,
|
|
141
|
+
},
|
|
142
|
+
);
|
|
134
143
|
|
|
135
144
|
// Attach media if provided
|
|
136
145
|
if (data.mediaIds && data.mediaIds.length > 0) {
|
|
@@ -57,7 +57,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
|
|
|
57
57
|
|
|
58
58
|
await settingsService.uploadAvatar(
|
|
59
59
|
{ file },
|
|
60
|
-
{
|
|
60
|
+
{
|
|
61
|
+
media: mediaService,
|
|
62
|
+
storage,
|
|
63
|
+
storageProvider: "r2",
|
|
64
|
+
maxFileSizeMB: 500,
|
|
65
|
+
},
|
|
61
66
|
);
|
|
62
67
|
|
|
63
68
|
const avatarKey = await settingsService.get("SITE_AVATAR");
|
|
@@ -72,7 +77,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
|
|
|
72
77
|
|
|
73
78
|
await settingsService.uploadAvatar(
|
|
74
79
|
{ file },
|
|
75
|
-
{
|
|
80
|
+
{
|
|
81
|
+
media: mediaService,
|
|
82
|
+
storage,
|
|
83
|
+
storageProvider: "r2",
|
|
84
|
+
maxFileSizeMB: 500,
|
|
85
|
+
},
|
|
76
86
|
);
|
|
77
87
|
|
|
78
88
|
const mediaList = await mediaService.list();
|
|
@@ -89,7 +99,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
|
|
|
89
99
|
|
|
90
100
|
await settingsService.uploadAvatar(
|
|
91
101
|
{ file, faviconIco: fakeIcoData.buffer },
|
|
92
|
-
{
|
|
102
|
+
{
|
|
103
|
+
media: mediaService,
|
|
104
|
+
storage,
|
|
105
|
+
storageProvider: "r2",
|
|
106
|
+
maxFileSizeMB: 500,
|
|
107
|
+
},
|
|
93
108
|
);
|
|
94
109
|
|
|
95
110
|
const stored = await settingsService.get("SITE_FAVICON_ICO");
|
|
@@ -105,7 +120,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
|
|
|
105
120
|
|
|
106
121
|
await settingsService.uploadAvatar(
|
|
107
122
|
{ file, appleTouchIcon: appleTouchData },
|
|
108
|
-
{
|
|
123
|
+
{
|
|
124
|
+
media: mediaService,
|
|
125
|
+
storage,
|
|
126
|
+
storageProvider: "r2",
|
|
127
|
+
maxFileSizeMB: 500,
|
|
128
|
+
},
|
|
109
129
|
);
|
|
110
130
|
|
|
111
131
|
const stored = await settingsService.get("SITE_FAVICON_APPLE_TOUCH");
|
|
@@ -120,7 +140,12 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
|
|
|
120
140
|
|
|
121
141
|
await settingsService.uploadAvatar(
|
|
122
142
|
{ file },
|
|
123
|
-
{
|
|
143
|
+
{
|
|
144
|
+
media: mediaService,
|
|
145
|
+
storage,
|
|
146
|
+
storageProvider: "r2",
|
|
147
|
+
maxFileSizeMB: 500,
|
|
148
|
+
},
|
|
124
149
|
);
|
|
125
150
|
|
|
126
151
|
const stored = await settingsService.get("SITE_FAVICON_VERSION");
|
|
@@ -135,19 +160,29 @@ describe("Dashboard Settings - Avatar Upload Logic", () => {
|
|
|
135
160
|
await expect(
|
|
136
161
|
settingsService.uploadAvatar(
|
|
137
162
|
{ file },
|
|
138
|
-
{
|
|
163
|
+
{
|
|
164
|
+
media: mediaService,
|
|
165
|
+
storage,
|
|
166
|
+
storageProvider: "r2",
|
|
167
|
+
maxFileSizeMB: 500,
|
|
168
|
+
},
|
|
139
169
|
),
|
|
140
170
|
).rejects.toThrow("File type not allowed");
|
|
141
171
|
});
|
|
142
172
|
|
|
143
173
|
it("throws ValidationError for oversized file", async () => {
|
|
144
174
|
const storage = createMockStorage();
|
|
145
|
-
const file = createMockFile("big.png", "image/png",
|
|
175
|
+
const file = createMockFile("big.png", "image/png", 501 * 1024 * 1024);
|
|
146
176
|
|
|
147
177
|
await expect(
|
|
148
178
|
settingsService.uploadAvatar(
|
|
149
179
|
{ file },
|
|
150
|
-
{
|
|
180
|
+
{
|
|
181
|
+
media: mediaService,
|
|
182
|
+
storage,
|
|
183
|
+
storageProvider: "r2",
|
|
184
|
+
maxFileSizeMB: 500,
|
|
185
|
+
},
|
|
151
186
|
),
|
|
152
187
|
).rejects.toThrow("File too large");
|
|
153
188
|
});
|
|
@@ -93,7 +93,13 @@ dashIndexRoutes.get("/", async (c) => {
|
|
|
93
93
|
]);
|
|
94
94
|
|
|
95
95
|
return c.html(
|
|
96
|
-
<DashLayout
|
|
96
|
+
<DashLayout
|
|
97
|
+
c={c}
|
|
98
|
+
title="Dashboard"
|
|
99
|
+
siteName={siteName}
|
|
100
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
101
|
+
currentPath="/dash"
|
|
102
|
+
>
|
|
97
103
|
<DashboardContent
|
|
98
104
|
publishedCount={publishedCount}
|
|
99
105
|
draftCount={draftCount}
|
|
@@ -29,6 +29,7 @@ mediaRoutes.get("/", async (c) => {
|
|
|
29
29
|
c={c}
|
|
30
30
|
title="Media"
|
|
31
31
|
siteName={siteName}
|
|
32
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
32
33
|
currentPath="/dash/media"
|
|
33
34
|
>
|
|
34
35
|
<MediaListContent
|
|
@@ -36,6 +37,7 @@ mediaRoutes.get("/", async (c) => {
|
|
|
36
37
|
r2PublicUrl={c.var.appConfig.r2PublicUrl}
|
|
37
38
|
imageTransformUrl={c.var.appConfig.imageTransformUrl}
|
|
38
39
|
s3PublicUrl={c.var.appConfig.s3PublicUrl}
|
|
40
|
+
uploadMaxFileSize={c.var.appConfig.uploadMaxFileSize}
|
|
39
41
|
/>
|
|
40
42
|
</DashLayout>,
|
|
41
43
|
);
|
|
@@ -108,6 +110,7 @@ mediaRoutes.get("/:id", async (c) => {
|
|
|
108
110
|
c={c}
|
|
109
111
|
title={media.originalName}
|
|
110
112
|
siteName={siteName}
|
|
113
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
111
114
|
currentPath="/dash/media"
|
|
112
115
|
>
|
|
113
116
|
<ViewMediaContent
|
|
@@ -114,6 +114,7 @@ pagesRoutes.get("/", async (c) => {
|
|
|
114
114
|
c={c}
|
|
115
115
|
title="Pages"
|
|
116
116
|
siteName={siteName}
|
|
117
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
117
118
|
currentPath="/dash/pages"
|
|
118
119
|
>
|
|
119
120
|
<PagesContent pages={pages} />
|
|
@@ -128,6 +129,7 @@ pagesRoutes.get("/new", async (c) => {
|
|
|
128
129
|
c={c}
|
|
129
130
|
title="New Page"
|
|
130
131
|
siteName={siteName}
|
|
132
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
131
133
|
currentPath="/dash/pages"
|
|
132
134
|
>
|
|
133
135
|
<NewPageContent />
|
|
@@ -144,7 +146,8 @@ pagesRoutes.post("/", async (c) => {
|
|
|
144
146
|
parsed.error.issues[0]?.message ??
|
|
145
147
|
i18n._(
|
|
146
148
|
msg({
|
|
147
|
-
message:
|
|
149
|
+
message:
|
|
150
|
+
"Something doesn't look right. Check the form and try again.",
|
|
148
151
|
comment: "@context: Fallback validation error for page form",
|
|
149
152
|
}),
|
|
150
153
|
);
|
|
@@ -174,6 +177,7 @@ pagesRoutes.get("/:id", async (c) => {
|
|
|
174
177
|
c={c}
|
|
175
178
|
title={page.title || "Page"}
|
|
176
179
|
siteName={siteName}
|
|
180
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
177
181
|
currentPath="/dash/pages"
|
|
178
182
|
>
|
|
179
183
|
<ViewPageContent page={page} />
|
|
@@ -194,6 +198,7 @@ pagesRoutes.get("/:id/edit", async (c) => {
|
|
|
194
198
|
c={c}
|
|
195
199
|
title={`Edit: ${page.title || "Page"}`}
|
|
196
200
|
siteName={siteName}
|
|
201
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
197
202
|
currentPath="/dash/pages"
|
|
198
203
|
>
|
|
199
204
|
<EditPageContent page={page} />
|
|
@@ -213,7 +218,8 @@ pagesRoutes.post("/:id", async (c) => {
|
|
|
213
218
|
parsed.error.issues[0]?.message ??
|
|
214
219
|
i18n._(
|
|
215
220
|
msg({
|
|
216
|
-
message:
|
|
221
|
+
message:
|
|
222
|
+
"Something doesn't look right. Check the form and try again.",
|
|
217
223
|
comment: "@context: Fallback validation error for page form",
|
|
218
224
|
}),
|
|
219
225
|
);
|
|
@@ -81,6 +81,7 @@ postsRoutes.get("/", async (c) => {
|
|
|
81
81
|
c={c}
|
|
82
82
|
title="Posts"
|
|
83
83
|
siteName={siteName}
|
|
84
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
84
85
|
currentPath="/dash/posts"
|
|
85
86
|
>
|
|
86
87
|
<PostsListContent posts={postViews} />
|
|
@@ -98,6 +99,7 @@ postsRoutes.get("/new", async (c) => {
|
|
|
98
99
|
c={c}
|
|
99
100
|
title="New Post"
|
|
100
101
|
siteName={siteName}
|
|
102
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
101
103
|
currentPath="/dash/posts"
|
|
102
104
|
>
|
|
103
105
|
<NewPostContent collections={collections} />
|
|
@@ -120,7 +122,7 @@ postsRoutes.post("/", async (c) => {
|
|
|
120
122
|
title: body.title || undefined,
|
|
121
123
|
body: body.body,
|
|
122
124
|
status: body.status,
|
|
123
|
-
|
|
125
|
+
visibility: body.visibility,
|
|
124
126
|
pinned: body.pinned,
|
|
125
127
|
url: body.url || undefined,
|
|
126
128
|
quoteText: body.quoteText || undefined,
|
|
@@ -236,6 +238,7 @@ postsRoutes.get("/:id", async (c) => {
|
|
|
236
238
|
c={c}
|
|
237
239
|
title={pageTitle}
|
|
238
240
|
siteName={siteName}
|
|
241
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
239
242
|
currentPath="/dash/posts"
|
|
240
243
|
>
|
|
241
244
|
<ViewPostContent post={postView} />
|
|
@@ -265,6 +268,7 @@ postsRoutes.get("/:id/edit", async (c) => {
|
|
|
265
268
|
c={c}
|
|
266
269
|
title={`Edit: ${post.title || "Post"}`}
|
|
267
270
|
siteName={siteName}
|
|
271
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
268
272
|
currentPath="/dash/posts"
|
|
269
273
|
>
|
|
270
274
|
<EditPostContent
|
|
@@ -299,7 +303,7 @@ postsRoutes.post("/:id", async (c) => {
|
|
|
299
303
|
title: body.title || null,
|
|
300
304
|
body: body.body || null,
|
|
301
305
|
status: body.status,
|
|
302
|
-
|
|
306
|
+
visibility: body.visibility,
|
|
303
307
|
pinned: body.pinned,
|
|
304
308
|
url: body.url || null,
|
|
305
309
|
quoteText: body.quoteText || null,
|
|
@@ -11,7 +11,6 @@ import type { Bindings, Redirect } from "../../types.js";
|
|
|
11
11
|
import type { AppVariables } from "../../types/app-context.js";
|
|
12
12
|
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
13
13
|
import { EmptyState, ListItemRow, ActionButtons } from "../../ui/dash/index.js";
|
|
14
|
-
import { SettingsNav } from "../../ui/dash/settings/SettingsNav.js";
|
|
15
14
|
import { dsRedirect } from "../../lib/sse.js";
|
|
16
15
|
import { RedirectTypeSchema, parseValidated } from "../../lib/schemas.js";
|
|
17
16
|
|
|
@@ -30,8 +29,6 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
|
|
|
30
29
|
|
|
31
30
|
return (
|
|
32
31
|
<>
|
|
33
|
-
<SettingsNav currentTab="redirects" />
|
|
34
|
-
|
|
35
32
|
<div class="flex items-center justify-between mb-6">
|
|
36
33
|
<h2 class="text-lg font-medium">
|
|
37
34
|
{t({
|
|
@@ -50,7 +47,8 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
|
|
|
50
47
|
{redirects.length === 0 ? (
|
|
51
48
|
<EmptyState
|
|
52
49
|
message={t({
|
|
53
|
-
message:
|
|
50
|
+
message:
|
|
51
|
+
"No redirects yet. Create one to forward traffic from old URLs.",
|
|
54
52
|
comment: "@context: Empty state message",
|
|
55
53
|
})}
|
|
56
54
|
ctaText={t({
|
|
@@ -76,7 +74,7 @@ function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
|
|
|
76
74
|
>
|
|
77
75
|
<div class="flex items-center gap-2">
|
|
78
76
|
<code class="text-sm bg-muted px-1 rounded">{r.fromPath}</code>
|
|
79
|
-
<span class="text-muted-foreground"
|
|
77
|
+
<span class="text-muted-foreground">→</span>
|
|
80
78
|
<code class="text-sm bg-muted px-1 rounded">{r.toPath}</code>
|
|
81
79
|
<span class="badge-outline">{r.type}</span>
|
|
82
80
|
</div>
|
|
@@ -93,8 +91,6 @@ function NewRedirectContent() {
|
|
|
93
91
|
|
|
94
92
|
return (
|
|
95
93
|
<>
|
|
96
|
-
<SettingsNav currentTab="redirects" />
|
|
97
|
-
|
|
98
94
|
<h2 class="text-lg font-medium mb-6">
|
|
99
95
|
{t({ message: "New Redirect", comment: "@context: Page heading" })}
|
|
100
96
|
</h2>
|
|
@@ -203,6 +199,12 @@ function NewRedirectContent() {
|
|
|
203
199
|
);
|
|
204
200
|
}
|
|
205
201
|
|
|
202
|
+
const BREADCRUMB = {
|
|
203
|
+
parent: "Settings",
|
|
204
|
+
parentHref: "/dash/settings",
|
|
205
|
+
current: "Redirects",
|
|
206
|
+
};
|
|
207
|
+
|
|
206
208
|
// List redirects
|
|
207
209
|
redirectsRoutes.get("/", async (c) => {
|
|
208
210
|
const siteName = c.var.appConfig.siteName;
|
|
@@ -211,9 +213,11 @@ redirectsRoutes.get("/", async (c) => {
|
|
|
211
213
|
return c.html(
|
|
212
214
|
<DashLayout
|
|
213
215
|
c={c}
|
|
214
|
-
title="
|
|
216
|
+
title="Redirects"
|
|
215
217
|
siteName={siteName}
|
|
218
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
216
219
|
currentPath="/dash/settings"
|
|
220
|
+
breadcrumb={BREADCRUMB}
|
|
217
221
|
>
|
|
218
222
|
<RedirectsListContent redirects={redirects} />
|
|
219
223
|
</DashLayout>,
|
|
@@ -227,9 +231,11 @@ redirectsRoutes.get("/new", async (c) => {
|
|
|
227
231
|
return c.html(
|
|
228
232
|
<DashLayout
|
|
229
233
|
c={c}
|
|
230
|
-
title="
|
|
234
|
+
title="Redirects"
|
|
231
235
|
siteName={siteName}
|
|
236
|
+
siteAvatarUrl={c.var.appConfig.siteAvatarUrl}
|
|
232
237
|
currentPath="/dash/settings"
|
|
238
|
+
breadcrumb={BREADCRUMB}
|
|
233
239
|
>
|
|
234
240
|
<NewRedirectContent />
|
|
235
241
|
</DashLayout>,
|