@jant/core 0.3.6 → 0.3.7

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 (81) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +7 -21
  3. package/dist/db/schema.d.ts +36 -0
  4. package/dist/db/schema.d.ts.map +1 -1
  5. package/dist/db/schema.js +2 -0
  6. package/dist/i18n/locales/en.d.ts.map +1 -1
  7. package/dist/i18n/locales/en.js +1 -1
  8. package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
  9. package/dist/i18n/locales/zh-Hans.js +1 -1
  10. package/dist/i18n/locales/zh-Hant.d.ts.map +1 -1
  11. package/dist/i18n/locales/zh-Hant.js +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/lib/schemas.d.ts +17 -0
  16. package/dist/lib/schemas.d.ts.map +1 -1
  17. package/dist/lib/schemas.js +32 -2
  18. package/dist/lib/sse.d.ts +3 -3
  19. package/dist/lib/sse.d.ts.map +1 -1
  20. package/dist/lib/sse.js +7 -8
  21. package/dist/routes/api/posts.d.ts.map +1 -1
  22. package/dist/routes/api/posts.js +101 -5
  23. package/dist/routes/dash/media.js +38 -0
  24. package/dist/routes/dash/posts.d.ts.map +1 -1
  25. package/dist/routes/dash/posts.js +45 -6
  26. package/dist/routes/feed/rss.d.ts.map +1 -1
  27. package/dist/routes/feed/rss.js +10 -1
  28. package/dist/routes/pages/home.d.ts.map +1 -1
  29. package/dist/routes/pages/home.js +37 -4
  30. package/dist/routes/pages/post.d.ts.map +1 -1
  31. package/dist/routes/pages/post.js +28 -2
  32. package/dist/services/collection.d.ts +1 -0
  33. package/dist/services/collection.d.ts.map +1 -1
  34. package/dist/services/collection.js +13 -0
  35. package/dist/services/media.d.ts +7 -0
  36. package/dist/services/media.d.ts.map +1 -1
  37. package/dist/services/media.js +54 -1
  38. package/dist/theme/components/MediaGallery.d.ts +13 -0
  39. package/dist/theme/components/MediaGallery.d.ts.map +1 -0
  40. package/dist/theme/components/MediaGallery.js +107 -0
  41. package/dist/theme/components/PostForm.d.ts +6 -1
  42. package/dist/theme/components/PostForm.d.ts.map +1 -1
  43. package/dist/theme/components/PostForm.js +158 -2
  44. package/dist/theme/components/index.d.ts +1 -0
  45. package/dist/theme/components/index.d.ts.map +1 -1
  46. package/dist/theme/components/index.js +1 -0
  47. package/dist/types.d.ts +24 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types.js +27 -0
  50. package/package.json +1 -1
  51. package/src/__tests__/helpers/app.ts +6 -1
  52. package/src/__tests__/helpers/db.ts +10 -0
  53. package/src/app.tsx +7 -25
  54. package/src/db/migrations/0002_add_media_attachments.sql +3 -0
  55. package/src/db/schema.ts +2 -0
  56. package/src/i18n/locales/en.po +81 -37
  57. package/src/i18n/locales/en.ts +1 -1
  58. package/src/i18n/locales/zh-Hans.po +81 -37
  59. package/src/i18n/locales/zh-Hans.ts +1 -1
  60. package/src/i18n/locales/zh-Hant.po +81 -37
  61. package/src/i18n/locales/zh-Hant.ts +1 -1
  62. package/src/index.ts +8 -1
  63. package/src/lib/__tests__/schemas.test.ts +89 -1
  64. package/src/lib/__tests__/sse.test.ts +13 -1
  65. package/src/lib/schemas.ts +47 -1
  66. package/src/lib/sse.ts +10 -11
  67. package/src/routes/api/__tests__/posts.test.ts +239 -0
  68. package/src/routes/api/posts.ts +134 -5
  69. package/src/routes/dash/media.tsx +50 -0
  70. package/src/routes/dash/posts.tsx +79 -7
  71. package/src/routes/feed/rss.ts +14 -1
  72. package/src/routes/pages/home.tsx +80 -36
  73. package/src/routes/pages/post.tsx +36 -3
  74. package/src/services/__tests__/collection.test.ts +102 -0
  75. package/src/services/__tests__/media.test.ts +248 -0
  76. package/src/services/collection.ts +19 -0
  77. package/src/services/media.ts +76 -1
  78. package/src/theme/components/MediaGallery.tsx +128 -0
  79. package/src/theme/components/PostForm.tsx +170 -2
  80. package/src/theme/components/index.ts +1 -0
  81. package/src/types.ts +36 -0
