lightnet 3.10.6 → 3.10.8

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 +23 -0
  2. package/__e2e__/admin.spec.ts +19 -19
  3. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  4. package/__e2e__/fixtures/basics/package.json +2 -2
  5. package/__tests__/utils/markdown.spec.ts +16 -0
  6. package/package.json +7 -7
  7. package/src/admin/components/form/DynamicArray.tsx +10 -7
  8. package/src/admin/components/form/Input.tsx +31 -16
  9. package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +34 -10
  10. package/src/admin/components/form/MarkdownEditor.tsx +10 -8
  11. package/src/admin/components/form/Select.tsx +21 -6
  12. package/src/admin/components/form/SubmitButton.tsx +12 -7
  13. package/src/admin/components/form/atoms/ErrorMessage.tsx +1 -1
  14. package/src/admin/components/form/atoms/FileUpload.tsx +115 -0
  15. package/src/admin/components/form/atoms/Hint.tsx +12 -3
  16. package/src/admin/components/form/atoms/Label.tsx +15 -12
  17. package/src/admin/components/form/hooks/use-field-dirty.tsx +12 -0
  18. package/src/admin/components/form/hooks/use-field-error.tsx +23 -2
  19. package/src/admin/i18n/admin-i18n.ts +21 -0
  20. package/src/admin/i18n/translations/en.yml +15 -9
  21. package/src/admin/pages/AdminRoute.astro +0 -2
  22. package/src/admin/pages/media/EditForm.tsx +16 -13
  23. package/src/admin/pages/media/EditRoute.astro +31 -15
  24. package/src/admin/pages/media/fields/Authors.tsx +4 -28
  25. package/src/admin/pages/media/fields/Categories.tsx +5 -38
  26. package/src/admin/pages/media/fields/Collections.tsx +19 -78
  27. package/src/admin/pages/media/fields/Image.tsx +86 -0
  28. package/src/admin/pages/media/file-system.ts +6 -2
  29. package/src/admin/pages/media/media-item-store.ts +32 -3
  30. package/src/admin/types/media-item.ts +20 -0
  31. package/src/astro-integration/config.ts +10 -0
  32. package/src/astro-integration/integration.ts +7 -3
  33. package/src/components/HighlightSection.astro +1 -1
  34. package/src/content/get-media-items.ts +2 -1
  35. package/src/i18n/react/i18n-context.ts +16 -5
  36. package/src/layouts/Page.astro +5 -4
  37. package/src/layouts/components/LanguagePicker.astro +11 -5
  38. package/src/layouts/components/Menu.astro +76 -10
  39. package/src/pages/details-page/components/main-details/EditButton.astro +1 -1
  40. package/src/pages/details-page/components/main-details/OpenButton.astro +1 -1
  41. package/src/pages/search-page/components/LoadingSkeleton.tsx +1 -1
  42. package/src/pages/search-page/components/SearchListItem.tsx +1 -1
  43. package/src/utils/markdown.ts +6 -0
  44. package/src/admin/components/form/atoms/Legend.tsx +0 -20
  45. /package/src/i18n/react/{useI18n.ts → use-i18n.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { useI18n } from "../../../../i18n/react/useI18n"
1
+ import { useI18n } from "../../../../i18n/react/use-i18n"
2
2
 
3
3
  export default function ErrorMessage({ message }: { message?: string }) {
4
4
  const { t } = useI18n()
@@ -0,0 +1,115 @@
1
+ import { type ChangeEvent, type DragEvent, useRef, useState } from "react"
2
+ import {
3
+ type Control,
4
+ type FieldValues,
5
+ type Path,
6
+ useController,
7
+ } from "react-hook-form"
8
+ import config from "virtual:lightnet/config"
9
+
10
+ import { useI18n } from "../../../../i18n/react/use-i18n"
11
+
12
+ type FileType = "image/png" | "image/jpeg" | "image/webp"
13
+
14
+ export default function FileUpload<TFieldValues extends FieldValues>({
15
+ name,
16
+ control,
17
+ destinationPath,
18
+ onFileChange,
19
+ fileName,
20
+ acceptedFileTypes,
21
+ }: {
22
+ onFileChange: (file: File) => void
23
+ control: Control<TFieldValues>
24
+ name: Path<TFieldValues>
25
+ destinationPath: string
26
+ fileName?: string
27
+ acceptedFileTypes: Readonly<FileType[]>
28
+ }) {
29
+ const { field } = useController({
30
+ name,
31
+ control,
32
+ })
33
+ const fileInputRef = useRef<HTMLInputElement | null>(null)
34
+ const { t } = useI18n()
35
+
36
+ const [isDragging, setIsDragging] = useState(false)
37
+
38
+ const onFileSelected = (file?: File) => {
39
+ if (!file) {
40
+ return
41
+ }
42
+ if (!acceptedFileTypes.includes(file.type as any)) {
43
+ return
44
+ }
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()
55
+ }
56
+
57
+ const onDragEnter = (event: DragEvent<HTMLDivElement>) => {
58
+ event.preventDefault()
59
+ setIsDragging(true)
60
+ }
61
+
62
+ const onDrop = (event: DragEvent<HTMLDivElement>) => {
63
+ event.preventDefault()
64
+ setIsDragging(false)
65
+ onFileSelected(event.dataTransfer.files?.[0])
66
+ }
67
+
68
+ const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
69
+ onFileSelected(event.target.files?.[0])
70
+ // allow selecting the same file twice in a row
71
+ event.target.value = ""
72
+ }
73
+
74
+ return (
75
+ <>
76
+ <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`}
78
+ role="button"
79
+ tabIndex={0}
80
+ onClick={() => fileInputRef.current?.click()}
81
+ onBlur={field.onBlur}
82
+ onKeyDown={(event) => {
83
+ if (event.key === "Enter" || event.key === " ") {
84
+ event.preventDefault()
85
+ fileInputRef.current?.click()
86
+ }
87
+ }}
88
+ onDragOver={onDragEnter}
89
+ onDragLeave={() => setIsDragging(false)}
90
+ onDrop={onDrop}
91
+ >
92
+ <span className="text-sm text-gray-800">
93
+ {t("ln.admin.file-upload-hint")}
94
+ </span>
95
+ <span className="text-xs text-gray-600">
96
+ {t("ln.admin.file-upload-size-limit", {
97
+ limit: config.experimental?.admin?.maxFileSize,
98
+ })}
99
+ </span>
100
+ </div>
101
+ <input
102
+ id={field.name}
103
+ name={field.name}
104
+ ref={(ref) => {
105
+ fileInputRef.current = ref
106
+ field.ref(ref)
107
+ }}
108
+ type="file"
109
+ accept={acceptedFileTypes.join(",")}
110
+ className="sr-only"
111
+ onChange={onInputChange}
112
+ />
113
+ </>
114
+ )
115
+ }
@@ -1,9 +1,18 @@
1
- import { useI18n } from "../../../../i18n/react/useI18n"
1
+ import { useI18n } from "../../../../i18n/react/use-i18n"
2
2
 
3
- export default function Hint({ label }: { label?: string }) {
3
+ export default function Hint({
4
+ label,
5
+ preserveSpace,
6
+ }: {
7
+ label?: string
8
+ preserveSpace: boolean
9
+ }) {
4
10
  const { t } = useI18n()
11
+ if (!preserveSpace && !label) {
12
+ return null
13
+ }
5
14
  return (
6
- <div className="flex h-12 w-full items-start justify-end p-2">
15
+ <div className="flex h-8 w-full items-start justify-end p-2">
7
16
  {label && <span className="dy-label-text-alt">{t(label)}</span>}
8
17
  </div>
9
18
  )
@@ -1,23 +1,26 @@
1
- import { useI18n } from "../../../../i18n/react/useI18n"
1
+ import { useI18n } from "../../../../i18n/react/use-i18n"
2
2
 
3
3
  export default function Label({
4
4
  label,
5
- for: htmlFor,
6
- size = "sm",
7
- className,
5
+ size = "medium",
6
+ className = "",
7
+ isDirty,
8
+ isInvalid,
8
9
  }: {
9
10
  label: string
10
- for: string
11
11
  className?: string
12
- size?: "sm" | "xs"
12
+ size?: "small" | "medium"
13
+ isDirty?: boolean
14
+ isInvalid?: boolean
13
15
  }) {
14
16
  const { t } = useI18n()
15
17
  return (
16
- <label
17
- htmlFor={htmlFor}
18
- className={`font-bold uppercase text-gray-600 ${size === "sm" ? "pb-2 text-sm" : "pb-1 text-xs"} ${className}`}
19
- >
20
- {t(label)}
21
- </label>
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>
24
+ </div>
22
25
  )
23
26
  }
@@ -0,0 +1,12 @@
1
+ import { type Control, get, useFormState } from "react-hook-form"
2
+
3
+ export function useFieldDirty({
4
+ control,
5
+ name,
6
+ }: {
7
+ control: Control<any>
8
+ name: string
9
+ }) {
10
+ const { dirtyFields } = useFormState({ control, name, exact: true })
11
+ return get(dirtyFields, name) as boolean | undefined
12
+ }
@@ -3,11 +3,32 @@ import { type Control, get, useFormState } from "react-hook-form"
3
3
  export function useFieldError({
4
4
  control,
5
5
  name,
6
+ exact = true,
6
7
  }: {
7
8
  control: Control<any>
8
9
  name: string
10
+ exact?: boolean
9
11
  }) {
10
- const { errors } = useFormState({ control, name, exact: true })
12
+ const { errors } = useFormState({ control, name, exact })
11
13
  const error = get(errors, name) as { message: string } | undefined
12
- return error?.message
14
+ if (exact) {
15
+ return error?.message
16
+ } else {
17
+ return findErrorMessage(get(errors, name))
18
+ }
19
+ }
20
+
21
+ function findErrorMessage(errors?: unknown) {
22
+ if (!errors) {
23
+ return undefined
24
+ }
25
+ for (const [key, value] of Object.entries(errors)) {
26
+ if (key === "message" && typeof value === "string") {
27
+ return value
28
+ }
29
+ if (value && typeof value === "object" && !Array.isArray(value)) {
30
+ return findErrorMessage(value)
31
+ }
32
+ }
33
+ return undefined
13
34
  }
@@ -0,0 +1,21 @@
1
+ import config from "virtual:lightnet/config"
2
+
3
+ import { resolveDefaultLocale } from "../../i18n/resolve-default-locale"
4
+ import { resolveLanguage } from "../../i18n/resolve-language"
5
+ import { resolveLocales } from "../../i18n/resolve-locales"
6
+ import { translationKeys, useTranslate } from "../../i18n/translate"
7
+
8
+ const currentLocale = config.experimental?.admin?.languageCode ?? "en"
9
+ const t = useTranslate(currentLocale)
10
+ const { direction } = resolveLanguage(currentLocale)
11
+ const defaultLocale = resolveDefaultLocale(config)
12
+ const locales = resolveLocales(config)
13
+
14
+ export const adminI18n = {
15
+ currentLocale,
16
+ t,
17
+ direction,
18
+ translationKeys,
19
+ defaultLocale,
20
+ locales,
21
+ }
@@ -1,27 +1,33 @@
1
1
  ln.admin.edit: Edit
2
- ln.admin.publish-changes: Publish Changes
2
+ ln.admin.publish-changes: Publish changes
3
3
  ln.admin.published: Published
4
- ln.admin.save-changes: Save Changes
4
+ ln.admin.save-changes: Save changes
5
5
  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.add-author: Add Author
10
- ln.admin.add-category: Add Category
9
+ ln.admin.add-author: Add author
10
+ ln.admin.add-category: Add category
11
11
  ln.admin.collections: Collections
12
- ln.admin.add-collection: Add Collection
13
- ln.admin.edit-media-item: Edit media item
12
+ ln.admin.add-collection: Add collection
13
+ ln.admin.edit-media-item: Edit Media Item
14
14
  ln.admin.position-in-collection: Position in Collection
15
15
  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
+ 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.
22
+ ln.admin.select-file: Select file
18
23
  ln.admin.authors: Authors
19
24
  ln.admin.description: Description
20
- ln.admin.created-on: Created on
21
- ln.admin.created-on-hint: When has this item been created on this library?
22
- ln.admin.common-id-hint: The English title, all lowercase, words separated with hyphens.
25
+ ln.admin.date-created: Date Created
26
+ ln.admin.date-created-hint: The date this item was added to this media library.
27
+ ln.admin.common-id-hint: Use a shared Common ID to link translated versions of a media item.
23
28
  ln.admin.errors.non-empty-string: Please enter at least one character.
24
29
  ln.admin.errors.invalid-date: That date doesn't look right.
30
+ ln.admin.error.file-size-exceeded: This file is too big.
25
31
  ln.admin.errors.required: This field is required.
26
32
  ln.admin.errors.gte-0: Use a number zero or greater.
27
33
  ln.admin.errors.unique-elements: Please choose a different value for each entry.
@@ -1,7 +1,5 @@
1
1
  ---
2
2
  import Page from "../../layouts/Page.astro"
3
-
4
- export { getLocalePaths as getStaticPaths } from "../../i18n/get-locale-paths"
5
3
  ---
6
4
 
7
5
  <Page>
@@ -14,6 +14,7 @@ import { type MediaItem, mediaItemSchema } from "../../types/media-item"
14
14
  import Authors from "./fields/Authors"
15
15
  import Categories from "./fields/Categories"
16
16
  import Collections from "./fields/Collections"
17
+ import Image from "./fields/Image"
17
18
  import { updateMediaItem } from "./media-item-store"
18
19
 
19
20
  type SelectOption = { id: string; labelText: string }
@@ -36,8 +37,6 @@ export default function EditForm({
36
37
  collections: SelectOption[]
37
38
  }) {
38
39
  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.
41
40
  defaultValues: mediaItem,
42
41
  mode: "onTouched",
43
42
  shouldFocusError: true,
@@ -61,13 +60,6 @@ export default function EditForm({
61
60
  control={control}
62
61
  defaultValue={mediaItem.title}
63
62
  />
64
- <Input
65
- name="commonId"
66
- label="ln.admin.common-id"
67
- hint="ln.admin.common-id-hint"
68
- control={control}
69
- defaultValue={mediaItem.commonId}
70
- />
71
63
  <Select
72
64
  name="type"
73
65
  label="ln.type"
@@ -75,6 +67,11 @@ export default function EditForm({
75
67
  control={control}
76
68
  defaultValue={mediaItem.type}
77
69
  />
70
+ <Image
71
+ control={control}
72
+ defaultValue={mediaItem.image}
73
+ mediaId={mediaId}
74
+ />
78
75
  <Select
79
76
  name="language"
80
77
  label="ln.language"
@@ -85,8 +82,8 @@ export default function EditForm({
85
82
  <Authors control={control} defaultValue={mediaItem.authors} />
86
83
  <Input
87
84
  name="dateCreated"
88
- label="ln.admin.created-on"
89
- hint="ln.admin.created-on-hint"
85
+ label="ln.admin.date-created"
86
+ hint="ln.admin.date-created-hint"
90
87
  type="date"
91
88
  defaultValue={mediaItem.dateCreated}
92
89
  control={control}
@@ -105,10 +102,16 @@ export default function EditForm({
105
102
  control={control}
106
103
  name="description"
107
104
  label="ln.admin.description"
108
- defaultValue={mediaItem.description}
105
+ />
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}
109
112
  />
110
113
 
111
- <SubmitButton className="self-end" control={control} />
114
+ <SubmitButton className="mt-8 self-end" control={control} />
112
115
  </form>
113
116
  </I18nContext.Provider>
114
117
  )
@@ -1,28 +1,40 @@
1
1
  ---
2
2
  import type { GetStaticPaths } from "astro"
3
+ import { getImage } from "astro:assets"
3
4
  import { getCollection } from "astro:content"
4
5
  import config from "virtual:lightnet/config"
5
6
 
6
7
  import { getCategories } from "../../../content/get-categories"
7
- import { getRawMediaItem } from "../../../content/get-media-items"
8
+ import { getMediaItem, getRawMediaItem } from "../../../content/get-media-items"
8
9
  import { getMediaTypes } from "../../../content/get-media-types"
9
10
  import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
10
- import { resolveLocales } from "../../../i18n/resolve-locales"
11
11
  import Page from "../../../layouts/Page.astro"
12
+ import { adminI18n } from "../../i18n/admin-i18n"
12
13
  import EditForm from "./EditForm"
13
14
 
14
15
  export const getStaticPaths = (async () => {
15
16
  const mediaItems = await getCollection("media")
16
- return resolveLocales(config).flatMap((locale) =>
17
- mediaItems.map(({ id: mediaId }) => ({ params: { mediaId, locale } })),
18
- )
17
+ return mediaItems.map(({ id: mediaId }) => ({ params: { mediaId } }))
19
18
  }) satisfies GetStaticPaths
20
19
 
21
20
  const { mediaId } = Astro.params
22
21
  const { data: mediaItem } = await getRawMediaItem(mediaId)
22
+ const {
23
+ data: { image },
24
+ } = await getMediaItem(mediaId)
25
+
26
+ const previewImage = await getImage({
27
+ src: image,
28
+ width: 256,
29
+ format: "webp",
30
+ })
23
31
 
24
32
  const formData = {
25
33
  ...mediaItem,
34
+ image: {
35
+ path: mediaItem.image,
36
+ previewSrc: previewImage.src,
37
+ },
26
38
  authors: mediaItem.authors?.map((value) => ({ value })) ?? [],
27
39
  categories:
28
40
  mediaItem.categories?.map((value) => ({
@@ -31,34 +43,38 @@ const formData = {
31
43
  collections: mediaItem.collections ?? [],
32
44
  }
33
45
 
34
- const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
46
+ const i18nConfig = prepareI18nConfig(adminI18n, [
35
47
  "ln.admin.*",
36
48
  "ln.type",
37
49
  "ln.language",
38
50
  "ln.categories",
39
51
  ])
40
- const { t, currentLocale } = Astro.locals.i18n
52
+ const { t, currentLocale } = adminI18n
41
53
 
42
- const mediaTypes = (await getMediaTypes()).map(({ id, data: { label } }) => ({
54
+ const mediaTypes = (await getMediaTypes()).map(({ id }) => ({
43
55
  id,
44
- labelText: t(label),
56
+ labelText: id,
45
57
  }))
46
58
 
47
- const categories = (await getCategories(currentLocale, t)).map(
48
- ({ id, labelText }) => ({ id, labelText }),
49
- )
59
+ const categories = (await getCategories(currentLocale, t)).map(({ id }) => ({
60
+ id,
61
+ labelText: id,
62
+ }))
50
63
 
51
64
  const collections = (await getCollection("media-collections")).map(
52
- ({ id, data: { label } }) => ({ id, labelText: t(label) }),
65
+ ({ id }) => ({ id, labelText: id }),
53
66
  )
54
67
 
55
68
  const languages = config.languages.map(({ code, label }) => ({
56
69
  id: code,
57
- labelText: t(label),
70
+ labelText: `${code} - ${t(label)}`,
58
71
  }))
59
72
  ---
60
73
 
61
- <Page mainClass="bg-slate-500">
74
+ <Page
75
+ mainClass="bg-gradient-to-t from-sky-950 to-slate-800"
76
+ locale={currentLocale}
77
+ >
62
78
  <div class="mx-auto block max-w-screen-md px-4 md:px-8">
63
79
  <a
64
80
  class="block pb-4 pt-8 text-gray-200 underline"
@@ -1,8 +1,7 @@
1
1
  import { type Control } from "react-hook-form"
2
2
 
3
- import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
4
3
  import DynamicArray from "../../../components/form/DynamicArray"
5
- import { useFieldError } from "../../../components/form/hooks/use-field-error"
4
+ import Input from "../../../components/form/Input"
6
5
  import type { MediaItem } from "../../../types/media-item"
7
6
 
8
7
  export default function Authors({
@@ -18,8 +17,9 @@ export default function Authors({
18
17
  name="authors"
19
18
  label="ln.admin.authors"
20
19
  renderElement={(index) => (
21
- <AuthorInput
22
- index={index}
20
+ <Input
21
+ name={`authors.${index}.value`}
22
+ preserveHintSpace={false}
23
23
  control={control}
24
24
  defaultValue={defaultValue[index]?.value}
25
25
  />
@@ -32,27 +32,3 @@ export default function Authors({
32
32
  />
33
33
  )
34
34
  }
35
-
36
- function AuthorInput({
37
- index,
38
- control,
39
- defaultValue,
40
- }: {
41
- index: number
42
- control: Control<MediaItem>
43
- defaultValue?: string
44
- }) {
45
- const name = `authors.${index}.value` as const
46
- const errorMessage = useFieldError({ name, control })
47
- return (
48
- <>
49
- <input
50
- className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
51
- aria-invalid={!!errorMessage}
52
- defaultValue={defaultValue}
53
- {...control.register(name)}
54
- />
55
- <ErrorMessage message={errorMessage} />
56
- </>
57
- )
58
- }
@@ -1,8 +1,7 @@
1
1
  import { type Control } from "react-hook-form"
2
2
 
3
- import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
4
3
  import DynamicArray from "../../../components/form/DynamicArray"
5
- import { useFieldError } from "../../../components/form/hooks/use-field-error"
4
+ import Select from "../../../components/form/Select"
6
5
  import type { MediaItem } from "../../../types/media-item"
7
6
 
8
7
  export default function Categories({
@@ -20,11 +19,12 @@ export default function Categories({
20
19
  name="categories"
21
20
  label="ln.categories"
22
21
  renderElement={(index) => (
23
- <CategorySelect
24
- categories={categories}
22
+ <Select
23
+ options={categories}
25
24
  control={control}
26
- index={index}
25
+ name={`categories.${index}.value`}
27
26
  defaultValue={defaultValue[index]?.value}
27
+ preserveHintSpace={false}
28
28
  />
29
29
  )}
30
30
  addButton={{
@@ -35,36 +35,3 @@ export default function Categories({
35
35
  />
36
36
  )
37
37
  }
38
-
39
- function CategorySelect({
40
- control,
41
- categories,
42
- defaultValue,
43
- index,
44
- }: {
45
- control: Control<MediaItem>
46
- categories: { id: string; labelText: string }[]
47
- defaultValue?: string
48
- index: number
49
- }) {
50
- const name = `categories.${index}.value` as const
51
- const errorMessage = useFieldError({ name, control })
52
- return (
53
- <>
54
- <select
55
- {...control.register(name)}
56
- id={name}
57
- defaultValue={defaultValue}
58
- aria-invalid={!!errorMessage}
59
- className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
60
- >
61
- {categories.map(({ id, labelText }) => (
62
- <option key={id} value={id}>
63
- {labelText}
64
- </option>
65
- ))}
66
- </select>
67
- <ErrorMessage message={errorMessage} />
68
- </>
69
- )
70
- }