@jant/core 0.3.26 → 0.3.28

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.
Files changed (314) hide show
  1. package/dist/client/client.css +1 -0
  2. package/dist/client/client.js +31561 -0
  3. package/dist/index.js +15209 -15
  4. package/package.json +21 -15
  5. package/src/__tests__/helpers/app.ts +19 -3
  6. package/src/__tests__/helpers/db.ts +44 -0
  7. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  8. package/src/app.tsx +112 -173
  9. package/src/auth.ts +4 -1
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -265
  172. package/dist/auth.js +0 -36
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -1,98 +1,19 @@
1
1
  /**
2
2
  * General settings form
3
+ *
4
+ * Server-side template that renders Lit Web Components for the
5
+ * settings page. Provides translated labels, initial data, and
6
+ * timezone/language options as JSON attributes.
7
+ *
8
+ * The Lit components <jant-settings-avatar> and <jant-settings-general>
9
+ * handle all form state and rendering. The settings-bridge.ts script
10
+ * handles server communication.
3
11
  */
4
12
 
5
13
  import { useLingui } from "@lingui/react/macro";
6
14
  import type { TimezoneEntry } from "../../../lib/timezones.js";
7
15
  import { SettingsNav } from "./SettingsNav.js";
8
16
 
9
- /**
10
- * Build data-signals JSON with `_orig_<key>` duplicates for cancel/reset.
11
- * Private `_orig_*` signals store original values so Cancel can revert.
12
- * The `dirty` signal tracks whether the user has made any changes.
13
- */
14
- function buildSignals(fields: Record<string, string>, dirty: string): string {
15
- const signals: Record<string, string | boolean> = {};
16
- for (const [key, value] of Object.entries(fields)) {
17
- signals[key] = value;
18
- signals[`_orig_${key}`] = value;
19
- }
20
- signals[dirty] = false;
21
- return JSON.stringify(signals).replace(/</g, "\\u003c");
22
- }
23
-
24
- /** Spinner SVG shown inside buttons during loading */
25
- function Spinner({ show }: { show: string }) {
26
- return (
27
- <svg
28
- data-show={show}
29
- style="display:none"
30
- class="animate-spin size-4"
31
- xmlns="http://www.w3.org/2000/svg"
32
- viewBox="0 0 24 24"
33
- fill="none"
34
- stroke="currentColor"
35
- stroke-width="2"
36
- stroke-linecap="round"
37
- stroke-linejoin="round"
38
- role="status"
39
- >
40
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
41
- </svg>
42
- );
43
- }
44
-
45
- /**
46
- * Save + Cancel button pair.
47
- * Both are disabled when no changes (`!dirty`) or during loading.
48
- * Cancel resets all signals to originals and clears dirty.
49
- */
50
- function FormActions({
51
- indicator,
52
- dirty,
53
- fields,
54
- }: {
55
- indicator: string;
56
- dirty: string;
57
- fields: string[];
58
- }) {
59
- const { t } = useLingui();
60
- const resetExpr = [
61
- ...fields.map((f) => `$${f} = $_orig_${f}`),
62
- `$${dirty} = false`,
63
- ].join("; ");
64
-
65
- return (
66
- <div class="flex gap-2 mt-4">
67
- <button
68
- type="submit"
69
- class="btn"
70
- disabled
71
- data-attr:disabled={`$${indicator} || !$${dirty}`}
72
- >
73
- <Spinner show={`$${indicator}`} />
74
- {t({
75
- message: "Save",
76
- comment: "@context: Button to save settings",
77
- })}
78
- </button>
79
- <button
80
- type="button"
81
- class="btn-outline"
82
- disabled
83
- data-attr:disabled={`$${indicator} || !$${dirty}`}
84
- data-on:click={resetExpr}
85
- >
86
- {t({
87
- message: "Cancel",
88
- comment:
89
- "@context: Button to cancel unsaved changes and revert to original values",
90
- })}
91
- </button>
92
- </div>
93
- );
94
- }
95
-
96
17
  export function GeneralContent({
97
18
  siteName,
98
19
  siteDescription,
@@ -122,28 +43,129 @@ export function GeneralContent({
122
43
  }) {
123
44
  const { t } = useLingui();
124
45
 
125
- const generalSignals = buildSignals(
126
- {
127
- siteName,
128
- siteDescription,
129
- siteLanguage,
130
- homeDefaultView,
131
- timeZone,
132
- },
133
- "_generalDirty",
134
- );
135
-
136
- const footerSignals = buildSignals({ siteFooter }, "_footerDirty");
137
-
138
- const seoSignals = buildSignals(
139
- { noindex: noindex ? "" : "true" },
140
- "_seoDirty",
141
- );
142
-
143
- const avatarSignals = buildSignals(
144
- { showHeaderAvatar: showHeaderAvatar ? "true" : "" },
145
- "_avatarDisplayDirty",
146
- );
46
+ const labels = JSON.stringify({
47
+ blogAvatar: t({
48
+ message: "Blog Avatar",
49
+ comment: "@context: Settings section heading for avatar",
50
+ }),
51
+ uploadAvatar: t({
52
+ message: "Upload Avatar",
53
+ comment: "@context: Button to upload avatar image",
54
+ }),
55
+ remove: t({
56
+ message: "Remove",
57
+ comment: "@context: Button to remove the blog avatar",
58
+ }),
59
+ avatarHelp: t({
60
+ message:
61
+ "This is used for your favicon and apple-touch-icon. For best results, upload a square image at least 180x180 pixels.",
62
+ comment: "@context: Help text for avatar upload",
63
+ }),
64
+ displayInHeader: t({
65
+ message: "Display avatar in my site header",
66
+ comment: "@context: Checkbox to show avatar in the site header",
67
+ }),
68
+ processing: t({
69
+ message: "Processing...",
70
+ comment:
71
+ "@context: Avatar upload button text while generating favicon variants",
72
+ }),
73
+ uploading: t({
74
+ message: "Uploading...",
75
+ comment: "@context: Avatar upload button text while uploading",
76
+ }),
77
+ uploadError: t({
78
+ message: "Upload failed. Please try again.",
79
+ comment: "@context: Error message when avatar upload fails",
80
+ }),
81
+ general: t({
82
+ message: "General",
83
+ comment: "@context: Settings section heading",
84
+ }),
85
+ siteName: t({
86
+ message: "Site Name",
87
+ comment: "@context: Settings form field",
88
+ }),
89
+ aboutBlog: t({
90
+ message: "About this blog",
91
+ comment: "@context: Settings form field for site description",
92
+ }),
93
+ aboutBlogHelp: t({
94
+ message:
95
+ "Displayed above your blog posts on the home page. Also used as the meta description. Markdown supported.",
96
+ comment: "@context: Help text for site description field",
97
+ }),
98
+ language: t({
99
+ message: "Language",
100
+ comment: "@context: Settings form field",
101
+ }),
102
+ defaultHomepageView: t({
103
+ message: "Default Homepage View",
104
+ comment: "@context: Settings form field",
105
+ }),
106
+ latest: t({
107
+ message: "Latest",
108
+ comment: "@context: Homepage view option - show latest posts",
109
+ }),
110
+ featured: t({
111
+ message: "Featured",
112
+ comment: "@context: Homepage view option - show featured posts",
113
+ }),
114
+ timeZone: t({
115
+ message: "Time Zone",
116
+ comment: "@context: Settings form field",
117
+ }),
118
+ siteFooter: t({
119
+ message: "Site Footer",
120
+ comment: "@context: Settings section heading for site footer",
121
+ }),
122
+ footerHelp: t({
123
+ message:
124
+ "Displayed at the bottom of all posts and pages. Markdown supported.",
125
+ comment: "@context: Help text for site footer field",
126
+ }),
127
+ markdownSupported: t({
128
+ message: "Markdown supported",
129
+ comment: "@context: Placeholder hint for markdown-enabled textareas",
130
+ }),
131
+ seo: t({
132
+ message: "SEO",
133
+ comment: "@context: Settings section heading for SEO",
134
+ }),
135
+ allowIndexing: t({
136
+ message: "It's OK for search engines to index my site",
137
+ comment: "@context: Checkbox for allowing search engine indexing",
138
+ }),
139
+ save: t({
140
+ message: "Save",
141
+ comment: "@context: Button to save settings",
142
+ }),
143
+ cancel: t({
144
+ message: "Cancel",
145
+ comment:
146
+ "@context: Button to cancel unsaved changes and revert to original values",
147
+ }),
148
+ }).replace(/</g, "\\u003c");
149
+
150
+ const timezonesJson = JSON.stringify(
151
+ timezones.map((tz) => ({ value: tz.value, label: tz.label })),
152
+ ).replace(/</g, "\\u003c");
153
+
154
+ const languagesJson = JSON.stringify([
155
+ { value: "en", label: "English" },
156
+ { value: "zh-Hans", label: "\u7B80\u4F53\u4E2D\u6587" },
157
+ { value: "zh-Hant", label: "\u7E41\u9AD4\u4E2D\u6587" },
158
+ ]);
159
+
160
+ const initialData = JSON.stringify({
161
+ siteName,
162
+ siteDescription,
163
+ siteLanguage,
164
+ homeDefaultView,
165
+ timeZone,
166
+ siteFooter,
167
+ noindex,
168
+ }).replace(/</g, "\\u003c");
147
169
 
148
170
  return (
149
171
  <>
@@ -152,382 +174,41 @@ export function GeneralContent({
152
174
  </h1>
153
175
  <SettingsNav currentTab="general" />
154
176
 
155
- <div class="flex flex-col gap-6 max-w-lg">
156
- {/* Blog Avatar */}
157
- <div class="card">
158
- <header>
159
- <h2>
160
- {t({
161
- message: "Blog Avatar",
162
- comment: "@context: Settings section heading for avatar",
163
- })}
164
- </h2>
165
- </header>
166
- <section class="flex flex-col gap-4">
167
- <div class="flex items-center gap-4">
168
- {siteAvatarUrl ? (
169
- <img
170
- src={siteAvatarUrl}
171
- alt=""
172
- class="rounded-full object-cover"
173
- style="width:64px;height:64px"
174
- />
175
- ) : (
176
- <div
177
- class="rounded-full bg-muted flex items-center justify-center text-muted-foreground"
178
- style="width:64px;height:64px"
179
- >
180
- <svg
181
- xmlns="http://www.w3.org/2000/svg"
182
- width="24"
183
- height="24"
184
- viewBox="0 0 24 24"
185
- fill="none"
186
- stroke="currentColor"
187
- stroke-width="2"
188
- stroke-linecap="round"
189
- stroke-linejoin="round"
190
- >
191
- <rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
192
- <circle cx="9" cy="9" r="2" />
193
- <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
194
- </svg>
195
- </div>
196
- )}
197
- <div class="flex flex-col gap-2">
198
- <form
199
- action="/dash/settings/avatar"
200
- method="post"
201
- enctype="multipart/form-data"
202
- class="inline"
203
- >
204
- <label class="btn text-sm cursor-pointer">
205
- {t({
206
- message: "Upload Avatar",
207
- comment: "@context: Button to upload avatar image",
208
- })}
209
- <input
210
- type="file"
211
- name="file"
212
- accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml"
213
- class="hidden"
214
- data-avatar-upload
215
- data-text-processing={t({
216
- message: "Processing...",
217
- comment:
218
- "@context: Avatar upload button text while generating favicon variants",
219
- })}
220
- data-text-uploading={t({
221
- message: "Uploading...",
222
- comment:
223
- "@context: Avatar upload button text while uploading",
224
- })}
225
- data-text-error={t({
226
- message: "Upload failed. Please try again.",
227
- comment:
228
- "@context: Error message when avatar upload fails",
229
- })}
230
- />
231
- </label>
232
- </form>
233
- {siteAvatarUrl && (
234
- <form
235
- data-on:submit__prevent="@post('/dash/settings/avatar/remove')"
236
- data-indicator="_removeAvatarLoading"
237
- >
238
- <button
239
- type="submit"
240
- class="btn-outline text-sm"
241
- data-attr:disabled="$_removeAvatarLoading"
242
- >
243
- {t({
244
- message: "Remove",
245
- comment: "@context: Button to remove the blog avatar",
246
- })}
247
- </button>
248
- </form>
249
- )}
250
- </div>
251
- </div>
252
- <p class="text-sm text-muted-foreground">
253
- {t({
254
- message:
255
- "This is used for your favicon and apple-touch-icon. For best results, upload a square image at least 180x180 pixels.",
256
- comment: "@context: Help text for avatar upload",
257
- })}
258
- </p>
259
- <form
260
- data-signals={avatarSignals}
261
- data-on:submit__prevent="@post('/dash/settings/avatar/display')"
262
- data-indicator="_avatarDisplayLoading"
263
- >
264
- <label class="flex items-center gap-2 cursor-pointer">
265
- <input
266
- type="checkbox"
267
- class="checkbox"
268
- data-bind="showHeaderAvatar"
269
- data-on:change="$_avatarDisplayDirty = true"
270
- checked={showHeaderAvatar || undefined}
271
- value="true"
272
- />
273
- <span>
274
- {t({
275
- message: "Display avatar in my site header",
276
- comment:
277
- "@context: Checkbox to show avatar in the site header",
278
- })}
279
- </span>
280
- </label>
281
- <FormActions
282
- indicator="_avatarDisplayLoading"
283
- dirty="_avatarDisplayDirty"
284
- fields={["showHeaderAvatar"]}
285
- />
286
- </form>
287
- </section>
288
- </div>
289
-
290
- {/* General settings */}
291
- <form
292
- data-signals={generalSignals}
293
- data-on:submit__prevent="@post('/dash/settings')"
294
- data-indicator="_generalLoading"
177
+ <div class="flex flex-col max-w-lg">
178
+ <jant-settings-avatar
179
+ avatar-url={siteAvatarUrl}
180
+ show-in-header={showHeaderAvatar || undefined}
181
+ labels={labels}
295
182
  >
296
- <div class="card">
297
- <header>
298
- <h2>
299
- {t({
300
- message: "General",
301
- comment: "@context: Settings section heading",
302
- })}
303
- </h2>
304
- </header>
305
- <section class="flex flex-col gap-4">
306
- <div class="field">
307
- <label class="label">
308
- {t({
309
- message: "Site Name",
310
- comment: "@context: Settings form field",
311
- })}
312
- </label>
313
- <input
314
- type="text"
315
- data-bind="siteName"
316
- data-on:input="$_generalDirty = true"
317
- class="input"
318
- placeholder={siteNameFallback}
319
- />
320
- </div>
321
-
322
- <div class="field">
323
- <label class="label">
324
- {t({
325
- message: "About this blog",
326
- comment:
327
- "@context: Settings form field for site description",
328
- })}
329
- </label>
330
- <textarea
331
- data-bind="siteDescription"
332
- data-on:input="$_generalDirty = true"
333
- class="textarea"
334
- rows={3}
335
- placeholder={siteDescriptionFallback}
336
- >
337
- {siteDescription}
338
- </textarea>
339
- <p class="text-sm text-muted-foreground mt-1">
340
- {t({
341
- message:
342
- "This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.",
343
- comment: "@context: Help text for site description field",
344
- })}
345
- </p>
346
- </div>
347
-
348
- <div class="field">
349
- <label class="label">
350
- {t({
351
- message: "Language",
352
- comment: "@context: Settings form field",
353
- })}
354
- </label>
355
- <select
356
- data-bind="siteLanguage"
357
- data-on:change="$_generalDirty = true"
358
- class="select"
359
- >
360
- <option value="en" selected={siteLanguage === "en"}>
361
- English
362
- </option>
363
- <option value="zh-Hans" selected={siteLanguage === "zh-Hans"}>
364
- 简体中文
365
- </option>
366
- <option value="zh-Hant" selected={siteLanguage === "zh-Hant"}>
367
- 繁體中文
368
- </option>
369
- </select>
370
- </div>
371
-
372
- <div class="field">
373
- <label class="label">
374
- {t({
375
- message: "Default Homepage View",
376
- comment: "@context: Settings form field",
377
- })}
378
- </label>
379
- <select
380
- data-bind="homeDefaultView"
381
- data-on:change="$_generalDirty = true"
382
- class="select"
383
- >
384
- <option
385
- value="latest"
386
- selected={homeDefaultView === "latest"}
387
- >
388
- {t({
389
- message: "Latest",
390
- comment:
391
- "@context: Homepage view option - show latest posts",
392
- })}
393
- </option>
394
- <option
395
- value="featured"
396
- selected={homeDefaultView === "featured"}
397
- >
398
- {t({
399
- message: "Featured",
400
- comment:
401
- "@context: Homepage view option - show featured posts",
402
- })}
403
- </option>
404
- </select>
405
- </div>
406
-
407
- <div class="field">
408
- <label class="label">
409
- {t({
410
- message: "Time Zone",
411
- comment: "@context: Settings form field",
412
- })}
413
- </label>
414
- <select
415
- data-bind="timeZone"
416
- data-on:change="$_generalDirty = true"
417
- class="select"
418
- >
419
- {timezones.map((tz) => (
420
- <option
421
- key={tz.value}
422
- value={tz.value}
423
- selected={timeZone === tz.value}
424
- >
425
- {tz.label}
426
- </option>
427
- ))}
428
- </select>
429
- </div>
430
- <FormActions
431
- indicator="_generalLoading"
432
- dirty="_generalDirty"
433
- fields={[
434
- "siteName",
435
- "siteDescription",
436
- "siteLanguage",
437
- "homeDefaultView",
438
- "timeZone",
439
- ]}
440
- />
441
- </section>
183
+ {/* SSR fallback skeleton */}
184
+ <div>
185
+ <h2 class="skel-label" />
186
+ <div class="skel-section-sm" />
442
187
  </div>
443
- </form>
188
+ </jant-settings-avatar>
444
189
 
445
- {/* Site Footer */}
446
- <form
447
- data-signals={footerSignals}
448
- data-on:submit__prevent="@post('/dash/settings/footer')"
449
- data-indicator="_footerLoading"
450
- >
451
- <div class="card">
452
- <header>
453
- <h2>
454
- {t({
455
- message: "Site Footer",
456
- comment: "@context: Settings section heading for site footer",
457
- })}
458
- </h2>
459
- </header>
460
- <section class="flex flex-col gap-4">
461
- <textarea
462
- data-bind="siteFooter"
463
- data-on:input="$_footerDirty = true"
464
- class="textarea font-mono text-sm"
465
- rows={4}
466
- placeholder={t({
467
- message: "Markdown supported",
468
- comment: "@context: Placeholder for footer textarea",
469
- })}
470
- >
471
- {siteFooter}
472
- </textarea>
473
- <p class="text-sm text-muted-foreground">
474
- {t({
475
- message:
476
- "This is displayed at the bottom of all of your posts and pages. Markdown is supported.",
477
- comment: "@context: Help text for site footer field",
478
- })}
479
- </p>
480
- <FormActions
481
- indicator="_footerLoading"
482
- dirty="_footerDirty"
483
- fields={["siteFooter"]}
484
- />
485
- </section>
486
- </div>
487
- </form>
190
+ <hr class="my-8" />
488
191
 
489
- {/* SEO */}
490
- <form
491
- data-signals={seoSignals}
492
- data-on:submit__prevent="@post('/dash/settings/seo')"
493
- data-indicator="_seoLoading"
192
+ <jant-settings-general
193
+ labels={labels}
194
+ timezones={timezonesJson}
195
+ languages={languagesJson}
196
+ sitename-fallback={siteNameFallback}
197
+ sitedescription-fallback={siteDescriptionFallback}
494
198
  >
495
- <div class="card">
496
- <header>
497
- <h2>
498
- {t({
499
- message: "SEO",
500
- comment: "@context: Settings section heading for SEO",
501
- })}
502
- </h2>
503
- </header>
504
- <section>
505
- <label class="flex items-center gap-2 cursor-pointer">
506
- <input
507
- type="checkbox"
508
- class="checkbox"
509
- data-bind="noindex"
510
- data-on:change="$_seoDirty = true"
511
- checked={!noindex || undefined}
512
- value="true"
513
- />
514
- <span>
515
- {t({
516
- message: "It's OK for search engines to index my site",
517
- comment:
518
- "@context: Checkbox for allowing search engine indexing",
519
- })}
520
- </span>
521
- </label>
522
- <FormActions
523
- indicator="_seoLoading"
524
- dirty="_seoDirty"
525
- fields={["noindex"]}
526
- />
527
- </section>
199
+ {/* SSR fallback skeleton */}
200
+ <div>
201
+ <h2 class="skel-label" />
202
+ <div class="skel-section-lg" />
528
203
  </div>
529
- </form>
204
+ </jant-settings-general>
530
205
  </div>
206
+
207
+ <script
208
+ type="application/json"
209
+ id="settings-initial-data"
210
+ dangerouslySetInnerHTML={{ __html: initialData }}
211
+ />
531
212
  </>
532
213
  );
533
214
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { useLingui } from "@lingui/react/macro";
6
6
 
7
- export type SettingsTab = "general" | "appearance" | "account";
7
+ export type SettingsTab = "general" | "redirects" | "account";
8
8
 
9
9
  export function SettingsNav({ currentTab }: { currentTab: SettingsTab }) {
10
10
  const { t } = useLingui();
@@ -19,12 +19,12 @@ export function SettingsNav({ currentTab }: { currentTab: SettingsTab }) {
19
19
  href: "/dash/settings",
20
20
  },
21
21
  {
22
- id: "appearance",
22
+ id: "redirects",
23
23
  label: t({
24
- message: "Appearance",
24
+ message: "Redirects",
25
25
  comment: "@context: Settings sub-navigation tab",
26
26
  }),
27
- href: "/dash/settings/appearance",
27
+ href: "/dash/settings/redirects",
28
28
  },
29
29
  {
30
30
  id: "account",