lightnet 3.10.5 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 3.10.5
4
10
 
5
11
  ### 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()
@@ -151,13 +146,10 @@ test.describe("Media item edit page", () => {
151
146
  page.getByRole("button", { name: "Published" }).first(),
152
147
  ).toBeVisible()
153
148
 
154
- test("should edit title", async ({ page, startLightnet }) => {
155
- const ln = await startLightnet()
156
-
149
+ test("should edit title", async ({ page, lightnet }) => {
150
+ await lightnet("/en/admin/media/faithful-freestyle--en")
157
151
  const writeFileRequest = await recordWriteFile(page)
158
152
 
159
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
160
-
161
153
  const updatedTitle = "Faithful Freestyle (Edited)"
162
154
  const titleInput = page.getByLabel("Title")
163
155
  await expect(titleInput).toHaveValue("Faithful Freestyle")
@@ -182,13 +174,11 @@ test.describe("Media item edit page", () => {
182
174
  await expectPublishedMessage(page)
183
175
  })
184
176
 
185
- test("Should update media type", async ({ page, startLightnet }) => {
186
- const ln = await startLightnet()
177
+ test("Should update media type", async ({ page, lightnet }) => {
178
+ await lightnet("/en/admin/media/faithful-freestyle--en")
187
179
  const writeFileRequest = await recordWriteFile(page)
188
180
 
189
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
190
-
191
- const typeSelect = page.getByLabel("Type")
181
+ const typeSelect = page.getByLabel("Type").first()
192
182
  await expect(typeSelect).toHaveValue("book")
193
183
  await typeSelect.selectOption("video")
194
184
 
@@ -207,12 +197,10 @@ test.describe("Media item edit page", () => {
207
197
  await expectPublishedMessage(page)
208
198
  })
209
199
 
210
- test("Should update author name", async ({ page, startLightnet }) => {
211
- const ln = await startLightnet()
200
+ test("Should update author name", async ({ page, lightnet }) => {
201
+ await lightnet("/en/admin/media/faithful-freestyle--en")
212
202
  const writeFileRequest = await recordWriteFile(page)
213
203
 
214
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
215
-
216
204
  const authorsFieldset = page.getByRole("group", { name: "Authors" })
217
205
  const firstAuthorInput = authorsFieldset.getByRole("textbox").first()
218
206
  const updatedAuthor = "Sk8 Ministries International"
@@ -234,12 +222,10 @@ test.describe("Media item edit page", () => {
234
222
  await expectPublishedMessage(page)
235
223
  })
236
224
 
237
- test("Should add author", async ({ page, startLightnet }) => {
238
- const ln = await startLightnet()
225
+ test("Should add author", async ({ page, lightnet }) => {
226
+ await lightnet("/en/admin/media/faithful-freestyle--en")
239
227
  const writeFileRequest = await recordWriteFile(page)
240
228
 
241
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
242
-
243
229
  const authorsFieldset = page.getByRole("group", { name: "Authors" })
244
230
  const addAuthorButton = page.getByRole("button", { name: "Add Author" })
245
231
  await addAuthorButton.click()
@@ -262,12 +248,10 @@ test.describe("Media item edit page", () => {
262
248
  await expectPublishedMessage(page)
263
249
  })
264
250
 
265
- test("Should remove author", async ({ page, startLightnet }) => {
266
- const ln = await startLightnet()
251
+ test("Should remove author", async ({ page, lightnet }) => {
252
+ await lightnet("/en/admin/media/faithful-freestyle--en")
267
253
  const writeFileRequest = await recordWriteFile(page)
268
254
 
269
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
270
-
271
255
  const authorsFieldset = page.getByRole("group", { name: "Authors" })
272
256
  const addAuthorButton = page.getByRole("button", { name: "Add Author" })
273
257
  const replacementAuthor = "Skate Evangelists"
@@ -297,10 +281,9 @@ test.describe("Media item edit page", () => {
297
281
 
298
282
  test("should show error message if common id is set empty", async ({
299
283
  page,
300
- startLightnet,
284
+ lightnet,
301
285
  }) => {
302
- const ln = await startLightnet()
303
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
286
+ await lightnet("/en/admin/media/faithful-freestyle--en")
304
287
 
305
288
  const commonIdInput = page.getByLabel("Common ID")
306
289
  await expect(commonIdInput).toHaveValue("faithful-freestyle")
@@ -317,10 +300,9 @@ test.describe("Media item edit page", () => {
317
300
 
318
301
  test("should focus invalid field when submitting invalid form data", async ({
319
302
  page,
320
- startLightnet,
303
+ lightnet,
321
304
  }) => {
322
- const ln = await startLightnet()
323
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
305
+ await lightnet("/en/admin/media/faithful-freestyle--en")
324
306
 
325
307
  const categoriesFieldset = page.getByRole("group", { name: "Categories" })
326
308
  await page.getByRole("button", { name: "Add Category" }).click()
@@ -341,10 +323,9 @@ test.describe("Media item edit page", () => {
341
323
 
342
324
  test("should not allow assigning duplicate categories", async ({
343
325
  page,
344
- startLightnet,
326
+ lightnet,
345
327
  }) => {
346
- const ln = await startLightnet()
347
- await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
328
+ await lightnet("/en/admin/media/faithful-freestyle--en")
348
329
 
349
330
  const categoriesFieldset = page.getByRole("group", { name: "Categories" })
350
331
  await page.getByRole("button", { name: "Add Category" }).click()
@@ -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.5_@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.5_@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"
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.5_@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.5_@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"
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.5",
11
- "lightnet": "^3.10.4",
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.5",
6
+ "version": "3.10.6",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/LightNetDev/lightnet",
@@ -50,6 +50,7 @@
50
50
  "@hookform/resolvers": "^5.2.2",
51
51
  "@iconify-json/mdi": "^1.2.3",
52
52
  "@iconify/tailwind": "^1.2.0",
53
+ "@mdxeditor/editor": "^3.49.1",
53
54
  "@tailwindcss/typography": "^0.5.19",
54
55
  "@tanstack/react-virtual": "^3.13.12",
55
56
  "daisyui": "^4.12.24",
@@ -64,9 +65,9 @@
64
65
  "devDependencies": {
65
66
  "@playwright/test": "^1.56.1",
66
67
  "@types/node": "^22.19.1",
67
- "@types/react": "^19.2.4",
68
+ "@types/react": "^19.2.5",
68
69
  "typescript": "^5.9.3",
69
- "vitest": "^4.0.8"
70
+ "vitest": "^4.0.9"
70
71
  },
71
72
  "engines": {
72
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 */
@@ -8,12 +8,14 @@ import { useFieldError } from "./hooks/use-field-error"
8
8
  export default function Input<TFieldValues extends FieldValues>({
9
9
  name,
10
10
  label,
11
+ defaultValue,
11
12
  hint,
12
13
  control,
13
14
  type = "text",
14
15
  }: {
15
16
  name: Path<TFieldValues>
16
17
  label: string
18
+ defaultValue?: string
17
19
  hint?: string
18
20
  control: Control<TFieldValues>
19
21
  type?: "text" | "date"
@@ -26,6 +28,7 @@ export default function Input<TFieldValues extends FieldValues>({
26
28
  className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
27
29
  type={type}
28
30
  id={name}
31
+ defaultValue={defaultValue}
29
32
  aria-invalid={!!errorMessage}
30
33
  {...control.register(name)}
31
34
  />
@@ -0,0 +1,78 @@
1
+ import "@mdxeditor/editor/style.css"
2
+
3
+ import {
4
+ BlockTypeSelect,
5
+ BoldItalicUnderlineToggles,
6
+ CreateLink,
7
+ diffSourcePlugin,
8
+ DiffSourceToggleWrapper,
9
+ headingsPlugin,
10
+ linkDialogPlugin,
11
+ linkPlugin,
12
+ listsPlugin,
13
+ ListsToggle,
14
+ MDXEditor,
15
+ quotePlugin,
16
+ toolbarPlugin,
17
+ UndoRedo,
18
+ } from "@mdxeditor/editor"
19
+ import {
20
+ type Control,
21
+ Controller,
22
+ type FieldValues,
23
+ type Path,
24
+ } from "react-hook-form"
25
+
26
+ /**
27
+ * IMPORTANT: Do not import this component directly. It is
28
+ * very big. Use it with React lazy loading.
29
+ */
30
+ export default function LazyLoadedMarkdownEditor<
31
+ TFieldValues extends FieldValues,
32
+ >({
33
+ control,
34
+ defaultValue,
35
+ name,
36
+ }: {
37
+ name: Path<TFieldValues>
38
+ control: Control<TFieldValues>
39
+ defaultValue?: string
40
+ }) {
41
+ return (
42
+ <Controller
43
+ control={control}
44
+ name={name}
45
+ render={({ field: { onBlur, onChange, value, ref } }) => (
46
+ <MDXEditor
47
+ markdown={value ?? ""}
48
+ onBlur={onBlur}
49
+ onChange={onChange}
50
+ contentEditableClassName="prose bg-gray-50 h-80 w-full max-w-full overflow-y-auto"
51
+ ref={ref}
52
+ plugins={[
53
+ headingsPlugin(),
54
+ listsPlugin(),
55
+ linkPlugin(),
56
+ linkDialogPlugin(),
57
+ diffSourcePlugin({
58
+ viewMode: "rich-text",
59
+ diffMarkdown: defaultValue,
60
+ }),
61
+ quotePlugin(),
62
+ toolbarPlugin({
63
+ toolbarContents: () => (
64
+ <DiffSourceToggleWrapper>
65
+ <UndoRedo />
66
+ <BoldItalicUnderlineToggles />
67
+ <BlockTypeSelect />
68
+ <ListsToggle options={["bullet", "number"]} />
69
+ <CreateLink />
70
+ </DiffSourceToggleWrapper>
71
+ ),
72
+ }),
73
+ ]}
74
+ />
75
+ )}
76
+ />
77
+ )
78
+ }
@@ -0,0 +1,52 @@
1
+ import { lazy, Suspense } from "react"
2
+ import { type Control, type FieldValues, type Path } from "react-hook-form"
3
+
4
+ import ErrorMessage from "./atoms/ErrorMessage"
5
+ import Hint from "./atoms/Hint"
6
+ import Legend from "./atoms/Legend"
7
+ import { useFieldError } from "./hooks/use-field-error"
8
+
9
+ const LazyLoadedMarkdownEditor = lazy(
10
+ () => import("./LazyLoadedMarkdownEditor"),
11
+ )
12
+
13
+ export default function MarkdownEditor<TFieldValues extends FieldValues>({
14
+ control,
15
+ defaultValue,
16
+ name,
17
+ label,
18
+ hint,
19
+ }: {
20
+ name: Path<TFieldValues>
21
+ label: string
22
+ hint?: string
23
+ control: Control<TFieldValues>
24
+ defaultValue?: string
25
+ }) {
26
+ const errorMessage = useFieldError({ control, name })
27
+
28
+ return (
29
+ <fieldset key={name}>
30
+ <Legend label={label} />
31
+ <div
32
+ className={`overflow-hidden rounded-lg border border-gray-300 shadow-sm ${errorMessage ? "border-rose-800" : ""}`}
33
+ >
34
+ <Suspense
35
+ fallback={
36
+ <div className="h-[22.75rem] w-full bg-gray-50">
37
+ <div className="h-10 bg-gray-100"></div>
38
+ </div>
39
+ }
40
+ >
41
+ <LazyLoadedMarkdownEditor
42
+ control={control as Control<any>}
43
+ name={name}
44
+ defaultValue={defaultValue}
45
+ />
46
+ </Suspense>
47
+ </div>
48
+ <ErrorMessage message={errorMessage} />
49
+ <Hint label={hint} />
50
+ </fieldset>
51
+ )
52
+ }
@@ -9,12 +9,14 @@ export default function Select<TFieldValues extends FieldValues>({
9
9
  name,
10
10
  label,
11
11
  control,
12
+ defaultValue,
12
13
  hint,
13
14
  options,
14
15
  }: {
15
16
  name: Path<TFieldValues>
16
17
  label: string
17
18
  hint?: string
19
+ defaultValue?: string
18
20
  control: Control<TFieldValues>
19
21
  options: { id: string; labelText?: string }[]
20
22
  }) {
@@ -26,6 +28,7 @@ export default function Select<TFieldValues extends FieldValues>({
26
28
  {...control.register(name)}
27
29
  id={name}
28
30
  aria-invalid={!!errorMessage}
31
+ defaultValue={defaultValue}
29
32
  className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
30
33
  >
31
34
  {options.map(({ id, labelText }) => (
@@ -15,7 +15,7 @@ export default function Label({
15
15
  return (
16
16
  <label
17
17
  htmlFor={htmlFor}
18
- className={`font-bold uppercase text-gray-500 ${size === "sm" ? "pb-2 text-sm" : "pb-1 text-xs"} ${className}`}
18
+ className={`font-bold uppercase text-gray-600 ${size === "sm" ? "pb-2 text-sm" : "pb-1 text-xs"} ${className}`}
19
19
  >
20
20
  {t(label)}
21
21
  </label>
@@ -12,7 +12,7 @@ export default function Legend({
12
12
  const { t } = useI18n()
13
13
  return (
14
14
  <legend
15
- className={`pb-2 font-bold uppercase text-gray-500 ${size === "sm" ? "text-sm" : "text-xs"} ${className}`}
15
+ className={`pb-2 font-bold uppercase text-gray-600 ${size === "sm" ? "text-sm" : "text-xs"} ${className}`}
16
16
  >
17
17
  {t(label)}
18
18
  </legend>
@@ -16,6 +16,7 @@ ln.admin.back-to-details-page: Back to details page
16
16
  ln.admin.title: Title
17
17
  ln.admin.common-id: Common ID
18
18
  ln.admin.authors: Authors
19
+ ln.admin.description: Description
19
20
  ln.admin.created-on: Created on
20
21
  ln.admin.created-on-hint: When has this item been created on this library?
21
22
  ln.admin.common-id-hint: The English title, all lowercase, words separated with hyphens.
@@ -7,6 +7,7 @@ import {
7
7
  I18nContext,
8
8
  } from "../../../i18n/react/i18n-context"
9
9
  import Input from "../../components/form/Input"
10
+ import MarkdownEditor from "../../components/form/MarkdownEditor"
10
11
  import Select from "../../components/form/Select"
11
12
  import SubmitButton from "../../components/form/SubmitButton"
12
13
  import { type MediaItem, mediaItemSchema } from "../../types/media-item"
@@ -35,6 +36,8 @@ export default function EditForm({
35
36
  collections: SelectOption[]
36
37
  }) {
37
38
  const { handleSubmit, control } = useForm({
39
+ // Provide per-input defaults so SSG prerender matches, but keep a full
40
+ // defaultValues object here because useFieldArray does not accept default values.
38
41
  defaultValues: mediaItem,
39
42
  mode: "onTouched",
40
43
  shouldFocusError: true,
@@ -52,35 +55,58 @@ export default function EditForm({
52
55
  <SubmitButton control={control} />
53
56
  </div>
54
57
 
55
- <Input name="title" label="ln.admin.title" control={control} />
58
+ <Input
59
+ name="title"
60
+ label="ln.admin.title"
61
+ control={control}
62
+ defaultValue={mediaItem.title}
63
+ />
56
64
  <Input
57
65
  name="commonId"
58
66
  label="ln.admin.common-id"
59
67
  hint="ln.admin.common-id-hint"
60
68
  control={control}
69
+ defaultValue={mediaItem.commonId}
61
70
  />
62
71
  <Select
63
72
  name="type"
64
73
  label="ln.type"
65
74
  options={mediaTypes}
66
75
  control={control}
76
+ defaultValue={mediaItem.type}
67
77
  />
68
78
  <Select
69
79
  name="language"
70
80
  label="ln.language"
81
+ defaultValue={mediaItem.language}
71
82
  options={languages}
72
83
  control={control}
73
84
  />
74
- <Authors control={control} />
85
+ <Authors control={control} defaultValue={mediaItem.authors} />
75
86
  <Input
76
87
  name="dateCreated"
77
88
  label="ln.admin.created-on"
78
89
  hint="ln.admin.created-on-hint"
79
90
  type="date"
91
+ defaultValue={mediaItem.dateCreated}
92
+ control={control}
93
+ />
94
+ <Categories
95
+ categories={categories}
96
+ control={control}
97
+ defaultValue={mediaItem.categories}
98
+ />
99
+ <Collections
100
+ collections={collections}
101
+ control={control}
102
+ defaultValue={mediaItem.collections}
103
+ />
104
+ <MarkdownEditor
80
105
  control={control}
106
+ name="description"
107
+ label="ln.admin.description"
108
+ defaultValue={mediaItem.description}
81
109
  />
82
- <Categories categories={categories} control={control} />
83
- <Collections collections={collections} control={control} />
84
110
 
85
111
  <SubmitButton className="self-end" control={control} />
86
112
  </form>
@@ -61,8 +61,8 @@ const languages = config.languages.map(({ code, label }) => ({
61
61
  <Page mainClass="bg-slate-500">
62
62
  <div class="mx-auto block max-w-screen-md px-4 md:px-8">
63
63
  <a
64
- class="block py-4 text-gray-200 underline"
65
- href=`/${Astro.currentLocale}/media/faithful-freestyle--en`
64
+ class="block pb-4 pt-8 text-gray-200 underline"
65
+ href=`/${Astro.currentLocale}/media/${mediaId}`
66
66
  >{t("ln.admin.back-to-details-page")}</a
67
67
  >
68
68
  </div>
@@ -5,13 +5,25 @@ import DynamicArray from "../../../components/form/DynamicArray"
5
5
  import { useFieldError } from "../../../components/form/hooks/use-field-error"
6
6
  import type { MediaItem } from "../../../types/media-item"
7
7
 
8
- export default function Authors({ control }: { control: Control<MediaItem> }) {
8
+ export default function Authors({
9
+ control,
10
+ defaultValue,
11
+ }: {
12
+ control: Control<MediaItem>
13
+ defaultValue: MediaItem["authors"]
14
+ }) {
9
15
  return (
10
16
  <DynamicArray
11
17
  control={control}
12
18
  name="authors"
13
19
  label="ln.admin.authors"
14
- renderElement={(index) => <AuthorInput index={index} control={control} />}
20
+ renderElement={(index) => (
21
+ <AuthorInput
22
+ index={index}
23
+ control={control}
24
+ defaultValue={defaultValue[index]?.value}
25
+ />
26
+ )}
15
27
  addButton={{
16
28
  label: "ln.admin.add-author",
17
29
  onClick: (append, index) =>
@@ -24,9 +36,11 @@ export default function Authors({ control }: { control: Control<MediaItem> }) {
24
36
  function AuthorInput({
25
37
  index,
26
38
  control,
39
+ defaultValue,
27
40
  }: {
28
41
  index: number
29
42
  control: Control<MediaItem>
43
+ defaultValue?: string
30
44
  }) {
31
45
  const name = `authors.${index}.value` as const
32
46
  const errorMessage = useFieldError({ name, control })
@@ -35,6 +49,7 @@ function AuthorInput({
35
49
  <input
36
50
  className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
37
51
  aria-invalid={!!errorMessage}
52
+ defaultValue={defaultValue}
38
53
  {...control.register(name)}
39
54
  />
40
55
  <ErrorMessage message={errorMessage} />
@@ -8,8 +8,10 @@ import type { MediaItem } from "../../../types/media-item"
8
8
  export default function Categories({
9
9
  control,
10
10
  categories,
11
+ defaultValue,
11
12
  }: {
12
13
  control: Control<MediaItem>
14
+ defaultValue: MediaItem["categories"]
13
15
  categories: { id: string; labelText: string }[]
14
16
  }) {
15
17
  return (
@@ -22,6 +24,7 @@ export default function Categories({
22
24
  categories={categories}
23
25
  control={control}
24
26
  index={index}
27
+ defaultValue={defaultValue[index]?.value}
25
28
  />
26
29
  )}
27
30
  addButton={{
@@ -36,10 +39,12 @@ export default function Categories({
36
39
  function CategorySelect({
37
40
  control,
38
41
  categories,
42
+ defaultValue,
39
43
  index,
40
44
  }: {
41
45
  control: Control<MediaItem>
42
46
  categories: { id: string; labelText: string }[]
47
+ defaultValue?: string
43
48
  index: number
44
49
  }) {
45
50
  const name = `categories.${index}.value` as const
@@ -49,6 +54,7 @@ function CategorySelect({
49
54
  <select
50
55
  {...control.register(name)}
51
56
  id={name}
57
+ defaultValue={defaultValue}
52
58
  aria-invalid={!!errorMessage}
53
59
  className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
54
60
  >
@@ -9,9 +9,11 @@ import type { MediaItem } from "../../../types/media-item"
9
9
  export default function Collections({
10
10
  control,
11
11
  collections,
12
+ defaultValue,
12
13
  }: {
13
14
  control: Control<MediaItem>
14
15
  collections: { id: string; labelText: string }[]
16
+ defaultValue: MediaItem["collections"]
15
17
  }) {
16
18
  return (
17
19
  <DynamicArray
@@ -24,8 +26,13 @@ export default function Collections({
24
26
  collections={collections}
25
27
  control={control}
26
28
  index={index}
29
+ defaultValue={defaultValue[index]?.collection}
30
+ />
31
+ <CollectionIndex
32
+ control={control}
33
+ index={index}
34
+ defaultValue={defaultValue[index]?.index}
27
35
  />
28
- <CollectionIndex control={control} index={index} />
29
36
  </div>
30
37
  )}
31
38
  addButton={{
@@ -44,9 +51,11 @@ function CollectionSelect({
44
51
  control,
45
52
  collections,
46
53
  index,
54
+ defaultValue,
47
55
  }: {
48
56
  control: Control<MediaItem>
49
57
  collections: { id: string; labelText: string }[]
58
+ defaultValue?: string
50
59
  index: number
51
60
  }) {
52
61
  const name = `collections.${index}.collection` as const
@@ -57,6 +66,7 @@ function CollectionSelect({
57
66
  <select
58
67
  {...control.register(name)}
59
68
  id={name}
69
+ defaultValue={defaultValue}
60
70
  aria-invalid={!!errorMessage}
61
71
  className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
62
72
  >
@@ -74,9 +84,11 @@ function CollectionSelect({
74
84
  function CollectionIndex({
75
85
  control,
76
86
  index,
87
+ defaultValue,
77
88
  }: {
78
89
  control: Control<MediaItem>
79
90
  index: number
91
+ defaultValue?: number
80
92
  }) {
81
93
  const name = `collections.${index}.index` as const
82
94
  const errorMessage = useFieldError({ name, control })
@@ -92,6 +104,8 @@ function CollectionIndex({
92
104
  className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
93
105
  aria-invalid={!!errorMessage}
94
106
  type="number"
107
+ id={name}
108
+ defaultValue={defaultValue}
95
109
  step={1}
96
110
  {...control.register(name, {
97
111
  setValueAs: (value) => (value === "" ? undefined : Number(value)),
@@ -46,6 +46,7 @@ export const mediaItemSchema = z.object({
46
46
  .array()
47
47
  .superRefine(unique("collection")),
48
48
  dateCreated: z.string().date(INVALID_DATE),
49
+ description: z.string().optional(),
49
50
  })
50
51
 
51
52
  export type MediaItem = z.input<typeof mediaItemSchema>