@@ -2,7 +2,7 @@
2
2
  * Media Service
3
3
  *
4
4
  * Handles media upload and management with R2 storage
5
- */ import { eq, desc } from "drizzle-orm";
5
+ */ import { eq, desc, inArray, asc } from "drizzle-orm";
6
6
  import { uuidv7 } from "uuidv7";
7
7
  import { media } from "../db/schema.js";
8
8
  import { now } from "../lib/time.js";
@@ -19,6 +19,8 @@ export function createMediaService(db) {
19
19
  width: row.width,
20
20
  height: row.height,
21
21
  alt: row.alt,
22
+ position: row.position,
23
+ blurhash: row.blurhash,
22
24
  createdAt: row.createdAt
23
25
  };
24
26
  }
@@ -27,6 +29,33 @@ export function createMediaService(db) {
27
29
  const result = await db.select().from(media).where(eq(media.id, id)).limit(1);
28
30
  return result[0] ? toMedia(result[0]) : null;
29
31
  },
32
+ async getByIds (ids) {
33
+ if (ids.length === 0) return [];
34
+ const rows = await db.select().from(media).where(inArray(media.id, ids));
35
+ return rows.map(toMedia);
36
+ },
37
+ async getByPostId (postId) {
38
+ const rows = await db.select().from(media).where(eq(media.postId, postId)).orderBy(asc(media.position));
39
+ return rows.map(toMedia);
40
+ },
41
+ async getByPostIds (postIds) {
42
+ const result = new Map();
43
+ if (postIds.length === 0) return result;
44
+ const rows = await db.select().from(media).where(inArray(media.postId, postIds)).orderBy(asc(media.position));
45
+ for (const row of rows){
46
+ const m = toMedia(row);
47
+ if (m.postId === null) continue;
48
+ const list = result.get(m.postId);
49
+ if (list) {
50
+ list.push(m);
51
+ } else {
52
+ result.set(m.postId, [
53
+ m
54
+ ]);
55
+ }
56
+ }
57
+ return result;
58
+ },
30
59
  async getByR2Key (r2Key) {
31
60
  const result = await db.select().from(media).where(eq(media.r2Key, r2Key)).limit(1);
32
61
  return result[0] ? toMedia(result[0]) : null;
@@ -49,11 +78,35 @@ export function createMediaService(db) {
49
78
  width: data.width ?? null,
50
79
  height: data.height ?? null,
51
80
  alt: data.alt ?? null,
81
+ position: data.position ?? 0,
82
+ blurhash: data.blurhash ?? null,
52
83
  createdAt: timestamp
53
84
  }).returning();
54
85
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
55
86
  return toMedia(result[0]);
56
87
  },
