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,185 @@
|
|
|
1
|
+
import { z } from "astro/zod"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Link Schema.
|
|
5
|
+
*/
|
|
6
|
+
const linkSchema = z.object({
|
|
7
|
+
/**
|
|
8
|
+
* Address this should link to.
|
|
9
|
+
* Can either be a path like "/about" or a full
|
|
10
|
+
* url, like "https://your-ministry.com".
|
|
11
|
+
*/
|
|
12
|
+
href: z.string(),
|
|
13
|
+
/**
|
|
14
|
+
* Label to be used for the link.
|
|
15
|
+
* Can either be a translation key or a fixed string.
|
|
16
|
+
*/
|
|
17
|
+
label: z.string(),
|
|
18
|
+
/**
|
|
19
|
+
* If this is set to true the currentLocale will be appended to
|
|
20
|
+
* the href path. Eg. for href="/about"
|
|
21
|
+
* the resolved value will be "/en/about" if the current locale is "en".
|
|
22
|
+
*
|
|
23
|
+
* This option will be ignored if the path is a external url.
|
|
24
|
+
*
|
|
25
|
+
* Default is true.
|
|
26
|
+
*/
|
|
27
|
+
requiresLocale: z.boolean().default(true),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Language Schema.
|
|
32
|
+
*/
|
|
33
|
+
const languageSchema = z.object({
|
|
34
|
+
/**
|
|
35
|
+
* BCP-47 language code for this language.
|
|
36
|
+
*
|
|
37
|
+
* This will be the identifier of this language and will
|
|
38
|
+
* also appear on the URL paths of the website.
|
|
39
|
+
*/
|
|
40
|
+
code: z.string({ description: "BCP-47 language code" }),
|
|
41
|
+
/**
|
|
42
|
+
* The name of the language that will be shown on the Website.
|
|
43
|
+
*
|
|
44
|
+
* Can either be a fixed string or a translation key.
|
|
45
|
+
*/
|
|
46
|
+
label: z.string(),
|
|
47
|
+
/**
|
|
48
|
+
* The text direction of this language.
|
|
49
|
+
*
|
|
50
|
+
* Either right-to-left = rtl, or left-to-right = ltr.
|
|
51
|
+
*
|
|
52
|
+
* Default is "ltr".
|
|
53
|
+
*/
|
|
54
|
+
direction: z.enum(["rtl", "ltr"]).default("ltr"),
|
|
55
|
+
/**
|
|
56
|
+
* Translations for this language.
|
|
57
|
+
* This needs to be set if the the language should be used a UI locale.
|
|
58
|
+
*
|
|
59
|
+
* We expect a flat object with the keys being the translation keys and
|
|
60
|
+
* the values being the translated strings for this language.
|
|
61
|
+
*/
|
|
62
|
+
translations: z.record(z.string(), z.string()).optional(),
|
|
63
|
+
/**
|
|
64
|
+
* Should this language be used as the default language for the User Interface.
|
|
65
|
+
*
|
|
66
|
+
* Default locale will be the first language the user sees. Also translations
|
|
67
|
+
* will fallback to use the default locale if no translation entry is found.
|
|
68
|
+
*
|
|
69
|
+
* Default is false.
|
|
70
|
+
*/
|
|
71
|
+
isDefaultLocale: z.boolean().default(false),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const absolutePath = (path: string) =>
|
|
75
|
+
`${path.startsWith("/") ? "" : "/"}${path}`
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This API for setting a favicon uses the
|
|
79
|
+
* HTML standard attributes.
|
|
80
|
+
*
|
|
81
|
+
* @see https://en.wikipedia.org/wiki/Favicon
|
|
82
|
+
*
|
|
83
|
+
* We automatically infer the "type" of the icon. So you
|
|
84
|
+
* do not have to set this.
|
|
85
|
+
*/
|
|
86
|
+
const faviconSchema = z.object({
|
|
87
|
+
/**
|
|
88
|
+
* Reference the favicon. This must be a path to an image in the `public/` directory.
|
|
89
|
+
*
|
|
90
|
+
* @example "/favicon.svg"
|
|
91
|
+
*/
|
|
92
|
+
href: z.string().transform(absolutePath),
|
|
93
|
+
/**
|
|
94
|
+
* See HTML standard.
|
|
95
|
+
*/
|
|
96
|
+
rel: z.enum(["icon", "apple-touch-icon"]).default("icon"),
|
|
97
|
+
/**
|
|
98
|
+
* See HTML standard.
|
|
99
|
+
*/
|
|
100
|
+
sizes: z.string().optional(),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* LightNet Config Schema.
|
|
105
|
+
*/
|
|
106
|
+
export const configSchema = z.object({
|
|
107
|
+
/**
|
|
108
|
+
* Title of the web site.
|
|
109
|
+
*/
|
|
110
|
+
title: z.string(),
|
|
111
|
+
/**
|
|
112
|
+
* All languages: content languages and ui languages.
|
|
113
|
+
*/
|
|
114
|
+
languages: languageSchema.array(),
|
|
115
|
+
/**
|
|
116
|
+
* Favicons for your site.
|
|
117
|
+
*/
|
|
118
|
+
favicon: faviconSchema.array().optional(),
|
|
119
|
+
/**
|
|
120
|
+
* Link to manifest file within public/ folder
|
|
121
|
+
*/
|
|
122
|
+
manifest: z.string().transform(absolutePath).optional(),
|
|
123
|
+
/**
|
|
124
|
+
* Logo to be used for the header.
|
|
125
|
+
*/
|
|
126
|
+
logo: z
|
|
127
|
+
.object({
|
|
128
|
+
/**
|
|
129
|
+
* Path to a logo based in the /src/assets folder.
|
|
130
|
+
* We recommend a size of at least 150x150px. The logo
|
|
131
|
+
* will be optimized for performance.
|
|
132
|
+
*
|
|
133
|
+
* @example "/src/assets/your-logo.png"
|
|
134
|
+
*/
|
|
135
|
+
src: z.string(),
|
|
136
|
+
/**
|
|
137
|
+
* Alt attribute to add for screen reader etc.
|
|
138
|
+
* This can be a fixed string or a translation key.
|
|
139
|
+
*/
|
|
140
|
+
alt: z.string().default(""),
|
|
141
|
+
/**
|
|
142
|
+
* Size in px to use for the logo on the header bar.
|
|
143
|
+
* The size will be applied to the shorter side of your logo image.
|
|
144
|
+
*
|
|
145
|
+
* Default is 28 px.
|
|
146
|
+
*/
|
|
147
|
+
size: z.number().default(28),
|
|
148
|
+
})
|
|
149
|
+
.optional(),
|
|
150
|
+
/**
|
|
151
|
+
* Main menu structure.
|
|
152
|
+
*/
|
|
153
|
+
mainMenu: z.array(linkSchema).min(1).optional(),
|
|
154
|
+
/**
|
|
155
|
+
* The internalDomains configuration setting specifies a list of
|
|
156
|
+
* domain names that should be treated as internal.
|
|
157
|
+
*
|
|
158
|
+
* This setting is useful for bypassing external-link handling or marking
|
|
159
|
+
* trusted domains as internal resources.
|
|
160
|
+
*
|
|
161
|
+
* @notes
|
|
162
|
+
* - Domains are matched exactly as listed; wildcard or regex patterns are not supported.
|
|
163
|
+
* - Ensure that only trusted and necessary domains are included in this list.
|
|
164
|
+
*/
|
|
165
|
+
internalDomains: z.array(z.string()).default([]),
|
|
166
|
+
/**
|
|
167
|
+
* Configure search page behavior
|
|
168
|
+
*/
|
|
169
|
+
searchPage: z
|
|
170
|
+
.object({
|
|
171
|
+
/**
|
|
172
|
+
* When this is set to true, search results will be initially
|
|
173
|
+
* filtered by UI language. The filter will only be set when there
|
|
174
|
+
* is any media item in the UI language.
|
|
175
|
+
*/
|
|
176
|
+
filterByLocale: z.boolean().default(false),
|
|
177
|
+
})
|
|
178
|
+
.optional(),
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
export type Language = z.input<typeof languageSchema>
|
|
182
|
+
export type Link = z.input<typeof linkSchema>
|
|
183
|
+
|
|
184
|
+
export type LightnetConfig = z.input<typeof configSchema>
|
|
185
|
+
export type PreparedLightnetConfig = z.output<typeof configSchema>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
|
2
|
+
/// <reference path="../i18n/locals.d.ts" />
|
|
3
|
+
import react from "@astrojs/react"
|
|
4
|
+
import tailwind from "@astrojs/tailwind"
|
|
5
|
+
import type { AstroIntegration } from "astro"
|
|
6
|
+
|
|
7
|
+
import { resolveDefaultLocale } from "../i18n/resolve-default-locale"
|
|
8
|
+
import { resolveLocales } from "../i18n/resolve-locales"
|
|
9
|
+
import type { LightnetConfig } from "./config"
|
|
10
|
+
import { vitePluginLightnetConfig } from "./vite-plugin-lightnet-config"
|
|
11
|
+
|
|
12
|
+
export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
13
|
+
return {
|
|
14
|
+
name: "lightnet",
|
|
15
|
+
hooks: {
|
|
16
|
+
"astro:config:setup": ({
|
|
17
|
+
injectRoute,
|
|
18
|
+
config,
|
|
19
|
+
updateConfig,
|
|
20
|
+
logger,
|
|
21
|
+
addMiddleware,
|
|
22
|
+
}) => {
|
|
23
|
+
injectRoute({
|
|
24
|
+
pattern: "404",
|
|
25
|
+
entrypoint: "lightnet/pages/404.astro",
|
|
26
|
+
prerender: true,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
injectRoute({
|
|
30
|
+
pattern: "",
|
|
31
|
+
entrypoint: "lightnet/pages/RedirectToDefaultLocale.astro",
|
|
32
|
+
prerender: true,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
injectRoute({
|
|
36
|
+
pattern: "/[locale]/media",
|
|
37
|
+
entrypoint: "lightnet/pages/SearchPage.astro",
|
|
38
|
+
prerender: true,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
injectRoute({
|
|
42
|
+
pattern: "/api/search.json",
|
|
43
|
+
entrypoint: "lightnet/pages/api/search.ts",
|
|
44
|
+
prerender: true,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
injectRoute({
|
|
48
|
+
pattern: "/[locale]/media/[slug]",
|
|
49
|
+
entrypoint: "lightnet/pages/DetailsPage.astro",
|
|
50
|
+
prerender: true,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
addMiddleware({ entrypoint: "lightnet/locals", order: "pre" })
|
|
54
|
+
|
|
55
|
+
config.integrations.push(tailwind(), react())
|
|
56
|
+
|
|
57
|
+
updateConfig({
|
|
58
|
+
vite: {
|
|
59
|
+
plugins: [vitePluginLightnetConfig(lightnetConfig, config, logger)],
|
|
60
|
+
},
|
|
61
|
+
i18n: {
|
|
62
|
+
defaultLocale: resolveDefaultLocale(lightnetConfig),
|
|
63
|
+
locales: resolveLocales(lightnetConfig),
|
|
64
|
+
routing: {
|
|
65
|
+
redirectToDefaultLocale: false,
|
|
66
|
+
prefixDefaultLocale: false,
|
|
67
|
+
fallbackType: "rewrite",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare module "virtual:lightnet/config" {
|
|
2
|
+
const config: import("./config").PreparedLightnetConfig
|
|
3
|
+
export default config
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare module "virtual:lightnet/logo" {
|
|
7
|
+
const logo: ImageMetadata | undefined = import("astro").ImageMetadata
|
|
8
|
+
export default logo
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module "virtual:lightnet/project-context" {
|
|
12
|
+
const context: import("./project-context").ProjectContext
|
|
13
|
+
export default context
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
|
|
4
|
+
import type { AstroConfig, AstroIntegrationLogger, ViteUserConfig } from "astro"
|
|
5
|
+
|
|
6
|
+
import { verifySchema } from "../utils/verify-schema"
|
|
7
|
+
import { configSchema, type LightnetConfig } from "./config"
|
|
8
|
+
|
|
9
|
+
const CONFIG = "virtual:lightnet/config"
|
|
10
|
+
const LOGO = "virtual:lightnet/logo"
|
|
11
|
+
const PROJECT_CONTEXT = "virtual:lightnet/project-context"
|
|
12
|
+
|
|
13
|
+
const VIRTUAL_MODULES = [CONFIG, LOGO, PROJECT_CONTEXT] as const
|
|
14
|
+
|
|
15
|
+
export function vitePluginLightnetConfig(
|
|
16
|
+
lightnetConfig: LightnetConfig,
|
|
17
|
+
{ root, srcDir, site }: Pick<AstroConfig, "root" | "srcDir" | "site">,
|
|
18
|
+
logger: AstroIntegrationLogger,
|
|
19
|
+
): NonNullable<ViteUserConfig["plugins"]>[number] {
|
|
20
|
+
const resolveFilePath = (id: string) =>
|
|
21
|
+
JSON.stringify(id.startsWith(".") ? resolve(fileURLToPath(root), id) : id)
|
|
22
|
+
|
|
23
|
+
const config = verifySchema(
|
|
24
|
+
configSchema,
|
|
25
|
+
lightnetConfig,
|
|
26
|
+
"Invalid config passed to LightNet integration.",
|
|
27
|
+
)
|
|
28
|
+
return {
|
|
29
|
+
name: "vite-plugin-lightnet-config",
|
|
30
|
+
resolveId(id): string | undefined {
|
|
31
|
+
const module = VIRTUAL_MODULES.find((m) => m === id)
|
|
32
|
+
if (module) return `\0${module}`
|
|
33
|
+
},
|
|
34
|
+
handleHotUpdate({ file, server }) {
|
|
35
|
+
const srcPath = resolve(fileURLToPath(root), "src/translations/")
|
|
36
|
+
if (file.endsWith(".json") && file.startsWith(srcPath)) {
|
|
37
|
+
logger.info(`Update translations ${file.slice(srcPath.length)}`)
|
|
38
|
+
server.restart()
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
load(id): string | undefined {
|
|
42
|
+
const module = VIRTUAL_MODULES.find((m) => id === `\0${m}`)
|
|
43
|
+
switch (module) {
|
|
44
|
+
case CONFIG:
|
|
45
|
+
return `export default ${JSON.stringify(config)};`
|
|
46
|
+
case LOGO:
|
|
47
|
+
return config.logo
|
|
48
|
+
? `import logo from ${resolveFilePath(config.logo.src)}; export default logo;`
|
|
49
|
+
: "export default undefined;"
|
|
50
|
+
case PROJECT_CONTEXT:
|
|
51
|
+
return `export default ${JSON.stringify({ root, srcDir, site })}`
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCategories } from "../content/get-categories"
|
|
3
|
+
import { searchPagePath } from "../utils/paths"
|
|
4
|
+
import Section from "./Section.astro"
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
title?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { title } = Astro.props
|
|
11
|
+
const { t, currentLocale } = Astro.locals.i18n
|
|
12
|
+
|
|
13
|
+
const categories = await getCategories(currentLocale, t)
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
categories.length && (
|
|
18
|
+
<Section title={title ?? t("ln.common.categories")}>
|
|
19
|
+
<ul class="flex w-full flex-wrap gap-2 sm:gap-3">
|
|
20
|
+
{categories.map((category) => (
|
|
21
|
+
<li class="flex max-w-56 grow">
|
|
22
|
+
<a
|
|
23
|
+
class="flex h-12 w-full items-center justify-center rounded-xl bg-gray-200 p-2 px-8 shadow-sm hover:bg-gray-300 sm:h-14"
|
|
24
|
+
href={searchPagePath(currentLocale, {
|
|
25
|
+
category: category.id,
|
|
26
|
+
})}
|
|
27
|
+
>
|
|
28
|
+
<span class="line-clamp-2 block text-xs font-bold uppercase text-gray-600">
|
|
29
|
+
{category.name}
|
|
30
|
+
</span>
|
|
31
|
+
</a>
|
|
32
|
+
</li>
|
|
33
|
+
))}
|
|
34
|
+
</ul>
|
|
35
|
+
</Section>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ImageMetadata } from "astro"
|
|
3
|
+
import { Image } from "astro:assets"
|
|
4
|
+
|
|
5
|
+
import { getMediaTypes } from "../content/get-media-types"
|
|
6
|
+
import { detailsPagePath } from "../utils/paths"
|
|
7
|
+
import Icon from "./Icon"
|
|
8
|
+
|
|
9
|
+
type GalleryItem = {
|
|
10
|
+
id: string
|
|
11
|
+
data: {
|
|
12
|
+
title: string
|
|
13
|
+
type: { id: string }
|
|
14
|
+
image: ImageMetadata
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const t = Astro.locals.i18n.t
|
|
19
|
+
|
|
20
|
+
const types = Object.fromEntries(
|
|
21
|
+
(await getMediaTypes()).map((type) => [
|
|
22
|
+
type.id,
|
|
23
|
+
{ ...type.data, name: t(type.data.label, { allowFixedStrings: true }) },
|
|
24
|
+
]),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
items: GalleryItem[]
|
|
29
|
+
layout: "book" | "video" | "portrait" | "landscape"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { items, layout } = Astro.props
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
(layout === "book" || layout === "portrait") && (
|
|
37
|
+
<ol class="grid grid-cols-2 items-end justify-between gap-x-7 gap-y-4 sm:grid-cols-3 md:grid-cols-4 md:gap-8 lg:grid-cols-5">
|
|
38
|
+
{items.map((item) => (
|
|
39
|
+
<li>
|
|
40
|
+
<a
|
|
41
|
+
href={detailsPagePath(Astro.currentLocale, item)}
|
|
42
|
+
class="group flex flex-col gap-3"
|
|
43
|
+
>
|
|
44
|
+
<div
|
|
45
|
+
class="relative overflow-hidden shadow-md outline-2 outline-gray-400 transition-all duration-75 ease-in-out sm:group-hover:outline"
|
|
46
|
+
class:list={layout === "book" ? "rounded-sm" : "rounded-md"}
|
|
47
|
+
>
|
|
48
|
+
<Image
|
|
49
|
+
class="h-full w-full object-contain"
|
|
50
|
+
src={item.data.image}
|
|
51
|
+
alt=""
|
|
52
|
+
widths={[120, 160, 240, 320, 640]}
|
|
53
|
+
sizes={
|
|
54
|
+
"(max-width: 640px) calc(calc(100vw - 3.5rem ) / 2), " +
|
|
55
|
+
"(max-width: 768px) calc(calc(100vw - 5rem ) / 3), " +
|
|
56
|
+
"(max-width: 1024px) calc(calc(100vw - 10rem ) / 4), " +
|
|
57
|
+
"(max-width: 1280px) calc(calc(100vw - 12rem ) / 5), " +
|
|
58
|
+
"217px"
|
|
59
|
+
}
|
|
60
|
+
/>
|
|
61
|
+
{layout === "book" && (
|
|
62
|
+
<span class="absolute start-[3px] top-0 h-full w-[4px] bg-gradient-to-r from-gray-500/20 to-transparent" />
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
<span class="line-clamp-2 h-12 text-sm font-bold text-gray-700">
|
|
66
|
+
<Icon
|
|
67
|
+
className={`${types[item.data.type.id].icon} me-2 align-bottom`}
|
|
68
|
+
ariaLabel={types[item.data.type.id].name}
|
|
69
|
+
/>
|
|
70
|
+
{item.data.title}
|
|
71
|
+
</span>
|
|
72
|
+
</a>
|
|
73
|
+
</li>
|
|
74
|
+
))}
|
|
75
|
+
</ol>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
{
|
|
79
|
+
(layout === "video" || layout === "landscape") && (
|
|
80
|
+
<ol
|
|
81
|
+
class="grid grid-cols-1 justify-between gap-x-7 gap-y-4 sm:grid-cols-2 md:grid-cols-3 md:gap-8 lg:grid-cols-4 xl:grid-cols-4"
|
|
82
|
+
class:list={[layout === "landscape" && "items-end"]}
|
|
83
|
+
>
|
|
84
|
+
{items.map((item) => (
|
|
85
|
+
<li>
|
|
86
|
+
<a
|
|
87
|
+
href={detailsPagePath(Astro.currentLocale, item)}
|
|
88
|
+
class="group flex flex-col gap-3"
|
|
89
|
+
>
|
|
90
|
+
<div
|
|
91
|
+
class="relative overflow-hidden rounded-md shadow-md outline-2 outline-gray-400 transition-all duration-75 ease-in-out sm:group-hover:outline"
|
|
92
|
+
class:list={[layout === "video" && "aspect-video bg-gray-950"]}
|
|
93
|
+
>
|
|
94
|
+
<Image
|
|
95
|
+
class="h-full w-full object-contain"
|
|
96
|
+
class:list={[layout === "video" && "absolute top-0"]}
|
|
97
|
+
src={item.data.image}
|
|
98
|
+
alt=""
|
|
99
|
+
widths={[120, 160, 240, 320, 640]}
|
|
100
|
+
sizes={
|
|
101
|
+
"(max-width: 640px) calc(calc(100vw - 2rem ) / 1), " +
|
|
102
|
+
"(max-width: 768px) calc(calc(100vw - 3.5rem ) / 2), " +
|
|
103
|
+
"(max-width: 1024px) calc(calc(100vw - 8.5rem ) / 3), " +
|
|
104
|
+
"(max-width: 1280px) calc(calc(100vw - 10.5rem ) / 4), " +
|
|
105
|
+
"280px"
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
<span class="line-clamp-2 h-12 text-sm font-bold text-gray-700">
|
|
110
|
+
<Icon
|
|
111
|
+
className={`${types[item.data.type.id].icon} me-2 align-bottom`}
|
|
112
|
+
ariaLabel={types[item.data.type.id].name}
|
|
113
|
+
/>
|
|
114
|
+
{item.data.title}
|
|
115
|
+
</span>
|
|
116
|
+
</a>
|
|
117
|
+
</li>
|
|
118
|
+
))}
|
|
119
|
+
</ol>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ImageMetadata } from "astro"
|
|
3
|
+
import { Image } from "astro:assets"
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
image: ImageMetadata
|
|
7
|
+
title?: string
|
|
8
|
+
subtitle?: string
|
|
9
|
+
titleSize?: "sm" | "md" | "lg" | "xl"
|
|
10
|
+
titleClass?: string
|
|
11
|
+
subtitleSize?: "sm" | "md" | "lg" | "xl"
|
|
12
|
+
subtitleClass?: string
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
const {
|
|
16
|
+
image,
|
|
17
|
+
title,
|
|
18
|
+
subtitle,
|
|
19
|
+
titleSize = "md",
|
|
20
|
+
subtitleSize = "md",
|
|
21
|
+
titleClass,
|
|
22
|
+
subtitleClass,
|
|
23
|
+
className,
|
|
24
|
+
} = Astro.props
|
|
25
|
+
|
|
26
|
+
const titleSizes = {
|
|
27
|
+
sm: "text-3xl sm:text-4xl md:text-5xl",
|
|
28
|
+
md: "text-4xl sm:text-5xl md:text-6xl",
|
|
29
|
+
lg: "text-5xl sm:text-6xl md:text-7xl",
|
|
30
|
+
xl: "text-6xl sm:text-7xl md:text-8xl",
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
const subtitleSizes = {
|
|
34
|
+
sm: "text-sm sm:text-base md:text-lg",
|
|
35
|
+
md: "sm:text-lg md:text-2xl",
|
|
36
|
+
lg: "text-lg sm:text-xl md:text-3xl",
|
|
37
|
+
xl: "text-xl sm:text-2xl md:text-4xl",
|
|
38
|
+
} as const
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<div class="w-full">
|
|
42
|
+
<div class="group relative">
|
|
43
|
+
<Image
|
|
44
|
+
class="h-72 w-full object-cover object-center md:h-[20rem] lg:h-[24rem] xl:h-[30rem]"
|
|
45
|
+
src={image}
|
|
46
|
+
widths={[320, 768, 1280, 2560, 3000]}
|
|
47
|
+
sizes="100vw"
|
|
48
|
+
loading="eager"
|
|
49
|
+
alt=""
|
|
50
|
+
/>
|
|
51
|
+
<div
|
|
52
|
+
class="bg-gradient-radial absolute top-0 flex h-full w-full flex-col items-center justify-center gap-6 from-black/30 to-black/40 p-4 text-center text-gray-50"
|
|
53
|
+
class:list={[className]}
|
|
54
|
+
>
|
|
55
|
+
{
|
|
56
|
+
title && (
|
|
57
|
+
<h1
|
|
58
|
+
class="max-w-screen-md font-bold transition-transform duration-1000 group-hover:scale-[102%]"
|
|
59
|
+
class:list={[titleSizes[titleSize], titleClass]}
|
|
60
|
+
>
|
|
61
|
+
{title}
|
|
62
|
+
</h1>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
{
|
|
66
|
+
subtitle && (
|
|
67
|
+
<p
|
|
68
|
+
class="max-w-screen-sm font-bold"
|
|
69
|
+
class:list={[
|
|
70
|
+
"sm:text-lg md:text-2xl",
|
|
71
|
+
subtitleSizes[subtitleSize],
|
|
72
|
+
subtitleClass,
|
|
73
|
+
]}
|
|
74
|
+
>
|
|
75
|
+
{subtitle}
|
|
76
|
+
</p>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
<slot />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ImageMetadata } from "astro"
|
|
3
|
+
import { Image } from "astro:assets"
|
|
4
|
+
|
|
5
|
+
import Icon from "./Icon"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
image: ImageMetadata
|
|
9
|
+
id?: string
|
|
10
|
+
title?: string
|
|
11
|
+
text?: string
|
|
12
|
+
link?: { href: string; text: string }
|
|
13
|
+
className?: string
|
|
14
|
+
titleClass?: string
|
|
15
|
+
textClass?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { image, id, title, text, link, className, titleClass, textClass } =
|
|
19
|
+
Astro.props
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<section
|
|
23
|
+
class="mt-24 w-full bg-gray-200 md:mt-28"
|
|
24
|
+
class:list={[className]}
|
|
25
|
+
id={id}
|
|
26
|
+
>
|
|
27
|
+
<div class="flex flex-col overflow-hidden md:flex-row">
|
|
28
|
+
<Image
|
|
29
|
+
src={image}
|
|
30
|
+
alt=""
|
|
31
|
+
widths={[320, 640, 768, 1024, 1280, 2560]}
|
|
32
|
+
sizes="(max-width: 768px) 100vw, 50vw"
|
|
33
|
+
class="aspect-[4/3] w-full shrink-0 object-cover md:w-1/2 xl:aspect-video"
|
|
34
|
+
/>
|
|
35
|
+
<div class="my-16 max-w-screen-sm px-4 md:px-8 lg:my-24 xl:px-16">
|
|
36
|
+
{
|
|
37
|
+
title && (
|
|
38
|
+
<h2
|
|
39
|
+
class="mb-4 text-2xl font-bold sm:mb-8 sm:text-3xl"
|
|
40
|
+
class:list={titleClass}
|
|
41
|
+
>
|
|
42
|
+
{title}
|
|
43
|
+
</h2>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
{
|
|
47
|
+
text && (
|
|
48
|
+
<p class="mb-10 text-lg sm:mb-12" class:list={textClass}>
|
|
49
|
+
{text}
|
|
50
|
+
</p>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
{
|
|
54
|
+
link && (
|
|
55
|
+
<a
|
|
56
|
+
class="bg-primary hover:bg-primary/85 inline-flex items-center justify-center gap-2 rounded-2xl px-6 py-3 text-sm font-bold uppercase text-gray-50 shadow-sm hover:text-gray-100"
|
|
57
|
+
href={link.href}
|
|
58
|
+
>
|
|
59
|
+
{link.text}
|
|
60
|
+
<Icon
|
|
61
|
+
flipIcon={Astro.locals.i18n.direction === "rtl"}
|
|
62
|
+
className="mdi--arrow-right"
|
|
63
|
+
ariaLabel=""
|
|
64
|
+
/>
|
|
65
|
+
</a>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
<slot />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon Component.
|
|
3
|
+
*
|
|
4
|
+
* @param className containing a material design icon name prefixed with 'mdi--'
|
|
5
|
+
* @param ariaLabel accessibility label to be added e.g. for people using a screen reader. Empty string will hide the icon from a screen reader.
|
|
6
|
+
* @param flipIcon if set to true this will mirror the icon along its x-axis. Useful for RTL layouts.
|
|
7
|
+
* @see https://pictogrammers.com/library/mdi/ for available icons
|
|
8
|
+
* @returns icon
|
|
9
|
+
*/
|
|
10
|
+
export default function Icon({
|
|
11
|
+
className,
|
|
12
|
+
ariaLabel,
|
|
13
|
+
flipIcon,
|
|
14
|
+
}: {
|
|
15
|
+
className: string
|
|
16
|
+
ariaLabel: string
|
|
17
|
+
flipIcon?: boolean
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<span
|
|
21
|
+
className={`iconify text-2xl ${className} ${flipIcon && "scale-x-[-1]"}`}
|
|
22
|
+
aria-label={ariaLabel || undefined}
|
|
23
|
+
hidden={!ariaLabel}
|
|
24
|
+
role={ariaLabel ? "img" : undefined}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|