@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,9 +2,32 @@
2
2
  * Posts API Routes
3
3
  */ import { Hono } from "hono";
4
4
  import * as sqid from "../../lib/sqid.js";
5
- import { CreatePostSchema, UpdatePostSchema } from "../../lib/schemas.js";
5
+ import { CreatePostSchema, UpdatePostSchema, validateMediaForPostType } from "../../lib/schemas.js";
6
6
  import { requireAuthApi } from "../../middleware/auth.js";
7
+ import { getMediaUrl, getImageUrl } from "../../lib/image.js";
7
8
  export const postsApiRoutes = new Hono();
9
+ /**
10
+ * Converts a Media record to a MediaAttachment API response shape.
11
+ */ function toMediaAttachment(m, r2PublicUrl, imageTransformUrl) {
12
+ const url = getMediaUrl(m.id, m.r2Key, r2PublicUrl);
13
+ const previewUrl = getImageUrl(url, imageTransformUrl, {
14
+ width: 400,
15
+ quality: 80,
16
+ format: "auto",
17
+ fit: "cover"
18
+ });
19
+ return {
20
+ id: m.id,
21
+ url,
22
+ previewUrl,
23
+ alt: m.alt,
24
+ blurhash: m.blurhash,
25
+ width: m.width,
26
+ height: m.height,
27
+ position: m.position,
28
+ mimeType: m.mimeType
29
+ };
30
+ }
8
31
  // List posts
9
32
  postsApiRoutes.get("/", async (c)=>{
10
33
  const type = c.req.query("type");
@@ -22,10 +45,16 @@ postsApiRoutes.get("/", async (c)=>{
22
45
  cursor: cursor ? sqid.decode(cursor) ?? undefined : undefined,
23
46
  limit
24
47
  });
48
+ // Batch load media for all posts
49
+ const postIds = posts.map((p)=>p.id);
50
+ const mediaMap = await c.var.services.media.getByPostIds(postIds);
51
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
52
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
25
53
  return c.json({
26
54
  posts: posts.map((p)=>({
27
55
  ...p,
28
- sqid: sqid.encode(p.id)
56
+ sqid: sqid.encode(p.id),
57
+ mediaAttachments: (mediaMap.get(p.id) ?? []).map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
29
58
  })),
30
59
  nextCursor: posts.length === limit ? sqid.encode(posts[posts.length - 1]?.id ?? 0) : null
31
60
  });
@@ -40,9 +69,13 @@ postsApiRoutes.get("/:id", async (c)=>{
40
69
  if (!post) return c.json({
41
70
  error: "Not found"
42
71
  }, 404);
72
+ const mediaList = await c.var.services.media.getByPostId(post.id);
73
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
74
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
43
75
  return c.json({
44
76
  ...post,
45
- sqid: sqid.encode(post.id)
77
+ sqid: sqid.encode(post.id),
78
+ mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
46
79
  });
47
80
  });
48
81
  // Create post (requires auth)
@@ -57,6 +90,24 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
57
90
  }, 400);
58
91
  }
59
92
  const body = parseResult.data;
93
+ // Validate media for post type
94
+ if (body.mediaIds) {
95
+ const mediaError = validateMediaForPostType(body.type, body.mediaIds);
96
+ if (mediaError) {
97
+ return c.json({
98
+ error: mediaError
99
+ }, 400);
100
+ }
101
+ // Verify all media IDs exist
102
+ if (body.mediaIds.length > 0) {
103
+ const existing = await c.var.services.media.getByIds(body.mediaIds);
104
+ if (existing.length !== body.mediaIds.length) {
105
+ return c.json({
106
+ error: "One or more media IDs are invalid"
107
+ }, 400);
108
+ }
109
+ }
110
+ }
60
111
  const post = await c.var.services.posts.create({
61
112
  type: body.type,
62
113
  title: body.title,
@@ -68,9 +119,17 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
68
119
  replyToId: body.replyToId ? sqid.decode(body.replyToId) ?? undefined : undefined,
69
120
  publishedAt: body.publishedAt
70
121
  });