88
+ async attachToPost (postId, mediaIds) {
89
+ // Clear existing attachments
90
+ await db.update(media).set({
91
+ postId: null,
92
+ position: 0
93
+ }).where(eq(media.postId, postId));
94
+ // Set new attachments with position = array index
95
+ for(let i = 0; i < mediaIds.length; i++){
96
+ const mediaId = mediaIds[i];
97
+ if (!mediaId) continue;
98
+ await db.update(media).set({
99
+ postId,
100
+ position: i
101
+ }).where(eq(media.id, mediaId));
102
+ }
103
+ },
104
+ async detachFromPost (postId) {
105
+ await db.update(media).set({
106
+ postId: null,
107
+ position: 0
108
+ }).where(eq(media.postId, postId));
109
+ },
57
110
  async delete (id) {
58
111
  const result = await db.delete(media).where(eq(media.id, id)).returning();
59
112
  return result.length > 0;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Media Gallery Component
3
+ *
4
+ * Renders media attachments on public post pages.
5
+ * Layout adapts based on the number of images.
6
+ */
7
+ import type { FC } from "hono/jsx";
8
+ import type { MediaAttachment } from "../../types.js";
9
+ export interface MediaGalleryProps {
10
+ attachments: MediaAttachment[];
11
+ }
12
+ export declare const MediaGallery: FC<MediaGalleryProps>;
13
+ //# sourceMappingURL=MediaGallery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaGallery.d.ts","sourceRoot":"","sources":["../../../src/theme/components/MediaGallery.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAiH9C,CAAC"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Media Gallery Component
3
+ *
4
+ * Renders media attachments on public post pages.
5
+ * Layout adapts based on the number of images.
6
+ */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
7
+ export const MediaGallery = ({ attachments })=>{
8
+ const images = attachments.filter((a)=>a.mimeType.startsWith("image/"));
9
+ if (images.length === 0) return null;
10
+ if (images.length === 1) {
11
+ const [img] = images;
12
+ if (!img) return null;
13
+ return /*#__PURE__*/ _jsx("div", {
14
+ class: "mt-3",
15
+ children: /*#__PURE__*/ _jsx("a", {
16
+ href: img.url,
17
+ target: "_blank",
18
+ rel: "noopener noreferrer",
19
+ children: /*#__PURE__*/ _jsx("img", {
20
+ src: img.previewUrl,
21
+ alt: img.alt || "",
22
+ width: img.width ?? undefined,
23
+ height: img.height ?? undefined,
24
+ class: "rounded-lg max-w-full h-auto",
25
+ loading: "lazy"
26
+ })
27
+ })
28
+ });
29
+ }
30
+ if (images.length === 2) {
31
+ return /*#__PURE__*/ _jsx("div", {
32
+ class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
33
+ children: images.map((img)=>/*#__PURE__*/ _jsx("a", {
34
+ href: img.url,
35
+ target: "_blank",
36
+ rel: "noopener noreferrer",
37
+ class: "aspect-square",
38
+ children: /*#__PURE__*/ _jsx("img", {
39
+ src: img.previewUrl,
40
+ alt: img.alt || "",
41
+ class: "w-full h-full object-cover",
42
+ loading: "lazy"
43
+ })
44
+ }, img.id))
45
+ });
46
+ }
47
+ if (images.length === 3) {
48
+ const [first, ...rest] = images;
49
+ if (!first) return null;
50
+ return /*#__PURE__*/ _jsxs("div", {
51
+ class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
52
+ children: [
53
+ /*#__PURE__*/ _jsx("a", {
54
+ href: first.url,
55
+ target: "_blank",
56
+ rel: "noopener noreferrer",
57
+ class: "row-span-2",
58
+ children: /*#__PURE__*/ _jsx("img", {
59
+ src: first.previewUrl,
60
+ alt: first.alt || "",
61
+ class: "w-full h-full object-cover",
62
+ loading: "lazy"
63
+ })
64
+ }),
65
+ rest.map((img)=>/*#__PURE__*/ _jsx("a", {
66
+ href: img.url,
67
+ target: "_blank",
68
+ rel: "noopener noreferrer",
69
+ class: "aspect-square",
70
+ children: /*#__PURE__*/ _jsx("img", {
71
+ src: img.previewUrl,
72
+ alt: img.alt || "",
73
+ class: "w-full h-full object-cover",
74
+ loading: "lazy"
75
+ })
76
+ }, img.id))
77
+ ]
78
+ });
79
+ }
80
+ // 4+ images: 2-column grid, show first 4 with remaining count
81
+ const shown = images.slice(0, 4);
82
+ const remaining = images.length - 4;
83
+ return /*#__PURE__*/ _jsx("div", {
84
+ class: "mt-3 grid grid-cols-2 gap-1 rounded-lg overflow-hidden",
85
+ children: shown.map((img, i)=>/*#__PURE__*/ _jsxs("a", {
86
+ href: img.url,
87
+ target: "_blank",
88
+ rel: "noopener noreferrer",
89
+ class: "relative aspect-square",
90
+ children: [
91
+ /*#__PURE__*/ _jsx("img", {
92
+ src: img.previewUrl,
93
+ alt: img.alt || "",
94
+ class: "w-full h-full object-cover",
95
+ loading: "lazy"
96
+ }),
97
+ i === 3 && remaining > 0 && /*#__PURE__*/ _jsxs("div", {
98
+ class: "absolute inset-0 bg-black/50 flex items-center justify-center text-white text-xl font-semibold",
99
+ children: [
100
+ "+",
101
+ remaining
102
+ ]
103
+ })
104
+ ]
105
+ }, img.id))
106
+ });
107
+ };
@@ -2,10 +2,15 @@
2
2
  * Post Creation/Edit Form
3
3
  */
4
4
  import type { FC } from "hono/jsx";
5
- import type { Post } from "../../types.js";
5
+ import type { Post, Media, Collection } from "../../types.js";
6
6
  export interface PostFormProps {
7
7
  post?: Post;
8
8
  action: string;
9
+ mediaAttachments?: Media[];
10
+ r2PublicUrl?: string;
11
+ imageTransformUrl?: string;
12
+ collections?: Collection[];
13
+ postCollectionIds?: number[];
9
14
  }
10
15
  export declare const PostForm: FC<PostFormProps>;
11
16
  //# sourceMappingURL=PostForm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PostForm.d.ts","sourceRoot":"","sources":["../../../src/theme/components/PostForm.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAG3C,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CA4KtC,CAAC"}
1
+ {"version":3,"file":"PostForm.d.ts","sourceRoot":"","sources":["../../../src/theme/components/PostForm.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAI9D,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,KAAK,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CA8UtC,CAAC"}
@@ -2,16 +2,21 @@
2
2
  * Post Creation/Edit Form
3
3
  */ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
4
4
  import { useLingui as $_useLingui } from "@jant/core/i18n";
5
- export const PostForm = ({ post, action })=>{
5
+ import { getMediaUrl, getImageUrl } from "../../lib/image.js";
6
+ export const PostForm = ({ post, action, mediaAttachments, r2PublicUrl, imageTransformUrl, collections, postCollectionIds })=>{
6
7
  const { i18n: $__i18n, _: $__ } = $_useLingui();
7
8
  const isEdit = !!post;
9
+ const existingMediaIds = (mediaAttachments ?? []).map((m)=>m.id);
8
10
  const signals = JSON.stringify({
9
11
  type: post?.type ?? "note",
10
12
  title: post?.title ?? "",
11
13
  content: post?.content ?? "",
12
14
  sourceUrl: post?.sourceUrl ?? "",
15
+ sourceName: post?.sourceName ?? "",
13
16
  visibility: post?.visibility ?? "quiet",
14
- path: post?.path ?? ""
17
+ path: post?.path ?? "",
18
+ mediaIds: existingMediaIds,
19
+ collectionIds: postCollectionIds ?? []
15
20
  }).replace(/</g, "\\u003c");
16
21
  return /*#__PURE__*/ _jsxs("form", {
17
22
  "data-signals": signals,
@@ -123,6 +128,70 @@ export const PostForm = ({ post, action })=>{
123
128
  })
124
129
  ]
125
130
  }),
131
+ /*#__PURE__*/ _jsxs("div", {
132
+ class: "field",
133
+ "data-show": "$type !== 'page'",
134
+ children: [
135
+ /*#__PURE__*/ _jsx("label", {
136
+ class: "label",
137
+ children: $__i18n._({
138
+ id: "xYilR2",
139
+ message: "Media"
140
+ })
141
+ }),
142
+ /*#__PURE__*/ _jsx("p", {
143
+ class: "text-xs text-muted-foreground mb-2",
144
+ "data-show": "$type === 'image'",
145
+ children: $__i18n._({
146
+ id: "I8hDlV",
147
+ message: "At least 1 image required for image posts."
148
+ })
149
+ }),
150
+ mediaAttachments && mediaAttachments.length > 0 && /*#__PURE__*/ _jsx("div", {
151
+ class: "grid grid-cols-4 sm:grid-cols-6 gap-2 mb-2",
152
+ children: mediaAttachments.map((m)=>{
153
+ const url = getMediaUrl(m.id, m.r2Key, r2PublicUrl);
154
+ const thumbUrl = getImageUrl(url, imageTransformUrl, {
155
+ width: 150,
156
+ quality: 80,
157
+ format: "auto",
158
+ fit: "cover"
159
+ });
160
+ return /*#__PURE__*/ _jsxs("div", {
161
+ class: "relative group aspect-square",
162
+ "data-show": `$mediaIds.includes('${m.id}')`,
163
+ children: [
164
+ /*#__PURE__*/ _jsx("img", {
165
+ src: thumbUrl,
166
+ alt: m.alt || m.originalName,
167
+ class: "w-full h-full object-cover rounded-lg border",
168
+ loading: "lazy"
169
+ }),
170
+ /*#__PURE__*/ _jsx("button", {
171
+ type: "button",
172
+ class: "absolute top-1 right-1 w-5 h-5 flex items-center justify-center bg-black/60 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer",
173
+ "data-on:click": `$mediaIds = $mediaIds.filter(id => id !== '${m.id}')`,
174
+ title: $__i18n._({
175
+ id: "t/YqKh",
176
+ message: "Remove"
177
+ }),
178
+ children: "×"
179
+ })
180
+ ]
181
+ }, m.id);
182
+ })
183
+ }),
184
+ /*#__PURE__*/ _jsx("button", {
185
+ type: "button",
186
+ class: "btn-outline text-sm",
187
+ "data-on:click": "document.getElementById('media-picker-dialog').showModal(); fetch('/dash/media/picker').then(r => r.text()).then(html => document.getElementById('media-picker-grid').innerHTML = html)",
188
+ children: $__i18n._({
189
+ id: "qiXmlF",
190
+ message: "Add Media"
191
+ })
192
+ })
193
+ ]
194
+ }),
126
195
  /*#__PURE__*/ _jsxs("div", {
127
196
  class: "field",
128
197
  children: [
@@ -141,6 +210,27 @@ export const PostForm = ({ post, action })=>{
141
210
  })
142
211
  ]
143
212
  }),
213
+ /*#__PURE__*/ _jsxs("div", {
214
+ class: "field",
215
+ children: [
216
+ /*#__PURE__*/ _jsx("label", {
217
+ class: "label",
218
+ children: $__i18n._({
219
+ id: "oJFOZk",
220
+ message: "Source Name (optional)"
221
+ })
222
+ }),
223
+ /*#__PURE__*/ _jsx("input", {
224
+ type: "text",
225
+ "data-bind": "sourceName",
226
+ class: "input",
227
+ placeholder: $__i18n._({
228
+ id: "1o+wgo",
229
+ message: "e.g. The Verge, John Doe"
230
+ })
231
+ })
232
+ ]
233
+ }),
144
234
  /*#__PURE__*/ _jsxs("div", {
145
235
  class: "field",
146
236
  children: [
@@ -191,6 +281,33 @@ export const PostForm = ({ post, action })=>{
191
281
  })
192
282
  ]
193
283
  }),
