lightnet 3.9.1 → 3.10.1

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 (67) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +4 -0
  3. package/__e2e__/admin.spec.ts +113 -0
  4. package/__e2e__/fixtures/basics/astro.config.mjs +6 -0
  5. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  6. package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
  7. package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
  8. package/__e2e__/fixtures/basics/node_modules/.bin/tsc +2 -2
  9. package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +2 -2
  10. package/__e2e__/fixtures/basics/package.json +9 -9
  11. package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +15 -0
  12. package/__e2e__/fixtures/basics/src/content/media-types/audio.json +7 -0
  13. package/__e2e__/fixtures/basics/src/translations/de.yml +1 -0
  14. package/__e2e__/fixtures/basics/src/translations/en.yml +1 -0
  15. package/__e2e__/homepage.spec.ts +21 -0
  16. package/package.json +18 -11
  17. package/src/admin/api/fs/writeText.ts +50 -0
  18. package/src/admin/components/form/FieldErrors.tsx +19 -0
  19. package/src/admin/components/form/SubmitButton.tsx +77 -0
  20. package/src/admin/components/form/TextField.tsx +24 -0
  21. package/src/admin/components/form/form-context.ts +4 -0
  22. package/src/admin/components/form/index.ts +16 -0
  23. package/src/admin/i18n/translations/en.yml +1 -0
  24. package/src/admin/i18n/translations.ts +5 -0
  25. package/src/admin/pages/AdminRoute.astro +16 -0
  26. package/src/admin/pages/media/EditForm.tsx +58 -0
  27. package/src/admin/pages/media/EditRoute.astro +33 -0
  28. package/src/admin/pages/media/file-system.ts +37 -0
  29. package/src/admin/pages/media/media-item-store.ts +11 -0
  30. package/src/admin/types/media-item.ts +8 -0
  31. package/src/api/media/[mediaId].ts +16 -0
  32. package/src/{pages/api → api}/versions.ts +1 -1
  33. package/src/astro-integration/config.ts +19 -0
  34. package/src/astro-integration/integration.ts +44 -6
  35. package/src/components/CategoriesSection.astro +1 -1
  36. package/src/components/MediaGallerySection.astro +1 -1
  37. package/src/components/Toast.tsx +55 -0
  38. package/src/components/showToast.ts +61 -0
  39. package/src/content/astro-image.ts +1 -14
  40. package/src/content/content-schema.ts +10 -3
  41. package/src/content/get-media-items.ts +46 -1
  42. package/src/i18n/translations/ar.yml +1 -0
  43. package/src/i18n/translations/bn.yml +1 -0
  44. package/src/i18n/translations/de.yml +1 -0
  45. package/src/i18n/translations/en.yml +3 -0
  46. package/src/i18n/translations/es.yml +1 -0
  47. package/src/i18n/translations/fi.yml +1 -0
  48. package/src/i18n/translations/fr.yml +1 -0
  49. package/src/i18n/translations/hi.yml +1 -0
  50. package/src/i18n/translations/kk.yml +1 -0
  51. package/src/i18n/translations/pt.yml +1 -0
  52. package/src/i18n/translations/ru.yml +1 -0
  53. package/src/i18n/translations/uk.yml +1 -0
  54. package/src/i18n/translations/zh.yml +1 -0
  55. package/src/i18n/translations.ts +21 -7
  56. package/src/layouts/Page.astro +3 -2
  57. package/src/layouts/components/Footer.astro +24 -0
  58. package/src/layouts/components/LightNetLogo.svg +1 -0
  59. package/src/pages/details-page/components/MainDetailsSection.astro +5 -1
  60. package/src/pages/details-page/components/VideoDetailsSection.astro +5 -1
  61. package/src/pages/details-page/components/main-details/EditButton.astro +30 -0
  62. package/src/pages/details-page/components/main-details/ShareButton.astro +9 -13
  63. package/src/pages/{api → search-page/api}/search.ts +3 -3
  64. package/src/pages/search-page/components/SearchListItem.tsx +1 -1
  65. package/src/pages/search-page/hooks/use-search.ts +3 -3
  66. package/tailwind.config.ts +1 -0
  67. /package/src/pages/{api → search-page/api}/search-response.ts +0 -0
