@jant/core 0.2.11 → 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/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/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 +13 -3
- 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 -25
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +60 -56
- package/dist/routes/dash/pages.d.ts.map +1 -1
- 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 +2 -2
- 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 +7 -15
- package/src/app.tsx +222 -59
- package/src/auth.ts +5 -1
- package/src/client.ts +1 -1
- package/src/db/migrations/meta/0000_snapshot.json +16 -47
- package/src/db/migrations/meta/_journal.json +1 -1
- package/src/db/schema.ts +22 -7
- package/src/i18n/EXAMPLES.md +45 -23
- package/src/i18n/README.md +39 -25
- 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 +14 -6
- package/src/lib/image.ts +2 -2
- package/src/lib/schemas.ts +7 -3
- package/src/lib/sse.ts +133 -51
- package/src/middleware/auth.ts +6 -2
- package/src/routes/api/posts.ts +9 -9
- package/src/routes/api/upload.ts +39 -10
- package/src/routes/dash/collections.tsx +249 -81
- package/src/routes/dash/index.tsx +22 -7
- package/src/routes/dash/media.tsx +94 -24
- package/src/routes/dash/pages.tsx +132 -54
- package/src/routes/dash/posts.tsx +99 -57
- package/src/routes/dash/redirects.tsx +117 -36
- package/src/routes/dash/settings.tsx +268 -55
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/pages/archive.tsx +78 -24
- package/src/routes/pages/collection.tsx +32 -8
- package/src/routes/pages/home.tsx +38 -10
- package/src/routes/pages/page.tsx +15 -5
- package/src/routes/pages/post.tsx +17 -6
- package/src/routes/pages/search.tsx +50 -13
- package/src/services/collection.ts +29 -8
- package/src/services/index.ts +4 -1
- package/src/services/media.ts +15 -3
- package/src/services/post.ts +37 -10
- package/src/services/redirect.ts +4 -1
- package/src/services/settings.ts +14 -3
- package/src/theme/components/ActionButtons.tsx +31 -15
- package/src/theme/components/CrudPageHeader.tsx +3 -4
- package/src/theme/components/DangerZone.tsx +19 -13
- package/src/theme/components/EmptyState.tsx +1 -5
- package/src/theme/components/PageForm.tsx +80 -25
- package/src/theme/components/Pagination.tsx +34 -31
- package/src/theme/components/PostForm.tsx +91 -27
- package/src/theme/components/PostList.tsx +23 -6
- package/src/theme/components/ThreadView.tsx +25 -10
- package/src/theme/components/TypeBadge.tsx +13 -4
- package/src/theme/components/VisibilityBadge.tsx +17 -5
- package/src/theme/components/index.ts +12 -2
- package/src/theme/layouts/BaseLayout.tsx +6 -5
- package/src/theme/layouts/DashLayout.tsx +71 -18
- package/src/types/lingui-react-macro.d.ts +34 -0
- package/src/types.ts +16 -4
- package/src/vendor/datastar.js +9 -0
- package/src/vendor/datastar.js.map +7 -0
- package/dist/plugin.d.ts +0 -3
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -20
- package/dist/tailwind.d.ts +0 -12
- package/dist/tailwind.d.ts.map +0 -1
- package/dist/tailwind.js +0 -15
|
@@ -3,91 +3,304 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import { useLingui } from "
|
|
6
|
+
import { useLingui } from "@lingui/react/macro";
|
|
7
7
|
import type { Bindings } from "../../types.js";
|
|
8
8
|
import type { AppVariables } from "../../app.js";
|
|
9
9
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
10
|
+
import { sse } from "../../lib/sse.js";
|
|
10
11
|
|
|
11
12
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
12
13
|
|
|
13
14
|
export const settingsRoutes = new Hono<Env>();
|
|
14
15
|
|
|
15
|
-
function SettingsContent({
|
|
16
|
+
function SettingsContent({
|
|
17
|
+
siteName,
|
|
18
|
+
siteDescription,
|
|
19
|
+
siteLanguage,
|
|
20
|
+
saved,
|
|
21
|
+
}: {
|
|
22
|
+
siteName: string;
|
|
23
|
+
siteDescription: string;
|
|
24
|
+
siteLanguage: string;
|
|
25
|
+
saved: boolean;
|
|
26
|
+
}) {
|
|
16
27
|
const { t } = useLingui();
|
|
17
28
|
|
|
29
|
+
const generalSignals = JSON.stringify({
|
|
30
|
+
siteName,
|
|
31
|
+
siteDescription,
|
|
32
|
+
siteLanguage,
|
|
33
|
+
}).replace(/</g, "\\u003c");
|
|
34
|
+
|
|
18
35
|
return (
|
|
19
36
|
<>
|
|
20
|
-
<h1 class="text-2xl font-semibold mb-6">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<label class="label">{t({ message: "Site Description", comment: "@context: Settings form field" })}</label>
|
|
35
|
-
<textarea name="siteDescription" class="textarea" rows={3}>
|
|
36
|
-
{siteDescription}
|
|
37
|
-
</textarea>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<div class="field">
|
|
41
|
-
<label class="label">{t({ message: "Language", comment: "@context: Settings form field" })}</label>
|
|
42
|
-
<select name="siteLanguage" class="select">
|
|
43
|
-
<option value="en" selected={siteLanguage === "en"}>
|
|
44
|
-
English
|
|
45
|
-
</option>
|
|
46
|
-
<option value="zh-Hans" selected={siteLanguage === "zh-Hans"}>
|
|
47
|
-
简体中文
|
|
48
|
-
</option>
|
|
49
|
-
<option value="zh-Hant" selected={siteLanguage === "zh-Hant"}>
|
|
50
|
-
繁體中文
|
|
51
|
-
</option>
|
|
52
|
-
</select>
|
|
53
|
-
</div>
|
|
54
|
-
</section>
|
|
37
|
+
<h1 class="text-2xl font-semibold mb-6">
|
|
38
|
+
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
39
|
+
</h1>
|
|
40
|
+
|
|
41
|
+
{saved && (
|
|
42
|
+
<div
|
|
43
|
+
id="settings-saved-toast"
|
|
44
|
+
class="mb-4 max-w-lg rounded-md border border-green-200 bg-green-50 p-3 text-sm text-green-800 transition-opacity duration-300 dark:border-green-800 dark:bg-green-950 dark:text-green-200"
|
|
45
|
+
data-init={`console.log('[toast] init fired at', Date.now()); history.replaceState({}, '', '/dash/settings'); setTimeout(() => { console.log('[toast] hiding at', Date.now()); const el = document.getElementById('settings-saved-toast'); if (el) { el.style.opacity = '0'; setTimeout(() => el.remove(), 300) } }, 3000)`}
|
|
46
|
+
>
|
|
47
|
+
{t({
|
|
48
|
+
message: "Settings saved successfully.",
|
|
49
|
+
comment: "@context: Toast message after saving settings",
|
|
50
|
+
})}
|
|
55
51
|
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
<div class="flex flex-col gap-6 max-w-lg">
|
|
55
|
+
<form
|
|
56
|
+
data-signals={generalSignals}
|
|
57
|
+
data-on:submit__prevent="@post('/dash/settings')"
|
|
58
|
+
>
|
|
59
|
+
<div id="settings-message"></div>
|
|
60
|
+
<div class="card">
|
|
61
|
+
<header>
|
|
62
|
+
<h2>
|
|
63
|
+
{t({
|
|
64
|
+
message: "General",
|
|
65
|
+
comment: "@context: Settings section heading",
|
|
66
|
+
})}
|
|
67
|
+
</h2>
|
|
68
|
+
</header>
|
|
69
|
+
<section class="flex flex-col gap-4">
|
|
70
|
+
<div class="field">
|
|
71
|
+
<label class="label">
|
|
72
|
+
{t({
|
|
73
|
+
message: "Site Name",
|
|
74
|
+
comment: "@context: Settings form field",
|
|
75
|
+
})}
|
|
76
|
+
</label>
|
|
77
|
+
<input
|
|
78
|
+
type="text"
|
|
79
|
+
data-bind="siteName"
|
|
80
|
+
class="input"
|
|
81
|
+
required
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="field">
|
|
86
|
+
<label class="label">
|
|
87
|
+
{t({
|
|
88
|
+
message: "Site Description",
|
|
89
|
+
comment: "@context: Settings form field",
|
|
90
|
+
})}
|
|
91
|
+
</label>
|
|
92
|
+
<textarea data-bind="siteDescription" class="textarea" rows={3}>
|
|
93
|
+
{siteDescription}
|
|
94
|
+
</textarea>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="field">
|
|
98
|
+
<label class="label">
|
|
99
|
+
{t({
|
|
100
|
+
message: "Language",
|
|
101
|
+
comment: "@context: Settings form field",
|
|
102
|
+
})}
|
|
103
|
+
</label>
|
|
104
|
+
<select data-bind="siteLanguage" class="select">
|
|
105
|
+
<option value="en" selected={siteLanguage === "en"}>
|
|
106
|
+
English
|
|
107
|
+
</option>
|
|
108
|
+
<option value="zh-Hans" selected={siteLanguage === "zh-Hans"}>
|
|
109
|
+
简体中文
|
|
110
|
+
</option>
|
|
111
|
+
<option value="zh-Hant" selected={siteLanguage === "zh-Hant"}>
|
|
112
|
+
繁體中文
|
|
113
|
+
</option>
|
|
114
|
+
</select>
|
|
115
|
+
</div>
|
|
116
|
+
</section>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<button type="submit" class="btn mt-4">
|
|
120
|
+
{t({
|
|
121
|
+
message: "Save Settings",
|
|
122
|
+
comment: "@context: Button to save settings",
|
|
123
|
+
})}
|
|
124
|
+
</button>
|
|
125
|
+
</form>
|
|
126
|
+
|
|
127
|
+
<form
|
|
128
|
+
data-signals="{currentPassword: '', newPassword: '', confirmPassword: ''}"
|
|
129
|
+
data-on:submit__prevent="@post('/dash/settings/password')"
|
|
130
|
+
>
|
|
131
|
+
<div id="password-message"></div>
|
|
132
|
+
<div class="card">
|
|
133
|
+
<header>
|
|
134
|
+
<h2>
|
|
135
|
+
{t({
|
|
136
|
+
message: "Change Password",
|
|
137
|
+
comment: "@context: Settings section heading",
|
|
138
|
+
})}
|
|
139
|
+
</h2>
|
|
140
|
+
</header>
|
|
141
|
+
<section class="flex flex-col gap-4">
|
|
142
|
+
<div class="field">
|
|
143
|
+
<label class="label">
|
|
144
|
+
{t({
|
|
145
|
+
message: "Current Password",
|
|
146
|
+
comment: "@context: Password form field",
|
|
147
|
+
})}
|
|
148
|
+
</label>
|
|
149
|
+
<input
|
|
150
|
+
type="password"
|
|
151
|
+
data-bind="currentPassword"
|
|
152
|
+
class="input"
|
|
153
|
+
required
|
|
154
|
+
autocomplete="current-password"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="field">
|
|
159
|
+
<label class="label">
|
|
160
|
+
{t({
|
|
161
|
+
message: "New Password",
|
|
162
|
+
comment: "@context: Password form field",
|
|
163
|
+
})}
|
|
164
|
+
</label>
|
|
165
|
+
<input
|
|
166
|
+
type="password"
|
|
167
|
+
data-bind="newPassword"
|
|
168
|
+
class="input"
|
|
169
|
+
required
|
|
170
|
+
minlength={8}
|
|
171
|
+
autocomplete="new-password"
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="field">
|
|
176
|
+
<label class="label">
|
|
177
|
+
{t({
|
|
178
|
+
message: "Confirm New Password",
|
|
179
|
+
comment: "@context: Password form field",
|
|
180
|
+
})}
|
|
181
|
+
</label>
|
|
182
|
+
<input
|
|
183
|
+
type="password"
|
|
184
|
+
data-bind="confirmPassword"
|
|
185
|
+
class="input"
|
|
186
|
+
required
|
|
187
|
+
minlength={8}
|
|
188
|
+
autocomplete="new-password"
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
</section>
|
|
192
|
+
</div>
|
|
56
193
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
194
|
+
<button type="submit" class="btn mt-4">
|
|
195
|
+
{t({
|
|
196
|
+
message: "Change Password",
|
|
197
|
+
comment: "@context: Button to change password",
|
|
198
|
+
})}
|
|
199
|
+
</button>
|
|
200
|
+
</form>
|
|
201
|
+
</div>
|
|
61
202
|
</>
|
|
62
203
|
);
|
|
63
204
|
}
|
|
64
205
|
|
|
65
206
|
// Settings page
|
|
66
207
|
settingsRoutes.get("/", async (c) => {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
208
|
+
const all = await c.var.services.settings.getAll();
|
|
209
|
+
const siteName = all["SITE_NAME"] ?? "Jant";
|
|
210
|
+
const siteDescription = all["SITE_DESCRIPTION"] ?? "";
|
|
211
|
+
const siteLanguage = all["SITE_LANGUAGE"] ?? "en";
|
|
212
|
+
const saved = c.req.query("saved") !== undefined;
|
|
70
213
|
|
|
71
214
|
return c.html(
|
|
72
|
-
<DashLayout
|
|
73
|
-
|
|
74
|
-
|
|
215
|
+
<DashLayout
|
|
216
|
+
c={c}
|
|
217
|
+
title="Settings"
|
|
218
|
+
siteName={siteName}
|
|
219
|
+
currentPath="/dash/settings"
|
|
220
|
+
>
|
|
221
|
+
<SettingsContent
|
|
222
|
+
siteName={siteName}
|
|
223
|
+
siteDescription={siteDescription}
|
|
224
|
+
siteLanguage={siteLanguage}
|
|
225
|
+
saved={saved}
|
|
226
|
+
/>
|
|
227
|
+
</DashLayout>,
|
|
75
228
|
);
|
|
76
229
|
});
|
|
77
230
|
|
|
78
231
|
// Update settings
|
|
79
232
|
settingsRoutes.post("/", async (c) => {
|
|
80
|
-
const
|
|
233
|
+
const body = await c.req.json<{
|
|
234
|
+
siteName: string;
|
|
235
|
+
siteDescription: string;
|
|
236
|
+
siteLanguage: string;
|
|
237
|
+
}>();
|
|
81
238
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const siteLanguage = formData.get("siteLanguage") as string;
|
|
239
|
+
const oldLanguage =
|
|
240
|
+
(await c.var.services.settings.get("SITE_LANGUAGE")) ?? "en";
|
|
85
241
|
|
|
86
242
|
await c.var.services.settings.setMany({
|
|
87
|
-
SITE_NAME: siteName,
|
|
88
|
-
SITE_DESCRIPTION: siteDescription,
|
|
89
|
-
SITE_LANGUAGE: siteLanguage,
|
|
243
|
+
SITE_NAME: body.siteName,
|
|
244
|
+
SITE_DESCRIPTION: body.siteDescription,
|
|
245
|
+
SITE_LANGUAGE: body.siteLanguage,
|
|
90
246
|
});
|
|
91
247
|
|
|
92
|
-
|
|
248
|
+
const languageChanged = oldLanguage !== body.siteLanguage;
|
|
249
|
+
|
|
250
|
+
return sse(c, async (stream) => {
|
|
251
|
+
if (languageChanged) {
|
|
252
|
+
// Language changed - full reload needed to update all UI text
|
|
253
|
+
await stream.redirect("/dash/settings?saved");
|
|
254
|
+
} else {
|
|
255
|
+
// No language change - show inline success message
|
|
256
|
+
await stream.patchElements(
|
|
257
|
+
'<div id="settings-message"><div class="rounded-md border border-green-200 bg-green-50 p-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200 mb-4 transition-opacity duration-300" data-init="setTimeout(() => { el.style.opacity = \'0\'; setTimeout(() => el.remove(), 300) }, 3000)">Settings saved successfully.</div></div>',
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Change password
|
|
264
|
+
settingsRoutes.post("/password", async (c) => {
|
|
265
|
+
const body = await c.req.json<{
|
|
266
|
+
currentPassword: string;
|
|
267
|
+
newPassword: string;
|
|
268
|
+
confirmPassword: string;
|
|
269
|
+
}>();
|
|
270
|
+
|
|
271
|
+
if (body.newPassword !== body.confirmPassword) {
|
|
272
|
+
return sse(c, async (stream) => {
|
|
273
|
+
await stream.patchElements(
|
|
274
|
+
'<div id="password-message"><div class="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200 mb-4">Passwords do not match.</div></div>',
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await c.var.auth.api.changePassword({
|
|
281
|
+
body: {
|
|
282
|
+
currentPassword: body.currentPassword,
|
|
283
|
+
newPassword: body.newPassword,
|
|
284
|
+
revokeOtherSessions: false,
|
|
285
|
+
},
|
|
286
|
+
headers: c.req.raw.headers,
|
|
287
|
+
});
|
|
288
|
+
} catch {
|
|
289
|
+
return sse(c, async (stream) => {
|
|
290
|
+
await stream.patchElements(
|
|
291
|
+
'<div id="password-message"><div class="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200 mb-4">Current password is incorrect.</div></div>',
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return sse(c, async (stream) => {
|
|
297
|
+
await stream.patchElements(
|
|
298
|
+
'<div id="password-message"><div class="rounded-md border border-green-200 bg-green-50 p-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200 mb-4">Password changed successfully.</div></div>',
|
|
299
|
+
);
|
|
300
|
+
await stream.patchSignals({
|
|
301
|
+
currentPassword: "",
|
|
302
|
+
newPassword: "",
|
|
303
|
+
confirmPassword: "",
|
|
304
|
+
});
|
|
305
|
+
});
|
|
93
306
|
});
|
package/src/routes/feed/rss.ts
CHANGED
|
@@ -14,8 +14,9 @@ export const rssRoutes = new Hono<Env>();
|
|
|
14
14
|
|
|
15
15
|
// RSS 2.0 Feed - main feed at /feed
|
|
16
16
|
rssRoutes.get("/", async (c) => {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const all = await c.var.services.settings.getAll();
|
|
18
|
+
const siteName = all["SITE_NAME"] ?? "Jant";
|
|
19
|
+
const siteDescription = all["SITE_DESCRIPTION"] ?? "";
|
|
19
20
|
const siteUrl = c.env.SITE_URL;
|
|
20
21
|
|
|
21
22
|
const posts = await c.var.services.posts.list({
|
|
@@ -61,8 +62,9 @@ rssRoutes.get("/", async (c) => {
|
|
|
61
62
|
|
|
62
63
|
// Atom Feed
|
|
63
64
|
rssRoutes.get("/atom.xml", async (c) => {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
65
|
+
const all = await c.var.services.settings.getAll();
|
|
66
|
+
const siteName = all["SITE_NAME"] ?? "Jant";
|
|
67
|
+
const siteDescription = all["SITE_DESCRIPTION"] ?? "";
|
|
66
68
|
const siteUrl = c.env.SITE_URL;
|
|
67
69
|
|
|
68
70
|
const posts = await c.var.services.posts.list({
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import { useLingui } from "
|
|
8
|
+
import { useLingui } from "@lingui/react/macro";
|
|
9
9
|
import type { Bindings, Post, PostType } from "../../types.js";
|
|
10
10
|
import type { AppVariables } from "../../app.js";
|
|
11
11
|
import { BaseLayout } from "../../theme/layouts/index.js";
|
|
@@ -24,10 +24,19 @@ function getTypeLabel(type: string): string {
|
|
|
24
24
|
const { t } = useLingui();
|
|
25
25
|
const labels: Record<string, string> = {
|
|
26
26
|
note: t({ message: "Note", comment: "@context: Post type label - note" }),
|
|
27
|
-
article: t({
|
|
27
|
+
article: t({
|
|
28
|
+
message: "Article",
|
|
29
|
+
comment: "@context: Post type label - article",
|
|
30
|
+
}),
|
|
28
31
|
link: t({ message: "Link", comment: "@context: Post type label - link" }),
|
|
29
|
-
quote: t({
|
|
30
|
-
|
|
32
|
+
quote: t({
|
|
33
|
+
message: "Quote",
|
|
34
|
+
comment: "@context: Post type label - quote",
|
|
35
|
+
}),
|
|
36
|
+
image: t({
|
|
37
|
+
message: "Image",
|
|
38
|
+
comment: "@context: Post type label - image",
|
|
39
|
+
}),
|
|
31
40
|
page: t({ message: "Page", comment: "@context: Post type label - page" }),
|
|
32
41
|
};
|
|
33
42
|
return labels[type] ?? type;
|
|
@@ -36,12 +45,30 @@ function getTypeLabel(type: string): string {
|
|
|
36
45
|
function getTypeLabelPlural(type: string): string {
|
|
37
46
|
const { t } = useLingui();
|
|
38
47
|
const labels: Record<string, string> = {
|
|
39
|
-
note: t({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
note: t({
|
|
49
|
+
message: "Notes",
|
|
50
|
+
comment: "@context: Post type label plural - notes",
|
|
51
|
+
}),
|
|
52
|
+
article: t({
|
|
53
|
+
message: "Articles",
|
|
54
|
+
comment: "@context: Post type label plural - articles",
|
|
55
|
+
}),
|
|
56
|
+
link: t({
|
|
57
|
+
message: "Links",
|
|
58
|
+
comment: "@context: Post type label plural - links",
|
|
59
|
+
}),
|
|
60
|
+
quote: t({
|
|
61
|
+
message: "Quotes",
|
|
62
|
+
comment: "@context: Post type label plural - quotes",
|
|
63
|
+
}),
|
|
64
|
+
image: t({
|
|
65
|
+
message: "Images",
|
|
66
|
+
comment: "@context: Post type label plural - images",
|
|
67
|
+
}),
|
|
68
|
+
page: t({
|
|
69
|
+
message: "Pages",
|
|
70
|
+
comment: "@context: Post type label plural - pages",
|
|
71
|
+
}),
|
|
45
72
|
};
|
|
46
73
|
return labels[type] ?? `${type}s`;
|
|
47
74
|
}
|
|
@@ -84,7 +111,10 @@ function ArchiveContent({
|
|
|
84
111
|
href="/archive"
|
|
85
112
|
class={`badge ${!type ? "badge-primary" : "badge-outline"}`}
|
|
86
113
|
>
|
|
87
|
-
{t({
|
|
114
|
+
{t({
|
|
115
|
+
message: "All",
|
|
116
|
+
comment: "@context: Archive filter - all types",
|
|
117
|
+
})}
|
|
88
118
|
</a>
|
|
89
119
|
{POST_TYPES.filter((t) => t !== "page").map((typeKey) => (
|
|
90
120
|
<a
|
|
@@ -101,7 +131,10 @@ function ArchiveContent({
|
|
|
101
131
|
<main>
|
|
102
132
|
{displayPosts.length === 0 ? (
|
|
103
133
|
<p class="text-muted-foreground">
|
|
104
|
-
{t({
|
|
134
|
+
{t({
|
|
135
|
+
message: "No posts found.",
|
|
136
|
+
comment: "@context: Archive empty state",
|
|
137
|
+
})}
|
|
105
138
|
</p>
|
|
106
139
|
) : (
|
|
107
140
|
Array.from(grouped.entries()).map(([yearMonth, monthPosts]) => (
|
|
@@ -125,16 +158,31 @@ function ArchiveContent({
|
|
|
125
158
|
href={`/p/${sqid.encode(post.id)}`}
|
|
126
159
|
class="hover:underline"
|
|
127
160
|
>
|
|
128
|
-
{post.title ||
|
|
161
|
+
{post.title ||
|
|
162
|
+
post.content?.slice(0, 80) ||
|
|
163
|
+
`Post #${post.id}`}
|
|
129
164
|
</a>
|
|
130
165
|
{!type && (
|
|
131
|
-
<span class="ml-2 badge-outline text-xs">
|
|
166
|
+
<span class="ml-2 badge-outline text-xs">
|
|
167
|
+
{getTypeLabel(post.type)}
|
|
168
|
+
</span>
|
|
132
169
|
)}
|
|
133
170
|
{replyCount && replyCount > 0 && (
|
|
134
171
|
<span class="ml-2 text-xs text-muted-foreground">
|
|
135
|
-
(
|
|
136
|
-
|
|
137
|
-
|
|
172
|
+
(
|
|
173
|
+
{replyCount === 1
|
|
174
|
+
? t({
|
|
175
|
+
message: "1 reply",
|
|
176
|
+
comment:
|
|
177
|
+
"@context: Archive post reply indicator - single",
|
|
178
|
+
})
|
|
179
|
+
: t({
|
|
180
|
+
message: "{count} replies",
|
|
181
|
+
comment:
|
|
182
|
+
"@context: Archive post reply indicator - plural",
|
|
183
|
+
values: { count: String(replyCount) },
|
|
184
|
+
})}
|
|
185
|
+
)
|
|
138
186
|
</span>
|
|
139
187
|
)}
|
|
140
188
|
</div>
|
|
@@ -156,7 +204,11 @@ function ArchiveContent({
|
|
|
156
204
|
|
|
157
205
|
<nav class="mt-4">
|
|
158
206
|
<a href="/" class="text-sm hover:underline">
|
|
159
|
-
←
|
|
207
|
+
←{" "}
|
|
208
|
+
{t({
|
|
209
|
+
message: "Back to home",
|
|
210
|
+
comment: "@context: Navigation link back to home page",
|
|
211
|
+
})}
|
|
160
212
|
</a>
|
|
161
213
|
</nav>
|
|
162
214
|
</div>
|
|
@@ -166,7 +218,8 @@ function ArchiveContent({
|
|
|
166
218
|
// Archive page - all posts
|
|
167
219
|
archiveRoutes.get("/", async (c) => {
|
|
168
220
|
const typeParam = c.req.query("type") as PostType | undefined;
|
|
169
|
-
const type =
|
|
221
|
+
const type =
|
|
222
|
+
typeParam && POST_TYPES.includes(typeParam) ? typeParam : undefined;
|
|
170
223
|
|
|
171
224
|
// Parse cursor
|
|
172
225
|
const cursorParam = c.req.query("cursor");
|
|
@@ -191,10 +244,11 @@ archiveRoutes.get("/", async (c) => {
|
|
|
191
244
|
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
192
245
|
|
|
193
246
|
// Get next cursor
|
|
194
|
-
const nextCursor =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
247
|
+
const nextCursor =
|
|
248
|
+
hasMore && displayPosts.length > 0
|
|
249
|
+
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Length check above guarantees element exists
|
|
250
|
+
displayPosts[displayPosts.length - 1]!.id
|
|
251
|
+
: undefined;
|
|
198
252
|
|
|
199
253
|
// Group posts by year-month
|
|
200
254
|
const grouped = new Map<string, typeof displayPosts>();
|
|
@@ -218,6 +272,6 @@ archiveRoutes.get("/", async (c) => {
|
|
|
218
272
|
grouped={grouped}
|
|
219
273
|
replyCounts={replyCounts}
|
|
220
274
|
/>
|
|
221
|
-
</BaseLayout
|
|
275
|
+
</BaseLayout>,
|
|
222
276
|
);
|
|
223
277
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import { useLingui } from "
|
|
6
|
+
import { useLingui } from "@lingui/react/macro";
|
|
7
7
|
import type { Bindings, Collection, Post } from "../../types.js";
|
|
8
8
|
import type { AppVariables } from "../../app.js";
|
|
9
9
|
import { BaseLayout } from "../../theme/layouts/index.js";
|
|
@@ -14,7 +14,13 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
14
14
|
|
|
15
15
|
export const collectionRoutes = new Hono<Env>();
|
|
16
16
|
|
|
17
|
-
function CollectionContent({
|
|
17
|
+
function CollectionContent({
|
|
18
|
+
collection,
|
|
19
|
+
posts,
|
|
20
|
+
}: {
|
|
21
|
+
collection: Collection;
|
|
22
|
+
posts: Post[];
|
|
23
|
+
}) {
|
|
18
24
|
const { t } = useLingui();
|
|
19
25
|
|
|
20
26
|
return (
|
|
@@ -28,13 +34,21 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
|
|
|
28
34
|
|
|
29
35
|
<main class="flex flex-col gap-6">
|
|
30
36
|
{posts.length === 0 ? (
|
|
31
|
-
<p class="text-muted-foreground">
|
|
37
|
+
<p class="text-muted-foreground">
|
|
38
|
+
{t({
|
|
39
|
+
message: "No posts in this collection.",
|
|
40
|
+
comment: "@context: Empty state message",
|
|
41
|
+
})}
|
|
42
|
+
</p>
|
|
32
43
|
) : (
|
|
33
44
|
posts.map((post) => (
|
|
34
45
|
<article key={post.id} class="h-entry">
|
|
35
46
|
{post.title && (
|
|
36
47
|
<h2 class="p-name text-lg font-medium mb-2">
|
|
37
|
-
<a
|
|
48
|
+
<a
|
|
49
|
+
href={`/p/${sqid.encode(post.id)}`}
|
|
50
|
+
class="u-url hover:underline"
|
|
51
|
+
>
|
|
38
52
|
{post.title}
|
|
39
53
|
</a>
|
|
40
54
|
</h2>
|
|
@@ -44,7 +58,10 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
|
|
|
44
58
|
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
45
59
|
/>
|
|
46
60
|
<footer class="mt-2 text-sm text-muted-foreground">
|
|
47
|
-
<time
|
|
61
|
+
<time
|
|
62
|
+
class="dt-published"
|
|
63
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
64
|
+
>
|
|
48
65
|
{time.formatDate(post.publishedAt)}
|
|
49
66
|
</time>
|
|
50
67
|
</footer>
|
|
@@ -55,7 +72,10 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
|
|
|
55
72
|
|
|
56
73
|
<nav class="mt-8">
|
|
57
74
|
<a href="/" class="text-sm hover:underline">
|
|
58
|
-
{t({
|
|
75
|
+
{t({
|
|
76
|
+
message: "← Back to home",
|
|
77
|
+
comment: "@context: Navigation link",
|
|
78
|
+
})}
|
|
59
79
|
</a>
|
|
60
80
|
</nav>
|
|
61
81
|
</div>
|
|
@@ -72,8 +92,12 @@ collectionRoutes.get("/:path", async (c) => {
|
|
|
72
92
|
const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
|
|
73
93
|
|
|
74
94
|
return c.html(
|
|
75
|
-
<BaseLayout
|
|
95
|
+
<BaseLayout
|
|
96
|
+
title={`${collection.title} - ${siteName}`}
|
|
97
|
+
description={collection.description ?? undefined}
|
|
98
|
+
c={c}
|
|
99
|
+
>
|
|
76
100
|
<CollectionContent collection={collection} posts={posts} />
|
|
77
|
-
</BaseLayout
|
|
101
|
+
</BaseLayout>,
|
|
78
102
|
);
|
|
79
103
|
});
|