284
+ collections && collections.length > 0 && /*#__PURE__*/ _jsxs("fieldset", {
285
+ class: "field",
286
+ children: [
287
+ /*#__PURE__*/ _jsx("legend", {
288
+ class: "label",
289
+ children: $__i18n._({
290
+ id: "MWBOxm",
291
+ message: "Collections (optional)"
292
+ })
293
+ }),
294
+ /*#__PURE__*/ _jsx("div", {
295
+ class: "flex flex-col gap-1",
296
+ children: collections.map((col)=>/*#__PURE__*/ _jsxs("label", {
297
+ class: "flex items-center gap-2 text-sm",
298
+ children: [
299
+ /*#__PURE__*/ _jsx("input", {
300
+ type: "checkbox",
301
+ class: "checkbox",
302
+ "data-attr:checked": `$collectionIds.includes(${col.id})`,
303
+ "data-on:change": `$collectionIds.includes(${col.id}) ? $collectionIds = $collectionIds.filter(id => id !== ${col.id}) : $collectionIds = [...$collectionIds, ${col.id}]`
304
+ }),
305
+ col.title
306
+ ]
307
+ }, col.id))
308
+ })
309
+ ]
310
+ }),
194
311
  /*#__PURE__*/ _jsxs("div", {
195
312
  class: "field",
196
313
  children: [
@@ -232,6 +349,45 @@ export const PostForm = ({ post, action })=>{
232
349
  })
233
350
  })