@@ -0,0 +1,58 @@
1
+ import { revalidateLogic } from "@tanstack/react-form"
2
+
3
+ import { showToastById } from "../../../components/showToast"
4
+ import Toast from "../../../components/Toast"
5
+ import { useAppForm } from "../../components/form"
6
+ import { type MediaItem, mediaItemSchema } from "../../types/media-item"
7
+ import { updateMediaItem } from "./media-item-store"
8
+
9
+ export default function EditForm({
10
+ mediaId,
11
+ mediaItem,
12
+ }: {
13
+ mediaId: string
14
+ mediaItem: MediaItem
15
+ }) {
16
+ const form = useAppForm({
17
+ defaultValues: mediaItem,
18
+ validators: {
19
+ onDynamic: mediaItemSchema,
20
+ },
21
+ validationLogic: revalidateLogic({
22
+ mode: "blur",
23
+ modeAfterSubmission: "change",
24
+ }),
25
+ onSubmit: async ({ value }) => {
26
+ await updateMediaItem(mediaId, { ...mediaItem, ...value })
27
+ },
28
+ onSubmitInvalid: () => {
29
+ showToastById("invalid-form-data-toast")
30
+ },
31
+ })
32
+
33
+ return (
34
+ <form
35
+ onSubmit={(e) => {
36
+ e.preventDefault()
37
+ form.handleSubmit()
38
+ }}
39
+ className="flex flex-col items-start gap-4"
40
+ >
41
+ <form.AppField
42
+ name="commonId"
43
+ children={(field) => <field.TextField label="Common ID" />}
44
+ />
45
+ <form.AppField
46
+ name="title"
47
+ children={(field) => <field.TextField label="Title" />}
48
+ />
49
+ <form.AppForm>
50
+ <form.SubmitButton />
51
+ <Toast id="invalid-form-data-toast" variant="error">
52
+ <div className="font-bold text-gray-700">Invalid form data</div>
53
+ Check the fields and try again.
54
+ </Toast>
55
+ </form.AppForm>
56
+ </form>
57
+ )
58
+ }
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { GetStaticPaths } from "astro"
3
+ import { getCollection } from "astro:content"
4
+ import config from "virtual:lightnet/config"
5
+
6
+ import { getRawMediaItem } from "../../../content/get-media-items"
7
+ import { resolveLocales } from "../../../i18n/resolve-locales"
8
+ import Page from "../../../layouts/Page.astro"
9
+ import EditForm from "./EditForm"
10
+
11
+ export const getStaticPaths = (async () => {
12
+ const mediaItems = await getCollection("media")
13
+ return resolveLocales(config).flatMap((locale) =>
14
+ mediaItems.map(({ id: mediaId }) => ({ params: { mediaId, locale } })),
15
+ )
16
+ }) satisfies GetStaticPaths
17
+
18
+ const { mediaId } = Astro.params
19
+ const mediaItemEntry = await getRawMediaItem(mediaId)
20
+ ---
21
+
22
+ <Page>
23
+ <div class="mx-auto max-w-screen-lg px-4 pt-12 md:px-8">
24
+ <a
25
+ class="underline"
26
+ href=`/${Astro.currentLocale}/media/faithful-freestyle--en`
27
+ >Back to details page</a
28
+ >
29
+ <h1 class="mb-4 mt-8 text-lg">Edit media item</h1>
30
+
31
+ <EditForm mediaId={mediaId} mediaItem={mediaItemEntry.data} client:load />
32
+ </div>
33
+ </Page>
@@ -0,0 +1,37 @@
1
+ export const writeText = (path: string, body: string) => {
2
+ return fetch(
3
+ `/api/internal/fs/writeText?path=${encodeURIComponent(path.replace(/^\//, ""))}`,
4
+ {
5
+ method: "POST",
6
+ headers: { "Content-Type": resolveContentType(path) },
7
+ body,
8
+ },
9
+ )
10
+ }
11
+
12
+ export const writeJson = async (path: string, object: unknown) => {
13
+ return writeText(path, JSON.stringify(sortObject(object), null, 2))
14
+ }
15
+
16
+ const resolveContentType = (path: string) => {
17
+ const normalizedPath = path.trim().toLowerCase()
18
+ return normalizedPath.endsWith(".json")
19
+ ? "application/json"
20
+ : "text/plain; charset=utf-8"
21
+ }
22
+
23
+ const sortObject = (value: unknown): unknown => {
24
+ if (Array.isArray(value)) {
25
+ return value.map(sortObject)
26
+ }
27
+
28
+ if (value && typeof value === "object") {
29
+ const entries = Object.entries(value as Record<string, unknown>)
30
+ .sort(([a], [b]) => (a > b ? 1 : a < b ? -1 : 0))
31
+ .map(([key, nestedValue]) => [key, sortObject(nestedValue)])
32
+
33
+ return Object.fromEntries(entries)
34
+ }
35
+
36
+ return value
37
+ }
@@ -0,0 +1,11 @@
1
+ import { type MediaItem, mediaItemSchema } from "../../types/media-item"
2
+ import { writeJson } from "./file-system"
3
+
4
+ export const loadMediaItem = (id: string) =>
5
+ fetch(`/api/media/${id}.json`)
6
+ .then((response) => response.json())
7
+ .then((json) => mediaItemSchema.parse(json.content))
8
+
9
+ export const updateMediaItem = async (id: string, item: MediaItem) => {
10
+ return writeJson(`/src/content/media/${id}.json`, item)
11
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from "astro/zod"
2
+
3
+ export const mediaItemSchema = z.object({
4
+ commonId: z.string().nonempty(),
5
+ title: z.string().nonempty(),
6
+ })
7
+
8
+ export type MediaItem = z.infer<typeof mediaItemSchema>
@@ -0,0 +1,16 @@
1
+ import type { APIRoute, GetStaticPaths } from "astro"
2
+ import { getCollection } from "astro:content"
3
+
4
+ import { getRawMediaItem } from "../../content/get-media-items"
5
+
6
+ export const getStaticPaths = (async () => {
7
+ const mediaItems = await getCollection("media")
8
+ return mediaItems.map(({ id: mediaId }) => ({ params: { mediaId } }))
9
+ }) satisfies GetStaticPaths
10
+
11
+ export const GET: APIRoute = async ({ params: { mediaId } }) => {
12
+ const entry = await getRawMediaItem(mediaId!)
13
+ return new Response(
14
+ JSON.stringify({ id: entry.id, content: entry.data }, null, 2),
15
+ )
16
+ }
@@ -1,6 +1,6 @@
1
1
  import type { APIRoute } from "astro"
2
2
 
3
- import pkg from "../../../package.json" assert { type: "json" }
3
+ import pkg from "../../package.json" assert { type: "json" }
4
4
 
5
5
  export const GET: APIRoute = () => {
6
6
  return new Response(JSON.stringify({ lightnet: pkg.version }))
@@ -132,6 +132,10 @@ export const configSchema = z.object({
132
132
  * Favicons for your site.
133
133
  */
134
134
  favicon: faviconSchema.array().optional(),
135
+ /**
136
+ * Enable displaying a “Powered by LightNet” link in your site’s footer.
137
+ */
138
+ credits: z.boolean().default(false),
135
139
  /**
136
140
  * Link to manifest file within public/ folder
137
141
  */
@@ -213,6 +217,21 @@ export const configSchema = z.object({
213
217
  hideHeaderSearchIcon: z.boolean().default(false),
214
218
  })
215
219
  .optional(),
220
+ /**
221
+ * Experimental features. Subject to change with any release.
222
+ */
223
+ experimental: z
224
+ .object({
225
+ /**
226
+ * Configure administration interface.
227
+ */
228
+ admin: z
229
+ .object({
230
+ enabled: z.boolean().default(false),
231
+ })
232
+ .optional(),
233
+ })
234
+ .optional(),
216
235
  })
217
236
 
218
237
  export type Language = z.input<typeof languageSchema>
@@ -19,6 +19,7 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
19
19
  updateConfig,
20
20
  logger,
21
21
  addMiddleware,
22
+ command,
22
23
  }) => {
23
24
  const config = verifySchema(
24
25
  configSchema,
@@ -46,23 +47,60 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
46
47
  })
