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.
- package/CHANGELOG.md +22 -0
- package/__e2e__/admin.spec.ts +1 -365
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +3 -3
- package/package.json +8 -15
- package/src/astro-integration/config.ts +1 -22
- package/src/astro-integration/integration.ts +4 -39
- package/src/content/content-schema.ts +0 -7
- package/src/content/get-media-items.ts +1 -47
- package/src/i18n/translate.ts +1 -1
- package/src/i18n/translations/TRANSLATION-STATUS.md +37 -12
- package/src/i18n/translations/en.yml +5 -0
- package/src/i18n/translations.ts +0 -7
- package/src/layouts/Page.astro +2 -0
- package/src/layouts/global.css +3 -0
- package/src/pages/details-page/components/main-details/EditButton.astro +2 -4
- package/src/pages/search-page/components/SearchFilter.tsx +1 -1
- package/src/admin/api/fs/write-file.ts +0 -53
- package/src/admin/components/form/DynamicArray.tsx +0 -129
- package/src/admin/components/form/Input.tsx +0 -68
- package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +0 -109
- package/src/admin/components/form/MarkdownEditor.tsx +0 -62
- package/src/admin/components/form/Select.tsx +0 -71
- package/src/admin/components/form/SubmitButton.tsx +0 -86
- package/src/admin/components/form/atoms/Button.tsx +0 -27
- package/src/admin/components/form/atoms/ErrorMessage.tsx +0 -13
- package/src/admin/components/form/atoms/FileUpload.tsx +0 -190
- package/src/admin/components/form/atoms/Hint.tsx +0 -19
- package/src/admin/components/form/atoms/Label.tsx +0 -37
- package/src/admin/components/form/hooks/use-field-dirty.tsx +0 -12
- package/src/admin/components/form/hooks/use-field-error.tsx +0 -34
- package/src/admin/components/form/utils/get-border-class.ts +0 -22
- package/src/admin/i18n/admin-i18n.ts +0 -21
- package/src/admin/i18n/translations/en.yml +0 -50
- package/src/admin/i18n/translations.ts +0 -5
- package/src/admin/pages/AdminRoute.astro +0 -14
- package/src/admin/pages/media/EditForm.tsx +0 -136
- package/src/admin/pages/media/EditRoute.astro +0 -100
- package/src/admin/pages/media/fields/Authors.tsx +0 -44
- package/src/admin/pages/media/fields/Categories.tsx +0 -49
- package/src/admin/pages/media/fields/Collections.tsx +0 -68
- package/src/admin/pages/media/fields/Content.tsx +0 -150
- package/src/admin/pages/media/fields/Image.tsx +0 -119
- package/src/admin/pages/media/file-system.ts +0 -41
- package/src/admin/pages/media/media-item-store.ts +0 -61
- package/src/admin/types/media-item.ts +0 -81
- 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,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
|
-
}
|