@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.
Files changed (146) hide show
  1. package/bin/jant.js +3 -1
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +112 -85
  4. package/dist/auth.d.ts +1 -0
  5. package/dist/auth.d.ts.map +1 -1
  6. package/dist/auth.js +2 -1
  7. package/dist/client.js +1 -1
  8. package/dist/db/schema.d.ts.map +1 -1
  9. package/dist/i18n/context.d.ts.map +1 -1
  10. package/dist/i18n/context.js +0 -3
  11. package/dist/i18n/detect.d.ts +0 -11
  12. package/dist/i18n/detect.d.ts.map +1 -1
  13. package/dist/i18n/detect.js +1 -52
  14. package/dist/i18n/i18n.d.ts +4 -14
  15. package/dist/i18n/i18n.d.ts.map +1 -1
  16. package/dist/i18n/i18n.js +19 -25
  17. package/dist/i18n/index.d.ts +1 -1
  18. package/dist/i18n/index.d.ts.map +1 -1
  19. package/dist/i18n/index.js +1 -1
  20. package/dist/i18n/middleware.d.ts +2 -5
  21. package/dist/i18n/middleware.d.ts.map +1 -1
  22. package/dist/i18n/middleware.js +12 -23
  23. package/dist/lib/constants.d.ts.map +1 -1
  24. package/dist/lib/image.d.ts.map +1 -1
  25. package/dist/lib/schemas.d.ts.map +1 -1
  26. package/dist/lib/sse.d.ts +45 -17
  27. package/dist/lib/sse.d.ts.map +1 -1
  28. package/dist/lib/sse.js +77 -37
  29. package/dist/middleware/auth.d.ts.map +1 -1
  30. package/dist/routes/api/posts.js +0 -1
  31. package/dist/routes/api/upload.js +3 -1
  32. package/dist/routes/dash/collections.d.ts.map +1 -1
  33. package/dist/routes/dash/collections.js +134 -142
  34. package/dist/routes/dash/index.js +25 -26
  35. package/dist/routes/dash/media.d.ts.map +1 -1
  36. package/dist/routes/dash/media.js +60 -56
  37. package/dist/routes/dash/pages.js +64 -66
  38. package/dist/routes/dash/posts.d.ts.map +1 -1
  39. package/dist/routes/dash/posts.js +50 -59
  40. package/dist/routes/dash/redirects.d.ts.map +1 -1
  41. package/dist/routes/dash/redirects.js +63 -60
  42. package/dist/routes/dash/settings.d.ts.map +1 -1
  43. package/dist/routes/dash/settings.js +249 -93
  44. package/dist/routes/feed/rss.js +6 -4
  45. package/dist/routes/pages/archive.js +60 -62
  46. package/dist/routes/pages/collection.js +8 -8
  47. package/dist/routes/pages/home.js +14 -14
  48. package/dist/routes/pages/page.js +7 -6
  49. package/dist/routes/pages/post.js +8 -8
  50. package/dist/routes/pages/search.js +25 -27
  51. package/dist/services/collection.d.ts.map +1 -1
  52. package/dist/services/index.d.ts.map +1 -1
  53. package/dist/services/media.d.ts.map +1 -1
  54. package/dist/services/post.d.ts.map +1 -1
  55. package/dist/services/redirect.d.ts.map +1 -1
  56. package/dist/services/settings.d.ts.map +1 -1
  57. package/dist/theme/components/ActionButtons.d.ts +1 -1
  58. package/dist/theme/components/ActionButtons.d.ts.map +1 -1
  59. package/dist/theme/components/ActionButtons.js +17 -21
  60. package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
  61. package/dist/theme/components/DangerZone.d.ts.map +1 -1
  62. package/dist/theme/components/DangerZone.js +12 -15
  63. package/dist/theme/components/EmptyState.d.ts.map +1 -1
  64. package/dist/theme/components/PageForm.d.ts.map +1 -1
  65. package/dist/theme/components/PageForm.js +58 -56
  66. package/dist/theme/components/Pagination.d.ts.map +1 -1
  67. package/dist/theme/components/Pagination.js +22 -25
  68. package/dist/theme/components/PostForm.d.ts +0 -1
  69. package/dist/theme/components/PostForm.d.ts.map +1 -1
  70. package/dist/theme/components/PostForm.js +85 -77
  71. package/dist/theme/components/PostList.d.ts.map +1 -1
  72. package/dist/theme/components/PostList.js +17 -17
  73. package/dist/theme/components/ThreadView.d.ts.map +1 -1
  74. package/dist/theme/components/ThreadView.js +15 -18
  75. package/dist/theme/components/TypeBadge.d.ts.map +1 -1
  76. package/dist/theme/components/TypeBadge.js +20 -20
  77. package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
  78. package/dist/theme/components/VisibilityBadge.js +14 -14
  79. package/dist/theme/components/index.d.ts +1 -1
  80. package/dist/theme/components/index.d.ts.map +1 -1
  81. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  82. package/dist/theme/layouts/BaseLayout.js +4 -2
  83. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  84. package/dist/theme/layouts/DashLayout.js +29 -29
  85. package/dist/types/lingui-react-macro.d.js +9 -0
  86. package/dist/types.d.ts +2 -0
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/vendor/datastar.js +1606 -0
  89. package/package.json +5 -2
  90. package/src/app.tsx +175 -56
  91. package/src/auth.ts +5 -1
  92. package/src/client.ts +1 -1
  93. package/src/db/schema.ts +22 -7
  94. package/src/i18n/EXAMPLES.md +34 -14
  95. package/src/i18n/README.md +19 -9
  96. package/src/i18n/context.tsx +1 -4
  97. package/src/i18n/detect.ts +1 -67
  98. package/src/i18n/i18n.ts +15 -19
  99. package/src/i18n/index.ts +0 -3
  100. package/src/i18n/middleware.ts +12 -24
  101. package/src/lib/constants.ts +2 -1
  102. package/src/lib/image-processor.ts +23 -7
  103. package/src/lib/image.ts +6 -2
  104. package/src/lib/schemas.ts +6 -2
  105. package/src/lib/sse.ts +138 -50
  106. package/src/middleware/auth.ts +6 -2
  107. package/src/routes/api/posts.ts +14 -5
  108. package/src/routes/api/upload.ts +25 -7
  109. package/src/routes/dash/collections.tsx +162 -70
  110. package/src/routes/dash/index.tsx +22 -7
  111. package/src/routes/dash/media.tsx +59 -16
  112. package/src/routes/dash/pages.tsx +102 -44
  113. package/src/routes/dash/posts.tsx +87 -54
  114. package/src/routes/dash/redirects.tsx +74 -26
  115. package/src/routes/dash/settings.tsx +250 -57
  116. package/src/routes/feed/rss.ts +6 -4
  117. package/src/routes/pages/archive.tsx +71 -21
  118. package/src/routes/pages/collection.tsx +21 -6
  119. package/src/routes/pages/home.tsx +30 -9
  120. package/src/routes/pages/page.tsx +14 -5
  121. package/src/routes/pages/post.tsx +21 -7
  122. package/src/routes/pages/search.tsx +42 -11
  123. package/src/services/collection.ts +34 -9
  124. package/src/services/index.ts +4 -1
  125. package/src/services/media.ts +15 -3
  126. package/src/services/post.ts +39 -10
  127. package/src/services/redirect.ts +4 -1
  128. package/src/services/settings.ts +14 -3
  129. package/src/theme/components/ActionButtons.tsx +26 -14
  130. package/src/theme/components/CrudPageHeader.tsx +6 -1
  131. package/src/theme/components/DangerZone.tsx +19 -13
  132. package/src/theme/components/EmptyState.tsx +6 -1
  133. package/src/theme/components/PageForm.tsx +71 -24
  134. package/src/theme/components/Pagination.tsx +26 -8
  135. package/src/theme/components/PostForm.tsx +72 -25
  136. package/src/theme/components/PostList.tsx +16 -5
  137. package/src/theme/components/ThreadView.tsx +25 -7
  138. package/src/theme/components/TypeBadge.tsx +13 -4
  139. package/src/theme/components/VisibilityBadge.tsx +17 -5
  140. package/src/theme/components/index.ts +4 -1
  141. package/src/theme/layouts/BaseLayout.tsx +5 -2
  142. package/src/theme/layouts/DashLayout.tsx +41 -12
  143. package/src/types/lingui-react-macro.d.ts +34 -0
  144. package/src/types.ts +16 -2
  145. package/src/vendor/datastar.js +9 -0
  146. package/src/vendor/datastar.js.map +7 -0
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import { useLingui } from "../../i18n/index.js";
6
+ import { useLingui } from "@lingui/react/macro";
7
7
  import type { Bindings, Post } from "../../types.js";
