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.
Files changed (122) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1 -0
  4. package/__e2e__/detailPage.spec.ts +0 -0
  5. package/__e2e__/fixtures/basics/astro.config.mjs +38 -0
  6. package/__e2e__/fixtures/basics/node_modules/.bin/astro +17 -0
  7. package/__e2e__/fixtures/basics/node_modules/.bin/astro-check +17 -0
  8. package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +17 -0
  9. package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +17 -0
  10. package/__e2e__/fixtures/basics/node_modules/.bin/tsc +17 -0
  11. package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +17 -0
  12. package/__e2e__/fixtures/basics/package.json +19 -0
  13. package/__e2e__/fixtures/basics/public/favicon.svg +1 -0
  14. package/__e2e__/fixtures/basics/public/files/example.pdf +0 -0
  15. package/__e2e__/fixtures/basics/src/assets/logo.png +0 -0
  16. package/__e2e__/fixtures/basics/src/content/categories/christian-living.json +3 -0
  17. package/__e2e__/fixtures/basics/src/content/categories/teens.json +3 -0
  18. package/__e2e__/fixtures/basics/src/content/categories/theology.json +3 -0
  19. package/__e2e__/fixtures/basics/src/content/media/faithful-freestyle--en.json +13 -0
  20. package/__e2e__/fixtures/basics/src/content/media/how-to-kickflip--de.json +12 -0
  21. package/__e2e__/fixtures/basics/src/content/media/images/cover.jpg +0 -0
  22. package/__e2e__/fixtures/basics/src/content/media/images/how-to-kickflip--en.webp +0 -0
  23. package/__e2e__/fixtures/basics/src/content/media-collections/how-to-articles.json +3 -0
  24. package/__e2e__/fixtures/basics/src/content/media-types/book.json +9 -0
  25. package/__e2e__/fixtures/basics/src/content/media-types/video.json +7 -0
  26. package/__e2e__/fixtures/basics/src/content.config.ts +3 -0
  27. package/__e2e__/fixtures/basics/src/pages/[locale]/index.astro +14 -0
  28. package/__e2e__/fixtures/basics/src/translations/de.json +10 -0
  29. package/__e2e__/fixtures/basics/src/translations/en.json +10 -0
  30. package/__e2e__/fixtures/basics/tailwind.config.mjs +8 -0
  31. package/__e2e__/homepage.spec.ts +108 -0
  32. package/__e2e__/search.spec.ts +16 -0
  33. package/__e2e__/test-utils.ts +80 -0
  34. package/__tests__/pages/details-page/create-content-metadata.spec.ts +104 -0
  35. package/__tests__/utils/markdown.spec.ts +33 -0
  36. package/exports/components.ts +9 -0
  37. package/exports/content.ts +8 -0
  38. package/exports/details-page.ts +1 -0
  39. package/exports/i18n.ts +2 -0
  40. package/exports/index.ts +6 -0
  41. package/exports/utils.ts +2 -0
  42. package/package.json +54 -0
  43. package/playwright.config.ts +30 -0
  44. package/src/astro-integration/config.ts +185 -0
  45. package/src/astro-integration/integration.ts +74 -0
  46. package/src/astro-integration/project-context.ts +5 -0
  47. package/src/astro-integration/virtual.d.ts +14 -0
  48. package/src/astro-integration/vite-plugin-lightnet-config.ts +55 -0
  49. package/src/components/CategoriesOverview.astro +37 -0
  50. package/src/components/Gallery.astro +121 -0
  51. package/src/components/Hero.astro +82 -0
  52. package/src/components/HighlightSection.astro +71 -0
  53. package/src/components/Icon.tsx +27 -0
  54. package/src/components/MediaItemList.astro +84 -0
  55. package/src/components/Section.astro +49 -0
  56. package/src/content/astro-image.ts +14 -0
  57. package/src/content/content-schema-internal.ts +52 -0
  58. package/src/content/content-schema.ts +263 -0
  59. package/src/content/external-api.ts +7 -0
  60. package/src/content/get-categories.ts +15 -0
  61. package/src/content/get-languages.ts +14 -0
  62. package/src/content/get-media-items.ts +27 -0
  63. package/src/content/get-media-types.ts +23 -0
  64. package/src/content/query-media-items.ts +89 -0
  65. package/src/content/resolve-category-label.ts +20 -0
  66. package/src/i18n/get-locale-paths.ts +8 -0
  67. package/src/i18n/languages.ts +10 -0
  68. package/src/i18n/locals.d.ts +38 -0
  69. package/src/i18n/locals.ts +28 -0
  70. package/src/i18n/resolve-default-locale.ts +30 -0
  71. package/src/i18n/resolve-language.ts +25 -0
  72. package/src/i18n/resolve-locales.ts +5 -0
  73. package/src/i18n/translate.ts +64 -0
  74. package/src/i18n/translations/de.json +25 -0
  75. package/src/i18n/translations/en.json +25 -0
  76. package/src/layouts/MarkdownPage.astro +11 -0
  77. package/src/layouts/Page.astro +54 -0
  78. package/src/layouts/components/Favicon.astro +32 -0
  79. package/src/layouts/components/LanguagePicker.astro +38 -0
  80. package/src/layouts/components/Menu.astro +28 -0
  81. package/src/layouts/components/MenuItem.astro +21 -0
  82. package/src/layouts/components/PageNavigation.astro +65 -0
  83. package/src/layouts/components/PageTitle.astro +44 -0
  84. package/src/layouts/components/PreloadReact.tsx +3 -0
  85. package/src/pages/404.astro +14 -0
  86. package/src/pages/RedirectToDefaultLocale.astro +3 -0
  87. package/src/pages/api/search-response.ts +14 -0
  88. package/src/pages/api/search.ts +47 -0
  89. package/src/pages/details-page/DefaultDetails.astro +44 -0
  90. package/src/pages/details-page/DetailsPage.astro +53 -0
  91. package/src/pages/details-page/VideoDetails.astro +43 -0
  92. package/src/pages/details-page/components/Authors.astro +19 -0
  93. package/src/pages/details-page/components/Content.astro +73 -0
  94. package/src/pages/details-page/components/Cover.astro +35 -0
  95. package/src/pages/details-page/components/Description.astro +26 -0
  96. package/src/pages/details-page/components/MediaCollection.astro +39 -0
  97. package/src/pages/details-page/components/MediaCollections.astro +21 -0
  98. package/src/pages/details-page/components/OpenButton.astro +34 -0
  99. package/src/pages/details-page/components/SectionTitle.astro +8 -0
  100. package/src/pages/details-page/components/ShareButton.astro +58 -0
  101. package/src/pages/details-page/components/Title.astro +18 -0
  102. package/src/pages/details-page/components/VideoPlayer.astro +78 -0
  103. package/src/pages/details-page/components/details/Categories.astro +31 -0
  104. package/src/pages/details-page/components/details/Details.astro +17 -0
  105. package/src/pages/details-page/components/details/Label.astro +3 -0
  106. package/src/pages/details-page/components/details/Languages.astro +46 -0
  107. package/src/pages/details-page/utils/create-content-metadata.ts +78 -0
  108. package/src/pages/search-page/Search.tsx +71 -0
  109. package/src/pages/search-page/SearchPage.astro +51 -0
  110. package/src/pages/search-page/components/ResultList.tsx +135 -0
  111. package/src/pages/search-page/components/SearchFilter.tsx +189 -0
  112. package/src/pages/search-page/hooks/use-debounce.ts +17 -0
  113. package/src/pages/search-page/hooks/use-search.ts +95 -0
  114. package/src/pages/search-page/types.ts +9 -0
  115. package/src/pages/search-page/utils/search-translations.ts +22 -0
  116. package/src/pages/search-page/utils/use-provided-translations.ts +5 -0
  117. package/src/utils/markdown.ts +41 -0
  118. package/src/utils/paths.ts +45 -0
  119. package/src/utils/urls.ts +29 -0
  120. package/src/utils/verify-schema.ts +38 -0
  121. package/tailwind.config.ts +56 -0
  122. 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,5 @@
1
+ export type ProjectContext = {
2
+ root: URL
3
+ srcDir: URL
4
+ site?: string
5
+ }
@@ -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
+ }