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,38 @@
|
|
|
1
|
+
declare namespace App {
|
|
2
|
+
interface Locals {
|
|
3
|
+
/**
|
|
4
|
+
* Provides internationalization helpers.
|
|
5
|
+
*/
|
|
6
|
+
i18n: {
|
|
7
|
+
/**
|
|
8
|
+
* Translate a key to the language of the current locale.
|
|
9
|
+
*
|
|
10
|
+
* @param TranslationKey to be translated.
|
|
11
|
+
*/
|
|
12
|
+
t: import("./translate").TranslateFn
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The current locale or the default locale if the current locale is not available.
|
|
16
|
+
*
|
|
17
|
+
* In comparison to Astro.currentLocale this will always return a locale.
|
|
18
|
+
* Use Astro.currentLocale if you want to know the locale that is included in the current path.
|
|
19
|
+
*/
|
|
20
|
+
currentLocale: string
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The current text direction. Left-to-right or right-to-left.
|
|
24
|
+
*/
|
|
25
|
+
direction: "ltr" | "rtl"
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The default locale as defined in the project configuration.
|
|
29
|
+
*/
|
|
30
|
+
defaultLocale: string
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The available locales as defined in the project configuration.
|
|
34
|
+
*/
|
|
35
|
+
locales: string[]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "astro"
|
|
2
|
+
import config from "virtual:lightnet/config"
|
|
3
|
+
|
|
4
|
+
import { resolveDefaultLocale } from "./resolve-default-locale"
|
|
5
|
+
import { resolveLanguage } from "./resolve-language"
|
|
6
|
+
import { resolveLocales } from "./resolve-locales"
|
|
7
|
+
import { useTranslate } from "./translate"
|
|
8
|
+
|
|
9
|
+
export const onRequest: MiddlewareHandler = (
|
|
10
|
+
{ locals, currentLocale: astroCurrentLocale },
|
|
11
|
+
next,
|
|
12
|
+
) => {
|
|
13
|
+
if (!locals.i18n) {
|
|
14
|
+
const t = useTranslate(astroCurrentLocale)
|
|
15
|
+
const defaultLocale = resolveDefaultLocale(config)
|
|
16
|
+
const locales = resolveLocales(config)
|
|
17
|
+
const currentLocale = astroCurrentLocale ?? defaultLocale
|
|
18
|
+
const { direction } = resolveLanguage(currentLocale)
|
|
19
|
+
locals.i18n = {
|
|
20
|
+
t,
|
|
21
|
+
currentLocale,
|
|
22
|
+
defaultLocale,
|
|
23
|
+
direction,
|
|
24
|
+
locales,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return next()
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AstroError } from "astro/errors"
|
|
2
|
+
|
|
3
|
+
export const resolveDefaultLocale = ({
|
|
4
|
+
languages,
|
|
5
|
+
}: {
|
|
6
|
+
languages: {
|
|
7
|
+
code: string
|
|
8
|
+
isDefaultLocale?: boolean
|
|
9
|
+
translations?: unknown
|
|
10
|
+
}[]
|
|
11
|
+
}) => {
|
|
12
|
+
const defaultLanguage = languages.find((l) => l.isDefaultLocale)
|
|
13
|
+
if (defaultLanguage) {
|
|
14
|
+
return defaultLanguage.code
|
|
15
|
+
}
|
|
16
|
+
const uiLanguages = languages.filter((l) => !!l.translations)
|
|
17
|
+
if (uiLanguages.length === 0) {
|
|
18
|
+
throw new AstroError(
|
|
19
|
+
"No user interface language found",
|
|
20
|
+
"Make sure you include a translations object for at least one of your languages.",
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
if (uiLanguages.length > 1) {
|
|
24
|
+
throw new AstroError(
|
|
25
|
+
"Could not identify the default user interface language",
|
|
26
|
+
"Make sure you have set one language to be the default language by setting the isDefaultLocale property.",
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
return uiLanguages[0].code
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AstroError } from "astro/errors"
|
|
2
|
+
|
|
3
|
+
import { ALL_LANGUAGES } from "./languages"
|
|
4
|
+
import type { TranslateFn } from "./translate"
|
|
5
|
+
|
|
6
|
+
const languages = Object.fromEntries(
|
|
7
|
+
ALL_LANGUAGES.map((lang) => [lang.code, lang]),
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export const resolveLanguage = (bcp47: string) => {
|
|
11
|
+
const language = languages[bcp47]
|
|
12
|
+
|
|
13
|
+
if (!language) {
|
|
14
|
+
throw new AstroError(`There is no language definition for: ${bcp47}`)
|
|
15
|
+
}
|
|
16
|
+
return language
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const resolveTranslatedLanguage = (bcp47: string, t: TranslateFn) => {
|
|
20
|
+
const language = resolveLanguage(bcp47)
|
|
21
|
+
return {
|
|
22
|
+
...language,
|
|
23
|
+
name: t(language.label, { allowFixedStrings: true }),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AstroError } from "astro/errors"
|
|
2
|
+
import config from "virtual:lightnet/config"
|
|
3
|
+
|
|
4
|
+
import { resolveDefaultLocale } from "./resolve-default-locale"
|
|
5
|
+
import de from "./translations/de.json"
|
|
6
|
+
import en from "./translations/en.json"
|
|
7
|
+
|
|
8
|
+
type TranslationsByLocales = Record<string, Record<string, string>>
|
|
9
|
+
|
|
10
|
+
// We add (string & NonNullable<unknown>) to preserve typescript autocompletion for known keys
|
|
11
|
+
export type TranslationKey = keyof typeof en | (string & NonNullable<unknown>)
|
|
12
|
+
export type TranslationOptions = { allowFixedStrings?: boolean }
|
|
13
|
+
|
|
14
|
+
export type TranslateFn = (
|
|
15
|
+
key: TranslationKey,
|
|
16
|
+
options?: TranslationOptions,
|
|
17
|
+
) => string
|
|
18
|
+
|
|
19
|
+
const configTranslations = config.languages
|
|
20
|
+
.filter((l) => !!l.translations)
|
|
21
|
+
.reduce((prev, curr) => ({ ...prev, [curr.code]: curr.translations }), {})
|
|
22
|
+
const translationsByLocales = merge({ de, en }, configTranslations)
|
|
23
|
+
const defaultLocale = resolveDefaultLocale(config)
|
|
24
|
+
|
|
25
|
+
export function useTranslate(locale: string | undefined): TranslateFn {
|
|
26
|
+
const resolvedLocale = locale ?? defaultLocale
|
|
27
|
+
const translations = translationsByLocales[resolvedLocale]
|
|
28
|
+
const defaultTranslations = translationsByLocales[defaultLocale]
|
|
29
|
+
if (!translations) {
|
|
30
|
+
throw new AstroError(
|
|
31
|
+
`No translations found for language ${resolvedLocale}`,
|
|
32
|
+
"Add them to your lightnet config inside astro.config.mjs.",
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
return (key: TranslationKey, options?: TranslationOptions) => {
|
|
36
|
+
const value = translations[key] ?? defaultTranslations[key]
|
|
37
|
+
const isTranslationKey = key.startsWith("custom.") || key.startsWith("ln.")
|
|
38
|
+
if (!value && options?.allowFixedStrings && !isTranslationKey) {
|
|
39
|
+
return key
|
|
40
|
+
}
|
|
41
|
+
if (!value) {
|
|
42
|
+
throw new AstroError(
|
|
43
|
+
`Missing translation: '${key}' is undefined for language '${resolvedLocale}'.`,
|
|
44
|
+
`Add a translation for '${key}' to src/translations/${resolvedLocale}.json`,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
return value
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function merge(
|
|
52
|
+
source: TranslationsByLocales,
|
|
53
|
+
toMerge: TranslationsByLocales,
|
|
54
|
+
): TranslationsByLocales {
|
|
55
|
+
const result = { ...source }
|
|
56
|
+
for (const key of Object.keys(toMerge)) {
|
|
57
|
+
if (!source[key]) {
|
|
58
|
+
result[key] = toMerge[key]
|
|
59
|
+
} else {
|
|
60
|
+
result[key] = { ...source[key], ...toMerge[key] }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ln.404.go-to-the-home-page": "Zurück zur Startseite",
|
|
3
|
+
"ln.404.page-not-found": "Seite nicht gefunden",
|
|
4
|
+
"ln.common.categories": "Kategorien",
|
|
5
|
+
"ln.common.category": "Kategorie",
|
|
6
|
+
"ln.common.language": "Sprache",
|
|
7
|
+
"ln.common.languages": "Sprachen",
|
|
8
|
+
"ln.common.type": "Typ",
|
|
9
|
+
"ln.common.a11y.external-link": "Externer link",
|
|
10
|
+
"ln.details.open": "Öffnen",
|
|
11
|
+
"ln.details.part-of": "Teil der Sammlung",
|
|
12
|
+
"ln.details.download": "Download",
|
|
13
|
+
"ln.details.share": "Teilen",
|
|
14
|
+
"ln.header.a11y.open-main-menu": "Öffne Hauptmenü",
|
|
15
|
+
"ln.header.a11y.select-language": "Sprache auswählen",
|
|
16
|
+
"ln.home.title": "Startseite",
|
|
17
|
+
"ln.search.all-categories": "Alle Kategorien",
|
|
18
|
+
"ln.search.all-languages": "Alle Sprachen",
|
|
19
|
+
"ln.search.all-types": "Alle Typen",
|
|
20
|
+
"ln.search.more-results": "Mehr Ergebnisse",
|
|
21
|
+
"ln.search.no-results": "Keine Ergebnisse",
|
|
22
|
+
"ln.search.placeholder": "Suche in Titel, Autor, Beschreibung...",
|
|
23
|
+
"ln.search.title": "Suche",
|
|
24
|
+
"ln.share.url-copied-to-clipboard": "URL in die Zwischenablage kopiert"
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ln.404.go-to-the-home-page": "Go to the Home page",
|
|
3
|
+
"ln.404.page-not-found": "Page not found",
|
|
4
|
+
"ln.common.categories": "Categories",
|
|
5
|
+
"ln.common.category": "Category",
|
|
6
|
+
"ln.common.language": "Language",
|
|
7
|
+
"ln.common.languages": "Languages",
|
|
8
|
+
"ln.common.a11y.external-link": "External link",
|
|
9
|
+
"ln.common.type": "Type",
|
|
10
|
+
"ln.details.open": "Open",
|
|
11
|
+
"ln.details.share": "Share",
|
|
12
|
+
"ln.details.part-of-collection": "Part of Collection",
|
|
13
|
+
"ln.details.download": "Download",
|
|
14
|
+
"ln.header.a11y.open-main-menu": "Open Main Menu",
|
|
15
|
+
"ln.header.a11y.select-language": "Select language",
|
|
16
|
+
"ln.home.title": "Home",
|
|
17
|
+
"ln.search.all-categories": "All Categories",
|
|
18
|
+
"ln.search.all-languages": "All Languages",
|
|
19
|
+
"ln.search.all-types": "All Types",
|
|
20
|
+
"ln.search.more-results": "More results",
|
|
21
|
+
"ln.search.no-results": "No Results",
|
|
22
|
+
"ln.search.placeholder": "Search in Title, Author, Description...",
|
|
23
|
+
"ln.search.title": "Search",
|
|
24
|
+
"ln.share.url-copied-to-clipboard": "URL copied to clipboard"
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ClientRouter } from "astro:transitions"
|
|
3
|
+
import config from "virtual:lightnet/config"
|
|
4
|
+
|
|
5
|
+
import { resolveLanguage } from "../i18n/resolve-language"
|
|
6
|
+
import Favicon from "./components/Favicon.astro"
|
|
7
|
+
import PageNavigation from "./components/PageNavigation.astro"
|
|
8
|
+
import PageTitle from "./components/PageTitle.astro"
|
|
9
|
+
import PreloadReact from "./components/PreloadReact"
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
title?: string
|
|
13
|
+
description?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { title, description } = Astro.props
|
|
17
|
+
const configTitle = Astro.locals.i18n.t(config.title, {
|
|
18
|
+
allowFixedStrings: true,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const { currentLocale } = Astro.locals.i18n
|
|
22
|
+
const language = resolveLanguage(currentLocale)
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<!doctype html>
|
|
26
|
+
<html lang={currentLocale} dir={language.direction}>
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="UTF-8" />
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
30
|
+
<title>{title ? `${title} | ${configTitle}` : configTitle}</title>
|
|
31
|
+
{description && <meta name="description" content={description} />}
|
|
32
|
+
{config.manifest && <link rel="manifest" href={config.manifest} />}
|
|
33
|
+
<link rel="prefetch" href="/api/search.json" />
|
|
34
|
+
<Favicon />
|
|
35
|
+
<ClientRouter />
|
|
36
|
+
</head>
|
|
37
|
+
<body class="overflow-y-scroll bg-gray-50 text-gray-900">
|
|
38
|
+
<header
|
|
39
|
+
class="fixed top-0 z-50 h-14 w-full bg-gray-50 shadow-lg sm:h-20"
|
|
40
|
+
transition:animate="none"
|
|
41
|
+
>
|
|
42
|
+
<div
|
|
43
|
+
class="mx-auto flex h-full max-w-screen-xl justify-between px-4 md:px-8"
|
|
44
|
+
>
|
|
45
|
+
<PageTitle />
|
|
46
|
+
<PageNavigation />
|
|
47
|
+
</div>
|
|
48
|
+
</header>
|
|
49
|
+
<main class="mx-auto min-h-screen pb-8 pt-14 sm:py-20">
|
|
50
|
+
<slot />
|
|
51
|
+
</main>
|
|
52
|
+
<PreloadReact client:idle />
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { extname } from "node:path"
|
|
3
|
+
|
|
4
|
+
import { AstroError } from "astro/errors"
|
|
5
|
+
import config from "virtual:lightnet/config"
|
|
6
|
+
|
|
7
|
+
const faviconTypes = {
|
|
8
|
+
".ico": "image/x-icon",
|
|
9
|
+
".gif": "image/gif",
|
|
10
|
+
".jpeg": "image/jpeg",
|
|
11
|
+
".jpg": "image/jpeg",
|
|
12
|
+
".png": "image/png",
|
|
13
|
+
".svg": "image/svg+xml",
|
|
14
|
+
} as Record<string, string>
|
|
15
|
+
|
|
16
|
+
const favicons = config.favicon?.map((favicon) => {
|
|
17
|
+
const type = faviconTypes[extname(favicon.href)]
|
|
18
|
+
if (!type) {
|
|
19
|
+
throw new AstroError(
|
|
20
|
+
`Unsupported favicon ${favicon.href}`,
|
|
21
|
+
"favicon must be a .ico, .gif, .jpg, .png, or .svg file",
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
return { ...favicon, type }
|
|
25
|
+
})
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
favicons?.map(({ rel, type, href, sizes }) => (
|
|
30
|
+
<link rel={rel} type={type} href={href} sizes={sizes} />
|
|
31
|
+
))
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { resolveTranslatedLanguage } from "../../i18n/resolve-language"
|
|
3
|
+
import { localizePath } from "../../utils/paths"
|
|
4
|
+
import Menu from "./Menu.astro"
|
|
5
|
+
import MenuItem from "./MenuItem.astro"
|
|
6
|
+
|
|
7
|
+
const { t, locales } = Astro.locals.i18n
|
|
8
|
+
|
|
9
|
+
const translations = locales
|
|
10
|
+
.map((locale) => ({
|
|
11
|
+
locale,
|
|
12
|
+
label: resolveTranslatedLanguage(locale, t).name,
|
|
13
|
+
active: locale === Astro.currentLocale,
|
|
14
|
+
href: currentPathWithLocale(locale),
|
|
15
|
+
}))
|
|
16
|
+
.sort((a, b) => a.label.localeCompare(b.label))
|
|
17
|
+
|
|
18
|
+
function currentPathWithLocale(locale: string) {
|
|
19
|
+
const currentPath = Astro.url.pathname
|
|
20
|
+
const currentPathWithoutLocale =
|
|
21
|
+
Astro.currentLocale && currentPath.startsWith(`/${Astro.currentLocale}`)
|
|
22
|
+
? currentPath.slice(Astro.currentLocale.length + 1)
|
|
23
|
+
: currentPath
|
|
24
|
+
return localizePath(locale, currentPathWithoutLocale)
|
|
25
|
+
}
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
translations.length > 1 && (
|
|
30
|
+
<Menu icon="mdi--translate" label="ln.header.a11y.select-language">
|
|
31
|
+
{translations.map(({ label, locale, active, href }) => (
|
|
32
|
+
<MenuItem href={href} hreflang={locale} active={active}>
|
|
33
|
+
{label}
|
|
34
|
+
</MenuItem>
|
|
35
|
+
))}
|
|
36
|
+
</Menu>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "../../components/Icon"
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
icon: string
|
|
6
|
+
label: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { icon, label } = Astro.props
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="dy-dropdown dy-dropdown-end">
|
|
13
|
+
<div
|
|
14
|
+
role="button"
|
|
15
|
+
tabindex="0"
|
|
16
|
+
aria-label={Astro.locals.i18n.t(label)}
|
|
17
|
+
class="hover:text-primary flex rounded-md p-3 text-gray-600"
|
|
18
|
+
>
|
|
19
|
+
<Icon className={icon} ariaLabel="" />
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<ul
|
|
23
|
+
tabindex="0"
|
|
24
|
+
class="dy-dropdown-content top-px me-3 mt-[3.25rem] w-48 overflow-hidden rounded-b-md bg-gray-50 py-3 shadow-lg sm:mt-16"
|
|
25
|
+
>
|
|
26
|
+
<slot />
|
|
27
|
+
</ul>
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
href: string
|
|
4
|
+
active: boolean
|
|
5
|
+
hreflang?: string
|
|
6
|
+
target?: "_blank" | "_self"
|
|
7
|
+
}
|
|
8
|
+
const { href, hreflang, active, target } = Astro.props
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<li>
|
|
12
|
+
<a
|
|
13
|
+
href={href}
|
|
14
|
+
hreflang={hreflang}
|
|
15
|
+
target={target}
|
|
16
|
+
class="flex items-center gap-3 px-6 py-3 decoration-gray-800"
|
|
17
|
+
class:list={[active ? "font-bold" : "hover:underline"]}
|
|
18
|
+
>
|
|
19
|
+
<slot />
|
|
20
|
+
</a>
|
|
21
|
+
</li>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from "virtual:lightnet/config"
|
|
3
|
+
|
|
4
|
+
import Icon from "../../components/Icon"
|
|
5
|
+
import { localizePath, searchPagePath } from "../../utils/paths"
|
|
6
|
+
import { isExternalUrl } from "../../utils/urls"
|
|
7
|
+
import LanguagePicker from "./LanguagePicker.astro"
|
|
8
|
+
import Menu from "./Menu.astro"
|
|
9
|
+
import MenuItem from "./MenuItem.astro"
|
|
10
|
+
|
|
11
|
+
const currentPath = Astro.url.pathname
|
|
12
|
+
const items = (config.mainMenu ?? []).map(({ href, label, requiresLocale }) => {
|
|
13
|
+
const isExternal = isExternalUrl(href)
|
|
14
|
+
const path =
|
|
15
|
+
isExternal || !requiresLocale
|
|
16
|
+
? href
|
|
17
|
+
: localizePath(Astro.currentLocale, href)
|
|
18
|
+
const isActive =
|
|
19
|
+
!isExternal &&
|
|
20
|
+
(currentPath === localizePath(Astro.currentLocale, href) ||
|
|
21
|
+
currentPath === localizePath(Astro.currentLocale, `${href}/`) ||
|
|
22
|
+
currentPath === href)
|
|
23
|
+
return {
|
|
24
|
+
path,
|
|
25
|
+
isExternal,
|
|
26
|
+
label,
|
|
27
|
+
isActive,
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const t = Astro.locals.i18n.t
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
<nav class="-me-3 flex items-center">
|
|
35
|
+
<a
|
|
36
|
+
class="hover:text-primary flex p-3 text-gray-600"
|
|
37
|
+
aria-label={t("ln.search.title")}
|
|
38
|
+
data-astro-prefetch="viewport"
|
|
39
|
+
href={searchPagePath(Astro.currentLocale)}
|
|
40
|
+
><Icon className="mdi--magnify" ariaLabel="" /></a
|
|
41
|
+
>
|
|
42
|
+
<LanguagePicker />
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
!!items.length && (
|
|
46
|
+
<Menu icon="mdi--menu" label="ln.header.a11y.open-main-menu">
|
|
47
|
+
{items.map(({ label, path, isActive, isExternal }) => (
|
|
48
|
+
<MenuItem
|
|
49
|
+
href={path}
|
|
50
|
+
active={isActive}
|
|
51
|
+
target={isExternal ? "_blank" : "_self"}
|
|
52
|
+
>
|
|
53
|
+
{t(label, { allowFixedStrings: true })}
|
|
54
|
+
{isExternal && (
|
|
55
|
+
<Icon
|
|
56
|
+
className="mdi--external-link shrink-0 text-base"
|
|
57
|
+
ariaLabel=""
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</MenuItem>
|
|
61
|
+
))}
|
|
62
|
+
</Menu>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
</nav>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ImageMetadata } from "astro"
|
|
3
|
+
import { Image } from "astro:assets"
|
|
4
|
+
import config from "virtual:lightnet/config"
|
|
5
|
+
import logo from "virtual:lightnet/logo"
|
|
6
|
+
|
|
7
|
+
import { localizePath } from "../../utils/paths"
|
|
8
|
+
|
|
9
|
+
function getWidth(logo: ImageMetadata) {
|
|
10
|
+
const size = config.logo?.size ?? 28
|
|
11
|
+
// Calculate width to use for the logo.
|
|
12
|
+
// This based on the size that should measure the shorter side
|
|
13
|
+
// of the image
|
|
14
|
+
return Math.floor(size * Math.max(1, logo.width / logo.height))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const logoAlt = config.logo?.alt ?? ""
|
|
18
|
+
const t = Astro.locals.i18n.t
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<a
|
|
22
|
+
class="flex items-center gap-3 font-bold text-gray-600 md:text-lg"
|
|
23
|
+
href={localizePath(Astro.currentLocale, "/")}
|
|
24
|
+
>
|
|
25
|
+
{
|
|
26
|
+
logo &&
|
|
27
|
+
(logo.format === "svg" ? (
|
|
28
|
+
<img
|
|
29
|
+
src={logo.src}
|
|
30
|
+
alt={t(logoAlt, { allowFixedStrings: true })}
|
|
31
|
+
style={`width:${getWidth(logo)}px;`}
|
|
32
|
+
/>
|
|
33
|
+
) : (
|
|
34
|
+
<Image
|
|
35
|
+
src={logo}
|
|
36
|
+
alt={t(logoAlt, { allowFixedStrings: true })}
|
|
37
|
+
style={`width:${getWidth(logo)}px;`}
|
|
38
|
+
width={getWidth(logo)}
|
|
39
|
+
densities={[1.5, 2, 3]}
|
|
40
|
+
loading="eager"
|
|
41
|
+
/>
|
|
42
|
+
))
|
|
43
|
+
}{t(config.title, { allowFixedStrings: true })}</a
|
|
44
|
+
>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Page from "../layouts/Page.astro"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<Page>
|
|
6
|
+
<div class="flex flex-col items-center">
|
|
7
|
+
<p class="pb-8 pt-20 text-center text-3xl opacity-80">
|
|
8
|
+
{Astro.locals.i18n.t("ln.404.page-not-found")}
|
|
9
|
+
</p>
|
|
10
|
+
<a href={`/${Astro.locals.i18n.defaultLocale}`} class="dy-btn"
|
|
11
|
+
>{Astro.locals.i18n.t("ln.404.go-to-the-home-page")}</a
|
|
12
|
+
>
|
|
13
|
+
</div>
|
|
14
|
+
</Page>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type SearchItem = {
|
|
2
|
+
title: string
|
|
3
|
+
id: string
|
|
4
|
+
type: string
|
|
5
|
+
authors?: string[]
|
|
6
|
+
categories?: string[]
|
|
7
|
+
description?: string
|
|
8
|
+
language: string
|
|
9
|
+
image: { src: string; width: number; height: number }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type SearchResponse = {
|
|
13
|
+
items: SearchItem[]
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { APIRoute } from "astro"
|
|
2
|
+
import { getImage } from "astro:assets"
|
|
3
|
+
|
|
4
|
+
import type { MediaItemEntry } from "../../content/content-schema-internal"
|
|
5
|
+
import { getMediaItems } from "../../content/get-media-items"
|
|
6
|
+
import { markdownToText } from "../../utils/markdown"
|
|
7
|
+
import type { SearchItem } from "./search-response"
|
|
8
|
+
|
|
9
|
+
export const GET: APIRoute = async () => {
|
|
10
|
+
const items = await searchResults()
|
|
11
|
+
return new Response(JSON.stringify({ items }))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function searchResults() {
|
|
15
|
+
const items: SearchItem[] = []
|
|
16
|
+
for await (const mediaItem of await getMediaItems()) {
|
|
17
|
+
items.push(await createSearchItem(mediaItem))
|
|
18
|
+
}
|
|
19
|
+
return items.sort((a, b) => {
|
|
20
|
+
return 10 * a.type.localeCompare(b.type) + a.title.localeCompare(b.title)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function createSearchItem(mediaItem: MediaItemEntry) {
|
|
25
|
+
const {
|
|
26
|
+
id,
|
|
27
|
+
data: { image, title, authors, categories, language, description, type },
|
|
28
|
+
} = mediaItem
|
|
29
|
+
const {
|
|
30
|
+
src,
|
|
31
|
+
attributes: { width, height },
|
|
32
|
+
} = await getImage({
|
|
33
|
+
src: image,
|
|
34
|
+
format: "webp",
|
|
35
|
+
width: 144,
|
|
36
|
+
})
|
|
37
|
+
return {
|
|
38
|
+
title,
|
|
39
|
+
id,
|
|
40
|
+
type: type.id,
|
|
41
|
+
authors,
|
|
42
|
+
categories: categories?.map(({ id }) => id),
|
|
43
|
+
description: markdownToText(description)?.slice(0, 350),
|
|
44
|
+
language,
|
|
45
|
+
image: { src, width, height },
|
|
46
|
+
}
|
|
47
|
+
}
|