lightnet 2.15.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 +428 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/__e2e__/detailPage.spec.ts +0 -0
- package/__e2e__/fixtures/basics/astro.config.mjs +38 -0
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +17 -0
- package/__e2e__/fixtures/basics/node_modules/.bin/astro-check +17 -0
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +17 -0
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +17 -0
- package/__e2e__/fixtures/basics/node_modules/.bin/tsc +17 -0
- package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +17 -0
- package/__e2e__/fixtures/basics/package.json +19 -0
- package/__e2e__/fixtures/basics/public/favicon.svg +1 -0
- package/__e2e__/fixtures/basics/public/files/example.pdf +0 -0
- package/__e2e__/fixtures/basics/src/assets/logo.png +0 -0
- package/__e2e__/fixtures/basics/src/content/categories/christian-living.json +3 -0
- package/__e2e__/fixtures/basics/src/content/categories/teens.json +3 -0
- package/__e2e__/fixtures/basics/src/content/categories/theology.json +3 -0
- package/__e2e__/fixtures/basics/src/content/media/faithful-freestyle--en.json +13 -0
- package/__e2e__/fixtures/basics/src/content/media/how-to-kickflip--de.json +12 -0
- package/__e2e__/fixtures/basics/src/content/media/images/cover.jpg +0 -0
- package/__e2e__/fixtures/basics/src/content/media/images/how-to-kickflip--en.webp +0 -0
- package/__e2e__/fixtures/basics/src/content/media-collections/how-to-articles.json +3 -0
- package/__e2e__/fixtures/basics/src/content/media-types/book.json +9 -0
- package/__e2e__/fixtures/basics/src/content/media-types/video.json +7 -0
- package/__e2e__/fixtures/basics/src/content.config.ts +3 -0
- package/__e2e__/fixtures/basics/src/pages/[locale]/index.astro +14 -0
- package/__e2e__/fixtures/basics/src/translations/de.json +10 -0
- package/__e2e__/fixtures/basics/src/translations/en.json +10 -0
- package/__e2e__/fixtures/basics/tailwind.config.mjs +8 -0
- package/__e2e__/homepage.spec.ts +108 -0
- package/__e2e__/search.spec.ts +16 -0
- package/__e2e__/test-utils.ts +80 -0
- package/__tests__/pages/details-page/create-content-metadata.spec.ts +104 -0
- package/__tests__/utils/markdown.spec.ts +33 -0
- package/exports/components.ts +9 -0
- package/exports/content.ts +8 -0
- package/exports/details-page.ts +1 -0
- package/exports/i18n.ts +2 -0
- package/exports/index.ts +6 -0
- package/exports/utils.ts +2 -0
- package/package.json +54 -0
- package/playwright.config.ts +30 -0
- package/src/astro-integration/config.ts +185 -0
- package/src/astro-integration/integration.ts +74 -0
- package/src/astro-integration/project-context.ts +5 -0
- package/src/astro-integration/virtual.d.ts +14 -0
- package/src/astro-integration/vite-plugin-lightnet-config.ts +55 -0
- package/src/components/CategoriesOverview.astro +37 -0
- package/src/components/Gallery.astro +121 -0
- package/src/components/Hero.astro +82 -0
- package/src/components/HighlightSection.astro +71 -0
- package/src/components/Icon.tsx +27 -0
- package/src/components/MediaItemList.astro +84 -0
- package/src/components/Section.astro +49 -0
- package/src/content/astro-image.ts +14 -0
- package/src/content/content-schema-internal.ts +52 -0
- package/src/content/content-schema.ts +263 -0
- package/src/content/external-api.ts +7 -0
- package/src/content/get-categories.ts +15 -0
- package/src/content/get-languages.ts +14 -0
- package/src/content/get-media-items.ts +27 -0
- package/src/content/get-media-types.ts +23 -0
- package/src/content/query-media-items.ts +89 -0
- package/src/content/resolve-category-label.ts +20 -0
- package/src/i18n/get-locale-paths.ts +8 -0
- package/src/i18n/languages.ts +10 -0
- package/src/i18n/locals.d.ts +38 -0
- package/src/i18n/locals.ts +28 -0
- package/src/i18n/resolve-default-locale.ts +30 -0
- package/src/i18n/resolve-language.ts +25 -0
- package/src/i18n/resolve-locales.ts +5 -0
- package/src/i18n/translate.ts +64 -0
- package/src/i18n/translations/de.json +25 -0
- package/src/i18n/translations/en.json +25 -0
- package/src/layouts/MarkdownPage.astro +11 -0
- package/src/layouts/Page.astro +54 -0
- package/src/layouts/components/Favicon.astro +32 -0
- package/src/layouts/components/LanguagePicker.astro +38 -0
- package/src/layouts/components/Menu.astro +28 -0
- package/src/layouts/components/MenuItem.astro +21 -0
- package/src/layouts/components/PageNavigation.astro +65 -0
- package/src/layouts/components/PageTitle.astro +44 -0
- package/src/layouts/components/PreloadReact.tsx +3 -0
- package/src/pages/404.astro +14 -0
- package/src/pages/RedirectToDefaultLocale.astro +3 -0
- package/src/pages/api/search-response.ts +14 -0
- package/src/pages/api/search.ts +47 -0
- package/src/pages/details-page/DefaultDetails.astro +44 -0
- package/src/pages/details-page/DetailsPage.astro +53 -0
- package/src/pages/details-page/VideoDetails.astro +43 -0
- package/src/pages/details-page/components/Authors.astro +19 -0
- package/src/pages/details-page/components/Content.astro +73 -0
- package/src/pages/details-page/components/Cover.astro +35 -0
- package/src/pages/details-page/components/Description.astro +26 -0
- package/src/pages/details-page/components/MediaCollection.astro +39 -0
- package/src/pages/details-page/components/MediaCollections.astro +21 -0
- package/src/pages/details-page/components/OpenButton.astro +34 -0
- package/src/pages/details-page/components/SectionTitle.astro +8 -0
- package/src/pages/details-page/components/ShareButton.astro +58 -0
- package/src/pages/details-page/components/Title.astro +18 -0
- package/src/pages/details-page/components/VideoPlayer.astro +78 -0
- package/src/pages/details-page/components/details/Categories.astro +31 -0
- package/src/pages/details-page/components/details/Details.astro +17 -0
- package/src/pages/details-page/components/details/Label.astro +3 -0
- package/src/pages/details-page/components/details/Languages.astro +46 -0
- package/src/pages/details-page/utils/create-content-metadata.ts +78 -0
- package/src/pages/search-page/Search.tsx +71 -0
- package/src/pages/search-page/SearchPage.astro +51 -0
- package/src/pages/search-page/components/ResultList.tsx +135 -0
- package/src/pages/search-page/components/SearchFilter.tsx +189 -0
- package/src/pages/search-page/hooks/use-debounce.ts +17 -0
- package/src/pages/search-page/hooks/use-search.ts +95 -0
- package/src/pages/search-page/types.ts +9 -0
- package/src/pages/search-page/utils/search-translations.ts +22 -0
- package/src/pages/search-page/utils/use-provided-translations.ts +5 -0
- package/src/utils/markdown.ts +41 -0
- package/src/utils/paths.ts +45 -0
- package/src/utils/urls.ts +29 -0
- package/src/utils/verify-schema.ts +38 -0
- package/tailwind.config.ts +56 -0
- package/vitest.config.js +19 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Authors from "./components/Authors.astro"
|
|
3
|
+
import Content from "./components/Content.astro"
|
|
4
|
+
import Cover from "./components/Cover.astro"
|
|
5
|
+
import Description from "./components/Description.astro"
|
|
6
|
+
import Details from "./components/details/Details.astro"
|
|
7
|
+
import MediaCollections from "./components/MediaCollections.astro"
|
|
8
|
+
import OpenButton from "./components/OpenButton.astro"
|
|
9
|
+
import ShareButton from "./components/ShareButton.astro"
|
|
10
|
+
import Title from "./components/Title.astro"
|
|
11
|
+
|
|
12
|
+
export type Props = {
|
|
13
|
+
slug: string
|
|
14
|
+
coverStyle?: "default" | "book"
|
|
15
|
+
openActionLabel?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
slug,
|
|
20
|
+
coverStyle = "default",
|
|
21
|
+
openActionLabel = "ln.details.open",
|
|
22
|
+
} = Astro.props
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<div
|
|
26
|
+
class="mx-auto mt-10 flex max-w-screen-md flex-col items-center gap-8 px-4 sm:mt-20 sm:flex-row sm:items-start sm:gap-14 md:px-8"
|
|
27
|
+
>
|
|
28
|
+
<Cover slug={slug} style={coverStyle} />
|
|
29
|
+
<div class="flex flex-col items-center sm:items-start">
|
|
30
|
+
<Title className="text-center sm:text-start" slug={slug} />
|
|
31
|
+
<Authors className="text-center sm:text-start" slug={slug} />
|
|
32
|
+
|
|
33
|
+
<div
|
|
34
|
+
class="mt-8 flex flex-col justify-center gap-4 sm:justify-start md:mt-14"
|
|
35
|
+
>
|
|
36
|
+
<OpenButton slug={slug} openActionLabel={openActionLabel} />
|
|
37
|
+
<ShareButton />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<Description slug={slug} />
|
|
42
|
+
<Content slug={slug} />
|
|
43
|
+
<MediaCollections slug={slug} />
|
|
44
|
+
<Details slug={slug} />
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { GetStaticPaths } from "astro"
|
|
3
|
+
import { AstroError } from "astro/errors"
|
|
4
|
+
import config from "virtual:lightnet/config"
|
|
5
|
+
|
|
6
|
+
import { getMediaItem, getMediaItems } from "../../content/get-media-items"
|
|
7
|
+
import { getMediaType } from "../../content/get-media-types"
|
|
8
|
+
import { resolveLocales } from "../../i18n/resolve-locales"
|
|
9
|
+
import Page from "../../layouts/Page.astro"
|
|
10
|
+
import { markdownToText } from "../../utils/markdown"
|
|
11
|
+
import DefaultDetails from "./DefaultDetails.astro"
|
|
12
|
+
import VideoDetails from "./VideoDetails.astro"
|
|
13
|
+
|
|
14
|
+
export const getStaticPaths = (async () => {
|
|
15
|
+
const mediaItems = await getMediaItems()
|
|
16
|
+
return resolveLocales(config).flatMap((locale) =>
|
|
17
|
+
mediaItems.map(({ id: slug }) => ({ params: { slug, locale } })),
|
|
18
|
+
)
|
|
19
|
+
}) satisfies GetStaticPaths
|
|
20
|
+
|
|
21
|
+
const { slug } = Astro.params
|
|
22
|
+
const mediaItem = (await getMediaItem(Astro.params.slug)).data
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
data: { detailsPage },
|
|
26
|
+
} = await getMediaType(mediaItem.type.id)
|
|
27
|
+
|
|
28
|
+
let CustomDetails
|
|
29
|
+
const layout = detailsPage?.layout ?? "default"
|
|
30
|
+
if (detailsPage?.layout === "custom") {
|
|
31
|
+
const d = import.meta.glob("/src/details-pages/*.astro")
|
|
32
|
+
const customDetailsImport = d[
|
|
33
|
+
`/src/details-pages/${detailsPage.customComponent}`
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
] as () => Promise<any>
|
|
36
|
+
if (!customDetailsImport) {
|
|
37
|
+
throw new AstroError(
|
|
38
|
+
`Unknown details page ${detailsPage.customComponent}`,
|
|
39
|
+
"Make sure to add your details component within /src/details-pages/",
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
CustomDetails = (await customDetailsImport()).default
|
|
43
|
+
}
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<Page
|
|
47
|
+
title={mediaItem.title}
|
|
48
|
+
description={markdownToText(mediaItem.description)}
|
|
49
|
+
>
|
|
50
|
+
{layout === "default" && <DefaultDetails slug={slug} {...detailsPage} />}
|
|
51
|
+
{layout === "video" && <VideoDetails slug={slug} {...detailsPage} />}
|
|
52
|
+
{CustomDetails && <CustomDetails slug={slug} {...detailsPage} />}
|
|
53
|
+
</Page>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getMediaItem } from "../../content/get-media-items"
|
|
3
|
+
import Authors from "./components/Authors.astro"
|
|
4
|
+
import Content from "./components/Content.astro"
|
|
5
|
+
import Description from "./components/Description.astro"
|
|
6
|
+
import Details from "./components/details/Details.astro"
|
|
7
|
+
import MediaCollections from "./components/MediaCollections.astro"
|
|
8
|
+
import ShareButton from "./components/ShareButton.astro"
|
|
9
|
+
import Title from "./components/Title.astro"
|
|
10
|
+
import VideoPlayer from "./components/VideoPlayer.astro"
|
|
11
|
+
|
|
12
|
+
export type Props = {
|
|
13
|
+
slug: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { slug } = Astro.props
|
|
17
|
+
|
|
18
|
+
const item = await getMediaItem(slug)
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<div class="mx-auto max-w-screen-md md:px-8">
|
|
22
|
+
<div
|
|
23
|
+
class="aspect-video w-full overflow-hidden bg-black md:mt-8 md:rounded-lg"
|
|
24
|
+
>
|
|
25
|
+
<VideoPlayer
|
|
26
|
+
url={item.data.content[0].url!}
|
|
27
|
+
title={item.data.title}
|
|
28
|
+
image={item.data.image}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div
|
|
33
|
+
class="mx-auto mt-12 flex max-w-screen-md flex-col items-start px-4 md:mt-14 md:px-8"
|
|
34
|
+
>
|
|
35
|
+
<Title slug={slug} />
|
|
36
|
+
<Authors slug={slug} />
|
|
37
|
+
<ShareButton className="mt-8" />
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<Description slug={slug} />
|
|
41
|
+
<Content slug={slug} />
|
|
42
|
+
<MediaCollections slug={slug} />
|
|
43
|
+
<Details slug={slug} />
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getMediaItem } from "../../../content/get-media-items"
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
slug: string
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const item = await getMediaItem(Astro.props.slug)
|
|
10
|
+
const authors = item.data.authors?.join(", ")
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
authors && (
|
|
15
|
+
<p class="mt-2 text-xl sm:text-2xl" class:list={Astro.props.className}>
|
|
16
|
+
{authors}
|
|
17
|
+
</p>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "../../../components/Icon"
|
|
3
|
+
import { getMediaItem } from "../../../content/get-media-items"
|
|
4
|
+
import {
|
|
5
|
+
createContentMetadata,
|
|
6
|
+
type UrlType,
|
|
7
|
+
} from "../utils/create-content-metadata"
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
slug: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const item = await getMediaItem(Astro.props.slug)
|
|
14
|
+
|
|
15
|
+
if (item.data.content.length < 2) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const content = item.data.content.map((c) => createContentMetadata(c))
|
|
20
|
+
const typeIcons: { [k in UrlType]: string } = {
|
|
21
|
+
audio: "mdi--music",
|
|
22
|
+
text: "mdi--text",
|
|
23
|
+
source: "mdi--code-tags",
|
|
24
|
+
link: "mdi--link-variant",
|
|
25
|
+
image: "mdi--image-outline",
|
|
26
|
+
video: "mdi--video-outline",
|
|
27
|
+
package: "mdi--zip-box-outline",
|
|
28
|
+
} as const
|
|
29
|
+
|
|
30
|
+
const { t, direction } = Astro.locals.i18n
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<ol
|
|
34
|
+
class="mx-auto mt-20 max-w-screen-md overflow-hidden bg-gray-200 md:mt-24 md:rounded-xl"
|
|
35
|
+
>
|
|
36
|
+
{
|
|
37
|
+
content.map(
|
|
38
|
+
({ extension, name, type, canBeOpened, url, target }, index) => (
|
|
39
|
+
<li class="group -mt-px px-4 transition-colors ease-in-out hover:bg-gray-300 md:px-8">
|
|
40
|
+
<a
|
|
41
|
+
href={url}
|
|
42
|
+
target={target}
|
|
43
|
+
class="flex items-center justify-between py-8"
|
|
44
|
+
>
|
|
45
|
+
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-800">
|
|
46
|
+
<Icon
|
|
47
|
+
className={`${typeIcons[type]} text-gray-200`}
|
|
48
|
+
ariaLabel=""
|
|
49
|
+
/>
|
|
50
|
+
</span>
|
|
51
|
+
|
|
52
|
+
<div class="ms-4 line-clamp-1 shrink grow overflow-hidden sm:ms-8">
|
|
53
|
+
{name}
|
|
54
|
+
</div>
|
|
55
|
+
<div class="me-4 ms-2 shrink-0 font-bold uppercase text-gray-600 sm:me-8">
|
|
56
|
+
{extension}
|
|
57
|
+
</div>
|
|
58
|
+
<Icon
|
|
59
|
+
className={`${canBeOpened ? "mdi--chevron-right" : "mdi--download"} shrink-0 bg-gray-600 group-hover:bg-gray-800`}
|
|
60
|
+
ariaLabel={
|
|
61
|
+
canBeOpened ? t("ln.details.open") : t("ln.details.download")
|
|
62
|
+
}
|
|
63
|
+
flipIcon={direction === "rtl"}
|
|
64
|
+
/>
|
|
65
|
+
</a>
|
|
66
|
+
{index !== content.length - 1 && (
|
|
67
|
+
<div class="h-px w-full bg-gray-300" />
|
|
68
|
+
)}
|
|
69
|
+
</li>
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
</ol>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Image } from "astro:assets"
|
|
3
|
+
|
|
4
|
+
import { getMediaItem } from "../../../content/get-media-items"
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
slug: string
|
|
8
|
+
style?: "default" | "book"
|
|
9
|
+
}
|
|
10
|
+
const { slug, style = "default" } = Astro.props
|
|
11
|
+
|
|
12
|
+
const item = await getMediaItem(slug)
|
|
13
|
+
const image = item.data.image
|
|
14
|
+
const isPortraitImage = image.height > image.width
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
class="relative shrink-0 overflow-hidden shadow-sm"
|
|
19
|
+
class:list={style === "book" ? "rounded-sm" : "rounded-md"}
|
|
20
|
+
>
|
|
21
|
+
<Image
|
|
22
|
+
class:list={isPortraitImage ? "h-52 w-auto sm:h-72" : "h-auto w-52 sm:w-72"}
|
|
23
|
+
alt=""
|
|
24
|
+
widths={[208, 288, 576, 864]}
|
|
25
|
+
sizes="(max-width: 640px) 13rem, 18rem"
|
|
26
|
+
src={image}
|
|
27
|
+
quality="high"
|
|
28
|
+
loading="eager"
|
|
29
|
+
/>
|
|
30
|
+
{
|
|
31
|
+
style === "book" && (
|
|
32
|
+
<span class="absolute start-[3px] top-0 h-full w-[4px] bg-gradient-to-r from-gray-500/20 to-transparent" />
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getMediaItem } from "../../../content/get-media-items"
|
|
3
|
+
import { resolveLanguage } from "../../../i18n/resolve-language"
|
|
4
|
+
import { markdownToHtml } from "../../../utils/markdown"
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
slug: string
|
|
8
|
+
}
|
|
9
|
+
const item = await getMediaItem(Astro.props.slug)
|
|
10
|
+
|
|
11
|
+
const { description, language } = item.data
|
|
12
|
+
|
|
13
|
+
const { direction, code: lang } = resolveLanguage(language)
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
description && (
|
|
18
|
+
<div
|
|
19
|
+
dir={direction}
|
|
20
|
+
class="prose prose-gray mx-auto mt-10 max-w-screen-md px-4 sm:mt-14 md:px-8"
|
|
21
|
+
set:html={await markdownToHtml(description)}
|
|
22
|
+
lang={lang}
|
|
23
|
+
id="description"
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { AstroError } from "astro/errors"
|
|
3
|
+
import { getEntry } from "astro:content"
|
|
4
|
+
|
|
5
|
+
import MediaItemList from "../../../components/MediaItemList.astro"
|
|
6
|
+
import { getMediaItems } from "../../../content/get-media-items"
|
|
7
|
+
import { queryMediaItems } from "../../../content/query-media-items"
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
collectionId: string
|
|
11
|
+
disableItem: string
|
|
12
|
+
}
|
|
13
|
+
const { collectionId, disableItem } = Astro.props
|
|
14
|
+
|
|
15
|
+
const collection = await getEntry("media-collections", collectionId)
|
|
16
|
+
if (!collection) {
|
|
17
|
+
throw new AstroError(`Unknown media collection id ${collection}.`)
|
|
18
|
+
}
|
|
19
|
+
const items = (
|
|
20
|
+
await queryMediaItems(getMediaItems(), {
|
|
21
|
+
where: { collection: collection.id },
|
|
22
|
+
})
|
|
23
|
+
).map((item) => ({ ...item, disabled: item.id === disableItem }))
|
|
24
|
+
|
|
25
|
+
if (items.length < 2) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
const t = Astro.locals.i18n.t
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<section class="mx-auto mt-16 max-w-screen-md px-4 md:mt-20 md:px-8">
|
|
32
|
+
<h2 class="mb-3 text-xs font-bold uppercase text-gray-600">
|
|
33
|
+
{t("ln.details.part-of-collection")}
|
|
34
|
+
</h2>
|
|
35
|
+
<h3 class="mb-2 text-lg font-bold text-gray-700">
|
|
36
|
+
{t(collection.data.label, { allowFixedStrings: true })}
|
|
37
|
+
</h3>
|
|
38
|
+
<MediaItemList items={items} />
|
|
39
|
+
</section>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getMediaItem } from "../../../content/get-media-items"
|
|
3
|
+
import MediaCollection from "./MediaCollection.astro"
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
slug: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { slug } = Astro.props
|
|
10
|
+
const item = await getMediaItem(slug)
|
|
11
|
+
|
|
12
|
+
const collections = item.data.collections?.map(
|
|
13
|
+
({ collection }) => collection.id,
|
|
14
|
+
)
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
collections?.map((id) => (
|
|
19
|
+
<MediaCollection collectionId={id} disableItem={slug} />
|
|
20
|
+
))
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "../../../components/Icon"
|
|
3
|
+
import { getMediaItem } from "../../../content/get-media-items"
|
|
4
|
+
import { createContentMetadata } from "../utils/create-content-metadata"
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
slug: string
|
|
8
|
+
openActionLabel: string
|
|
9
|
+
}
|
|
10
|
+
const { slug, openActionLabel } = Astro.props
|
|
11
|
+
const item = await getMediaItem(slug)
|
|
12
|
+
const content = createContentMetadata(item.data.content[0])
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<a
|
|
16
|
+
class="flex min-w-52 items-center justify-center gap-2 rounded-2xl bg-gray-800 px-6 py-3 font-bold uppercase text-gray-100 shadow-sm hover:bg-gray-950 hover:text-gray-300"
|
|
17
|
+
href={content.url}
|
|
18
|
+
target={content.target}
|
|
19
|
+
hreflang={item.data.language}
|
|
20
|
+
>
|
|
21
|
+
{
|
|
22
|
+
content.canBeOpened
|
|
23
|
+
? Astro.locals.i18n.t(openActionLabel)
|
|
24
|
+
: Astro.locals.i18n.t("ln.details.download")
|
|
25
|
+
}
|
|
26
|
+
{
|
|
27
|
+
content.isExternal && (
|
|
28
|
+
<Icon
|
|
29
|
+
ariaLabel={Astro.locals.i18n.t("ln.common.a11y.external-link")}
|
|
30
|
+
className={`mdi--external-link shrink-0`}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
</a>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "../../../components/Icon"
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
className?: string
|
|
6
|
+
}
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<button
|
|
10
|
+
class="flex items-center justify-center gap-2 rounded-2xl bg-gray-200 px-4 py-2 text-xs font-bold uppercase text-gray-600 shadow-sm hover:bg-gray-300"
|
|
11
|
+
class:list={[Astro.props.className]}
|
|
12
|
+
id="share-btn"
|
|
13
|
+
><Icon className="mdi--share-variant-outline" ariaLabel="" />
|
|
14
|
+
{Astro.locals.i18n.t("ln.details.share")}</button
|
|
15
|
+
>
|
|
16
|
+
<div
|
|
17
|
+
id="share-success"
|
|
18
|
+
class="dy-toast pointer-events-none opacity-0 transition-opacity duration-300"
|
|
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>
|
|
24
|
+
<script>
|
|
25
|
+
document.addEventListener("astro:after-swap", () => {
|
|
26
|
+
initShareButton()
|
|
27
|
+
})
|
|
28
|
+
initShareButton()
|
|
29
|
+
|
|
30
|
+
function initShareButton() {
|
|
31
|
+
const btn = document.querySelector("#share-btn")
|
|
32
|
+
if (!btn) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
btn?.addEventListener("click", () => {
|
|
36
|
+
if (navigator.share) {
|
|
37
|
+
navigator
|
|
38
|
+
.share({
|
|
39
|
+
url: window.location.href,
|
|
40
|
+
})
|
|
41
|
+
.catch((e) => console.debug("Could not share", e))
|
|
42
|
+
} else {
|
|
43
|
+
navigator.clipboard
|
|
44
|
+
.writeText(window.location.href)
|
|
45
|
+
.then(() => {
|
|
46
|
+
const toast = document.querySelector<HTMLElement>("#share-success")!
|
|
47
|
+
toast.style.opacity = "100%"
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
toast.style.opacity = "0%"
|
|
50
|
+
}, 3000)
|
|
51
|
+
})
|
|
52
|
+
.catch((error) =>
|
|
53
|
+
console.log("Error copying URL to clipboard:", error),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getMediaItem } from "../../../content/get-media-items"
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
slug: string
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const item = await getMediaItem(Astro.props.slug)
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<h1
|
|
13
|
+
class="text-4xl font-bold sm:text-5xl"
|
|
14
|
+
lang={item.data.language}
|
|
15
|
+
class:list={Astro.props.className}
|
|
16
|
+
>
|
|
17
|
+
{item.data.title}
|
|
18
|
+
</h1>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ImageMetadata } from "astro"
|
|
3
|
+
import { getImage } from "astro:assets"
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
url: string
|
|
7
|
+
title: string
|
|
8
|
+
image: ImageMetadata
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { url, title, image: imageMetadata } = Astro.props
|
|
12
|
+
|
|
13
|
+
const { host, id, image } = await parseUrl(url)
|
|
14
|
+
|
|
15
|
+
async function parseUrl(urlToParse: string): Promise<{
|
|
16
|
+
host: "youtube" | "vimeo" | "mp4"
|
|
17
|
+
id: string | null
|
|
18
|
+
image?: string
|
|
19
|
+
}> {
|
|
20
|
+
const url = new URL(urlToParse)
|
|
21
|
+
// https://www.youtube.com/embed/ABC123abc
|
|
22
|
+
// https://www.youtube.com/watch?v=ABC123abc
|
|
23
|
+
if (url.hostname === "www.youtube.com") {
|
|
24
|
+
if (url.pathname.startsWith("/embed/")) {
|
|
25
|
+
return { host: "youtube", id: url.pathname.slice(7) }
|
|
26
|
+
}
|
|
27
|
+
return { host: "youtube", id: url.searchParams.get("v") }
|
|
28
|
+
}
|
|
29
|
+
// https://youtu.be/ABC123abc
|
|
30
|
+
if (url.hostname === "youtu.be") {
|
|
31
|
+
return { host: "youtube", id: url.pathname.slice(1) }
|
|
32
|
+
}
|
|
33
|
+
// https://www.youtube-nocookie.com/embed/ABC123abc
|
|
34
|
+
if (url.hostname === "www.youtube-nocookie.com") {
|
|
35
|
+
return { host: "youtube", id: url.pathname.slice(7) }
|
|
36
|
+
}
|
|
37
|
+
// https://vimeo.com/12345678
|
|
38
|
+
if (url.hostname === "vimeo.com") {
|
|
39
|
+
return { host: "vimeo", id: url.pathname.slice(1) }
|
|
40
|
+
}
|
|
41
|
+
// https://player.vimeo.com/video/12345678
|
|
42
|
+
if (url.hostname === "player.vimeo.com") {
|
|
43
|
+
return { host: "vimeo", id: url.pathname.slice(7) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// https://domain.com/video.mp4
|
|
47
|
+
if (url.pathname.endsWith(".mp4")) {
|
|
48
|
+
const image = (await getImage({ src: imageMetadata, format: "webp" })).src
|
|
49
|
+
return { host: "mp4", id: url.toString(), image }
|
|
50
|
+
}
|
|
51
|
+
throw Error(`Unsupported video url: ${urlToParse}`)
|
|
52
|
+
}
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
host === "youtube" ? (
|
|
57
|
+
<iframe
|
|
58
|
+
class="h-full w-full"
|
|
59
|
+
src={`https://www.youtube-nocookie.com/embed/${id}`}
|
|
60
|
+
title={title}
|
|
61
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
62
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
63
|
+
allowfullscreen
|
|
64
|
+
/>
|
|
65
|
+
) : host === "vimeo" ? (
|
|
66
|
+
<iframe
|
|
67
|
+
class="h-full w-full"
|
|
68
|
+
src={`https://player.vimeo.com/video/${id}`}
|
|
69
|
+
allow="autoplay; fullscreen; picture-in-picture"
|
|
70
|
+
allowfullscreen
|
|
71
|
+
/>
|
|
72
|
+
) : host === "mp4" ? (
|
|
73
|
+
<video class="h-full w-full" controls preload="auto" poster={image}>
|
|
74
|
+
<source src={id} type="video/mp4" />
|
|
75
|
+
Your browser does not support the video tag.
|
|
76
|
+
</video>
|
|
77
|
+
) : null
|
|
78
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getMediaItem } from "../../../../content/get-media-items"
|
|
3
|
+
import { resolveCategoryLabel } from "../../../../content/resolve-category-label"
|
|
4
|
+
import { searchPagePath } from "../../../../utils/paths"
|
|
5
|
+
import Label from "./Label.astro"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
slug: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const item = await getMediaItem(Astro.props.slug)
|
|
12
|
+
|
|
13
|
+
const categories = item.data.categories?.map(({ id }) => id)
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
!!categories?.length && (
|
|
18
|
+
<div>
|
|
19
|
+
<Label>{Astro.locals.i18n.t("ln.common.categories")}</Label>
|
|
20
|
+
<ul class="flex flex-wrap gap-2">
|
|
21
|
+
{categories.map(async (category) => (
|
|
22
|
+
<li class="flex rounded-lg bg-gray-200 px-4 py-1 text-gray-500 hover:bg-gray-300">
|
|
23
|
+
<a href={searchPagePath(Astro.currentLocale, { category })}>
|
|
24
|
+
{resolveCategoryLabel(Astro.locals.i18n.t, category)}
|
|
25
|
+
</a>
|
|
26
|
+
</li>
|
|
27
|
+
))}
|
|
28
|
+
</ul>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Categories from "./Categories.astro"
|
|
3
|
+
import Languages from "./Languages.astro"
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
slug: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { slug } = Astro.props
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="mx-auto mt-20 max-w-screen-md px-4 sm:mt-24 md:px-8" id="details">
|
|
13
|
+
<div class="flex flex-col gap-4">
|
|
14
|
+
<Languages slug={slug} />
|
|
15
|
+
<Categories slug={slug} />
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
getMediaItem,
|
|
4
|
+
getMediaItems,
|
|
5
|
+
} from "../../../../content/get-media-items"
|
|
6
|
+
import { resolveTranslatedLanguage } from "../../../../i18n/resolve-language"
|
|
7
|
+
import { detailsPagePath } from "../../../../utils/paths"
|
|
8
|
+
import Label from "./Label.astro"
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
slug: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const item = await getMediaItem(Astro.props.slug)
|
|
15
|
+
|
|
16
|
+
const { slug } = Astro.props
|
|
17
|
+
const translations = (await getMediaItems())
|
|
18
|
+
.filter(
|
|
19
|
+
(entry) => entry.data.commonId === item.data.commonId && entry.id !== slug,
|
|
20
|
+
)
|
|
21
|
+
.sort((a, b) => a.data.language.localeCompare(b.data.language))
|
|
22
|
+
|
|
23
|
+
const label = translations.length ? "ln.common.languages" : "ln.common.language"
|
|
24
|
+
const { t } = Astro.locals.i18n
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<div>
|
|
28
|
+
<Label>{t(label)}</Label>
|
|
29
|
+
<ul class="flex flex-wrap gap-2">
|
|
30
|
+
<li class="py-1 pe-2 text-gray-800">
|
|
31
|
+
{resolveTranslatedLanguage(item.data.language, t).name}
|
|
32
|
+
</li>
|
|
33
|
+
{
|
|
34
|
+
translations.map((translation) => (
|
|
35
|
+
<li class="flex rounded-lg border border-gray-200 px-4 py-1 text-gray-600 hover:bg-gray-200">
|
|
36
|
+
<a
|
|
37
|
+
href={detailsPagePath(Astro.currentLocale, translation)}
|
|
38
|
+
hreflang={translation.data.language}
|
|
39
|
+
>
|
|
40
|
+
{resolveTranslatedLanguage(translation.data.language, t).name}
|
|
41
|
+
</a>
|
|
42
|
+
</li>
|
|
43
|
+
))
|
|
44
|
+
}
|
|
45
|
+
</ul>
|
|
46
|
+
</div>
|