lightnet 3.9.1 → 3.10.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/README.md +4 -0
- package/__e2e__/admin.spec.ts +113 -0
- package/__e2e__/fixtures/basics/astro.config.mjs +6 -0
- 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/node_modules/.bin/tsc +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +2 -2
- package/__e2e__/fixtures/basics/package.json +9 -9
- package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +15 -0
- package/__e2e__/fixtures/basics/src/content/media-types/audio.json +7 -0
- package/__e2e__/fixtures/basics/src/translations/de.yml +1 -0
- package/__e2e__/fixtures/basics/src/translations/en.yml +1 -0
- package/__e2e__/homepage.spec.ts +21 -0
- package/package.json +18 -11
- package/src/admin/api/fs/writeText.ts +50 -0
- package/src/admin/components/form/FieldErrors.tsx +19 -0
- package/src/admin/components/form/SubmitButton.tsx +77 -0
- package/src/admin/components/form/TextField.tsx +24 -0
- package/src/admin/components/form/form-context.ts +4 -0
- package/src/admin/components/form/index.ts +16 -0
- package/src/admin/i18n/translations/en.yml +1 -0
- package/src/admin/i18n/translations.ts +5 -0
- package/src/admin/pages/AdminRoute.astro +16 -0
- package/src/admin/pages/media/EditForm.tsx +58 -0
- package/src/admin/pages/media/EditRoute.astro +33 -0
- package/src/admin/pages/media/file-system.ts +37 -0
- package/src/admin/pages/media/media-item-store.ts +11 -0
- package/src/admin/types/media-item.ts +8 -0
- package/src/api/media/[mediaId].ts +16 -0
- package/src/{pages/api → api}/versions.ts +1 -1
- package/src/astro-integration/config.ts +19 -0
- package/src/astro-integration/integration.ts +44 -6
- package/src/components/CategoriesSection.astro +1 -1
- package/src/components/MediaGallerySection.astro +1 -1
- package/src/components/Toast.tsx +55 -0
- package/src/components/showToast.ts +61 -0
- package/src/content/astro-image.ts +1 -14
- package/src/content/content-schema.ts +10 -3
- package/src/content/get-media-items.ts +46 -1
- package/src/i18n/translations/ar.yml +1 -0
- package/src/i18n/translations/bn.yml +1 -0
- package/src/i18n/translations/de.yml +1 -0
- package/src/i18n/translations/en.yml +3 -0
- package/src/i18n/translations/es.yml +1 -0
- package/src/i18n/translations/fi.yml +1 -0
- package/src/i18n/translations/fr.yml +1 -0
- package/src/i18n/translations/hi.yml +1 -0
- package/src/i18n/translations/kk.yml +1 -0
- package/src/i18n/translations/pt.yml +1 -0
- package/src/i18n/translations/ru.yml +1 -0
- package/src/i18n/translations/uk.yml +1 -0
- package/src/i18n/translations/zh.yml +1 -0
- package/src/i18n/translations.ts +21 -7
- package/src/layouts/Page.astro +3 -2
- package/src/layouts/components/Footer.astro +24 -0
- package/src/layouts/components/LightNetLogo.svg +1 -0
- package/src/pages/details-page/components/MainDetailsSection.astro +5 -1
- package/src/pages/details-page/components/VideoDetailsSection.astro +5 -1
- package/src/pages/details-page/components/main-details/EditButton.astro +30 -0
- package/src/pages/details-page/components/main-details/ShareButton.astro +9 -13
- package/src/pages/{api → search-page/api}/search.ts +3 -3
- package/src/pages/search-page/components/SearchListItem.tsx +1 -1
- package/src/pages/search-page/hooks/use-search.ts +3 -3
- package/tailwind.config.ts +1 -0
- /package/src/pages/{api → search-page/api}/search-response.ts +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { revalidateLogic } from "@tanstack/react-form"
|
|
2
|
+
|
|
3
|
+
import { showToastById } from "../../../components/showToast"
|
|
4
|
+
import Toast from "../../../components/Toast"
|
|
5
|
+
import { useAppForm } from "../../components/form"
|
|
6
|
+
import { type MediaItem, mediaItemSchema } from "../../types/media-item"
|
|
7
|
+
import { updateMediaItem } from "./media-item-store"
|
|
8
|
+
|
|
9
|
+
export default function EditForm({
|
|
10
|
+
mediaId,
|
|
11
|
+
mediaItem,
|
|
12
|
+
}: {
|
|
13
|
+
mediaId: string
|
|
14
|
+
mediaItem: MediaItem
|
|
15
|
+
}) {
|
|
16
|
+
const form = useAppForm({
|
|
17
|
+
defaultValues: mediaItem,
|
|
18
|
+
validators: {
|
|
19
|
+
onDynamic: mediaItemSchema,
|
|
20
|
+
},
|
|
21
|
+
validationLogic: revalidateLogic({
|
|
22
|
+
mode: "blur",
|
|
23
|
+
modeAfterSubmission: "change",
|
|
24
|
+
}),
|
|
25
|
+
onSubmit: async ({ value }) => {
|
|
26
|
+
await updateMediaItem(mediaId, { ...mediaItem, ...value })
|
|
27
|
+
},
|
|
28
|
+
onSubmitInvalid: () => {
|
|
29
|
+
showToastById("invalid-form-data-toast")
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<form
|
|
35
|
+
onSubmit={(e) => {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
form.handleSubmit()
|
|
38
|
+
}}
|
|
39
|
+
className="flex flex-col items-start gap-4"
|
|
40
|
+
>
|
|
41
|
+
<form.AppField
|
|
42
|
+
name="commonId"
|
|
43
|
+
children={(field) => <field.TextField label="Common ID" />}
|
|
44
|
+
/>
|
|
45
|
+
<form.AppField
|
|
46
|
+
name="title"
|
|
47
|
+
children={(field) => <field.TextField label="Title" />}
|
|
48
|
+
/>
|
|
49
|
+
<form.AppForm>
|
|
50
|
+
<form.SubmitButton />
|
|
51
|
+
<Toast id="invalid-form-data-toast" variant="error">
|
|
52
|
+
<div className="font-bold text-gray-700">Invalid form data</div>
|
|
53
|
+
Check the fields and try again.
|
|
54
|
+
</Toast>
|
|
55
|
+
</form.AppForm>
|
|
56
|
+
</form>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { GetStaticPaths } from "astro"
|
|
3
|
+
import { getCollection } from "astro:content"
|
|
4
|
+
import config from "virtual:lightnet/config"
|
|
5
|
+
|
|
6
|
+
import { getRawMediaItem } from "../../../content/get-media-items"
|
|
7
|
+
import { resolveLocales } from "../../../i18n/resolve-locales"
|
|
8
|
+
import Page from "../../../layouts/Page.astro"
|
|
9
|
+
import EditForm from "./EditForm"
|
|
10
|
+
|
|
11
|
+
export const getStaticPaths = (async () => {
|
|
12
|
+
const mediaItems = await getCollection("media")
|
|
13
|
+
return resolveLocales(config).flatMap((locale) =>
|
|
14
|
+
mediaItems.map(({ id: mediaId }) => ({ params: { mediaId, locale } })),
|
|
15
|
+
)
|
|
16
|
+
}) satisfies GetStaticPaths
|
|
17
|
+
|
|
18
|
+
const { mediaId } = Astro.params
|
|
19
|
+
const mediaItemEntry = await getRawMediaItem(mediaId)
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<Page>
|
|
23
|
+
<div class="mx-auto max-w-screen-lg px-4 pt-12 md:px-8">
|
|
24
|
+
<a
|
|
25
|
+
class="underline"
|
|
26
|
+
href=`/${Astro.currentLocale}/media/faithful-freestyle--en`
|
|
27
|
+
>Back to details page</a
|
|
28
|
+
>
|
|
29
|
+
<h1 class="mb-4 mt-8 text-lg">Edit media item</h1>
|
|
30
|
+
|
|
31
|
+
<EditForm mediaId={mediaId} mediaItem={mediaItemEntry.data} client:load />
|
|
32
|
+
</div>
|
|
33
|
+
</Page>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const writeText = (path: string, body: string) => {
|
|
2
|
+
return fetch(
|
|
3
|
+
`/api/internal/fs/writeText?path=${encodeURIComponent(path.replace(/^\//, ""))}`,
|
|
4
|
+
{
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: { "Content-Type": resolveContentType(path) },
|
|
7
|
+
body,
|
|
8
|
+
},
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const writeJson = async (path: string, object: unknown) => {
|
|
13
|
+
return writeText(path, JSON.stringify(sortObject(object), null, 2))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const resolveContentType = (path: string) => {
|
|
17
|
+
const normalizedPath = path.trim().toLowerCase()
|
|
18
|
+
return normalizedPath.endsWith(".json")
|
|
19
|
+
? "application/json"
|
|
20
|
+
: "text/plain; charset=utf-8"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sortObject = (value: unknown): unknown => {
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
return value.map(sortObject)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (value && typeof value === "object") {
|
|
29
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
30
|
+
.sort(([a], [b]) => (a > b ? 1 : a < b ? -1 : 0))
|
|
31
|
+
.map(([key, nestedValue]) => [key, sortObject(nestedValue)])
|
|
32
|
+
|
|
33
|
+
return Object.fromEntries(entries)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type MediaItem, mediaItemSchema } from "../../types/media-item"
|
|
2
|
+
import { writeJson } from "./file-system"
|
|
3
|
+
|
|
4
|
+
export const loadMediaItem = (id: string) =>
|
|
5
|
+
fetch(`/api/media/${id}.json`)
|
|
6
|
+
.then((response) => response.json())
|
|
7
|
+
.then((json) => mediaItemSchema.parse(json.content))
|
|
8
|
+
|
|
9
|
+
export const updateMediaItem = async (id: string, item: MediaItem) => {
|
|
10
|
+
return writeJson(`/src/content/media/${id}.json`, item)
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { APIRoute } from "astro"
|
|
2
2
|
|
|
3
|
-
import pkg from "
|
|
3
|
+
import pkg from "../../package.json" assert { type: "json" }
|
|
4
4
|
|
|
5
5
|
export const GET: APIRoute = () => {
|
|
6
6
|
return new Response(JSON.stringify({ lightnet: pkg.version }))
|
|
@@ -132,6 +132,10 @@ export const configSchema = z.object({
|
|
|
132
132
|
* Favicons for your site.
|
|
133
133
|
*/
|
|
134
134
|
favicon: faviconSchema.array().optional(),
|
|
135
|
+
/**
|
|
136
|
+
* Enable displaying a “Powered by LightNet” link in your site’s footer.
|
|
137
|
+
*/
|
|
138
|
+
credits: z.boolean().default(false),
|
|
135
139
|
/**
|
|
136
140
|
* Link to manifest file within public/ folder
|
|
137
141
|
*/
|
|
@@ -213,6 +217,21 @@ export const configSchema = z.object({
|
|
|
213
217
|
hideHeaderSearchIcon: z.boolean().default(false),
|
|
214
218
|
})
|
|
215
219
|
.optional(),
|
|
220
|
+
/**
|
|
221
|
+
* Experimental features. Subject to change with any release.
|
|
222
|
+
*/
|
|
223
|
+
experimental: z
|
|
224
|
+
.object({
|
|
225
|
+
/**
|
|
226
|
+
* Configure administration interface.
|
|
227
|
+
*/
|
|
228
|
+
admin: z
|
|
229
|
+
.object({
|
|
230
|
+
enabled: z.boolean().default(false),
|
|
231
|
+
})
|
|
232
|
+
.optional(),
|
|
233
|
+
})
|
|
234
|
+
.optional(),
|
|
216
235
|
})
|
|
217
236
|
|
|
218
237
|
export type Language = z.input<typeof languageSchema>
|
|
@@ -19,6 +19,7 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
|
19
19
|
updateConfig,
|
|
20
20
|
logger,
|
|
21
21
|
addMiddleware,
|
|
22
|
+
command,
|
|
22
23
|
}) => {
|
|
23
24
|
const config = verifySchema(
|
|
24
25
|
configSchema,
|
|
@@ -46,23 +47,60 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
|
46
47
|
})
|
|
47
48
|
|
|
48
49
|
injectRoute({
|
|
49
|
-
pattern: "/
|
|
50
|
-
entrypoint: "lightnet/pages/
|
|
50
|
+
pattern: "/[locale]/media/[mediaId]",
|
|
51
|
+
entrypoint: "lightnet/pages/DetailsPageRoute.astro",
|
|
51
52
|
prerender: true,
|
|
52
53
|
})
|
|
53
54
|
|
|
54
55
|
injectRoute({
|
|
55
|
-
pattern: "/api/
|
|
56
|
-
entrypoint: "lightnet/
|
|
56
|
+
pattern: "/api/internal/search.json",
|
|
57
|
+
entrypoint: "lightnet/api/internal/search.ts",
|
|
57
58
|
prerender: true,
|
|
58
59
|
})
|
|
59
60
|
|
|
60
61
|
injectRoute({
|
|
61
|
-
pattern: "/
|
|
62
|
-
entrypoint: "lightnet/
|
|
62
|
+
pattern: "/api/versions.json",
|
|
63
|
+
entrypoint: "lightnet/api/versions.ts",
|
|
63
64
|
prerender: true,
|
|
64
65
|
})
|
|
65
66
|
|
|
67
|
+
if (config.experimental?.admin?.enabled) {
|
|
68
|
+
injectRoute({
|
|
69
|
+
pattern: "/api/media/[mediaId].json",
|
|
70
|
+
entrypoint: "lightnet/api/media/[mediaId].ts",
|
|
71
|
+
prerender: true,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
injectRoute({
|
|
75
|
+
pattern: "/[locale]/admin",
|
|
76
|
+
entrypoint: "lightnet/admin/pages/AdminRoute.astro",
|
|
77
|
+
prerender: true,
|
|
78
|
+
})
|
|
79
|
+
injectRoute({
|
|
80
|
+
pattern: "/[locale]/admin/media/[mediaId]",
|
|
81
|
+
entrypoint: "lightnet/admin/pages/media/EditRoute.astro",
|
|
82
|
+
prerender: true,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// During local development admin ui can use
|
|
87
|
+
// this endpoints to write files.
|
|
88
|
+
if (config.experimental?.admin?.enabled && command === "dev") {
|
|
89
|
+
injectRoute({
|
|
90
|
+
pattern: "/api/internal/fs/writeText",
|
|
91
|
+
entrypoint: "lightnet/api/internal/fs/writeText.ts",
|
|
92
|
+
prerender: false,
|
|
93
|
+
})
|
|
94
|
+
// Add empty adapter to avoid warning
|
|
95
|
+
// about missing adapter.
|
|
96
|
+
// This hack might break in the future :(
|
|
97
|
+
// We could also set the "node" adapter if no
|
|
98
|
+
// adapter has been set by user.
|
|
99
|
+
if (!astroConfig.adapter) {
|
|
100
|
+
updateConfig({ adapter: {} })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
66
104
|
addMiddleware({ entrypoint: "lightnet/locals", order: "pre" })
|
|
67
105
|
|
|
68
106
|
astroConfig.integrations.push(tailwind(), react())
|
|
@@ -7,7 +7,7 @@ import { searchPagePath } from "../utils/paths"
|
|
|
7
7
|
import CarouselSection from "./CarouselSection.astro"
|
|
8
8
|
import Section, { type Props as SectionProps } from "./Section.astro"
|
|
9
9
|
|
|
10
|
-
type Props = SectionProps & {
|
|
10
|
+
type Props = Omit<SectionProps, "maxWidth"> & {
|
|
11
11
|
layout?: "grid" | "carousel"
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
export type ToastVariant = "info" | "success" | "warning" | "error"
|
|
4
|
+
|
|
5
|
+
export type ToastProps = {
|
|
6
|
+
id?: string
|
|
7
|
+
children: ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
variant?: ToastVariant
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const variantClassName: Record<ToastVariant, string> = {
|
|
13
|
+
info: "border-slate-400 bg-white/95",
|
|
14
|
+
success: "border-emerald-500 bg-emerald-100",
|
|
15
|
+
warning: "border-amber-500 bg-amber-100",
|
|
16
|
+
error: "border-rose-500 bg-rose-100",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function Toast({
|
|
20
|
+
id,
|
|
21
|
+
children,
|
|
22
|
+
className = "",
|
|
23
|
+
variant = "info",
|
|
24
|
+
}: ToastProps) {
|
|
25
|
+
const alertClasses = variantClassName[variant] ?? variantClassName.info
|
|
26
|
+
const ariaLive = variant === "error" ? "assertive" : "polite"
|
|
27
|
+
const hiddenTransform = "translateY(1.5rem)"
|
|
28
|
+
const overshootTransform = "translateY(-0.25rem)"
|
|
29
|
+
const visibleTransform = "translateY(0)"
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
id={id}
|
|
34
|
+
className={`pointer-events-none fixed bottom-4 end-0 flex justify-end px-4 opacity-0 transition duration-300 will-change-transform ${className}`}
|
|
35
|
+
data-toast="true"
|
|
36
|
+
data-variant={variant}
|
|
37
|
+
data-toast-hidden-transform={hiddenTransform}
|
|
38
|
+
data-toast-overshoot-transform={overshootTransform}
|
|
39
|
+
data-toast-visible-transform={visibleTransform}
|
|
40
|
+
role="status"
|
|
41
|
+
aria-live={ariaLive}
|
|
42
|
+
style={{
|
|
43
|
+
transform: hiddenTransform,
|
|
44
|
+
transitionProperty: "opacity, transform",
|
|
45
|
+
transitionTimingFunction: "cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<div
|
|
49
|
+
className={`pointer-events-auto flex max-w-sm flex-col items-start gap-1 rounded-2xl border p-4 text-base shadow-md backdrop-blur-sm ${alertClasses}`}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const DEFAULT_DURATION_MS = 3000
|
|
2
|
+
const TIMEOUT_DATA_KEY = "toastHideTimeoutId"
|
|
3
|
+
const DEFAULT_HIDDEN_TRANSFORM = "translateY(1.5rem)"
|
|
4
|
+
const DEFAULT_VISIBLE_TRANSFORM = "translateY(0)"
|
|
5
|
+
const DEFAULT_OVERSHOOT_TRANSFORM = "translateY(-0.25rem)"
|
|
6
|
+
|
|
7
|
+
type ShowToastOptions = {
|
|
8
|
+
duration?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Shows a toast element by toggling its opacity and schedules it to hide again.
|
|
13
|
+
* Works with markup rendered by the Toast component but can target any element.
|
|
14
|
+
*/
|
|
15
|
+
export function showToast(
|
|
16
|
+
element: HTMLElement,
|
|
17
|
+
options: ShowToastOptions = {},
|
|
18
|
+
) {
|
|
19
|
+
const duration = options.duration ?? DEFAULT_DURATION_MS
|
|
20
|
+
const existingTimeoutId = element.dataset[TIMEOUT_DATA_KEY]
|
|
21
|
+
const hiddenTransform =
|
|
22
|
+
element.dataset.toastHiddenTransform ?? DEFAULT_HIDDEN_TRANSFORM
|
|
23
|
+
const overshootTransform =
|
|
24
|
+
element.dataset.toastOvershootTransform ?? DEFAULT_OVERSHOOT_TRANSFORM
|
|
25
|
+
const visibleTransform =
|
|
26
|
+
element.dataset.toastVisibleTransform ?? DEFAULT_VISIBLE_TRANSFORM
|
|
27
|
+
|
|
28
|
+
if (existingTimeoutId) {
|
|
29
|
+
window.clearTimeout(Number(existingTimeoutId))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
element.style.opacity = "100%"
|
|
33
|
+
element.style.transform = overshootTransform
|
|
34
|
+
element.dataset.toastVisible = "true"
|
|
35
|
+
|
|
36
|
+
const settleIntoPlace = () => {
|
|
37
|
+
element.style.transform = visibleTransform
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
window.requestAnimationFrame(() => {
|
|
41
|
+
window.requestAnimationFrame(settleIntoPlace)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const timeoutId = window.setTimeout(() => {
|
|
45
|
+
element.style.opacity = "0%"
|
|
46
|
+
element.style.transform = hiddenTransform
|
|
47
|
+
element.dataset.toastVisible = "false"
|
|
48
|
+
delete element.dataset[TIMEOUT_DATA_KEY]
|
|
49
|
+
}, duration)
|
|
50
|
+
|
|
51
|
+
element.dataset[TIMEOUT_DATA_KEY] = String(timeoutId)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function showToastById(id: string, options?: ShowToastOptions) {
|
|
55
|
+
const element = document.getElementById(id)
|
|
56
|
+
if (!element) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
showToast(element as HTMLElement, options)
|
|
61
|
+
}
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* We use this function to make sure decap's relative paths will resolve correctly
|
|
5
|
-
* with astro content.
|
|
6
|
-
*
|
|
7
|
-
* @param image astro content image function
|
|
8
|
-
* @returns image property
|
|
9
|
-
*/
|
|
10
|
-
export const astroImage = (image: ImageFunction) =>
|
|
11
|
-
z
|
|
12
|
-
.string()
|
|
13
|
-
.transform((path) => (path.startsWith("./") ? path : `./${path}`))
|
|
14
|
-
.pipe(image())
|
|
1
|
+
import { z } from "astro/zod"
|
|
15
2
|
|
|
16
3
|
/**
|
|
17
4
|
* The Astro image function resolves to this schema.
|
|
@@ -3,7 +3,7 @@ import { z } from "astro/zod"
|
|
|
3
3
|
import type { SchemaContext } from "astro:content"
|
|
4
4
|
import { defineCollection, reference } from "astro:content"
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { imageSchema } from "./astro-image"
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Category Schema
|
|
@@ -172,12 +172,12 @@ export const mediaItemSchema = z.object({
|
|
|
172
172
|
*/
|
|
173
173
|
export const createMediaItemSchema = ({ image }: SchemaContext) =>
|
|
174
174
|
mediaItemSchema.extend({
|
|
175
|
-
image:
|
|
175
|
+
image: image(),
|
|
176
176
|
})
|
|
177
177
|
|
|
178
178
|
export const createCategorySchema = ({ image }: SchemaContext) =>
|
|
179
179
|
categorySchema.extend({
|
|
180
|
-
image:
|
|
180
|
+
image: image().optional(),
|
|
181
181
|
})
|
|
182
182
|
|
|
183
183
|
/**
|
|
@@ -315,6 +315,13 @@ export const LIGHTNET_COLLECTIONS = {
|
|
|
315
315
|
}),
|
|
316
316
|
schema: mediaTypeSchema,
|
|
317
317
|
}),
|
|
318
|
+
"internal-media-image-path": defineCollection({
|
|
319
|
+
loader: glob({
|
|
320
|
+
pattern: "*.json",
|
|
321
|
+
base: "./src/content/media",
|
|
322
|
+
}),
|
|
323
|
+
schema: z.object({ image: z.string() }),
|
|
324
|
+
}),
|
|
318
325
|
}
|
|
319
326
|
|
|
320
327
|
export const mediaItemEntrySchema = z.object({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getCollection, getEntry } from "astro:content"
|
|
2
2
|
|
|
3
3
|
import { verifySchemaAsync } from "../utils/verify-schema"
|
|
4
|
-
import { mediaItemEntrySchema } from "./content-schema"
|
|
4
|
+
import { type MediaItemEntry, mediaItemEntrySchema } from "./content-schema"
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Internal API to get media items. Since this package is a Astro integration
|
|
@@ -26,3 +26,48 @@ const prepareItem = async (item: unknown) => {
|
|
|
26
26
|
(id) => `Fix these issues inside "src/content/media/${id}.json":`,
|
|
27
27
|
)
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Revert media items like they it is stored in the
|
|
32
|
+
* content collection folder.
|
|
33
|
+
*/
|
|
34
|
+
export const getRawMediaItem = async (id: string) => {
|
|
35
|
+
const item = await getMediaItem(id)
|
|
36
|
+
return revertMediaItemEntry(item)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Revert media items like they are stored in the
|
|
41
|
+
* content collection folder.
|
|
42
|
+
*/
|
|
43
|
+
export const getRawMediaItems = async () => {
|
|
44
|
+
const mediaItems = await getMediaItems()
|
|
45
|
+
return Promise.all(mediaItems.map(revertMediaItemEntry))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the media item like it is stored in the content collection json.
|
|
50
|
+
* We need to revert Astro's modifications to references and images.
|
|
51
|
+
*
|
|
52
|
+
* @param mediaItem media item parsed by Astro
|
|
53
|
+
* @returns media item like before parsing
|
|
54
|
+
*/
|
|
55
|
+
async function revertMediaItemEntry({ id, data: mediaItem }: MediaItemEntry) {
|
|
56
|
+
const type = mediaItem.type.id
|
|
57
|
+
const categories = mediaItem.categories?.map((category) => category.id)
|
|
58
|
+
const collections = mediaItem.collections?.map((collection) => ({
|
|
59
|
+
...collection,
|
|
60
|
+
collection: collection.collection.id,
|
|
61
|
+
}))
|
|
62
|
+
const image = (await getEntry("internal-media-image-path", id))?.data.image
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
data: {
|
|
66
|
+
...mediaItem,
|
|
67
|
+
type,
|
|
68
|
+
categories,
|
|
69
|
+
collections,
|
|
70
|
+
image,
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -143,3 +143,6 @@ ln.404.page-not-found: Page not found
|
|
|
143
143
|
# English: Go to Home page
|
|
144
144
|
# Used on: https://sk8-ministries.pages.dev/unexisting-path
|
|
145
145
|
ln.404.go-to-the-home-page: Go to Home page
|
|
146
|
+
|
|
147
|
+
# Footer text to give credits to LightNet
|
|
148
|
+
ln.footer.powered-by-lightnet: Powered by LightNet
|
|
@@ -22,3 +22,4 @@ ln.details.download: Transferir
|
|
|
22
22
|
ln.share.url-copied-to-clipboard: Ligação copiada para a área de transferência
|
|
23
23
|
ln.404.page-not-found: Página não encontrada
|
|
24
24
|
ln.404.go-to-the-home-page: Ir para a página inicial
|
|
25
|
+
ln.footer.powered-by-lightnet: Com tecnologia LightNet
|
|
@@ -22,3 +22,4 @@ ln.details.download: Завантажити
|
|
|
22
22
|
ln.share.url-copied-to-clipboard: Посилання скопійовано до буферу обміну
|
|
23
23
|
ln.404.page-not-found: Сторінку не знайдено
|
|
24
24
|
ln.404.go-to-the-home-page: Перейти на Головну сторінку
|
|
25
|
+
ln.footer.powered-by-lightnet: Працює на платформі LightNet
|