lightnet 3.12.0 → 3.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/__e2e__/admin.spec.ts +1 -365
  3. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  4. package/__e2e__/fixtures/basics/package.json +3 -3
  5. package/package.json +8 -15
  6. package/src/astro-integration/config.ts +1 -22
  7. package/src/astro-integration/integration.ts +4 -39
  8. package/src/content/content-schema.ts +0 -7
  9. package/src/content/get-media-items.ts +1 -47
  10. package/src/i18n/translate.ts +1 -1
  11. package/src/i18n/translations/TRANSLATION-STATUS.md +37 -12
  12. package/src/i18n/translations/en.yml +5 -0
  13. package/src/i18n/translations.ts +0 -7
  14. package/src/layouts/Page.astro +2 -0
  15. package/src/layouts/global.css +3 -0
  16. package/src/pages/details-page/components/main-details/EditButton.astro +2 -4
  17. package/src/pages/search-page/components/SearchFilter.tsx +1 -1
  18. package/src/admin/api/fs/write-file.ts +0 -53
  19. package/src/admin/components/form/DynamicArray.tsx +0 -129
  20. package/src/admin/components/form/Input.tsx +0 -68
  21. package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +0 -109
  22. package/src/admin/components/form/MarkdownEditor.tsx +0 -62
  23. package/src/admin/components/form/Select.tsx +0 -71
  24. package/src/admin/components/form/SubmitButton.tsx +0 -86
  25. package/src/admin/components/form/atoms/Button.tsx +0 -27
  26. package/src/admin/components/form/atoms/ErrorMessage.tsx +0 -13
  27. package/src/admin/components/form/atoms/FileUpload.tsx +0 -190
  28. package/src/admin/components/form/atoms/Hint.tsx +0 -19
  29. package/src/admin/components/form/atoms/Label.tsx +0 -37
  30. package/src/admin/components/form/hooks/use-field-dirty.tsx +0 -12
  31. package/src/admin/components/form/hooks/use-field-error.tsx +0 -34
  32. package/src/admin/components/form/utils/get-border-class.ts +0 -22
  33. package/src/admin/i18n/admin-i18n.ts +0 -21
  34. package/src/admin/i18n/translations/en.yml +0 -50
  35. package/src/admin/i18n/translations.ts +0 -5
  36. package/src/admin/pages/AdminRoute.astro +0 -14
  37. package/src/admin/pages/media/EditForm.tsx +0 -136
  38. package/src/admin/pages/media/EditRoute.astro +0 -100
  39. package/src/admin/pages/media/fields/Authors.tsx +0 -44
  40. package/src/admin/pages/media/fields/Categories.tsx +0 -49
  41. package/src/admin/pages/media/fields/Collections.tsx +0 -68
  42. package/src/admin/pages/media/fields/Content.tsx +0 -150
  43. package/src/admin/pages/media/fields/Image.tsx +0 -119
  44. package/src/admin/pages/media/file-system.ts +0 -41
  45. package/src/admin/pages/media/media-item-store.ts +0 -61
  46. package/src/admin/types/media-item.ts +0 -81
  47. package/src/api/media/[mediaId].ts +0 -16
