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
@@ -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
  }
@@ -81,3 +93,5 @@ export type LightNetTranslationKey =
81
93
  | "ln.search.placeholder"
82
94
  | "ln.search.title"
83
95
  | "ln.share.url-copied-to-clipboard"
96
+ | "ln.footer.powered-by-lightnet"
97
+ | AdminTranslationKey
@@ -5,6 +5,7 @@ import config from "virtual:lightnet/config"
5
5
 
6
6
  import { resolveLanguage } from "../i18n/resolve-language"
7
7
  import Favicon from "./components/Favicon.astro"
8
+ import Footer from "./components/Footer.astro"
8
9
  import Header from "./components/Header.astro"
9
10
  import PreloadReact from "./components/PreloadReact"
10
11
  import ViewTransition from "./components/ViewTransition.astro"
@@ -30,7 +31,7 @@ const language = resolveLanguage(currentLocale)
30
31
  <title>{title ? `${title} | ${configTitle}` : configTitle}</title>
31
32
  {description && <meta name="description" content={description} />}
32
33
  {config.manifest && <link rel="manifest" href={config.manifest} />}
33
- <link rel="prefetch" href="/api/search.json" />
34
+ <link rel="prefetch" href="/api/internal/search.json" />
34
35
  <Favicon />
35
36
  <ViewTransition />
36
37
  </head>
@@ -41,7 +42,7 @@ const language = resolveLanguage(currentLocale)
41
42
  <main class="grow pb-8 pt-14 sm:py-20">
42
43
  <slot />
43
44
  </main>
44
- {CustomFooter && <CustomFooter />}
45
+ {CustomFooter ? <CustomFooter /> : <Footer />}
45
46
  <PreloadReact client:idle />
46
47
  </body>
47
48
  </html>
@@ -0,0 +1,24 @@
1
+ ---
2
+ import config from "virtual:lightnet/config"
3
+
4
+ import LightNetLogo from "./LightNetLogo.svg"
5
+
6
+ if (!config.credits) {
7
+ return
8
+ }
9
+ ---
10
+
11
+ <footer class="w-full border-t border-gray-300">
12
+ <div
13
+ class="mx-auto flex w-full max-w-screen-xl justify-end px-4 py-4 md:px-8"
14
+ >
15
+ <a
16
+ class="flex items-center gap-2 py-2 text-sm text-gray-800 hover:underline"
17
+ href="https://lightnet.community"
18
+ target="_blank"
19
+ >
20
+ <img src={LightNetLogo.src} alt="" class="h-5 w-auto" loading="lazy" />
21
+ {Astro.locals.i18n.t("ln.footer.powered-by-lightnet")}
22
+ </a>
23
+ </div>
24
+ </footer>
@@ -0,0 +1 @@
1
+ <svg viewBox="16 16 141 173" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M88.007 180.642c-21.767-5.846-39.688-19.304-40.574-42.862-.887-23.558 17.751-43.334 45.426-40.84v-7.844c.011-4.03 4.977-5.495 8.021-2.928l29.136 23.729c2.155 1.821 2.15 5.16-.019 7.08l-29.117 24.152c-3.059 2.72-8.031.681-8.02-3.349v-8.005c-2.916-.453-5.137-.579-6.708-.453-25.207 2.016-23.083 44.6 20.454 42.388 18.705-.951 49.148-21.73 50.394-53.985 0-48.62-60.4-64.433-45.205-97.762 1.529-3.354-4.627-4.867-8.501-3.393-15.573 4.29-32.752 13.714-42.526 41.693-1.251 3.581-2.824 12.038-4.168 15.558-1.242 3.533-3.034 11.445-7.996 11.025-6.501-.55-4.236-11.168-2.815-15.74 1.197-3.854-4.14-7.109-9.028-2.073-8.3 8.549-16.598 19.982-19.804 37.823-1.259 7.001-1.327 16.903.101 25.208 6.204 36.183 37.705 59.276 68.356 58.915 7.58-.09 7.787-6.942 2.593-8.337Z" fill="#374151"/></svg>
@@ -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