lightnet 3.10.3 → 3.10.5
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 +12 -0
- package/__e2e__/admin.spec.ts +356 -100
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +5 -5
- package/package.json +9 -9
- package/src/admin/components/form/DynamicArray.tsx +74 -0
- package/src/admin/components/form/Input.tsx +36 -0
- package/src/admin/components/form/Select.tsx +22 -20
- package/src/admin/components/form/SubmitButton.tsx +20 -18
- package/src/admin/components/form/atoms/ErrorMessage.tsx +13 -0
- package/src/admin/components/form/atoms/Hint.tsx +3 -3
- package/src/admin/components/form/atoms/Label.tsx +17 -6
- package/src/admin/components/form/atoms/Legend.tsx +20 -0
- package/src/admin/components/form/hooks/use-field-error.tsx +13 -0
- package/src/admin/i18n/translations/en.yml +18 -7
- package/src/admin/pages/media/EditForm.tsx +52 -68
- package/src/admin/pages/media/EditRoute.astro +35 -11
- package/src/admin/pages/media/fields/Authors.tsx +43 -0
- package/src/admin/pages/media/fields/Categories.tsx +64 -0
- package/src/admin/pages/media/fields/Collections.tsx +103 -0
- package/src/admin/pages/media/media-item-store.ts +14 -7
- package/src/admin/types/media-item.ts +38 -2
- package/src/components/CategoriesSection.astro +2 -2
- package/src/components/HeroSection.astro +1 -1
- package/src/components/HighlightSection.astro +1 -1
- package/src/components/MediaGallerySection.astro +3 -3
- package/src/components/MediaList.astro +2 -2
- package/src/components/SearchInput.astro +1 -1
- package/src/content/get-categories.ts +18 -3
- package/src/i18n/react/i18n-context.ts +14 -12
- package/src/i18n/resolve-language.ts +1 -1
- package/src/layouts/MarkdownPage.astro +1 -1
- package/src/layouts/Page.astro +3 -2
- package/src/layouts/components/LanguagePicker.astro +1 -1
- package/src/layouts/components/Menu.astro +1 -1
- package/src/layouts/components/PageNavigation.astro +1 -1
- package/src/pages/details-page/components/main-details/OpenButton.astro +1 -1
- package/src/pages/details-page/components/more-details/Languages.astro +2 -2
- package/src/pages/search-page/components/LoadingSkeleton.tsx +1 -1
- package/src/pages/search-page/components/SearchFilter.astro +7 -7
- package/src/pages/search-page/components/SearchFilter.tsx +5 -5
- package/src/pages/search-page/components/SearchList.astro +4 -4
- package/src/pages/search-page/components/SearchListItem.tsx +5 -5
- package/src/pages/search-page/components/Select.tsx +4 -4
- package/src/pages/search-page/hooks/use-search.ts +4 -4
- package/src/admin/components/form/TextInput.tsx +0 -34
- package/src/admin/components/form/atoms/FieldErrors.tsx +0 -22
- package/src/admin/components/form/form-context.ts +0 -4
- package/src/admin/components/form/index.ts +0 -18
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Control, type FieldValues, type Path } from "react-hook-form"
|
|
2
|
+
|
|
3
|
+
import ErrorMessage from "./atoms/ErrorMessage"
|
|
4
|
+
import Hint from "./atoms/Hint"
|
|
5
|
+
import Label from "./atoms/Label"
|
|
6
|
+
import { useFieldError } from "./hooks/use-field-error"
|
|
7
|
+
|
|
8
|
+
export default function Input<TFieldValues extends FieldValues>({
|
|
9
|
+
name,
|
|
10
|
+
label,
|
|
11
|
+
hint,
|
|
12
|
+
control,
|
|
13
|
+
type = "text",
|
|
14
|
+
}: {
|
|
15
|
+
name: Path<TFieldValues>
|
|
16
|
+
label: string
|
|
17
|
+
hint?: string
|
|
18
|
+
control: Control<TFieldValues>
|
|
19
|
+
type?: "text" | "date"
|
|
20
|
+
}) {
|
|
21
|
+
const errorMessage = useFieldError({ control, name })
|
|
22
|
+
return (
|
|
23
|
+
<div key={name} className="flex w-full flex-col">
|
|
24
|
+
<Label for={name} label={label} />
|
|
25
|
+
<input
|
|
26
|
+
className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
|
|
27
|
+
type={type}
|
|
28
|
+
id={name}
|
|
29
|
+
aria-invalid={!!errorMessage}
|
|
30
|
+
{...control.register(name)}
|
|
31
|
+
/>
|
|
32
|
+
<ErrorMessage message={errorMessage} />
|
|
33
|
+
<Hint label={hint} />
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -1,39 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import type { Control, FieldValues, Path } from "react-hook-form"
|
|
2
|
+
|
|
3
|
+
import ErrorMessage from "./atoms/ErrorMessage"
|
|
3
4
|
import Hint from "./atoms/Hint"
|
|
4
5
|
import Label from "./atoms/Label"
|
|
5
|
-
import {
|
|
6
|
+
import { useFieldError } from "./hooks/use-field-error"
|
|
6
7
|
|
|
7
|
-
export default function Select({
|
|
8
|
+
export default function Select<TFieldValues extends FieldValues>({
|
|
9
|
+
name,
|
|
8
10
|
label,
|
|
11
|
+
control,
|
|
9
12
|
hint,
|
|
10
13
|
options,
|
|
11
14
|
}: {
|
|
15
|
+
name: Path<TFieldValues>
|
|
12
16
|
label: string
|
|
13
17
|
hint?: string
|
|
14
|
-
|
|
18
|
+
control: Control<TFieldValues>
|
|
19
|
+
options: { id: string; labelText?: string }[]
|
|
15
20
|
}) {
|
|
16
|
-
const
|
|
17
|
-
const { t } = useI18n()
|
|
21
|
+
const errorMessage = useFieldError({ control, name })
|
|
18
22
|
return (
|
|
19
|
-
<
|
|
20
|
-
<Label label={label} />
|
|
23
|
+
<div key={name} className="flex w-full flex-col">
|
|
24
|
+
<Label for={name} label={label} />
|
|
21
25
|
<select
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
onBlur={field.handleBlur}
|
|
27
|
-
className={`dy-select dy-select-bordered ${field.state.meta.errors.length ? "dy-select-error" : ""}"`}
|
|
26
|
+
{...control.register(name)}
|
|
27
|
+
id={name}
|
|
28
|
+
aria-invalid={!!errorMessage}
|
|
29
|
+
className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
|
|
28
30
|
>
|
|
29
|
-
{options.map(({ id,
|
|
31
|
+
{options.map(({ id, labelText }) => (
|
|
30
32
|
<option key={id} value={id}>
|
|
31
|
-
{
|
|
33
|
+
{labelText ?? id}
|
|
32
34
|
</option>
|
|
33
35
|
))}
|
|
34
36
|
</select>
|
|
35
|
-
<
|
|
36
|
-
<Hint
|
|
37
|
-
</
|
|
37
|
+
<ErrorMessage message={errorMessage} />
|
|
38
|
+
<Hint label={hint} />
|
|
39
|
+
</div>
|
|
38
40
|
)
|
|
39
41
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { useStore } from "@tanstack/react-form"
|
|
2
1
|
import { useEffect, useRef, useState } from "react"
|
|
2
|
+
import { type Control, useFormState } from "react-hook-form"
|
|
3
3
|
|
|
4
4
|
import Icon from "../../../components/Icon"
|
|
5
5
|
import { useI18n } from "../../../i18n/react/useI18n"
|
|
6
|
-
import {
|
|
6
|
+
import type { MediaItem } from "../../types/media-item"
|
|
7
7
|
|
|
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-
|
|
11
|
+
"flex min-w-52 items-center justify-center gap-2 rounded-2xl px-4 py-3 font-bold uppercase shadow-sm transition-colors easy-in-out focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900 disabled:cursor-not-allowed"
|
|
12
12
|
|
|
13
13
|
const buttonStateClasses = {
|
|
14
14
|
idle: "bg-gray-800 text-gray-100 hover:bg-gray-950 hover:text-gray-300 disabled:bg-gray-600 disabled:text-gray-200",
|
|
@@ -19,8 +19,10 @@ const buttonStateClasses = {
|
|
|
19
19
|
} as const
|
|
20
20
|
|
|
21
21
|
const buttonLabels = {
|
|
22
|
-
idle:
|
|
23
|
-
|
|
22
|
+
idle: import.meta.env.DEV
|
|
23
|
+
? "ln.admin.save-changes"
|
|
24
|
+
: "ln.admin.publish-changes",
|
|
25
|
+
success: import.meta.env.DEV ? "ln.admin.saved" : "ln.admin.published",
|
|
24
26
|
error: "ln.admin.failed",
|
|
25
27
|
} as const
|
|
26
28
|
|
|
@@ -30,20 +32,20 @@ const icons = {
|
|
|
30
32
|
error: "mdi--error-outline",
|
|
31
33
|
} as const
|
|
32
34
|
|
|
33
|
-
export default function SubmitButton(
|
|
34
|
-
|
|
35
|
+
export default function SubmitButton({
|
|
36
|
+
control,
|
|
37
|
+
className,
|
|
38
|
+
}: {
|
|
39
|
+
control: Control<MediaItem>
|
|
40
|
+
className?: string
|
|
41
|
+
}) {
|
|
35
42
|
const { t } = useI18n()
|
|
36
|
-
const {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
submissionAttempts: state.submissionAttempts,
|
|
43
|
-
}),
|
|
44
|
-
)
|
|
45
|
-
const buttonState = useButtonState(isSubmitSuccessful, submissionAttempts)
|
|
46
|
-
const buttonClass = `${baseButtonClass} ${buttonStateClasses[buttonState]}`
|
|
43
|
+
const { isSubmitting, isSubmitSuccessful, submitCount } = useFormState({
|
|
44
|
+
control,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const buttonState = useButtonState(isSubmitSuccessful, submitCount)
|
|
48
|
+
const buttonClass = `${baseButtonClass} ${buttonStateClasses[buttonState]} ${className}`
|
|
47
49
|
const label = buttonLabels[buttonState]
|
|
48
50
|
const icon = icons[buttonState]
|
|
49
51
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
2
|
+
|
|
3
|
+
export default function ErrorMessage({ message }: { message?: string }) {
|
|
4
|
+
const { t } = useI18n()
|
|
5
|
+
if (!message) {
|
|
6
|
+
return null
|
|
7
|
+
}
|
|
8
|
+
return (
|
|
9
|
+
<p className="my-2 flex flex-col gap-1 text-sm text-rose-800" role="alert">
|
|
10
|
+
{t(message)}
|
|
11
|
+
</p>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
2
2
|
|
|
3
|
-
export default function Hint({
|
|
3
|
+
export default function Hint({ label }: { label?: string }) {
|
|
4
4
|
const { t } = useI18n()
|
|
5
5
|
return (
|
|
6
|
-
<div className="flex h-
|
|
7
|
-
{
|
|
6
|
+
<div className="flex h-12 w-full items-start justify-end p-2">
|
|
7
|
+
{label && <span className="dy-label-text-alt">{t(label)}</span>}
|
|
8
8
|
</div>
|
|
9
9
|
)
|
|
10
10
|
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
2
2
|
|
|
3
|
-
export default function Label({
|
|
3
|
+
export default function Label({
|
|
4
|
+
label,
|
|
5
|
+
for: htmlFor,
|
|
6
|
+
size = "sm",
|
|
7
|
+
className,
|
|
8
|
+
}: {
|
|
9
|
+
label: string
|
|
10
|
+
for: string
|
|
11
|
+
className?: string
|
|
12
|
+
size?: "sm" | "xs"
|
|
13
|
+
}) {
|
|
4
14
|
const { t } = useI18n()
|
|
5
15
|
return (
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
<label
|
|
17
|
+
htmlFor={htmlFor}
|
|
18
|
+
className={`font-bold uppercase text-gray-500 ${size === "sm" ? "pb-2 text-sm" : "pb-1 text-xs"} ${className}`}
|
|
19
|
+
>
|
|
20
|
+
{t(label)}
|
|
21
|
+
</label>
|
|
11
22
|
)
|
|
12
23
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
2
|
+
|
|
3
|
+
export default function Legend({
|
|
4
|
+
label,
|
|
5
|
+
size = "sm",
|
|
6
|
+
className,
|
|
7
|
+
}: {
|
|
8
|
+
label: string
|
|
9
|
+
className?: string
|
|
10
|
+
size?: "sm" | "xs"
|
|
11
|
+
}) {
|
|
12
|
+
const { t } = useI18n()
|
|
13
|
+
return (
|
|
14
|
+
<legend
|
|
15
|
+
className={`pb-2 font-bold uppercase text-gray-500 ${size === "sm" ? "text-sm" : "text-xs"} ${className}`}
|
|
16
|
+
>
|
|
17
|
+
{t(label)}
|
|
18
|
+
</legend>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Control, get, useFormState } from "react-hook-form"
|
|
2
|
+
|
|
3
|
+
export function useFieldError({
|
|
4
|
+
control,
|
|
5
|
+
name,
|
|
6
|
+
}: {
|
|
7
|
+
control: Control<any>
|
|
8
|
+
name: string
|
|
9
|
+
}) {
|
|
10
|
+
const { errors } = useFormState({ control, name, exact: true })
|
|
11
|
+
const error = get(errors, name) as { message: string } | undefined
|
|
12
|
+
return error?.message
|
|
13
|
+
}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
ln.admin.edit: Edit
|
|
2
|
-
ln.admin.
|
|
2
|
+
ln.admin.publish-changes: Publish Changes
|
|
3
|
+
ln.admin.published: Published
|
|
4
|
+
ln.admin.save-changes: Save Changes
|
|
3
5
|
ln.admin.saved: Saved
|
|
4
6
|
ln.admin.failed: Failed
|
|
7
|
+
ln.admin.remove: Remove
|
|
8
|
+
ln.admin.name: Name
|
|
9
|
+
ln.admin.add-author: Add Author
|
|
10
|
+
ln.admin.add-category: Add Category
|
|
11
|
+
ln.admin.collections: Collections
|
|
12
|
+
ln.admin.add-collection: Add Collection
|
|
5
13
|
ln.admin.edit-media-item: Edit media item
|
|
14
|
+
ln.admin.position-in-collection: Position in Collection
|
|
6
15
|
ln.admin.back-to-details-page: Back to details page
|
|
7
16
|
ln.admin.title: Title
|
|
8
17
|
ln.admin.common-id: Common ID
|
|
18
|
+
ln.admin.authors: Authors
|
|
9
19
|
ln.admin.created-on: Created on
|
|
10
20
|
ln.admin.created-on-hint: When has this item been created on this library?
|
|
11
|
-
ln.admin.common-id-hint: The
|
|
12
|
-
ln.admin.
|
|
13
|
-
ln.admin.
|
|
14
|
-
ln.admin.errors.
|
|
15
|
-
ln.admin.errors.
|
|
16
|
-
ln.admin.errors.
|
|
21
|
+
ln.admin.common-id-hint: The English title, all lowercase, words separated with hyphens.
|
|
22
|
+
ln.admin.errors.non-empty-string: Please enter at least one character.
|
|
23
|
+
ln.admin.errors.invalid-date: That date doesn't look right.
|
|
24
|
+
ln.admin.errors.required: This field is required.
|
|
25
|
+
ln.admin.errors.gte-0: Use a number zero or greater.
|
|
26
|
+
ln.admin.errors.unique-elements: Please choose a different value for each entry.
|
|
27
|
+
ln.admin.errors.integer: Please enter a whole number.
|
|
@@ -1,104 +1,88 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { zodResolver } from "@hookform/resolvers/zod"
|
|
2
|
+
import { useForm } from "react-hook-form"
|
|
2
3
|
|
|
3
|
-
import { showToastById } from "../../../components/showToast"
|
|
4
|
-
import Toast from "../../../components/Toast"
|
|
5
4
|
import {
|
|
6
5
|
createI18n,
|
|
7
6
|
type I18nConfig,
|
|
8
7
|
I18nContext,
|
|
9
8
|
} from "../../../i18n/react/i18n-context"
|
|
10
|
-
import
|
|
9
|
+
import Input from "../../components/form/Input"
|
|
10
|
+
import Select from "../../components/form/Select"
|
|
11
|
+
import SubmitButton from "../../components/form/SubmitButton"
|
|
11
12
|
import { type MediaItem, mediaItemSchema } from "../../types/media-item"
|
|
13
|
+
import Authors from "./fields/Authors"
|
|
14
|
+
import Categories from "./fields/Categories"
|
|
15
|
+
import Collections from "./fields/Collections"
|
|
12
16
|
import { updateMediaItem } from "./media-item-store"
|
|
13
17
|
|
|
18
|
+
type SelectOption = { id: string; labelText: string }
|
|
19
|
+
|
|
14
20
|
export default function EditForm({
|
|
15
21
|
mediaId,
|
|
16
22
|
mediaItem,
|
|
17
23
|
i18nConfig,
|
|
18
24
|
mediaTypes,
|
|
19
25
|
languages,
|
|
26
|
+
categories,
|
|
27
|
+
collections,
|
|
20
28
|
}: {
|
|
21
29
|
mediaId: string
|
|
22
30
|
mediaItem: MediaItem
|
|
23
31
|
i18nConfig: I18nConfig
|
|
24
|
-
mediaTypes:
|
|
25
|
-
languages:
|
|
32
|
+
mediaTypes: SelectOption[]
|
|
33
|
+
languages: SelectOption[]
|
|
34
|
+
categories: SelectOption[]
|
|
35
|
+
collections: SelectOption[]
|
|
26
36
|
}) {
|
|
27
|
-
const
|
|
28
|
-
defaultValues:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
validationLogic: revalidateLogic({
|
|
33
|
-
mode: "blur",
|
|
34
|
-
modeAfterSubmission: "change",
|
|
35
|
-
}),
|
|
36
|
-
onSubmit: async ({ value }) => {
|
|
37
|
-
await updateMediaItem(mediaId, { ...mediaItem, ...value })
|
|
38
|
-
},
|
|
39
|
-
onSubmitInvalid: () => {
|
|
40
|
-
showToastById("invalid-form-data-toast")
|
|
41
|
-
},
|
|
37
|
+
const { handleSubmit, control } = useForm({
|
|
38
|
+
defaultValues: mediaItem,
|
|
39
|
+
mode: "onTouched",
|
|
40
|
+
shouldFocusError: true,
|
|
41
|
+
resolver: zodResolver(mediaItemSchema),
|
|
42
42
|
})
|
|
43
|
+
const onSubmit = handleSubmit(
|
|
44
|
+
async (data) => await updateMediaItem(mediaId, { ...mediaItem, ...data }),
|
|
45
|
+
)
|
|
43
46
|
const i18n = createI18n(i18nConfig)
|
|
44
|
-
const { t } = i18n
|
|
45
|
-
|
|
46
47
|
return (
|
|
47
48
|
<I18nContext.Provider value={i18n}>
|
|
48
|
-
<form
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
name="title"
|
|
57
|
-
children={(field) => <field.TextInput label="ln.admin.title" />}
|
|
58
|
-
/>
|
|
59
|
-
<form.AppField
|
|
49
|
+
<form className="flex flex-col" onSubmit={onSubmit}>
|
|
50
|
+
<div className="mb-8 flex items-end justify-between">
|
|
51
|
+
<h1 className="text-3xl">{i18n.t("ln.admin.edit-media-item")}</h1>
|
|
52
|
+
<SubmitButton control={control} />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<Input name="title" label="ln.admin.title" control={control} />
|
|
56
|
+
<Input
|
|
60
57
|
name="commonId"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
hint="ln.admin.common-id-hint"
|
|
65
|
-
/>
|
|
66
|
-
)}
|
|
58
|
+
label="ln.admin.common-id"
|
|
59
|
+
hint="ln.admin.common-id-hint"
|
|
60
|
+
control={control}
|
|
67
61
|
/>
|
|
68
|
-
<
|
|
62
|
+
<Select
|
|
69
63
|
name="type"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
label="ln.type"
|
|
65
|
+
options={mediaTypes}
|
|
66
|
+
control={control}
|
|
73
67
|
/>
|
|
74
|
-
<
|
|
68
|
+
<Select
|
|
75
69
|
name="language"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
label="ln.language"
|
|
71
|
+
options={languages}
|
|
72
|
+
control={control}
|
|
79
73
|
/>
|
|
80
|
-
<
|
|
74
|
+
<Authors control={control} />
|
|
75
|
+
<Input
|
|
81
76
|
name="dateCreated"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
hint="ln.admin.created-on-hint"
|
|
87
|
-
/>
|
|
88
|
-
)}
|
|
77
|
+
label="ln.admin.created-on"
|
|
78
|
+
hint="ln.admin.created-on-hint"
|
|
79
|
+
type="date"
|
|
80
|
+
control={control}
|
|
89
81
|
/>
|
|
82
|
+
<Categories categories={categories} control={control} />
|
|
83
|
+
<Collections collections={collections} control={control} />
|
|
90
84
|
|
|
91
|
-
<
|
|
92
|
-
<form.AppForm>
|
|
93
|
-
<form.SubmitButton />
|
|
94
|
-
<Toast id="invalid-form-data-toast" variant="error">
|
|
95
|
-
<div className="font-bold text-gray-700">
|
|
96
|
-
{t("ln.admin.toast.invalid-data.title")}
|
|
97
|
-
</div>
|
|
98
|
-
{t("ln.admin.toast.invalid-data.hint")}
|
|
99
|
-
</Toast>
|
|
100
|
-
</form.AppForm>
|
|
101
|
-
</div>
|
|
85
|
+
<SubmitButton className="self-end" control={control} />
|
|
102
86
|
</form>
|
|
103
87
|
</I18nContext.Provider>
|
|
104
88
|
)
|
|
@@ -3,6 +3,7 @@ import type { GetStaticPaths } from "astro"
|
|
|
3
3
|
import { getCollection } from "astro:content"
|
|
4
4
|
import config from "virtual:lightnet/config"
|
|
5
5
|
|
|
6
|
+
import { getCategories } from "../../../content/get-categories"
|
|
6
7
|
import { getRawMediaItem } from "../../../content/get-media-items"
|
|
7
8
|
import { getMediaTypes } from "../../../content/get-media-types"
|
|
8
9
|
import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
|
|
@@ -18,42 +19,65 @@ export const getStaticPaths = (async () => {
|
|
|
18
19
|
}) satisfies GetStaticPaths
|
|
19
20
|
|
|
20
21
|
const { mediaId } = Astro.params
|
|
21
|
-
const
|
|
22
|
+
const { data: mediaItem } = await getRawMediaItem(mediaId)
|
|
23
|
+
|
|
24
|
+
const formData = {
|
|
25
|
+
...mediaItem,
|
|
26
|
+
authors: mediaItem.authors?.map((value) => ({ value })) ?? [],
|
|
27
|
+
categories:
|
|
28
|
+
mediaItem.categories?.map((value) => ({
|
|
29
|
+
value,
|
|
30
|
+
})) ?? [],
|
|
31
|
+
collections: mediaItem.collections ?? [],
|
|
32
|
+
}
|
|
22
33
|
|
|
23
34
|
const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
|
|
24
35
|
"ln.admin.*",
|
|
25
36
|
"ln.type",
|
|
26
37
|
"ln.language",
|
|
38
|
+
"ln.categories",
|
|
27
39
|
])
|
|
28
|
-
const { t } = Astro.locals.i18n
|
|
40
|
+
const { t, currentLocale } = Astro.locals.i18n
|
|
29
41
|
|
|
30
42
|
const mediaTypes = (await getMediaTypes()).map(({ id, data: { label } }) => ({
|
|
31
43
|
id,
|
|
32
|
-
|
|
44
|
+
labelText: t(label),
|
|
33
45
|
}))
|
|
46
|
+
|
|
47
|
+
const categories = (await getCategories(currentLocale, t)).map(
|
|
48
|
+
({ id, labelText }) => ({ id, labelText }),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const collections = (await getCollection("media-collections")).map(
|
|
52
|
+
({ id, data: { label } }) => ({ id, labelText: t(label) }),
|
|
53
|
+
)
|
|
54
|
+
|
|
34
55
|
const languages = config.languages.map(({ code, label }) => ({
|
|
35
56
|
id: code,
|
|
36
|
-
|
|
57
|
+
labelText: t(label),
|
|
37
58
|
}))
|
|
38
59
|
---
|
|
39
60
|
|
|
40
|
-
<Page>
|
|
41
|
-
<div class="mx-auto max-w-screen-md px-4
|
|
61
|
+
<Page mainClass="bg-slate-500">
|
|
62
|
+
<div class="mx-auto block max-w-screen-md px-4 md:px-8">
|
|
42
63
|
<a
|
|
43
|
-
class="underline"
|
|
64
|
+
class="block py-4 text-gray-200 underline"
|
|
44
65
|
href=`/${Astro.currentLocale}/media/faithful-freestyle--en`
|
|
45
66
|
>{t("ln.admin.back-to-details-page")}</a
|
|
46
67
|
>
|
|
47
|
-
|
|
48
|
-
{t("ln.admin.edit-media-item")}
|
|
49
|
-
</h1>
|
|
68
|
+
</div>
|
|
50
69
|
|
|
70
|
+
<div
|
|
71
|
+
class="mx-auto max-w-screen-md rounded-2xl bg-gray-200 px-4 py-8 shadow-md md:px-8"
|
|
72
|
+
>
|
|
51
73
|
<EditForm
|
|
52
74
|
mediaId={mediaId}
|
|
53
|
-
mediaItem={
|
|
75
|
+
mediaItem={formData}
|
|
54
76
|
i18nConfig={i18nConfig}
|
|
55
77
|
mediaTypes={mediaTypes}
|
|
56
78
|
languages={languages}
|
|
79
|
+
categories={categories}
|
|
80
|
+
collections={collections}
|
|
57
81
|
client:load
|
|
58
82
|
/>
|
|
59
83
|
</div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Control } from "react-hook-form"
|
|
2
|
+
|
|
3
|
+
import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
|
|
4
|
+
import DynamicArray from "../../../components/form/DynamicArray"
|
|
5
|
+
import { useFieldError } from "../../../components/form/hooks/use-field-error"
|
|
6
|
+
import type { MediaItem } from "../../../types/media-item"
|
|
7
|
+
|
|
8
|
+
export default function Authors({ control }: { control: Control<MediaItem> }) {
|
|
9
|
+
return (
|
|
10
|
+
<DynamicArray
|
|
11
|
+
control={control}
|
|
12
|
+
name="authors"
|
|
13
|
+
label="ln.admin.authors"
|
|
14
|
+
renderElement={(index) => <AuthorInput index={index} control={control} />}
|
|
15
|
+
addButton={{
|
|
16
|
+
label: "ln.admin.add-author",
|
|
17
|
+
onClick: (append, index) =>
|
|
18
|
+
append({ value: "" }, { focusName: `authors.${index}.value` }),
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Control } from "react-hook-form"
|
|
2
|
+
|
|
3
|
+
import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
|
|
4
|
+
import DynamicArray from "../../../components/form/DynamicArray"
|
|
5
|
+
import { useFieldError } from "../../../components/form/hooks/use-field-error"
|
|
6
|
+
import type { MediaItem } from "../../../types/media-item"
|
|
7
|
+
|
|
8
|
+
export default function Categories({
|
|
9
|
+
control,
|
|
10
|
+
categories,
|
|
11
|
+
}: {
|
|
12
|
+
control: Control<MediaItem>
|
|
13
|
+
categories: { id: string; labelText: string }[]
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<DynamicArray
|
|
17
|
+
control={control}
|
|
18
|
+
name="categories"
|
|
19
|
+
label="ln.categories"
|
|
20
|
+
renderElement={(index) => (
|
|
21
|
+
<CategorySelect
|
|
22
|
+
categories={categories}
|
|
23
|
+
control={control}
|
|
24
|
+
index={index}
|
|
25
|
+
/>
|
|
26
|
+
)}
|
|
27
|
+
addButton={{
|
|
28
|
+
label: "ln.admin.add-category",
|
|
29
|
+
onClick: (append, index) =>
|
|
30
|
+
append({ value: "" }, { focusName: `categories.${index}.value` }),
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
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
|
+
}
|