lightnet 3.10.0 → 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 (47) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/__e2e__/admin.spec.ts +113 -0
  3. package/__e2e__/fixtures/basics/astro.config.mjs +6 -0
  4. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  5. package/__e2e__/fixtures/basics/package.json +2 -2
  6. package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +15 -0
  7. package/__e2e__/fixtures/basics/src/content/media-types/audio.json +7 -0
  8. package/__e2e__/fixtures/basics/src/translations/de.yml +1 -0
  9. package/__e2e__/fixtures/basics/src/translations/en.yml +1 -0
  10. package/__e2e__/homepage.spec.ts +21 -0
  11. package/package.json +13 -6
  12. package/src/admin/api/fs/writeText.ts +50 -0
  13. package/src/admin/components/form/FieldErrors.tsx +19 -0
  14. package/src/admin/components/form/SubmitButton.tsx +77 -0
  15. package/src/admin/components/form/TextField.tsx +24 -0
  16. package/src/admin/components/form/form-context.ts +4 -0
  17. package/src/admin/components/form/index.ts +16 -0
  18. package/src/admin/i18n/translations/en.yml +1 -0
  19. package/src/admin/i18n/translations.ts +5 -0
  20. package/src/admin/pages/AdminRoute.astro +16 -0
  21. package/src/admin/pages/media/EditForm.tsx +58 -0
  22. package/src/admin/pages/media/EditRoute.astro +33 -0
  23. package/src/admin/pages/media/file-system.ts +37 -0
  24. package/src/admin/pages/media/media-item-store.ts +11 -0
  25. package/src/admin/types/media-item.ts +8 -0
  26. package/src/api/media/[mediaId].ts +16 -0
  27. package/src/{pages/api → api}/versions.ts +1 -1
  28. package/src/astro-integration/config.ts +15 -0
  29. package/src/astro-integration/integration.ts +44 -6
  30. package/src/components/CategoriesSection.astro +1 -1
  31. package/src/components/MediaGallerySection.astro +1 -1
  32. package/src/components/Toast.tsx +55 -0
  33. package/src/components/showToast.ts +61 -0
  34. package/src/content/astro-image.ts +1 -14
  35. package/src/content/content-schema.ts +10 -3
  36. package/src/content/get-media-items.ts +46 -1
  37. package/src/i18n/translations.ts +20 -7
  38. package/src/layouts/Page.astro +1 -1
  39. package/src/pages/details-page/components/MainDetailsSection.astro +5 -1
  40. package/src/pages/details-page/components/VideoDetailsSection.astro +5 -1
  41. package/src/pages/details-page/components/main-details/EditButton.astro +30 -0
  42. package/src/pages/details-page/components/main-details/ShareButton.astro +9 -13
  43. package/src/pages/{api → search-page/api}/search.ts +3 -3
  44. package/src/pages/search-page/components/SearchListItem.tsx +1 -1
  45. package/src/pages/search-page/hooks/use-search.ts +3 -3
  46. package/tailwind.config.ts +1 -0
  47. /package/src/pages/{api → search-page/api}/search-response.ts +0 -0
@@ -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 }))
@@ -217,6 +217,21 @@ export const configSchema = z.object({
217
217
  hideHeaderSearchIcon: z.boolean().default(false),
218
218
  })
219
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(),
220
235
  })
221
236
 
222
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
+ }
@@ -1,5 +1,10 @@
1
1
  import YAML from "yaml"
2
2
 
