@jant/core 0.2.10 → 0.2.12
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 +1 -3
- package/dist/app.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.map +1 -1
- package/dist/routes/api/upload.js +10 -2
- package/dist/routes/dash/collections.d.ts.map +1 -1
- package/dist/routes/dash/index.js +2 -1
- package/dist/routes/dash/pages.d.ts.map +1 -1
- package/dist/routes/dash/redirects.d.ts.map +1 -1
- package/dist/services/collection.d.ts.map +1 -1
- package/dist/services/post.d.ts.map +1 -1
- package/dist/theme/components/ActionButtons.d.ts.map +1 -1
- package/dist/theme/components/CrudPageHeader.d.ts.map +1 -1
- package/dist/theme/components/EmptyState.d.ts.map +1 -1
- package/dist/theme/components/PageForm.d.ts.map +1 -1
- package/dist/theme/components/Pagination.d.ts.map +1 -1
- package/dist/theme/components/PostForm.d.ts.map +1 -1
- package/dist/theme/components/PostList.d.ts.map +1 -1
- package/dist/theme/components/ThreadView.d.ts.map +1 -1
- 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/DashLayout.d.ts.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -18
- package/src/app.tsx +56 -12
- package/src/db/migrations/meta/0000_snapshot.json +16 -47
- package/src/db/migrations/meta/_journal.json +1 -1
- package/src/i18n/EXAMPLES.md +15 -13
- package/src/i18n/README.md +22 -18
- package/src/i18n/context.tsx +1 -1
- package/src/lib/image-processor.ts +2 -10
- package/src/lib/image.ts +1 -5
- package/src/lib/schemas.ts +6 -6
- package/src/lib/sse.ts +2 -8
- package/src/preset.css +3 -1
- package/src/routes/api/posts.ts +4 -13
- package/src/routes/api/upload.ts +19 -8
- package/src/routes/dash/collections.tsx +102 -26
- package/src/routes/dash/index.tsx +5 -5
- package/src/routes/dash/media.tsx +51 -24
- package/src/routes/dash/pages.tsx +41 -21
- package/src/routes/dash/posts.tsx +12 -3
- package/src/routes/dash/redirects.tsx +53 -20
- package/src/routes/dash/settings.tsx +26 -6
- package/src/routes/pages/archive.tsx +19 -15
- package/src/routes/pages/collection.tsx +11 -2
- package/src/routes/pages/home.tsx +10 -3
- package/src/routes/pages/page.tsx +6 -5
- package/src/routes/pages/post.tsx +1 -4
- package/src/routes/pages/search.tsx +14 -8
- package/src/services/collection.ts +1 -5
- package/src/services/post.ts +1 -3
- package/src/theme/components/ActionButtons.tsx +6 -2
- package/src/theme/components/CrudPageHeader.tsx +4 -10
- package/src/theme/components/EmptyState.tsx +2 -11
- package/src/theme/components/PageForm.tsx +17 -9
- package/src/theme/components/Pagination.tsx +25 -40
- package/src/theme/components/PostForm.tsx +25 -8
- package/src/theme/components/PostList.tsx +17 -11
- package/src/theme/components/ThreadView.tsx +16 -19
- package/src/theme/components/index.ts +8 -1
- package/src/theme/layouts/BaseLayout.tsx +1 -3
- package/src/theme/layouts/DashLayout.tsx +32 -8
- package/src/types.ts +0 -2
- 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
- package/src/tailwind.ts +0 -20
|
@@ -45,7 +45,10 @@ function SearchContent({
|
|
|
45
45
|
type="search"
|
|
46
46
|
name="q"
|
|
47
47
|
class="input flex-1"
|
|
48
|
-
placeholder={t({
|
|
48
|
+
placeholder={t({
|
|
49
|
+
message: "Search posts...",
|
|
50
|
+
comment: "@context: Search input placeholder",
|
|
51
|
+
})}
|
|
49
52
|
value={query}
|
|
50
53
|
autofocus
|
|
51
54
|
/>
|
|
@@ -56,11 +59,7 @@ function SearchContent({
|
|
|
56
59
|
</form>
|
|
57
60
|
|
|
58
61
|
{/* Error */}
|
|
59
|
-
{error &&
|
|
60
|
-
<div class="p-4 rounded-lg bg-destructive/10 text-destructive mb-6">
|
|
61
|
-
{error}
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
62
|
+
{error && <div class="p-4 rounded-lg bg-destructive/10 text-destructive mb-6">{error}</div>}
|
|
64
63
|
|
|
65
64
|
{/* Results */}
|
|
66
65
|
{query && !error && (
|
|
@@ -69,8 +68,15 @@ function SearchContent({
|
|
|
69
68
|
{results.length === 0
|
|
70
69
|
? t({ message: "No results found.", comment: "@context: Search empty results" })
|
|
71
70
|
: results.length === 1
|
|
72
|
-
? t({
|
|
73
|
-
|
|
71
|
+
? t({
|
|
72
|
+
message: "Found 1 result",
|
|
73
|
+
comment: "@context: Search results count - single",
|
|
74
|
+
})
|
|
75
|
+
: t({
|
|
76
|
+
message: "Found {count} results",
|
|
77
|
+
comment: "@context: Search results count - multiple",
|
|
78
|
+
values: { count: String(results.length) },
|
|
79
|
+
})}
|
|
74
80
|
</p>
|
|
75
81
|
|
|
76
82
|
{results.length > 0 && (
|
|
@@ -75,11 +75,7 @@ export function createCollectionService(db: Database): CollectionService {
|
|
|
75
75
|
},
|
|
76
76
|
|
|
77
77
|
async getByPath(path) {
|
|
78
|
-
const result = await db
|
|
79
|
-
.select()
|
|
80
|
-
.from(collections)
|
|
81
|
-
.where(eq(collections.path, path))
|
|
82
|
-
.limit(1);
|
|
78
|
+
const result = await db.select().from(collections).where(eq(collections.path, path)).limit(1);
|
|
83
79
|
return result[0] ? toCollection(result[0]) : null;
|
|
84
80
|
},
|
|
85
81
|
|
package/src/services/post.ts
CHANGED
|
@@ -239,9 +239,7 @@ export function createPostService(db: Database): PostService {
|
|
|
239
239
|
const rows = await db
|
|
240
240
|
.select()
|
|
241
241
|
.from(posts)
|
|
242
|
-
.where(
|
|
243
|
-
and(or(eq(posts.id, rootId), eq(posts.threadId, rootId)), isNull(posts.deletedAt))
|
|
244
|
-
)
|
|
242
|
+
.where(and(or(eq(posts.id, rootId), eq(posts.threadId, rootId)), isNull(posts.deletedAt)))
|
|
245
243
|
.orderBy(posts.createdAt);
|
|
246
244
|
|
|
247
245
|
return rows.map(toPost);
|
|
@@ -64,10 +64,14 @@ export const ActionButtons: FC<ActionButtonsProps> = ({
|
|
|
64
64
|
|
|
65
65
|
const editClass = size === "sm" ? "btn-sm-outline" : "btn-outline";
|
|
66
66
|
const viewClass = size === "sm" ? "btn-sm-ghost" : "btn-ghost";
|
|
67
|
-
const deleteClass =
|
|
67
|
+
const deleteClass =
|
|
68
|
+
size === "sm" ? "btn-sm-ghost text-destructive" : "btn-ghost text-destructive";
|
|
68
69
|
|
|
69
70
|
const defaultEditLabel = t({ message: "Edit", comment: "@context: Button to edit item" });
|
|
70
|
-
const defaultViewLabel = t({
|
|
71
|
+
const defaultViewLabel = t({
|
|
72
|
+
message: "View",
|
|
73
|
+
comment: "@context: Button to view item on public site",
|
|
74
|
+
});
|
|
71
75
|
const defaultDeleteLabel = t({ message: "Delete", comment: "@context: Button to delete item" });
|
|
72
76
|
|
|
73
77
|
return (
|
|
@@ -27,22 +27,16 @@ export interface CrudPageHeaderProps extends PropsWithChildren {
|
|
|
27
27
|
// Optional children to render in place of default CTA button (useful for custom actions like upload buttons)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export const CrudPageHeader: FC<CrudPageHeaderProps> = ({
|
|
31
|
-
title,
|
|
32
|
-
ctaLabel,
|
|
33
|
-
ctaHref,
|
|
34
|
-
children,
|
|
35
|
-
}) => {
|
|
30
|
+
export const CrudPageHeader: FC<CrudPageHeaderProps> = ({ title, ctaLabel, ctaHref, children }) => {
|
|
36
31
|
return (
|
|
37
32
|
<div class="flex items-center justify-between mb-6">
|
|
38
33
|
<h1 class="text-2xl font-semibold">{title}</h1>
|
|
39
|
-
{children ||
|
|
40
|
-
ctaLabel && ctaHref && (
|
|
34
|
+
{children ||
|
|
35
|
+
(ctaLabel && ctaHref && (
|
|
41
36
|
<a href={ctaHref} class="btn">
|
|
42
37
|
{ctaLabel}
|
|
43
38
|
</a>
|
|
44
|
-
)
|
|
45
|
-
)}
|
|
39
|
+
))}
|
|
46
40
|
</div>
|
|
47
41
|
);
|
|
48
42
|
};
|
|
@@ -29,18 +29,9 @@ export interface EmptyStateProps {
|
|
|
29
29
|
centered?: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export const EmptyState: FC<EmptyStateProps> = ({
|
|
33
|
-
message,
|
|
34
|
-
ctaText,
|
|
35
|
-
ctaHref,
|
|
36
|
-
centered = true,
|
|
37
|
-
}) => {
|
|
32
|
+
export const EmptyState: FC<EmptyStateProps> = ({ message, ctaText, ctaHref, centered = true }) => {
|
|
38
33
|
if (!centered) {
|
|
39
|
-
return
|
|
40
|
-
<p class="text-muted-foreground">
|
|
41
|
-
{message}
|
|
42
|
-
</p>
|
|
43
|
-
);
|
|
34
|
+
return <p class="text-muted-foreground">{message}</p>;
|
|
44
35
|
}
|
|
45
36
|
|
|
46
37
|
return (
|
|
@@ -14,11 +14,7 @@ export interface PageFormProps {
|
|
|
14
14
|
cancelUrl?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export const PageForm: FC<PageFormProps> = ({
|
|
18
|
-
page,
|
|
19
|
-
action,
|
|
20
|
-
cancelUrl = "/dash/pages",
|
|
21
|
-
}) => {
|
|
17
|
+
export const PageForm: FC<PageFormProps> = ({ page, action, cancelUrl = "/dash/pages" }) => {
|
|
22
18
|
const { t } = useLingui();
|
|
23
19
|
const isEdit = !!page;
|
|
24
20
|
|
|
@@ -56,12 +52,18 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
56
52
|
placeholder="about"
|
|
57
53
|
value={page?.path ?? ""}
|
|
58
54
|
pattern="[a-z0-9\-]+"
|
|
59
|
-
title={t({
|
|
55
|
+
title={t({
|
|
56
|
+
message: "Lowercase letters, numbers, and hyphens only",
|
|
57
|
+
comment: "@context: Page path validation message",
|
|
58
|
+
})}
|
|
60
59
|
required
|
|
61
60
|
/>
|
|
62
61
|
</div>
|
|
63
62
|
<p class="text-xs text-muted-foreground mt-1">
|
|
64
|
-
{t({
|
|
63
|
+
{t({
|
|
64
|
+
message: "The URL path for this page. Use lowercase letters, numbers, and hyphens.",
|
|
65
|
+
comment: "@context: Page path helper text",
|
|
66
|
+
})}
|
|
65
67
|
</p>
|
|
66
68
|
</div>
|
|
67
69
|
|
|
@@ -73,7 +75,10 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
73
75
|
<textarea
|
|
74
76
|
name="content"
|
|
75
77
|
class="textarea min-h-48"
|
|
76
|
-
placeholder={t({
|
|
78
|
+
placeholder={t({
|
|
79
|
+
message: "Page content (Markdown supported)...",
|
|
80
|
+
comment: "@context: Page content placeholder",
|
|
81
|
+
})}
|
|
77
82
|
required
|
|
78
83
|
>
|
|
79
84
|
{page?.content ?? ""}
|
|
@@ -94,7 +99,10 @@ export const PageForm: FC<PageFormProps> = ({
|
|
|
94
99
|
</option>
|
|
95
100
|
</select>
|
|
96
101
|
<p class="text-xs text-muted-foreground mt-1">
|
|
97
|
-
{t({
|
|
102
|
+
{t({
|
|
103
|
+
message: "Published pages are accessible via their path. Drafts are not visible.",
|
|
104
|
+
comment: "@context: Page status helper text",
|
|
105
|
+
})}
|
|
98
106
|
</p>
|
|
99
107
|
</div>
|
|
100
108
|
|
|
@@ -42,38 +42,31 @@ export const Pagination: FC<PaginationProps> = ({
|
|
|
42
42
|
return `${url.pathname}${url.search}`;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
const prevText = t({
|
|
45
|
+
const prevText = t({
|
|
46
|
+
message: "Previous",
|
|
47
|
+
comment: "@context: Pagination button - previous page",
|
|
48
|
+
});
|
|
46
49
|
const nextText = t({ message: "Next", comment: "@context: Pagination button - next page" });
|
|
47
50
|
|
|
48
51
|
return (
|
|
49
52
|
<nav class="flex items-center justify-between py-4" aria-label="Pagination">
|
|
50
53
|
<div>
|
|
51
54
|
{hasPrev ? (
|
|
52
|
-
<a
|
|
53
|
-
href={buildUrl(prevCursor)}
|
|
54
|
-
class="btn-outline text-sm"
|
|
55
|
-
>
|
|
55
|
+
<a href={buildUrl(prevCursor)} class="btn-outline text-sm">
|
|
56
56
|
← {prevText}
|
|
57
57
|
</a>
|
|
58
58
|
) : (
|
|
59
|
-
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
60
|
-
← {prevText}
|
|
61
|
-
</span>
|
|
59
|
+
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">← {prevText}</span>
|
|
62
60
|
)}
|
|
63
61
|
</div>
|
|
64
62
|
|
|
65
63
|
<div>
|
|
66
64
|
{hasNext ? (
|
|
67
|
-
<a
|
|
68
|
-
href={buildUrl(nextCursor)}
|
|
69
|
-
class="btn-outline text-sm"
|
|
70
|
-
>
|
|
65
|
+
<a href={buildUrl(nextCursor)} class="btn-outline text-sm">
|
|
71
66
|
{nextText} →
|
|
72
67
|
</a>
|
|
73
68
|
) : (
|
|
74
|
-
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
75
|
-
{nextText} →
|
|
76
|
-
</span>
|
|
69
|
+
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">{nextText} →</span>
|
|
77
70
|
)}
|
|
78
71
|
</div>
|
|
79
72
|
</nav>
|
|
@@ -92,17 +85,14 @@ export interface LoadMoreProps {
|
|
|
92
85
|
text?: string;
|
|
93
86
|
}
|
|
94
87
|
|
|
95
|
-
export const LoadMore: FC<LoadMoreProps> = ({
|
|
96
|
-
href,
|
|
97
|
-
hasMore,
|
|
98
|
-
text,
|
|
99
|
-
}) => {
|
|
88
|
+
export const LoadMore: FC<LoadMoreProps> = ({ href, hasMore, text }) => {
|
|
100
89
|
const { t } = useLingui();
|
|
101
90
|
if (!hasMore) {
|
|
102
91
|
return null;
|
|
103
92
|
}
|
|
104
93
|
|
|
105
|
-
const buttonText =
|
|
94
|
+
const buttonText =
|
|
95
|
+
text ?? t({ message: "Load more", comment: "@context: Pagination button - load more items" });
|
|
106
96
|
|
|
107
97
|
return (
|
|
108
98
|
<div class="text-center py-4">
|
|
@@ -152,43 +142,38 @@ export const PagePagination: FC<PagePaginationProps> = ({
|
|
|
152
142
|
return `${url.pathname}${url.search}`;
|
|
153
143
|
};
|
|
154
144
|
|
|
155
|
-
const prevText = t({
|
|
145
|
+
const prevText = t({
|
|
146
|
+
message: "Previous",
|
|
147
|
+
comment: "@context: Pagination button - previous page",
|
|
148
|
+
});
|
|
156
149
|
const nextText = t({ message: "Next", comment: "@context: Pagination button - next page" });
|
|
157
|
-
const pageText = t({
|
|
150
|
+
const pageText = t({
|
|
151
|
+
message: "Page {page}",
|
|
152
|
+
comment: "@context: Pagination - current page indicator",
|
|
153
|
+
values: { page: String(currentPage) },
|
|
154
|
+
});
|
|
158
155
|
|
|
159
156
|
return (
|
|
160
157
|
<nav class="flex items-center justify-between py-4" aria-label="Pagination">
|
|
161
158
|
<div>
|
|
162
159
|
{hasPrev ? (
|
|
163
|
-
<a
|
|
164
|
-
href={buildUrl(currentPage - 1)}
|
|
165
|
-
class="btn-outline text-sm"
|
|
166
|
-
>
|
|
160
|
+
<a href={buildUrl(currentPage - 1)} class="btn-outline text-sm">
|
|
167
161
|
← {prevText}
|
|
168
162
|
</a>
|
|
169
163
|
) : (
|
|
170
|
-
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
171
|
-
← {prevText}
|
|
172
|
-
</span>
|
|
164
|
+
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">← {prevText}</span>
|
|
173
165
|
)}
|
|
174
166
|
</div>
|
|
175
167
|
|
|
176
|
-
<span class="text-sm text-muted-foreground">
|
|
177
|
-
{pageText}
|
|
178
|
-
</span>
|
|
168
|
+
<span class="text-sm text-muted-foreground">{pageText}</span>
|
|
179
169
|
|
|
180
170
|
<div>
|
|
181
171
|
{hasNext ? (
|
|
182
|
-
<a
|
|
183
|
-
href={buildUrl(currentPage + 1)}
|
|
184
|
-
class="btn-outline text-sm"
|
|
185
|
-
>
|
|
172
|
+
<a href={buildUrl(currentPage + 1)} class="btn-outline text-sm">
|
|
186
173
|
{nextText} →
|
|
187
174
|
</a>
|
|
188
175
|
) : (
|
|
189
|
-
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">
|
|
190
|
-
{nextText} →
|
|
191
|
-
</span>
|
|
176
|
+
<span class="btn-outline text-sm opacity-50 cursor-not-allowed">{nextText} →</span>
|
|
192
177
|
)}
|
|
193
178
|
</div>
|
|
194
179
|
</nav>
|
|
@@ -20,7 +20,9 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
20
20
|
<form method={method} action={action} class="flex flex-col gap-4">
|
|
21
21
|
{/* Type selector */}
|
|
22
22
|
<div class="field">
|
|
23
|
-
<label class="label">
|
|
23
|
+
<label class="label">
|
|
24
|
+
{t({ message: "Type", comment: "@context: Post form field - post type" })}
|
|
25
|
+
</label>
|
|
24
26
|
<select name="type" class="select" required>
|
|
25
27
|
<option value="note" selected={post?.type === "note"}>
|
|
26
28
|
{t({ message: "Note", comment: "@context: Post type option" })}
|
|
@@ -42,7 +44,9 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
42
44
|
|
|
43
45
|
{/* Title (optional) */}
|
|
44
46
|
<div class="field">
|
|
45
|
-
<label class="label">
|
|
47
|
+
<label class="label">
|
|
48
|
+
{t({ message: "Title (optional)", comment: "@context: Post form field" })}
|
|
49
|
+
</label>
|
|
46
50
|
<input
|
|
47
51
|
type="text"
|
|
48
52
|
name="title"
|
|
@@ -54,11 +58,16 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
54
58
|
|
|
55
59
|
{/* Content */}
|
|
56
60
|
<div class="field">
|
|
57
|
-
<label class="label">
|
|
61
|
+
<label class="label">
|
|
62
|
+
{t({ message: "Content", comment: "@context: Post form field" })}
|
|
63
|
+
</label>
|
|
58
64
|
<textarea
|
|
59
65
|
name="content"
|
|
60
66
|
class="textarea min-h-32"
|
|
61
|
-
placeholder={t({
|
|
67
|
+
placeholder={t({
|
|
68
|
+
message: "What's on your mind?",
|
|
69
|
+
comment: "@context: Post content placeholder",
|
|
70
|
+
})}
|
|
62
71
|
required
|
|
63
72
|
>
|
|
64
73
|
{post?.content ?? ""}
|
|
@@ -67,7 +76,9 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
67
76
|
|
|
68
77
|
{/* Source URL (for link/quote types) */}
|
|
69
78
|
<div class="field">
|
|
70
|
-
<label class="label">
|
|
79
|
+
<label class="label">
|
|
80
|
+
{t({ message: "Source URL (optional)", comment: "@context: Post form field" })}
|
|
81
|
+
</label>
|
|
71
82
|
<input
|
|
72
83
|
type="url"
|
|
73
84
|
name="sourceUrl"
|
|
@@ -79,7 +90,9 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
79
90
|
|
|
80
91
|
{/* Visibility */}
|
|
81
92
|
<div class="field">
|
|
82
|
-
<label class="label">
|
|
93
|
+
<label class="label">
|
|
94
|
+
{t({ message: "Visibility", comment: "@context: Post form field" })}
|
|
95
|
+
</label>
|
|
83
96
|
<select name="visibility" class="select">
|
|
84
97
|
<option value="quiet" selected={post?.visibility === "quiet" || !post}>
|
|
85
98
|
{t({ message: "Quiet (normal)", comment: "@context: Post visibility option" })}
|
|
@@ -98,7 +111,9 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
98
111
|
|
|
99
112
|
{/* Custom path (optional) */}
|
|
100
113
|
<div class="field">
|
|
101
|
-
<label class="label">
|
|
114
|
+
<label class="label">
|
|
115
|
+
{t({ message: "Custom Path (optional)", comment: "@context: Post form field" })}
|
|
116
|
+
</label>
|
|
102
117
|
<input
|
|
103
118
|
type="text"
|
|
104
119
|
name="path"
|
|
@@ -111,7 +126,9 @@ export const PostForm: FC<PostFormProps> = ({ post, action, method = "post" }) =
|
|
|
111
126
|
{/* Submit */}
|
|
112
127
|
<div class="flex gap-2">
|
|
113
128
|
<button type="submit" class="btn">
|
|
114
|
-
{isEdit
|
|
129
|
+
{isEdit
|
|
130
|
+
? t({ message: "Update", comment: "@context: Button to update existing post" })
|
|
131
|
+
: t({ message: "Publish", comment: "@context: Button to publish new post" })}
|
|
115
132
|
</button>
|
|
116
133
|
<a href="/dash/posts" class="btn-outline">
|
|
117
134
|
{t({ message: "Cancel", comment: "@context: Button to cancel form" })}
|
|
@@ -22,8 +22,14 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
|
22
22
|
if (posts.length === 0) {
|
|
23
23
|
return (
|
|
24
24
|
<EmptyState
|
|
25
|
-
message={t({
|
|
26
|
-
|
|
25
|
+
message={t({
|
|
26
|
+
message: "No posts yet.",
|
|
27
|
+
comment: "@context: Empty state message when no posts exist",
|
|
28
|
+
})}
|
|
29
|
+
ctaText={t({
|
|
30
|
+
message: "Create your first post",
|
|
31
|
+
comment: "@context: Button in empty state to create first post",
|
|
32
|
+
})}
|
|
27
33
|
ctaHref="/dash/posts/new"
|
|
28
34
|
/>
|
|
29
35
|
);
|
|
@@ -39,22 +45,22 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
|
39
45
|
editHref={`/dash/posts/${sqid.encode(post.id)}/edit`}
|
|
40
46
|
editLabel={t({ message: "Edit", comment: "@context: Button to edit post" })}
|
|
41
47
|
viewHref={`/p/${sqid.encode(post.id)}`}
|
|
42
|
-
viewLabel={t({
|
|
48
|
+
viewLabel={t({
|
|
49
|
+
message: "View",
|
|
50
|
+
comment: "@context: Button to view post on public site",
|
|
51
|
+
})}
|
|
43
52
|
/>
|
|
44
53
|
}
|
|
45
54
|
>
|
|
46
55
|
<div class="flex items-center gap-2 mb-1">
|
|
47
56
|
<TypeBadge type={post.type} />
|
|
48
57
|
<VisibilityBadge visibility={post.visibility} />
|
|
49
|
-
<span class="text-xs text-muted-foreground">
|
|
50
|
-
{time.formatDate(post.publishedAt)}
|
|
51
|
-
</span>
|
|
58
|
+
<span class="text-xs text-muted-foreground">{time.formatDate(post.publishedAt)}</span>
|
|
52
59
|
</div>
|
|
53
|
-
<a
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{post.title || post.content?.slice(0, 60) || t({ message: "Untitled", comment: "@context: Default title for untitled post" })}
|
|
60
|
+
<a href={`/dash/posts/${sqid.encode(post.id)}`} class="font-medium hover:underline">
|
|
61
|
+
{post.title ||
|
|
62
|
+
post.content?.slice(0, 60) ||
|
|
63
|
+
t({ message: "Untitled", comment: "@context: Default title for untitled post" })}
|
|
58
64
|
</a>
|
|
59
65
|
{post.content && !post.title && (
|
|
60
66
|
<p class="text-sm text-muted-foreground mt-1 line-clamp-2">
|
|
@@ -51,14 +51,14 @@ const ThreadPost: FC<{
|
|
|
51
51
|
</time>
|
|
52
52
|
{isRoot && (
|
|
53
53
|
<span class="text-xs">
|
|
54
|
-
{t({
|
|
54
|
+
{t({
|
|
55
|
+
message: "Thread start",
|
|
56
|
+
comment: "@context: Thread view indicator - first post in thread",
|
|
57
|
+
})}
|
|
55
58
|
</span>
|
|
56
59
|
)}
|
|
57
60
|
{!isCurrent && (
|
|
58
|
-
<a
|
|
59
|
-
href={`/p/${sqid.encode(post.id)}`}
|
|
60
|
-
class="text-xs hover:underline"
|
|
61
|
-
>
|
|
61
|
+
<a href={`/p/${sqid.encode(post.id)}`} class="text-xs hover:underline">
|
|
62
62
|
{t({ message: "Permalink", comment: "@context: Link to individual post in thread" })}
|
|
63
63
|
</a>
|
|
64
64
|
)}
|
|
@@ -84,32 +84,29 @@ export const ThreadView: FC<ThreadViewProps> = ({ posts, currentPostId }) => {
|
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
const threadLabel =
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
const threadLabel =
|
|
88
|
+
posts.length === 1
|
|
89
|
+
? t({ message: "Thread with 1 post", comment: "@context: Thread view header - single post" })
|
|
90
|
+
: t({
|
|
91
|
+
message: "Thread with {count} posts",
|
|
92
|
+
comment: "@context: Thread view header - multiple posts",
|
|
93
|
+
values: { count: String(posts.length) },
|
|
94
|
+
});
|
|
90
95
|
|
|
91
96
|
return (
|
|
92
97
|
<div class="thread-view">
|
|
93
|
-
<div class="mb-4 text-sm text-muted-foreground">
|
|
94
|
-
{threadLabel}
|
|
95
|
-
</div>
|
|
98
|
+
<div class="mb-4 text-sm text-muted-foreground">{threadLabel}</div>
|
|
96
99
|
|
|
97
100
|
<div class="flex flex-col gap-3">
|
|
98
101
|
{posts.map((post, index) => (
|
|
99
102
|
<div key={post.id} class="relative">
|
|
100
103
|
{/* Connection line */}
|
|
101
|
-
{index > 0 &&
|
|
102
|
-
<div class="absolute left-6 -top-3 w-0.5 h-3 bg-border" />
|
|
103
|
-
)}
|
|
104
|
+
{index > 0 && <div class="absolute left-6 -top-3 w-0.5 h-3 bg-border" />}
|
|
104
105
|
{index < posts.length - 1 && (
|
|
105
106
|
<div class="absolute left-6 -bottom-3 w-0.5 h-3 bg-border" />
|
|
106
107
|
)}
|
|
107
108
|
|
|
108
|
-
<ThreadPost
|
|
109
|
-
post={post}
|
|
110
|
-
isCurrent={post.id === currentPostId}
|
|
111
|
-
isRoot={index === 0}
|
|
112
|
-
/>
|
|
109
|
+
<ThreadPost post={post} isCurrent={post.id === currentPostId} isRoot={index === 0} />
|
|
113
110
|
</div>
|
|
114
111
|
))}
|
|
115
112
|
</div>
|
|
@@ -4,7 +4,14 @@ export { DangerZone, type DangerZoneProps } from "./DangerZone.js";
|
|
|
4
4
|
export { EmptyState, type EmptyStateProps } from "./EmptyState.js";
|
|
5
5
|
export { ListItemRow, type ListItemRowProps } from "./ListItemRow.js";
|
|
6
6
|
export { PageForm, type PageFormProps } from "./PageForm.js";
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
Pagination,
|
|
9
|
+
LoadMore,
|
|
10
|
+
PagePagination,
|
|
11
|
+
type PaginationProps,
|
|
12
|
+
type LoadMoreProps,
|
|
13
|
+
type PagePaginationProps,
|
|
14
|
+
} from "./Pagination.js";
|
|
8
15
|
export { PostForm, type PostFormProps } from "./PostForm.js";
|
|
9
16
|
export { PostList, type PostListProps } from "./PostList.js";
|
|
10
17
|
export { ThreadView, type ThreadViewProps } from "./ThreadView.js";
|
|
@@ -40,9 +40,7 @@ export const BaseLayout: FC<PropsWithChildren<BaseLayoutProps>> = ({
|
|
|
40
40
|
<Link href="/src/style.css" rel="stylesheet" />
|
|
41
41
|
<Script src="/src/client.ts" />
|
|
42
42
|
</head>
|
|
43
|
-
<body class="bg-background text-foreground antialiased">
|
|
44
|
-
{content}
|
|
45
|
-
</body>
|
|
43
|
+
<body class="bg-background text-foreground antialiased">{content}</body>
|
|
46
44
|
</html>
|
|
47
45
|
);
|
|
48
46
|
};
|
|
@@ -46,7 +46,10 @@ function DashLayoutContent({
|
|
|
46
46
|
</a>
|
|
47
47
|
<nav class="flex items-center gap-4">
|
|
48
48
|
<a href="/" class="text-sm text-muted-foreground hover:text-foreground">
|
|
49
|
-
{t({
|
|
49
|
+
{t({
|
|
50
|
+
message: "View Site",
|
|
51
|
+
comment: "@context: Dashboard header link to view the public site",
|
|
52
|
+
})}
|
|
50
53
|
</a>
|
|
51
54
|
<a href="/signout" class="text-sm text-muted-foreground hover:text-foreground">
|
|
52
55
|
{t({ message: "Sign Out", comment: "@context: Dashboard header link to sign out" })}
|
|
@@ -61,25 +64,46 @@ function DashLayoutContent({
|
|
|
61
64
|
<aside class="w-48 shrink-0">
|
|
62
65
|
<nav class="flex flex-col gap-1">
|
|
63
66
|
<a href="/dash" class={navClass("/dash", /^\/dash$/)}>
|
|
64
|
-
{t({
|
|
67
|
+
{t({
|
|
68
|
+
message: "Dashboard",
|
|
69
|
+
comment: "@context: Dashboard navigation - main dashboard page",
|
|
70
|
+
})}
|
|
65
71
|
</a>
|
|
66
72
|
<a href="/dash/posts" class={navClass("/dash/posts", /^\/dash\/posts/)}>
|
|
67
|
-
{t({
|
|
73
|
+
{t({
|
|
74
|
+
message: "Posts",
|
|
75
|
+
comment: "@context: Dashboard navigation - posts management",
|
|
76
|
+
})}
|
|
68
77
|
</a>
|
|
69
78
|
<a href="/dash/pages" class={navClass("/dash/pages", /^\/dash\/pages/)}>
|
|
70
|
-
{t({
|
|
79
|
+
{t({
|
|
80
|
+
message: "Pages",
|
|
81
|
+
comment: "@context: Dashboard navigation - pages management",
|
|
82
|
+
})}
|
|
71
83
|
</a>
|
|
72
84
|
<a href="/dash/media" class={navClass("/dash/media", /^\/dash\/media/)}>
|
|
73
85
|
{t({ message: "Media", comment: "@context: Dashboard navigation - media library" })}
|
|
74
86
|
</a>
|
|
75
|
-
<a
|
|
76
|
-
|
|
87
|
+
<a
|
|
88
|
+
href="/dash/collections"
|
|
89
|
+
class={navClass("/dash/collections", /^\/dash\/collections/)}
|
|
90
|
+
>
|
|
91
|
+
{t({
|
|
92
|
+
message: "Collections",
|
|
93
|
+
comment: "@context: Dashboard navigation - collections management",
|
|
94
|
+
})}
|
|
77
95
|
</a>
|
|
78
96
|
<a href="/dash/redirects" class={navClass("/dash/redirects", /^\/dash\/redirects/)}>
|
|
79
|
-
{t({
|
|
97
|
+
{t({
|
|
98
|
+
message: "Redirects",
|
|
99
|
+
comment: "@context: Dashboard navigation - URL redirects",
|
|
100
|
+
})}
|
|
80
101
|
</a>
|
|
81
102
|
<a href="/dash/settings" class={navClass("/dash/settings", /^\/dash\/settings/)}>
|
|
82
|
-
{t({
|
|
103
|
+
{t({
|
|
104
|
+
message: "Settings",
|
|
105
|
+
comment: "@context: Dashboard navigation - site settings",
|
|
106
|
+
})}
|
|
83
107
|
</a>
|
|
84
108
|
</nav>
|
|
85
109
|
</aside>
|
package/src/types.ts
CHANGED
|
@@ -12,7 +12,6 @@ export type PostType = (typeof POST_TYPES)[number];
|
|
|
12
12
|
export const VISIBILITY_LEVELS = ["featured", "quiet", "unlisted", "draft"] as const;
|
|
13
13
|
export type Visibility = (typeof VISIBILITY_LEVELS)[number];
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
// =============================================================================
|
|
17
16
|
// Cloudflare Bindings
|
|
18
17
|
// =============================================================================
|
|
@@ -26,7 +25,6 @@ export interface Bindings {
|
|
|
26
25
|
IMAGE_TRANSFORM_URL?: string;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
|
|
30
28
|
// =============================================================================
|
|
31
29
|
// Entity Types
|
|
32
30
|
// =============================================================================
|
package/dist/plugin.d.ts
DELETED
package/dist/plugin.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAiB1C,eAAO,MAAM,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAO7D,CAAC"}
|