47
48
 
48
49
  injectRoute({
49
- pattern: "/api/search.json",
50
- entrypoint: "lightnet/pages/api/search.ts",
50
+ pattern: "/[locale]/media/[mediaId]",
51
+ entrypoint: "lightnet/pages/DetailsPageRoute.astro",
51
52
  prerender: true,
52
53
  })
53
54
 
54
55
  injectRoute({
55
- pattern: "/api/versions.json",
56
- entrypoint: "lightnet/pages/api/versions.ts",
56
+ pattern: "/api/internal/search.json",
57
+ entrypoint: "lightnet/api/internal/search.ts",
57
58
  prerender: true,
58
59
  })
59
60
 
60
61
  injectRoute({
61
- pattern: "/[locale]/media/[mediaId]",
62
- entrypoint: "lightnet/pages/DetailsPageRoute.astro",
62
+ pattern: "/api/versions.json",
63
+ entrypoint: "lightnet/api/versions.ts",
63
64
  prerender: true,
64
65
  })
65
66
 
67
+ if (config.experimental?.admin?.enabled) {
68
+ injectRoute({
69
+ pattern: "/api/media/[mediaId].json",
70
+ entrypoint: "lightnet/api/media/[mediaId].ts",
71
+ prerender: true,
72
+ })
73
+
74
+ injectRoute({
75
+ pattern: "/[locale]/admin",
76
+ entrypoint: "lightnet/admin/pages/AdminRoute.astro",
77
+ prerender: true,
78
+ })
79
+ injectRoute({
80
+ pattern: "/[locale]/admin/media/[mediaId]",
81
+ entrypoint: "lightnet/admin/pages/media/EditRoute.astro",
82
+ prerender: true,
83
+ })
84
+ }
85
+
86
+ // During local development admin ui can use
87
+ // this endpoints to write files.
88
+ if (config.experimental?.admin?.enabled && command === "dev") {
89
+ injectRoute({
90
+ pattern: "/api/internal/fs/writeText",
91
+ entrypoint: "lightnet/api/internal/fs/writeText.ts",
92
+ prerender: false,
93
+ })
94
+ // Add empty adapter to avoid warning
95
+ // about missing adapter.
96
+ // This hack might break in the future :(
97
+ // We could also set the "node" adapter if no
98
+ // adapter has been set by user.
99
+ if (!astroConfig.adapter) {
100
+ updateConfig({ adapter: {} })
101
+ }
102
+ }
103
+
66
104
  addMiddleware({ entrypoint: "lightnet/locals", order: "pre" })