234
351
  ]
352
+ }),
353
+ /*#__PURE__*/ _jsxs("dialog", {
354
+ id: "media-picker-dialog",
355
+ class: "p-6 rounded-lg max-w-2xl w-full backdrop:bg-black/50",
356
+ onclick: "event.target === this && this.close()",
357
+ children: [
358
+ /*#__PURE__*/ _jsxs("div", {
359
+ class: "flex items-center justify-between mb-4",
360
+ children: [
361
+ /*#__PURE__*/ _jsx("h2", {
362
+ class: "text-lg font-semibold",
363
+ children: $__i18n._({
364
+ id: "2fUwEY",
365
+ message: "Select Media"
366
+ })
367
+ }),
368
+ /*#__PURE__*/ _jsx("button", {
369
+ type: "button",
370
+ class: "btn-outline text-sm",
371
+ onclick: "this.closest('dialog').close()",
372
+ children: $__i18n._({
373
+ id: "DPfwMq",
374
+ message: "Done"
375
+ })
376
+ })
377
+ ]
378
+ }),
379
+ /*#__PURE__*/ _jsx("div", {
380
+ id: "media-picker-grid",
381
+ class: "grid grid-cols-4 gap-2 max-h-96 overflow-y-auto",
382
+ children: /*#__PURE__*/ _jsx("p", {
383
+ class: "text-muted-foreground text-sm col-span-4",
384
+ children: $__i18n._({
385
+ id: "Z3FXyt",
386
+ message: "Loading..."
387
+ })
388
+ })
389
+ })
390
+ ]
235
391
  })
