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.
- package/CHANGELOG.md +25 -0
- package/__e2e__/admin.spec.ts +54 -20
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
- package/__e2e__/fixtures/basics/package.json +5 -5
- package/__tests__/pages/details-page/create-content-metadata.spec.ts +23 -3
- package/package.json +10 -10
- package/src/admin/components/form/DynamicArray.tsx +82 -30
- package/src/admin/components/form/Input.tsx +17 -3
- package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +2 -2
- package/src/admin/components/form/MarkdownEditor.tsx +12 -4
- package/src/admin/components/form/Select.tsx +25 -13
- package/src/admin/components/form/SubmitButton.tsx +3 -3
- package/src/admin/components/form/atoms/Button.tsx +27 -0
- package/src/admin/components/form/atoms/ErrorMessage.tsx +1 -1
- package/src/admin/components/form/atoms/FileUpload.tsx +190 -0
- package/src/admin/components/form/atoms/Hint.tsx +3 -3
- package/src/admin/components/form/atoms/Label.tsx +18 -7
- package/src/admin/components/form/hooks/use-field-error.tsx +23 -2
- package/src/admin/components/form/utils/get-border-class.ts +22 -0
- package/src/admin/i18n/admin-i18n.ts +21 -0
- package/src/admin/i18n/translations/en.yml +24 -2
- package/src/admin/pages/AdminRoute.astro +1 -3
- package/src/admin/pages/media/EditForm.tsx +35 -11
- package/src/admin/pages/media/EditRoute.astro +33 -17
- package/src/admin/pages/media/fields/Authors.tsx +15 -5
- package/src/admin/pages/media/fields/Categories.tsx +17 -5
- package/src/admin/pages/media/fields/Collections.tsx +21 -11
- package/src/admin/pages/media/fields/Content.tsx +150 -0
- package/src/admin/pages/media/fields/Image.tsx +119 -0
- package/src/admin/pages/media/file-system.ts +6 -2
- package/src/admin/pages/media/media-item-store.ts +46 -3
- package/src/admin/types/media-item.ts +29 -0
- package/src/astro-integration/config.ts +10 -0
- package/src/astro-integration/integration.ts +7 -3
- package/src/components/SearchInput.astro +3 -3
- package/src/content/get-media-items.ts +2 -1
- package/src/i18n/react/i18n-context.ts +16 -5
- package/src/i18n/react/prepare-i18n-config.ts +1 -1
- package/src/i18n/react/{useI18n.ts → use-i18n.ts} +1 -1
- package/src/i18n/translations/TRANSLATION-STATUS.md +4 -0
- package/src/i18n/translations/ur.yml +25 -0
- package/src/i18n/translations.ts +1 -0
- package/src/layouts/Page.astro +5 -6
- package/src/layouts/components/LanguagePicker.astro +11 -5
- package/src/layouts/components/Menu.astro +76 -10
- package/src/pages/404Route.astro +3 -1
- package/src/pages/details-page/components/main-details/EditButton.astro +1 -1
- package/src/pages/details-page/utils/create-content-metadata.ts +2 -1
- package/src/pages/search-page/components/LoadingSkeleton.tsx +21 -14
- package/src/pages/search-page/components/SearchFilter.tsx +2 -2
- package/src/pages/search-page/components/SearchList.tsx +33 -29
- package/src/pages/search-page/components/SearchListItem.tsx +1 -1
- package/src/pages/search-page/components/Select.tsx +15 -13
- 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
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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/
|
|
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-
|
|
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-
|
|
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
|
+
}
|
|
@@ -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/
|
|
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-
|
|
16
|
-
{label && <span className="
|
|
15
|
+
<div className="flex h-10 w-full items-start justify-end p-2">
|
|
16
|
+
{label && <span className="text-xs">{t(label)}</span>}
|
|
17
17
|
</div>
|
|
18
18
|
)
|
|
19
19
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useI18n } from "../../../../i18n/react/
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</span>
|
|
30
|
+
<div
|
|
31
|
+
className={`inline-flex rounded-t-xl px-4 font-bold ${getColor()} py-2 transition-colors duration-150 group-focus-within:!bg-sky-700 group-focus-within:text-slate-50 group-focus-within:ring-1 group-focus-within:ring-sky-700 ${size === "medium" ? "text-sm" : "text-xs"} ${className}`}
|
|
32
|
+
>
|
|
33
|
+
{t(label)}
|
|
34
|
+
{!required && <span className="ms-1">({t("ln.admin.optional")})</span>}
|
|
24
35
|
</div>
|
|
25
36
|
)
|
|
26
37
|
}
|
|
@@ -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
|
|
12
|
+
const { errors } = useFormState({ control, name, exact })
|
|
11
13
|
const error = get(errors, name) as { message: string } | undefined
|
|
12
|
-
|
|
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:
|
|
22
|
-
ln.admin.common-id-hint:
|
|
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-
|
|
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
|
|
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
|
-
<
|
|
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
|
)
|