@jant/core 0.2.12 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/jant.js +3 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +112 -85
- package/dist/auth.d.ts +1 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -1
- package/dist/client.js +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/i18n/context.d.ts.map +1 -1
- package/dist/i18n/context.js +0 -3
- package/dist/i18n/detect.d.ts +0 -11
- package/dist/i18n/detect.d.ts.map +1 -1
- package/dist/i18n/detect.js +1 -52
- package/dist/i18n/i18n.d.ts +4 -14
- package/dist/i18n/i18n.d.ts.map +1 -1
- package/dist/i18n/i18n.js +19 -25
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/middleware.d.ts +2 -5
- package/dist/i18n/middleware.d.ts.map +1 -1
- package/dist/i18n/middleware.js +12 -23
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/image.d.ts.map +1 -1
- package/dist/lib/schemas.d.ts.map +1 -1
- package/dist/lib/sse.d.ts +45 -17
- package/dist/lib/sse.d.ts.map +1 -1
- package/dist/lib/sse.js +77 -37
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/routes/api/posts.js +0 -1
- package/dist/routes/api/upload.js +3 -1
- package/dist/routes/dash/collections.d.ts.map +1 -1
- package/dist/routes/dash/collections.js +134 -142
- package/dist/routes/dash/index.js +25 -26
- package/dist/routes/dash/media.d.ts.map +1 -1
- package/dist/routes/dash/media.js +60 -56
- package/dist/routes/dash/pages.js +64 -66
- package/dist/routes/dash/posts.d.ts.map +1 -1
- package/dist/routes/dash/posts.js +50 -59
- package/dist/routes/dash/redirects.d.ts.map +1 -1
- package/dist/routes/dash/redirects.js +63 -60
- package/dist/routes/dash/settings.d.ts.map +1 -1
- package/dist/routes/dash/settings.js +249 -93
- package/dist/routes/feed/rss.js +6 -4
- package/dist/routes/pages/archive.js +60 -62
- package/dist/routes/pages/collection.js +8 -8
- package/dist/routes/pages/home.js +14 -14
- package/dist/routes/pages/page.js +7 -6
- package/dist/routes/pages/post.js +8 -8
- package/dist/routes/pages/search.js +25 -27
- package/dist/services/collection.d.ts.map +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/post.d.ts.map +1 -1
- package/dist/services/redirect.d.ts.map +1 -1
- package/dist/services/settings.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.d.ts +1 -1
- package/dist/theme/components/ActionButtons.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.js +17 -21
- package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.d.ts.map +1 -1
- package/dist/theme/components/DangerZone.js +12 -15
- package/dist/theme/components/EmptyState.d.ts.map +1 -1
- package/dist/theme/components/PageForm.d.ts.map +1 -1
- package/dist/theme/components/PageForm.js +58 -56
- package/dist/theme/components/Pagination.d.ts.map +1 -1
- package/dist/theme/components/Pagination.js +22 -25
- package/dist/theme/components/PostForm.d.ts +0 -1
- package/dist/theme/components/PostForm.d.ts.map +1 -1
- package/dist/theme/components/PostForm.js +85 -77
- package/dist/theme/components/PostList.d.ts.map +1 -1
- package/dist/theme/components/PostList.js +17 -17
- package/dist/theme/components/ThreadView.d.ts.map +1 -1
- package/dist/theme/components/ThreadView.js +15 -18
- package/dist/theme/components/TypeBadge.d.ts.map +1 -1
- package/dist/theme/components/TypeBadge.js +20 -20
- package/dist/theme/components/VisibilityBadge.d.ts.map +1 -1
- package/dist/theme/components/VisibilityBadge.js +14 -14
- package/dist/theme/components/index.d.ts +1 -1
- package/dist/theme/components/index.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.d.ts.map +1 -1
- package/dist/theme/layouts/BaseLayout.js +4 -2
- package/dist/theme/layouts/DashLayout.d.ts.map +1 -1
- package/dist/theme/layouts/DashLayout.js +29 -29
- package/dist/types/lingui-react-macro.d.js +9 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/datastar.js +1606 -0
- package/package.json +5 -2
- package/src/app.tsx +175 -56
- package/src/auth.ts +5 -1
- package/src/client.ts +1 -1
- package/src/db/schema.ts +22 -7
- package/src/i18n/EXAMPLES.md +34 -14
- package/src/i18n/README.md +19 -9
- package/src/i18n/context.tsx +1 -4
- package/src/i18n/detect.ts +1 -67
- package/src/i18n/i18n.ts +15 -19
- package/src/i18n/index.ts +0 -3
- package/src/i18n/middleware.ts +12 -24
- package/src/lib/constants.ts +2 -1
- package/src/lib/image-processor.ts +23 -7
- package/src/lib/image.ts +6 -2
- package/src/lib/schemas.ts +6 -2
- package/src/lib/sse.ts +138 -50
- package/src/middleware/auth.ts +6 -2
- package/src/routes/api/posts.ts +14 -5
- package/src/routes/api/upload.ts +25 -7
- package/src/routes/dash/collections.tsx +162 -70
- package/src/routes/dash/index.tsx +22 -7
- package/src/routes/dash/media.tsx +59 -16
- package/src/routes/dash/pages.tsx +102 -44
- package/src/routes/dash/posts.tsx +87 -54
- package/src/routes/dash/redirects.tsx +74 -26
- package/src/routes/dash/settings.tsx +250 -57
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/pages/archive.tsx +71 -21
- package/src/routes/pages/collection.tsx +21 -6
- package/src/routes/pages/home.tsx +30 -9
- package/src/routes/pages/page.tsx +14 -5
- package/src/routes/pages/post.tsx +21 -7
- package/src/routes/pages/search.tsx +42 -11
- package/src/services/collection.ts +34 -9
- package/src/services/index.ts +4 -1
- package/src/services/media.ts +15 -3
- package/src/services/post.ts +39 -10
- package/src/services/redirect.ts +4 -1
- package/src/services/settings.ts +14 -3
- package/src/theme/components/ActionButtons.tsx +26 -14
- package/src/theme/components/CrudPageHeader.tsx +6 -1
- package/src/theme/components/DangerZone.tsx +19 -13
- package/src/theme/components/EmptyState.tsx +6 -1
- package/src/theme/components/PageForm.tsx +71 -24
- package/src/theme/components/Pagination.tsx +26 -8
- package/src/theme/components/PostForm.tsx +72 -25
- package/src/theme/components/PostList.tsx +16 -5
- package/src/theme/components/ThreadView.tsx +25 -7
- package/src/theme/components/TypeBadge.tsx +13 -4
- package/src/theme/components/VisibilityBadge.tsx +17 -5
- package/src/theme/components/index.ts +4 -1
- package/src/theme/layouts/BaseLayout.tsx +5 -2
- package/src/theme/layouts/DashLayout.tsx +41 -12
- package/src/types/lingui-react-macro.d.ts +34 -0
- package/src/types.ts +16 -2
- package/src/vendor/datastar.js +9 -0
- package/src/vendor/datastar.js.map +7 -0
|
@@ -3,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
|
|
@@ -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({
|
|
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
|
|
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
|
|
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({
|
|
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
|
|
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 "
|
|
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 &&
|
|
23
|
+
{page.title && (
|
|
24
|
+
<h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
|
|
25
|
+
)}
|
|
24
26
|
|
|
25
|
-
<div
|
|
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
|
-
←
|
|
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 "
|
|
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 &&
|
|
23
|
+
{post.title && (
|
|
24
|
+
<h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
|
|
25
|
+
)}
|
|
24
26
|
|
|
25
|
-
<div
|
|
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
|
|
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({
|
|
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({
|
|
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 "
|
|
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">
|
|
@@ -53,20 +56,30 @@ function SearchContent({
|
|
|
53
56
|
autofocus
|
|
54
57
|
/>
|
|
55
58
|
<button type="submit" class="btn">
|
|
56
|
-
{t({
|
|
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 &&
|
|
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({
|
|
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
|
|
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
|
|
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
|
-
←
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
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
|
|
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
|
|
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> = {
|
|
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)
|
|
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
|
|
141
|
+
await db
|
|
142
|
+
.delete(postCollections)
|
|
143
|
+
.where(eq(postCollections.collectionId, id));
|
|
128
144
|
|
|
129
|
-
const result = await db
|
|
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(
|
|
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(
|
|
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));
|
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
|
},
|
|
@@ -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(
|
|
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);
|
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
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { FC } from "hono/jsx";
|
|
8
|
-
import { useLingui } from "
|
|
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
|
|
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"
|
|
68
|
+
size === "sm"
|
|
69
|
+
? "btn-sm-ghost text-destructive"
|
|
70
|
+
: "btn-ghost text-destructive";
|
|
69
71
|
|
|
70
|
-
const defaultEditLabel = t({
|
|
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({
|
|
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
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
);
|