lightnet 3.10.8 → 3.11.0

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 +10 -0
  2. package/__e2e__/admin.spec.ts +35 -1
  3. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  4. package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
  5. package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
  6. package/__e2e__/fixtures/basics/package.json +5 -5
  7. package/__tests__/pages/details-page/create-content-metadata.spec.ts +23 -3
  8. package/package.json +8 -8
  9. package/src/admin/components/form/DynamicArray.tsx +81 -29
  10. package/src/admin/components/form/Input.tsx +17 -3
  11. package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +2 -2
  12. package/src/admin/components/form/MarkdownEditor.tsx +12 -4
  13. package/src/admin/components/form/Select.tsx +25 -13
  14. package/src/admin/components/form/SubmitButton.tsx +2 -2
  15. package/src/admin/components/form/atoms/Button.tsx +27 -0
  16. package/src/admin/components/form/atoms/FileUpload.tsx +124 -49
  17. package/src/admin/components/form/atoms/Hint.tsx +2 -2
  18. package/src/admin/components/form/atoms/Label.tsx +17 -6
  19. package/src/admin/components/form/utils/get-border-class.ts +22 -0
  20. package/src/admin/i18n/translations/en.yml +19 -3
  21. package/src/admin/pages/AdminRoute.astro +1 -1
  22. package/src/admin/pages/media/EditForm.tsx +33 -15
  23. package/src/admin/pages/media/EditRoute.astro +2 -2
  24. package/src/admin/pages/media/fields/Authors.tsx +15 -5
  25. package/src/admin/pages/media/fields/Categories.tsx +17 -5
  26. package/src/admin/pages/media/fields/Collections.tsx +21 -11
  27. package/src/admin/pages/media/fields/Content.tsx +150 -0
  28. package/src/admin/pages/media/fields/Image.tsx +43 -10
  29. package/src/admin/pages/media/media-item-store.ts +16 -2
  30. package/src/admin/types/media-item.ts +9 -0
  31. package/src/components/SearchInput.astro +3 -3
  32. package/src/i18n/react/prepare-i18n-config.ts +1 -1
  33. package/src/i18n/react/use-i18n.ts +1 -1
  34. package/src/i18n/translations/TRANSLATION-STATUS.md +4 -0
  35. package/src/i18n/translations/ur.yml +25 -0
  36. package/src/i18n/translations.ts +1 -0
  37. package/src/layouts/Page.astro +0 -2
  38. package/src/layouts/components/Menu.astro +1 -1
  39. package/src/pages/404Route.astro +3 -1
  40. package/src/pages/details-page/utils/create-content-metadata.ts +2 -1
  41. package/src/pages/search-page/components/LoadingSkeleton.tsx +20 -13
  42. package/src/pages/search-page/components/SearchFilter.tsx +2 -2
  43. package/src/pages/search-page/components/SearchList.tsx +33 -29
  44. package/src/pages/search-page/components/Select.tsx +15 -13
  45. package/src/layouts/components/PreloadReact.tsx +0 -3
@@ -1,57 +1,103 @@
1
- import { type ChangeEvent, type DragEvent, useRef, useState } from "react"
2
1
  import {
3
- type Control,
4
- type FieldValues,
5
- type Path,
6
- useController,
7
- } from "react-hook-form"
2
+ type ChangeEvent,
3
+ type DragEvent,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from "react"
8
8
  import config from "virtual:lightnet/config"
9
9
 
10
+ import Icon from "../../../../components/Icon"
10
11
  import { useI18n } from "../../../../i18n/react/use-i18n"
11
12
 
12
13
  type FileType = "image/png" | "image/jpeg" | "image/webp"
13
14
 
