lightnet 3.11.0 → 3.12.1
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 +24 -0
- package/__e2e__/admin.spec.ts +1 -365
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +4 -4
- package/package.json +8 -15
- package/src/astro-integration/config.ts +1 -22
- package/src/astro-integration/integration.ts +4 -39
- package/src/components/VideoPlayer.astro +74 -10
- 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 +8 -4
- 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/AudioPanel.astro +1 -12
- package/src/pages/details-page/components/AudioPlayer.astro +40 -8
- 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 -102
- 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,150 +0,0 @@
|
|
|
1
|
-
import { type Control, useWatch } from "react-hook-form"
|
|
2
|
-
|
|
3
|
-
import Icon from "../../../../components/Icon"
|
|
4
|
-
import { useI18n } from "../../../../i18n/react/use-i18n"
|
|
5
|
-
import { createContentMetadata } from "../../../../pages/details-page/utils/create-content-metadata"
|
|
6
|
-
import Button from "../../../components/form/atoms/Button"
|
|
7
|
-
import FileUpload from "../../../components/form/atoms/FileUpload"
|
|
8
|
-
import DynamicArray from "../../../components/form/DynamicArray"
|
|
9
|
-
import Input from "../../../components/form/Input"
|
|
10
|
-
import type { MediaItem } from "../../../types/media-item"
|
|
11
|
-
|
|
12
|
-
export default function Content({
|
|
13
|
-
control,
|
|
14
|
-
defaultValue,
|
|
15
|
-
}: {
|
|
16
|
-
control: Control<MediaItem>
|
|
17
|
-
defaultValue: MediaItem["content"]
|
|
18
|
-
}) {
|
|
19
|
-
const { t } = useI18n()
|
|
20
|
-
return (
|
|
21
|
-
<DynamicArray
|
|
22
|
-
control={control}
|
|
23
|
-
name="content"
|
|
24
|
-
required
|
|
25
|
-
label="ln.admin.content"
|
|
26
|
-
renderElement={(index) => (
|
|
27
|
-
<div className="flex w-full flex-col gap-6">
|
|
28
|
-
<URLInput
|
|
29
|
-
control={control}
|
|
30
|
-
defaultValue={defaultValue[index]?.url}
|
|
31
|
-
index={index}
|
|
32
|
-
/>
|
|
33
|
-
<LabelInput
|
|
34
|
-
control={control}
|
|
35
|
-
defaultValue={defaultValue[index]?.label}
|
|
36
|
-
index={index}
|
|
37
|
-
/>
|
|
38
|
-
</div>
|
|
39
|
-
)}
|
|
40
|
-
renderElementMeta={(index) => {
|
|
41
|
-
if (index !== 0) {
|
|
42
|
-
return <span></span>
|
|
43
|
-
}
|
|
44
|
-
return (
|
|
45
|
-
<span className="ms-1 flex items-center gap-1 rounded-lg bg-sky-700 px-2 py-1 text-xs uppercase text-slate-100">
|
|
46
|
-
<Icon className="text-sm mdi--star" ariaLabel="" />
|
|
47
|
-
{t("ln.admin.primary-content")}
|
|
48
|
-
</span>
|
|
49
|
-
)
|
|
50
|
-
}}
|
|
51
|
-
renderAddButton={({ addElement, index }) => (
|
|
52
|
-
<div className="flex w-2/3 flex-col items-center gap-2 pb-2 pt-4">
|
|
53
|
-
<Button
|
|
54
|
-
variant="secondary"
|
|
55
|
-
onClick={() =>
|
|
56
|
-
addElement({ url: "" }, { focusName: `content.${index}.url` })
|
|
57
|
-
}
|
|
58
|
-
>
|
|
59
|
-
<Icon className="mdi--link-variant" ariaLabel="" />
|
|
60
|
-
{t("ln.admin.add-link")}
|
|
61
|
-
</Button>
|
|
62
|
-
<span className="text-sm font-bold uppercase text-slate-500">
|
|
63
|
-
{t("ln.admin.or")}
|
|
64
|
-
</span>
|
|
65
|
-
<FileUpload
|
|
66
|
-
title="ln.admin.files-upload-title"
|
|
67
|
-
description="ln.admin.files-upload-description"
|
|
68
|
-
icon="mdi--file-upload-outline"
|
|
69
|
-
multiple
|
|
70
|
-
onUpload={(...files: File[]) =>
|
|
71
|
-
files.forEach((file, i) => {
|
|
72
|
-
addElement(
|
|
73
|
-
{ url: `/files/${file.name}`, file },
|
|
74
|
-
{ focusName: `content.${index + i}.url` },
|
|
75
|
-
)
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
/>
|
|
79
|
-
</div>
|
|
80
|
-
)}
|
|
81
|
-
/>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function URLInput({
|
|
86
|
-
control,
|
|
87
|
-
defaultValue,
|
|
88
|
-
index,
|
|
89
|
-
}: {
|
|
90
|
-
defaultValue?: string
|
|
91
|
-
index: number
|
|
92
|
-
control: Control<MediaItem>
|
|
93
|
-
}) {
|
|
94
|
-
const { url, file } = useWatch({ control, name: `content.${index}` })
|
|
95
|
-
const { t } = useI18n()
|
|
96
|
-
const isFile = url.startsWith("/files/")
|
|
97
|
-
|
|
98
|
-
const label = () => {
|
|
99
|
-
if (!isFile) {
|
|
100
|
-
return "ln.admin.link"
|
|
101
|
-
}
|
|
102
|
-
if (!file) {
|
|
103
|
-
return "ln.admin.file"
|
|
104
|
-
}
|
|
105
|
-
return t("ln.admin.file-upload", {
|
|
106
|
-
fileSize: (file.size / 1024 / 1024).toFixed(1),
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<Input
|
|
112
|
-
control={control}
|
|
113
|
-
label={label()}
|
|
114
|
-
disabled={isFile}
|
|
115
|
-
labelSize="small"
|
|
116
|
-
required
|
|
117
|
-
preserveHintSpace={false}
|
|
118
|
-
defaultValue={defaultValue}
|
|
119
|
-
name={`content.${index}.url`}
|
|
120
|
-
/>
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function LabelInput({
|
|
125
|
-
control,
|
|
126
|
-
defaultValue,
|
|
127
|
-
index,
|
|
128
|
-
}: {
|
|
129
|
-
control: Control<MediaItem>
|
|
130
|
-
defaultValue?: string
|
|
131
|
-
index: number
|
|
132
|
-
}) {
|
|
133
|
-
const url = useWatch({ control, name: `content.${index}.url` })
|
|
134
|
-
const { label: defaultLabel } = createContentMetadata({ url })
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<Input
|
|
138
|
-
name={`content.${index}.label`}
|
|
139
|
-
control={control}
|
|
140
|
-
label="ln.admin.label"
|
|
141
|
-
labelSize="small"
|
|
142
|
-
placeholder={defaultLabel}
|
|
143
|
-
preserveHintSpace={false}
|
|
144
|
-
defaultValue={defaultValue}
|
|
145
|
-
registerOptions={{
|
|
146
|
-
setValueAs: (value) => value?.trim() || undefined,
|
|
147
|
-
}}
|
|
148
|
-
/>
|
|
149
|
-
)
|
|
150
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react"
|
|
2
|
-
import { type Control, useController } from "react-hook-form"
|
|
3
|
-
|
|
4
|
-
import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
|
|
5
|
-
import FileUpload from "../../../components/form/atoms/FileUpload"
|
|
6
|
-
import Hint from "../../../components/form/atoms/Hint"
|
|
7
|
-
import Label from "../../../components/form/atoms/Label"
|
|
8
|
-
import { useFieldDirty } from "../../../components/form/hooks/use-field-dirty"
|
|
9
|
-
import { useFieldError } from "../../../components/form/hooks/use-field-error"
|
|
10
|
-
import type { MediaItem } from "../../../types/media-item"
|
|
11
|
-
|
|
12
|
-
const acceptedFileTypes = ["image/jpeg", "image/png", "image/webp"] as const
|
|
13
|
-
|
|
14
|
-
const sanitizeImageUrl = (url?: string) => {
|
|
15
|
-
if (!url) {
|
|
16
|
-
return undefined
|
|
17
|
-
}
|
|
18
|
-
const trimmed = url.trim()
|
|
19
|
-
if (!trimmed) {
|
|
20
|
-
return undefined
|
|
21
|
-
}
|
|
22
|
-
if (
|
|
23
|
-
trimmed.startsWith("/") ||
|
|
24
|
-
trimmed.startsWith("./") ||
|
|
25
|
-
trimmed.startsWith("blob:")
|
|
26
|
-
) {
|
|
27
|
-
return trimmed
|
|
28
|
-
}
|
|
29
|
-
if (/^https?:\/\//i.test(trimmed)) {
|
|
30
|
-
return trimmed
|
|
31
|
-
}
|
|
32
|
-
if (/^data:image\/(png|jpe?g|webp);/i.test(trimmed)) {
|
|
33
|
-
return trimmed
|
|
34
|
-
}
|
|
35
|
-
return undefined
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export default function Image({
|
|
39
|
-
control,
|
|
40
|
-
defaultValue,
|
|
41
|
-
mediaId,
|
|
42
|
-
}: {
|
|
43
|
-
control: Control<MediaItem>
|
|
44
|
-
defaultValue: MediaItem["image"]
|
|
45
|
-
mediaId: string
|
|
46
|
-
}) {
|
|
47
|
-
const { field } = useController({ control, name: "image" })
|
|
48
|
-
const objectUrlRef = useRef<string | null>(null)
|
|
49
|
-
const [previewSrc, setPreviewSrc] = useState<string | undefined>(
|
|
50
|
-
defaultValue.previewSrc,
|
|
51
|
-
)
|
|
52
|
-
const isDirty = useFieldDirty({ control, name: "image" })
|
|
53
|
-
const errorMessage = useFieldError({ control, name: "image", exact: false })
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
// cleanup on component unmount
|
|
57
|
-
return () => {
|
|
58
|
-
if (objectUrlRef.current) {
|
|
59
|
-
URL.revokeObjectURL(objectUrlRef.current)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}, [])
|
|
63
|
-
|
|
64
|
-
const updateImage = (file?: File) => {
|
|
65
|
-
if (!file) {
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
if (!acceptedFileTypes.includes(file.type as any)) {
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
if (objectUrlRef.current) {
|
|
72
|
-
URL.revokeObjectURL(objectUrlRef.current)
|
|
73
|
-
}
|
|
74
|
-
const objectUrl = URL.createObjectURL(file)
|
|
75
|
-
objectUrlRef.current = objectUrl
|
|
76
|
-
|
|
77
|
-
const nameParts = file.name.split(".")
|
|
78
|
-
const extension = nameParts.pop()
|
|
79
|
-
setPreviewSrc(objectUrl)
|
|
80
|
-
field.onChange({
|
|
81
|
-
...field.value,
|
|
82
|
-
path: `./images/${mediaId}.${extension}`,
|
|
83
|
-
file,
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className="group flex w-full flex-col">
|
|
89
|
-
<label htmlFor="image">
|
|
90
|
-
<Label
|
|
91
|
-
label="ln.admin.image"
|
|
92
|
-
isDirty={isDirty}
|
|
93
|
-
required
|
|
94
|
-
isInvalid={!!errorMessage}
|
|
95
|
-
/>
|
|
96
|
-
</label>
|
|
97
|
-
<div
|
|
98
|
-
className={`flex w-full items-stretch gap-4 rounded-xl rounded-ss-none border bg-slate-50 px-4 py-3 shadow-inner outline-none transition-colors focus-within:border-sky-700 focus-within:ring-1 focus-within:ring-sky-700 ${isDirty && !errorMessage ? "border-slate-700" : "border-slate-300"} ${errorMessage ? "border-rose-800" : ""} `}
|
|
99
|
-
>
|
|
100
|
-
<div className="flex h-32 w-32 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-slate-200 p-1">
|
|
101
|
-
<img
|
|
102
|
-
src={sanitizeImageUrl(previewSrc)}
|
|
103
|
-
alt=""
|
|
104
|
-
className="h-full w-full object-contain"
|
|
105
|
-
/>
|
|
106
|
-
</div>
|
|
107
|
-
<FileUpload
|
|
108
|
-
title="ln.admin.image-upload-title"
|
|
109
|
-
description="ln.admin.image-upload-description"
|
|
110
|
-
onUpload={updateImage}
|
|
111
|
-
onBlur={field.onBlur}
|
|
112
|
-
acceptedFileTypes={acceptedFileTypes}
|
|
113
|
-
/>
|
|
114
|
-
</div>
|
|
115
|
-
<ErrorMessage message={errorMessage} />
|
|
116
|
-
<Hint preserveSpace={true} />
|
|
117
|
-
</div>
|
|
118
|
-
)
|
|
119
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
export const writeFile = (
|
|
2
|
-
path: string,
|
|
3
|
-
body: BodyInit,
|
|
4
|
-
contentType?: string,
|
|
5
|
-
) => {
|
|
6
|
-
return fetch(
|
|
7
|
-
`/api/internal/fs/write-file?path=${encodeURIComponent(path.replace(/^\//, ""))}`,
|
|
8
|
-
{
|
|
9
|
-
method: "POST",
|
|
10
|
-
headers: { "Content-Type": contentType ?? resolveContentType(path) },
|
|
11
|
-
body,
|
|
12
|
-
},
|
|
13
|
-
)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const writeJson = async (path: string, object: unknown) => {
|
|
17
|
-
return writeFile(path, JSON.stringify(sortObject(object), null, 2))
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const resolveContentType = (path: string) => {
|
|
21
|
-
const normalizedPath = path.trim().toLowerCase()
|
|
22
|
-
return normalizedPath.endsWith(".json")
|
|
23
|
-
? "application/json"
|
|
24
|
-
: "text/plain; charset=utf-8"
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const sortObject = (value: unknown): unknown => {
|
|
28
|
-
if (Array.isArray(value)) {
|
|
29
|
-
return value.map(sortObject)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (value && typeof value === "object") {
|
|
33
|
-
const entries = Object.entries(value as Record<string, unknown>)
|
|
34
|
-
.sort(([a], [b]) => (a > b ? 1 : a < b ? -1 : 0))
|
|
35
|
-
.map(([key, nestedValue]) => [key, sortObject(nestedValue)])
|
|
36
|
-
|
|
37
|
-
return Object.fromEntries(entries)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return value
|
|
41
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { type MediaItem } from "../../types/media-item"
|
|
2
|
-
import { writeFile, writeJson } from "./file-system"
|
|
3
|
-
|
|
4
|
-
export const updateMediaItem = async (id: string, item: MediaItem) => {
|
|
5
|
-
const imagePath = await saveImage(item.image)
|
|
6
|
-
await Promise.all(item.content.map(saveContentFile))
|
|
7
|
-
return writeJson(
|
|
8
|
-
`/src/content/media/${id}.json`,
|
|
9
|
-
mapToContentSchema(item, imagePath),
|
|
10
|
-
)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const ensureRelativePath = (path: string) => {
|
|
14
|
-
const trimmed = path.trim()
|
|
15
|
-
if (!trimmed) {
|
|
16
|
-
return ""
|
|
17
|
-
}
|
|
18
|
-
if (trimmed.startsWith("./")) {
|
|
19
|
-
return trimmed
|
|
20
|
-
}
|
|
21
|
-
return `./${trimmed}`
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const saveImage = async (image: MediaItem["image"]) => {
|
|
25
|
-
const relativePath = ensureRelativePath(image?.path ?? "")
|
|
26
|
-
if (!relativePath || !image?.file) {
|
|
27
|
-
return relativePath
|
|
28
|
-
}
|
|
29
|
-
await writeFile(
|
|
30
|
-
`/src/content/media/${relativePath.replace(/^\.\//, "")}`,
|
|
31
|
-
await image.file.arrayBuffer(),
|
|
32
|
-
image.file.type || "application/octet-stream",
|
|
33
|
-
)
|
|
34
|
-
return relativePath
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const saveContentFile = async ({ url, file }: MediaItem["content"][number]) => {
|
|
38
|
-
if (!file) {
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
const path = `/public/${url.replace(/^\//, "")}`
|
|
42
|
-
return writeFile(
|
|
43
|
-
path,
|
|
44
|
-
await file.arrayBuffer(),
|
|
45
|
-
file.type || "application/octet-stream",
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const mapToContentSchema = (item: MediaItem, imagePath: string) => {
|
|
50
|
-
return {
|
|
51
|
-
...item,
|
|
52
|
-
image: imagePath,
|
|
53
|
-
authors: flatten(item.authors),
|
|
54
|
-
content: item.content.map(({ file: _, ...content }) => content),
|
|
55
|
-
categories: flatten(item.categories),
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const flatten = <TValue>(valueArray: { value: TValue }[]) => {
|
|
60
|
-
return valueArray.map(({ value }) => value)
|
|
61
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { type RefinementCtx, z } from "astro/zod"
|
|
2
|
-
import config from "virtual:lightnet/config"
|
|
3
|
-
|
|
4
|
-
const NON_EMPTY_STRING = "ln.admin.errors.non-empty-string"
|
|
5
|
-
const INVALID_DATE = "ln.admin.errors.invalid-date"
|
|
6
|
-
const REQUIRED = "ln.admin.errors.required"
|
|
7
|
-
const GTE_0 = "ln.admin.errors.gte-0"
|
|
8
|
-
const INTEGER = "ln.admin.errors.integer"
|
|
9
|
-
const UNIQUE_ELEMENTS = "ln.admin.errors.unique-elements"
|
|
10
|
-
const FILE_SIZE_EXCEEDED = "ln.admin.error.file-size-exceeded"
|
|
11
|
-
const NON_EMPTY_LIST = "ln.admin.errors.non-empty-list"
|
|
12
|
-
|
|
13
|
-
const unique = <TArrayItem>(path: Extract<keyof TArrayItem, string>) => {
|
|
14
|
-
return (values: TArrayItem[], ctx: RefinementCtx) => {
|
|
15
|
-
const seenValues = new Set<unknown>()
|
|
16
|
-
values.forEach((value, index) => {
|
|
17
|
-
if (seenValues.has(value[path])) {
|
|
18
|
-
ctx.addIssue({
|
|
19
|
-
path: [index, path],
|
|
20
|
-
message: UNIQUE_ELEMENTS,
|
|
21
|
-
code: "custom",
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
seenValues.add(value[path])
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const fileShape = z
|
|
30
|
-
.instanceof(File)
|
|
31
|
-
.optional()
|
|
32
|
-
.refine(
|
|
33
|
-
(file) =>
|
|
34
|
-
!file ||
|
|
35
|
-
!!(
|
|
36
|
-
file.size <
|
|
37
|
-
(config.experimental?.admin?.maxFileSize ?? 0) * 1024 * 1024
|
|
38
|
-
),
|
|
39
|
-
{ message: FILE_SIZE_EXCEEDED },
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
export const mediaItemSchema = z.object({
|
|
43
|
-
commonId: z.string().nonempty(NON_EMPTY_STRING),
|
|
44
|
-
title: z.string().nonempty(NON_EMPTY_STRING),
|
|
45
|
-
type: z.string().nonempty(REQUIRED),
|
|
46
|
-
language: z.string().nonempty(REQUIRED),
|
|
47
|
-
authors: z
|
|
48
|
-
.object({ value: z.string().nonempty(NON_EMPTY_STRING) })
|
|
49
|
-
.array()
|
|
50
|
-
.superRefine(unique("value")),
|
|
51
|
-
categories: z
|
|
52
|
-
.object({
|
|
53
|
-
value: z.string().nonempty(REQUIRED),
|
|
54
|
-
})
|
|
55
|
-
.array()
|
|
56
|
-
.superRefine(unique("value")),
|
|
57
|
-
collections: z
|
|
58
|
-
.object({
|
|
59
|
-
collection: z.string().nonempty(REQUIRED),
|
|
60
|
-
index: z.number().int(INTEGER).gte(0, GTE_0).optional(),
|
|
61
|
-
})
|
|
62
|
-
.array()
|
|
63
|
-
.superRefine(unique("collection")),
|
|
64
|
-
dateCreated: z.string().date(INVALID_DATE),
|
|
65
|
-
description: z.string().optional(),
|
|
66
|
-
image: z.object({
|
|
67
|
-
path: z.string().nonempty(NON_EMPTY_STRING),
|
|
68
|
-
previewSrc: z.string(),
|
|
69
|
-
file: fileShape,
|
|
70
|
-
}),
|
|
71
|
-
content: z
|
|
72
|
-
.object({
|
|
73
|
-
url: z.string().nonempty(NON_EMPTY_STRING),
|
|
74
|
-
file: fileShape,
|
|
75
|
-
label: z.string().optional(),
|
|
76
|
-
})
|
|
77
|
-
.array()
|
|
78
|
-
.min(1, NON_EMPTY_LIST),
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
export type MediaItem = z.input<typeof mediaItemSchema>
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { APIRoute, GetStaticPaths } from "astro"
|
|
2
|
-
import { getCollection } from "astro:content"
|
|
3
|
-
|
|
4
|
-
import { getRawMediaItem } from "../../content/get-media-items"
|
|
5
|
-
|
|
6
|
-
export const getStaticPaths = (async () => {
|
|
7
|
-
const mediaItems = await getCollection("media")
|
|
8
|
-
return mediaItems.map(({ id: mediaId }) => ({ params: { mediaId } }))
|
|
9
|
-
}) satisfies GetStaticPaths
|
|
10
|
-
|
|
11
|
-
export const GET: APIRoute = async ({ params: { mediaId } }) => {
|
|
12
|
-
const entry = await getRawMediaItem(mediaId!)
|
|
13
|
-
return new Response(
|
|
14
|
-
JSON.stringify({ id: entry.id, content: entry.data }, null, 2),
|
|
15
|
-
)
|
|
16
|
-
}
|