lightnet 3.10.7 → 3.11.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/__e2e__/admin.spec.ts +54 -20
  3. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  4. package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
  5. package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
  6. package/__e2e__/fixtures/basics/package.json +5 -5
  7. package/__tests__/pages/details-page/create-content-metadata.spec.ts +23 -3
  8. package/package.json +10 -10
  9. package/src/admin/components/form/DynamicArray.tsx +82 -30
  10. package/src/admin/components/form/Input.tsx +17 -3
  11. package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +2 -2
  12. package/src/admin/components/form/MarkdownEditor.tsx +12 -4
  13. package/src/admin/components/form/Select.tsx +25 -13
  14. package/src/admin/components/form/SubmitButton.tsx +3 -3
  15. package/src/admin/components/form/atoms/Button.tsx +27 -0
  16. package/src/admin/components/form/atoms/ErrorMessage.tsx +1 -1
  17. package/src/admin/components/form/atoms/FileUpload.tsx +190 -0
  18. package/src/admin/components/form/atoms/Hint.tsx +3 -3
  19. package/src/admin/components/form/atoms/Label.tsx +18 -7
  20. package/src/admin/components/form/hooks/use-field-error.tsx +23 -2
  21. package/src/admin/components/form/utils/get-border-class.ts +22 -0
  22. package/src/admin/i18n/admin-i18n.ts +21 -0
  23. package/src/admin/i18n/translations/en.yml +24 -2
  24. package/src/admin/pages/AdminRoute.astro +1 -3
  25. package/src/admin/pages/media/EditForm.tsx +35 -11
  26. package/src/admin/pages/media/EditRoute.astro +33 -17
  27. package/src/admin/pages/media/fields/Authors.tsx +15 -5
  28. package/src/admin/pages/media/fields/Categories.tsx +17 -5
  29. package/src/admin/pages/media/fields/Collections.tsx +21 -11
  30. package/src/admin/pages/media/fields/Content.tsx +150 -0
  31. package/src/admin/pages/media/fields/Image.tsx +119 -0
  32. package/src/admin/pages/media/file-system.ts +6 -2
  33. package/src/admin/pages/media/media-item-store.ts +46 -3
  34. package/src/admin/types/media-item.ts +29 -0
  35. package/src/astro-integration/config.ts +10 -0
  36. package/src/astro-integration/integration.ts +7 -3
  37. package/src/components/SearchInput.astro +3 -3
  38. package/src/content/get-media-items.ts +2 -1
  39. package/src/i18n/react/i18n-context.ts +16 -5
  40. package/src/i18n/react/prepare-i18n-config.ts +1 -1
  41. package/src/i18n/react/{useI18n.ts → use-i18n.ts} +1 -1
  42. package/src/i18n/translations/TRANSLATION-STATUS.md +4 -0
  43. package/src/i18n/translations/ur.yml +25 -0
  44. package/src/i18n/translations.ts +1 -0
  45. package/src/layouts/Page.astro +5 -6
  46. package/src/layouts/components/LanguagePicker.astro +11 -5
  47. package/src/layouts/components/Menu.astro +76 -10
  48. package/src/pages/404Route.astro +3 -1
  49. package/src/pages/details-page/components/main-details/EditButton.astro +1 -1
  50. package/src/pages/details-page/utils/create-content-metadata.ts +2 -1
  51. package/src/pages/search-page/components/LoadingSkeleton.tsx +21 -14
  52. package/src/pages/search-page/components/SearchFilter.tsx +2 -2
  53. package/src/pages/search-page/components/SearchList.tsx +33 -29
  54. package/src/pages/search-page/components/SearchListItem.tsx +1 -1
  55. package/src/pages/search-page/components/Select.tsx +15 -13
  56. package/src/layouts/components/PreloadReact.tsx +0 -3
@@ -1,15 +1,18 @@
1
1
  import { type Control, type FieldValues, type Path } from "react-hook-form"
2
2
 
3
+ import Icon from "../../../components/Icon"
3
4
  import ErrorMessage from "./atoms/ErrorMessage"