236
392
  ]
237
393
  });
@@ -3,6 +3,7 @@ export { CrudPageHeader, type CrudPageHeaderProps } from "./CrudPageHeader.js";
3
3
  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
+ export { MediaGallery, type MediaGalleryProps } from "./MediaGallery.js";
6
7
  export { PageForm, type PageFormProps } from "./PageForm.js";
7
8
  export { Pagination, LoadMore, PagePagination, type PaginationProps, type LoadMoreProps, type PagePaginationProps, } from "./Pagination.js";
8
9
  export { PostForm, type PostFormProps } from "./PostForm.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/theme/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EACL,UAAU,EACV,QAAQ,EACR,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/theme/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EACL,UAAU,EACV,QAAQ,EACR,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC"}
@@ -3,6 +3,7 @@ export { CrudPageHeader } from "./CrudPageHeader.js";
3
3
  export { DangerZone } from "./DangerZone.js";
4
4
  export { EmptyState } from "./EmptyState.js";
5
5
  export { ListItemRow } from "./ListItemRow.js";
6
+ export { MediaGallery } from "./MediaGallery.js";
6
7
  export { PageForm } from "./PageForm.js";
7
8
  export { Pagination, LoadMore, PagePagination } from "./Pagination.js";
8
9
  export { PostForm } from "./PostForm.js";