14
- export default function FileUpload<TFieldValues extends FieldValues>({
15
- name,
16
- control,
17
- destinationPath,
18
- onFileChange,
19
- fileName,
15
+ export default function FileUpload({
16
+ onUpload,
17
+ onBlur,
20
18
  acceptedFileTypes,
19
+ title,
20
+ icon,
21
+ multiple,
22
+ description,
21
23
  }: {
22
- onFileChange: (file: File) => void
23
- control: Control<TFieldValues>
24
- name: Path<TFieldValues>
25
- destinationPath: string
26
- fileName?: string
27
- acceptedFileTypes: Readonly<FileType[]>
24
+ onUpload: (...file: File[]) => void
25
+ onBlur?: () => void
26
+ acceptedFileTypes?: Readonly<FileType[]>
27
+ title: string
28
+ icon?: string
29
+ multiple?: boolean
30
+ description: string
28
31
  }) {
29
- const { field } = useController({
30
- name,
31
- control,
32
- })
33
32
  const fileInputRef = useRef<HTMLInputElement | null>(null)
33
+ const invalidFeedbackTimeout = useRef<number | null>(null)
34
34
  const { t } = useI18n()
35
35
 
36
36
  const [isDragging, setIsDragging] = useState(false)
37
+ const [invalidFeedbackMessage, setInvalidFeedbackMessage] = useState<
38
+ string | null
39
+ >(null)
37
40
 
38
- const onFileSelected = (file?: File) => {
39
- if (!file) {
41
+ useEffect(() => {
42
+ return () => {
43
+ if (invalidFeedbackTimeout.current !== null) {
44
+ window.clearTimeout(invalidFeedbackTimeout.current)
45
+ }
46
+ }
47
+ }, [])
48
+
49
+ useEffect(() => {
50
+ const blockBrowserFileOpen = (event: globalThis.DragEvent) => {
51
+ event.preventDefault()
52
+ event.stopPropagation()
53
+ }
54
+ window.addEventListener("drop", blockBrowserFileOpen)
55
+ window.addEventListener("dragover", blockBrowserFileOpen)
56
+ return () => {
57
+ window.removeEventListener("drop", blockBrowserFileOpen)
58
+ window.removeEventListener("dragover", blockBrowserFileOpen)
59
+ }
60
+ }, [])
61
+
62
+ const triggerInvalidFeedback = (message: string) => {
63
+ setInvalidFeedbackMessage(message)
64
+ if (invalidFeedbackTimeout.current !== null) {
65
+ window.clearTimeout(invalidFeedbackTimeout.current)
66
+ }
67
+ invalidFeedbackTimeout.current = window.setTimeout(() => {
68
+ setInvalidFeedbackMessage(null)
69
+ invalidFeedbackTimeout.current = null
70
+ }, 2000)
71
+ }
72
+
73
+ const maxFileSizeBytes =
74
+ (config.experimental?.admin?.maxFileSize ?? 0) * 1024 * 1024
75
+
76
+ const onFilesSelected = (files?: File[]) => {
77
+ if (!files || files.length === 0) {
78
+ return
79
+ }
80
+
81
+ if (
82
+ acceptedFileTypes &&
83
+ files.find((file) => !acceptedFileTypes.includes(file.type as any))
84
+ ) {
85
+ triggerInvalidFeedback(t("ln.admin.file-invalid-type"))
40
86
  return
41
87
  }
42
- if (!acceptedFileTypes.includes(file.type as any)) {
88
+ if (
89
+ maxFileSizeBytes &&
90
+ files.find((file) => file.size > maxFileSizeBytes)
91
+ ) {
92
+ triggerInvalidFeedback(
93
+ t("ln.admin.file-too-big", {
94
+ maxFileSize: config.experimental?.admin?.maxFileSize,
95
+ }),
96
+ )
43
97
  return
44
98
  }
45
- const nameParts = file.name.split(".")
46
- const extension = nameParts.pop()
47
- const name = nameParts.join(".")
48
- field.onChange({
49
- ...field.value,
50
- path: `${destinationPath}/${fileName ?? name}.${extension}`,
51
- file,
52
- })
53
- onFileChange(file)
54
- field.onBlur()
99
+ onUpload(...files)
100
+ setInvalidFeedbackMessage(null)
55
101
  }
56
102
 
57
103
  const onDragEnter = (event: DragEvent<HTMLDivElement>) => {
@@ -62,23 +108,41 @@ export default function FileUpload<TFieldValues extends FieldValues>({
62
108
  const onDrop = (event: DragEvent<HTMLDivElement>) => {
63
109
  event.preventDefault()
64
110
  setIsDragging(false)
65
- onFileSelected(event.dataTransfer.files?.[0])
111
+ onFilesSelected([...event.dataTransfer.files])
66
112
  }
67
113
 
68
114
  const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
69
- onFileSelected(event.target.files?.[0])
115
+ const { files } = event.target
116
+ if (!files) {
117
+ return
118
+ }
119
+ const filesArray = []
120
+ for (let i = 0; i < files.length; i++) {
121
+ filesArray.push(files[i])
122
+ }
123
+ onFilesSelected(filesArray)
70
124
  // allow selecting the same file twice in a row
71
125
  event.target.value = ""
72
126
  }
73
127
 
128
+ const colorClass = () => {
129
+ if (invalidFeedbackMessage) {
130
+ return "bg-slate-200 border-rose-800 "
131
+ }
132
+ if (isDragging) {
133
+ return "border-sky-700 bg-sky-50"
134
+ }
135
+ return "bg-slate-100 border-slate-300 hover:bg-sky-50"
136
+ }
137
+
74
138
  return (
75
139
  <>
76
140
  <div
77
- className={`flex w-full flex-col items-center justify-center gap-1 rounded-md border-2 border-dashed border-gray-300 bg-gray-200 p-4 transition-colors ease-in-out ${isDragging ? "border-sky-700 bg-sky-50" : ""} focus-within:border-sky-700 hover:bg-sky-50`}
141
+ className={`relative flex w-full flex-col items-center justify-center gap-1 overflow-hidden rounded-xl border-2 border-dashed ${colorClass()} p-4 transition-colors ease-in-out focus-within:border-sky-700 focus-within:outline-none`}
78
142
  role="button"
79
143
  tabIndex={0}
144
+ onBlur={onBlur}
80
145
  onClick={() => fileInputRef.current?.click()}
81
- onBlur={field.onBlur}
82
146
  onKeyDown={(event) => {
83
147
  if (event.key === "Enter" || event.key === " ") {
84
148
  event.preventDefault()
@@ -89,25 +153,36 @@ export default function FileUpload<TFieldValues extends FieldValues>({
89
153
  onDragLeave={() => setIsDragging(false)}
90
154
  onDrop={onDrop}
91
155
  >
92
- <span className="text-sm text-gray-800">
93
- {t("ln.admin.file-upload-hint")}
156
+ <span className="mb-2 flex items-center gap-1 text-sm font-bold text-slate-700">
157
+ {icon && <Icon className={`${icon}`} ariaLabel="" />}
158
+ {t(title)}
94
159
  </span>
95
- <span className="text-xs text-gray-600">
96
- {t("ln.admin.file-upload-size-limit", {
97
- limit: config.experimental?.admin?.maxFileSize,
160
+ <span className="max-w-xs text-balance text-center text-xs text-slate-600">
161
+ {t(description, {
162
+ maxFileSize: config.experimental?.admin?.maxFileSize,
98
163
  })}
99
164
  </span>
165
+
166
+ {invalidFeedbackMessage && (
167
+ <div
168
+ className="pointer-events-none absolute inset-0 flex items-center justify-center gap-2 bg-slate-50/85 text-rose-800"
169
+ role="alert"
170
+ aria-hidden="true"
171
+ >
172
+ <Icon className="mdi--alert-circle-outline" ariaLabel="" />
173
+ <span className="font-semibold">{invalidFeedbackMessage}</span>
174
+ </div>
175
+ )}
100
176
  </div>
101
177
  <input
102
- id={field.name}
103
- name={field.name}
178
+ tabIndex={-1}
104
179
  ref={(ref) => {
105
180
  fileInputRef.current = ref
106
- field.ref(ref)
107
181
  }}
108
182
  type="file"
109
- accept={acceptedFileTypes.join(",")}
110
- className="sr-only"
183
+ multiple={multiple}
184
+ accept={acceptedFileTypes?.join(",")}
185
+ className="hidden"
111
186
  onChange={onInputChange}
112
187
  />
113
188
  </>
@@ -12,8 +12,8 @@ export default function Hint({
12
12
  return null
13
13
  }
14
14
  return (
15
- <div className="flex h-8 w-full items-start justify-end p-2">
16
- {label && <span className="dy-label-text-alt">{t(label)}</span>}
15
+ <div className="flex h-10 w-full items-start justify-end p-2">
16
+ {label && <span className="text-xs">{t(label)}</span>}
17
17
  </div>
18
18
  )
19
19
  }
@@ -6,21 +6,32 @@ export default function Label({
6
6
  className = "",
7
7
  isDirty,
8
8
  isInvalid,
9
+ required,
9
10
  }: {
10
11
  label: string
11
12
  className?: string
12
13
  size?: "small" | "medium"
13
14
  isDirty?: boolean
14
15
  isInvalid?: boolean
16
+ required: boolean
15
17
  }) {
16
18
  const { t } = useI18n()
19
+
20
+ const getColor = () => {
21
+ if (isInvalid) {
22
+ return "bg-rose-800 text-white"
23
+ }
24
+ if (isDirty) {
25
+ return "bg-slate-400 text-slate-950"
26
+ }
27
+ return "bg-slate-300 text-slate-800"
28
+ }
17
29
  return (
18
- <div className="flex">
19
- <span
20
- className={`rounded-t-md bg-gray-300 px-4 font-bold text-gray-700 shadow-sm transition-colors duration-150 group-focus-within:bg-sky-700 group-focus-within:text-gray-50 group-focus-within:ring-1 group-focus-within:ring-sky-700 ${size === "medium" ? "py-2 text-sm" : "py-1 text-xs"} ${isDirty ? "bg-gray-700 !text-white" : ""} ${isInvalid ? "bg-rose-800 !text-gray-50" : ""} ${className}`}
21
- >
22
- {t(label)}
23
- </span>
30
+ <div
31
+ className={`inline-flex rounded-t-xl px-4 font-bold ${getColor()} py-2 transition-colors duration-150 group-focus-within:!bg-sky-700 group-focus-within:text-slate-50 group-focus-within:ring-1 group-focus-within:ring-sky-700 ${size === "medium" ? "text-sm" : "text-xs"} ${className}`}
32
+ >
33
+ {t(label)}
34
+ {!required && <span className="ms-1">({t("ln.admin.optional")})</span>}
24
35
  </div>
25
36
  )
26
37
  }
@@ -0,0 +1,22 @@
1
+ const focusColors = (focusWithin: boolean | undefined) =>
2
+ focusWithin
3
+ ? "group-focus-within:border-sky-700 group-focus-within:ring-1 group-focus-within:ring-sky-700"
4
+ : "focus:border-sky-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-sky-700"
5
+
6
+ export const getBorderClass = ({
7
+ isDirty,
8
+ errorMessage,
9
+ focusWithin,
10
+ }: {
11
+ isDirty?: boolean
12
+ focusWithin?: boolean
13
+ errorMessage?: string
14
+ }) => {
15
+ if (errorMessage) {
16
+ return "border border-rose-800 " + focusColors(focusWithin)
17
+ }
18
+ if (isDirty) {
19
+ return "border border-slate-400 " + focusColors(focusWithin)
20
+ }
21
+ return "border border-slate-300 " + focusColors(focusWithin)
22
+ }
@@ -6,6 +6,18 @@ ln.admin.saved: Saved
6
6
  ln.admin.failed: Failed
7
7
  ln.admin.remove: Remove
8
8
  ln.admin.name: Name
9
+ ln.admin.move-up: Move up
10
+ ln.admin.move-down: Move down
11
+ ln.admin.content: Content
12
+ ln.admin.or: or
13
+ ln.admin.label: Label
14
+ ln.admin.link: Link
15
+ ln.admin.file: File
16
+ ln.admin.file-upload: File - Upload {{fileSize}} MB
17
+ ln.admin.add-link: Add link
18
+ ln.admin.primary-content: Primary content
19
+ ln.admin.files-upload-title: Upload files
20
+ ln.admin.files-upload-description: Drag files here or click to browse files. Files up to {{maxFileSize}} MB accepted.
9
21
  ln.admin.add-author: Add author
10
22
  ln.admin.add-category: Add category
11
23
  ln.admin.collections: Collections
@@ -16,11 +28,14 @@ ln.admin.back-to-details-page: Back to details page
16
28
  ln.admin.title: Title
17
29
  ln.admin.common-id: Common ID
18
30
  ln.admin.image: Image
19
- ln.admin.file-upload-hint: Drag a file here or click to browse files.
20
- ln.admin.file-upload-size-limit: "Files up to {{limit}} MB accepted."
21
- ln.admin.image-hint: Cover image with format PNG, JPG or WebP.
31
+ ln.admin.image-upload-title: Upload image
32
+ ln.admin.image-upload-description: Drop an image here or click to browse. PNG, JPG, or WebP image up to {{maxFileSize}} MB.
22
33
  ln.admin.select-file: Select file
34
+ ln.admin.file-invalid-type: File type not allowed.
35
+ ln.admin.file-too-big: File is too big. Max {{maxFileSize}} MB.
23
36
  ln.admin.authors: Authors
37
+ ln.admin.optional: optional
38
+ ln.admin.author-name: Author name
24
39
  ln.admin.description: Description
25
40
  ln.admin.date-created: Date Created
26
41
  ln.admin.date-created-hint: The date this item was added to this media library.
@@ -32,3 +47,4 @@ ln.admin.errors.required: This field is required.
32
47
  ln.admin.errors.gte-0: Use a number zero or greater.
33
48
  ln.admin.errors.unique-elements: Please choose a different value for each entry.
34
49
  ln.admin.errors.integer: Please enter a whole number.
50
+ ln.admin.errors.non-empty-list: List must contain at least 1 element.
@@ -4,7 +4,7 @@ import Page from "../../layouts/Page.astro"
4
4
 
5
5
  <Page>
6
6
  <div
7
- class="flex h-96 w-full items-center justify-center text-lg font-bold text-gray-500"
7
+ class="flex h-96 w-full items-center justify-center text-lg font-bold text-slate-500"
8
8
  >
9
9
  Admin features are enabled now.
10
10
  </div>
@@ -1,4 +1,5 @@
1
1
  import { zodResolver } from "@hookform/resolvers/zod"
2
+ import type { KeyboardEvent } from "react"
2
3
  import { useForm } from "react-hook-form"
3
4
 
4
5
  import {
@@ -14,6 +15,7 @@ import { type MediaItem, mediaItemSchema } from "../../types/media-item"
14
15
  import Authors from "./fields/Authors"
15
16
  import Categories from "./fields/Categories"
16
17
  import Collections from "./fields/Collections"
18
+ import Content from "./fields/Content"
17
19
  import Image from "./fields/Image"
18
20
  import { updateMediaItem } from "./media-item-store"
19
21
 
@@ -42,13 +44,24 @@ export default function EditForm({
42
44
  shouldFocusError: true,
43
45
  resolver: zodResolver(mediaItemSchema),
44
46
  })
47
+
48
+ const preventSubmitOnEnter = (event: KeyboardEvent<HTMLFormElement>) => {
49
+ if (event.key === "Enter" && !(event.target instanceof HTMLButtonElement)) {
50
+ event.preventDefault()
51
+ }
52
+ }
53
+
45
54
  const onSubmit = handleSubmit(
46
55
  async (data) => await updateMediaItem(mediaId, { ...mediaItem, ...data }),
47
56
  )
48
57
  const i18n = createI18n(i18nConfig)
49
58
  return (
50
59
  <I18nContext.Provider value={i18n}>
51
- <form className="flex flex-col" onSubmit={onSubmit}>
60
+ <form
61
+ className="flex flex-col"
62
+ onSubmit={onSubmit}
63
+ onKeyDown={preventSubmitOnEnter}
64
+ >
52
65
  <div className="mb-8 flex items-end justify-between">
53
66
  <h1 className="text-3xl">{i18n.t("ln.admin.edit-media-item")}</h1>
54
67
  <SubmitButton control={control} />
@@ -57,37 +70,50 @@ export default function EditForm({
57
70
  <Input
58
71
  name="title"
59
72
  label="ln.admin.title"
73
+ required
60
74
  control={control}
61
75
  defaultValue={mediaItem.title}
62
76
  />
63
77
  <Select
64
78
  name="type"
65
79
  label="ln.type"
80
+ required
66
81
  options={mediaTypes}
67
82
  control={control}
68
83
  defaultValue={mediaItem.type}
69
84
  />
70
- <Image
71
- control={control}
72
- defaultValue={mediaItem.image}
73
- mediaId={mediaId}
74
- />
75
85
  <Select
76
86
  name="language"
77
87
  label="ln.language"
88
+ required
78
89
  defaultValue={mediaItem.language}
79
90
  options={languages}
80
91
  control={control}
81
92
  />
82
- <Authors control={control} defaultValue={mediaItem.authors} />
93
+ <Image
94
+ control={control}
95
+ defaultValue={mediaItem.image}
96
+ mediaId={mediaId}
97
+ />
98
+ <Content control={control} defaultValue={mediaItem.content} />
83
99
  <Input
84
100
  name="dateCreated"
85
101
  label="ln.admin.date-created"
86
102
  hint="ln.admin.date-created-hint"
87
103
  type="date"
104
+ required
88
105
  defaultValue={mediaItem.dateCreated}
89
106
  control={control}
90
107
  />
108
+ <Input
109
+ name="commonId"
110
+ label="ln.admin.common-id"
111
+ required
112
+ hint="ln.admin.common-id-hint"
113
+ control={control}
114
+ defaultValue={mediaItem.commonId}
115
+ />
116
+ <Authors control={control} defaultValue={mediaItem.authors} />
91
117
  <Categories
92
118
  categories={categories}
93
119
  control={control}
@@ -103,14 +129,6 @@ export default function EditForm({
103
129
  name="description"
104
130
  label="ln.admin.description"
105
131
  />
106
- <Input
107
- name="commonId"
108
- label="ln.admin.common-id"
109
- hint="ln.admin.common-id-hint"
110
- control={control}
111
- defaultValue={mediaItem.commonId}
112
- />
113
-
114
132
  <SubmitButton className="mt-8 self-end" control={control} />
115
133
  </form>
116
134
  </I18nContext.Provider>
@@ -77,14 +77,14 @@ const languages = config.languages.map(({ code, label }) => ({
77
77
  >
78
78
  <div class="mx-auto block max-w-screen-md px-4 md:px-8">
79
79
  <a
80
- class="block pb-4 pt-8 text-gray-200 underline"
80
+ class="block pb-4 pt-8 text-slate-200 underline"
81
81
  href=`/${Astro.currentLocale}/media/${mediaId}`
82
82
  >{t("ln.admin.back-to-details-page")}</a
83
83
  >
84
84
  </div>
85
85
 
86
86
  <div
87
- class="mx-auto max-w-screen-md rounded-2xl bg-gray-200 px-4 py-8 shadow-md md:px-8"
87
+ class="mx-auto min-h-screen max-w-screen-md rounded-xl bg-slate-100 px-4 py-8 shadow-md md:px-8"
88
88
  >
89
89
  <EditForm
90
90
  mediaId={mediaId}
@@ -1,5 +1,7 @@
1
1
  import { type Control } from "react-hook-form"
2
2
 
3
+ import { useI18n } from "../../../../i18n/react/use-i18n"
4
+ import Button from "../../../components/form/atoms/Button"
3
5
  import DynamicArray from "../../../components/form/DynamicArray"
4
6
  import Input from "../../../components/form/Input"
5
7
  import type { MediaItem } from "../../../types/media-item"
@@ -11,6 +13,7 @@ export default function Authors({
11
13
  control: Control<MediaItem>
12
14
  defaultValue: MediaItem["authors"]
13
15
  }) {
16
+ const { t } = useI18n()
14
17
  return (
15
18
  <DynamicArray
16
19
  control={control}
@@ -20,15 +23,22 @@ export default function Authors({
20
23
  <Input
21
24
  name={`authors.${index}.value`}
22
25
  preserveHintSpace={false}
26
+ placeholder={t("ln.admin.author-name")}
27
+ required
23
28
  control={control}
24
29
  defaultValue={defaultValue[index]?.value}
25
30
  />
26
31
  )}
27
- addButton={{
28
- label: "ln.admin.add-author",
29
- onClick: (append, index) =>
30
- append({ value: "" }, { focusName: `authors.${index}.value` }),
31
- }}
32
+ renderAddButton={({ addElement, index }) => (
33
+ <Button
34
+ variant="secondary"
35
+ onClick={() =>
36
+ addElement({ value: "" }, { focusName: `authors.${index}.value` })
37
+ }
38
+ >
39
+ {t("ln.admin.add-author")}
40
+ </Button>
41
+ )}
32
42
  />
33
43
  )
34
44
  }
@@ -1,5 +1,7 @@
1
1
  import { type Control } from "react-hook-form"
2
2
 
3
+ import { useI18n } from "../../../../i18n/react/use-i18n"
4
+ import Button from "../../../components/form/atoms/Button"
3
5
  import DynamicArray from "../../../components/form/DynamicArray"
4
6
  import Select from "../../../components/form/Select"
5
7
  import type { MediaItem } from "../../../types/media-item"
@@ -13,6 +15,7 @@ export default function Categories({
13
15
  defaultValue: MediaItem["categories"]
14
16
  categories: { id: string; labelText: string }[]
15
17
  }) {
18
+ const { t } = useI18n()
16
19
  return (
17
20
  <DynamicArray
18
21
  control={control}
@@ -22,16 +25,25 @@ export default function Categories({
22
25
  <Select
23
26
  options={categories}
24
27
  control={control}
28
+ required
25
29
  name={`categories.${index}.value`}
26
30
  defaultValue={defaultValue[index]?.value}
27
31
  preserveHintSpace={false}
28
32
  />
29
33
  )}
30
- addButton={{
31
- label: "ln.admin.add-category",
32
- onClick: (append, index) =>
33
- append({ value: "" }, { focusName: `categories.${index}.value` }),
34
- }}
34
+ renderAddButton={({ addElement, index }) => (
35
+ <Button
36
+ variant="secondary"
37
+ onClick={() =>
38
+ addElement(
39
+ { value: "" },
40
+ { focusName: `categories.${index}.value` },
41
+ )
42
+ }
43
+ >
44
+ {t("ln.admin.add-category")}
45
+ </Button>
46
+ )}
35
47
  />
36
48
  )
37
49
  }
@@ -1,5 +1,7 @@
1
1
  import { type Control } from "react-hook-form"
2
2
 
3
+ import { useI18n } from "../../../../i18n/react/use-i18n"
4
+ import Button from "../../../components/form/atoms/Button"
3
5
  import DynamicArray from "../../../components/form/DynamicArray"
4
6
  import Input from "../../../components/form/Input"
5
7
  import Select from "../../../components/form/Select"
@@ -14,17 +16,19 @@ export default function Collections({
14
16
  collections: { id: string; labelText: string }[]
15
17
  defaultValue: MediaItem["collections"]
16
18
  }) {
19
+ const { t } = useI18n()
17
20
  return (
18
21
  <DynamicArray
19
22
  control={control}
20
23
  name="collections"
21
24
  label="ln.admin.collections"
22
25
  renderElement={(index) => (
23
- <div className="flex w-full flex-col gap-4 py-2">
26
+ <div className="flex w-full flex-col gap-6">
24
27
  <Select
25
28
  options={collections}
26
29
  label="ln.admin.name"
27
30
  labelSize="small"
31
+ required
28
32
  preserveHintSpace={false}
29
33
  name={`collections.${index}.collection`}
30
34
  control={control}
@@ -39,20 +43,26 @@ export default function Collections({
39
43
  min={0}
40
44
  preserveHintSpace={false}
41
45
  defaultValue={defaultValue[index]?.index}
42
- {...control.register(`collections.${index}.index`, {
46
+ name={`collections.${index}.index`}
47
+ registerOptions={{
43
48
  setValueAs: (value) => (value === "" ? undefined : Number(value)),
44
- })}
49
+ }}
45
50
  />
46
51
  </div>
47
52
  )}
48
- addButton={{
49
- label: "ln.admin.add-collection",
50
- onClick: (append, index) =>
51
- append(
52
- { collection: "" },
53
- { focusName: `collections.${index}.collection` },
54
- ),
55
- }}
53
+ renderAddButton={({ addElement, index }) => (
54
+ <Button
55
+ variant="secondary"
56
+ onClick={() =>
57
+ addElement(
58
+ { collection: "" },
59
+ { focusName: `collections.${index}.collection` },
60
+ )
61
+ }
62
+ >
63
+ {t("ln.admin.add-collection")}
64
+ </Button>
65
+ )}
56
66
  />
57
67
  )
58
68
  }