@jant/core 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +112 -85
- package/dist/auth.d.ts +1 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -1
- package/dist/client.js +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/i18n/context.d.ts.map +1 -1
- package/dist/i18n/context.js +0 -3
- package/dist/i18n/detect.d.ts +0 -11
- package/dist/i18n/detect.d.ts.map +1 -1
- package/dist/i18n/detect.js +1 -52
- package/dist/i18n/i18n.d.ts +4 -14
- package/dist/i18n/i18n.d.ts.map +1 -1
- package/dist/i18n/i18n.js +19 -25
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/middleware.d.ts +2 -5
- package/dist/i18n/middleware.d.ts.map +1 -1
- package/dist/i18n/middleware.js +12 -23
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/sse.d.ts +45 -17
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +77 -37
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/routes/api/posts.js +0 -1
- package/dist/routes/api/upload.js +13 -3
- package/dist/routes/dash/collections.d.ts.map +1 -1
- package/dist/routes/dash/collections.js +134 -142
- package/dist/routes/dash/index.js +25 -25
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +60 -56
- package/dist/routes/dash/pages.d.ts.map +1 -1
- package/dist/routes/dash/pages.js +64 -66
- package/dist/routes/dash/posts.d.ts.map +1 -1
- package/dist/routes/dash/posts.js +50 -59
- package/dist/routes/dash/redirects.d.ts.map +1 -1
- package/dist/routes/dash/redirects.js +63 -60
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +249 -93
- package/dist/routes/feed/rss.js +6 -4
- package/dist/routes/pages/archive.js +60 -62
- package/dist/routes/pages/collection.js +8 -8
- package/dist/routes/pages/home.js +14 -14
- package/dist/routes/pages/page.js +7 -6
- package/dist/routes/pages/post.js +8 -8
- package/dist/routes/pages/search.js +25 -27
- package/dist/services/collection.d.ts.map +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/post.d.ts.map +1 -1
- package/dist/services/redirect.d.ts.map +1 -1
- package/dist/services/settings.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.d.ts +1 -1
- package/dist/theme/components/ActionButtons.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.js +17 -21
- package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.js +12 -15
- package/dist/theme/components/EmptyState.d.ts.map +1 -1
- package/dist/theme/components/PageForm.d.ts.map +1 -1
- package/dist/theme/components/PageForm.js +58 -56
- package/dist/theme/components/Pagination.d.ts.map +1 -1
- package/dist/theme/components/Pagination.js +22 -25
- package/dist/theme/components/PostForm.d.ts +0 -1
- package/dist/theme/components/PostForm.d.ts.map +1 -1
- package/dist/theme/components/PostForm.js +85 -77
- package/dist/theme/components/PostList.d.ts.map +1 -1
- package/dist/theme/components/PostList.js +17 -17
- package/dist/theme/components/ThreadView.d.ts.map +1 -1
- package/dist/theme/components/ThreadView.js +15 -18
- package/dist/theme/components/TypeBadge.d.ts.map +1 -1
- package/dist/theme/components/TypeBadge.js +20 -20
- package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
- package/dist/theme/components/VisibilityBadge.js +14 -14
- package/dist/theme/components/index.d.ts +2 -2
- package/dist/theme/components/index.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.js +4 -2
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +29 -29
- package/dist/types/lingui-react-macro.d.js +9 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/datastar.js +1606 -0
- package/package.json +7 -15
- package/src/app.tsx +222 -59
- package/src/auth.ts +5 -1
- package/src/client.ts +1 -1
- package/src/db/migrations/meta/0000_snapshot.json +16 -47
- package/src/db/migrations/meta/_journal.json +1 -1
- package/src/db/schema.ts +22 -7
- package/src/i18n/EXAMPLES.md +45 -23
- package/src/i18n/README.md +39 -25
- package/src/i18n/context.tsx +1 -4
- package/src/i18n/detect.ts +1 -67
- package/src/i18n/i18n.ts +15 -19
- package/src/i18n/index.ts +0 -3
- package/src/i18n/middleware.ts +12 -24
- package/src/lib/constants.ts +2 -1
- package/src/lib/image-processor.ts +14 -6
- package/src/lib/image.ts +2 -2
- package/src/lib/schemas.ts +7 -3
- package/src/lib/sse.ts +133 -51
- package/src/middleware/auth.ts +6 -2
- package/src/routes/api/posts.ts +9 -9
- package/src/routes/api/upload.ts +39 -10
- package/src/routes/dash/collections.tsx +249 -81
- package/src/routes/dash/index.tsx +22 -7
- package/src/routes/dash/media.tsx +94 -24
- package/src/routes/dash/pages.tsx +132 -54
- package/src/routes/dash/posts.tsx +99 -57
- package/src/routes/dash/redirects.tsx +117 -36
- package/src/routes/dash/settings.tsx +268 -55
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/pages/archive.tsx +78 -24
- package/src/routes/pages/collection.tsx +32 -8
- package/src/routes/pages/home.tsx +38 -10
- package/src/routes/pages/page.tsx +15 -5
- package/src/routes/pages/post.tsx +17 -6
- package/src/routes/pages/search.tsx +50 -13
- package/src/services/collection.ts +29 -8
- package/src/services/index.ts +4 -1
- package/src/services/media.ts +15 -3
- package/src/services/post.ts +37 -10
- package/src/services/redirect.ts +4 -1
- package/src/services/settings.ts +14 -3
- package/src/theme/components/ActionButtons.tsx +31 -15
- package/src/theme/components/CrudPageHeader.tsx +3 -4
- package/src/theme/components/DangerZone.tsx +19 -13
- package/src/theme/components/EmptyState.tsx +1 -5
- package/src/theme/components/PageForm.tsx +80 -25
- package/src/theme/components/Pagination.tsx +34 -31
- package/src/theme/components/PostForm.tsx +91 -27
- package/src/theme/components/PostList.tsx +23 -6
- package/src/theme/components/ThreadView.tsx +25 -10
- package/src/theme/components/TypeBadge.tsx +13 -4
- package/src/theme/components/VisibilityBadge.tsx +17 -5
- package/src/theme/components/index.ts +12 -2
- package/src/theme/layouts/BaseLayout.tsx +6 -5
- package/src/theme/layouts/DashLayout.tsx +71 -18
- package/src/types/lingui-react-macro.d.ts +34 -0
- package/src/types.ts +16 -4
- package/src/vendor/datastar.js +9 -0
- package/src/vendor/datastar.js.map +7 -0
- package/dist/plugin.d.ts +0 -3
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -20
- package/dist/tailwind.d.ts +0 -12
- package/dist/tailwind.d.ts.map +0 -1
- package/dist/tailwind.js +0 -15
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import { useLingui } from "
|
|
6
|
+
import { useLingui } from "@lingui/react/macro";
|
|
7
7
|
import type { Bindings, 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
|
|
26
|
-
|
|
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">
|
|
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
|
|
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
|
|
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">
|
|
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
|
|
67
|
-
|
|
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 "
|
|
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 &&
|
|
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
|
-
←
|
|
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
|
|
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 "
|
|
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 &&
|
|
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
|
|
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({
|
|
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({
|
|
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 "
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
79
|
+
? t({
|
|
80
|
+
message: "No results found.",
|
|
81
|
+
comment: "@context: Search empty results",
|
|
82
|
+
})
|
|
71
83
|
: results.length === 1
|
|
72
|
-
? t({
|
|
73
|
-
|
|
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
|
|
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
|
|
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
|
-
←
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
|
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> = {
|
|
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)
|
|
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
|
|
141
|
+
await db
|
|
142
|
+
.delete(postCollections)
|
|
143
|
+
.where(eq(postCollections.collectionId, id));
|
|
132
144
|
|
|
133
|
-
const result = await db
|
|
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(
|
|
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(
|
|
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));
|
package/src/services/index.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
package/src/services/media.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
package/src/services/post.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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> = {
|
|
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)
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
|
package/src/services/redirect.ts
CHANGED
|
@@ -62,7 +62,10 @@ export function createRedirectService(db: Database): RedirectService {
|
|
|
62
62
|
},
|
|
63
63
|
|
|
64
64
|
async delete(id) {
|
|
65
|
-
const result = await db
|
|
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
|
|
package/src/services/settings.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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(
|
|
81
|
+
await this.set(
|
|
82
|
+
SETTINGS_KEYS.ONBOARDING_STATUS,
|
|
83
|
+
ONBOARDING_STATUS.COMPLETED,
|
|
84
|
+
);
|
|
74
85
|
},
|
|
75
86
|
};
|
|
76
87
|
}
|