122
+ // Attach media
123
+ if (body.mediaIds && body.mediaIds.length > 0) {
124
+ await c.var.services.media.attachToPost(post.id, body.mediaIds);
125
+ }
126
+ const mediaList = await c.var.services.media.getByPostId(post.id);
127
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
128
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
71
129
  return c.json({
72
130
  ...post,
73
- sqid: sqid.encode(post.id)
131
+ sqid: sqid.encode(post.id),
132
+ mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
74
133
  }, 201);
75
134
  });
76
135
  // Update post (requires auth)
@@ -89,6 +148,33 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
89
148
  }, 400);
90
149
  }
91
150
  const body = parseResult.data;
151
+ // Validate media for post type if mediaIds is provided
152
+ if (body.mediaIds !== undefined) {
153
+ // Need the post type — use the new type if provided, else fetch existing
154
+ let postType = body.type;
155
+ if (!postType) {
156
+ const existing = await c.var.services.posts.getById(id);
157
+ if (!existing) return c.json({
158
+ error: "Not found"
159
+ }, 404);
160
+ postType = existing.type;
161
+ }
162
+ const mediaError = validateMediaForPostType(postType, body.mediaIds);
163
+ if (mediaError) {
164
+ return c.json({
165
+ error: mediaError
166
+ }, 400);
167
+ }
168
+ // Verify all media IDs exist
169
+ if (body.mediaIds.length > 0) {
170
+ const existing = await c.var.services.media.getByIds(body.mediaIds);
171
+ if (existing.length !== body.mediaIds.length) {
172
+ return c.json({
173
+ error: "One or more media IDs are invalid"
174
+ }, 400);
175
+ }
176
+ }
177
+ }
92
178
  const post = await c.var.services.posts.update(id, {
93
179
  type: body.type,
94
180
  title: body.title,
@@ -102,9 +188,17 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
102
188
  if (!post) return c.json({
103
189
  error: "Not found"
104
190
  }, 404);
191
+ // Update media attachments if provided (including empty array to clear)
192
+ if (body.mediaIds !== undefined) {
193
+ await c.var.services.media.attachToPost(post.id, body.mediaIds);
194
+ }
195
+ const mediaList = await c.var.services.media.getByPostId(post.id);
196
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
197
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
105
198
  return c.json({
106
199
  ...post,
107
- sqid: sqid.encode(post.id)
200
+ sqid: sqid.encode(post.id),
201
+ mediaAttachments: mediaList.map((m)=>toMediaAttachment(m, r2PublicUrl, imageTransformUrl))
108
202
  });
109
203
  });
110
204
  // Delete post (requires auth)
@@ -113,6 +207,8 @@ postsApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
113
207
  if (!id) return c.json({
114
208
  error: "Invalid ID"
115
209
  }, 400);
210
+ // Detach media before deleting
211
+ await c.var.services.media.detachFromPost(id);
116
212
  const success = await c.var.services.posts.delete(id);
117
213
  if (!success) return c.json({
118
214
  error: "Not found"
@@ -398,6 +398,44 @@ mediaRoutes.get("/", async (c)=>{
398
398
  })
399
399
  }));
400
400
  });
401
+ // Media picker (returns HTML fragment for PostForm dialog)
402
+ // Must be defined before /:id to avoid "picker" matching as an ID
403
+ mediaRoutes.get("/picker", async (c)=>{
404
+ const mediaList = await c.var.services.media.list(100);
405
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
406
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
407
+ if (mediaList.length === 0) {
408
+ return c.html(/*#__PURE__*/ _jsx("p", {
409
+ class: "text-muted-foreground text-sm col-span-4",
410
+ children: "No media uploaded yet. Upload media from the Media page first."
411
+ }));
412
+ }
413
+ return c.html(/*#__PURE__*/ _jsx(_Fragment, {
414
+ children: mediaList.filter((m)=>m.mimeType.startsWith("image/")).map((m)=>{
415
+ const url = getMediaUrl(m.id, m.r2Key, r2PublicUrl);
416
+ const thumbUrl = getImageUrl(url, imageTransformUrl, {
417
+ width: 150,
418
+ quality: 80,
419
+ format: "auto",
420
+ fit: "cover"
421
+ });
422
+ return /*#__PURE__*/ _jsx("button", {
423
+ type: "button",
424
+ class: "aspect-square rounded-lg overflow-hidden border-2 hover:border-primary cursor-pointer transition-colors",
425
+ "data-on:click": `$mediaIds.includes('${m.id}') ? ($mediaIds = $mediaIds.filter(id => id !== '${m.id}')) : ($mediaIds = [...$mediaIds, '${m.id}'])`,
426
+ "data-class:border-primary": `$mediaIds.includes('${m.id}')`,
427
+ "data-class:ring-2": `$mediaIds.includes('${m.id}')`,
428
+ "data-class:ring-primary": `$mediaIds.includes('${m.id}')`,
429
+ children: /*#__PURE__*/ _jsx("img", {
430
+ src: thumbUrl,
431
+ alt: m.alt || m.originalName,
432
+ class: "w-full h-full object-cover",
433
+ loading: "lazy"
434
+ })
435
+ }, m.id);
436
+ })
437
+ }));
438
+ });
401
439
  // View single media
