@jant/core 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +112 -85
  3. package/dist/auth.d.ts +1 -0
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +2 -1
  6. package/dist/client.js +1 -1
  7. package/dist/db/schema.d.ts.map +1 -1
  8. package/dist/i18n/context.d.ts.map +1 -1
  9. package/dist/i18n/context.js +0 -3
  10. package/dist/i18n/detect.d.ts +0 -11
  11. package/dist/i18n/detect.d.ts.map +1 -1
  12. package/dist/i18n/detect.js +1 -52
  13. package/dist/i18n/i18n.d.ts +4 -14
  14. package/dist/i18n/i18n.d.ts.map +1 -1
  15. package/dist/i18n/i18n.js +19 -25
  16. package/dist/i18n/index.d.ts +1 -1
  17. package/dist/i18n/index.d.ts.map +1 -1
  18. package/dist/i18n/index.js +1 -1
  19. package/dist/i18n/middleware.d.ts +2 -5
  20. package/dist/i18n/middleware.d.ts.map +1 -1
  21. package/dist/i18n/middleware.js +12 -23
  22. package/dist/lib/constants.d.ts.map +1 -1
  23. package/dist/lib/schemas.d.ts.map +1 -1
  24. package/dist/lib/sse.d.ts +45 -17
  25. package/dist/lib/sse.d.ts.map +1 -1
  26. package/dist/lib/sse.js +77 -37
  27. package/dist/middleware/auth.d.ts.map +1 -1
  28. package/dist/routes/api/posts.js +0 -1
  29. package/dist/routes/api/upload.js +13 -3
  30. package/dist/routes/dash/collections.d.ts.map +1 -1
  31. package/dist/routes/dash/collections.js +134 -142
  32. package/dist/routes/dash/index.js +25 -25
  33. package/dist/routes/dash/media.d.ts.map +1 -1
  34. package/dist/routes/dash/media.js +60 -56
  35. package/dist/routes/dash/pages.d.ts.map +1 -1
  36. package/dist/routes/dash/pages.js +64 -66
  37. package/dist/routes/dash/posts.d.ts.map +1 -1
  38. package/dist/routes/dash/posts.js +50 -59
  39. package/dist/routes/dash/redirects.d.ts.map +1 -1
  40. package/dist/routes/dash/redirects.js +63 -60
  41. package/dist/routes/dash/settings.d.ts.map +1 -1
  42. package/dist/routes/dash/settings.js +249 -93
  43. package/dist/routes/feed/rss.js +6 -4
  44. package/dist/routes/pages/archive.js +60 -62
  45. package/dist/routes/pages/collection.js +8 -8
  46. package/dist/routes/pages/home.js +14 -14
  47. package/dist/routes/pages/page.js +7 -6
  48. package/dist/routes/pages/post.js +8 -8
  49. package/dist/routes/pages/search.js +25 -27
  50. package/dist/services/collection.d.ts.map +1 -1
  51. package/dist/services/index.d.ts.map +1 -1
  52. package/dist/services/media.d.ts.map +1 -1
  53. package/dist/services/post.d.ts.map +1 -1
  54. package/dist/services/redirect.d.ts.map +1 -1
  55. package/dist/services/settings.d.ts.map +1 -1
  56. package/dist/theme/components/ActionButtons.d.ts +1 -1
  57. package/dist/theme/components/ActionButtons.d.ts.map +1 -1
  58. package/dist/theme/components/ActionButtons.js +17 -21
  59. package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
  60. package/dist/theme/components/DangerZone.d.ts.map +1 -1
  61. package/dist/theme/components/DangerZone.js +12 -15
  62. package/dist/theme/components/EmptyState.d.ts.map +1 -1
  63. package/dist/theme/components/PageForm.d.ts.map +1 -1
  64. package/dist/theme/components/PageForm.js +58 -56
  65. package/dist/theme/components/Pagination.d.ts.map +1 -1
  66. package/dist/theme/components/Pagination.js +22 -25
  67. package/dist/theme/components/PostForm.d.ts +0 -1
  68. package/dist/theme/components/PostForm.d.ts.map +1 -1
  69. package/dist/theme/components/PostForm.js +85 -77
  70. package/dist/theme/components/PostList.d.ts.map +1 -1
  71. package/dist/theme/components/PostList.js +17 -17
  72. package/dist/theme/components/ThreadView.d.ts.map +1 -1
  73. package/dist/theme/components/ThreadView.js +15 -18
  74. package/dist/theme/components/TypeBadge.d.ts.map +1 -1
  75. package/dist/theme/components/TypeBadge.js +20 -20
  76. package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
  77. package/dist/theme/components/VisibilityBadge.js +14 -14
  78. package/dist/theme/components/index.d.ts +2 -2
  79. package/dist/theme/components/index.d.ts.map +1 -1
  80. package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
  81. package/dist/theme/layouts/BaseLayout.js +4 -2
  82. package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
  83. package/dist/theme/layouts/DashLayout.js +29 -29
  84. package/dist/types/lingui-react-macro.d.js +9 -0
  85. package/dist/types.d.ts +2 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/vendor/datastar.js +1606 -0
  88. package/package.json +7 -15
  89. package/src/app.tsx +222 -59
  90. package/src/auth.ts +5 -1
  91. package/src/client.ts +1 -1
  92. package/src/db/migrations/meta/0000_snapshot.json +16 -47
  93. package/src/db/migrations/meta/_journal.json +1 -1
  94. package/src/db/schema.ts +22 -7
  95. package/src/i18n/EXAMPLES.md +45 -23
  96. package/src/i18n/README.md +39 -25
  97. package/src/i18n/context.tsx +1 -4
  98. package/src/i18n/detect.ts +1 -67
  99. package/src/i18n/i18n.ts +15 -19
  100. package/src/i18n/index.ts +0 -3
  101. package/src/i18n/middleware.ts +12 -24
  102. package/src/lib/constants.ts +2 -1
  103. package/src/lib/image-processor.ts +14 -6
  104. package/src/lib/image.ts +2 -2
  105. package/src/lib/schemas.ts +7 -3
  106. package/src/lib/sse.ts +133 -51
  107. package/src/middleware/auth.ts +6 -2
  108. package/src/routes/api/posts.ts +9 -9
  109. package/src/routes/api/upload.ts +39 -10
  110. package/src/routes/dash/collections.tsx +249 -81
  111. package/src/routes/dash/index.tsx +22 -7
  112. package/src/routes/dash/media.tsx +94 -24
  113. package/src/routes/dash/pages.tsx +132 -54
  114. package/src/routes/dash/posts.tsx +99 -57
  115. package/src/routes/dash/redirects.tsx +117 -36
  116. package/src/routes/dash/settings.tsx +268 -55
  117. package/src/routes/feed/rss.ts +6 -4
  118. package/src/routes/pages/archive.tsx +78 -24
  119. package/src/routes/pages/collection.tsx +32 -8
  120. package/src/routes/pages/home.tsx +38 -10
  121. package/src/routes/pages/page.tsx +15 -5
  122. package/src/routes/pages/post.tsx +17 -6
  123. package/src/routes/pages/search.tsx +50 -13
  124. package/src/services/collection.ts +29 -8
  125. package/src/services/index.ts +4 -1
  126. package/src/services/media.ts +15 -3
  127. package/src/services/post.ts +37 -10
  128. package/src/services/redirect.ts +4 -1
  129. package/src/services/settings.ts +14 -3
  130. package/src/theme/components/ActionButtons.tsx +31 -15
  131. package/src/theme/components/CrudPageHeader.tsx +3 -4
  132. package/src/theme/components/DangerZone.tsx +19 -13
  133. package/src/theme/components/EmptyState.tsx +1 -5
  134. package/src/theme/components/PageForm.tsx +80 -25
  135. package/src/theme/components/Pagination.tsx +34 -31
  136. package/src/theme/components/PostForm.tsx +91 -27
  137. package/src/theme/components/PostList.tsx +23 -6
  138. package/src/theme/components/ThreadView.tsx +25 -10
  139. package/src/theme/components/TypeBadge.tsx +13 -4
  140. package/src/theme/components/VisibilityBadge.tsx +17 -5
  141. package/src/theme/components/index.ts +12 -2
  142. package/src/theme/layouts/BaseLayout.tsx +6 -5
  143. package/src/theme/layouts/DashLayout.tsx +71 -18
  144. package/src/types/lingui-react-macro.d.ts +34 -0
  145. package/src/types.ts +16 -4
  146. package/src/vendor/datastar.js +9 -0
  147. package/src/vendor/datastar.js.map +7 -0
  148. package/dist/plugin.d.ts +0 -3
  149. package/dist/plugin.d.ts.map +0 -1
  150. package/dist/plugin.js +0 -20
  151. package/dist/tailwind.d.ts +0 -12
  152. package/dist/tailwind.d.ts.map +0 -1
  153. package/dist/tailwind.js +0 -15
