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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # lightnet
2
2
 
3
+ ## 3.10.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#321](https://github.com/LightNetDev/LightNet/pull/321) [`87e0faf`](https://github.com/LightNetDev/LightNet/commit/87e0faf4ba7a6615d60e755385479092c5d2aa84) Thanks [@smn-cds](https://github.com/smn-cds)! - Update dependencies
8
+
9
+ - [#321](https://github.com/LightNetDev/LightNet/pull/321) [`87e0faf`](https://github.com/LightNetDev/LightNet/commit/87e0faf4ba7a6615d60e755385479092c5d2aa84) Thanks [@smn-cds](https://github.com/smn-cds)! - Start implementing integrated experimental Admin UI.
10
+
11
+ - [#321](https://github.com/LightNetDev/LightNet/pull/321) [`87e0faf`](https://github.com/LightNetDev/LightNet/commit/87e0faf4ba7a6615d60e755385479092c5d2aa84) Thanks [@smn-cds](https://github.com/smn-cds)! - Astro 5.14.8 resolves relative image paths like `image/my-image.jpg` without a `./` prefix, so remove the manual prefixing in `packages/lightnet/src/content/astro-image.ts`. See [Astro's release notes for version 5.14.8](https://github.com/withastro/astro/releases/tag/astro%405.14.8).
12
+
13
+ - [#323](https://github.com/LightNetDev/LightNet/pull/323) [`a2e6980`](https://github.com/LightNetDev/LightNet/commit/a2e6980b2866795b1517dc7a916698f5d92231b1) Thanks [@smn-cds](https://github.com/smn-cds)! - Remove the unsupported `maxWidth` option from `MediaGallerySection` and `CategoriesSection`.
14
+
15
+ The option never worked correctly, but our docs previously only mentioned that in a note. Tighten the typings and runtime guardrails so that consumers see explicit feedback instead of misconfiguring the component.
16
+
17
+ ## 3.10.0
18
+
19
+ ### Minor Changes
20
+
21
+ - [#315](https://github.com/LightNetDev/LightNet/pull/315) [`f01fd72`](https://github.com/LightNetDev/LightNet/commit/f01fd728efe1577248db40111b9dfe5bd1a33423) Thanks [@smn-cds](https://github.com/smn-cds)! - Add optional LightNet credits footer
22
+ - Adds `credits: boolean` to LightNet config to show a “Built with LightNet” footer; default is `false`
23
+ - Footer includes LightNet logo/text and appears when no `CustomFooter` is provided
24
+ - Adds i18n key `ln.footer.powered-by-lightnet` (English + AI generated translations provided)
25
+ - Enable via `astro.config.mjs`, lightnet config: `credits: true` (PR #315)
26
+
3
27
  ## 3.9.1
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -19,6 +19,10 @@ to see how it powers a live site.
19
19
 
20
20
  Take a look at the [LightNet developer docs](https://docs.lightnet.community).
21
21
 
22
+ ## 🛟 Getting support
23
+
24
+ If you have questions about using LightNet or need guidance, visit [Getting support & getting involved](https://docs.lightnet.community/concepts/collaboration/#getting-support--getting-involved).
25
+
22
26
  ## 🤝 Contributing
23
27
 
24
28
  Got ideas or improvements? We’d love your help! [See the contribution guide](https://github.com/LightNetDev/lightnet/blob/main/CONTRIBUTING.md) to get started.
@@ -0,0 +1,113 @@
1
+ import { expect } from "@playwright/test"
2
+
3
+ import { lightnetTest } from "./test-utils"
4
+
5
+ const test = lightnetTest("./fixtures/basics/")
6
+
7
+ test("Should not show `Edit` button on details page by default.", async ({
8
+ page,
9
+ startLightnet,
10
+ }) => {
11
+ await startLightnet()
12
+
13
+ await page.getByRole("link", { name: "Faithful Freestyle" }).click()
14
+ await expect(
15
+ page.getByRole("heading", { name: "Faithful Freestyle" }),
16
+ ).toBeVisible()
17
+
18
+ const editButton = page.locator("#edit-btn")
19
+ await expect(editButton).toBeHidden()
20
+ })
21
+
22
+ test("Should show `Edit` button on book details page after visiting `/en/admin/` path.", async ({
23
+ page,
24
+ startLightnet,
25
+ }) => {
26
+ const ln = await startLightnet()
27
+
28
+ await page.goto(ln.resolveURL("/en/admin/"))
29
+ await expect(
30
+ page.getByText("Admin features are enabled now.", { exact: true }),
31
+ ).toBeVisible()
32
+
33
+ await page.goto(ln.resolveURL("/en/media/faithful-freestyle--en"))
34
+ await expect(
35
+ page.getByRole("heading", { name: "Faithful Freestyle" }),
36
+ ).toBeVisible()
37
+
38
+ const editButton = page.locator("#edit-btn")
39
+ await expect(editButton).toBeVisible()
40
+ await expect(editButton).toHaveAttribute(
41
+ "href",
42
+ "/en/admin/media/faithful-freestyle--en",
43
+ )
44
+ })
45
+
46
+ test("Should show `Edit` button on video details page after visiting `/en/admin/` path.", async ({
47
+ page,
48
+ startLightnet,
49
+ }) => {
50
+ const ln = await startLightnet()
51
+
52
+ await page.goto(ln.resolveURL("/en/admin/"))
53
+ await expect(
54
+ page.getByText("Admin features are enabled now.", { exact: true }),
55
+ ).toBeVisible()
56
+
57
+ await page.goto(ln.resolveURL("/en/media/how-to-kickflip--de"))
58
+ await expect(
59
+ page.getByRole("heading", { name: "Kickflip Anleitung" }),
60
+ ).toBeVisible()
61
+
62
+ const editButton = page.locator("#edit-btn")
63
+ await expect(editButton).toBeVisible()
64
+ await expect(editButton).toHaveAttribute(
65
+ "href",
66
+ "/en/admin/media/how-to-kickflip--de",
67
+ )
68
+ })
69
+
70
+ test("Should show `Edit` button on audio details page after visiting `/en/admin/` path.", async ({
71
+ page,
72
+ startLightnet,
73
+ }) => {
74
+ const ln = await startLightnet()
75
+
76
+ await page.goto(ln.resolveURL("/en/admin/"))
77
+ await expect(
78
+ page.getByText("Admin features are enabled now.", { exact: true }),
79
+ ).toBeVisible()
80
+
81
+ await page.goto(ln.resolveURL("/en/media/skate-sounds--en"))
82
+ await expect(
83
+ page.getByRole("heading", { name: "Skate Sounds" }),
84
+ ).toBeVisible()
85
+
86
+ const editButton = page.locator("#edit-btn")
87
+ await expect(editButton).toBeVisible()
88
+ await expect(editButton).toHaveAttribute(
89
+ "href",
90
+ "/en/admin/media/skate-sounds--en",
91
+ )
92
+ })
93
+
94
+ test("Edit button on details page should navigate to media item edit page", async ({
95
+ page,
96
+ startLightnet,
97
+ }) => {
98
+ const ln = await startLightnet()
99
+
100
+ await page.goto(ln.resolveURL("/en/admin/"))
101
+ await page.goto(ln.resolveURL("/en/media/faithful-freestyle--en"))
102
+
103
+ const editButton = page.locator("#edit-btn")
104
+ await expect(editButton).toBeVisible()
105
+
106
+ await editButton.click()
107
+ await expect(page).toHaveURL(
108
+ ln.resolveURL("/en/admin/media/faithful-freestyle--en"),
109
+ )
110
+ await expect(
111
+ page.getByText("Edit media item", { exact: false }),
112
+ ).toBeVisible()
113
+ })
@@ -9,6 +9,7 @@ export default defineConfig({
9
9
  lightnet({
10
10
  title: "Basic Test",
11
11
  logo: { src: "./src/assets/logo.png" },
12
+ credits: true,
12
13
  languages: [
13
14
  {
14
15
  code: "en",
@@ -29,6 +30,11 @@ export default defineConfig({
29
30
  },
30
31
  { href: "/media", label: "ln.search.title" },
31
32
  ],
33
+ experimental: {
34
+ admin: {
35
+ enabled: true,
36
+ },
37
+ },
32
38
  }),
33
39
  ],
34
40
  })
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.13.3_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.48.1_terser@5.39.0_typescript@5.9.2_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.13.3_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.48.1_terser@5.39.0_typescript@5.9.2_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.14.8_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.52.5_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.14.8_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.52.5_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.13.3_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.48.1_terser@5.39.0_typescript@5.9.2_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.13.3_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.48.1_terser@5.39.0_typescript@5.9.2_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.14.8_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.52.5_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.14.8_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.52.5_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../astro/astro.js" "$@"
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../tailwindcss/lib/cli.js" "$@"
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.17/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.18_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../tailwindcss/lib/cli.js" "$@"
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
@@ -10,9 +10,9 @@ case `uname` in
10
10
  esac
11
11
 
12
12
  if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
13
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
14
14
  else
15
- export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
16
16
  fi
17
17
  if [ -x "$basedir/node" ]; then
18
18
  exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
@@ -4,16 +4,16 @@
4
4
  "version": "0.0.1",
5
5
  "private": "true",
6
6
  "dependencies": {
7
- "@astrojs/react": "^4.3.0",
7
+ "@astrojs/react": "^4.4.0",
8
8
  "@astrojs/tailwind": "^6.0.2",
9
- "@lightnet/decap-admin": "^3.1.2",
10
- "astro": "^5.13.3",
11
- "lightnet": "^3.9.0",
12
- "react": "^19.1.1",
13
- "react-dom": "^19.1.1",
14
- "sharp": "^0.34.3",
15
- "tailwindcss": "^3.4.17",
16
- "typescript": "^5.9.2"
9
+ "@lightnet/decap-admin": "^3.1.3",
10
+ "astro": "^5.14.8",
11
+ "lightnet": "^3.10.0",
12
+ "react": "^19.2.0",
13
+ "react-dom": "^19.2.0",
14
+ "sharp": "^0.34.4",
15
+ "tailwindcss": "^3.4.18",
16
+ "typescript": "^5.9.3"
17
17
  },
18
18
  "engines": {
19
19
  "node": ">=22"
@@ -0,0 +1,15 @@
1
+ {
2
+ "commonId": "skate-sounds",
3
+ "title": "Skate Sounds",
4
+ "type": "audio",
5
+ "dateCreated": "2024-04-21",
6
+ "authors": ["Sk8 Ministries"],
7
+ "content": [
8
+ { "url": "/files/example-audio.mp3" },
9
+ { "url": "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3" }
10
+ ],
11
+ "image": "./images/cover.jpg",
12
+ "language": "en",
13
+ "categories": ["christian-living"],
14
+ "description": "A vibrant collection of authentic skate park sounds to energize your projects and playlists.\n\n**Highlights:**\n- Real-world skating ambience\n- Perfect for creative mixes\n- Inspiring background audio for worship gatherings"
15
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "label": "type.audio",
3
+ "detailsPage": {
4
+ "layout": "audio"
5
+ },
6
+ "icon": "mdi--music-box-outline"
7
+ }
@@ -4,5 +4,6 @@ category.teens: Teens
4
4
  category.theology: Theologie
5
5
  type.book: Buch
6
6
  type.video: Video
7
+ type.audio: Audio
7
8
  details.action.read: Lesen
8
9
  details.action.watch: Anschauen
@@ -4,5 +4,6 @@ category.teens: Teens
4
4
  category.theology: Theology
5
5
  type.book: Book
6
6
  type.video: Video
7
+ type.audio: Audio
7
8
  details.action.read: Read
8
9
  details.action.watch: Watch
@@ -102,3 +102,24 @@ test("Should verify DE Detail media page url and title", async ({
102
102
  await expect(page.getByText("Sprache")).toBeVisible()
103
103
  await expect(page.getByText("Kategorie")).toBeVisible()
104
104
  })
105
+
106
+ test("Should show `Powered by LightNet` in footer", async ({
107
+ page,
108
+ startLightnet,
109
+ }) => {
110
+ await startLightnet()
111
+
112
+ const footerLink = page
113
+ .getByRole("contentinfo")
114
+ .getByRole("link", { name: /LightNet/ })
115
+
116
+ await expect(footerLink).toHaveText("Powered by LightNet")
117
+ await expect(footerLink).toHaveAttribute("href", "https://lightnet.community")
118
+
119
+ await page.getByLabel("Select language").click()
120
+ await page.getByRole("link", { name: "Deutsch" }).click()
121
+
122
+ await expect(
123
+ page.getByRole("contentinfo").getByRole("link", { name: /LightNet/ }),
124
+ ).toHaveText("Ermöglicht durch LightNet")
125
+ })
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "lightnet",
3
+ "description": "LightNet makes it easy to run your own digital media library.",
3
4
  "type": "module",
4
5
  "license": "MIT",
5
- "version": "3.9.1",
6
+ "version": "3.10.1",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "https://github.com/LightNetDev/lightnet",
@@ -30,8 +31,12 @@
30
31
  "./pages/RootRoute.astro": "./src/pages/RootRoute.astro",
31
32
  "./pages/SearchPageRoute.astro": "./src/pages/search-page/SearchPageRoute.astro",
32
33
  "./pages/DetailsPageRoute.astro": "./src/pages/details-page/DetailsPageRoute.astro",
33
- "./pages/api/search.ts": "./src/pages/api/search.ts",
34
- "./pages/api/versions.ts": "./src/pages/api/versions.ts"
34
+ "./api/internal/search.ts": "./src/pages/search-page/api/search.ts",
35
+ "./api/versions.ts": "./src/api/versions.ts",
36
+ "./api/media/[mediaId].ts": "./src/api/media/[mediaId].ts",
37
+ "./api/internal/fs/writeText.ts": "./src/admin/api/fs/writeText.ts",
38
+ "./admin/pages/AdminRoute.astro": "./src/admin/pages/AdminRoute.astro",
39
+ "./admin/pages/media/EditRoute.astro": "./src/admin/pages/media/EditRoute.astro"
35
40
  },
36
41
  "peerDependencies": {
37
42
  "astro": "^5.1.0",
@@ -40,25 +45,27 @@
40
45
  "tailwindcss": ">=3.4.0 <4.0.0"
41
46
  },
42
47
  "dependencies": {
43
- "@astrojs/react": "^4.3.0",
48
+ "@astrojs/react": "^4.4.0",
44
49
  "@astrojs/tailwind": "^6.0.2",
45
50
  "@iconify-json/mdi": "^1.2.3",
46
51
  "@iconify/tailwind": "^1.2.0",
47
- "@tailwindcss/typography": "^0.5.16",
52
+ "@tailwindcss/typography": "^0.5.19",
53
+ "@tanstack/react-form": "^1.23.7",
54
+ "@tanstack/react-query": "^5.90.5",
48
55
  "@tanstack/react-virtual": "^3.13.12",
49
56
  "daisyui": "^4.12.24",
50
57
  "embla-carousel": "^8.6.0",
51
58
  "embla-carousel-wheel-gestures": "^8.1.0",
52
59
  "fuse.js": "^7.1.0",
53
- "i18next": "^25.4.2",
54
- "marked": "^16.2.0",
60
+ "i18next": "^25.6.0",
61
+ "marked": "^16.4.1",
55
62
  "yaml": "^2.8.1"
56
63
  },
57
64
  "devDependencies": {
58
- "@playwright/test": "^1.55.0",
59
- "@types/node": "^22.18.0",
60
- "@types/react": "^19.1.11",
61
- "typescript": "^5.9.2",
65
+ "@playwright/test": "^1.56.1",
66
+ "@types/node": "^22.18.12",
67
+ "@types/react": "^19.2.2",
68
+ "typescript": "^5.9.3",
62
69
  "vitest": "^3.2.4"
63
70
  },
64
71
  "engines": {
@@ -0,0 +1,50 @@
1
+ import { mkdir, rename, rm, writeFile } from "node:fs/promises"
2
+ import { dirname, isAbsolute, relative, resolve } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ import type { APIRoute } from "astro"
6
+ import { root } from "astro:config/server"
7
+
8
+ export const prerender = false
9
+
10
+ export const POST: APIRoute = async ({ request }) => {
11
+ const rootDirPath = fileURLToPath(root)
12
+ const requestedPath = new URL(request.url).searchParams.get("path")
13
+ if (!requestedPath) {
14
+ throw new Error("'path' search param is undefined.")
15
+ }
16
+ if (isAbsolute(requestedPath)) {
17
+ throw new Error("Absolute paths are not allowed.")
18
+ }
19
+
20
+ const targetPath = resolve(rootDirPath, requestedPath)
21
+ const relativeToRoot = relative(rootDirPath, targetPath)
22
+ if (
23
+ relativeToRoot.startsWith("..") ||
24
+ relativeToRoot === "" ||
25
+ isAbsolute(relativeToRoot)
26
+ ) {
27
+ throw new Error("Path escapes project root.")
28
+ }
29
+
30
+ const targetDir = dirname(targetPath)
31
+ await mkdir(targetDir, { recursive: true })
32
+
33
+ const body = await request.text()
34
+ const timestamp = Date.now()
35
+ const tmpPath = `${targetPath}.tmp-${timestamp}`
36
+ try {
37
+ const tmpPath = `${targetPath}.tmp-${Date.now()}`
38
+ await writeFile(tmpPath, body, "utf-8")
39
+ await rename(tmpPath, targetPath)
40
+ } finally {
41
+ await rm(tmpPath, { force: true }).catch(() => {})
42
+ }
43
+
44
+ return new Response(JSON.stringify({ status: "ok" }), {
45
+ status: 200,
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ },
49
+ })
50
+ }
@@ -0,0 +1,19 @@
1
+ import type { AnyFieldMeta } from "@tanstack/react-form"
2
+
3
+ type FieldErrorsProps = {
4
+ meta: AnyFieldMeta
5
+ }
6
+
7
+ export const FieldErrors = ({ meta }: FieldErrorsProps) => {
8
+ if (!meta.isTouched || meta.isValid) return null
9
+
10
+ return (
11
+ <ul className="my-2 flex flex-col gap-1" role="alert">
12
+ {meta.errors.map((error) => (
13
+ <li className="text-sm text-rose-800" key={error.code}>
14
+ {error.message}
15
+ </li>
16
+ ))}
17
+ </ul>
18
+ )
19
+ }
@@ -0,0 +1,77 @@
1
+ import { useStore } from "@tanstack/react-form"
2
+ import { useEffect, useRef, useState } from "react"
3
+
4
+ import Icon from "../../../components/Icon"
5
+ import { useFormContext } from "./form-context"
6
+
7
+ const SUCCESS_DURATION_MS = 2000
8
+
9
+ const baseButtonClass =
10
+ "flex min-w-52 items-center justify-center gap-2 rounded-2xl px-6 py-3 font-bold uppercase shadow-sm transition-colors duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900 disabled:cursor-not-allowed"
11
+
12
+ const buttonStateClasses = {
13
+ idle: "bg-gray-800 text-gray-100 hover:bg-gray-950 hover:text-gray-300 disabled:bg-gray-600 disabled:text-gray-200",
14
+ error:
15
+ "bg-rose-700 text-white hover:bg-rose-800 hover:text-white disabled:bg-rose-600",
16
+ success:
17
+ "bg-emerald-600 text-white hover:bg-emerald-700 hover:text-white disabled:bg-emerald-500",
18
+ } as const
19
+
20
+ const buttonLabels = {
21
+ idle: "Save",
22
+ success: "Saved",
23
+ error: "Failed",
24
+ } as const
25
+
26
+ const icons = {
27
+ idle: undefined,
28
+ success: "mdi--check",
29
+ error: "mdi--error-outline",
30
+ } as const
31
+
32
+ export default function SubmitButton() {
33
+ const form = useFormContext()
34
+ const { submissionAttempts, isSubmitting, isSubmitSuccessful } = useStore(
35
+ form.store,
36
+ (state) => ({
37
+ canSubmit: state.canSubmit,
38
+ isSubmitting: state.isSubmitting,
39
+ isSubmitSuccessful: state.isSubmitSuccessful,
40
+ submissionAttempts: state.submissionAttempts,
41
+ }),
42
+ )
43
+ const buttonState = useButtonState(isSubmitSuccessful, submissionAttempts)
44
+ const buttonClass = `${baseButtonClass} ${buttonStateClasses[buttonState]}`
45
+ const label = buttonLabels[buttonState]
46
+ const icon = icons[buttonState]
47
+
48
+ return (
49
+ <button className={buttonClass} type="submit" disabled={isSubmitting}>
50
+ {icon && <Icon className={icon} ariaLabel="" />}
51
+ {label}
52
+ </button>
53
+ )
54
+ }
55
+
56
+ function useButtonState(
57
+ isSubmitSuccessful: boolean,
58
+ submissionAttempts: number,
59
+ ) {
60
+ const [state, setState] = useState<"success" | "error" | "idle">("idle")
61
+ const timeoutRef = useRef<number | undefined>(undefined)
62
+ useEffect(() => {
63
+ if (submissionAttempts === 0) {
64
+ return
65
+ }
66
+ setState(isSubmitSuccessful ? "success" : "error")
67
+ if (timeoutRef.current !== undefined) {
68
+ window.clearTimeout(timeoutRef.current)
69
+ }
70
+ timeoutRef.current = window.setTimeout(() => {
71
+ setState("idle")
72
+ timeoutRef.current = undefined
73
+ }, SUCCESS_DURATION_MS)
74
+ }, [submissionAttempts, isSubmitSuccessful])
75
+
76
+ return state
77
+ }
@@ -0,0 +1,24 @@
1
+ import { FieldErrors } from "./FieldErrors"
2
+ import { useFieldContext } from "./form-context"
3
+
4
+ export default function TextField({ label }: { label: string }) {
5
+ const field = useFieldContext<string>()
6
+ return (
7
+ <>
8
+ <label className="dy-form-control w-full max-w-sm">
9
+ <div className="dy-label">
10
+ <span className="dy-label-text">{label}</span>
11
+ </div>
12
+ <input
13
+ id={field.name}
14
+ name={field.name}
15
+ value={field.state.value}
16
+ onChange={(e) => field.handleChange(e.target.value)}
17
+ onBlur={field.handleBlur}
18
+ className={`dy-input dy-input-bordered dy-input-sm w-full max-w-sm ${field.state.meta.errors.length ? "dy-input-error" : ""}`}
19
+ />
20
+ <FieldErrors meta={field.state.meta} />
21
+ </label>
22
+ </>
23
+ )
24
+ }
@@ -0,0 +1,4 @@
1
+ import { createFormHookContexts } from "@tanstack/react-form"
2
+
3
+ export const { fieldContext, useFieldContext, formContext, useFormContext } =
4
+ createFormHookContexts()
@@ -0,0 +1,16 @@
1
+ import { createFormHook } from "@tanstack/react-form"
2
+
3
+ import { fieldContext, formContext } from "./form-context"
4
+ import SubmitButton from "./SubmitButton"
5
+ import TextField from "./TextField"
6
+
7
+ export const { useAppForm, withForm } = createFormHook({
8
+ fieldComponents: {
9
+ TextField,
10
+ },
11
+ formComponents: {
12
+ SubmitButton,
13
+ },
14
+ fieldContext,
15
+ formContext,
16
+ })
@@ -0,0 +1 @@
1
+ ln.admin.edit: Edit
@@ -0,0 +1,5 @@
1
+ export const builtInAdminTranslations = {
2
+ en: () => import("./translations/en.yml?raw"),
3
+ } as const
4
+
5
+ export type AdminTranslationKey = "ln.admin.edit"
@@ -0,0 +1,16 @@
1
+ ---
2
+ import Page from "../../layouts/Page.astro"
3
+
4
+ export { getLocalePaths as getStaticPaths } from "../../i18n/get-locale-paths"
5
+ ---
6
+
7
+ <Page>
8
+ <div
9
+ class="flex h-96 w-full items-center justify-center text-lg font-bold text-gray-500"
10
+ >
11
+ Admin features are enabled now.
12
+ </div>
13
+ </Page>
14
+ <script>
15
+ localStorage.setItem("ln-admin-enabled", "true")
16
+ </script>