lightnet 3.10.0 → 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 +14 -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/package.json +2 -2
- 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 +13 -6
- 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 +15 -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.ts +20 -7
- package/src/layouts/Page.astro +1 -1
- 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,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 }))
|
|
@@ -217,6 +217,21 @@ export const configSchema = z.object({
|
|
|
217
217
|
hideHeaderSearchIcon: z.boolean().default(false),
|
|
218
218
|
})
|
|
219
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(),
|
|
220
235
|
})
|
|
221
236
|
|
|
222
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
|
+
}
|
package/src/i18n/translations.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import YAML from "yaml"
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
type AdminTranslationKey,
|
|
5
|
+
builtInAdminTranslations,
|
|
6
|
+
} from "../admin/i18n/translations"
|
|
7
|
+
|
|
3
8
|
const builtInTranslations = {
|
|
4
9
|
ar: () => import("./translations/ar.yml?raw"),
|
|
5
10
|
bn: () => import("./translations/bn.yml?raw"),
|
|
@@ -32,23 +37,30 @@ const userTranslations = Object.fromEntries(
|
|
|
32
37
|
)
|
|
33
38
|
|
|
34
39
|
export const loadTranslations = async (bcp47: string) => ({
|
|
35
|
-
...(await loadBuiltInTranslations(bcp47)),
|
|
40
|
+
...(await loadBuiltInTranslations(builtInTranslations, bcp47)),
|
|
41
|
+
...(await loadBuiltInTranslations(builtInAdminTranslations, bcp47)),
|
|
36
42
|
...(await loadUserTranslations(bcp47)),
|
|
37
43
|
})
|
|
38
44
|
|
|
39
|
-
function
|
|
40
|
-
|
|
45
|
+
function hasTranslations(
|
|
46
|
+
translationMap: Record<string, unknown>,
|
|
47
|
+
bcp47: string,
|
|
48
|
+
): bcp47 is BuiltInLanguage {
|
|
49
|
+
return Object.hasOwn(translationMap, bcp47)
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
const loadBuiltInTranslations = async (
|
|
53
|
+
translationMap: Record<string, () => Promise<typeof import("*?raw")>>,
|
|
54
|
+
bcp47: string,
|
|
55
|
+
) => {
|
|
56
|
+
if (!hasTranslations(translationMap, bcp47)) {
|
|
45
57
|
return {}
|
|
46
58
|
}
|
|
47
|
-
const yml = (await
|
|
59
|
+
const yml = (await translationMap[bcp47]()).default
|
|
48
60
|
return YAML.parse(yml)
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
|
|
63
|
+
const loadUserTranslations = async (bcp47: string) => {
|
|
52
64
|
if (!userTranslations[bcp47]) {
|
|
53
65
|
return {}
|
|
54
66
|
}
|
|
@@ -82,3 +94,4 @@ export type LightNetTranslationKey =
|
|
|
82
94
|
| "ln.search.title"
|
|
83
95
|
| "ln.share.url-copied-to-clipboard"
|
|
84
96
|
| "ln.footer.powered-by-lightnet"
|
|
97
|
+
| AdminTranslationKey
|
package/src/layouts/Page.astro
CHANGED
|
@@ -31,7 +31,7 @@ const language = resolveLanguage(currentLocale)
|
|
|
31
31
|
<title>{title ? `${title} | ${configTitle}` : configTitle}</title>
|
|
32
32
|
{description && <meta name="description" content={description} />}
|
|
33
33
|
{config.manifest && <link rel="manifest" href={config.manifest} />}
|
|
34
|
-
<link rel="prefetch" href="/api/search.json" />
|
|
34
|
+
<link rel="prefetch" href="/api/internal/search.json" />
|
|
35
35
|
<Favicon />
|
|
36
36
|
<ViewTransition />
|
|
37
37
|
</head>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Authors from "./main-details/Authors.astro"
|
|
3
3
|
import CoverImage from "./main-details/CoverImage.astro"
|
|
4
|
+
import EditButton from "./main-details/EditButton.astro"
|
|
4
5
|
import ShareButton from "./main-details/ShareButton.astro"
|
|
5
6
|
import Title from "./main-details/Title.astro"
|
|
6
7
|
|
|
@@ -19,7 +20,10 @@ const { mediaId, imageSize } = Astro.props
|
|
|
19
20
|
<div class="flex w-full grow flex-col items-center sm:items-start">
|
|
20
21
|
<Title className="text-center sm:text-start" mediaId={mediaId} />
|
|
21
22
|
<Authors className="mt-2" mediaId={mediaId} />
|
|
22
|
-
<
|
|
23
|
+
<div class="mt-4 flex gap-6">
|
|
24
|
+
<ShareButton />
|
|
25
|
+
<EditButton mediaId={mediaId} />
|
|
26
|
+
</div>
|
|
23
27
|
<slot />
|
|
24
28
|
</div>
|
|
25
29
|
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import VideoPlayer from "../../../components/VideoPlayer.astro"
|
|
3
3
|
import { getMediaItem } from "../../../content/get-media-items"
|
|
4
4
|
import Authors from "./main-details/Authors.astro"
|
|
5
|
+
import EditButton from "./main-details/EditButton.astro"
|
|
5
6
|
import ShareButton from "./main-details/ShareButton.astro"
|
|
6
7
|
import Title from "./main-details/Title.astro"
|
|
7
8
|
|
|
@@ -26,5 +27,8 @@ const item = await getMediaItem(mediaId)
|
|
|
26
27
|
>
|
|
27
28
|
<Title mediaId={mediaId} />
|
|
28
29
|
<Authors mediaId={mediaId} />
|
|
29
|
-
<
|
|
30
|
+
<div class="mt-4 flex gap-6">
|
|
31
|
+
<ShareButton />
|
|
32
|
+
<EditButton mediaId={mediaId} />
|
|
33
|
+
</div>
|
|
30
34
|
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from "virtual:lightnet/config"
|
|
3
|
+
|
|
4
|
+
import Icon from "../../../../components/Icon"
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
mediaId: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { mediaId } = Astro.props
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<a
|
|
14
|
+
class="hidden cursor-pointer items-center gap-2 font-bold text-gray-700 underline"
|
|
15
|
+
id="edit-btn"
|
|
16
|
+
data-admin-enabled={config.experimental?.admin?.enabled}
|
|
17
|
+
href={`/${Astro.currentLocale}/admin/media/${mediaId}`}
|
|
18
|
+
><Icon className="mdi--square-edit-outline" ariaLabel="" />
|
|
19
|
+
{Astro.locals.i18n.t("ln.admin.edit")}</a
|
|
20
|
+
>
|
|
21
|
+
<script>
|
|
22
|
+
const btn: HTMLAnchorElement | null = document.querySelector("#edit-btn")
|
|
23
|
+
const showEditButton =
|
|
24
|
+
btn?.dataset.adminEnabled === "true" &&
|
|
25
|
+
(import.meta.env.DEV || localStorage.getItem("ln-admin-enabled") === "true")
|
|
26
|
+
if (showEditButton) {
|
|
27
|
+
btn?.classList.remove("hidden")
|
|
28
|
+
btn?.classList.add("flex")
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Icon from "../../../../components/Icon"
|
|
3
|
+
import Toast from "../../../../components/Toast"
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
className?: string
|
|
@@ -13,16 +14,15 @@ interface Props {
|
|
|
13
14
|
><Icon className="mdi--share" ariaLabel="" />
|
|
14
15
|
{Astro.locals.i18n.t("ln.details.share")}</button
|
|
15
16
|
>
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
>
|
|
20
|
-
<div class="dy-alert dy-alert-success">
|
|
21
|
-
<span>{Astro.locals.i18n.t("ln.share.url-copied-to-clipboard")}</span>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
17
|
+
<Toast id="share-success" variant="success">
|
|
18
|
+
{Astro.locals.i18n.t("ln.share.url-copied-to-clipboard")}
|
|
19
|
+
</Toast>
|
|
24
20
|
<script>
|
|
21
|
+
import { showToastById } from "../../../../components/showToast"
|
|
22
|
+
|
|
25
23
|
const btn = document.querySelector("#share-btn")
|
|
24
|
+
const toastId = "share-success"
|
|
25
|
+
|
|
26
26
|
btn?.addEventListener("click", () => {
|
|
27
27
|
if (navigator.share) {
|
|
28
28
|
navigator
|
|
@@ -34,11 +34,7 @@ interface Props {
|
|
|
34
34
|
navigator.clipboard
|
|
35
35
|
.writeText(window.location.href)
|
|
36
36
|
.then(() => {
|
|
37
|
-
|
|
38
|
-
toast.style.opacity = "100%"
|
|
39
|
-
setTimeout(() => {
|
|
40
|
-
toast.style.opacity = "0%"
|
|
41
|
-
}, 3000)
|
|
37
|
+
showToastById(toastId)
|
|
42
38
|
})
|
|
43
39
|
.catch((error) => console.log("Error copying URL to clipboard:", error))
|
|
44
40
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { APIRoute } from "astro"
|
|
2
2
|
import { getImage } from "astro:assets"
|
|
3
3
|
|
|
4
|
-
import type { MediaItemEntry } from "
|
|
5
|
-
import { getMediaItems } from "
|
|
6
|
-
import { markdownToText } from "
|
|
4
|
+
import type { MediaItemEntry } from "../../../content/content-schema"
|
|
5
|
+
import { getMediaItems } from "../../../content/get-media-items"
|
|
6
|
+
import { markdownToText } from "../../../utils/markdown"
|
|
7
7
|
import type { SearchItem } from "./search-response"
|
|
8
8
|
|
|
9
9
|
export const GET: APIRoute = async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import CoverImageDecorator from "../../../components/CoverImageDecorator"
|
|
2
2
|
import Icon from "../../../components/Icon"
|
|
3
3
|
import { detailsPagePath } from "../../../utils/paths"
|
|
4
|
-
import type { SearchItem } from "
|
|
4
|
+
import type { SearchItem } from "../api/search-response"
|
|
5
5
|
|
|
6
6
|
export type MediaType = {
|
|
7
7
|
name: string
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Fuse from "fuse.js"
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react"
|
|
3
3
|
|
|
4
|
-
import type { SearchItem, SearchResponse } from "
|
|
4
|
+
import type { SearchItem, SearchResponse } from "../api/search-response"
|
|
5
5
|
import { observeSearchQuery, type SearchQuery } from "../utils/search-query"
|
|
6
6
|
|
|
7
7
|
interface Context {
|
|
@@ -28,10 +28,10 @@ export function useSearch({ categories, mediaTypes, languages }: Context) {
|
|
|
28
28
|
})
|
|
29
29
|
const fetchData = async () => {
|
|
30
30
|
try {
|
|
31
|
-
const response = await fetch("/api/search.json")
|
|
31
|
+
const response = await fetch("/api/internal/search.json")
|
|
32
32
|
if (!response.ok) {
|
|
33
33
|
throw new Error(
|
|
34
|
-
"Was not able to load search results from /api/search.json.",
|
|
34
|
+
"Was not able to load search results from /api/internal/search.json.",
|
|
35
35
|
)
|
|
36
36
|
}
|
|
37
37
|
const { items }: SearchResponse = await response.json()
|
package/tailwind.config.ts
CHANGED
|
File without changes
|