402
440
  mediaRoutes.get("/:id", async (c)=>{
403
441
  const id = c.req.param("id");
@@ -1 +1 @@
1
- {"version":3,"file":"posts.d.ts","sourceRoot":"","sources":["../../../src/routes/dash/posts.tsx"],"names":[],"mappings":"AACA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAAQ,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAWjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,WAAW,kDAAkB,CAAC"}
1
+ {"version":3,"file":"posts.d.ts","sourceRoot":"","sources":["../../../src/routes/dash/posts.tsx"],"names":[],"mappings":"AACA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAA2B,MAAM,gBAAgB,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAWjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,WAAW,kDAAkB,CAAC"}
@@ -30,7 +30,7 @@ function PostsListContent({ posts }) {
30
30
  ]
31
31
  });
32
32
  }
33
- function NewPostContent() {
33
+ function NewPostContent({ collections }) {
34
34
  const { i18n: $__i18n, _: $__ } = $_useLingui();
35
35
  return /*#__PURE__*/ _jsxs(_Fragment, {
36
36
  children: [
@@ -42,7 +42,8 @@ function NewPostContent() {
42
42
  })
43
43
  }),
44
44
  /*#__PURE__*/ _jsx(PostForm, {
45
- action: "/dash/posts"
45
+ action: "/dash/posts",
46
+ collections: collections
46
47
  })
47
48
  ]
48
49
  });
@@ -71,12 +72,15 @@ postsRoutes.get("/", async (c)=>{
71
72
  // New post form
72
73
  postsRoutes.get("/new", async (c)=>{
73
74
  const siteName = await getSiteName(c);
75
+ const collections = await c.var.services.collections.list();
74
76
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
75
77
  c: c,
76
78
  title: "New Post",
77
79
  siteName: siteName,
78
80
  currentPath: "/dash/posts",
79
- children: /*#__PURE__*/ _jsx(NewPostContent, {})
81
+ children: /*#__PURE__*/ _jsx(NewPostContent, {
82
+ collections: collections
83
+ })
80
84
  }));
81
85
  });
82
86
  // Create post
@@ -88,8 +92,17 @@ postsRoutes.post("/", async (c)=>{
88
92
  content: body.content,
89
93
  visibility: body.visibility,
90
94
  sourceUrl: body.sourceUrl || undefined,
95
+ sourceName: body.sourceName || undefined,
91
96
  path: body.path || undefined
92
97
  });
98
+ // Attach media if provided
99
+ if (body.mediaIds && body.mediaIds.length > 0) {
100
+ await c.var.services.media.attachToPost(post.id, body.mediaIds);
101
+ }
102
+ // Sync collection associations
103
+ if (body.collectionIds) {
104
+ await c.var.services.collections.syncPostCollections(post.id, body.collectionIds);
105
+ }
93
106
  return dsRedirect(`/dash/posts/${sqid.encode(post.id)}`);
94
107
  });
95
108
  function ViewPostContent({ post }) {
@@ -135,7 +148,7 @@ function ViewPostContent({ post }) {
135
148
  ]
136
149
  });
137
150
  }
