lightnet 3.10.5 → 3.10.7
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 +14 -0
- package/__e2e__/admin.spec.ts +29 -48
- package/__e2e__/{test-utils.ts → basics-fixture.ts} +20 -23
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +2 -2
- package/__e2e__/fixtures/basics/src/content/media/faithful-freestyle--en.json +1 -1
- package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +1 -1
- package/__e2e__/global.teardown.ts +5 -0
- package/__e2e__/homepage.spec.ts +17 -19
- package/__e2e__/search.spec.ts +3 -5
- package/__tests__/utils/markdown.spec.ts +16 -0
- package/package.json +6 -5
- package/playwright.config.ts +1 -0
- package/src/admin/components/form/DynamicArray.tsx +9 -6
- package/src/admin/components/form/Input.tsx +31 -13
- package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +102 -0
- package/src/admin/components/form/MarkdownEditor.tsx +54 -0
- package/src/admin/components/form/Select.tsx +24 -6
- package/src/admin/components/form/SubmitButton.tsx +11 -6
- package/src/admin/components/form/atoms/Hint.tsx +11 -2
- package/src/admin/components/form/atoms/Label.tsx +14 -11
- package/src/admin/components/form/hooks/use-field-dirty.tsx +12 -0
- package/src/admin/i18n/translations/en.yml +9 -8
- package/src/admin/pages/media/EditForm.tsx +29 -6
- package/src/admin/pages/media/EditRoute.astro +3 -3
- package/src/admin/pages/media/fields/Authors.tsx +16 -25
- package/src/admin/pages/media/fields/Categories.tsx +8 -35
- package/src/admin/pages/media/fields/Collections.tsx +25 -70
- package/src/admin/types/media-item.ts +1 -0
- package/src/components/HighlightSection.astro +1 -1
- package/src/pages/details-page/components/main-details/OpenButton.astro +1 -1
- package/src/utils/markdown.ts +6 -0
- package/src/admin/components/form/atoms/Legend.tsx +0 -20
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import "@mdxeditor/editor/style.css"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BlockTypeSelect,
|
|
5
|
+
BoldItalicUnderlineToggles,
|
|
6
|
+
type CodeBlockEditorProps,
|
|
7
|
+
codeBlockPlugin,
|
|
8
|
+
CreateLink,
|
|
9
|
+
headingsPlugin,
|
|
10
|
+
linkDialogPlugin,
|
|
11
|
+
linkPlugin,
|
|
12
|
+
listsPlugin,
|
|
13
|
+
ListsToggle,
|
|
14
|
+
markdownShortcutPlugin,
|
|
15
|
+
MDXEditor,
|
|
16
|
+
quotePlugin,
|
|
17
|
+
toolbarPlugin,
|
|
18
|
+
UndoRedo,
|
|
19
|
+
useCodeBlockEditorContext,
|
|
20
|
+
} from "@mdxeditor/editor"
|
|
21
|
+
import {
|
|
22
|
+
type Control,
|
|
23
|
+
Controller,
|
|
24
|
+
type FieldValues,
|
|
25
|
+
type Path,
|
|
26
|
+
} from "react-hook-form"
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* IMPORTANT: Do not import this component directly. It is
|
|
30
|
+
* very big. Use it with React lazy loading.
|
|
31
|
+
*/
|
|
32
|
+
export default function LazyLoadedMarkdownEditor<
|
|
33
|
+
TFieldValues extends FieldValues,
|
|
34
|
+
>({
|
|
35
|
+
control,
|
|
36
|
+
name,
|
|
37
|
+
}: {
|
|
38
|
+
name: Path<TFieldValues>
|
|
39
|
+
control: Control<TFieldValues>
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<Controller
|
|
43
|
+
control={control}
|
|
44
|
+
name={name}
|
|
45
|
+
render={({ field: { onBlur, onChange, value, ref } }) => (
|
|
46
|
+
<MDXEditor
|
|
47
|
+
markdown={value ?? ""}
|
|
48
|
+
onBlur={onBlur}
|
|
49
|
+
onChange={onChange}
|
|
50
|
+
contentEditableClassName="prose bg-gray-50 h-80 w-full max-w-full overflow-y-auto"
|
|
51
|
+
ref={ref}
|
|
52
|
+
onError={(error) =>
|
|
53
|
+
console.error("Error while editing markdown", error)
|
|
54
|
+
}
|
|
55
|
+
plugins={[
|
|
56
|
+
headingsPlugin(),
|
|
57
|
+
listsPlugin(),
|
|
58
|
+
linkPlugin(),
|
|
59
|
+
codeBlockPlugin({
|
|
60
|
+
codeBlockEditorDescriptors: [
|
|
61
|
+
{
|
|
62
|
+
match: () => true,
|
|
63
|
+
priority: 0,
|
|
64
|
+
Editor: TextAreaCodeEditor,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
}),
|
|
68
|
+
linkDialogPlugin(),
|
|
69
|
+
quotePlugin(),
|
|
70
|
+
markdownShortcutPlugin(),
|
|
71
|
+
toolbarPlugin({
|
|
72
|
+
toolbarContents: () => (
|
|
73
|
+
<>
|
|
74
|
+
<UndoRedo />
|
|
75
|
+
<BoldItalicUnderlineToggles />
|
|
76
|
+
<BlockTypeSelect />
|
|
77
|
+
<ListsToggle options={["bullet", "number"]} />
|
|
78
|
+
<CreateLink />
|
|
79
|
+
</>
|
|
80
|
+
),
|
|
81
|
+
toolbarClassName: "!rounded-none",
|
|
82
|
+
}),
|
|
83
|
+
]}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function TextAreaCodeEditor(props: CodeBlockEditorProps) {
|
|
91
|
+
const cb = useCodeBlockEditorContext()
|
|
92
|
+
return (
|
|
93
|
+
<div onKeyDown={(e) => e.nativeEvent.stopImmediatePropagation()}>
|
|
94
|
+
<textarea
|
|
95
|
+
rows={3}
|
|
96
|
+
cols={20}
|
|
97
|
+
defaultValue={props.code}
|
|
98
|
+
onChange={(e) => cb.setCode(e.target.value)}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { lazy, Suspense } from "react"
|
|
2
|
+
import { type Control, type FieldValues, type Path } from "react-hook-form"
|
|
3
|
+
|
|
4
|
+
import ErrorMessage from "./atoms/ErrorMessage"
|
|
5
|
+
import Hint from "./atoms/Hint"
|
|
6
|
+
import Label from "./atoms/Label"
|
|
7
|
+
import { useFieldDirty } from "./hooks/use-field-dirty"
|
|
8
|
+
import { useFieldError } from "./hooks/use-field-error"
|
|
9
|
+
|
|
10
|
+
const LazyLoadedMarkdownEditor = lazy(
|
|
11
|
+
() => import("./LazyLoadedMarkdownEditor"),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export default function MarkdownEditor<TFieldValues extends FieldValues>({
|
|
15
|
+
control,
|
|
16
|
+
name,
|
|
17
|
+
label,
|
|
18
|
+
hint,
|
|
19
|
+
}: {
|
|
20
|
+
name: Path<TFieldValues>
|
|
21
|
+
label: string
|
|
22
|
+
hint?: string
|
|
23
|
+
control: Control<TFieldValues>
|
|
24
|
+
}) {
|
|
25
|
+
const isDirty = useFieldDirty({ control, name })
|
|
26
|
+
const errorMessage = useFieldError({ control, name })
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<fieldset key={name} className="group">
|
|
30
|
+
<legend>
|
|
31
|
+
<Label label={label} isDirty={isDirty} isInvalid={!!errorMessage} />
|
|
32
|
+
</legend>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
className={`overflow-hidden rounded-lg rounded-ss-none border border-gray-300 shadow-sm group-focus-within:border-blue-700 group-focus-within:ring-1 group-focus-within:ring-blue-700 ${isDirty && !errorMessage ? "border-gray-700" : ""} ${errorMessage ? "border-rose-800" : ""}`}
|
|
36
|
+
>
|
|
37
|
+
<Suspense
|
|
38
|
+
fallback={
|
|
39
|
+
<div className="h-[22.75rem] w-full bg-gray-50">
|
|
40
|
+
<div className="h-10 bg-gray-100"></div>
|
|
41
|
+
</div>
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
<LazyLoadedMarkdownEditor
|
|
45
|
+
control={control as Control<any>}
|
|
46
|
+
name={name}
|
|
47
|
+
/>
|
|
48
|
+
</Suspense>
|
|
49
|
+
</div>
|
|
50
|
+
<ErrorMessage message={errorMessage} />
|
|
51
|
+
<Hint preserveSpace={true} label={hint} />
|
|
52
|
+
</fieldset>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -1,32 +1,50 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Control, type FieldValues, type Path } from "react-hook-form"
|
|
2
2
|
|
|
3
3
|
import ErrorMessage from "./atoms/ErrorMessage"
|
|
4
4
|
import Hint from "./atoms/Hint"
|
|
5
5
|
import Label from "./atoms/Label"
|
|
6
|
+
import { useFieldDirty } from "./hooks/use-field-dirty"
|
|
6
7
|
import { useFieldError } from "./hooks/use-field-error"
|
|
7
8
|
|
|
8
9
|
export default function Select<TFieldValues extends FieldValues>({
|
|
9
10
|
name,
|
|
10
11
|
label,
|
|
12
|
+
labelSize,
|
|
11
13
|
control,
|
|
14
|
+
defaultValue,
|
|
12
15
|
hint,
|
|
16
|
+
preserveHintSpace = true,
|
|
13
17
|
options,
|
|
14
18
|
}: {
|
|
15
19
|
name: Path<TFieldValues>
|
|
16
|
-
label
|
|
20
|
+
label?: string
|
|
21
|
+
labelSize?: "small" | "medium"
|
|
17
22
|
hint?: string
|
|
23
|
+
preserveHintSpace?: boolean
|
|
24
|
+
defaultValue?: string
|
|
18
25
|
control: Control<TFieldValues>
|
|
19
26
|
options: { id: string; labelText?: string }[]
|
|
20
27
|
}) {
|
|
28
|
+
const isDirty = useFieldDirty({ control, name })
|
|
21
29
|
const errorMessage = useFieldError({ control, name })
|
|
22
30
|
return (
|
|
23
|
-
<div key={name} className="flex w-full flex-col">
|
|
24
|
-
|
|
31
|
+
<div key={name} className="group flex w-full flex-col">
|
|
32
|
+
{label && (
|
|
33
|
+
<label htmlFor={name}>
|
|
34
|
+
<Label
|
|
35
|
+
label={label}
|
|
36
|
+
size={labelSize}
|
|
37
|
+
isDirty={isDirty}
|
|
38
|
+
isInvalid={!!errorMessage}
|
|
39
|
+
/>
|
|
40
|
+
</label>
|
|
41
|
+
)}
|
|
25
42
|
<select
|
|
26
43
|
{...control.register(name)}
|
|
27
44
|
id={name}
|
|
28
45
|
aria-invalid={!!errorMessage}
|
|
29
|
-
|
|
46
|
+
defaultValue={defaultValue}
|
|
47
|
+
className={`dy-select dy-select-bordered text-base shadow-sm focus:border-blue-700 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-700 ${isDirty && !errorMessage ? "border-gray-700" : ""} ${errorMessage ? "border-rose-800" : ""} ${label ? "rounded-ss-none" : ""}`}
|
|
30
48
|
>
|
|
31
49
|
{options.map(({ id, labelText }) => (
|
|
32
50
|
<option key={id} value={id}>
|
|
@@ -35,7 +53,7 @@ export default function Select<TFieldValues extends FieldValues>({
|
|
|
35
53
|
))}
|
|
36
54
|
</select>
|
|
37
55
|
<ErrorMessage message={errorMessage} />
|
|
38
|
-
<Hint label={hint} />
|
|
56
|
+
<Hint preserveSpace={preserveHintSpace} label={hint} />
|
|
39
57
|
</div>
|
|
40
58
|
)
|
|
41
59
|
}
|
|
@@ -8,10 +8,10 @@ import type { MediaItem } from "../../types/media-item"
|
|
|
8
8
|
const SUCCESS_DURATION_MS = 2000
|
|
9
9
|
|
|
10
10
|
const baseButtonClass =
|
|
11
|
-
"flex min-w-52 items-center justify-center gap-2 rounded-2xl px-4 py-3 font-bold
|
|
11
|
+
"flex min-w-52 items-center justify-center gap-2 rounded-2xl px-4 py-3 font-bold shadow-sm transition-colors easy-in-out focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-blue-700 focus-visible:ring-offset-1 disabled:cursor-not-allowed"
|
|
12
12
|
|
|
13
13
|
const buttonStateClasses = {
|
|
14
|
-
idle: "bg-gray-800 text-gray-
|
|
14
|
+
idle: "bg-gray-800 text-gray-50 hover:bg-gray-950 hover:text-gray-300 disabled:bg-gray-500 disabled:text-gray-300",
|
|
15
15
|
error:
|
|
16
16
|
"bg-rose-700 text-white hover:bg-rose-800 hover:text-white disabled:bg-rose-600",
|
|
17
17
|
success:
|
|
@@ -40,9 +40,10 @@ export default function SubmitButton({
|
|
|
40
40
|
className?: string
|
|
41
41
|
}) {
|
|
42
42
|
const { t } = useI18n()
|
|
43
|
-
const { isSubmitting, isSubmitSuccessful, submitCount } =
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const { isSubmitting, isSubmitSuccessful, submitCount, isDirty } =
|
|
44
|
+
useFormState({
|
|
45
|
+
control,
|
|
46
|
+
})
|
|
46
47
|
|
|
47
48
|
const buttonState = useButtonState(isSubmitSuccessful, submitCount)
|
|
48
49
|
const buttonClass = `${baseButtonClass} ${buttonStateClasses[buttonState]} ${className}`
|
|
@@ -50,7 +51,11 @@ export default function SubmitButton({
|
|
|
50
51
|
const icon = icons[buttonState]
|
|
51
52
|
|
|
52
53
|
return (
|
|
53
|
-
<button
|
|
54
|
+
<button
|
|
55
|
+
className={buttonClass}
|
|
56
|
+
type="submit"
|
|
57
|
+
disabled={isSubmitting || !isDirty}
|
|
58
|
+
>
|
|
54
59
|
{icon && <Icon className={icon} ariaLabel="" />}
|
|
55
60
|
{t(label)}
|
|
56
61
|
</button>
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
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
|
)
|
|
@@ -2,22 +2,25 @@ import { useI18n } from "../../../../i18n/react/useI18n"
|
|
|
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-blue-700 group-focus-within:text-gray-50 group-focus-within:ring-1 group-focus-within:ring-blue-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
|
+
}
|
|
@@ -1,23 +1,24 @@
|
|
|
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
18
|
ln.admin.authors: Authors
|
|
19
|
-
ln.admin.
|
|
20
|
-
ln.admin.created
|
|
19
|
+
ln.admin.description: Description
|
|
20
|
+
ln.admin.date-created: Date Created
|
|
21
|
+
ln.admin.date-created-hint: When has this item been created on this library?
|
|
21
22
|
ln.admin.common-id-hint: The English title, all lowercase, words separated with hyphens.
|
|
22
23
|
ln.admin.errors.non-empty-string: Please enter at least one character.
|
|
23
24
|
ln.admin.errors.invalid-date: That date doesn't look right.
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
I18nContext,
|
|
8
8
|
} from "../../../i18n/react/i18n-context"
|
|
9
9
|
import Input from "../../components/form/Input"
|
|
10
|
+
import MarkdownEditor from "../../components/form/MarkdownEditor"
|
|
10
11
|
import Select from "../../components/form/Select"
|
|
11
12
|
import SubmitButton from "../../components/form/SubmitButton"
|
|
12
13
|
import { type MediaItem, mediaItemSchema } from "../../types/media-item"
|
|
@@ -52,35 +53,57 @@ export default function EditForm({
|
|
|
52
53
|
<SubmitButton control={control} />
|
|
53
54
|
</div>
|
|
54
55
|
|
|
55
|
-
<Input
|
|
56
|
+
<Input
|
|
57
|
+
name="title"
|
|
58
|
+
label="ln.admin.title"
|
|
59
|
+
control={control}
|
|
60
|
+
defaultValue={mediaItem.title}
|
|
61
|
+
/>
|
|
56
62
|
<Input
|
|
57
63
|
name="commonId"
|
|
58
64
|
label="ln.admin.common-id"
|
|
59
65
|
hint="ln.admin.common-id-hint"
|
|
60
66
|
control={control}
|
|
67
|
+
defaultValue={mediaItem.commonId}
|
|
61
68
|
/>
|
|
62
69
|
<Select
|
|
63
70
|
name="type"
|
|
64
71
|
label="ln.type"
|
|
65
72
|
options={mediaTypes}
|
|
66
73
|
control={control}
|
|
74
|
+
defaultValue={mediaItem.type}
|
|
67
75
|
/>
|
|
68
76
|
<Select
|
|
69
77
|
name="language"
|
|
70
78
|
label="ln.language"
|
|
79
|
+
defaultValue={mediaItem.language}
|
|
71
80
|
options={languages}
|
|
72
81
|
control={control}
|
|
73
82
|
/>
|
|
74
|
-
<Authors control={control} />
|
|
83
|
+
<Authors control={control} defaultValue={mediaItem.authors} />
|
|
75
84
|
<Input
|
|
76
85
|
name="dateCreated"
|
|
77
|
-
label="ln.admin.created
|
|
78
|
-
hint="ln.admin.created-
|
|
86
|
+
label="ln.admin.date-created"
|
|
87
|
+
hint="ln.admin.date-created-hint"
|
|
79
88
|
type="date"
|
|
89
|
+
defaultValue={mediaItem.dateCreated}
|
|
90
|
+
control={control}
|
|
91
|
+
/>
|
|
92
|
+
<Categories
|
|
93
|
+
categories={categories}
|
|
94
|
+
control={control}
|
|
95
|
+
defaultValue={mediaItem.categories}
|
|
96
|
+
/>
|
|
97
|
+
<Collections
|
|
98
|
+
collections={collections}
|
|
99
|
+
control={control}
|
|
100
|
+
defaultValue={mediaItem.collections}
|
|
101
|
+
/>
|
|
102
|
+
<MarkdownEditor
|
|
80
103
|
control={control}
|
|
104
|
+
name="description"
|
|
105
|
+
label="ln.admin.description"
|
|
81
106
|
/>
|
|
82
|
-
<Categories categories={categories} control={control} />
|
|
83
|
-
<Collections collections={collections} control={control} />
|
|
84
107
|
|
|
85
108
|
<SubmitButton className="self-end" control={control} />
|
|
86
109
|
</form>
|
|
@@ -58,11 +58,11 @@ const languages = config.languages.map(({ code, label }) => ({
|
|
|
58
58
|
}))
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
-
<Page mainClass="bg-
|
|
61
|
+
<Page mainClass="bg-gray-700">
|
|
62
62
|
<div class="mx-auto block max-w-screen-md px-4 md:px-8">
|
|
63
63
|
<a
|
|
64
|
-
class="block
|
|
65
|
-
href=`/${Astro.currentLocale}/media
|
|
64
|
+
class="block pb-4 pt-8 text-gray-200 underline"
|
|
65
|
+
href=`/${Astro.currentLocale}/media/${mediaId}`
|
|
66
66
|
>{t("ln.admin.back-to-details-page")}</a
|
|
67
67
|
>
|
|
68
68
|
</div>
|
|
@@ -1,17 +1,29 @@
|
|
|
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
|
-
export default function Authors({
|
|
7
|
+
export default function Authors({
|
|
8
|
+
control,
|
|
9
|
+
defaultValue,
|
|
10
|
+
}: {
|
|
11
|
+
control: Control<MediaItem>
|
|
12
|
+
defaultValue: MediaItem["authors"]
|
|
13
|
+
}) {
|
|
9
14
|
return (
|
|
10
15
|
<DynamicArray
|
|
11
16
|
control={control}
|
|
12
17
|
name="authors"
|
|
13
18
|
label="ln.admin.authors"
|
|
14
|
-
renderElement={(index) =>
|
|
19
|
+
renderElement={(index) => (
|
|
20
|
+
<Input
|
|
21
|
+
name={`authors.${index}.value`}
|
|
22
|
+
preserveHintSpace={false}
|
|
23
|
+
control={control}
|
|
24
|
+
defaultValue={defaultValue[index]?.value}
|
|
25
|
+
/>
|
|
26
|
+
)}
|
|
15
27
|
addButton={{
|
|
16
28
|
label: "ln.admin.add-author",
|
|
17
29
|
onClick: (append, index) =>
|
|
@@ -20,24 +32,3 @@ export default function Authors({ control }: { control: Control<MediaItem> }) {
|
|
|
20
32
|
/>
|
|
21
33
|
)
|
|
22
34
|
}
|
|
23
|
-
|
|
24
|
-
function AuthorInput({
|
|
25
|
-
index,
|
|
26
|
-
control,
|
|
27
|
-
}: {
|
|
28
|
-
index: number
|
|
29
|
-
control: Control<MediaItem>
|
|
30
|
-
}) {
|
|
31
|
-
const name = `authors.${index}.value` as const
|
|
32
|
-
const errorMessage = useFieldError({ name, control })
|
|
33
|
-
return (
|
|
34
|
-
<>
|
|
35
|
-
<input
|
|
36
|
-
className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
|
|
37
|
-
aria-invalid={!!errorMessage}
|
|
38
|
-
{...control.register(name)}
|
|
39
|
-
/>
|
|
40
|
-
<ErrorMessage message={errorMessage} />
|
|
41
|
-
</>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
@@ -1,15 +1,16 @@
|
|
|
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({
|
|
9
8
|
control,
|
|
10
9
|
categories,
|
|
10
|
+
defaultValue,
|
|
11
11
|
}: {
|
|
12
12
|
control: Control<MediaItem>
|
|
13
|
+
defaultValue: MediaItem["categories"]
|
|
13
14
|
categories: { id: string; labelText: string }[]
|
|
14
15
|
}) {
|
|
15
16
|
return (
|
|
@@ -18,10 +19,12 @@ export default function Categories({
|
|
|
18
19
|
name="categories"
|
|
19
20
|
label="ln.categories"
|
|
20
21
|
renderElement={(index) => (
|
|
21
|
-
<
|
|
22
|
-
|
|
22
|
+
<Select
|
|
23
|
+
options={categories}
|
|
23
24
|
control={control}
|
|
24
|
-
|
|
25
|
+
name={`categories.${index}.value`}
|
|
26
|
+
defaultValue={defaultValue[index]?.value}
|
|
27
|
+
preserveHintSpace={false}
|
|
25
28
|
/>
|
|
26
29
|
)}
|
|
27
30
|
addButton={{
|
|
@@ -32,33 +35,3 @@ export default function Categories({
|
|
|
32
35
|
/>
|
|
33
36
|
)
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
function CategorySelect({
|
|
37
|
-
control,
|
|
38
|
-
categories,
|
|
39
|
-
index,
|
|
40
|
-
}: {
|
|
41
|
-
control: Control<MediaItem>
|
|
42
|
-
categories: { id: string; labelText: string }[]
|
|
43
|
-
index: number
|
|
44
|
-
}) {
|
|
45
|
-
const name = `categories.${index}.value` as const
|
|
46
|
-
const errorMessage = useFieldError({ name, control })
|
|
47
|
-
return (
|
|
48
|
-
<>
|
|
49
|
-
<select
|
|
50
|
-
{...control.register(name)}
|
|
51
|
-
id={name}
|
|
52
|
-
aria-invalid={!!errorMessage}
|
|
53
|
-
className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
|
|
54
|
-
>
|
|
55
|
-
{categories.map(({ id, labelText }) => (
|
|
56
|
-
<option key={id} value={id}>
|
|
57
|
-
{labelText}
|
|
58
|
-
</option>
|
|
59
|
-
))}
|
|
60
|
-
</select>
|
|
61
|
-
<ErrorMessage message={errorMessage} />
|
|
62
|
-
</>
|
|
63
|
-
)
|
|
64
|
-
}
|