lightnet 3.10.4 → 3.10.6

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 (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/__e2e__/admin.spec.ts +93 -53
  3. package/__e2e__/{test-utils.ts → basics-fixture.ts} +20 -23
  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/faithful-freestyle--en.json +1 -1
  7. package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +1 -1
  8. package/__e2e__/global.teardown.ts +5 -0
  9. package/__e2e__/homepage.spec.ts +17 -19
  10. package/__e2e__/search.spec.ts +3 -5
  11. package/package.json +6 -6
  12. package/playwright.config.ts +1 -0
  13. package/src/admin/components/form/DynamicArray.tsx +74 -0
  14. package/src/admin/components/form/Input.tsx +8 -5
  15. package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +78 -0
  16. package/src/admin/components/form/MarkdownEditor.tsx +52 -0
  17. package/src/admin/components/form/Select.tsx +11 -10
  18. package/src/admin/components/form/SubmitButton.tsx +9 -12
  19. package/src/admin/components/form/atoms/ErrorMessage.tsx +7 -28
  20. package/src/admin/components/form/atoms/Hint.tsx +2 -2
  21. package/src/admin/components/form/atoms/Label.tsx +5 -1
  22. package/src/admin/components/form/atoms/Legend.tsx +12 -2
  23. package/src/admin/components/form/hooks/use-field-error.tsx +3 -11
  24. package/src/admin/i18n/translations/en.yml +16 -5
  25. package/src/admin/pages/media/EditForm.tsx +48 -6
  26. package/src/admin/pages/media/EditRoute.astro +29 -10
  27. package/src/admin/pages/media/fields/Authors.tsx +51 -51
  28. package/src/admin/pages/media/fields/Categories.tsx +70 -0
  29. package/src/admin/pages/media/fields/Collections.tsx +117 -0
  30. package/src/admin/pages/media/media-item-store.ts +6 -1
  31. package/src/admin/types/media-item.ts +35 -2
  32. package/src/components/CategoriesSection.astro +2 -2
  33. package/src/components/MediaGallerySection.astro +3 -3
  34. package/src/components/MediaList.astro +2 -2
  35. package/src/content/get-categories.ts +18 -3
  36. package/src/i18n/resolve-language.ts +1 -1
  37. package/src/layouts/Page.astro +3 -2
  38. package/src/layouts/components/LanguagePicker.astro +1 -1
  39. package/src/pages/details-page/components/more-details/Languages.astro +2 -2
  40. package/src/pages/search-page/components/SearchFilter.astro +7 -7
  41. package/src/pages/search-page/components/SearchFilter.tsx +4 -4
  42. package/src/pages/search-page/components/SearchList.astro +4 -4
  43. package/src/pages/search-page/components/SearchListItem.tsx +4 -4
  44. package/src/pages/search-page/components/Select.tsx +3 -3
  45. package/src/pages/search-page/hooks/use-search.ts +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # lightnet
2
2
 
3
+ ## 3.10.6
4
+
5
+ ### Patch Changes
6
+
7
+ - [#333](https://github.com/LightNetDev/LightNet/pull/333) [`539a377`](https://github.com/LightNetDev/LightNet/commit/539a377702df9213d0869ae63646f569c10b1867) Thanks [@smn-cds](https://github.com/smn-cds)! - Update dependencies
8
+
9
+ ## 3.10.5
10
+
11
+ ### Patch Changes
12
+
13
+ - [#331](https://github.com/LightNetDev/LightNet/pull/331) [`cdacfdf`](https://github.com/LightNetDev/LightNet/commit/cdacfdfca311469248cb7d1ac0b46f92d9fd5521) Thanks [@smn-cds](https://github.com/smn-cds)! - Experimenatl Admin UI: support editing categories and collections
14
+
3
15
  ## 3.10.4
4
16
 
5
17
  ### Patch Changes
@@ -2,9 +2,8 @@ import { readFile } from "node:fs/promises"
2
2
 
3
3
  import { expect, type Page } from "@playwright/test"
4
4
 
5
- import { lightnetTest } from "./test-utils"
5
+ import { test } from "./basics-fixture"
6
6
 
7
- const test = lightnetTest("./fixtures/basics/")
8
7
  const faithfulFreestyleMediaUrl = new URL(
9
8
  "./fixtures/basics/src/content/media/faithful-freestyle--en.json",
10
9
  import.meta.url,
@@ -13,9 +12,9 @@ const faithfulFreestyleMediaUrl = new URL(
13
12
  test.describe("Edit button on details page", () => {
14
13
  test("Should not show `Edit` button on details page by default.", async ({
15
14
  page,
16
- startLightnet,
15
+ lightnet,
17
16
  }) => {
18
- await startLightnet()
17
+ await lightnet()
19
18
 
20
19
  await page.getByRole("link", { name: "Faithful Freestyle" }).click()
21
20
  await expect(
@@ -28,9 +27,9 @@ test.describe("Edit button on details page", () => {
28
27
 
29
28
  test("Should show `Edit` button on book details page after visiting `/en/admin/` path.", async ({
30
29
  page,
31
- startLightnet,
30
+ lightnet,
32
31
  }) => {
33
- const ln = await startLightnet()
32
+ const ln = await lightnet()
34
33
 
35
34
  await page.goto(ln.resolveURL("/en/admin/"))
36
35
  await expect(
@@ -52,11 +51,10 @@ test.describe("Edit button on details page", () => {
52
51
 
53
52
  test("Should show `Edit` button on video details page after visiting `/en/admin/` path.", async ({
54
53
  page,
55
- startLightnet,
54
+ lightnet,
56
55
  }) => {
57
- const ln = await startLightnet()
56
+ const ln = await lightnet("/en/admin/")
58
57
 
59
- await page.goto(ln.resolveURL("/en/admin/"))
60
58
  await expect(
61
59
  page.getByText("Admin features are enabled now.", { exact: true }),
62
60
  ).toBeVisible()
@@ -76,11 +74,10 @@ test.describe("Edit button on details page", () => {
76
74
 
77
75
  test("Should show `Edit` button on audio details page after visiting `/en/admin/` path.", async ({
78
76
  page,
79
- startLightnet,
77
+ lightnet,
80
78
  }) => {
81
- const ln = await startLightnet()
79
+ const ln = await lightnet("/en/admin/")
82
80
 
83
- await page.goto(ln.resolveURL("/en/admin/"))
84
81
  await expect(
85
82
  page.getByText("Admin features are enabled now.", { exact: true }),
86
83
  ).toBeVisible()
@@ -100,12 +97,10 @@ test.describe("Edit button on details page", () => {
100
97
 
101
98
  test("Edit button on details page should navigate to media item edit page", async ({
102
99
  page,
103
- startLightnet,
100
+ lightnet,
104
101
  }) => {
105
- const ln = await startLightnet()
106
-
107
- await page.goto(ln.resolveURL("/en/admin/"))
108
- await page.goto(ln.resolveURL("/en/media/faithful-freestyle--en"))
102
+ const ln = await lightnet("/en/admin/")
103
+ await ln.goto("/en/media/faithful-freestyle--en")
109
104
 
110
105
  const editButton = page.locator("#edit-btn")
111
106
  await expect(editButton).toBeVisible()
@@ -143,19 +138,24 @@ test.describe("Media item edit page", () => {
143
138
  return () => writeFileRequestPromise
144
139
  }
145
140
 
146
- test("should edit title", async ({ page, startLightnet }) => {
147
- const ln = await startLightnet()
141
+ const getPublishButton = (page: Page) =>
142
+ page.getByRole("button", { name: "Publish Changes" }).first()
148
143
 
149
- const writeFileRequest = await recordWriteFile(page)
144
+ const expectPublishedMessage = (page: Page) =>
145
+ expect(
146
+ page.getByRole("button", { name: "Published" }).first(),
147
+ ).toBeVisible()
150
148
 
151
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
149
+ test("should edit title", async ({ page, lightnet }) => {
150
+ await lightnet("/en/admin/media/faithful-freestyle--en")
151
+ const writeFileRequest = await recordWriteFile(page)
152
152
 
153
153
  const updatedTitle = "Faithful Freestyle (Edited)"
154
154
  const titleInput = page.getByLabel("Title")
155
155
  await expect(titleInput).toHaveValue("Faithful Freestyle")
156
156
  await titleInput.fill(updatedTitle)
157
157
 
158
- const saveButton = page.getByRole("button", { name: "Save" })
158
+ const saveButton = getPublishButton(page)
159
159
  await expect(saveButton).toBeEnabled()
160
160
  await saveButton.click()
161
161
 
@@ -171,20 +171,18 @@ test.describe("Media item edit page", () => {
171
171
  ...expectedMediaItem,
172
172
  title: updatedTitle,
173
173
  })
174
- await expect(page.getByRole("button", { name: "Saved" })).toBeVisible()
174
+ await expectPublishedMessage(page)
175
175
  })
176
176
 
177
- test("Should update media type", async ({ page, startLightnet }) => {
178
- const ln = await startLightnet()
177
+ test("Should update media type", async ({ page, lightnet }) => {
178
+ await lightnet("/en/admin/media/faithful-freestyle--en")
179
179
  const writeFileRequest = await recordWriteFile(page)
180
180
 
181
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
182
-
183
- const typeSelect = page.getByLabel("Type")
181
+ const typeSelect = page.getByLabel("Type").first()
184
182
  await expect(typeSelect).toHaveValue("book")
185
183
  await typeSelect.selectOption("video")
186
184
 
187
- const saveButton = page.getByRole("button", { name: "Save" })
185
+ const saveButton = getPublishButton(page)
188
186
  await expect(saveButton).toBeEnabled()
189
187
  await saveButton.click()
190
188
 
@@ -196,22 +194,20 @@ test.describe("Media item edit page", () => {
196
194
  ...expectedMediaItem,
197
195
  type: "video",
198
196
  })
199
- await expect(page.getByRole("button", { name: "Saved" })).toBeVisible()
197
+ await expectPublishedMessage(page)
200
198
  })
201
199
 
202
- test("Should update author name", async ({ page, startLightnet }) => {
203
- const ln = await startLightnet()
200
+ test("Should update author name", async ({ page, lightnet }) => {
201
+ await lightnet("/en/admin/media/faithful-freestyle--en")
204
202
  const writeFileRequest = await recordWriteFile(page)
205
203
 
206
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
207
-
208
204
  const authorsFieldset = page.getByRole("group", { name: "Authors" })
209
205
  const firstAuthorInput = authorsFieldset.getByRole("textbox").first()
210
206
  const updatedAuthor = "Sk8 Ministries International"
211
207
  await expect(firstAuthorInput).toHaveValue("Sk8 Ministries")
212
208
  await firstAuthorInput.fill(updatedAuthor)
213
209
 
214
- const saveButton = page.getByRole("button", { name: "Save" })
210
+ const saveButton = getPublishButton(page)
215
211
  await expect(saveButton).toBeEnabled()
216
212
  await saveButton.click()
217
213
 
@@ -223,15 +219,13 @@ test.describe("Media item edit page", () => {
223
219
  ...expectedMediaItem,
224
220
  authors: [updatedAuthor],
225
221
  })
226
- await expect(page.getByRole("button", { name: "Saved" })).toBeVisible()
222
+ await expectPublishedMessage(page)
227
223
  })
228
224
 
229
- test("Should add author", async ({ page, startLightnet }) => {
230
- const ln = await startLightnet()
225
+ test("Should add author", async ({ page, lightnet }) => {
226
+ await lightnet("/en/admin/media/faithful-freestyle--en")
231
227
  const writeFileRequest = await recordWriteFile(page)
232
228
 
233
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
234
-
235
229
  const authorsFieldset = page.getByRole("group", { name: "Authors" })
236
230
  const addAuthorButton = page.getByRole("button", { name: "Add Author" })
237
231
  await addAuthorButton.click()
@@ -239,7 +233,7 @@ test.describe("Media item edit page", () => {
239
233
  const additionalAuthor = "Tony Hawk"
240
234
  await newAuthorInput.fill(additionalAuthor)
241
235
 
242
- const saveButton = page.getByRole("button", { name: "Save" })
236
+ const saveButton = getPublishButton(page)
243
237
  await expect(saveButton).toBeEnabled()
244
238
  await saveButton.click()
245
239
 
@@ -251,15 +245,13 @@ test.describe("Media item edit page", () => {
251
245
  ...expectedMediaItem,
252
246
  authors: ["Sk8 Ministries", additionalAuthor],
253
247
  })
254
- await expect(page.getByRole("button", { name: "Saved" })).toBeVisible()
248
+ await expectPublishedMessage(page)
255
249
  })
256
250
 
257
- test("Should remove author", async ({ page, startLightnet }) => {
258
- const ln = await startLightnet()
251
+ test("Should remove author", async ({ page, lightnet }) => {
252
+ await lightnet("/en/admin/media/faithful-freestyle--en")
259
253
  const writeFileRequest = await recordWriteFile(page)
260
254
 
261
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
262
-
263
255
  const authorsFieldset = page.getByRole("group", { name: "Authors" })
264
256
  const addAuthorButton = page.getByRole("button", { name: "Add Author" })
265
257
  const replacementAuthor = "Skate Evangelists"
@@ -272,7 +264,7 @@ test.describe("Media item edit page", () => {
272
264
  })
273
265
  await removeButtons.first().click()
274
266
 
275
- const saveButton = page.getByRole("button", { name: "Save" })
267
+ const saveButton = getPublishButton(page)
276
268
  await expect(saveButton).toBeEnabled()
277
269
  await saveButton.click()
278
270
 
@@ -284,15 +276,14 @@ test.describe("Media item edit page", () => {
284
276
  ...expectedMediaItem,
285
277
  authors: [replacementAuthor],
286
278
  })
287
- await expect(page.getByRole("button", { name: "Saved" })).toBeVisible()
279
+ await expectPublishedMessage(page)
288
280
  })
289
281
 
290
282
  test("should show error message if common id is set empty", async ({
291
283
  page,
292
- startLightnet,
284
+ lightnet,
293
285
  }) => {
294
- const ln = await startLightnet()
295
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
286
+ await lightnet("/en/admin/media/faithful-freestyle--en")
296
287
 
297
288
  const commonIdInput = page.getByLabel("Common ID")
298
289
  await expect(commonIdInput).toHaveValue("faithful-freestyle")
@@ -303,8 +294,57 @@ test.describe("Media item edit page", () => {
303
294
  await expect(
304
295
  page
305
296
  .getByRole("alert")
306
- .filter({ hasText: "String must contain at least 1 character(s)" }),
297
+ .filter({ hasText: "Please enter at least one character." }),
298
+ ).toBeVisible()
299
+ })
300
+
301
+ test("should focus invalid field when submitting invalid form data", async ({
302
+ page,
303
+ lightnet,
304
+ }) => {
305
+ await lightnet("/en/admin/media/faithful-freestyle--en")
306
+
307
+ const categoriesFieldset = page.getByRole("group", { name: "Categories" })
308
+ await page.getByRole("button", { name: "Add Category" }).click()
309
+ const newCategorySelect = categoriesFieldset.getByRole("combobox").last()
310
+ await expect(newCategorySelect).toHaveValue("")
311
+
312
+ // move focus away so the submission handler needs to return focus
313
+ await page.getByLabel("Title").click()
314
+
315
+ const saveButton = getPublishButton(page)
316
+ await saveButton.click()
317
+
318
+ await expect(
319
+ page.getByRole("alert").filter({ hasText: "This field is required." }),
307
320
  ).toBeVisible()
308
- await expect(page.getByRole("button", { name: "Save" })).toBeDisabled()
321
+ await expect(newCategorySelect).toBeFocused()
322
+ })
323
+
324
+ test("should not allow assigning duplicate categories", async ({
325
+ page,
326
+ lightnet,
327
+ }) => {
328
+ await lightnet("/en/admin/media/faithful-freestyle--en")
329
+
330
+ const categoriesFieldset = page.getByRole("group", { name: "Categories" })
331
+ await page.getByRole("button", { name: "Add Category" }).click()
332
+
333
+ const categorySelects = categoriesFieldset.getByRole("combobox")
334
+ const firstCategoryValue = await categorySelects.first().inputValue()
335
+ const duplicateCategorySelect = categorySelects.last()
336
+ await duplicateCategorySelect.selectOption(firstCategoryValue)
337
+
338
+ const publishButton = getPublishButton(page)
339
+ await publishButton.click()
340
+
341
+ const duplicateCategoryError = categoriesFieldset
342
+ .getByRole("alert")
343
+ .filter({ hasText: "Please choose a different value for each entry." })
344
+ await expect(duplicateCategoryError).toBeVisible()
345
+ await expect(duplicateCategorySelect).toHaveAttribute(
346
+ "aria-invalid",
347
+ "true",
348
+ )
309
349
  })
310
350
  })
@@ -24,7 +24,6 @@
24
24
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
25
  * SOFTWARE.
26
26
  */
27
-
28
27
  import { fileURLToPath } from "node:url"
29
28
 
30
29
  import { type Page, test as baseTest } from "@playwright/test"
@@ -34,30 +33,26 @@ export { expect, type Locator } from "@playwright/test"
34
33
 
35
34
  process.env.ASTRO_TELEMETRY_DISABLED = "true"
36
35
  process.env.ASTRO_DISABLE_UPDATE_CHECK = "true"
37
- export function lightnetTest(fixturePath: string) {
38
- const root = fileURLToPath(new URL(fixturePath, import.meta.url))
39
-
40
- let server: Server | null = null
41
- const test = baseTest.extend<{
42
- startLightnet: (path?: string) => Promise<LightNetPage>
43
- }>({
44
- startLightnet: ({ page }, use) =>
45
- use(async (path) => {
46
- if (!server) {
47
- await build({ logLevel: "error", root })
48
- server = await preview({ logLevel: "error", root })
49
- }
50
- const ln = new LightNetPage(server, page)
51
- await ln.goto(path ?? "/")
52
- return ln
53
- }),
54
- })
36
+ const root = fileURLToPath(new URL("./fixtures/basics/", import.meta.url))
55
37
 
56
- test.afterAll(async () => {
57
- await server?.stop()
58
- })
38
+ let server: Server | null = null
39
+ const test = baseTest.extend<{
40
+ lightnet: (path?: string) => Promise<LightNetPage>
41
+ }>({
42
+ lightnet: ({ page }, use) =>
43
+ use(async (path) => {
44
+ if (!server) {
45
+ await build({ logLevel: "error", root })
46
+ server = await preview({ logLevel: "error", root })
47
+ }
48
+ const ln = new LightNetPage(server, page)
49
+ await ln.goto(path ?? "/")
50
+ return ln
51
+ }),
52
+ })
59
53
 
60
- return test
54
+ const teardown = async () => {
55
+ await server?.stop()
61
56
  }
62
57
 
63
58
  // A Playwright test fixture accessible from within all tests.
@@ -78,3 +73,5 @@ class LightNetPage {
78
73
  }
79
74
 
80
75
  type Server = Awaited<ReturnType<typeof preview>>
76
+
77
+ export { teardown, test }
@@ -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.15.4_@types+node@24.10.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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.15.4_@types+node@24.10.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_terser@5.39.0_typescript@5.9.3_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.15.8_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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.15.8_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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.15.4_@types+node@24.10.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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.15.4_@types+node@24.10.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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"
15
+ export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.15.8_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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.15.8_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_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" "$@"
@@ -7,8 +7,8 @@
7
7
  "@astrojs/react": "^4.4.2",
8
8
  "@astrojs/tailwind": "^6.0.2",
9
9
  "@lightnet/decap-admin": "^3.1.4",
10
- "astro": "^5.15.4",
11
- "lightnet": "^3.10.3",
10
+ "astro": "^5.15.8",
11
+ "lightnet": "^3.10.5",
12
12
  "react": "^19.2.0",
13
13
  "react-dom": "^19.2.0",
14
14
  "sharp": "^0.34.5",
@@ -9,5 +9,5 @@
9
9
  "language": "en",
10
10
  "categories": ["christian-living"],
11
11
  "collections": [{ "collection": "how-to-articles" }],
12
- "description": "*How to: Faithful Freestyle* empowers you to express your Christianity through the unique and creative outlet of skateboarding. This book includes:\n\n- **Creative ways to incorporate faith** into your skating routines\n- **Stories of freestyle skaters** who honor God through their sport\n- **Practical advice on witnessing** to others in the skateboarding scene\n- **Inspirational devotions** designed for skaters\n\nEmbrace a faithful freestyle and let every trick and turn reflect your devotion to Christ."
12
+ "description": "*How to: Faithful Freestyle* empowers you to express your Christianity through the unique and creative outlet of skateboarding. This book includes:\n\n* **Creative ways to incorporate faith** into your skating routines\n* **Stories of freestyle skaters** who honor God through their sport\n* **Practical advice on witnessing** to others in the skateboarding scene\n* **Inspirational devotions** designed for skaters\n\nEmbrace a faithful freestyle and let every trick and turn reflect your devotion to Christ."
13
13
  }
@@ -11,5 +11,5 @@
11
11
  "image": "./images/cover.jpg",
12
12
  "language": "en",
13
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"
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
15
  }
@@ -0,0 +1,5 @@
1
+ import { teardown } from "./basics-fixture"
2
+
3
+ export default async function globalTeardown() {
4
+ await teardown()
5
+ }
@@ -1,34 +1,32 @@
1
1
  import { expect } from "@playwright/test"
2
2
 
3
- import { lightnetTest } from "./test-utils"
3
+ import { test } from "./basics-fixture"
4
4
 
5
- const test = lightnetTest("./fixtures/basics/")
6
-
7
- test("Should have title set", async ({ page, startLightnet }) => {
8
- await startLightnet()
5
+ test("Should have title set", async ({ page, lightnet }) => {
6
+ await lightnet()
9
7
  await expect(page).toHaveTitle("Basic Test")
10
8
  })
11
9
 
12
10
  test("Should have header title that navigates to home page", async ({
13
11
  page,
14
- startLightnet,
12
+ lightnet,
15
13
  }) => {
16
- const ln = await startLightnet()
14
+ const ln = await lightnet()
17
15
  await page.getByRole("link", { name: "Basic Test" }).click()
18
16
 
19
17
  await expect(page).toHaveURL(ln.resolveURL("/en/"))
20
18
  })
21
19
 
22
- test("Should have item section", async ({ page, startLightnet }) => {
23
- await startLightnet()
20
+ test("Should have item section", async ({ page, lightnet }) => {
21
+ await lightnet()
24
22
  await expect(page.getByRole("heading", { name: "All items" })).toBeVisible()
25
23
  })
26
24
 
27
25
  test("Should navigate to search page from main menu", async ({
28
26
  page,
29
- startLightnet,
27
+ lightnet,
30
28
  }) => {
31
- const ln = await startLightnet()
29
+ const ln = await lightnet()
32
30
  await expect(
33
31
  page.getByRole("button", { name: "Open Main Menu" }),
34
32
  ).toBeVisible()
@@ -42,8 +40,8 @@ test("Should navigate to search page from main menu", async ({
42
40
  await expect(page.getByRole("heading", { name: "Search" })).toBeVisible()
43
41
  })
44
42
 
45
- test("Should switch languages", async ({ page, startLightnet }) => {
46
- const ln = await startLightnet()
43
+ test("Should switch languages", async ({ page, lightnet }) => {
44
+ const ln = await lightnet()
47
45
 
48
46
  await page.getByLabel("Select language").click()
49
47
  await page.getByRole("link", { name: "Deutsch" }).click()
@@ -57,9 +55,9 @@ test("Should switch languages", async ({ page, startLightnet }) => {
57
55
 
58
56
  test("Should verify EN Detail media page url and title", async ({
59
57
  page,
60
- startLightnet,
58
+ lightnet,
61
59
  }) => {
62
- const ln = await startLightnet()
60
+ const ln = await lightnet()
63
61
 
64
62
  await page.getByRole("link", { name: "Faithful Freestyle" }).click()
65
63
  await expect(
@@ -80,9 +78,9 @@ test("Should verify EN Detail media page url and title", async ({
80
78
 
81
79
  test("Should verify DE Detail media page url and title", async ({
82
80
  page,
83
- startLightnet,
81
+ lightnet,
84
82
  }) => {
85
- const ln = await startLightnet()
83
+ const ln = await lightnet()
86
84
 
87
85
  await page.getByLabel("Select language").click()
88
86
  await page.getByRole("link", { name: "Deutsch" }).click()
@@ -105,9 +103,9 @@ test("Should verify DE Detail media page url and title", async ({
105
103
 
106
104
  test("Should show `Powered by LightNet` in footer", async ({
107
105
  page,
108
- startLightnet,
106
+ lightnet,
109
107
  }) => {
110
- await startLightnet()
108
+ await lightnet()
111
109
 
112
110
  const footerLink = page
113
111
  .getByRole("contentinfo")
@@ -1,14 +1,12 @@
1
1
  import { expect } from "@playwright/test"
2
2
 
3
- import { lightnetTest } from "./test-utils"
4
-
5
- const test = lightnetTest("./fixtures/basics/")
3
+ import { test } from "./basics-fixture"
6
4
 
7
5
  test("Search should have heading section and URL", async ({
8
6
  page,
9
- startLightnet,
7
+ lightnet,
10
8
  }) => {
11
- const ln = await startLightnet()
9
+ const ln = await lightnet()
12
10
 
13
11
  await page.getByLabel("Search").click()
14
12
  await expect(page.getByRole("heading", { name: "Search" })).toBeVisible()
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "LightNet makes it easy to run your own digital media library.",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "version": "3.10.4",
6
+ "version": "3.10.6",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/LightNetDev/lightnet",
@@ -47,27 +47,27 @@
47
47
  "dependencies": {
48
48
  "@astrojs/react": "^4.4.2",
49
49
  "@astrojs/tailwind": "^6.0.2",
50
- "@hookform/error-message": "^2.0.1",
51
50
  "@hookform/resolvers": "^5.2.2",
52
51
  "@iconify-json/mdi": "^1.2.3",
53
52
  "@iconify/tailwind": "^1.2.0",
53
+ "@mdxeditor/editor": "^3.49.1",
54
54
  "@tailwindcss/typography": "^0.5.19",
55
55
  "@tanstack/react-virtual": "^3.13.12",
56
56
  "daisyui": "^4.12.24",
57
57
  "embla-carousel": "^8.6.0",
58
58
  "embla-carousel-wheel-gestures": "^8.1.0",
59
59
  "fuse.js": "^7.1.0",
60
- "i18next": "^25.6.1",
60
+ "i18next": "^25.6.2",
61
61
  "marked": "^16.4.2",
62
62
  "react-hook-form": "^7.66.0",
63
63
  "yaml": "^2.8.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@playwright/test": "^1.56.1",
67
- "@types/node": "^22.19.0",
68
- "@types/react": "^19.2.2",
67
+ "@types/node": "^22.19.1",
68
+ "@types/react": "^19.2.5",
69
69
  "typescript": "^5.9.3",
70
- "vitest": "^4.0.8"
70
+ "vitest": "^4.0.9"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=22"
@@ -5,6 +5,7 @@ import { defineConfig, devices } from "@playwright/test"
5
5
  */
6
6
  export default defineConfig({
7
7
  testDir: "./__e2e__",
8
+ globalTeardown: "./__e2e__/global.teardown.ts",
8
9
  /* Fail the build on CI if you accidentally left test.only in the source code. */
9
10
  forbidOnly: !!process.env.CI,
10
11
  /* Retry on CI only */
@@ -0,0 +1,74 @@
1
+ import type { ReactNode } from "react"
2
+ import {
3
+ type ArrayPath,
4
+ type Control,
5
+ type FieldValues,
6
+ useFieldArray,
7
+ type UseFieldArrayAppend,
8
+ } from "react-hook-form"
9
+
10
+ import Icon from "../../../components/Icon"
11
+ import { useI18n } from "../../../i18n/react/useI18n"
12
+ import ErrorMessage from "./atoms/ErrorMessage"
13
+ import Hint from "./atoms/Hint"
14
+ import Legend from "./atoms/Legend"
15
+ import { useFieldError } from "./hooks/use-field-error"
16
+
17
+ export default function DynamicArray<TFieldValues extends FieldValues>({
18
+ control,
19
+ name,
20
+ label,
21
+ hint,
22
+ renderElement,
23
+ addButton,
24
+ }: {
25
+ name: ArrayPath<TFieldValues>
26
+ label: string
27
+ hint?: string
28
+ control: Control<TFieldValues>
29
+ renderElement: (index: number) => ReactNode
30
+ addButton: {
31
+ label: string
32
+ onClick: (
33
+ append: UseFieldArrayAppend<TFieldValues, ArrayPath<TFieldValues>>,
34
+ elementIndex: number,
35
+ ) => void
36
+ }
37
+ }) {
38
+ const { fields, append, remove } = useFieldArray({
39
+ name,
40
+ control,
41
+ })
42
+ const { t } = useI18n()
43
+ const errorMessage = useFieldError({ control, name })
44
+ return (
45
+ <fieldset key={name}>
46
+ <Legend label={label} />
47
+ <div className="flex w-full flex-col divide-y divide-gray-300 overflow-hidden rounded-lg border border-gray-300 bg-gray-100 shadow-sm">
48
+ {fields.map((field, index) => (
49
+ <div className="flex w-full items-center gap-2 p-2" key={field.id}>
50
+ <div className="flex grow flex-col">{renderElement(index)}</div>
51
+ <button
52
+ className="flex items-center p-2 text-gray-600 transition-colors ease-in-out hover:text-rose-800"
53
+ type="button"
54
+ onClick={() => remove(index)}
55
+ >
56
+ <Icon className="mdi--remove" ariaLabel={t("ln.admin.remove")} />
57
+ </button>
58
+ </div>
59
+ ))}
60
+ <button
61
+ type="button"
62
+ className="p-4 text-sm font-bold text-gray-500 transition-colors ease-in-out hover:bg-gray-200"
63
+ onClick={() => {
64
+ addButton.onClick(append, fields.length)
65
+ }}
66
+ >
67
+ {t(addButton.label)}
68
+ </button>
69
+ </div>
70
+ <ErrorMessage message={errorMessage} />
71
+ <Hint label={hint} />
72
+ </fieldset>
73
+ )
74
+ }