lightnet 3.10.8 → 3.12.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.
- package/CHANGELOG.md +18 -0
- package/__e2e__/admin.spec.ts +35 -1
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
- package/__e2e__/fixtures/basics/package.json +5 -5
- package/__tests__/pages/details-page/create-content-metadata.spec.ts +23 -3
- package/package.json +8 -8
- package/src/admin/components/form/DynamicArray.tsx +81 -29
- package/src/admin/components/form/Input.tsx +17 -3
- package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +12 -5
- package/src/admin/components/form/MarkdownEditor.tsx +12 -4
- package/src/admin/components/form/Select.tsx +25 -13
- package/src/admin/components/form/SubmitButton.tsx +2 -2
- package/src/admin/components/form/atoms/Button.tsx +27 -0
- package/src/admin/components/form/atoms/FileUpload.tsx +124 -49
- package/src/admin/components/form/atoms/Hint.tsx +2 -2
- package/src/admin/components/form/atoms/Label.tsx +17 -6
- package/src/admin/components/form/utils/get-border-class.ts +22 -0
- package/src/admin/i18n/translations/en.yml +19 -3
- package/src/admin/pages/AdminRoute.astro +1 -1
- package/src/admin/pages/media/EditForm.tsx +33 -15
- package/src/admin/pages/media/EditRoute.astro +2 -2
- package/src/admin/pages/media/fields/Authors.tsx +15 -5
- package/src/admin/pages/media/fields/Categories.tsx +17 -5
- package/src/admin/pages/media/fields/Collections.tsx +21 -11
- package/src/admin/pages/media/fields/Content.tsx +150 -0
- package/src/admin/pages/media/fields/Image.tsx +43 -10
- package/src/admin/pages/media/media-item-store.ts +16 -2
- package/src/admin/types/media-item.ts +9 -0
- package/src/components/SearchInput.astro +3 -3
- package/src/components/VideoPlayer.astro +74 -10
- package/src/i18n/react/prepare-i18n-config.ts +1 -1
- package/src/i18n/react/use-i18n.ts +1 -1
- package/src/i18n/translations/TRANSLATION-STATUS.md +4 -0
- package/src/i18n/translations/en.yml +3 -4
- package/src/i18n/translations/ur.yml +25 -0
- package/src/i18n/translations.ts +1 -0
- package/src/layouts/Page.astro +0 -2
- package/src/layouts/components/Menu.astro +1 -1
- package/src/pages/404Route.astro +3 -1
- package/src/pages/details-page/components/AudioPanel.astro +1 -12
- package/src/pages/details-page/components/AudioPlayer.astro +40 -8
- package/src/pages/details-page/utils/create-content-metadata.ts +2 -1
- package/src/pages/search-page/components/LoadingSkeleton.tsx +20 -13
- package/src/pages/search-page/components/SearchFilter.tsx +2 -2
- package/src/pages/search-page/components/SearchList.tsx +33 -29
- package/src/pages/search-page/components/Select.tsx +15 -13
- 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
|
|
4
|
-
type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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 (
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
111
|
+
onFilesSelected([...event.dataTransfer.files])
|
|
66
112
|
}
|
|
67
113
|
|
|
68
114
|
const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
69
|
-
|
|
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-
|
|
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-
|
|
93
|
-
{
|
|
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-
|
|
96
|
-
{t(
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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-
|
|
16
|
-
{label && <span className="
|
|
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-600 text-slate-50"
|
|
26
|
+
}
|
|
27
|
+
return "bg-slate-300 text-slate-700"
|
|
28
|
+
}
|
|
17
29
|
return (
|
|
18
|
-
<div
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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-600 " + 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.
|
|
20
|
-
ln.admin.
|
|
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-
|
|
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
|
|
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
|
-
<
|
|
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-
|
|
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-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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-
|
|
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
|
-
{
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
}
|