package/dist/types.d.ts CHANGED
@@ -3,6 +3,12 @@
3
3
  */
4
4
  export declare const POST_TYPES: readonly ["note", "article", "link", "quote", "image", "page"];
5
5
  export type PostType = (typeof POST_TYPES)[number];
6
+ export declare const MAX_MEDIA_ATTACHMENTS = 20;
7
+ /**
8
+ * Media attachment rules per post type.
9
+ * Each entry is [min, max] or null (no media allowed).
10
+ */
11
+ export declare const POST_TYPE_MEDIA_RULES: Record<PostType, [number, number] | null>;
6
12
  export declare const VISIBILITY_LEVELS: readonly ["featured", "quiet", "unlisted", "draft"];
7
13
  export type Visibility = (typeof VISIBILITY_LEVELS)[number];
8
14
  export interface Bindings {
@@ -96,8 +102,24 @@ export interface Media {
96
102
  width: number | null;
97
103
  height: number | null;
98
104
  alt: string | null;
105
+ position: number;
106
+ blurhash: string | null;
99
107
  createdAt: number;
100
108
  }
109
+ export interface MediaAttachment {
110
+ id: string;
111
+ url: string;
112
+ previewUrl: string;
113
+ alt: string | null;
114
+ blurhash: string | null;
115
+ width: number | null;
116
+ height: number | null;
117
+ position: number;
118
+ mimeType: string;
119
+ }
120
+ export interface PostWithMedia extends Post {
121
+ mediaAttachments: MediaAttachment[];
122
+ }
101
123
  export interface Collection {
102
124
  id: number;
103
125
  title: string;
@@ -133,6 +155,7 @@ export interface CreatePost {
133
155
  sourceName?: string;
134
156
  replyToId?: number;
135
157
  publishedAt?: number;
158
+ mediaIds?: string[];
136
159
  }
137
160
  export interface UpdatePost {
138
161
  type?: PostType;
@@ -143,6 +166,7 @@ export interface UpdatePost {
143
166
  sourceUrl?: string | null;
144
167
  sourceName?: string | null;
145
168
  publishedAt?: number;
169
+ mediaIds?: string[];
146
170
  }
147
171
  import type { FC, PropsWithChildren } from "hono/jsx";
148
172
  import type { ColorTheme } from "./theme/color-themes.js";
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,eAAO,MAAM,UAAU,gEAOb,CAAC;AACX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAQzE,CAAC;AAEJ,eAAO,MAAM,iBAAiB,qDAKpB,CAAC;AACX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAM5D,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,UAAU,CAAC;IACf,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwChB,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,aAAa,CAAC;AAMnD,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAc,SAAQ,IAAI;IACzC,gBAAgB,EAAE,eAAe,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,GAAG,GAAG,GAAG,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAMD,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,IAAI,CAAC;IACX,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC;IAC7B,UAAU,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,iBAAiB;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,uDAAuD;IACvD,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,sDAAsD;IACtD,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB"}
package/dist/types.js CHANGED
@@ -11,6 +11,33 @@ export const POST_TYPES = [
11
11
  "image",
12
12
  "page"
13
13
  ];
14
+ export const MAX_MEDIA_ATTACHMENTS = 20;
15
+ /**
16
+ * Media attachment rules per post type.
17
+ * Each entry is [min, max] or null (no media allowed).
18
+ */ export const POST_TYPE_MEDIA_RULES = {
19
+ note: [
20
+ 0,
21
+ 20
22
+ ],
23
+ article: [
24
+ 0,
25
+ 20
26
+ ],
27
+ image: [
28
+ 1,
29
+ 20
30
+ ],
31
+ link: [
32
+ 0,
33
+ 1
34
+ ],
35
+ quote: [
36
+ 0,
37
+ 20
38
+ ],
39
+ page: null
40
+ };
14
41
  export const VISIBILITY_LEVELS = [
15
42
  "featured",
16
43
  "quiet",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jant/core",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "A modern, open-source microblogging platform built on Cloudflare Workers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,8 +49,13 @@ export function createTestApp(options: TestAppOptions = {}) {
49
49
 
50
50
  const app = new Hono<Env>();
51
51
 
52
- // Inject services middleware
52
+ // Inject env bindings and services middleware
53
53
  app.use("*", async (c, next) => {
54
+ // Provide mock env bindings so c.env.* works in route handlers
55
+ c.env = {
56
+ SITE_URL: "http://localhost:9019",
57
+ } as AppVariables["services"] extends never ? never : Bindings;
58
+
54
59
  c.set("services", services as AppVariables["services"]);
55
60
  c.set("config", {});
56
61
 
@@ -79,6 +79,16 @@ export function createTestDatabase(options?: { fts?: boolean }) {
79
79
  }
80
80
  }
81
81
 
82
+ // Apply media attachments migration (position + blurhash)
83
+ const migration2 = readFileSync(
84
+ resolve(MIGRATIONS_DIR, "0002_add_media_attachments.sql"),
85
+ "utf-8",
86
+ );
87
+ for (const sql of migration2.split("--> statement-breakpoint")) {
88
+ const trimmed = sql.trim();
89
+ if (trimmed) sqlite.exec(trimmed);
90
+ }
91
+
82
92
  const db = drizzle(sqlite, { schema });
83
93
 
84
94
  return { db, sqlite };
package/src/app.tsx CHANGED
@@ -411,32 +411,14 @@ export function createApp(config: JantConfig = {}): App {
411
411
  const { email, password } = body;
412
412
 
413
413
  try {
414
- const signInRequest = new Request(
415
- `${c.env.SITE_URL}/api/auth/sign-in/email`,
416
- {
417
- method: "POST",
418
- headers: { "Content-Type": "application/json" },
419
- body: JSON.stringify({ email, password }),
420
- },
421
- );
422
-
423
- const response = await c.var.auth.handler(signInRequest);
424
-
425
- if (!response.ok) {
426
- return dsToast("Invalid email or password", "error");
427
- }
428
-
429
- // Forward Set-Cookie headers from auth response
430
- const cookieHeaders: Record<string, string> = {};
431
- const setCookie = response.headers.get("set-cookie");
432
- if (setCookie) {
433
- cookieHeaders["Set-Cookie"] = setCookie;
434
- }
414
+ const { headers } = await c.var.auth.api.signInEmail({
415
+ returnHeaders: true,
416
+ body: { email, password },
417
+ headers: c.req.raw.headers,
418
+ });
435
419
 
436
- return dsRedirect("/dash", { headers: cookieHeaders });
437
- } catch (err) {
438
- // eslint-disable-next-line no-console -- Error logging is intentional
439
- console.error("Signin error:", err);
420
+ return dsRedirect("/dash", { headers });
421
+ } catch {
440
422
  return dsToast("Invalid email or password", "error");
441
423
  }
442
424
  });
@@ -0,0 +1,3 @@
1
+ ALTER TABLE `media` ADD COLUMN `position` integer NOT NULL DEFAULT 0;
2
+ --> statement-breakpoint
3
+ ALTER TABLE `media` ADD COLUMN `blurhash` text;