@jant/core 0.3.24 → 0.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +101 -571
- package/dist/client.js +1 -0
- package/dist/db/schema.js +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +3 -9
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -9
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +48 -3
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +16 -11
- package/dist/lib/schemas.js +34 -3
- package/dist/lib/theme.js +4 -4
- package/dist/lib/timeline.js +24 -48
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +3 -3
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +3 -3
- package/dist/routes/api/search.js +2 -2
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -416
- package/dist/routes/dash/index.js +1 -1
- package/dist/routes/dash/media.js +13 -393
- package/dist/routes/dash/pages.js +112 -86
- package/dist/routes/dash/posts.js +3 -5
- package/dist/routes/dash/redirects.js +20 -14
- package/dist/routes/dash/settings.js +213 -518
- package/dist/routes/feed/rss.js +4 -3
- package/dist/routes/feed/sitemap.js +5 -3
- package/dist/routes/pages/archive.js +3 -6
- package/dist/routes/pages/collection.js +3 -6
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +36 -0
- package/dist/routes/pages/home.js +33 -49
- package/dist/routes/pages/latest.js +45 -0
- package/dist/routes/pages/page.js +29 -32
- package/dist/routes/pages/post.js +3 -6
- package/dist/routes/pages/search.js +3 -6
- package/dist/services/page.js +5 -1
- package/dist/services/post.js +45 -31
- package/dist/services/search.js +1 -1
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/{theme → ui}/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +467 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
- package/dist/{theme/components → ui/dash}/PostList.js +6 -6
- package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
- package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
- package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
- package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +169 -0
- package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
- package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
- package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
- package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
- package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
- package/dist/ui/shared/index.js +5 -0
- package/package.json +1 -9
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/app.tsx +131 -561
- package/src/client.ts +1 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +1 -1
- package/src/i18n/locales/en.po +477 -261
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +477 -261
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +477 -261
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +7 -36
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/schemas.test.ts +60 -19
- package/src/lib/__tests__/timeline.test.ts +45 -81
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +15 -9
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -10
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +73 -4
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +22 -15
- package/src/lib/schemas.ts +47 -6
- package/src/lib/theme.ts +5 -5
- package/src/lib/timeline.ts +28 -57
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +3 -3
- package/src/preset.css +2 -1
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +3 -3
- package/src/routes/api/search.ts +2 -2
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +18 -367
- package/src/routes/dash/index.tsx +1 -1
- package/src/routes/dash/media.tsx +13 -415
- package/src/routes/dash/pages.tsx +131 -98
- package/src/routes/dash/posts.tsx +3 -7
- package/src/routes/dash/redirects.tsx +22 -16
- package/src/routes/dash/settings.tsx +265 -478
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +5 -3
- package/src/routes/feed/sitemap.ts +5 -3
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +2 -6
- package/src/routes/pages/collection.tsx +2 -6
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +44 -0
- package/src/routes/pages/home.tsx +30 -53
- package/src/routes/pages/latest.tsx +59 -0
- package/src/routes/pages/page.tsx +28 -30
- package/src/routes/pages/post.tsx +2 -5
- package/src/routes/pages/search.tsx +2 -6
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post.test.ts +114 -15
- package/src/services/page.ts +13 -1
- package/src/services/post.ts +58 -40
- package/src/services/search.ts +2 -2
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +475 -0
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -774
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/{theme → ui}/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +414 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
- package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
- package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
- package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
- package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
- package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +164 -0
- package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
- package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
- package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
- package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
- package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
- package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -46
- package/dist/routes/dash/navigation.js +0 -289
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
- package/dist/themes/threads/index.js +0 -81
- package/dist/themes/threads/pages/HomePage.js +0 -25
- package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
- package/dist/themes/threads/timeline/TimelineItem.js +0 -36
- package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
- package/dist/themes/threads/timeline/groupByDate.js +0 -22
- package/dist/themes/threads/timeline/timelineMore.js +0 -107
- package/src/lib/__tests__/theme-components.test.ts +0 -105
- package/src/lib/theme-components.ts +0 -65
- package/src/routes/dash/navigation.tsx +0 -317
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
- package/src/themes/threads/index.ts +0 -100
- package/src/themes/threads/style.css +0 -336
- package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
- package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
- package/src/themes/threads/timeline/groupByDate.ts +0 -30
- package/src/themes/threads/timeline/timelineMore.tsx +0 -130
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -41,7 +41,6 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
41
41
|
body: post?.body ?? "",
|
|
42
42
|
url: post?.url ?? "",
|
|
43
43
|
quoteText: post?.quoteText ?? "",
|
|
44
|
-
slug: post?.slug ?? "",
|
|
45
44
|
status: post?.status ?? "published",
|
|
46
45
|
featured: post?.featured === 1,
|
|
47
46
|
pinned: post?.pinned === 1,
|
|
@@ -169,7 +168,7 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
169
168
|
r2PublicUrl,
|
|
170
169
|
s3PublicUrl,
|
|
171
170
|
);
|
|
172
|
-
const mUrl = getMediaUrl(m.
|
|
171
|
+
const mUrl = getMediaUrl(m.storageKey, pUrl);
|
|
173
172
|
const thumbUrl = getImageUrl(mUrl, imageTransformUrl, {
|
|
174
173
|
width: 150,
|
|
175
174
|
quality: 80,
|
|
@@ -287,51 +286,33 @@ export const PostForm: FC<PostFormProps> = ({
|
|
|
287
286
|
</div>
|
|
288
287
|
)}
|
|
289
288
|
|
|
290
|
-
{/* Custom slug (optional) */}
|
|
291
|
-
<div class="field">
|
|
292
|
-
<label class="label">
|
|
293
|
-
{t({
|
|
294
|
-
message: "Custom Slug (optional)",
|
|
295
|
-
comment: "@context: Post form field",
|
|
296
|
-
})}
|
|
297
|
-
</label>
|
|
298
|
-
<input
|
|
299
|
-
type="text"
|
|
300
|
-
data-bind="slug"
|
|
301
|
-
class="input"
|
|
302
|
-
placeholder="my-custom-url"
|
|
303
|
-
pattern="[a-z0-9-]*"
|
|
304
|
-
/>
|
|
305
|
-
<p class="text-xs text-muted-foreground mt-1">
|
|
306
|
-
{t({
|
|
307
|
-
message:
|
|
308
|
-
"Custom URL path. Leave empty to use default /p/ID format.",
|
|
309
|
-
comment: "@context: Slug help text",
|
|
310
|
-
})}
|
|
311
|
-
</p>
|
|
312
|
-
</div>
|
|
313
|
-
|
|
314
289
|
{/* Submit */}
|
|
315
290
|
<div class="flex gap-2">
|
|
316
|
-
<button type="submit" class="btn" data-attr
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
291
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
292
|
+
<svg
|
|
293
|
+
data-show="$_loading"
|
|
294
|
+
style="display:none"
|
|
295
|
+
class="animate-spin size-4"
|
|
296
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
297
|
+
viewBox="0 0 24 24"
|
|
298
|
+
fill="none"
|
|
299
|
+
stroke="currentColor"
|
|
300
|
+
stroke-width="2"
|
|
301
|
+
stroke-linecap="round"
|
|
302
|
+
stroke-linejoin="round"
|
|
303
|
+
role="status"
|
|
304
|
+
>
|
|
305
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
306
|
+
</svg>
|
|
307
|
+
{isEdit
|
|
308
|
+
? t({
|
|
309
|
+
message: "Update",
|
|
310
|
+
comment: "@context: Button to update existing post",
|
|
311
|
+
})
|
|
312
|
+
: t({
|
|
313
|
+
message: "Publish",
|
|
314
|
+
comment: "@context: Button to publish new post",
|
|
315
|
+
})}
|
|
335
316
|
</button>
|
|
336
317
|
<a href="/dash/posts" class="btn-outline">
|
|
337
318
|
{t({ message: "Cancel", comment: "@context: Button to cancel form" })}
|
|
@@ -7,9 +7,9 @@ import { useLingui } from "@lingui/react/macro";
|
|
|
7
7
|
import type { Post } from "../../types.js";
|
|
8
8
|
import * as sqid from "../../lib/sqid.js";
|
|
9
9
|
import * as time from "../../lib/time.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { EmptyState } from "
|
|
10
|
+
import { StatusBadge } from "./StatusBadge.js";
|
|
11
|
+
import { FormatBadge } from "./FormatBadge.js";
|
|
12
|
+
import { EmptyState } from "../shared/EmptyState.js";
|
|
13
13
|
import { ListItemRow } from "./ListItemRow.js";
|
|
14
14
|
import { ActionButtons } from "./ActionButtons.js";
|
|
15
15
|
|
|
@@ -38,8 +38,8 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
|
38
38
|
return (
|
|
39
39
|
<div class="flex flex-col divide-y">
|
|
40
40
|
{posts.map((post) => {
|
|
41
|
-
const permalink = post.
|
|
42
|
-
? `/${post.
|
|
41
|
+
const permalink = post.path
|
|
42
|
+
? `/${post.path}`
|
|
43
43
|
: `/p/${sqid.encode(post.id)}`;
|
|
44
44
|
return (
|
|
45
45
|
<ListItemRow
|
|
@@ -67,8 +67,8 @@ export const PostList: FC<PostListProps> = ({ posts }) => {
|
|
|
67
67
|
}
|
|
68
68
|
>
|
|
69
69
|
<div class="flex items-center gap-2 mb-1">
|
|
70
|
-
<
|
|
71
|
-
<
|
|
70
|
+
<FormatBadge type={post.format} />
|
|
71
|
+
<StatusBadge
|
|
72
72
|
status={post.status}
|
|
73
73
|
featured={post.featured === 1}
|
|
74
74
|
pinned={post.pinned === 1}
|
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
* Status Badge Component
|
|
3
3
|
*
|
|
4
4
|
* Displays badges for post status, featured, and pinned state.
|
|
5
|
-
* Named VisibilityBadge for backward compatibility with theme overrides.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import type { FC } from "hono/jsx";
|
|
9
8
|
import { useLingui } from "@lingui/react/macro";
|
|
10
9
|
import type { Status } from "../../types.js";
|
|
11
10
|
|
|
12
|
-
export interface
|
|
11
|
+
export interface StatusBadgeProps {
|
|
13
12
|
status: Status;
|
|
14
13
|
featured?: boolean;
|
|
15
14
|
pinned?: boolean;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
export const
|
|
17
|
+
export const StatusBadge: FC<StatusBadgeProps> = ({
|
|
19
18
|
status,
|
|
20
19
|
featured,
|
|
21
20
|
pinned,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared collection form (new + edit)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import type { Collection } from "../../../types.js";
|
|
7
|
+
|
|
8
|
+
export function CollectionForm({
|
|
9
|
+
collection,
|
|
10
|
+
isEdit,
|
|
11
|
+
}: {
|
|
12
|
+
collection?: Collection;
|
|
13
|
+
isEdit?: boolean;
|
|
14
|
+
}) {
|
|
15
|
+
const { t } = useLingui();
|
|
16
|
+
|
|
17
|
+
const signals = JSON.stringify({
|
|
18
|
+
title: collection?.title ?? "",
|
|
19
|
+
slug: collection?.slug ?? "",
|
|
20
|
+
description: collection?.description ?? "",
|
|
21
|
+
}).replace(/</g, "\\u003c");
|
|
22
|
+
|
|
23
|
+
const action = isEdit
|
|
24
|
+
? `/dash/collections/${collection?.id}`
|
|
25
|
+
: "/dash/collections";
|
|
26
|
+
|
|
27
|
+
const heading = isEdit
|
|
28
|
+
? t({ message: "Edit Collection", comment: "@context: Page heading" })
|
|
29
|
+
: t({ message: "New Collection", comment: "@context: Page heading" });
|
|
30
|
+
|
|
31
|
+
const submitLabel = isEdit
|
|
32
|
+
? t({
|
|
33
|
+
message: "Update Collection",
|
|
34
|
+
comment: "@context: Button to save collection changes",
|
|
35
|
+
})
|
|
36
|
+
: t({
|
|
37
|
+
message: "Create Collection",
|
|
38
|
+
comment: "@context: Button to save new collection",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const cancelHref = isEdit
|
|
42
|
+
? `/dash/collections/${collection?.id}`
|
|
43
|
+
: "/dash/collections";
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<h1 class="text-2xl font-semibold mb-6">{heading}</h1>
|
|
48
|
+
|
|
49
|
+
<form
|
|
50
|
+
data-signals={signals}
|
|
51
|
+
data-on:submit__prevent={`@post('${action}')`}
|
|
52
|
+
data-indicator="_loading"
|
|
53
|
+
class="flex flex-col gap-4 max-w-lg"
|
|
54
|
+
>
|
|
55
|
+
<div class="field">
|
|
56
|
+
<label class="label">
|
|
57
|
+
{t({
|
|
58
|
+
message: "Title",
|
|
59
|
+
comment: "@context: Collection form field",
|
|
60
|
+
})}
|
|
61
|
+
</label>
|
|
62
|
+
<input
|
|
63
|
+
type="text"
|
|
64
|
+
data-bind="title"
|
|
65
|
+
class="input"
|
|
66
|
+
required
|
|
67
|
+
placeholder={
|
|
68
|
+
isEdit
|
|
69
|
+
? undefined
|
|
70
|
+
: t({
|
|
71
|
+
message: "My Collection",
|
|
72
|
+
comment: "@context: Collection title placeholder",
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="field">
|
|
79
|
+
<label class="label">
|
|
80
|
+
{t({ message: "Slug", comment: "@context: Collection form field" })}
|
|
81
|
+
</label>
|
|
82
|
+
<input
|
|
83
|
+
type="text"
|
|
84
|
+
data-bind="slug"
|
|
85
|
+
class="input"
|
|
86
|
+
required
|
|
87
|
+
pattern="[a-z0-9-]+"
|
|
88
|
+
placeholder={isEdit ? undefined : "my-collection"}
|
|
89
|
+
/>
|
|
90
|
+
{!isEdit && (
|
|
91
|
+
<p class="text-xs text-muted-foreground mt-1">
|
|
92
|
+
{t({
|
|
93
|
+
message: "URL-safe identifier (lowercase, numbers, hyphens)",
|
|
94
|
+
comment: "@context: Collection path help text",
|
|
95
|
+
})}
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="field">
|
|
101
|
+
<label class="label">
|
|
102
|
+
{t({
|
|
103
|
+
message: "Description (optional)",
|
|
104
|
+
comment: "@context: Collection form field",
|
|
105
|
+
})}
|
|
106
|
+
</label>
|
|
107
|
+
<textarea
|
|
108
|
+
data-bind="description"
|
|
109
|
+
class="textarea"
|
|
110
|
+
rows={3}
|
|
111
|
+
placeholder={
|
|
112
|
+
isEdit
|
|
113
|
+
? undefined
|
|
114
|
+
: t({
|
|
115
|
+
message: "What's this collection about?",
|
|
116
|
+
comment: "@context: Collection description placeholder",
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
>
|
|
120
|
+
{collection?.description ?? ""}
|
|
121
|
+
</textarea>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="flex gap-2">
|
|
125
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
126
|
+
<svg
|
|
127
|
+
data-show="$_loading"
|
|
128
|
+
style="display:none"
|
|
129
|
+
class="animate-spin size-4"
|
|
130
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
131
|
+
viewBox="0 0 24 24"
|
|
132
|
+
fill="none"
|
|
133
|
+
stroke="currentColor"
|
|
134
|
+
stroke-width="2"
|
|
135
|
+
stroke-linecap="round"
|
|
136
|
+
stroke-linejoin="round"
|
|
137
|
+
role="status"
|
|
138
|
+
>
|
|
139
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
140
|
+
</svg>
|
|
141
|
+
{submitLabel}
|
|
142
|
+
</button>
|
|
143
|
+
<a href={cancelHref} class="btn-outline">
|
|
144
|
+
{t({
|
|
145
|
+
message: "Cancel",
|
|
146
|
+
comment: "@context: Button to cancel form",
|
|
147
|
+
})}
|
|
148
|
+
</a>
|
|
149
|
+
</div>
|
|
150
|
+
</form>
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections list view
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import type { Collection } from "../../../types.js";
|
|
7
|
+
import {
|
|
8
|
+
EmptyState,
|
|
9
|
+
ListItemRow,
|
|
10
|
+
ActionButtons,
|
|
11
|
+
CrudPageHeader,
|
|
12
|
+
} from "../index.js";
|
|
13
|
+
|
|
14
|
+
export function CollectionsListContent({
|
|
15
|
+
collections,
|
|
16
|
+
}: {
|
|
17
|
+
collections: Collection[];
|
|
18
|
+
}) {
|
|
19
|
+
const { t } = useLingui();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<CrudPageHeader
|
|
24
|
+
title={t({
|
|
25
|
+
message: "Collections",
|
|
26
|
+
comment: "@context: Dashboard heading",
|
|
27
|
+
})}
|
|
28
|
+
ctaLabel={t({
|
|
29
|
+
message: "New Collection",
|
|
30
|
+
comment: "@context: Button to create new collection",
|
|
31
|
+
})}
|
|
32
|
+
ctaHref="/dash/collections/new"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
{collections.length === 0 ? (
|
|
36
|
+
<EmptyState
|
|
37
|
+
message={t({
|
|
38
|
+
message: "No collections yet.",
|
|
39
|
+
comment: "@context: Empty state message",
|
|
40
|
+
})}
|
|
41
|
+
ctaText={t({
|
|
42
|
+
message: "New Collection",
|
|
43
|
+
comment: "@context: Button to create new collection",
|
|
44
|
+
})}
|
|
45
|
+
ctaHref="/dash/collections/new"
|
|
46
|
+
/>
|
|
47
|
+
) : (
|
|
48
|
+
<div class="flex flex-col divide-y">
|
|
49
|
+
{collections.map((col) => (
|
|
50
|
+
<ListItemRow
|
|
51
|
+
key={col.id}
|
|
52
|
+
actions={
|
|
53
|
+
<ActionButtons
|
|
54
|
+
editHref={`/dash/collections/${col.id}/edit`}
|
|
55
|
+
editLabel={t({
|
|
56
|
+
message: "Edit",
|
|
57
|
+
comment: "@context: Button to edit collection",
|
|
58
|
+
})}
|
|
59
|
+
viewHref={`/c/${col.slug}`}
|
|
60
|
+
viewLabel={t({
|
|
61
|
+
message: "View",
|
|
62
|
+
comment: "@context: Button to view collection",
|
|
63
|
+
})}
|
|
64
|
+
/>
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
<a
|
|
68
|
+
href={`/dash/collections/${col.id}`}
|
|
69
|
+
class="font-medium hover:underline"
|
|
70
|
+
>
|
|
71
|
+
{col.title}
|
|
72
|
+
</a>
|
|
73
|
+
<p class="text-sm text-muted-foreground">/{col.slug}</p>
|
|
74
|
+
{col.description && (
|
|
75
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
76
|
+
{col.description}
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
</ListItemRow>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single collection detail view
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import type { Collection, PostView } from "../../../types.js";
|
|
7
|
+
import { ActionButtons } from "../index.js";
|
|
8
|
+
import { encode } from "../../../lib/sqid.js";
|
|
9
|
+
|
|
10
|
+
export function ViewCollectionContent({
|
|
11
|
+
collection,
|
|
12
|
+
posts,
|
|
13
|
+
}: {
|
|
14
|
+
collection: Collection;
|
|
15
|
+
posts: PostView[];
|
|
16
|
+
}) {
|
|
17
|
+
const { t } = useLingui();
|
|
18
|
+
const postsHeader = t({
|
|
19
|
+
message: "Posts in Collection ({count})",
|
|
20
|
+
comment: "@context: Collection posts section heading",
|
|
21
|
+
values: { count: String(posts.length) },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<div class="flex items-center justify-between mb-6">
|
|
27
|
+
<div>
|
|
28
|
+
<h1 class="text-2xl font-semibold">{collection.title}</h1>
|
|
29
|
+
<p class="text-sm text-muted-foreground">/{collection.slug}</p>
|
|
30
|
+
</div>
|
|
31
|
+
<ActionButtons
|
|
32
|
+
editHref={`/dash/collections/${collection.id}/edit`}
|
|
33
|
+
editLabel={t({
|
|
34
|
+
message: "Edit",
|
|
35
|
+
comment: "@context: Button to edit collection",
|
|
36
|
+
})}
|
|
37
|
+
viewHref={`/c/${collection.slug}`}
|
|
38
|
+
viewLabel={t({
|
|
39
|
+
message: "View",
|
|
40
|
+
comment: "@context: Button to view collection",
|
|
41
|
+
})}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{collection.description && (
|
|
46
|
+
<p class="text-muted-foreground mb-6">{collection.description}</p>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div class="card">
|
|
50
|
+
<header>
|
|
51
|
+
<h2>{postsHeader}</h2>
|
|
52
|
+
</header>
|
|
53
|
+
<section>
|
|
54
|
+
{posts.length === 0 ? (
|
|
55
|
+
<p class="text-muted-foreground">
|
|
56
|
+
{t({
|
|
57
|
+
message: "No posts in this collection.",
|
|
58
|
+
comment: "@context: Empty state message",
|
|
59
|
+
})}
|
|
60
|
+
</p>
|
|
61
|
+
) : (
|
|
62
|
+
<div class="flex flex-col divide-y">
|
|
63
|
+
{posts.map((post) => (
|
|
64
|
+
<div key={post.id} class="py-3 flex items-center gap-4">
|
|
65
|
+
<div class="flex-1 min-w-0">
|
|
66
|
+
<a
|
|
67
|
+
href={`/dash/posts/${encode(post.id)}`}
|
|
68
|
+
class="font-medium hover:underline"
|
|
69
|
+
>
|
|
70
|
+
{post.title ||
|
|
71
|
+
post.excerpt?.slice(0, 50) ||
|
|
72
|
+
`Post #${post.id}`}
|
|
73
|
+
</a>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</section>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="mt-6">
|
|
83
|
+
<a href="/dash/collections" class="text-sm hover:underline">
|
|
84
|
+
{t({
|
|
85
|
+
message: "\u2190 Back to Collections",
|
|
86
|
+
comment: "@context: Navigation link",
|
|
87
|
+
})}
|
|
88
|
+
</a>
|
|
89
|
+
</div>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ActionButtons, type ActionButtonsProps } from "./ActionButtons.js";
|
|
2
|
+
export { CrudPageHeader, type CrudPageHeaderProps } from "./CrudPageHeader.js";
|
|
3
|
+
export { DangerZone, type DangerZoneProps } from "./DangerZone.js";
|
|
4
|
+
export { EmptyState, type EmptyStateProps } from "../shared/EmptyState.js";
|
|
5
|
+
export { FormatBadge, type FormatBadgeProps } from "./FormatBadge.js";
|
|
6
|
+
export { ListItemRow, type ListItemRowProps } from "./ListItemRow.js";
|
|
7
|
+
export { PageForm, type PageFormProps } from "./PageForm.js";
|
|
8
|
+
export { PostForm, type PostFormProps } from "./PostForm.js";
|
|
9
|
+
export { PostList, type PostListProps } from "./PostList.js";
|
|
10
|
+
export { StatusBadge, type StatusBadgeProps } from "./StatusBadge.js";
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media grid list with upload UI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useLingui } from "@lingui/react/macro";
|
|
6
|
+
import type { Media } from "../../../types.js";
|
|
7
|
+
import { EmptyState } from "../index.js";
|
|
8
|
+
import {
|
|
9
|
+
getMediaUrl,
|
|
10
|
+
getImageUrl,
|
|
11
|
+
getPublicUrlForProvider,
|
|
12
|
+
} from "../../../lib/image.js";
|
|
13
|
+
|
|
14
|
+
function formatSize(bytes: number): string {
|
|
15
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
16
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
17
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function MediaCard({
|
|
21
|
+
media,
|
|
22
|
+
r2PublicUrl,
|
|
23
|
+
imageTransformUrl,
|
|
24
|
+
s3PublicUrl,
|
|
25
|
+
}: {
|
|
26
|
+
media: Media;
|
|
27
|
+
r2PublicUrl?: string;
|
|
28
|
+
imageTransformUrl?: string;
|
|
29
|
+
s3PublicUrl?: string;
|
|
30
|
+
}) {
|
|
31
|
+
const publicUrl = getPublicUrlForProvider(
|
|
32
|
+
media.provider,
|
|
33
|
+
r2PublicUrl,
|
|
34
|
+
s3PublicUrl,
|
|
35
|
+
);
|
|
36
|
+
const fullUrl = getMediaUrl(media.storageKey, publicUrl);
|
|
37
|
+
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
38
|
+
width: 300,
|
|
39
|
+
quality: 80,
|
|
40
|
+
format: "auto",
|
|
41
|
+
fit: "cover",
|
|
42
|
+
});
|
|
43
|
+
const isImage = media.mimeType.startsWith("image/");
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div class="group relative" data-media-id={media.id}>
|
|
47
|
+
{isImage ? (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
class="block w-full aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary cursor-pointer"
|
|
51
|
+
onclick={`document.getElementById('lightbox-img').src = '${fullUrl}'; document.getElementById('lightbox').showModal()`}
|
|
52
|
+
>
|
|
53
|
+
<img
|
|
54
|
+
src={thumbnailUrl}
|
|
55
|
+
alt={media.alt || media.originalName}
|
|
56
|
+
class="w-full h-full object-cover"
|
|
57
|
+
loading="lazy"
|
|
58
|
+
/>
|
|
59
|
+
</button>
|
|
60
|
+
) : (
|
|
61
|
+
<a
|
|
62
|
+
href={`/dash/media/${media.id}`}
|
|
63
|
+
class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
|
|
64
|
+
>
|
|
65
|
+
<div class="w-full h-full flex items-center justify-center text-muted-foreground">
|
|
66
|
+
<span class="text-xs">{media.mimeType}</span>
|
|
67
|
+
</div>
|
|
68
|
+
</a>
|
|
69
|
+
)}
|
|
70
|
+
<a
|
|
71
|
+
href={`/dash/media/${media.id}`}
|
|
72
|
+
class="block mt-2 text-xs truncate hover:underline"
|
|
73
|
+
title={media.originalName}
|
|
74
|
+
>
|
|
75
|
+
{media.originalName}
|
|
76
|
+
</a>
|
|
77
|
+
<div class="text-xs text-muted-foreground">{formatSize(media.size)}</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function MediaListContent({
|
|
83
|
+
mediaList,
|
|
84
|
+
r2PublicUrl,
|
|
85
|
+
imageTransformUrl,
|
|
86
|
+
s3PublicUrl,
|
|
87
|
+
}: {
|
|
88
|
+
mediaList: Media[];
|
|
89
|
+
r2PublicUrl?: string;
|
|
90
|
+
imageTransformUrl?: string;
|
|
91
|
+
s3PublicUrl?: string;
|
|
92
|
+
}) {
|
|
93
|
+
const { t } = useLingui();
|
|
94
|
+
|
|
95
|
+
const processingText = t({
|
|
96
|
+
message: "Processing...",
|
|
97
|
+
comment: "@context: Upload status - processing",
|
|
98
|
+
});
|
|
99
|
+
const uploadingText = t({
|
|
100
|
+
message: "Uploading...",
|
|
101
|
+
comment: "@context: Upload status - uploading",
|
|
102
|
+
});
|
|
103
|
+
const uploadText = t({
|
|
104
|
+
message: "Upload",
|
|
105
|
+
comment: "@context: Button to upload media file",
|
|
106
|
+
});
|
|
107
|
+
const errorText = t({
|
|
108
|
+
message: "Upload failed. Please try again.",
|
|
109
|
+
comment: "@context: Upload error message",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<>
|
|
114
|
+
{/* Hidden form for Datastar-driven upload */}
|
|
115
|
+
<form
|
|
116
|
+
id="upload-form"
|
|
117
|
+
class="hidden"
|
|
118
|
+
enctype="multipart/form-data"
|
|
119
|
+
data-on:submit__prevent="@post('/api/upload', {contentType: 'form'})"
|
|
120
|
+
>
|
|
121
|
+
<input id="upload-file-input" type="file" name="file" />
|
|
122
|
+
</form>
|
|
123
|
+
|
|
124
|
+
{/* Header */}
|
|
125
|
+
<div class="flex items-center justify-between mb-6">
|
|
126
|
+
<h1 class="text-2xl font-semibold">
|
|
127
|
+
{t({ message: "Media", comment: "@context: Media main heading" })}
|
|
128
|
+
</h1>
|
|
129
|
+
<label class="btn cursor-pointer">
|
|
130
|
+
<span>{uploadText}</span>
|
|
131
|
+
<input
|
|
132
|
+
type="file"
|
|
133
|
+
class="hidden"
|
|
134
|
+
accept="image/*"
|
|
135
|
+
data-media-upload
|
|
136
|
+
data-text-processing={processingText}
|
|
137
|
+
data-text-uploading={uploadingText}
|
|
138
|
+
data-text-error={errorText}
|
|
139
|
+
/>
|
|
140
|
+
</label>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Upload instructions */}
|
|
144
|
+
<div class="card mb-6">
|
|
145
|
+
<section class="text-sm text-muted-foreground">
|
|
146
|
+
<p>
|
|
147
|
+
{t({
|
|
148
|
+
message:
|
|
149
|
+
"Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.",
|
|
150
|
+
comment:
|
|
151
|
+
"@context: Media upload instructions - auto optimization",
|
|
152
|
+
})}
|
|
153
|
+
</p>
|
|
154
|
+
</section>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Media grid or empty state */}
|
|
158
|
+
<div id="media-content">
|
|
159
|
+
{mediaList.length === 0 ? (
|
|
160
|
+
<div id="empty-state">
|
|
161
|
+
<EmptyState
|
|
162
|
+
message={t({
|
|
163
|
+
message: "No media uploaded yet.",
|
|
164
|
+
comment: "@context: Empty state message when no media exists",
|
|
165
|
+
})}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
) : (
|
|
169
|
+
<div
|
|
170
|
+
id="media-grid"
|
|
171
|
+
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"
|
|
172
|
+
>
|
|
173
|
+
{mediaList.map((m) => (
|
|
174
|
+
<MediaCard
|
|
175
|
+
key={m.id}
|
|
176
|
+
media={m}
|
|
177
|
+
r2PublicUrl={r2PublicUrl}
|
|
178
|
+
imageTransformUrl={imageTransformUrl}
|
|
179
|
+
s3PublicUrl={s3PublicUrl}
|
|
180
|
+
/>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* Lightbox */}
|
|
187
|
+
<dialog
|
|
188
|
+
id="lightbox"
|
|
189
|
+
class="p-0 m-auto bg-transparent backdrop:bg-black/80"
|
|
190
|
+
onclick="event.target === this && this.close()"
|
|
191
|
+
>
|
|
192
|
+
<img
|
|
193
|
+
id="lightbox-img"
|
|
194
|
+
src=""
|
|
195
|
+
alt=""
|
|
196
|
+
class="max-w-[90vw] max-h-[90vh] object-contain rounded-lg"
|
|
197
|
+
/>
|
|
198
|
+
</dialog>
|
|
199
|
+
</>
|
|
200
|
+
);
|
|
201
|
+
}
|