67
105
 
68
106
  astroConfig.integrations.push(tailwind(), react())
@@ -7,7 +7,7 @@ import { searchPagePath } from "../utils/paths"
7
7
  import CarouselSection from "./CarouselSection.astro"
8
8
  import Section, { type Props as SectionProps } from "./Section.astro"
9
9
 
10
- type Props = SectionProps & {
10
+ type Props = Omit<SectionProps, "maxWidth"> & {
11
11
  layout?: "grid" | "carousel"
12
12
  }
13
13
 
@@ -20,7 +20,7 @@ type MediaItem = {
20
20
  }
21
21
  }
22
22
 
23
- type Props = SectionProps & {
23
+ type Props = Omit<SectionProps, "maxWidth"> & {
24
24
  items: (MediaItem | undefined)[]
25
25
  layout: ItemStyle
26
26
  viewLayout?: "grid" | "carousel"
@@ -0,0 +1,55 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ export type ToastVariant = "info" | "success" | "warning" | "error"
4
+
5
+ export type ToastProps = {
6
+ id?: string
7
+ children: ReactNode
8
+ className?: string
9
+ variant?: ToastVariant
10
+ }
11
+
12
+ const variantClassName: Record<ToastVariant, string> = {
13
+ info: "border-slate-400 bg-white/95",
14
+ success: "border-emerald-500 bg-emerald-100",
15
+ warning: "border-amber-500 bg-amber-100",
16
+ error: "border-rose-500 bg-rose-100",
17
+ }
18
+
19
+ export default function Toast({
20
+ id,
21
+ children,
22
+ className = "",
23
+ variant = "info",
24
+ }: ToastProps) {
25
+ const alertClasses = variantClassName[variant] ?? variantClassName.info
26
+ const ariaLive = variant === "error" ? "assertive" : "polite"
27
+ const hiddenTransform = "translateY(1.5rem)"
28
+ const overshootTransform = "translateY(-0.25rem)"
29
+ const visibleTransform = "translateY(0)"
30
+
31
+ return (
32
+ <div
33
+ id={id}
34
+ className={`pointer-events-none fixed bottom-4 end-0 flex justify-end px-4 opacity-0 transition duration-300 will-change-transform ${className}`}
35
+ data-toast="true"
36
+ data-variant={variant}
37
+ data-toast-hidden-transform={hiddenTransform}
38
+ data-toast-overshoot-transform={overshootTransform}
39
+ data-toast-visible-transform={visibleTransform}
40
+ role="status"
41
+ aria-live={ariaLive}
42
+ style={{
43
+ transform: hiddenTransform,
44
+ transitionProperty: "opacity, transform",
45
+ transitionTimingFunction: "cubic-bezier(0.34, 1.56, 0.64, 1)",
46
+ }}
47
+ >
48
+ <div
49
+ className={`pointer-events-auto flex max-w-sm flex-col items-start gap-1 rounded-2xl border p-4 text-base shadow-md backdrop-blur-sm ${alertClasses}`}
50
+ >
51
+ {children}
52
+ </div>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,61 @@
1
+ const DEFAULT_DURATION_MS = 3000
2
+ const TIMEOUT_DATA_KEY = "toastHideTimeoutId"
3
+ const DEFAULT_HIDDEN_TRANSFORM = "translateY(1.5rem)"
4
+ const DEFAULT_VISIBLE_TRANSFORM = "translateY(0)"
5
+ const DEFAULT_OVERSHOOT_TRANSFORM = "translateY(-0.25rem)"
6
+
7
+ type ShowToastOptions = {
8
+ duration?: number
9
+ }
10
+
11
+ /**
12
+ * Shows a toast element by toggling its opacity and schedules it to hide again.
13
+ * Works with markup rendered by the Toast component but can target any element.
14
+ */
15
+ export function showToast(
16
+ element: HTMLElement,
17
+ options: ShowToastOptions = {},
18
+ ) {
19
+ const duration = options.duration ?? DEFAULT_DURATION_MS
20
+ const existingTimeoutId = element.dataset[TIMEOUT_DATA_KEY]
21
+ const hiddenTransform =
22
+ element.dataset.toastHiddenTransform ?? DEFAULT_HIDDEN_TRANSFORM
23
+ const overshootTransform =
24
+ element.dataset.toastOvershootTransform ?? DEFAULT_OVERSHOOT_TRANSFORM
25
+ const visibleTransform =
26
+ element.dataset.toastVisibleTransform ?? DEFAULT_VISIBLE_TRANSFORM
27
+
28
+ if (existingTimeoutId) {
29
+ window.clearTimeout(Number(existingTimeoutId))
30
+ }
31
+
32
+ element.style.opacity = "100%"
33
+ element.style.transform = overshootTransform
34
+ element.dataset.toastVisible = "true"
35
+
36
+ const settleIntoPlace = () => {
37
+ element.style.transform = visibleTransform
38
+ }
39
+
40
+ window.requestAnimationFrame(() => {
41
+ window.requestAnimationFrame(settleIntoPlace)
42
+ })
43
+
44
+ const timeoutId = window.setTimeout(() => {
45
+ element.style.opacity = "0%"
46
+ element.style.transform = hiddenTransform
47
+ element.dataset.toastVisible = "false"
48
+ delete element.dataset[TIMEOUT_DATA_KEY]
49
+ }, duration)
50
+
51
+ element.dataset[TIMEOUT_DATA_KEY] = String(timeoutId)
52
+ }
53
+
54
+ export function showToastById(id: string, options?: ShowToastOptions) {
55
+ const element = document.getElementById(id)
56
+ if (!element) {
57
+ return
58
+ }
59
+
60
+ showToast(element as HTMLElement, options)
61
+ }
@@ -1,17 +1,4 @@
1
- import { type ImageFunction, z } from "astro:content"
2
-
3
- /**
4
- * We use this function to make sure decap's relative paths will resolve correctly
5
- * with astro content.
6
- *
7
- * @param image astro content image function
8
- * @returns image property
9
- */
10
- export const astroImage = (image: ImageFunction) =>
11
- z
12
- .string()
13
- .transform((path) => (path.startsWith("./") ? path : `./${path}`))
14
- .pipe(image())
1
+ import { z } from "astro/zod"
15
2
 
16
3
  /**
17
4
  * The Astro image function resolves to this schema.
@@ -3,7 +3,7 @@ import { z } from "astro/zod"
3
3
  import type { SchemaContext } from "astro:content"
4
4
  import { defineCollection, reference } from "astro:content"
5
5
 
6
- import { astroImage, imageSchema } from "./astro-image"
6
+ import { imageSchema } from "./astro-image"
7
7
 
8
8
  /**
9
9
  * Category Schema
@@ -172,12 +172,12 @@ export const mediaItemSchema = z.object({
172
172
  */
173
173
  export const createMediaItemSchema = ({ image }: SchemaContext) =>
174
174
  mediaItemSchema.extend({
175
- image: astroImage(image),
175
+ image: image(),
176
176
  })
177
177
 
178
178
  export const createCategorySchema = ({ image }: SchemaContext) =>
179
179
  categorySchema.extend({
180
- image: astroImage(image).optional(),
180
+ image: image().optional(),
181
181
  })
182
182
 
183
183
  /**
@@ -315,6 +315,13 @@ export const LIGHTNET_COLLECTIONS = {
315
315
  }),
316
316
  schema: mediaTypeSchema,
317
317
  }),
318
+ "internal-media-image-path": defineCollection({
319
+ loader: glob({
320
+ pattern: "*.json",
321
+ base: "./src/content/media",
322
+ }),
323
+ schema: z.object({ image: z.string() }),
324
+ }),
318
325
  }
319
326
 
320
327
  export const mediaItemEntrySchema = z.object({
@@ -1,7 +1,7 @@
1
1
  import { getCollection, getEntry } from "astro:content"
2
2
 
3
3
  import { verifySchemaAsync } from "../utils/verify-schema"
4
- import { mediaItemEntrySchema } from "./content-schema"
4
+ import { type MediaItemEntry, mediaItemEntrySchema } from "./content-schema"
5
5
 
6
6
  /**
7
7
  * Internal API to get media items. Since this package is a Astro integration
@@ -26,3 +26,48 @@ const prepareItem = async (item: unknown) => {
26
26
  (id) => `Fix these issues inside "src/content/media/${id}.json":`,
27
27
  )
28
28
  }
29
+
30
+ /**
31
+ * Revert media items like they it is stored in the
32
+ * content collection folder.
33
+ */
34
+ export const getRawMediaItem = async (id: string) => {
35
+ const item = await getMediaItem(id)
36
+ return revertMediaItemEntry(item)
37
+ }
38
+
39
+ /**
40
+ * Revert media items like they are stored in the
41
+ * content collection folder.
42
+ */
43
+ export const getRawMediaItems = async () => {
44
+ const mediaItems = await getMediaItems()
45
+ return Promise.all(mediaItems.map(revertMediaItemEntry))
46
+ }
47
+
48
+ /**
49
+ * Returns the media item like it is stored in the content collection json.
50
+ * We need to revert Astro's modifications to references and images.
51
+ *
52
+ * @param mediaItem media item parsed by Astro
53
+ * @returns media item like before parsing
54
+ */
55
+ async function revertMediaItemEntry({ id, data: mediaItem }: MediaItemEntry) {
56
+ const type = mediaItem.type.id
57
+ const categories = mediaItem.categories?.map((category) => category.id)
58
+ const collections = mediaItem.collections?.map((collection) => ({
59
+ ...collection,
60
+ collection: collection.collection.id,
61
+ }))
62
+ const image = (await getEntry("internal-media-image-path", id))?.data.image
63
+ return {
64
+ id,
65
+ data: {
66
+ ...mediaItem,
67
+ type,
68
+ categories,
69
+ collections,
70
+ image,
71
+ },
72
+ }
73
+ }
@@ -22,3 +22,4 @@ ln.details.download: تنزيل
22
22
  ln.share.url-copied-to-clipboard: تم نسخ الرابط إلى الحافظة
23
23
  ln.404.page-not-found: الصفحة غير موجودة
24
24
  ln.404.go-to-the-home-page: العودة إلى الصفحة الرئيسية
25
+ ln.footer.powered-by-lightnet: مدعوم من LightNet
@@ -22,3 +22,4 @@ ln.details.download: ডাউনলোড করুন
22
22
  ln.share.url-copied-to-clipboard: লিঙ্ক ক্লিপবোর্ডে কপি হয়েছে
23
23
  ln.404.page-not-found: পৃষ্ঠা পাওয়া যায়নি
24
24
  ln.404.go-to-the-home-page: হোম পৃষ্ঠায় যান
25
+ ln.footer.powered-by-lightnet: LightNet দ্বারা পরিচালিত
@@ -22,3 +22,4 @@ ln.search.no-results: Keine Ergebnisse
22
22
  ln.search.placeholder: Suche Medien
23
23
  ln.search.title: Suche
24
24
  ln.share.url-copied-to-clipboard: Link in die Zwischenablage kopiert
25
+ ln.footer.powered-by-lightnet: Ermöglicht durch LightNet
@@ -143,3 +143,6 @@ ln.404.page-not-found: Page not found
143
143
  # English: Go to Home page
144
144
  # Used on: https://sk8-ministries.pages.dev/unexisting-path
145
145
  ln.404.go-to-the-home-page: Go to Home page
146
+
147
+ # Footer text to give credits to LightNet
148
+ ln.footer.powered-by-lightnet: Powered by LightNet
@@ -22,3 +22,4 @@ ln.details.download: Descargar
22
22
  ln.share.url-copied-to-clipboard: Enlace copiado al portapapeles
23
23
  ln.404.page-not-found: Página no encontrada
24
24
  ln.404.go-to-the-home-page: Ir a la página de inicio
25
+ ln.footer.powered-by-lightnet: Impulsado por LightNet
@@ -22,3 +22,4 @@ ln.details.download: Lataa
22
22
  ln.share.url-copied-to-clipboard: Linkki kopioitu leikepöydälle
23
23
  ln.404.page-not-found: Sivua ei löytynyt
24
24
  ln.404.go-to-the-home-page: Palaa etusivulle
25
+ ln.footer.powered-by-lightnet: Toimii LightNet-teknologialla
@@ -22,3 +22,4 @@ ln.details.download: Télécharger
22
22
  ln.share.url-copied-to-clipboard: Lien copié dans le presse-papiers
23
23
  ln.404.page-not-found: Page non trouvée
24
24
  ln.404.go-to-the-home-page: Aller à la page d’accueil
25
+ ln.footer.powered-by-lightnet: Propulsé par LightNet
@@ -22,3 +22,4 @@ ln.details.download: डाउनलोड करें
22
22
  ln.share.url-copied-to-clipboard: लिंक क्लिपबोर्ड पर कॉपी हो गया है
23
23
  ln.404.page-not-found: पृष्ठ नहीं मिला
24
24
  ln.404.go-to-the-home-page: मुखपृष्ठ पर जाएँ
25
+ ln.footer.powered-by-lightnet: LightNet द्वारा संचालित
@@ -20,3 +20,4 @@ ln.details.download: Жүктеу
20
20
  ln.share.url-copied-to-clipboard: Жүктеме көшірілді
21
21
  ln.404.page-not-found: Ештеңе табылмады
22
22
  ln.404.go-to-the-home-page: Бастапқы бетке өту
23
+ ln.footer.powered-by-lightnet: LightNet платформасында жұмыс істейді
@@ -22,3 +22,4 @@ ln.details.download: Transferir
22
22
  ln.share.url-copied-to-clipboard: Ligação copiada para a área de transferência
23
23
  ln.404.page-not-found: Página não encontrada
24
24
  ln.404.go-to-the-home-page: Ir para a página inicial
25
+ ln.footer.powered-by-lightnet: Com tecnologia LightNet
@@ -22,3 +22,4 @@ ln.details.download: Скачать
22
22
  ln.share.url-copied-to-clipboard: Ссылка скопированa в буфер
23
23
  ln.404.page-not-found: Страница не найдена
24
24
  ln.404.go-to-the-home-page: Перейти на главную страницу
25
+ ln.footer.powered-by-lightnet: Работает на платформе LightNet
@@ -22,3 +22,4 @@ ln.details.download: Завантажити
22
22
  ln.share.url-copied-to-clipboard: Посилання скопійовано до буферу обміну
23
23
  ln.404.page-not-found: Сторінку не знайдено
24
24
  ln.404.go-to-the-home-page: Перейти на Головну сторінку
25
+ ln.footer.powered-by-lightnet: Працює на платформі LightNet
@@ -22,3 +22,4 @@ ln.details.download: 下载
22
22
  ln.share.url-copied-to-clipboard: 链接已复制到剪贴板
23
23
  ln.404.page-not-found: 页面未找到
24
24
  ln.404.go-to-the-home-page: 返回首页
25
+ ln.footer.powered-by-lightnet: 由 LightNet 提供支持