8
8
  import type { AppVariables } from "../../app.js";
9
9
  import { BaseLayout } from "../../theme/layouts/index.js";
@@ -22,8 +22,14 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
22
22
  <header class="mb-8 flex items-center justify-between">
23
23
  <h1 class="text-2xl font-semibold">{siteName}</h1>
24
24
  <nav class="flex items-center gap-4 text-sm">
25
- <a href="/archive" class="text-muted-foreground hover:text-foreground">
26
- {t({ message: "Archive", comment: "@context: Navigation link to archive page" })}
25
+ <a
26
+ href="/archive"
27
+ class="text-muted-foreground hover:text-foreground"
28
+ >
29
+ {t({
30
+ message: "Archive",
31
+ comment: "@context: Navigation link to archive page",
32
+ })}
27
33
  </a>
28
34
  <a href="/feed" class="text-muted-foreground hover:text-foreground">
29
35
  RSS
@@ -34,14 +40,20 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
34
40
  <main class="flex flex-col gap-6">
35
41
  {posts.length === 0 ? (
36
42
  <p class="text-muted-foreground">
37
- {t({ message: "No posts yet.", comment: "@context: Empty state message on home page" })}
43
+ {t({
44
+ message: "No posts yet.",
45
+ comment: "@context: Empty state message on home page",
46
+ })}
38
47
  </p>
39
48
  ) : (
40
49
  posts.map((post) => (
41
50
  <article key={post.id} class="h-entry">
42
51
  {post.title && (
43
52
  <h2 class="p-name text-lg font-medium mb-2">
44
- <a href={`/p/${sqid.encode(post.id)}`} class="u-url hover:underline">
53
+ <a
54
+ href={`/p/${sqid.encode(post.id)}`}
55
+ class="u-url hover:underline"
56
+ >
45
57
  {post.title}
46
58
  </a>
47
59
  </h2>
@@ -51,12 +63,18 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
51
63
  dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
52
64
  />
53
65
  <footer class="mt-2 text-sm text-muted-foreground">
54
- <time class="dt-published" datetime={time.toISOString(post.publishedAt)}>
66
+ <time
67
+ class="dt-published"
68
+ datetime={time.toISOString(post.publishedAt)}
69
+ >
55
70
  {time.formatDate(post.publishedAt)}
56
71
  </time>
57
72
  {post.visibility === "featured" && (
58
73
  <span class="ml-2 text-xs">
59
- {t({ message: "Featured", comment: "@context: Post visibility badge" })}
74
+ {t({
75
+ message: "Featured",
76
+ comment: "@context: Post visibility badge",
77
+ })}
60
78
  </span>
61
79
  )}
62
80
  </footer>
@@ -67,7 +85,10 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
67
85
 
68
86
  {posts.length >= 20 && (
69
87
  <nav class="mt-8 text-center">
70
- <a href="/archive" class="text-sm text-muted-foreground hover:text-foreground">
88
+ <a
89
+ href="/archive"
90
+ class="text-sm text-muted-foreground hover:text-foreground"
91
+ >
71
92
  {t({
72
93
  message: "View all posts →",
73
94
  comment: "@context: Link to view all posts on archive page",
@@ -95,6 +116,6 @@ homeRoutes.get("/", async (c) => {
95
116
  return c.html(
96
117
  <BaseLayout title={siteName} c={c}>
97
118
  <HomeContent siteName={siteName} posts={posts} />
98
- </BaseLayout>
119
+ </BaseLayout>,
99
120
  );
100
121
  });
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
- import { useLingui } from "../../i18n/index.js";
8
+ import { useLingui } from "@lingui/react/macro";
9
9
  import type { Bindings, Post } from "../../types.js";
10
10
  import type { AppVariables } from "../../app.js";
11
11
  import { BaseLayout } from "../../theme/layouts/index.js";
@@ -20,14 +20,23 @@ function PageContent({ page }: { page: Post }) {
20
20
  return (
21
21
  <div class="container py-8 max-w-2xl">
22
22
  <article class="h-entry">
23
- {page.title && <h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>}
23
+ {page.title && (
24
+ <h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
25
+ )}
24
26
 
25
- <div class="e-content prose" dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }} />
27
+ <div
28
+ class="e-content prose"
29
+ dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
30
+ />
26
31
  </article>
27
32
 
28
33
  <nav class="mt-8 pt-6 border-t">
29
34
  <a href="/" class="text-sm hover:underline">
30
- {t({ message: "Back to home", comment: "@context: Navigation link back to home page" })}
35
+ ←{" "}
36
+ {t({
37
+ message: "Back to home",
38
+ comment: "@context: Navigation link back to home page",
39
+ })}
31
40
  </a>
32
41
  </nav>
33
42
  </div>
@@ -60,6 +69,6 @@ pageRoutes.get("/:path", async (c) => {
60
69
  c={c}
61
70
  >
62
71
  <PageContent page={page} />
63
- </BaseLayout>
72
+ </BaseLayout>,
64
73
  );
65
74
  });
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import { useLingui } from "../../i18n/index.js";
6
+ import { useLingui } from "@lingui/react/macro";
7
7
  import type { Bindings, Post } from "../../types.js";
8
8
  import type { AppVariables } from "../../app.js";
9
9
  import { BaseLayout } from "../../theme/layouts/index.js";
@@ -20,23 +20,37 @@ function PostContent({ post }: { post: Post }) {
20
20
  return (
21
21
  <div class="container py-8">
22
22
  <article class="h-entry">
23
- {post.title && <h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>}
23
+ {post.title && (
24
+ <h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
25
+ )}
24
26
 
25
- <div class="e-content prose" dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }} />
27
+ <div
28
+ class="e-content prose"
29
+ dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
30
+ />
26
31
 
27
32
  <footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
28
- <time class="dt-published" datetime={time.toISOString(post.publishedAt)}>
33
+ <time
34
+ class="dt-published"
35
+ datetime={time.toISOString(post.publishedAt)}
36
+ >
29
37
  {time.formatDate(post.publishedAt)}
30
38
  </time>
31
39
  <a href={`/p/${sqid.encode(post.id)}`} class="u-url ml-4">
32
- {t({ message: "Permalink", comment: "@context: Link to permanent URL of post" })}
40
+ {t({
41
+ message: "Permalink",
42
+ comment: "@context: Link to permanent URL of post",
43
+ })}
33
44
  </a>
34
45
  </footer>
35
46
  </article>
36
47
 
37
48
  <nav class="mt-8">
38
49
  <a href="/" class="text-sm hover:underline">
39
- {t({ message: "← Back to home", comment: "@context: Navigation link" })}
50
+ {t({
51
+ message: "← Back to home",
52
+ comment: "@context: Navigation link",
53
+ })}
40
54
  </a>
41
55
  </nav>
42
56
  </div>
@@ -73,6 +87,6 @@ postRoutes.get("/:id", async (c) => {
73
87
  return c.html(
74
88
  <BaseLayout title={title} description={post.content?.slice(0, 160)} c={c}>
75
89
  <PostContent post={post} />
76
- </BaseLayout>
90
+ </BaseLayout>,
77
91
  );
78
92
  });
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import { useLingui } from "../../i18n/index.js";
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 type { SearchResult } from "../../services/search.js";
@@ -32,7 +32,10 @@ function SearchContent({
32
32
  page: number;
33
33
  }) {
34
34
  const { t } = useLingui();
35
- const searchTitle = t({ message: "Search", comment: "@context: Search page title" });
35
+ const searchTitle = t({
36
+ message: "Search",
37
+ comment: "@context: Search page title",
38
+ });
36
39
 
37
40
  return (
38
41
  <div class="container py-8 max-w-2xl">
@@ -53,20 +56,30 @@ function SearchContent({
53
56
  autofocus
54
57
  />
55
58
  <button type="submit" class="btn">
56
- {t({ message: "Search", comment: "@context: Search submit button" })}
59
+ {t({
60
+ message: "Search",
61
+ comment: "@context: Search submit button",
62
+ })}
57
63
  </button>
58
64
  </div>
59
65
  </form>
60
66
 
61
67
  {/* Error */}
62
- {error && <div class="p-4 rounded-lg bg-destructive/10 text-destructive mb-6">{error}</div>}
68
+ {error && (
69
+ <div class="p-4 rounded-lg bg-destructive/10 text-destructive mb-6">
70
+ {error}
71
+ </div>
72
+ )}
63
73
 
64
74
  {/* Results */}
65
75
  {query && !error && (
66
76
  <div>
67
77
  <p class="text-sm text-muted-foreground mb-4">
68
78
  {results.length === 0
69
- ? t({ message: "No results found.", comment: "@context: Search empty results" })
79
+ ? t({
80
+ message: "No results found.",
81
+ comment: "@context: Search empty results",
82
+ })
70
83
  : results.length === 1
71
84
  ? t({
72
85
  message: "Found 1 result",
@@ -83,7 +96,10 @@ function SearchContent({
83
96
  <>
84
97
  <div class="flex flex-col gap-4">
85
98
  {results.map((result) => (
86
- <article key={result.post.id} class="p-4 rounded-lg border hover:border-primary">
99
+ <article
100
+ key={result.post.id}
101
+ class="p-4 rounded-lg border hover:border-primary"
102
+ >
87
103
  <a href={`/p/${sqid.encode(result.post.id)}`} class="block">
88
104
  <h2 class="font-medium hover:underline">
89
105
  {result.post.title ||
@@ -100,7 +116,9 @@ function SearchContent({
100
116
 
101
117
  <footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
102
118
  <span class="badge-outline">{result.post.type}</span>
103
- <time datetime={time.toISOString(result.post.publishedAt)}>
119
+ <time
120
+ datetime={time.toISOString(result.post.publishedAt)}
121
+ >
104
122
  {time.formatDate(result.post.publishedAt)}
105
123
  </time>
106
124
  </footer>
@@ -121,7 +139,11 @@ function SearchContent({
121
139
 
122
140
  <nav class="mt-8 pt-6 border-t">
123
141
  <a href="/" class="text-sm hover:underline">
124
- {t({ message: "Back to home", comment: "@context: Navigation link back to home page" })}
142
+ ←{" "}
143
+ {t({
144
+ message: "Back to home",
145
+ comment: "@context: Navigation link back to home page",
146
+ })}
125
147
  </a>
126
148
  </nav>
127
149
  </div>
@@ -161,8 +183,17 @@ searchRoutes.get("/", async (c) => {
161
183
  }
162
184
 
163
185
  return c.html(
164
- <BaseLayout title={query ? `Search: ${query} - ${siteName}` : `Search - ${siteName}`} c={c}>
165
- <SearchContent query={query} results={results} error={error} hasMore={hasMore} page={page} />
166
- </BaseLayout>
186
+ <BaseLayout
187
+ title={query ? `Search: ${query} - ${siteName}` : `Search - ${siteName}`}
188
+ c={c}
189
+ >
190
+ <SearchContent
191
+ query={query}
192
+ results={results}
193
+ error={error}
194
+ hasMore={hasMore}
195
+ page={page}
196
+ />
197
+ </BaseLayout>,
167
198
  );
168
199
  });
@@ -70,17 +70,28 @@ export function createCollectionService(db: Database): CollectionService {
70
70
 
71
71
  return {
72
72
  async getById(id) {
73
- const result = await db.select().from(collections).where(eq(collections.id, id)).limit(1);
73
+ const result = await db
74
+ .select()
75
+ .from(collections)
76
+ .where(eq(collections.id, id))
77
+ .limit(1);
74
78
  return result[0] ? toCollection(result[0]) : null;
75
79
  },
76
80
 
77
81
  async getByPath(path) {
78
- const result = await db.select().from(collections).where(eq(collections.path, path)).limit(1);
82
+ const result = await db
83
+ .select()
84
+ .from(collections)
85
+ .where(eq(collections.path, path))
86
+ .limit(1);
79
87
  return result[0] ? toCollection(result[0]) : null;
80
88
  },
81
89
 
82
90
  async list() {
83
- const rows = await db.select().from(collections).orderBy(desc(collections.createdAt));
91
+ const rows = await db
92
+ .select()
93
+ .from(collections)
94
+ .orderBy(desc(collections.createdAt));
84
95
  return rows.map(toCollection);
85
96
  },
86
97
 
@@ -107,11 +118,14 @@ export function createCollectionService(db: Database): CollectionService {
107
118
  if (!existing) return null;
108
119
 
109
120
  const timestamp = now();
110
- const updates: Partial<typeof collections.$inferInsert> = { updatedAt: timestamp };
121
+ const updates: Partial<typeof collections.$inferInsert> = {
122
+ updatedAt: timestamp,
123
+ };
111
124
 
112
125
  if (data.title !== undefined) updates.title = data.title;
113
126
  if (data.path !== undefined) updates.path = data.path;
114
- if (data.description !== undefined) updates.description = data.description;
127
+ if (data.description !== undefined)
128
+ updates.description = data.description;
115
129
 
116
130
  const result = await db
117
131
  .update(collections)
@@ -124,9 +138,14 @@ export function createCollectionService(db: Database): CollectionService {
124
138
 
125
139
  async delete(id) {
126
140
  // Delete all post-collection relationships first
127
- await db.delete(postCollections).where(eq(postCollections.collectionId, id));
141
+ await db
142
+ .delete(postCollections)
143
+ .where(eq(postCollections.collectionId, id));
128
144
 
129
- const result = await db.delete(collections).where(eq(collections.id, id)).returning();
145
+ const result = await db
146
+ .delete(collections)
147
+ .where(eq(collections.id, id))
148
+ .returning();
130
149
  return result.length > 0;
131
150
  },
132
151
 
@@ -148,7 +167,10 @@ export function createCollectionService(db: Database): CollectionService {
148
167
  await db
149
168
  .delete(postCollections)
150
169
  .where(
151
- and(eq(postCollections.collectionId, collectionId), eq(postCollections.postId, postId))
170
+ and(
171
+ eq(postCollections.collectionId, collectionId),
172
+ eq(postCollections.postId, postId),
173
+ ),
152
174
  );
153
175
  },
154
176
 
@@ -167,7 +189,10 @@ export function createCollectionService(db: Database): CollectionService {
167
189
  const rows = await db
168
190
  .select({ collection: collections })
169
191
  .from(postCollections)
170
- .innerJoin(collections, eq(postCollections.collectionId, collections.id))
192
+ .innerJoin(
193
+ collections,
194
+ eq(postCollections.collectionId, collections.id),
195
+ )
171
196
  .where(eq(postCollections.postId, postId));
172
197
 
173
198
  return rows.map((r) => toCollection(r.collection));
@@ -9,7 +9,10 @@ import { createSettingsService, type SettingsService } from "./settings.js";
9
9
  import { createPostService, type PostService } from "./post.js";
10
10
  import { createRedirectService, type RedirectService } from "./redirect.js";
11
11
  import { createMediaService, type MediaService } from "./media.js";
12
- import { createCollectionService, type CollectionService } from "./collection.js";
12
+ import {
13
+ createCollectionService,
14
+ type CollectionService,
15
+ } from "./collection.js";
13
16
  import { createSearchService, type SearchService } from "./search.js";
14
17
 
15
18
  export interface Services {
@@ -50,17 +50,29 @@ export function createMediaService(db: Database): MediaService {
50
50
 
51
51
  return {
52
52
  async getById(id) {
53
- const result = await db.select().from(media).where(eq(media.id, id)).limit(1);
53
+ const result = await db
54
+ .select()
55
+ .from(media)
56
+ .where(eq(media.id, id))
57
+ .limit(1);
54
58
  return result[0] ? toMedia(result[0]) : null;
55
59
  },
56
60
 
57
61
  async getByR2Key(r2Key) {
58
- const result = await db.select().from(media).where(eq(media.r2Key, r2Key)).limit(1);
62
+ const result = await db
63
+ .select()
64
+ .from(media)
65
+ .where(eq(media.r2Key, r2Key))
66
+ .limit(1);
59
67
  return result[0] ? toMedia(result[0]) : null;
60
68
  },
61
69
 
62
70
  async list(limit = 100) {
63
- const rows = await db.select().from(media).orderBy(desc(media.createdAt)).limit(limit);
71
+ const rows = await db
72
+ .select()
73
+ .from(media)
74
+ .orderBy(desc(media.createdAt))
75
+ .limit(limit);
64
76
  return rows.map(toMedia);
65
77
  },
66
78
 
@@ -10,7 +10,13 @@ import { posts } from "../db/schema.js";
10
10
  import { now } from "../lib/time.js";
11
11
  import { extractDomain } from "../lib/url.js";
12
12
  import { render as renderMarkdown } from "../lib/markdown.js";
13
- import type { PostType, Visibility, Post, CreatePost, UpdatePost } from "../types.js";
13
+ import type {
14
+ PostType,
15
+ Visibility,
16
+ Post,
17
+ CreatePost,
18
+ UpdatePost,
19
+ } from "../types.js";
14
20
 
15
21
  export interface PostFilters {
16
22
  type?: PostType;
@@ -133,7 +139,9 @@ export function createPostService(db: Database): PostService {
133
139
  const contentHtml = data.content ? renderMarkdown(data.content) : null;
134
140
 
135
141
  // Extract domain from source URL
136
- const sourceDomain = data.sourceUrl ? extractDomain(data.sourceUrl) : null;
142
+ const sourceDomain = data.sourceUrl
143
+ ? extractDomain(data.sourceUrl)
144
+ : null;
137
145
 
138
146
  // Handle thread relationship
139
147
  let threadId: number | null = null;
@@ -145,7 +153,9 @@ export function createPostService(db: Database): PostService {
145
153
  // thread_id = parent's thread_id or parent's id (if parent is root)
146
154
  threadId = parent.threadId ?? parent.id;
147
155
  // Inherit visibility from root
148
- const root = parent.threadId ? await this.getById(parent.threadId) : parent;
156
+ const root = parent.threadId
157
+ ? await this.getById(parent.threadId)
158
+ : parent;
149
159
  if (root) {
150
160
  visibility = root.visibility;
151
161
  }
@@ -181,25 +191,35 @@ export function createPostService(db: Database): PostService {
181
191
  if (!existing) return null;
182
192
 
183
193
  const timestamp = now();
184
- const updates: Partial<typeof posts.$inferInsert> = { updatedAt: timestamp };
194
+ const updates: Partial<typeof posts.$inferInsert> = {
195
+ updatedAt: timestamp,
196
+ };
185
197
 
186
198
  if (data.type !== undefined) updates.type = data.type;
187
199
  if (data.title !== undefined) updates.title = data.title;
188
200
  if (data.path !== undefined) updates.path = data.path;
189
- if (data.publishedAt !== undefined) updates.publishedAt = data.publishedAt;
201
+ if (data.publishedAt !== undefined)
202
+ updates.publishedAt = data.publishedAt;
190
203
  if (data.sourceUrl !== undefined) {
191
204
  updates.sourceUrl = data.sourceUrl;
192
- updates.sourceDomain = data.sourceUrl ? extractDomain(data.sourceUrl) : null;
205
+ updates.sourceDomain = data.sourceUrl
206
+ ? extractDomain(data.sourceUrl)
207
+ : null;
193
208
  }
194
209
  if (data.sourceName !== undefined) updates.sourceName = data.sourceName;
195
210
 
196
211
  if (data.content !== undefined) {
197
212
  updates.content = data.content;
198
- updates.contentHtml = data.content ? renderMarkdown(data.content) : null;
213
+ updates.contentHtml = data.content
214
+ ? renderMarkdown(data.content)
215
+ : null;
199
216
  }
200
217
 
201
218
  // Handle visibility change - cascade to thread if this is root
202
- if (data.visibility !== undefined && data.visibility !== existing.visibility) {
219
+ if (
220
+ data.visibility !== undefined &&
221
+ data.visibility !== existing.visibility
222
+ ) {
203
223
  updates.visibility = data.visibility;
204
224
  // If this is a root post, cascade visibility to all thread posts
205
225
  if (!existing.threadId) {
@@ -207,7 +227,11 @@ export function createPostService(db: Database): PostService {
207
227
  }
208
228
  }
209
229
 
210
- const result = await db.update(posts).set(updates).where(eq(posts.id, id)).returning();
230
+ const result = await db
231
+ .update(posts)
232
+ .set(updates)
233
+ .where(eq(posts.id, id))
234
+ .returning();
211
235
 
212
236
  return result[0] ? toPost(result[0]) : null;
213
237
  },
@@ -239,7 +263,12 @@ export function createPostService(db: Database): PostService {
239
263
  const rows = await db
240
264
  .select()
241
265
  .from(posts)
242
- .where(and(or(eq(posts.id, rootId), eq(posts.threadId, rootId)), isNull(posts.deletedAt)))
266
+ .where(
267
+ and(
268
+ or(eq(posts.id, rootId), eq(posts.threadId, rootId)),
269
+ isNull(posts.deletedAt),
270
+ ),
271
+ )
243
272
  .orderBy(posts.createdAt);
244
273
 
245
274
  return rows.map(toPost);
@@ -62,7 +62,10 @@ export function createRedirectService(db: Database): RedirectService {
62
62
  },
63
63
 
64
64
  async delete(id) {
65
- const result = await db.delete(redirects).where(eq(redirects.id, id)).returning();
65
+ const result = await db
66
+ .delete(redirects)
67
+ .where(eq(redirects.id, id))
68
+ .returning();
66
69
  return result.length > 0;
67
70
  },
68
71
 
@@ -8,7 +8,11 @@ import { eq } from "drizzle-orm";
8
8
  import type { Database } from "../db/index.js";
9
9
  import { settings } from "../db/schema.js";
10
10
  import { now } from "../lib/time.js";
11
- import { SETTINGS_KEYS, ONBOARDING_STATUS, type SettingsKey } from "../lib/constants.js";
11
+ import {
12
+ SETTINGS_KEYS,
13
+ ONBOARDING_STATUS,
14
+ type SettingsKey,
15
+ } from "../lib/constants.js";
12
16
 
13
17
  export interface SettingsService {
14
18
  get(key: SettingsKey): Promise<string | null>;
@@ -22,7 +26,11 @@ export interface SettingsService {
22
26
  export function createSettingsService(db: Database): SettingsService {
23
27
  return {
24
28
  async get(key) {
25
- const result = await db.select().from(settings).where(eq(settings.key, key)).limit(1);
29
+ const result = await db
30
+ .select()
31
+ .from(settings)
32
+ .where(eq(settings.key, key))
33
+ .limit(1);
26
34
  return result[0]?.value ?? null;
27
35
  },
28
36
 
@@ -70,7 +78,10 @@ export function createSettingsService(db: Database): SettingsService {
70
78
  },
71
79
 
72
80
  async completeOnboarding() {
73
- await this.set(SETTINGS_KEYS.ONBOARDING_STATUS, ONBOARDING_STATUS.COMPLETED);
81
+ await this.set(
82
+ SETTINGS_KEYS.ONBOARDING_STATUS,
83
+ ONBOARDING_STATUS.COMPLETED,
84
+ );
74
85
  },
75
86
  };
76
87
  }
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { FC } from "hono/jsx";
8
- import { useLingui } from "../../i18n/index.js";
8
+ import { useLingui } from "@lingui/react/macro";
9
9
 
10
10
  export interface ActionButtonsProps {
11
11
  /**
@@ -19,7 +19,7 @@ export interface ActionButtonsProps {
19
19
  viewHref?: string;
20
20
 
21
21
  /**
22
- * Delete button form action
22
+ * Delete action URL (sends POST via Datastar @post)
23
23
  */
24
24
  deleteAction?: string;
25
25
 
@@ -65,14 +65,28 @@ export const ActionButtons: FC<ActionButtonsProps> = ({
65
65
  const editClass = size === "sm" ? "btn-sm-outline" : "btn-outline";
66
66
  const viewClass = size === "sm" ? "btn-sm-ghost" : "btn-ghost";
67
67
  const deleteClass =
68
- size === "sm" ? "btn-sm-ghost text-destructive" : "btn-ghost text-destructive";
68
+ size === "sm"
69
+ ? "btn-sm-ghost text-destructive"
70
+ : "btn-ghost text-destructive";
69
71
 
70
- const defaultEditLabel = t({ message: "Edit", comment: "@context: Button to edit item" });
72
+ const defaultEditLabel = t({
73
+ message: "Edit",
74
+ comment: "@context: Button to edit item",
75
+ });
71
76
  const defaultViewLabel = t({
72
77
  message: "View",
73
78
  comment: "@context: Button to view item on public site",
74
79
  });
75
- const defaultDeleteLabel = t({ message: "Delete", comment: "@context: Button to delete item" });
80
+ const defaultDeleteLabel = t({
81
+ message: "Delete",
82
+ comment: "@context: Button to delete item",
83
+ });
84
+
85
+ const deleteClickHandler = deleteAction
86
+ ? deleteConfirm
87
+ ? `confirm('${deleteConfirm}') && @post('${deleteAction}')`
88
+ : `@post('${deleteAction}')`
89
+ : undefined;
76
90
 
77
91
  return (
78
92
  <>
@@ -87,15 +101,13 @@ export const ActionButtons: FC<ActionButtonsProps> = ({
87
101
  </a>
88
102
  )}
89
103
  {deleteAction && (
90
- <form method="post" action={deleteAction} style="display: inline">
91
- <button
92
- type="submit"
93
- class={deleteClass}
94
- onclick={deleteConfirm ? `return confirm('${deleteConfirm}')` : undefined}
95
- >
96
- {deleteLabel || defaultDeleteLabel}
97
- </button>
98
- </form>
104
+ <button
105
+ type="button"
106
+ class={deleteClass}
107
+ data-on:click__prevent={deleteClickHandler}
108
+ >
109
+ {deleteLabel || defaultDeleteLabel}
110
+ </button>
99
111
  )}
100
112
  </>
101
113
  );