lightnet 3.8.1 → 3.9.0
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 +41 -0
- package/README.md +14 -22
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +2 -2
- package/package.json +7 -8
- package/src/components/CarouselSection.astro +9 -2
- package/src/components/CategoriesSection.astro +10 -6
- package/src/components/CoverImageDecorator.tsx +29 -0
- package/src/components/MediaGallerySection.astro +151 -6
- package/src/components/MediaList.astro +17 -7
- package/src/components/SearchInput.astro +1 -1
- package/src/content/content-schema.ts +105 -74
- package/src/i18n/translations/TRANSLATION-STATUS.md +7 -0
- package/src/i18n/translations/kk.yml +22 -0
- package/src/i18n/translations.ts +1 -0
- package/src/layouts/components/Header.astro +1 -1
- package/src/pages/details-page/AudioDetailsPage.astro +1 -1
- package/src/pages/details-page/DefaultDetailsPage.astro +2 -3
- package/src/pages/details-page/components/MainDetailsSection.astro +4 -5
- package/src/pages/details-page/components/MediaCollection.astro +1 -1
- package/src/pages/details-page/components/main-details/{Thumbnail.astro → CoverImage.astro} +6 -12
- package/src/pages/search-page/components/SearchList.astro +1 -0
- package/src/pages/search-page/components/SearchListItem.tsx +14 -9
- package/src/components/MediaGallery.astro +0 -124
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# lightnet
|
|
2
2
|
|
|
3
|
+
## 3.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#309](https://github.com/LightNetDev/LightNet/pull/309) [`5ea2c13`](https://github.com/LightNetDev/LightNet/commit/5ea2c139e52a20b951643530dd0a57e50ed04ad1) Thanks [@smn-cds](https://github.com/smn-cds)! - Media type config: Added `coverImageStyle`
|
|
8
|
+
- Introduced a new `coverImageStyle` option for media type configuration.
|
|
9
|
+
- Controls how cover images are rendered for media items.
|
|
10
|
+
|
|
11
|
+
Supported values:
|
|
12
|
+
- `"default"` — unmodified media item image (default)
|
|
13
|
+
- `"book"` — styled as a book cover (book fold, sharper edges)
|
|
14
|
+
- `"video"` — forced 16:9 aspect ratio, ⚠️ removed filling up with a black background but scale the image to cover the whole cover area.
|
|
15
|
+
|
|
16
|
+
#### Deprecation Notice
|
|
17
|
+
|
|
18
|
+
The existing `detailsPage.coverStyle` option is now deprecated and will be removed in a future major release. Use `coverImageStyle` instead.
|
|
19
|
+
|
|
20
|
+
- [#309](https://github.com/LightNetDev/LightNet/pull/309) [`5ea2c13`](https://github.com/LightNetDev/LightNet/commit/5ea2c139e52a20b951643530dd0a57e50ed04ad1) Thanks [@smn-cds](https://github.com/smn-cds)! - `MediaGallerySection`: Added `carousel` layout option
|
|
21
|
+
- Introduced a new `viewLayout` prop for `MediaGallerySection`.
|
|
22
|
+
- Supported values:
|
|
23
|
+
- `"grid"` (default; existing behavior)
|
|
24
|
+
- `"carousel"` (new; renders items in a horizontal carousel)
|
|
25
|
+
- This change is **backwards-compatible**. Existing usages without `viewLayout` will continue to render as a grid.
|
|
26
|
+
|
|
27
|
+
### Example
|
|
28
|
+
|
|
29
|
+
```astro
|
|
30
|
+
<MediaGallerySection
|
|
31
|
+
title={t("x.home.our-latest-books")}
|
|
32
|
+
items={latestBooks}
|
|
33
|
+
layout="book"
|
|
34
|
+
viewLayout="carousel"
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- [#309](https://github.com/LightNetDev/LightNet/pull/309) [`5ea2c13`](https://github.com/LightNetDev/LightNet/commit/5ea2c139e52a20b951643530dd0a57e50ed04ad1) Thanks [@smn-cds](https://github.com/smn-cds)! - Added Kazakh language.
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- [#309](https://github.com/LightNetDev/LightNet/pull/309) [`5ea2c13`](https://github.com/LightNetDev/LightNet/commit/5ea2c139e52a20b951643530dd0a57e50ed04ad1) Thanks [@smn-cds](https://github.com/smn-cds)! - Improved page styling: shadows, spacing
|
|
43
|
+
|
|
3
44
|
## 3.8.1
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,37 +1,29 @@
|
|
|
1
1
|

|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Many ministries struggle to share media content effectively. LightNet makes it easy to **run your own digital media library**, so more people can find what they need and grow in faith.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Built as an integration for the [Astro framework](https://astro.build), LightNet enables the creation of fast, statically generated websites that can be easily hosted on any file server.
|
|
5
|
+
Built as an integration for the [Astro framework](https://astro.build), LightNet creates fast, static sites that run anywhere.
|
|
8
6
|
|
|
9
7
|
Learn more on the [LightNet homepage](https://lightnet.community).
|
|
10
8
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
Get up and running quickly with LightNet by exploring its features and best practices. You can build your own media library by starting with the [LightNet example template](https://github.com/LightNetDev/example-template), which creates a local copy of a demo site for a fictional skateboard ministry. This beginner-friendly template serves as a great starting point for developers.
|
|
14
|
-
|
|
15
|
-
To get started, simply run the following command in your terminal:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm create astro@latest -- --template LightNetDev/example-template
|
|
19
|
-
```
|
|
9
|
+
## 🚀 Launch your own library
|
|
20
10
|
|
|
21
|
-
|
|
11
|
+
Quickly launch your own media library with LightNet. [Follow the getting started guide](https://docs.lightnet.community/start-here/getting-started/) to spin up a template, then customize and expand it to fit your community’s needs.
|
|
22
12
|
|
|
23
|
-
##
|
|
13
|
+
## 🌍 Real-world example
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
Curious what LightNet looks like in action? [Check out the MediaWorks digital library](https://library.mediaworks.global)
|
|
16
|
+
to see how it powers a live site.
|
|
26
17
|
|
|
27
|
-
##
|
|
18
|
+
## 👀 Want to learn more?
|
|
28
19
|
|
|
29
|
-
|
|
20
|
+
Take a look at the [LightNet developer docs](https://docs.lightnet.community).
|
|
30
21
|
|
|
31
|
-
## Contributing
|
|
22
|
+
## 🤝 Contributing
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
Got ideas or improvements? We’d love your help! [See the contribution guide](https://github.com/LightNetDev/lightnet/blob/main/CONTRIBUTING.md) to get started.
|
|
34
25
|
|
|
35
|
-
## License
|
|
26
|
+
## 📄 License
|
|
36
27
|
|
|
37
|
-
LightNet
|
|
28
|
+
LightNet uses the MIT License. You can read the full details in the [LICENSE](https://github.com/LightNetDev/lightnet/blob/main/LICENSE)
|
|
29
|
+
file.
|
|
@@ -10,9 +10,9 @@ case `uname` in
|
|
|
10
10
|
esac
|
|
11
11
|
|
|
12
12
|
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.
|
|
13
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.13.3_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.48.1_terser@5.39.0_typescript@5.9.2_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.13.3_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.48.1_terser@5.39.0_typescript@5.9.2_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
|
|
14
14
|
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.
|
|
15
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.13.3_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.48.1_terser@5.39.0_typescript@5.9.2_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.13.3_@types+node@24.3.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.48.1_terser@5.39.0_typescript@5.9.2_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
16
|
fi
|
|
17
17
|
if [ -x "$basedir/node" ]; then
|
|
18
18
|
exec "$basedir/node" "$basedir/../astro/astro.js" "$@"
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"@astrojs/react": "^4.3.0",
|
|
8
8
|
"@astrojs/tailwind": "^6.0.2",
|
|
9
9
|
"@lightnet/decap-admin": "^3.1.2",
|
|
10
|
-
"astro": "^5.
|
|
11
|
-
"lightnet": "^3.
|
|
10
|
+
"astro": "^5.13.3",
|
|
11
|
+
"lightnet": "^3.9.0",
|
|
12
12
|
"react": "^19.1.1",
|
|
13
13
|
"react-dom": "^19.1.1",
|
|
14
14
|
"sharp": "^0.34.3",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "lightnet",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.9.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/LightNetDev/lightnet",
|
|
@@ -48,17 +48,16 @@
|
|
|
48
48
|
"@tanstack/react-virtual": "^3.13.12",
|
|
49
49
|
"daisyui": "^4.12.24",
|
|
50
50
|
"embla-carousel": "^8.6.0",
|
|
51
|
-
"embla-carousel-
|
|
52
|
-
"embla-carousel-wheel-gestures": "^8.0.2",
|
|
51
|
+
"embla-carousel-wheel-gestures": "^8.1.0",
|
|
53
52
|
"fuse.js": "^7.1.0",
|
|
54
|
-
"i18next": "^25.
|
|
55
|
-
"marked": "^16.
|
|
53
|
+
"i18next": "^25.4.2",
|
|
54
|
+
"marked": "^16.2.0",
|
|
56
55
|
"yaml": "^2.8.1"
|
|
57
56
|
},
|
|
58
57
|
"devDependencies": {
|
|
59
|
-
"@playwright/test": "^1.
|
|
60
|
-
"@types/node": "^22.
|
|
61
|
-
"@types/react": "^19.1.
|
|
58
|
+
"@playwright/test": "^1.55.0",
|
|
59
|
+
"@types/node": "^22.18.0",
|
|
60
|
+
"@types/react": "^19.1.11",
|
|
62
61
|
"typescript": "^5.9.2",
|
|
63
62
|
"vitest": "^3.2.4"
|
|
64
63
|
},
|
|
@@ -47,7 +47,6 @@ const { t, direction } = Astro.locals.i18n
|
|
|
47
47
|
|
|
48
48
|
<script>
|
|
49
49
|
import EmblaCarousel from "embla-carousel"
|
|
50
|
-
import AutoHeightPlugin from "embla-carousel-auto-height"
|
|
51
50
|
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures"
|
|
52
51
|
|
|
53
52
|
class Carousel extends HTMLElement {
|
|
@@ -65,7 +64,7 @@ const { t, direction } = Astro.locals.i18n
|
|
|
65
64
|
skipSnaps: true,
|
|
66
65
|
direction,
|
|
67
66
|
},
|
|
68
|
-
[
|
|
67
|
+
[WheelGesturesPlugin({ forceWheelAxis: "x" })],
|
|
69
68
|
)
|
|
70
69
|
const prevBtn = this.querySelector(
|
|
71
70
|
"[data-button-prev]",
|
|
@@ -78,6 +77,14 @@ const { t, direction } = Astro.locals.i18n
|
|
|
78
77
|
nextBtn.addEventListener("click", () => carousel.scrollNext())
|
|
79
78
|
|
|
80
79
|
const updateArrowButtons = () => {
|
|
80
|
+
if (!carousel.canScrollPrev() && !carousel.canScrollNext()) {
|
|
81
|
+
prevBtn.style.visibility = "hidden"
|
|
82
|
+
nextBtn.style.visibility = "hidden"
|
|
83
|
+
} else {
|
|
84
|
+
prevBtn.style.visibility = "visible"
|
|
85
|
+
nextBtn.style.visibility = "visible"
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
if (carousel.canScrollPrev()) {
|
|
82
89
|
prevBtn.removeAttribute("disabled")
|
|
83
90
|
} else {
|
|
@@ -40,7 +40,7 @@ function getImage({ image, id }: Category) {
|
|
|
40
40
|
class="group flex h-full flex-col gap-3"
|
|
41
41
|
>
|
|
42
42
|
<div
|
|
43
|
-
class="relative overflow-hidden rounded-2xl shadow-
|
|
43
|
+
class="relative overflow-hidden rounded-2xl shadow-sm outline-2 outline-gray-400 transition-colors duration-75 ease-in-out sm:group-hover:outline"
|
|
44
44
|
class:list={[
|
|
45
45
|
!category.image && "h-full min-h-28 w-full bg-gray-300",
|
|
46
46
|
]}
|
|
@@ -52,8 +52,8 @@ function getImage({ image, id }: Category) {
|
|
|
52
52
|
alt=""
|
|
53
53
|
widths={[128, 256, 388, 512, 768]}
|
|
54
54
|
sizes={
|
|
55
|
-
"(max-width: 640px) calc(calc(100vw -
|
|
56
|
-
"(max-width: 768px) calc(calc(100vw -
|
|
55
|
+
"(max-width: 640px) calc(calc(100vw - 3rem ) / 2), " +
|
|
56
|
+
"(max-width: 768px) calc(calc(100vw - 4rem ) / 3), " +
|
|
57
57
|
"(max-width: 1024px) calc(calc(100vw - 10rem ) / 4), " +
|
|
58
58
|
"(max-width: 1280px) calc(calc(100vw - 12rem ) / 5), " +
|
|
59
59
|
"217px"
|
|
@@ -86,18 +86,22 @@ function getImage({ image, id }: Category) {
|
|
|
86
86
|
<CarouselSection {...props} title={resolvedTitle}>
|
|
87
87
|
{categories.map((category) => (
|
|
88
88
|
<li
|
|
89
|
-
class="carousel-item--narrow
|
|
89
|
+
class="carousel-item--narrow"
|
|
90
90
|
role="group"
|
|
91
91
|
aria-roledescription="slide"
|
|
92
|
+
class:list={[
|
|
93
|
+
/** Category cards without images should take the full height of other cards */
|
|
94
|
+
category.image ? "h-full" : "self-stretch",
|
|
95
|
+
]}
|
|
92
96
|
>
|
|
93
97
|
<a
|
|
94
98
|
href={searchPagePath(currentLocale, { category: category.id })}
|
|
95
99
|
class="group flex h-full flex-col gap-3"
|
|
96
100
|
>
|
|
97
101
|
<div
|
|
98
|
-
class="relative overflow-hidden rounded-2xl shadow-
|
|
102
|
+
class="relative overflow-hidden rounded-2xl shadow-sm outline-2 outline-gray-400 transition-all duration-75 ease-in-out sm:group-hover:outline"
|
|
99
103
|
class:list={[
|
|
100
|
-
!category.image && "
|
|
104
|
+
!category.image && "min-h-28 w-full flex-1 bg-gray-300",
|
|
101
105
|
]}
|
|
102
106
|
>
|
|
103
107
|
{category.image && (
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
type ImageStyle = "default" | "book" | "video"
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
children: ReactNode
|
|
7
|
+
style?: ImageStyle
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const containerClass: { [key in ImageStyle]: string } = {
|
|
12
|
+
default: "rounded-md",
|
|
13
|
+
book: "rounded-sm",
|
|
14
|
+
video: "rounded-md aspect-video",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function CoverImageDecorator(props: Props) {
|
|
18
|
+
const { children, style = "default", className = "" } = props
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={`relative overflow-hidden ${containerClass[style]} shadow-sm ${className}`}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
{style === "book" && (
|
|
25
|
+
<span className="absolute start-[1%] top-0 h-full w-[2%] bg-gradient-to-r from-gray-500/20 to-transparent" />
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { ImageMetadata } from "astro"
|
|
3
|
+
import { Image } from "astro:assets"
|
|
3
4
|
|
|
4
|
-
import
|
|
5
|
+
import { getMediaTypes } from "../content/get-media-types"
|
|
6
|
+
import { detailsPagePath } from "../utils/paths"
|
|
7
|
+
import CarouselSection from "./CarouselSection.astro"
|
|
8
|
+
import CoverImageDecorator from "./CoverImageDecorator"
|
|
9
|
+
import Icon from "./Icon"
|
|
5
10
|
import Section, { type Props as SectionProps } from "./Section.astro"
|
|
6
11
|
|
|
12
|
+
type ItemStyle = "book" | "video" | "portrait" | "landscape"
|
|
13
|
+
|
|
7
14
|
type MediaItem = {
|
|
8
15
|
id: string
|
|
9
16
|
data: {
|
|
@@ -15,12 +22,150 @@ type MediaItem = {
|
|
|
15
22
|
|
|
16
23
|
type Props = SectionProps & {
|
|
17
24
|
items: (MediaItem | undefined)[]
|
|
18
|
-
layout:
|
|
25
|
+
layout: ItemStyle
|
|
26
|
+
viewLayout?: "grid" | "carousel"
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
const {
|
|
29
|
+
const {
|
|
30
|
+
items: itemsInput,
|
|
31
|
+
layout: itemStyle,
|
|
32
|
+
viewLayout = "grid",
|
|
33
|
+
...sectionProps
|
|
34
|
+
} = Astro.props
|
|
35
|
+
|
|
36
|
+
const t = Astro.locals.i18n.t
|
|
37
|
+
|
|
38
|
+
const types = Object.fromEntries(
|
|
39
|
+
(await getMediaTypes()).map((type) => [
|
|
40
|
+
type.id,
|
|
41
|
+
{ ...type.data, name: t(type.data.label) },
|
|
42
|
+
]),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// We allow for undefined values because
|
|
46
|
+
// this is in the return type of getEntry
|
|
47
|
+
const items = itemsInput.filter((item) => !!item)
|
|
48
|
+
|
|
49
|
+
const itemWidth = (
|
|
50
|
+
{
|
|
51
|
+
book: "narrow",
|
|
52
|
+
portrait: "narrow",
|
|
53
|
+
video: "wide",
|
|
54
|
+
landscape: "wide",
|
|
55
|
+
} as const
|
|
56
|
+
)[itemStyle]
|
|
57
|
+
|
|
58
|
+
const imageSizes = {
|
|
59
|
+
narrow:
|
|
60
|
+
// 2 columns
|
|
61
|
+
"(max-width: 640px) calc(calc(100vw - 3rem ) / 2), " +
|
|
62
|
+
// 3 columns
|
|
63
|
+
"(max-width: 768px) calc(calc(100vw - 4rem ) / 3), " +
|
|
64
|
+
// 4 columns
|
|
65
|
+
"(max-width: 1024px) calc(calc(100vw - 10rem ) / 4), " +
|
|
66
|
+
// 5 columns
|
|
67
|
+
"(max-width: 1280px) calc(calc(100vw - 12rem ) / 5), " +
|
|
68
|
+
// 5 columns - full size
|
|
69
|
+
"217px",
|
|
70
|
+
wide:
|
|
71
|
+
// 1 column
|
|
72
|
+
"(max-width: 640px) calc(calc(100vw - 2rem ) / 1), " +
|
|
73
|
+
// 2 columns
|
|
74
|
+
"(max-width: 768px) calc(calc(100vw - 3rem ) / 2), " +
|
|
75
|
+
// 3 columns
|
|
76
|
+
"(max-width: 1024px) calc(calc(100vw - 8rem ) / 3), " +
|
|
77
|
+
// 4 columns
|
|
78
|
+
"(max-width: 1280px) calc(calc(100vw - 10rem ) / 4), " +
|
|
79
|
+
// 5 columns - full size
|
|
80
|
+
"280px",
|
|
81
|
+
} as const
|
|
82
|
+
|
|
83
|
+
const gridLayouts = {
|
|
84
|
+
narrow: "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5",
|
|
85
|
+
wide: "grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
|
|
86
|
+
} as const
|
|
87
|
+
|
|
88
|
+
const coverImageStyle =
|
|
89
|
+
(["book", "video"] as const).find((style) => itemStyle === style) ?? "default"
|
|
22
90
|
---
|
|
23
91
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
92
|
+
{
|
|
93
|
+
viewLayout === "grid" && (
|
|
94
|
+
<Section {...sectionProps}>
|
|
95
|
+
<ol
|
|
96
|
+
class="grid items-end justify-between gap-4 md:gap-8"
|
|
97
|
+
class:list={gridLayouts[itemWidth]}
|
|
98
|
+
>
|
|
99
|
+
{items.map((item) => (
|
|
100
|
+
<li>
|
|
101
|
+
<a
|
|
102
|
+
href={detailsPagePath(Astro.currentLocale, item)}
|
|
103
|
+
class="group flex flex-col gap-3"
|
|
104
|
+
>
|
|
105
|
+
<CoverImageDecorator
|
|
106
|
+
style={coverImageStyle}
|
|
107
|
+
className="outline-2 outline-gray-400 transition-all duration-75 ease-in-out sm:group-hover:outline"
|
|
108
|
+
>
|
|
109
|
+
<Image
|
|
110
|
+
class="h-full w-full object-cover"
|
|
111
|
+
class:list={[itemStyle === "video" && "absolute top-0"]}
|
|
112
|
+
src={item.data.image}
|
|
113
|
+
alt=""
|
|
114
|
+
widths={[128, 256, 512, 768]}
|
|
115
|
+
sizes={imageSizes[itemWidth]}
|
|
116
|
+
/>
|
|
117
|
+
</CoverImageDecorator>
|
|
118
|
+
|
|
119
|
+
<span class="line-clamp-2 h-12 text-balance text-sm font-bold text-gray-700">
|
|
120
|
+
<Icon
|
|
121
|
+
className={`${types[item.data.type.id].icon} me-2 align-bottom`}
|
|
122
|
+
ariaLabel={types[item.data.type.id].name}
|
|
123
|
+
/>
|
|
124
|
+
{item.data.title}
|
|
125
|
+
</span>
|
|
126
|
+
</a>
|
|
127
|
+
</li>
|
|
128
|
+
))}
|
|
129
|
+
</ol>
|
|
130
|
+
</Section>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
{
|
|
134
|
+
viewLayout === "carousel" && (
|
|
135
|
+
<CarouselSection {...sectionProps}>
|
|
136
|
+
{items.map((item) => (
|
|
137
|
+
<li
|
|
138
|
+
class={`carousel-item--${itemWidth} h-full`}
|
|
139
|
+
role="group"
|
|
140
|
+
aria-roledescription="slide"
|
|
141
|
+
>
|
|
142
|
+
<a
|
|
143
|
+
href={detailsPagePath(Astro.currentLocale, item)}
|
|
144
|
+
class="group flex h-full flex-col justify-end gap-3"
|
|
145
|
+
>
|
|
146
|
+
<CoverImageDecorator
|
|
147
|
+
style={coverImageStyle}
|
|
148
|
+
className="outline-2 outline-gray-400 transition-all duration-75 ease-in-out sm:group-hover:outline"
|
|
149
|
+
>
|
|
150
|
+
<Image
|
|
151
|
+
class="h-full w-full object-cover"
|
|
152
|
+
src={item.data.image}
|
|
153
|
+
alt=""
|
|
154
|
+
widths={[128, 256, 512, 768]}
|
|
155
|
+
sizes={imageSizes[itemWidth]}
|
|
156
|
+
/>
|
|
157
|
+
</CoverImageDecorator>
|
|
158
|
+
|
|
159
|
+
<span class="line-clamp-2 h-12 text-balance text-sm font-bold text-gray-700">
|
|
160
|
+
<Icon
|
|
161
|
+
className={`${types[item.data.type.id].icon} me-2 align-bottom`}
|
|
162
|
+
ariaLabel={types[item.data.type.id].name}
|
|
163
|
+
/>
|
|
164
|
+
{item.data.title}
|
|
165
|
+
</span>
|
|
166
|
+
</a>
|
|
167
|
+
</li>
|
|
168
|
+
))}
|
|
169
|
+
</CarouselSection>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
@@ -4,6 +4,7 @@ import { Image } from "astro:assets"
|
|
|
4
4
|
|
|
5
5
|
import { getMediaTypes } from "../content/get-media-types"
|
|
6
6
|
import { detailsPagePath } from "../utils/paths"
|
|
7
|
+
import CoverImageDecorator from "./CoverImageDecorator"
|
|
7
8
|
import Icon from "./Icon"
|
|
8
9
|
|
|
9
10
|
type MediaItem = {
|
|
@@ -35,6 +36,7 @@ const mediaTypes = Object.fromEntries(
|
|
|
35
36
|
id: type.id,
|
|
36
37
|
name: t(type.data.label),
|
|
37
38
|
icon: type.data.icon,
|
|
39
|
+
coverImageStyle: type.data.coverImageStyle,
|
|
38
40
|
},
|
|
39
41
|
]),
|
|
40
42
|
)
|
|
@@ -59,16 +61,24 @@ const mediaTypes = Object.fromEntries(
|
|
|
59
61
|
tabindex={item.disabled ? -1 : 0}
|
|
60
62
|
>
|
|
61
63
|
<div class="flex h-32 w-32 shrink-0 flex-col items-start justify-center">
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
<CoverImageDecorator
|
|
65
|
+
style={mediaTypes[item.data.type.id].coverImageStyle}
|
|
66
|
+
>
|
|
67
|
+
<Image
|
|
68
|
+
class="max-h-32 w-auto max-w-32 object-cover"
|
|
69
|
+
class:list={[
|
|
70
|
+
mediaTypes[item.data.type.id].coverImageStyle === "video" &&
|
|
71
|
+
"aspect-video",
|
|
72
|
+
]}
|
|
73
|
+
src={item.data.image}
|
|
74
|
+
width={256}
|
|
75
|
+
alt=""
|
|
76
|
+
/>
|
|
77
|
+
</CoverImageDecorator>
|
|
68
78
|
</div>
|
|
69
79
|
|
|
70
80
|
<div
|
|
71
|
-
class="ms-
|
|
81
|
+
class="ms-4 flex grow flex-col justify-center sm:ms-6"
|
|
72
82
|
lang={item.data.language}
|
|
73
83
|
>
|
|
74
84
|
<p class="mb-1 line-clamp-3 text-balance font-bold text-gray-700 md:mb-3">
|
|
@@ -12,7 +12,7 @@ const { t } = Astro.locals.i18n
|
|
|
12
12
|
action={`/${Astro.currentLocale}/media`}
|
|
13
13
|
method="get"
|
|
14
14
|
role="search"
|
|
15
|
-
class="dy-join group w-full rounded-2xl shadow-
|
|
15
|
+
class="dy-join group w-full rounded-2xl shadow-sm outline-2 outline-offset-2 outline-gray-400 group-focus-within:outline"
|
|
16
16
|
class:list={[Astro.props.className]}
|
|
17
17
|
>
|
|
18
18
|
<input
|
|
@@ -183,80 +183,111 @@ export const createCategorySchema = ({ image }: SchemaContext) =>
|
|
|
183
183
|
/**
|
|
184
184
|
* Media Type Schema
|
|
185
185
|
*/
|
|
186
|
-
export const mediaTypeSchema = z
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
186
|
+
export const mediaTypeSchema = z
|
|
187
|
+
.object({
|
|
188
|
+
/**
|
|
189
|
+
* Name of this media type that will be shown on the pages.
|
|
190
|
+
*
|
|
191
|
+
* This can either be a fixed string or a translation key.
|
|
192
|
+
*
|
|
193
|
+
* @example "media-type.book"
|
|
194
|
+
*/
|
|
195
|
+
label: z.string(),
|
|
196
|
+
/**
|
|
197
|
+
* Defines how the cover image for a media item of this type is rendered.
|
|
198
|
+
*
|
|
199
|
+
* Options:
|
|
200
|
+
* - `"default"` — Renders the media item image with no modifications.
|
|
201
|
+
* - `"book"` — Adds a book fold effect and sharper edges, styled like a book cover.
|
|
202
|
+
* - `"video"` — Constrains the image to a 16:9 aspect ratio with a black background.
|
|
203
|
+
*
|
|
204
|
+
* @default "default"
|
|
205
|
+
*/
|
|
206
|
+
coverImageStyle: z.enum(["default", "book", "video"]).default("default"),
|
|
207
|
+
/**
|
|
208
|
+
* What media item details page to use for media items with this type.
|
|
209
|
+
*
|
|
210
|
+
*/
|
|
211
|
+
detailsPage: z
|
|
212
|
+
.discriminatedUnion("layout", [
|
|
213
|
+
z.object({
|
|
214
|
+
/**
|
|
215
|
+
* Details page for all media types.
|
|
216
|
+
*/
|
|
217
|
+
layout: z.literal("default"),
|
|
218
|
+
/**
|
|
219
|
+
* Label for the open action button. Use this if you want to change the text
|
|
220
|
+
* of the "Open" button to be more matching to your media item.
|
|
221
|
+
* For example you could change the text to be "Read" for a book media type.
|
|
222
|
+
*
|
|
223
|
+
* The label is a translation key.
|
|
224
|
+
*
|
|
225
|
+
* @example "ln.details.open"
|
|
226
|
+
*/
|
|
227
|
+
openActionLabel: z.string().optional(),
|
|
228
|
+
/**
|
|
229
|
+
* (Deprecated) Specifies the style of the cover image.
|
|
230
|
+
*
|
|
231
|
+
* Use `coverImageStyle` instead. This option will be removed in a future major release.
|
|
232
|
+
*
|
|
233
|
+
* Supported values:
|
|
234
|
+
* - `"default"` — unmodified media item image
|
|
235
|
+
* - `"book"` — styled as a book cover (book fold, sharper edges)
|
|
236
|
+
*
|
|
237
|
+
* @example "book"
|
|
238
|
+
* @deprecated Use `coverImageStyle` instead
|
|
239
|
+
*/
|
|
240
|
+
coverStyle: z.enum(["default", "book"]).default("default"),
|
|
241
|
+
}),
|
|
242
|
+
z.object({
|
|
243
|
+
/**
|
|
244
|
+
* Custom details page.
|
|
245
|
+
*/
|
|
246
|
+
layout: z.literal("custom"),
|
|
247
|
+
/**
|
|
248
|
+
* This references a custom component name to be used for the
|
|
249
|
+
* details page. The custom component has be located at src/details-pages/
|
|
250
|
+
*
|
|
251
|
+
* @example "MyArticleDetails.astro"
|
|
252
|
+
*/
|
|
253
|
+
customComponent: z.string(),
|
|
254
|
+
}),
|
|
255
|
+
z.object({
|
|
256
|
+
/**
|
|
257
|
+
* Detail page for videos.
|
|
258
|
+
*/
|
|
259
|
+
layout: z.literal("video"),
|
|
260
|
+
}),
|
|
261
|
+
z.object({
|
|
262
|
+
/**
|
|
263
|
+
* Detail page for audio files.
|
|
264
|
+
*
|
|
265
|
+
* This only supports mp3 files.
|
|
266
|
+
*/
|
|
267
|
+
layout: z.literal("audio"),
|
|
268
|
+
}),
|
|
269
|
+
])
|
|
270
|
+
.optional(),
|
|
271
|
+
/**
|
|
272
|
+
* Pick the media type's icon from https://pictogrammers.com/library/mdi/
|
|
273
|
+
* Prefix it's name with "mdi--"
|
|
274
|
+
*
|
|
275
|
+
* @example "mdi--ab-testing"
|
|
276
|
+
*/
|
|
277
|
+
icon: z.string(),
|
|
278
|
+
})
|
|
279
|
+
.transform((mediaType) => {
|
|
280
|
+
// migrate old cover images style to new property
|
|
281
|
+
const hasDeprecatedBookCover =
|
|
282
|
+
mediaType.detailsPage?.layout === "default" &&
|
|
283
|
+
mediaType.detailsPage.coverStyle === "book"
|
|
284
|
+
return {
|
|
285
|
+
...mediaType,
|
|
286
|
+
coverImageStyle: hasDeprecatedBookCover
|
|
287
|
+
? "book"
|
|
288
|
+
: mediaType.coverImageStyle,
|
|
289
|
+
}
|
|
290
|
+
})
|
|
260
291
|
|
|
261
292
|
export const LIGHTNET_COLLECTIONS = {
|
|
262
293
|
categories: defineCollection({
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
ln.header.open-main-menu: Бастапқы мәзірді ашу
|
|
2
|
+
ln.header.select-language: Тіл таңдау
|
|
3
|
+
ln.home.title: Басты бет
|
|
4
|
+
ln.category: Санат
|
|
5
|
+
ln.categories: Барлық санаттар
|
|
6
|
+
ln.language: Тіл
|
|
7
|
+
ln.languages: Тілдер
|
|
8
|
+
ln.type: Түрі
|
|
9
|
+
ln.external-link: Сыртқы сілтеме
|
|
10
|
+
ln.search.title: Іздеу
|
|
11
|
+
ln.search.placeholder: Медиа іздеу
|
|
12
|
+
ln.search.all-languages: Барлық тілдер
|
|
13
|
+
ln.search.all-types: Барлық түрлер
|
|
14
|
+
ln.search.all-categories: Барлық санаттар
|
|
15
|
+
ln.search.no-results: Нәтижие жоқ
|
|
16
|
+
ln.details.open: Ашу
|
|
17
|
+
ln.details.share: Бөлісу
|
|
18
|
+
ln.details.part-of-collection: Жинақ бөлімі
|
|
19
|
+
ln.details.download: Жүктеу
|
|
20
|
+
ln.share.url-copied-to-clipboard: Жүктеме көшірілді
|
|
21
|
+
ln.404.page-not-found: Ештеңе табылмады
|
|
22
|
+
ln.404.go-to-the-home-page: Бастапқы бетке өту
|
package/src/i18n/translations.ts
CHANGED
|
@@ -9,6 +9,7 @@ const builtInTranslations = {
|
|
|
9
9
|
fi: () => import("./translations/fi.yml?raw"),
|
|
10
10
|
fr: () => import("./translations/fr.yml?raw"),
|
|
11
11
|
hi: () => import("./translations/hi.yml?raw"),
|
|
12
|
+
kk: () => import("./translations/kk.yml?raw"),
|
|
12
13
|
pt: () => import("./translations/pt.yml?raw"),
|
|
13
14
|
ru: () => import("./translations/ru.yml?raw"),
|
|
14
15
|
uk: () => import("./translations/uk.yml?raw"),
|
|
@@ -3,7 +3,7 @@ import PageNavigation from "./PageNavigation.astro"
|
|
|
3
3
|
import PageTitle from "./PageTitle.astro"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<header class="fixed top-0 z-50 h-14 w-full bg-white shadow-
|
|
6
|
+
<header class="fixed top-0 z-50 h-14 w-full bg-white shadow-md sm:h-20">
|
|
7
7
|
<div class="mx-auto flex h-full max-w-screen-xl justify-between px-4 md:px-8">
|
|
8
8
|
<PageTitle />
|
|
9
9
|
<PageNavigation />
|
|
@@ -15,7 +15,7 @@ const { mediaId } = Astro.props
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
<DetailsPage mediaId={mediaId}>
|
|
18
|
-
<MainDetailsSection
|
|
18
|
+
<MainDetailsSection imageSize="md" mediaId={mediaId} />
|
|
19
19
|
<AudioPanel mediaId={mediaId} className="mt-10" />
|
|
20
20
|
<DescriptionSection mediaId={mediaId} />
|
|
21
21
|
<ContentSection mediaId={mediaId} minimumItems={1} />
|
|
@@ -9,15 +9,14 @@ import MoreDetailsSection from "./components/MoreDetailsSection.astro"
|
|
|
9
9
|
|
|
10
10
|
export type Props = {
|
|
11
11
|
mediaId: string
|
|
12
|
-
coverStyle?: "default" | "book"
|
|
13
12
|
openActionLabel?: string
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
const { mediaId,
|
|
15
|
+
const { mediaId, openActionLabel = "ln.details.open" } = Astro.props
|
|
17
16
|
---
|
|
18
17
|
|
|
19
18
|
<DetailsPage mediaId={mediaId}>
|
|
20
|
-
<MainDetailsSection mediaId={mediaId}
|
|
19
|
+
<MainDetailsSection mediaId={mediaId}>
|
|
21
20
|
<OpenButton
|
|
22
21
|
className="mt-8"
|
|
23
22
|
mediaId={mediaId}
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Authors from "./main-details/Authors.astro"
|
|
3
|
+
import CoverImage from "./main-details/CoverImage.astro"
|
|
3
4
|
import ShareButton from "./main-details/ShareButton.astro"
|
|
4
|
-
import Thumbnail from "./main-details/Thumbnail.astro"
|
|
5
5
|
import Title from "./main-details/Title.astro"
|
|
6
6
|
|
|
7
7
|
export type Props = {
|
|
8
8
|
mediaId: string
|
|
9
|
-
|
|
10
|
-
thumbnailSize?: "md" | "lg"
|
|
9
|
+
imageSize?: "md" | "lg"
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
const { mediaId,
|
|
12
|
+
const { mediaId, imageSize } = Astro.props
|
|
14
13
|
---
|
|
15
14
|
|
|
16
15
|
<div
|
|
17
16
|
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"
|
|
18
17
|
>
|
|
19
|
-
<
|
|
18
|
+
<CoverImage size={imageSize} mediaId={mediaId} />
|
|
20
19
|
<div class="flex w-full grow flex-col items-center sm:items-start">
|
|
21
20
|
<Title className="text-center sm:text-start" mediaId={mediaId} />
|
|
22
21
|
<Authors className="mt-2" mediaId={mediaId} />
|
|
@@ -33,7 +33,7 @@ const t = Astro.locals.i18n.t
|
|
|
33
33
|
<h2 class="mb-1 text-xs font-bold uppercase text-gray-600 md:mb-2">
|
|
34
34
|
{t("ln.details.part-of-collection")}
|
|
35
35
|
</h2>
|
|
36
|
-
<h3 class="text-lg font-bold text-gray-700">
|
|
36
|
+
<h3 class="mb-6 text-lg font-bold text-gray-700 sm:mb-8">
|
|
37
37
|
{t(collection.data.label)}
|
|
38
38
|
</h3>
|
|
39
39
|
<MediaList items={items} />
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Image } from "astro:assets"
|
|
3
3
|
|
|
4
|
+
import CoverImageDecorator from "../../../../components/CoverImageDecorator"
|
|
4
5
|
import { getMediaItem } from "../../../../content/get-media-items"
|
|
6
|
+
import { getMediaType } from "../../../../content/get-media-types"
|
|
5
7
|
|
|
6
8
|
interface Props {
|
|
7
9
|
mediaId: string
|
|
8
|
-
style?: "default" | "book"
|
|
9
10
|
size?: "md" | "lg"
|
|
10
11
|
}
|
|
11
|
-
const { mediaId,
|
|
12
|
+
const { mediaId, size = "lg" } = Astro.props
|
|
12
13
|
|
|
13
14
|
const item = await getMediaItem(mediaId)
|
|
15
|
+
const type = await getMediaType(item.data.type.id)
|
|
14
16
|
const image = item.data.image
|
|
15
17
|
const isPortraitImage = image.height > image.width
|
|
16
18
|
const imageSizes = {
|
|
@@ -25,10 +27,7 @@ const imageSizes = {
|
|
|
25
27
|
}
|
|
26
28
|
---
|
|
27
29
|
|
|
28
|
-
<
|
|
29
|
-
class="relative shrink-0 overflow-hidden shadow-sm"
|
|
30
|
-
class:list={style === "book" ? "rounded-sm" : "rounded-md"}
|
|
31
|
-
>
|
|
30
|
+
<CoverImageDecorator className="shrink-0" style={type.data.coverImageStyle}>
|
|
32
31
|
<Image
|
|
33
32
|
class:list={imageSizes[size].css}
|
|
34
33
|
alt=""
|
|
@@ -39,9 +38,4 @@ const imageSizes = {
|
|
|
39
38
|
loading="eager"
|
|
40
39
|
fetchpriority="high"
|
|
41
40
|
/>
|
|
42
|
-
|
|
43
|
-
style === "book" && (
|
|
44
|
-
<span class="absolute start-[3px] top-0 h-full w-[4px] bg-gradient-to-r from-gray-500/20 to-transparent" />
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
</div>
|
|
41
|
+
</CoverImageDecorator>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import CoverImageDecorator from "../../../components/CoverImageDecorator"
|
|
1
2
|
import Icon from "../../../components/Icon"
|
|
2
3
|
import { detailsPagePath } from "../../../utils/paths"
|
|
3
4
|
import type { SearchItem } from "../../api/search-response"
|
|
@@ -5,6 +6,7 @@ import type { SearchItem } from "../../api/search-response"
|
|
|
5
6
|
export type MediaType = {
|
|
6
7
|
name: string
|
|
7
8
|
icon: string
|
|
9
|
+
coverImageStyle: "default" | "book" | "video"
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export type TranslatedLanguage = {
|
|
@@ -31,6 +33,7 @@ export default function SearchListItem({
|
|
|
31
33
|
showLanguage,
|
|
32
34
|
mediaTypes,
|
|
33
35
|
}: Props) {
|
|
36
|
+
const coverImageStyle = mediaTypes[item.type].coverImageStyle
|
|
34
37
|
return (
|
|
35
38
|
<a
|
|
36
39
|
href={detailsPagePath(currentLocale, {
|
|
@@ -40,15 +43,17 @@ export default function SearchListItem({
|
|
|
40
43
|
className="group flex h-52 overflow-hidden py-2 transition-colors ease-in-out sm:h-64 md:rounded-sm md:hover:bg-gray-100"
|
|
41
44
|
>
|
|
42
45
|
<div className="flex h-full w-36 shrink-0 flex-col items-start justify-center">
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
<CoverImageDecorator style={coverImageStyle}>
|
|
47
|
+
<img
|
|
48
|
+
className={`max-h-40 w-auto max-w-36 object-cover ${coverImageStyle === "video" ? "aspect-video" : ""}`}
|
|
49
|
+
src={item.image.src}
|
|
50
|
+
width={item.image.width}
|
|
51
|
+
height={item.image.height}
|
|
52
|
+
alt=""
|
|
53
|
+
decoding="async"
|
|
54
|
+
loading="eager"
|
|
55
|
+
/>
|
|
56
|
+
</CoverImageDecorator>
|
|
52
57
|
</div>
|
|
53
58
|
|
|
54
59
|
<div className="ms-5 flex grow flex-col justify-center text-xs sm:ms-8">
|
|
@@ -1,124 +0,0 @@
|
|
|
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 MediaItem = {
|
|
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) },
|
|
24
|
-
]),
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
interface Props {
|
|
28
|
-
items: (MediaItem | undefined)[]
|
|
29
|
-
layout: "book" | "video" | "portrait" | "landscape"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const { items: itemsInput, layout } = Astro.props
|
|
33
|
-
// We allow users to pass undefined values because
|
|
34
|
-
// this is in the return type of getEntry
|
|
35
|
-
const items = itemsInput.filter((item) => !!item)
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
(layout === "book" || layout === "portrait") && (
|
|
40
|
-
<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">
|
|
41
|
-
{items.map((item) => (
|
|
42
|
-
<li>
|
|
43
|
-
<a
|
|
44
|
-
href={detailsPagePath(Astro.currentLocale, item)}
|
|
45
|
-
class="group flex flex-col gap-3"
|
|
46
|
-
>
|
|
47
|
-
<div
|
|
48
|
-
class="relative overflow-hidden shadow-md outline-2 outline-gray-400 transition-all duration-75 ease-in-out sm:group-hover:outline"
|
|
49
|
-
class:list={layout === "book" ? "rounded-sm" : "rounded-md"}
|
|
50
|
-
>
|
|
51
|
-
<Image
|
|
52
|
-
class="h-full w-full object-contain"
|
|
53
|
-
src={item.data.image}
|
|
54
|
-
alt=""
|
|
55
|
-
widths={[256, 512, 768, 1024]}
|
|
56
|
-
sizes={
|
|
57
|
-
"(max-width: 640px) calc(calc(100vw - 3.5rem ) / 2), " +
|
|
58
|
-
"(max-width: 768px) calc(calc(100vw - 5rem ) / 3), " +
|
|
59
|
-
"(max-width: 1024px) calc(calc(100vw - 10rem ) / 4), " +
|
|
60
|
-
"(max-width: 1280px) calc(calc(100vw - 12rem ) / 5), " +
|
|
61
|
-
"217px"
|
|
62
|
-
}
|
|
63
|
-
/>
|
|
64
|
-
{layout === "book" && (
|
|
65
|
-
<span class="absolute start-[3px] top-0 h-full w-[4px] bg-gradient-to-r from-gray-500/20 to-transparent" />
|
|
66
|
-
)}
|
|
67
|
-
</div>
|
|
68
|
-
<span class="line-clamp-2 h-12 text-balance text-sm font-bold text-gray-700">
|
|
69
|
-
<Icon
|
|
70
|
-
className={`${types[item.data.type.id].icon} me-2 align-bottom`}
|
|
71
|
-
ariaLabel={types[item.data.type.id].name}
|
|
72
|
-
/>
|
|
73
|
-
{item.data.title}
|
|
74
|
-
</span>
|
|
75
|
-
</a>
|
|
76
|
-
</li>
|
|
77
|
-
))}
|
|
78
|
-
</ol>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
{
|
|
82
|
-
(layout === "video" || layout === "landscape") && (
|
|
83
|
-
<ol
|
|
84
|
-
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"
|
|
85
|
-
class:list={[layout === "landscape" && "items-end"]}
|
|
86
|
-
>
|
|
87
|
-
{items.map((item) => (
|
|
88
|
-
<li>
|
|
89
|
-
<a
|
|
90
|
-
href={detailsPagePath(Astro.currentLocale, item)}
|
|
91
|
-
class="group flex flex-col gap-3"
|
|
92
|
-
>
|
|
93
|
-
<div
|
|
94
|
-
class="relative overflow-hidden rounded-md shadow-md outline-2 outline-gray-400 transition-all duration-75 ease-in-out sm:group-hover:outline"
|
|
95
|
-
class:list={[layout === "video" && "aspect-video bg-gray-950"]}
|
|
96
|
-
>
|
|
97
|
-
<Image
|
|
98
|
-
class="h-full w-full object-contain"
|
|
99
|
-
class:list={[layout === "video" && "absolute top-0"]}
|
|
100
|
-
src={item.data.image}
|
|
101
|
-
alt=""
|
|
102
|
-
widths={[120, 160, 240, 320, 640]}
|
|
103
|
-
sizes={
|
|
104
|
-
"(max-width: 640px) calc(calc(100vw - 2rem ) / 1), " +
|
|
105
|
-
"(max-width: 768px) calc(calc(100vw - 3.5rem ) / 2), " +
|
|
106
|
-
"(max-width: 1024px) calc(calc(100vw - 8.5rem ) / 3), " +
|
|
107
|
-
"(max-width: 1280px) calc(calc(100vw - 10.5rem ) / 4), " +
|
|
108
|
-
"280px"
|
|
109
|
-
}
|
|
110
|
-
/>
|
|
111
|
-
</div>
|
|
112
|
-
<span class="line-clamp-2 h-12 text-balance text-sm font-bold text-gray-700">
|
|
113
|
-
<Icon
|
|
114
|
-
className={`${types[item.data.type.id].icon} me-2 align-bottom`}
|
|
115
|
-
ariaLabel={types[item.data.type.id].name}
|
|
116
|
-
/>
|
|
117
|
-
{item.data.title}
|
|
118
|
-
</span>
|
|
119
|
-
</a>
|
|
120
|
-
</li>
|
|
121
|
-
))}
|
|
122
|
-
</ol>
|
|
123
|
-
)
|
|
124
|
-
}
|