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.
- package/CHANGELOG.md +23 -0
- package/__e2e__/admin.spec.ts +19 -19
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +2 -2
- package/__tests__/utils/markdown.spec.ts +16 -0
- package/package.json +7 -7
- package/src/admin/components/form/DynamicArray.tsx +10 -7
- package/src/admin/components/form/Input.tsx +31 -16
- package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +34 -10
- package/src/admin/components/form/MarkdownEditor.tsx +10 -8
- package/src/admin/components/form/Select.tsx +21 -6
- package/src/admin/components/form/SubmitButton.tsx +12 -7
- package/src/admin/components/form/atoms/ErrorMessage.tsx +1 -1
- package/src/admin/components/form/atoms/FileUpload.tsx +115 -0
- package/src/admin/components/form/atoms/Hint.tsx +12 -3
- package/src/admin/components/form/atoms/Label.tsx +15 -12
- package/src/admin/components/form/hooks/use-field-dirty.tsx +12 -0
- package/src/admin/components/form/hooks/use-field-error.tsx +23 -2
- package/src/admin/i18n/admin-i18n.ts +21 -0
- package/src/admin/i18n/translations/en.yml +15 -9
- package/src/admin/pages/AdminRoute.astro +0 -2
- package/src/admin/pages/media/EditForm.tsx +16 -13
- package/src/admin/pages/media/EditRoute.astro +31 -15
- package/src/admin/pages/media/fields/Authors.tsx +4 -28
- package/src/admin/pages/media/fields/Categories.tsx +5 -38
- package/src/admin/pages/media/fields/Collections.tsx +19 -78
- package/src/admin/pages/media/fields/Image.tsx +86 -0
- package/src/admin/pages/media/file-system.ts +6 -2
- package/src/admin/pages/media/media-item-store.ts +32 -3
- package/src/admin/types/media-item.ts +20 -0
- package/src/astro-integration/config.ts +10 -0
- package/src/astro-integration/integration.ts +7 -3
- package/src/components/HighlightSection.astro +1 -1
- package/src/content/get-media-items.ts +2 -1
- package/src/i18n/react/i18n-context.ts +16 -5
- package/src/layouts/Page.astro +5 -4
- package/src/layouts/components/LanguagePicker.astro +11 -5
- package/src/layouts/components/Menu.astro +76 -10
- package/src/pages/details-page/components/main-details/EditButton.astro +1 -1
- package/src/pages/details-page/components/main-details/OpenButton.astro +1 -1
- package/src/pages/search-page/components/LoadingSkeleton.tsx +1 -1
- package/src/pages/search-page/components/SearchListItem.tsx +1 -1
- package/src/utils/markdown.ts +6 -0
- package/src/admin/components/form/atoms/Legend.tsx +0 -20
- /package/src/i18n/react/{useI18n.ts → use-i18n.ts} +0 -0
|
@@ -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/
|
|
1
|
+
import { useI18n } from "../../../../i18n/react/use-i18n"
|
|
2
2
|
|
|
3
|
-
export default function Hint({
|
|
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-
|
|
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/
|
|
1
|
+
import { useI18n } from "../../../../i18n/react/use-i18n"
|
|
2
2
|
|
|
3
3
|
export default function Label({
|
|
4
4
|
label,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
size = "medium",
|
|
6
|
+
className = "",
|
|
7
|
+
isDirty,
|
|
8
|
+
isInvalid,
|
|
8
9
|
}: {
|
|
9
10
|
label: string
|
|
10
|
-
for: string
|
|
11
11
|
className?: string
|
|
12
|
-
size?: "
|
|
12
|
+
size?: "small" | "medium"
|
|
13
|
+
isDirty?: boolean
|
|
14
|
+
isInvalid?: boolean
|
|
13
15
|
}) {
|
|
14
16
|
const { t } = useI18n()
|
|
15
17
|
return (
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
12
|
+
const { errors } = useFormState({ control, name, exact })
|
|
11
13
|
const error = get(errors, name) as { message: string } | undefined
|
|
12
|
-
|
|
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
|
|
2
|
+
ln.admin.publish-changes: Publish changes
|
|
3
3
|
ln.admin.published: Published
|
|
4
|
-
ln.admin.save-changes: Save
|
|
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
|
|
10
|
-
ln.admin.add-category: Add
|
|
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
|
|
13
|
-
ln.admin.edit-media-item: Edit
|
|
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
|
|
21
|
-
ln.admin.created-
|
|
22
|
-
ln.admin.common-id-hint:
|
|
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.
|
|
@@ -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
|
|
89
|
-
hint="ln.admin.created-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 } =
|
|
52
|
+
const { t, currentLocale } = adminI18n
|
|
41
53
|
|
|
42
|
-
const mediaTypes = (await getMediaTypes()).map(({ id
|
|
54
|
+
const mediaTypes = (await getMediaTypes()).map(({ id }) => ({
|
|
43
55
|
id,
|
|
44
|
-
labelText:
|
|
56
|
+
labelText: id,
|
|
45
57
|
}))
|
|
46
58
|
|
|
47
|
-
const categories = (await getCategories(currentLocale, t)).map(
|
|
48
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
<
|
|
22
|
-
|
|
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
|
|
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
|
-
<
|
|
24
|
-
|
|
22
|
+
<Select
|
|
23
|
+
options={categories}
|
|
25
24
|
control={control}
|
|
26
|
-
|
|
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
|
-
}
|