3
+ import {
4
+ type AdminTranslationKey,
5
+ builtInAdminTranslations,
6
+ } from "../admin/i18n/translations"
7
+
3
8
  const builtInTranslations = {
4
9
  ar: () => import("./translations/ar.yml?raw"),
5
10
  bn: () => import("./translations/bn.yml?raw"),
@@ -32,23 +37,30 @@ const userTranslations = Object.fromEntries(
32
37
  )
33
38
 
34
39
  export const loadTranslations = async (bcp47: string) => ({
35
- ...(await loadBuiltInTranslations(bcp47)),
40
+ ...(await loadBuiltInTranslations(builtInTranslations, bcp47)),
41
+ ...(await loadBuiltInTranslations(builtInAdminTranslations, bcp47)),
36
42
  ...(await loadUserTranslations(bcp47)),
37
43
  })
38
44
 
39
- function isBuiltInLanguage(bcp47: string): bcp47 is BuiltInLanguage {
40
- return Object.hasOwn(builtInTranslations, bcp47)
45
+ function hasTranslations(
46
+ translationMap: Record<string, unknown>,
47
+ bcp47: string,
48
+ ): bcp47 is BuiltInLanguage {
49
+ return Object.hasOwn(translationMap, bcp47)
41
50
  }
42
51
 
43
- export const loadBuiltInTranslations = async (bcp47: string) => {
44
- if (!isBuiltInLanguage(bcp47)) {
52
+ const loadBuiltInTranslations = async (
53
+ translationMap: Record<string, () => Promise<typeof import("*?raw")>>,
54
+ bcp47: string,
55
+ ) => {
56
+ if (!hasTranslations(translationMap, bcp47)) {
45
57
  return {}
46
58
  }
47
- const yml = (await builtInTranslations[bcp47]()).default
59
+ const yml = (await translationMap[bcp47]()).default
48
60
  return YAML.parse(yml)
49
61
  }
50
62
 
51
- export const loadUserTranslations = async (bcp47: string) => {
63
+ const loadUserTranslations = async (bcp47: string) => {
52
64
  if (!userTranslations[bcp47]) {
53
65
  return {}
54
66
  }
@@ -82,3 +94,4 @@ export type LightNetTranslationKey =
82
94
  | "ln.search.title"
83
95
  | "ln.share.url-copied-to-clipboard"
84
96
  | "ln.footer.powered-by-lightnet"
97
+ | AdminTranslationKey
@@ -31,7 +31,7 @@ const language = resolveLanguage(currentLocale)
31
31
  <title>{title ? `${title} | ${configTitle}` : configTitle}</title>
32
32
  {description && <meta name="description" content={description} />}
33
33
  {config.manifest && <link rel="manifest" href={config.manifest} />}
34
- <link rel="prefetch" href="/api/search.json" />
34
+ <link rel="prefetch" href="/api/internal/search.json" />
35
35
  <Favicon />
36
36
  <ViewTransition />
37
37
  </head>
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import Authors from "./main-details/Authors.astro"
3
3
  import CoverImage from "./main-details/CoverImage.astro"
4
+ import EditButton from "./main-details/EditButton.astro"
4
5
  import ShareButton from "./main-details/ShareButton.astro"
5
6
  import Title from "./main-details/Title.astro"
6
7
 
@@ -19,7 +20,10 @@ const { mediaId, imageSize } = Astro.props
19
20
  <div class="flex w-full grow flex-col items-center sm:items-start">
20
21
  <Title className="text-center sm:text-start" mediaId={mediaId} />
21
22
  <Authors className="mt-2" mediaId={mediaId} />
22
- <ShareButton className="mt-3" />
23
+ <div class="mt-4 flex gap-6">
24
+ <ShareButton />
25
+ <EditButton mediaId={mediaId} />
26
+ </div>
23
27
  <slot />
24
28
  </div>
25
29
  </div>
@@ -2,6 +2,7 @@
2
2
  import VideoPlayer from "../../../components/VideoPlayer.astro"
3
3
  import { getMediaItem } from "../../../content/get-media-items"
4
4
  import Authors from "./main-details/Authors.astro"
5
+ import EditButton from "./main-details/EditButton.astro"
5
6
  import ShareButton from "./main-details/ShareButton.astro"
6
7
  import Title from "./main-details/Title.astro"
7
8
 
@@ -26,5 +27,8 @@ const item = await getMediaItem(mediaId)
26
27
  >
27
28
  <Title mediaId={mediaId} />
28
29
  <Authors mediaId={mediaId} />
29
- <ShareButton className="mt-3" />
30
+ <div class="mt-4 flex gap-6">
31
+ <ShareButton />
32
+ <EditButton mediaId={mediaId} />
33
+ </div>
30
34
  </div>
@@ -0,0 +1,30 @@
1
+ ---
2
+ import config from "virtual:lightnet/config"
3
+
4
+ import Icon from "../../../../components/Icon"
5
+
6
+ interface Props {
7
+ mediaId: string
8
+ }
9
+
10
+ const { mediaId } = Astro.props
11
+ ---
12
+
13
+ <a
14
+ class="hidden cursor-pointer items-center gap-2 font-bold text-gray-700 underline"
15
+ id="edit-btn"
16
+ data-admin-enabled={config.experimental?.admin?.enabled}
17
+ href={`/${Astro.currentLocale}/admin/media/${mediaId}`}
18
+ ><Icon className="mdi--square-edit-outline" ariaLabel="" />
19
+ {Astro.locals.i18n.t("ln.admin.edit")}</a
20
+ >
21
+ <script>
22
+ const btn: HTMLAnchorElement | null = document.querySelector("#edit-btn")
23
+ const showEditButton =
24
+ btn?.dataset.adminEnabled === "true" &&
25
+ (import.meta.env.DEV || localStorage.getItem("ln-admin-enabled") === "true")
26
+ if (showEditButton) {
27
+ btn?.classList.remove("hidden")
28
+ btn?.classList.add("flex")
29
+ }
30
+ </script>
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import Icon from "../../../../components/Icon"
3
+ import Toast from "../../../../components/Toast"
3
4
 
4
5
  interface Props {
5
6
  className?: string
@@ -13,16 +14,15 @@ interface Props {
13
14
  ><Icon className="mdi--share" ariaLabel="" />
14
15
  {Astro.locals.i18n.t("ln.details.share")}</button
15
16
  >
16
- <div
17
- id="share-success"
18
- class="dy-toast pointer-events-none opacity-0 transition-opacity duration-300"
19
- >
20
- <div class="dy-alert dy-alert-success">
21
- <span>{Astro.locals.i18n.t("ln.share.url-copied-to-clipboard")}</span>
22
- </div>
23
- </div>
17
+ <Toast id="share-success" variant="success">
18
+ {Astro.locals.i18n.t("ln.share.url-copied-to-clipboard")}
19
+ </Toast>
24
20
  <script>
21
+ import { showToastById } from "../../../../components/showToast"
22
+
25
23
  const btn = document.querySelector("#share-btn")
24
+ const toastId = "share-success"
25
+
26
26
  btn?.addEventListener("click", () => {
27
27
  if (navigator.share) {
28
28
  navigator
@@ -34,11 +34,7 @@ interface Props {
34
34
  navigator.clipboard
35
35
  .writeText(window.location.href)
36
36
  .then(() => {
37
- const toast = document.querySelector<HTMLElement>("#share-success")!
38
- toast.style.opacity = "100%"
39
- setTimeout(() => {
40
- toast.style.opacity = "0%"
41
- }, 3000)
37
+ showToastById(toastId)
42
38
  })
43
39
  .catch((error) => console.log("Error copying URL to clipboard:", error))
44
40
  }
@@ -1,9 +1,9 @@
1
1
  import type { APIRoute } from "astro"
2
2
  import { getImage } from "astro:assets"
3
3
 
4
- import type { MediaItemEntry } from "../../content/content-schema"
5
- import { getMediaItems } from "../../content/get-media-items"
6
- import { markdownToText } from "../../utils/markdown"
4
+ import type { MediaItemEntry } from "../../../content/content-schema"
5
+ import { getMediaItems } from "../../../content/get-media-items"
6
+ import { markdownToText } from "../../../utils/markdown"
7
7
  import type { SearchItem } from "./search-response"
8
8
 
9
9
  export const GET: APIRoute = async () => {
@@ -1,7 +1,7 @@
1
1
  import CoverImageDecorator from "../../../components/CoverImageDecorator"
2
2
  import Icon from "../../../components/Icon"
3
3
  import { detailsPagePath } from "../../../utils/paths"
4
- import type { SearchItem } from "../../api/search-response"
4
+ import type { SearchItem } from "../api/search-response"
5
5
 
6
6
  export type MediaType = {
7
7
  name: string
@@ -1,7 +1,7 @@
1
1
  import Fuse from "fuse.js"
2
2
  import { useEffect, useMemo, useRef, useState } from "react"
3
3
 
4
- import type { SearchItem, SearchResponse } from "../../api/search-response"
4
+ import type { SearchItem, SearchResponse } from "../api/search-response"
5
5
  import { observeSearchQuery, type SearchQuery } from "../utils/search-query"
6
6
 
7
7
  interface Context {
@@ -28,10 +28,10 @@ export function useSearch({ categories, mediaTypes, languages }: Context) {
28
28
  })
29
29
  const fetchData = async () => {
30
30
  try {
31
- const response = await fetch("/api/search.json")
31
+ const response = await fetch("/api/internal/search.json")
32
32
  if (!response.ok) {
33
33
  throw new Error(
34
- "Was not able to load search results from /api/search.json.",
34
+ "Was not able to load search results from /api/internal/search.json.",
35
35
  )
36
36
  }
37
37
  const { items }: SearchResponse = await response.json()
@@ -35,6 +35,7 @@ export function lightnetStyles({
35
35
  secondary: primary,
36
36
  accent: primary,
37
37
  neutral: "#030712",
38
+ error: "#9f1239",
38
39
  "base-100": "#f9fafb",
39
40
 
40
41
  "--rounded-box": "0.375rem", // border radius rounded-box utility class, used in card and other large boxes