@@ -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
@@ -33,13 +39,21 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
33
39
 
34
40
  <main class="flex flex-col gap-6">
35
41
  {posts.length === 0 ? (
36
- <p class="text-muted-foreground">{t({ message: "No posts yet.", comment: "@context: Empty state message on home page" })}</p>
42
+ <p class="text-muted-foreground">
43
+ {t({
44
+ message: "No posts yet.",
45
+ comment: "@context: Empty state message on home page",
46
+ })}
47
+ </p>
37
48
  ) : (
38
49
  posts.map((post) => (
39
50
  <article key={post.id} class="h-entry">
40
51
  {post.title && (
41
52
  <h2 class="p-name text-lg font-medium mb-2">
42
- <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
+ >
43
57
  {post.title}
44
58
  </a>
45
59
  </h2>
@@ -49,11 +63,19 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
49
63
  dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
50
64
  />
51
65
  <footer class="mt-2 text-sm text-muted-foreground">
52
- <time class="dt-published" datetime={time.toISOString(post.publishedAt)}>
66
+ <time
67
+ class="dt-published"
68
+ datetime={time.toISOString(post.publishedAt)}
69
+ >
53
70
  {time.formatDate(post.publishedAt)}
54
71
  </time>
55
72
  {post.visibility === "featured" && (
56
- <span class="ml-2 text-xs">{t({ message: "Featured", comment: "@context: Post visibility badge" })}</span>
73
+ <span class="ml-2 text-xs">
74
+ {t({
75
+ message: "Featured",
76
+ comment: "@context: Post visibility badge",
77
+ })}
78
+ </span>
57
79
  )}
58
80
  </footer>
59
81
  </article>
@@ -63,8 +85,14 @@ function HomeContent({ siteName, posts }: { siteName: string; posts: Post[] }) {
63
85
 
64
86
  {posts.length >= 20 && (
65
87
  <nav class="mt-8 text-center">
66
- <a href="/archive" class="text-sm text-muted-foreground hover:text-foreground">
67
- {t({ message: "View all posts →", comment: "@context: Link to view all posts on archive page" })}
88
+ <a
89
+ href="/archive"
90
+ class="text-sm text-muted-foreground hover:text-foreground"
91
+ >
92
+ {t({
93
+ message: "View all posts →",
94
+ comment: "@context: Link to view all posts on archive page",
95
+ })}
68
96
  </a>
69
97
  </nav>
70
98
  )}
@@ -88,6 +116,6 @@ homeRoutes.get("/", async (c) => {
88
116
  return c.html(
89
117
  <BaseLayout title={siteName} c={c}>
90
118
  <HomeContent siteName={siteName} posts={posts} />
91
- </BaseLayout>
119
+ </BaseLayout>,
92
120
  );
93
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,7 +20,9 @@ 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
27
  <div
26
28
  class="e-content prose"
@@ -30,7 +32,11 @@ function PageContent({ page }: { page: Post }) {
30
32
 
31
33
  <nav class="mt-8 pt-6 border-t">
32
34
  <a href="/" class="text-sm hover:underline">
33
- {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
+ })}
34
40
  </a>
35
41
  </nav>
36
42
  </div>
@@ -57,8 +63,12 @@ pageRoutes.get("/:path", async (c) => {
57
63
  const siteName = (await c.var.services.settings.get("SITE_NAME")) ?? "Jant";
58
64
 
59
65
  return c.html(
60
- <BaseLayout title={`${page.title} - ${siteName}`} description={page.content?.slice(0, 160)} c={c}>
66
+ <BaseLayout
67
+ title={`${page.title} - ${siteName}`}
68
+ description={page.content?.slice(0, 160)}
69
+ c={c}
70
+ >
61
71
  <PageContent page={page} />
62
- </BaseLayout>
72
+ </BaseLayout>,
63
73
  );
64
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,7 +20,9 @@ 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
27
  <div
26
28
  class="e-content prose"
@@ -28,18 +30,27 @@ function PostContent({ post }: { post: Post }) {
28
30
  />
29
31
 
30
32
  <footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
31
- <time class="dt-published" datetime={time.toISOString(post.publishedAt)}>
33
+ <time
34
+ class="dt-published"
35
+ datetime={time.toISOString(post.publishedAt)}
36
+ >
32
37
  {time.formatDate(post.publishedAt)}
33
38
  </time>
34
39
  <a href={`/p/${sqid.encode(post.id)}`} class="u-url ml-4">
35
- {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
+ })}
36
44
  </a>
37
45
  </footer>
38
46
  </article>
39
47
 
40
48
  <nav class="mt-8">
41
49
  <a href="/" class="text-sm hover:underline">
42
- {t({ message: "← Back to home", comment: "@context: Navigation link" })}
50
+ {t({
51
+ message: "← Back to home",
52
+ comment: "@context: Navigation link",
53
+ })}
43
54
  </a>
44
55
  </nav>
45
56
  </div>
@@ -76,6 +87,6 @@ postRoutes.get("/:id", async (c) => {
76
87
  return c.html(
77
88
  <BaseLayout title={title} description={post.content?.slice(0, 160)} c={c}>
78
89
  <PostContent post={post} />
79
- </BaseLayout>
90
+ </BaseLayout>,
80
91
  );
81
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">
@@ -45,12 +48,18 @@ function SearchContent({
45
48
  type="search"
46
49
  name="q"
47
50
  class="input flex-1"
48
- placeholder={t({ message: "Search posts...", comment: "@context: Search input placeholder" })}
51
+ placeholder={t({
52
+ message: "Search posts...",
53
+ comment: "@context: Search input placeholder",
54
+ })}
49
55
  value={query}
50
56
  autofocus
51
57
  />
52
58
  <button type="submit" class="btn">
53
- {t({ message: "Search", comment: "@context: Search submit button" })}
59
+ {t({
60
+ message: "Search",
61
+ comment: "@context: Search submit button",
62
+ })}
54
63
  </button>
55
64
  </div>
56
65
  </form>
@@ -67,17 +76,30 @@ function SearchContent({
67
76
  <div>
68
77
  <p class="text-sm text-muted-foreground mb-4">
69
78
  {results.length === 0
70
- ? t({ message: "No results found.", comment: "@context: Search empty results" })
79
+ ? t({
80
+ message: "No results found.",
81
+ comment: "@context: Search empty results",
82
+ })
71
83
  : results.length === 1
72
- ? t({ message: "Found 1 result", comment: "@context: Search results count - single" })
73
- : t({ message: "Found {count} results", comment: "@context: Search results count - multiple", values: { count: String(results.length) } })}
84
+ ? t({
85
+ message: "Found 1 result",
86
+ comment: "@context: Search results count - single",
87
+ })
88
+ : t({
89
+ message: "Found {count} results",
90
+ comment: "@context: Search results count - multiple",
91
+ values: { count: String(results.length) },
92
+ })}
74
93
  </p>
75
94
 
76
95
  {results.length > 0 && (
77
96
  <>
78
97
  <div class="flex flex-col gap-4">
79
98
  {results.map((result) => (
80
- <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
+ >
81
103
  <a href={`/p/${sqid.encode(result.post.id)}`} class="block">
82
104
  <h2 class="font-medium hover:underline">
83
105
  {result.post.title ||
@@ -94,7 +116,9 @@ function SearchContent({
94
116
 
95
117
  <footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
96
118
  <span class="badge-outline">{result.post.type}</span>
97
- <time datetime={time.toISOString(result.post.publishedAt)}>
119
+ <time
120
+ datetime={time.toISOString(result.post.publishedAt)}
121
+ >
98
122
  {time.formatDate(result.post.publishedAt)}
99
123
  </time>
100
124
  </footer>
@@ -115,7 +139,11 @@ function SearchContent({
115
139
 
116
140
  <nav class="mt-8 pt-6 border-t">
117
141
  <a href="/" class="text-sm hover:underline">
118
- {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
+ })}
119
147
  </a>
120
148
  </nav>
121
149
  </div>
@@ -155,8 +183,17 @@ searchRoutes.get("/", async (c) => {
155
183
  }
156
184
 
157
185
  return c.html(
158
- <BaseLayout title={query ? `Search: ${query} - ${siteName}` : `Search - ${siteName}`} c={c}>
159
- <SearchContent query={query} results={results} error={error} hasMore={hasMore} page={page} />
160
- </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>,
161
198
  );
162
199
  });
@@ -70,7 +70,11 @@ 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
 
@@ -84,7 +88,10 @@ export function createCollectionService(db: Database): CollectionService {
84
88
  },
85
89
 
86
90
  async list() {
87
- 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));
88
95
  return rows.map(toCollection);
89
96
  },
90
97
 
@@ -111,11 +118,14 @@ export function createCollectionService(db: Database): CollectionService {
111
118
  if (!existing) return null;
112
119
 
113
120
  const timestamp = now();
114
- const updates: Partial<typeof collections.$inferInsert> = { updatedAt: timestamp };
121
+ const updates: Partial<typeof collections.$inferInsert> = {
122
+ updatedAt: timestamp,
123
+ };
115
124
 
116
125
  if (data.title !== undefined) updates.title = data.title;
117
126
  if (data.path !== undefined) updates.path = data.path;
118
- if (data.description !== undefined) updates.description = data.description;
127
+ if (data.description !== undefined)
128
+ updates.description = data.description;
119
129
 
120
130
  const result = await db
121
131
  .update(collections)
@@ -128,9 +138,14 @@ export function createCollectionService(db: Database): CollectionService {
128
138
 
129
139
  async delete(id) {
130
140
  // Delete all post-collection relationships first
131
- await db.delete(postCollections).where(eq(postCollections.collectionId, id));
141
+ await db
142
+ .delete(postCollections)
143
+ .where(eq(postCollections.collectionId, id));
132
144
 
133
- 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();
134
149
  return result.length > 0;
135
150
  },
136
151
 
@@ -152,7 +167,10 @@ export function createCollectionService(db: Database): CollectionService {
152
167
  await db
153
168
  .delete(postCollections)
154
169
  .where(
155
- and(eq(postCollections.collectionId, collectionId), eq(postCollections.postId, postId))
170
+ and(
171
+ eq(postCollections.collectionId, collectionId),
172
+ eq(postCollections.postId, postId),
173
+ ),
156
174
  );
157
175
  },
158
176
 
@@ -171,7 +189,10 @@ export function createCollectionService(db: Database): CollectionService {
171
189
  const rows = await db
172
190
  .select({ collection: collections })
173
191
  .from(postCollections)
174
- .innerJoin(collections, eq(postCollections.collectionId, collections.id))
192
+ .innerJoin(
193
+ collections,
194
+ eq(postCollections.collectionId, collections.id),
195
+ )
175
196
  .where(eq(postCollections.postId, postId));
176
197
 
177
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
  },
@@ -240,7 +264,10 @@ export function createPostService(db: Database): PostService {
240
264
  .select()
241
265
  .from(posts)
242
266
  .where(
243
- and(or(eq(posts.id, rootId), eq(posts.threadId, rootId)), isNull(posts.deletedAt))
267
+ and(
268
+ or(eq(posts.id, rootId), eq(posts.threadId, rootId)),
269
+ isNull(posts.deletedAt),
270
+ ),
244
271
  )
245
272
  .orderBy(posts.createdAt);
246
273
 
@@ -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
  }