@@ -1,86 +0,0 @@
1
- import { useEffect, useRef, useState } from "react"
2
- import { type Control, useFormState } from "react-hook-form"
3
-
4
- import Icon from "../../../components/Icon"
5
- import { useI18n } from "../../../i18n/react/use-i18n"
6
- import type { MediaItem } from "../../types/media-item"
7
-
8
- const SUCCESS_DURATION_MS = 2000
9
-
10
- const baseButtonClass =
11
- "flex min-w-52 items-center justify-center gap-2 rounded-xl px-4 py-3 font-bold shadow-sm transition-colors easy-in-out focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-sky-700 focus-visible:ring-offset-1 disabled:cursor-not-allowed"
12
-
13
- const buttonStateClasses = {
14
- idle: "bg-slate-800 text-slate-50 hover:bg-slate-950 hover:text-slate-300 disabled:bg-slate-500 disabled:text-slate-300",
15
- error:
16
- "bg-rose-700 text-white hover:bg-rose-800 hover:text-white disabled:bg-rose-600",
17
- success:
18
- "bg-emerald-600 text-white hover:bg-emerald-700 hover:text-white disabled:bg-emerald-500",
19
- } as const
20
-
21
- const buttonLabels = {
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",
26
- error: "ln.admin.failed",
27
- } as const
28
-
29
- const icons = {
30
- idle: undefined,
31
- success: "mdi--check",
32
- error: "mdi--error-outline",
33
- } as const
34
-
35
- export default function SubmitButton({
36
- control,
37
- className,
38
- }: {
39
- control: Control<MediaItem>
40
- className?: string
41
- }) {
42
- const { t } = useI18n()
43
- const { isSubmitting, isSubmitSuccessful, submitCount, isDirty } =
44
- useFormState({
45
- control,
46
- })
47
-
48
- const buttonState = useButtonState(isSubmitSuccessful, submitCount)
49
- const buttonClass = `${baseButtonClass} ${buttonStateClasses[buttonState]} ${className}`
50
- const label = buttonLabels[buttonState]
51
- const icon = icons[buttonState]
52
-
53
- return (
54
- <button
55
- className={buttonClass}
56
- type="submit"
57
- disabled={isSubmitting || !isDirty}
58
- >
59
- {icon && <Icon className={icon} ariaLabel="" />}
60
- {t(label)}
61
- </button>
62
- )
63
- }
64
-
65
- function useButtonState(
66
- isSubmitSuccessful: boolean,
67
- submissionAttempts: number,
68
- ) {
69
- const [state, setState] = useState<"success" | "error" | "idle">("idle")
70
- const timeoutRef = useRef<number | undefined>(undefined)
71
- useEffect(() => {
72
- if (submissionAttempts === 0) {
73
- return
74
- }
75
- setState(isSubmitSuccessful ? "success" : "error")
76
- if (timeoutRef.current !== undefined) {
77
- window.clearTimeout(timeoutRef.current)
78
- }
79
- timeoutRef.current = window.setTimeout(() => {
80
- setState("idle")
81
- timeoutRef.current = undefined
82
- }, SUCCESS_DURATION_MS)
83
- }, [submissionAttempts, isSubmitSuccessful])
84
-
85
- return state
86
- }
@@ -1,27 +0,0 @@
1
- import type { ButtonHTMLAttributes, ReactNode } from "react"
2
-
3
- type Props = {
4
- variant: "secondary"
5
- children?: ReactNode
6
- } & ButtonHTMLAttributes<HTMLButtonElement>
7
-
8
- export default function Button({
9
- children,
10
- className,
11
- variant,
12
- ...buttonProps
13
- }: Props) {
14
- const styles = {
15
- secondary: "text-slate-800 bg-slate-50 hover:bg-sky-50",
16
- } as const
17
-
18
- return (
19
- <button
20
- className={`flex items-center gap-1 rounded-xl px-10 py-4 text-sm font-bold shadow-sm transition-colors ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-700 ${styles[variant]} ${className}`}
21
- type="button"
22
- {...buttonProps}
23
- >
24
- {children}
25
- </button>
26
- )
27
- }
@@ -1,13 +0,0 @@
1
- import { useI18n } from "../../../../i18n/react/use-i18n"
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,190 +0,0 @@
1
- import {
2
- type ChangeEvent,
3
- type DragEvent,
4
- useEffect,
5
- useRef,
6
- useState,
7
- } from "react"
8
- import config from "virtual:lightnet/config"
9
-
10
- import Icon from "../../../../components/Icon"
11
- import { useI18n } from "../../../../i18n/react/use-i18n"
12
-
13
- type FileType = "image/png" | "image/jpeg" | "image/webp"
14
-
15
- export default function FileUpload({
16
- onUpload,
17
- onBlur,
18
- acceptedFileTypes,
19
- title,
20
- icon,
21
- multiple,
22
- description,
23
- }: {
24
- onUpload: (...file: File[]) => void
25
- onBlur?: () => void
26
- acceptedFileTypes?: Readonly<FileType[]>
27
- title: string
28
- icon?: string
29
- multiple?: boolean
30
- description: string
31
- }) {
32
- const fileInputRef = useRef<HTMLInputElement | null>(null)
33
- const invalidFeedbackTimeout = useRef<number | null>(null)
34
- const { t } = useI18n()
35
-
36
- const [isDragging, setIsDragging] = useState(false)
37
- const [invalidFeedbackMessage, setInvalidFeedbackMessage] = useState<
38
- string | null
39
- >(null)
40
-
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"))
86
- return
87
- }
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
- )
97
- return
98
- }
99
- onUpload(...files)
100
- setInvalidFeedbackMessage(null)
101
- }
102
-
103
- const onDragEnter = (event: DragEvent<HTMLDivElement>) => {
104
- event.preventDefault()
105
- setIsDragging(true)
106
- }
107
-
108
- const onDrop = (event: DragEvent<HTMLDivElement>) => {
109
- event.preventDefault()
110
- setIsDragging(false)
111
- onFilesSelected([...event.dataTransfer.files])
112
- }
113
-
114
- const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
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)
124
- // allow selecting the same file twice in a row
125
- event.target.value = ""
126
- }
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
-
138
- return (
139
- <>
140
- <div
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`}
142
- role="button"
143
- tabIndex={0}
144
- onBlur={onBlur}
145
- onClick={() => fileInputRef.current?.click()}
146
- onKeyDown={(event) => {
147
- if (event.key === "Enter" || event.key === " ") {
148
- event.preventDefault()
149
- fileInputRef.current?.click()
150
- }
151
- }}
152
- onDragOver={onDragEnter}
153
- onDragLeave={() => setIsDragging(false)}
154
- onDrop={onDrop}
155
- >
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)}
159
- </span>
160
- <span className="max-w-xs text-balance text-center text-xs text-slate-600">
161
- {t(description, {
162
- maxFileSize: config.experimental?.admin?.maxFileSize,
163
- })}
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
- )}
176
- </div>
177
- <input
178
- tabIndex={-1}
179
- ref={(ref) => {
180
- fileInputRef.current = ref
181
- }}
182
- type="file"
183
- multiple={multiple}
184
- accept={acceptedFileTypes?.join(",")}
185
- className="hidden"
186
- onChange={onInputChange}
187
- />
188
- </>
189
- )
190
- }
@@ -1,19 +0,0 @@
1
- import { useI18n } from "../../../../i18n/react/use-i18n"
2
-
3
- export default function Hint({
4
- label,
5
- preserveSpace,
6
- }: {
7
- label?: string
8
- preserveSpace: boolean
9
- }) {
10
- const { t } = useI18n()
11
- if (!preserveSpace && !label) {
12
- return null
13
- }
14
- return (
15
- <div className="flex h-10 w-full items-start justify-end p-2">
16
- {label && <span className="text-xs">{t(label)}</span>}
17
- </div>
18
- )
19
- }
@@ -1,37 +0,0 @@
1
- import { useI18n } from "../../../../i18n/react/use-i18n"
2
-
3
- export default function Label({
4
- label,
5
- size = "medium",
6
- className = "",
7
- isDirty,
8
- isInvalid,
9
- required,
10
- }: {
11
- label: string
12
- className?: string
13
- size?: "small" | "medium"
14
- isDirty?: boolean
15
- isInvalid?: boolean
16
- required: boolean
17
- }) {
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
- }
29
- return (
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>}
35
- </div>
36
- )
37
- }
@@ -1,12 +0,0 @@
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,34 +0,0 @@
1
- import { type Control, get, useFormState } from "react-hook-form"
2
-
3
- export function useFieldError({
4
- control,
5
- name,
6
- exact = true,
7
- }: {
8
- control: Control<any>
9
- name: string
10
- exact?: boolean
11
- }) {
12
- const { errors } = useFormState({ control, name, exact })
13
- const error = get(errors, name) as { message: string } | undefined
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
34
- }
@@ -1,22 +0,0 @@
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
- }
@@ -1,21 +0,0 @@
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,50 +0,0 @@
1
- ln.admin.edit: Edit
2
- ln.admin.publish-changes: Publish changes
3
- ln.admin.published: Published
4
- ln.admin.save-changes: Save changes
5
- ln.admin.saved: Saved
6
- ln.admin.failed: Failed
7
- ln.admin.remove: Remove
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.
21
- ln.admin.add-author: Add author
22
- ln.admin.add-category: Add category
23
- ln.admin.collections: Collections
24
- ln.admin.add-collection: Add collection
25
- ln.admin.edit-media-item: Edit Media Item
26
- ln.admin.position-in-collection: Position in Collection
27
- ln.admin.back-to-details-page: Back to details page
28
- ln.admin.title: Title
29
- ln.admin.common-id: Common ID
30
- ln.admin.image: Image
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.
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.
36
- ln.admin.authors: Authors
37
- ln.admin.optional: optional
38
- ln.admin.author-name: Author name
39
- ln.admin.description: Description
40
- ln.admin.date-created: Date Created
41
- ln.admin.date-created-hint: The date this item was added to this media library.
42
- ln.admin.common-id-hint: Use a shared Common ID to link translated versions of a media item.
43
- ln.admin.errors.non-empty-string: Please enter at least one character.
44
- ln.admin.errors.invalid-date: That date doesn't look right.
45
- ln.admin.error.file-size-exceeded: This file is too big.
46
- ln.admin.errors.required: This field is required.
47
- ln.admin.errors.gte-0: Use a number zero or greater.
48
- ln.admin.errors.unique-elements: Please choose a different value for each entry.
49
- ln.admin.errors.integer: Please enter a whole number.
50
- ln.admin.errors.non-empty-list: List must contain at least 1 element.
@@ -1,5 +0,0 @@
1
- export const builtInAdminTranslations = {
2
- en: () => import("./translations/en.yml?raw"),
3
- } as const
4
-
5
- export type AdminTranslationKey = "ln.admin.edit"
@@ -1,14 +0,0 @@
1
- ---
2
- import Page from "../../layouts/Page.astro"
3
- ---
4
-
5
- <Page>
6
- <div
7
- class="flex h-96 w-full items-center justify-center text-lg font-bold text-slate-500"
8
- >
9
- Admin features are enabled now.
10
- </div>
11
- </Page>
12
- <script>
13
- localStorage.setItem("ln-admin-enabled", "true")
14
- </script>
@@ -1,136 +0,0 @@
1
- import { zodResolver } from "@hookform/resolvers/zod"
2
- import type { KeyboardEvent } from "react"
3
- import { useForm } from "react-hook-form"
4
-
5
- import {
6
- createI18n,
7
- type I18nConfig,
8
- I18nContext,
9
- } from "../../../i18n/react/i18n-context"
10
- import Input from "../../components/form/Input"
11
- import MarkdownEditor from "../../components/form/MarkdownEditor"
12
- import Select from "../../components/form/Select"
13
- import SubmitButton from "../../components/form/SubmitButton"
14
- import { type MediaItem, mediaItemSchema } from "../../types/media-item"
15
- import Authors from "./fields/Authors"
16
- import Categories from "./fields/Categories"
17
- import Collections from "./fields/Collections"
18
- import Content from "./fields/Content"
19
- import Image from "./fields/Image"
20
- import { updateMediaItem } from "./media-item-store"
21
-
22
- type SelectOption = { id: string; labelText: string }
23
-
24
- export default function EditForm({
25
- mediaId,
26
- mediaItem,
27
- i18nConfig,
28
- mediaTypes,
29
- languages,
30
- categories,
31
- collections,
32
- }: {
33
- mediaId: string
34
- mediaItem: MediaItem
35
- i18nConfig: I18nConfig
36
- mediaTypes: SelectOption[]
37
- languages: SelectOption[]
38
- categories: SelectOption[]
39
- collections: SelectOption[]
40
- }) {
41
- const { handleSubmit, control } = useForm({
42
- defaultValues: mediaItem,
43
- mode: "onTouched",
44
- shouldFocusError: true,
45
- resolver: zodResolver(mediaItemSchema),
46
- })
47
-
48
- const preventSubmitOnEnter = (event: KeyboardEvent<HTMLFormElement>) => {
49
- if (event.key === "Enter" && !(event.target instanceof HTMLButtonElement)) {
50
- event.preventDefault()
51
- }
52
- }
53
-
54
- const onSubmit = handleSubmit(
55
- async (data) => await updateMediaItem(mediaId, { ...mediaItem, ...data }),
56
- )
57
- const i18n = createI18n(i18nConfig)
58
- return (
59
- <I18nContext.Provider value={i18n}>
60
- <form
61
- className="flex flex-col"
62
- onSubmit={onSubmit}
63
- onKeyDown={preventSubmitOnEnter}
64
- >
65
- <div className="mb-8 flex items-end justify-between">
66
- <h1 className="text-3xl">{i18n.t("ln.admin.edit-media-item")}</h1>
67
- <SubmitButton control={control} />
68
- </div>
69
-
70
- <Input
71
- name="title"
72
- label="ln.admin.title"
73
- required
74
- control={control}
75
- defaultValue={mediaItem.title}
76
- />
77
- <Select
78
- name="type"
79
- label="ln.type"
80
- required
81
- options={mediaTypes}
82
- control={control}
83
- defaultValue={mediaItem.type}
84
- />
85
- <Select
86
- name="language"
87
- label="ln.language"
88
- required
89
- defaultValue={mediaItem.language}
90
- options={languages}
91
- control={control}
92
- />
93
- <Image
94
- control={control}
95
- defaultValue={mediaItem.image}
96
- mediaId={mediaId}
97
- />
98
- <Content control={control} defaultValue={mediaItem.content} />
99
- <Input
100
- name="dateCreated"
101
- label="ln.admin.date-created"
102
- hint="ln.admin.date-created-hint"
103
- type="date"
104
- required
105
- defaultValue={mediaItem.dateCreated}
106
- control={control}
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} />
117
- <Categories
118
- categories={categories}
119
- control={control}
120
- defaultValue={mediaItem.categories}
121
- />
122
- <Collections
123
- collections={collections}
124
- control={control}
125
- defaultValue={mediaItem.collections}
126
- />
127
- <MarkdownEditor
128
- control={control}
129
- name="description"
130
- label="ln.admin.description"
131
- />
132
- <SubmitButton className="mt-8 self-end" control={control} />
133
- </form>
134
- </I18nContext.Provider>
135
- )
136
- }