4
5
  import Hint from "./atoms/Hint"
5
6
  import Label from "./atoms/Label"
6
7
  import { useFieldDirty } from "./hooks/use-field-dirty"
7
8
  import { useFieldError } from "./hooks/use-field-error"
9
+ import { getBorderClass } from "./utils/get-border-class"
8
10
 
9
11
  export default function Select<TFieldValues extends FieldValues>({
10
12
  name,
11
13
  label,
12
14
  labelSize,
15
+ required = false,
13
16
  control,
14
17
  defaultValue,
15
18
  hint,
@@ -20,6 +23,7 @@ export default function Select<TFieldValues extends FieldValues>({
20
23
  label?: string
21
24
  labelSize?: "small" | "medium"
22
25
  hint?: string
26
+ required?: boolean
23
27
  preserveHintSpace?: boolean
24
28
  defaultValue?: string
25
29
  control: Control<TFieldValues>
@@ -35,23 +39,31 @@ export default function Select<TFieldValues extends FieldValues>({
35
39
  label={label}
36
40
  size={labelSize}
37
41
  isDirty={isDirty}
42
+ required={required}
38
43
  isInvalid={!!errorMessage}
39
44
  />
40
45
  </label>
41
46
  )}
42
- <select
43
- {...control.register(name)}
44
- id={name}
45
- aria-invalid={!!errorMessage}
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" : ""}`}
48
- >
49
- {options.map(({ id, labelText }) => (
50
- <option key={id} value={id}>
51
- {labelText ?? id}
52
- </option>
53
- ))}
54
- </select>
47
+ <div className="relative">
48
+ <select
49
+ {...control.register(name)}
50
+ id={name}
51
+ aria-invalid={!!errorMessage}
52
+ aria-required={required}
53
+ defaultValue={defaultValue}
54
+ className={`w-full appearance-none rounded-xl ${getBorderClass({ isDirty, errorMessage })} bg-white px-4 py-3 pe-12 shadow-sm ${label ? "rounded-ss-none" : ""}`}
55
+ >
56
+ {options.map(({ id, labelText }) => (
57
+ <option key={id} value={id}>
58
+ {labelText ?? id}
59
+ </option>
60
+ ))}
61
+ </select>
62
+ <Icon
63
+ className="absolute end-3 top-1/2 -translate-y-1/2 text-lg text-slate-600 mdi--chevron-down"
64
+ ariaLabel=""
65
+ />
66
+ </div>
55
67
  <ErrorMessage message={errorMessage} />
56
68
  <Hint preserveSpace={preserveHintSpace} label={hint} />
57
69
  </div>
@@ -2,16 +2,16 @@ import { useEffect, useRef, useState } from "react"
2
2
  import { type Control, useFormState } from "react-hook-form"
3
3
 
4
4
  import Icon from "../../../components/Icon"
5
- import { useI18n } from "../../../i18n/react/useI18n"
5
+ import { useI18n } from "../../../i18n/react/use-i18n"
6
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-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"
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
12
 
13
13
  const buttonStateClasses = {
14
- idle: "bg-gray-800 text-gray-50 hover:bg-gray-950 hover:text-gray-300 disabled:bg-gray-500 disabled:text-gray-300",
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
15
  error:
16
16
  "bg-rose-700 text-white hover:bg-rose-800 hover:text-white disabled:bg-rose-600",
17
17
  success:
@@ -0,0 +1,27 @@
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,4 +1,4 @@
1
- import { useI18n } from "../../../../i18n/react/useI18n"
1
+ import { useI18n } from "../../../../i18n/react/use-i18n"
2
2
 
3
3
  export default function ErrorMessage({ message }: { message?: string }) {
4
4
  const { t } = useI18n()
@@ -0,0 +1,190 @@
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,4 +1,4 @@
1
- import { useI18n } from "../../../../i18n/react/useI18n"
1
+ import { useI18n } from "../../../../i18n/react/use-i18n"
2
2
 
3
3
  export default function Hint({
4
4
  label,
@@ -12,8 +12,8 @@ export default function Hint({
12
12
  return null
13
13
  }
14
14
  return (
15
- <div className="flex h-8 w-full items-start justify-end p-2">
16
- {label && <span className="dy-label-text-alt">{t(label)}</span>}
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
  }
@@ -1,4 +1,4 @@
1
- import { useI18n } from "../../../../i18n/react/useI18n"
1
+ import { useI18n } from "../../../../i18n/react/use-i18n"
2
2
 
3
3
  export default function Label({
4
4
  label,
@@ -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-400 text-slate-950"
26
+ }
27
+ return "bg-slate-300 text-slate-800"
28
+ }
17
29
  return (
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>
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
  }
@@ -3,11 +3,32 @@ import { type Control, get, useFormState } from "react-hook-form"
3
3
  export function useFieldError({
4
4
  control,
5
5
  name,
6
+ exact = true,
6
7
  }: {
7
8
  control: Control<any>
8
9
  name: string
10
+ exact?: boolean
9
11
  }) {
10
- const { errors } = useFormState({ control, name, exact: true })
12
+ const { errors } = useFormState({ control, name, exact })
11
13
  const error = get(errors, name) as { message: string } | undefined
12
- return error?.message
14
+ if (exact) {
15
+ return error?.message
16
+ } else {
17
+ return findErrorMessage(get(errors, name))
18
+ }
19
+ }
20
+
21
+ function findErrorMessage(errors?: unknown) {
22
+ if (!errors) {
23
+ return undefined
24
+ }
25
+ for (const [key, value] of Object.entries(errors)) {
26
+ if (key === "message" && typeof value === "string") {
27
+ return value
28
+ }
29
+ if (value && typeof value === "object" && !Array.isArray(value)) {
30
+ return findErrorMessage(value)
31
+ }
32
+ }
33
+ return undefined
13
34
  }
@@ -0,0 +1,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-400 " + focusColors(focusWithin)
20
+ }
21
+ return "border border-slate-300 " + focusColors(focusWithin)
22
+ }
@@ -0,0 +1,21 @@
1
+ import config from "virtual:lightnet/config"
2
+
3
+ import { resolveDefaultLocale } from "../../i18n/resolve-default-locale"
4
+ import { resolveLanguage } from "../../i18n/resolve-language"
5
+ import { resolveLocales } from "../../i18n/resolve-locales"
6
+ import { translationKeys, useTranslate } from "../../i18n/translate"
7
+
8
+ const currentLocale = config.experimental?.admin?.languageCode ?? "en"
9
+ const t = useTranslate(currentLocale)
10
+ const { direction } = resolveLanguage(currentLocale)
11
+ const defaultLocale = resolveDefaultLocale(config)
12
+ const locales = resolveLocales(config)
13
+
14
+ export const adminI18n = {
15
+ currentLocale,
16
+ t,
17
+ direction,
18
+ translationKeys,
19
+ defaultLocale,
20
+ locales,
21
+ }
@@ -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
@@ -15,14 +27,24 @@ ln.admin.position-in-collection: Position in Collection
15
27
  ln.admin.back-to-details-page: Back to details page
16
28
  ln.admin.title: Title
17
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.
18
36
  ln.admin.authors: Authors
37
+ ln.admin.optional: optional
38
+ ln.admin.author-name: Author name
19
39
  ln.admin.description: Description
20
40
  ln.admin.date-created: Date Created
21
- ln.admin.date-created-hint: When has this item been created on this library?
22
- ln.admin.common-id-hint: The English title, all lowercase, words separated with hyphens.
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.
23
43
  ln.admin.errors.non-empty-string: Please enter at least one character.
24
44
  ln.admin.errors.invalid-date: That date doesn't look right.
45
+ ln.admin.error.file-size-exceeded: This file is too big.
25
46
  ln.admin.errors.required: This field is required.
26
47
  ln.admin.errors.gte-0: Use a number zero or greater.
27
48
  ln.admin.errors.unique-elements: Please choose a different value for each entry.
28
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,12 +1,10 @@
1
1
  ---
2
2
  import Page from "../../layouts/Page.astro"
3
-
4
- export { getLocalePaths as getStaticPaths } from "../../i18n/get-locale-paths"
5
3
  ---
6
4
 
7
5
  <Page>
8
6
  <div
9
- class="flex h-96 w-full items-center justify-center text-lg font-bold text-gray-500"
7
+ class="flex h-96 w-full items-center justify-center text-lg font-bold text-slate-500"
10
8
  >
11
9
  Admin features are enabled now.
12
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,8 @@ 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"
19
+ import Image from "./fields/Image"
17
20
  import { updateMediaItem } from "./media-item-store"
18
21
 
19
22
  type SelectOption = { id: string; labelText: string }
@@ -41,13 +44,24 @@ export default function EditForm({
41
44
  shouldFocusError: true,
42
45
  resolver: zodResolver(mediaItemSchema),
43
46
  })
47
+
48
+ const preventSubmitOnEnter = (event: KeyboardEvent<HTMLFormElement>) => {
49
+ if (event.key === "Enter" && !(event.target instanceof HTMLButtonElement)) {
50
+ event.preventDefault()
51
+ }
52
+ }
53
+
44
54
  const onSubmit = handleSubmit(
45
55
  async (data) => await updateMediaItem(mediaId, { ...mediaItem, ...data }),
46
56
  )
47
57
  const i18n = createI18n(i18nConfig)
48
58
  return (
49
59
  <I18nContext.Provider value={i18n}>
50
- <form className="flex flex-col" onSubmit={onSubmit}>
60
+ <form
61
+ className="flex flex-col"
62
+ onSubmit={onSubmit}
63
+ onKeyDown={preventSubmitOnEnter}
64
+ >
51
65
  <div className="mb-8 flex items-end justify-between">
52
66
  <h1 className="text-3xl">{i18n.t("ln.admin.edit-media-item")}</h1>
53
67
  <SubmitButton control={control} />
@@ -56,19 +70,14 @@ export default function EditForm({
56
70
  <Input
57
71
  name="title"
58
72
  label="ln.admin.title"
73
+ required
59
74
  control={control}
60
75
  defaultValue={mediaItem.title}
61
76
  />
62
- <Input
63
- name="commonId"
64
- label="ln.admin.common-id"
65
- hint="ln.admin.common-id-hint"
66
- control={control}
67
- defaultValue={mediaItem.commonId}
68
- />
69
77
  <Select
70
78
  name="type"
71
79
  label="ln.type"
80
+ required
72
81
  options={mediaTypes}
73
82
  control={control}
74
83
  defaultValue={mediaItem.type}
@@ -76,19 +85,35 @@ export default function EditForm({
76
85
  <Select
77
86
  name="language"
78
87
  label="ln.language"
88
+ required
79
89
  defaultValue={mediaItem.language}
80
90
  options={languages}
81
91
  control={control}
82
92
  />
83
- <Authors control={control} defaultValue={mediaItem.authors} />
93
+ <Image
94
+ control={control}
95
+ defaultValue={mediaItem.image}
96
+ mediaId={mediaId}
97
+ />
98
+ <Content control={control} defaultValue={mediaItem.content} />
84
99
  <Input
85
100
  name="dateCreated"
86
101
  label="ln.admin.date-created"
87
102
  hint="ln.admin.date-created-hint"
88
103
  type="date"
104
+ required
89
105
  defaultValue={mediaItem.dateCreated}
90
106
  control={control}
91
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} />
92
117
  <Categories
93
118
  categories={categories}
94
119
  control={control}
@@ -104,8 +129,7 @@ export default function EditForm({
104
129
  name="description"
105
130
  label="ln.admin.description"
106
131
  />
107
-
108
- <SubmitButton className="self-end" control={control} />
132
+ <SubmitButton className="mt-8 self-end" control={control} />
109
133
  </form>
110
134
  </I18nContext.Provider>
111
135
  )