@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
|
@@ -3,10 +3,11 @@
|
|
|
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
|
|
|
@@ -16,98 +17,290 @@ function SettingsContent({
|
|
|
16
17
|
siteName,
|
|
17
18
|
siteDescription,
|
|
18
19
|
siteLanguage,
|
|
20
|
+
saved,
|
|
19
21
|
}: {
|
|
20
22
|
siteName: string;
|
|
21
23
|
siteDescription: string;
|
|
22
24
|
siteLanguage: string;
|
|
25
|
+
saved: boolean;
|
|
23
26
|
}) {
|
|
24
27
|
const { t } = useLingui();
|
|
25
28
|
|
|
29
|
+
const generalSignals = JSON.stringify({
|
|
30
|
+
siteName,
|
|
31
|
+
siteDescription,
|
|
32
|
+
siteLanguage,
|
|
33
|
+
}).replace(/</g, "\\u003c");
|
|
34
|
+
|
|
26
35
|
return (
|
|
27
36
|
<>
|
|
28
37
|
<h1 class="text-2xl font-semibold mb-6">
|
|
29
38
|
{t({ message: "Settings", comment: "@context: Dashboard heading" })}
|
|
30
39
|
</h1>
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
<div
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<input type="text" name="siteName" class="input" value={siteName} required />
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<div class="field">
|
|
46
|
-
<label class="label">
|
|
47
|
-
{t({ message: "Site Description", comment: "@context: Settings form field" })}
|
|
48
|
-
</label>
|
|
49
|
-
<textarea name="siteDescription" class="textarea" rows={3}>
|
|
50
|
-
{siteDescription}
|
|
51
|
-
</textarea>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div class="field">
|
|
55
|
-
<label class="label">
|
|
56
|
-
{t({ message: "Language", comment: "@context: Settings form field" })}
|
|
57
|
-
</label>
|
|
58
|
-
<select name="siteLanguage" class="select">
|
|
59
|
-
<option value="en" selected={siteLanguage === "en"}>
|
|
60
|
-
English
|
|
61
|
-
</option>
|
|
62
|
-
<option value="zh-Hans" selected={siteLanguage === "zh-Hans"}>
|
|
63
|
-
简体中文
|
|
64
|
-
</option>
|
|
65
|
-
<option value="zh-Hant" selected={siteLanguage === "zh-Hant"}>
|
|
66
|
-
繁體中文
|
|
67
|
-
</option>
|
|
68
|
-
</select>
|
|
69
|
-
</div>
|
|
70
|
-
</section>
|
|
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
|
+
})}
|
|
71
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>
|
|
72
193
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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>
|
|
77
202
|
</>
|
|
78
203
|
);
|
|
79
204
|
}
|
|
80
205
|
|
|
81
206
|
// Settings page
|
|
82
207
|
settingsRoutes.get("/", async (c) => {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
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;
|
|
86
213
|
|
|
87
214
|
return c.html(
|
|
88
|
-
<DashLayout
|
|
215
|
+
<DashLayout
|
|
216
|
+
c={c}
|
|
217
|
+
title="Settings"
|
|
218
|
+
siteName={siteName}
|
|
219
|
+
currentPath="/dash/settings"
|
|
220
|
+
>
|
|
89
221
|
<SettingsContent
|
|
90
222
|
siteName={siteName}
|
|
91
223
|
siteDescription={siteDescription}
|
|
92
224
|
siteLanguage={siteLanguage}
|
|
225
|
+
saved={saved}
|
|
93
226
|
/>
|
|
94
|
-
</DashLayout
|
|
227
|
+
</DashLayout>,
|
|
95
228
|
);
|
|
96
229
|
});
|
|
97
230
|
|
|
98
231
|
// Update settings
|
|
99
232
|
settingsRoutes.post("/", async (c) => {
|
|
100
|
-
const
|
|
233
|
+
const body = await c.req.json<{
|
|
234
|
+
siteName: string;
|
|
235
|
+
siteDescription: string;
|
|
236
|
+
siteLanguage: string;
|
|
237
|
+
}>();
|
|
101
238
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const siteLanguage = formData.get("siteLanguage") as string;
|
|
239
|
+
const oldLanguage =
|
|
240
|
+
(await c.var.services.settings.get("SITE_LANGUAGE")) ?? "en";
|
|
105
241
|
|
|
106
242
|
await c.var.services.settings.setMany({
|
|
107
|
-
SITE_NAME: siteName,
|
|
108
|
-
SITE_DESCRIPTION: siteDescription,
|
|
109
|
-
SITE_LANGUAGE: siteLanguage,
|
|
243
|
+
SITE_NAME: body.siteName,
|
|
244
|
+
SITE_DESCRIPTION: body.siteDescription,
|
|
245
|
+
SITE_LANGUAGE: body.siteLanguage,
|
|
110
246
|
});
|
|
111
247
|
|
|
112
|
-
|
|
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
|
+
});
|
|
113
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
|
}
|
|
@@ -80,8 +107,14 @@ function ArchiveContent({
|
|
|
80
107
|
|
|
81
108
|
{/* Type filter */}
|
|
82
109
|
<nav class="flex flex-wrap gap-2 mt-4">
|
|
83
|
-
<a
|
|
84
|
-
|
|
110
|
+
<a
|
|
111
|
+
href="/archive"
|
|
112
|
+
class={`badge ${!type ? "badge-primary" : "badge-outline"}`}
|
|
113
|
+
>
|
|
114
|
+
{t({
|
|
115
|
+
message: "All",
|
|
116
|
+
comment: "@context: Archive filter - all types",
|
|
117
|
+
})}
|
|
85
118
|
</a>
|
|
86
119
|
{POST_TYPES.filter((t) => t !== "page").map((typeKey) => (
|
|
87
120
|
<a
|
|
@@ -98,7 +131,10 @@ function ArchiveContent({
|
|
|
98
131
|
<main>
|
|
99
132
|
{displayPosts.length === 0 ? (
|
|
100
133
|
<p class="text-muted-foreground">
|
|
101
|
-
{t({
|
|
134
|
+
{t({
|
|
135
|
+
message: "No posts found.",
|
|
136
|
+
comment: "@context: Archive empty state",
|
|
137
|
+
})}
|
|
102
138
|
</p>
|
|
103
139
|
) : (
|
|
104
140
|
Array.from(grouped.entries()).map(([yearMonth, monthPosts]) => (
|
|
@@ -118,11 +154,18 @@ function ArchiveContent({
|
|
|
118
154
|
{new Date(post.publishedAt * 1000).getDate()}
|
|
119
155
|
</time>
|
|
120
156
|
<div class="flex-1 min-w-0">
|
|
121
|
-
<a
|
|
122
|
-
{
|
|
157
|
+
<a
|
|
158
|
+
href={`/p/${sqid.encode(post.id)}`}
|
|
159
|
+
class="hover:underline"
|
|
160
|
+
>
|
|
161
|
+
{post.title ||
|
|
162
|
+
post.content?.slice(0, 80) ||
|
|
163
|
+
`Post #${post.id}`}
|
|
123
164
|
</a>
|
|
124
165
|
{!type && (
|
|
125
|
-
<span class="ml-2 badge-outline text-xs">
|
|
166
|
+
<span class="ml-2 badge-outline text-xs">
|
|
167
|
+
{getTypeLabel(post.type)}
|
|
168
|
+
</span>
|
|
126
169
|
)}
|
|
127
170
|
{replyCount && replyCount > 0 && (
|
|
128
171
|
<span class="ml-2 text-xs text-muted-foreground">
|
|
@@ -130,11 +173,13 @@ function ArchiveContent({
|
|
|
130
173
|
{replyCount === 1
|
|
131
174
|
? t({
|
|
132
175
|
message: "1 reply",
|
|
133
|
-
comment:
|
|
176
|
+
comment:
|
|
177
|
+
"@context: Archive post reply indicator - single",
|
|
134
178
|
})
|
|
135
179
|
: t({
|
|
136
180
|
message: "{count} replies",
|
|
137
|
-
comment:
|
|
181
|
+
comment:
|
|
182
|
+
"@context: Archive post reply indicator - plural",
|
|
138
183
|
values: { count: String(replyCount) },
|
|
139
184
|
})}
|
|
140
185
|
)
|
|
@@ -159,7 +204,11 @@ function ArchiveContent({
|
|
|
159
204
|
|
|
160
205
|
<nav class="mt-4">
|
|
161
206
|
<a href="/" class="text-sm hover:underline">
|
|
162
|
-
←
|
|
207
|
+
←{" "}
|
|
208
|
+
{t({
|
|
209
|
+
message: "Back to home",
|
|
210
|
+
comment: "@context: Navigation link back to home page",
|
|
211
|
+
})}
|
|
163
212
|
</a>
|
|
164
213
|
</nav>
|
|
165
214
|
</div>
|
|
@@ -169,7 +218,8 @@ function ArchiveContent({
|
|
|
169
218
|
// Archive page - all posts
|
|
170
219
|
archiveRoutes.get("/", async (c) => {
|
|
171
220
|
const typeParam = c.req.query("type") as PostType | undefined;
|
|
172
|
-
const type =
|
|
221
|
+
const type =
|
|
222
|
+
typeParam && POST_TYPES.includes(typeParam) ? typeParam : undefined;
|
|
173
223
|
|
|
174
224
|
// Parse cursor
|
|
175
225
|
const cursorParam = c.req.query("cursor");
|
|
@@ -222,6 +272,6 @@ archiveRoutes.get("/", async (c) => {
|
|
|
222
272
|
grouped={grouped}
|
|
223
273
|
replyCounts={replyCounts}
|
|
224
274
|
/>
|
|
225
|
-
</BaseLayout
|
|
275
|
+
</BaseLayout>,
|
|
226
276
|
);
|
|
227
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 (
|
|
@@ -39,7 +45,10 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
|
|
|
39
45
|
<article key={post.id} class="h-entry">
|
|
40
46
|
{post.title && (
|
|
41
47
|
<h2 class="p-name text-lg font-medium mb-2">
|
|
42
|
-
<a
|
|
48
|
+
<a
|
|
49
|
+
href={`/p/${sqid.encode(post.id)}`}
|
|
50
|
+
class="u-url hover:underline"
|
|
51
|
+
>
|
|
43
52
|
{post.title}
|
|
44
53
|
</a>
|
|
45
54
|
</h2>
|
|
@@ -49,7 +58,10 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
|
|
|
49
58
|
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
50
59
|
/>
|
|
51
60
|
<footer class="mt-2 text-sm text-muted-foreground">
|
|
52
|
-
<time
|
|
61
|
+
<time
|
|
62
|
+
class="dt-published"
|
|
63
|
+
datetime={time.toISOString(post.publishedAt)}
|
|
64
|
+
>
|
|
53
65
|
{time.formatDate(post.publishedAt)}
|
|
54
66
|
</time>
|
|
55
67
|
</footer>
|
|
@@ -60,7 +72,10 @@ function CollectionContent({ collection, posts }: { collection: Collection; post
|
|
|
60
72
|
|
|
61
73
|
<nav class="mt-8">
|
|
62
74
|
<a href="/" class="text-sm hover:underline">
|
|
63
|
-
{t({
|
|
75
|
+
{t({
|
|
76
|
+
message: "← Back to home",
|
|
77
|
+
comment: "@context: Navigation link",
|
|
78
|
+
})}
|
|
64
79
|
</a>
|
|
65
80
|
</nav>
|
|
66
81
|
</div>
|
|
@@ -83,6 +98,6 @@ collectionRoutes.get("/:path", async (c) => {
|
|
|
83
98
|
c={c}
|
|
84
99
|
>
|
|
85
100
|
<CollectionContent collection={collection} posts={posts} />
|
|
86
|
-
</BaseLayout
|
|
101
|
+
</BaseLayout>,
|
|
87
102
|
);
|
|
88
103
|
});
|