138
- function EditPostContent({ post }) {
151
+ function EditPostContent({ post, mediaAttachments, r2PublicUrl, imageTransformUrl, collections, postCollectionIds }) {
139
152
  const { i18n: $__i18n, _: $__ } = $_useLingui();
140
153
  return /*#__PURE__*/ _jsxs(_Fragment, {
141
154
  children: [
@@ -148,7 +161,12 @@ function EditPostContent({ post }) {
148
161
  }),
149
162
  /*#__PURE__*/ _jsx(PostForm, {
150
163
  post: post,
151
- action: `/dash/posts/${sqid.encode(post.id)}`
164
+ action: `/dash/posts/${sqid.encode(post.id)}`,
165
+ mediaAttachments: mediaAttachments,
166
+ r2PublicUrl: r2PublicUrl,
167
+ imageTransformUrl: imageTransformUrl,
168
+ collections: collections,
169
+ postCollectionIds: postCollectionIds
152
170
  })
153
171
  ]
154
172
  });
@@ -178,13 +196,24 @@ postsRoutes.get("/:id/edit", async (c)=>{
178
196
  const post = await c.var.services.posts.getById(id);
179
197
  if (!post) return c.notFound();
180
198
  const siteName = await getSiteName(c);
199
+ const mediaAttachments = await c.var.services.media.getByPostId(post.id);
200
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
201
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
202
+ const collections = await c.var.services.collections.list();
203
+ const postCollections = await c.var.services.collections.getCollectionsForPost(post.id);
204
+ const postCollectionIds = postCollections.map((col)=>col.id);
181
205
  return c.html(/*#__PURE__*/ _jsx(DashLayout, {
182
206
  c: c,
183
207
  title: `Edit: ${post.title || "Post"}`,
184
208
  siteName: siteName,
185
209
  currentPath: "/dash/posts",
186
210
  children: /*#__PURE__*/ _jsx(EditPostContent, {
187
- post: post
211
+ post: post,
212
+ mediaAttachments: mediaAttachments,
213
+ r2PublicUrl: r2PublicUrl,
214
+ imageTransformUrl: imageTransformUrl,
215
+ collections: collections,
216
+ postCollectionIds: postCollectionIds
188
217
  })
189
218
  }));
190
219
  });
@@ -199,14 +228,24 @@ postsRoutes.post("/:id", async (c)=>{
199
228
  content: body.content || null,
200
229
  visibility: body.visibility,
201
230
  sourceUrl: body.sourceUrl || null,
231
+ sourceName: body.sourceName || null,
202
232
  path: body.path || null
203
233
  });
234
+ // Update media attachments if provided
235
+ if (body.mediaIds !== undefined) {
236
+ await c.var.services.media.attachToPost(id, body.mediaIds);
237
+ }
238
+ // Sync collection associations
239
+ if (body.collectionIds !== undefined) {
240
+ await c.var.services.collections.syncPostCollections(id, body.collectionIds);
241
+ }
204
242
  return dsRedirect(`/dash/posts/${sqid.encode(id)}`);
205
243
  });
206
244
  // Delete post
207
245
  postsRoutes.post("/:id/delete", async (c)=>{
208
246
  const id = sqid.decode(c.req.param("id"));
209
247
  if (!id) return c.notFound();
248
+ await c.var.services.media.detachFromPost(id);
210
249
  await c.var.services.posts.delete(id);
211
250
  return dsRedirect("/dash/posts");
212
251
  });
@@ -1 +1 @@
1
- {"version":3,"file":"rss.d.ts","sourceRoot":"","sources":["../../../src/routes/feed/rss.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,SAAS,kDAAkB,CAAC"}
1
+ {"version":3,"file":"rss.d.ts","sourceRoot":"","sources":["../../../src/routes/feed/rss.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,SAAS,kDAAkB,CAAC"}
@@ -3,6 +3,7 @@
3
3
  */ import { Hono } from "hono";
4
4
  import * as sqid from "../../lib/sqid.js";
5
5
  import * as time from "../../lib/time.js";
6
+ import { getMediaUrl } from "../../lib/image.js";
6
7
  export const rssRoutes = new Hono();
7
8
  // RSS 2.0 Feed - main feed at /feed
8
9
  rssRoutes.get("/", async (c)=>{
@@ -10,6 +11,7 @@ rssRoutes.get("/", async (c)=>{
10
11
  const siteName = all["SITE_NAME"] ?? "Jant";
11
12
  const siteDescription = all["SITE_DESCRIPTION"] ?? "";
12
13
  const siteUrl = c.env.SITE_URL;
14
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
13
15
  const posts = await c.var.services.posts.list({
14
16
  visibility: [
15
17
  "featured",
@@ -17,17 +19,24 @@ rssRoutes.get("/", async (c)=>{
17
19
  ],
18
20
  limit: 50
19
21
  });
22
+ // Batch load media for enclosures
23
+ const postIds = posts.map((p)=>p.id);
24
+ const mediaMap = await c.var.services.media.getByPostIds(postIds);
20
25
  const items = posts.map((post)=>{
21
26
  const link = `${siteUrl}/p/${sqid.encode(post.id)}`;
22
27
  const title = post.title || `Post #${post.id}`;
23
28
  const pubDate = new Date(post.publishedAt * 1000).toUTCString();
29
+ // Add enclosure for first media attachment
30
+ const postMedia = mediaMap.get(post.id);
31
+ const firstMedia = postMedia?.[0];
32
+ const enclosure = firstMedia ? `\n <enclosure url="${getMediaUrl(firstMedia.id, firstMedia.r2Key, r2PublicUrl)}" length="${firstMedia.size}" type="${firstMedia.mimeType}"/>` : "";
24
33
  return `
25
34
  <item>
26
35
  <title><![CDATA[${escapeXml(title)}]]></title>
27
36
  <link>${link}</link>
28
37
  <guid isPermaLink="true">${link}</guid>
29
38
  <pubDate>${pubDate}</pubDate>
30
- <description><![CDATA[${post.contentHtml || ""}]]></description>
39
+ <description><![CDATA[${post.contentHtml || ""}]]></description>${enclosure}
31
40
  </item>`;
32
41
  }).join("");
33
42
  const rss = `<?xml version="1.0" encoding="UTF-8"?>
@@ -1 +1 @@
1
- {"version":3,"file":"home.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/home.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAAQ,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAMjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,UAAU,kDAAkB,CAAC"}
1
+ {"version":3,"file":"home.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/home.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAAyB,MAAM,gBAAgB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAQjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,UAAU,kDAAkB,CAAC"}
@@ -4,11 +4,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
4
4
  */ import { Hono } from "hono";
5
5
  import { useLingui as $_useLingui } from "@jant/core/i18n";
6
6
  import { BaseLayout } from "../../theme/layouts/index.js";
7
+ import { MediaGallery } from "../../theme/components/index.js";
7
8
  import * as sqid from "../../lib/sqid.js";
8
9
  import * as time from "../../lib/time.js";
9
10
  import { getSiteName } from "../../lib/config.js";
11
+ import { getMediaUrl, getImageUrl } from "../../lib/image.js";
10
12
  export const homeRoutes = new Hono();
11
- function HomeContent({ siteName, posts }) {
13
+ function HomeContent({ siteName, posts, mediaMap }) {
12
14
  const { i18n: $__i18n, _: $__ } = $_useLingui();
13
15
  return /*#__PURE__*/ _jsxs("div", {
14
16
  class: "container py-8",
@@ -48,7 +50,9 @@ function HomeContent({ siteName, posts }) {
48
50
  id: "ODiSoW",
49
51
  message: "No posts yet."
50
52
  })
51
- }) : posts.map((post)=>/*#__PURE__*/ _jsxs("article", {
53
+ }) : posts.map((post)=>{
54
+ const attachments = mediaMap.get(post.id) ?? [];
55
+ return /*#__PURE__*/ _jsxs("article", {
52
56
  class: "h-entry",
53
57
  children: [
54
58
  post.title && /*#__PURE__*/ _jsx("h2", {
@@ -65,6 +69,9 @@ function HomeContent({ siteName, posts }) {
65
69
  __html: post.contentHtml || ""
66
70
  }
67
71
  }),
72
+ attachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
73
+ attachments: attachments
74
+ }),
68
75
  /*#__PURE__*/ _jsxs("footer", {
69
76
  class: "mt-2 text-sm text-muted-foreground",
70
77
  children: [
@@ -83,7 +90,8 @@ function HomeContent({ siteName, posts }) {
83
90
  ]
84
91
  })
85
92
  ]
86
- }, post.id))
93
+ }, post.id);
94
+ })
87
95
  }),
88
96
  posts.length >= 20 && /*#__PURE__*/ _jsx("nav", {
89
97
  class: "mt-8 text-center",
@@ -108,12 +116,37 @@ homeRoutes.get("/", async (c)=>{
108
116
  ],
109
117
  limit: 20
110
118
  });
119
+ // Batch load media attachments
120
+ const postIds = posts.map((p)=>p.id);
121
+ const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
122
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
123
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
124
+ const mediaMap = new Map();
125
+ for (const [postId, mediaList] of rawMediaMap){
126
+ mediaMap.set(postId, mediaList.map((m)=>({
127
+ id: m.id,
128
+ url: getMediaUrl(m.id, m.r2Key, r2PublicUrl),
129
+ previewUrl: getImageUrl(getMediaUrl(m.id, m.r2Key, r2PublicUrl), imageTransformUrl, {
130
+ width: 400,
131
+ quality: 80,
132
+ format: "auto",
133
+ fit: "cover"
134
+ }),
135
+ alt: m.alt,
136
+ blurhash: m.blurhash,
137
+ width: m.width,
138
+ height: m.height,
139
+ position: m.position,
140
+ mimeType: m.mimeType
141
+ })));
142
+ }
111
143
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
112
144
  title: siteName,
113
145
  c: c,
114
146
  children: /*#__PURE__*/ _jsx(HomeContent, {
115
147
  siteName: siteName,
116
- posts: posts
148
+ posts: posts,
149
+ mediaMap: mediaMap
117
150
  })
118
151
  }));
119
152
  });
@@ -1 +1 @@
1
- {"version":3,"file":"post.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/post.tsx"],"names":[],"mappings":"AACA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAAQ,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,UAAU,kDAAkB,CAAC"}
1
+ {"version":3,"file":"post.d.ts","sourceRoot":"","sources":["../../../src/routes/pages/post.tsx"],"names":[],"mappings":"AACA;;GAEG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,EAAE,QAAQ,EAAyB,MAAM,gBAAgB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAOjD,KAAK,GAAG,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3D,eAAO,MAAM,UAAU,kDAAkB,CAAC"}
@@ -5,10 +5,12 @@ import { getSiteName } from "../../lib/config.js";
5
5
  */ import { Hono } from "hono";
6
6
  import { useLingui as $_useLingui } from "@jant/core/i18n";
7
7
  import { BaseLayout } from "../../theme/layouts/index.js";
8
+ import { MediaGallery } from "../../theme/components/index.js";
8
9
  import * as sqid from "../../lib/sqid.js";
9
10
  import * as time from "../../lib/time.js";
11
+ import { getMediaUrl, getImageUrl } from "../../lib/image.js";
10
12
  export const postRoutes = new Hono();
11
- function PostContent({ post }) {
13
+ function PostContent({ post, mediaAttachments }) {
12
14
  const { i18n: $__i18n, _: $__ } = $_useLingui();
13
15
  return /*#__PURE__*/ _jsxs("div", {
14
16
  class: "container py-8",
@@ -26,6 +28,9 @@ function PostContent({ post }) {
26
28
  __html: post.contentHtml || ""
27
29
  }
28
30
  }),
31
+ mediaAttachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
32
+ attachments: mediaAttachments
33
+ }),
29
34
  /*#__PURE__*/ _jsxs("footer", {
30
35
  class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
31
36
  children: [
@@ -78,6 +83,26 @@ postRoutes.get("/:id", async (c)=>{
78
83
  if (post.visibility === "draft") {
79
84
  return c.notFound();
80
85
  }
86
+ // Load media attachments
87
+ const rawMedia = await c.var.services.media.getByPostId(post.id);
88
+ const r2PublicUrl = c.env.R2_PUBLIC_URL;
89
+ const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
90
+ const mediaAttachments = rawMedia.map((m)=>({
91
+ id: m.id,
92
+ url: getMediaUrl(m.id, m.r2Key, r2PublicUrl),
93
+ previewUrl: getImageUrl(getMediaUrl(m.id, m.r2Key, r2PublicUrl), imageTransformUrl, {
94
+ width: 400,
95
+ quality: 80,
96
+ format: "auto",
97
+ fit: "cover"
98
+ }),
99
+ alt: m.alt,
100
+ blurhash: m.blurhash,
101
+ width: m.width,
102
+ height: m.height,
103
+ position: m.position,
104
+ mimeType: m.mimeType
105
+ }));
81
106
  const siteName = await getSiteName(c);
82
107
  const title = post.title || siteName;
83
108
  return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
@@ -85,7 +110,8 @@ postRoutes.get("/:id", async (c)=>{
85
110
  description: post.content?.slice(0, 160),
86
111
  c: c,
87
112
  children: /*#__PURE__*/ _jsx(PostContent, {
88
- post: post
113
+ post: post,
114
+ mediaAttachments: mediaAttachments
89
115
  })
90
116
  }));
91
117
  });
@@ -16,6 +16,7 @@ export interface CollectionService {
16
16
  removePost(collectionId: number, postId: number): Promise<void>;
17
17
  getPosts(collectionId: number): Promise<Post[]>;
18
18
  getCollectionsForPost(postId: number): Promise<Collection[]>;
19
+ syncPostCollections(postId: number, collectionIds: number[]): Promise<void>;
19
20
  }
20
21
  export interface CreateCollectionData {
21
22
  title: string;
@@ -1 +1 @@
1
- {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/services/collection.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAChD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACpD,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAC9B,MAAM,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3E,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAChD,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,QAAQ,GAAG,iBAAiB,CAmKvE"}
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/services/collection.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAChD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACpD,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAC9B,MAAM,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3E,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAChD,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAC7D,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7E;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,QAAQ,GAAG,iBAAiB,CAqLvE"}
@@ -103,6 +103,19 @@ export function createCollectionService(db) {
103
103
  collection: collections
104
104
  }).from(postCollections).innerJoin(collections, eq(postCollections.collectionId, collections.id)).where(eq(postCollections.postId, postId));
105
105
  return rows.map((r)=>toCollection(r.collection));
106
+ },
107
+ async syncPostCollections (postId, collectionIds) {
108
+ const current = await this.getCollectionsForPost(postId);
109
+ const currentIds = new Set(current.map((c)=>c.id));
110
+ const desiredIds = new Set(collectionIds);
111
+ const toAdd = collectionIds.filter((id)=>!currentIds.has(id));
112
+ const toRemove = current.map((c)=>c.id).filter((id)=>!desiredIds.has(id));
113
+ for (const collectionId of toAdd){
114
+ await this.addPost(collectionId, postId);
115
+ }
116
+ for (const collectionId of toRemove){
117
+ await this.removePost(collectionId, postId);
118
+ }
106
119
  }
107
120
  };
108
121
  }
@@ -7,10 +7,15 @@ import type { Database } from "../db/index.js";
7
7
  import type { Media } from "../types.js";
8
8
  export interface MediaService {
9
9
  getById(id: string): Promise<Media | null>;
10
+ getByIds(ids: string[]): Promise<Media[]>;
11
+ getByPostId(postId: number): Promise<Media[]>;
12
+ getByPostIds(postIds: number[]): Promise<Map<number, Media[]>>;
10
13
  list(limit?: number): Promise<Media[]>;
11
14
  create(data: CreateMediaData): Promise<Media>;
12
15
  delete(id: string): Promise<boolean>;
13
16
  getByR2Key(r2Key: string): Promise<Media | null>;
17
+ attachToPost(postId: number, mediaIds: string[]): Promise<void>;
18
+ detachFromPost(postId: number): Promise<void>;
14
19
  }
15
20
  export interface CreateMediaData {
16
21
  postId?: number;
@@ -22,6 +27,8 @@ export interface CreateMediaData {
22
27
  width?: number;
23
28
  height?: number;
24
29
  alt?: string;
30
+ position?: number;
31
+ blurhash?: string;
25
32
  }
26
33
  export declare function createMediaService(db: Database): MediaService;
27
34
  //# sourceMappingURL=media.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/services/media.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IACvC,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,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,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,GAAG,YAAY,CA2E7D"}
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/services/media.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC3C,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IAC1C,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IAC9C,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IACvC,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACjD,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,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,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,GAAG,YAAY,CA+I7D"}