lightnet 3.7.0 → 3.8.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 +36 -0
- package/README.md +23 -11
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tsc +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +2 -2
- package/__e2e__/fixtures/basics/package.json +10 -7
- package/__tests__/utils/markdown.spec.ts +4 -0
- package/__tests__/utils/urls.spec.ts +27 -0
- package/exports/experimental-components.ts +1 -0
- package/package.json +18 -10
- package/src/astro-integration/integration.ts +6 -0
- package/src/components/CarouselSection.astro +182 -0
- package/src/components/CategoriesSection.astro +96 -47
- package/src/components/MediaGallery.astro +1 -1
- package/src/components/SearchInput.astro +1 -0
- package/src/components/SearchSection.astro +9 -4
- package/src/components/Section.astro +31 -17
- package/src/content/get-categories.ts +1 -1
- package/src/i18n/translations/README.md +1 -1
- package/src/i18n/translations/ar.yml +2 -0
- package/src/i18n/translations/bn.yml +2 -0
- package/src/i18n/translations/de.yml +2 -0
- package/src/i18n/translations/en.yml +12 -0
- package/src/i18n/translations/es.yml +2 -0
- package/src/i18n/translations/fi.yml +2 -0
- package/src/i18n/translations/fr.yml +2 -0
- package/src/i18n/translations/hi.yml +2 -0
- package/src/i18n/translations/pt.yml +2 -0
- package/src/i18n/translations/ru.yml +2 -0
- package/src/i18n/translations/uk.yml +2 -0
- package/src/i18n/translations/zh.yml +2 -0
- package/src/i18n/translations.ts +2 -0
- package/src/layouts/Page.astro +2 -3
- package/src/layouts/components/Header.astro +1 -4
- package/src/layouts/components/ViewTransition.astro +9 -0
- package/src/pages/api/versions.ts +7 -0
- package/src/pages/details-page/components/AudioPanel.astro +15 -24
- package/src/pages/details-page/components/main-details/ShareButton.astro +20 -32
- package/src/pages/search-page/components/SearchList.tsx +0 -1
- package/src/pages/search-page/hooks/use-search.ts +2 -33
- package/src/utils/paths.ts +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# lightnet
|
|
2
2
|
|
|
3
|
+
## 3.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#299](https://github.com/LightNetDev/LightNet/pull/299) [`ecaf1f6`](https://github.com/LightNetDev/LightNet/commit/ecaf1f698231c3e4eec700ca07cb78d1dbf4378c) Thanks [@smn-cds](https://github.com/smn-cds)! - Added a new `titleHref` prop to the `Section` component.
|
|
8
|
+
|
|
9
|
+
This enables section titles to act as anchors, allowing navigation to dedicated pages for expanded content. Useful for situations where content previews need to link to full listings.
|
|
10
|
+
|
|
11
|
+
- [#297](https://github.com/LightNetDev/LightNet/pull/297) [`537f6e5`](https://github.com/LightNetDev/LightNet/commit/537f6e50c8bbf7d9a22a17773acddf79fbd11c21) Thanks [@smn-cds](https://github.com/smn-cds)! - Add /api/versions.json endpoint to return the LightNet version.
|
|
12
|
+
|
|
13
|
+
- [#299](https://github.com/LightNetDev/LightNet/pull/299) [`ecaf1f6`](https://github.com/LightNetDev/LightNet/commit/ecaf1f698231c3e4eec700ca07cb78d1dbf4378c) Thanks [@smn-cds](https://github.com/smn-cds)! - Added filter parameters (`type`, `language`, `search`) to `searchPagePath` function.
|
|
14
|
+
|
|
15
|
+
- [#298](https://github.com/LightNetDev/LightNet/pull/298) [`b5c3c7a`](https://github.com/LightNetDev/LightNet/commit/b5c3c7a6637b9973acdbd34af3db1de5ed01c9f5) Thanks [@smn-cds](https://github.com/smn-cds)! - Remove client router
|
|
16
|
+
|
|
17
|
+
We have been relying on [Astro's ClientRouter](https://docs.astro.build/en/reference/modules/astro-transitions/#clientrouter-) for
|
|
18
|
+
view transitions between different pages.
|
|
19
|
+
With this release we remove the use of ClientRouter and switch to the browser built-in [ViewTransitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API).
|
|
20
|
+
|
|
21
|
+
- [#299](https://github.com/LightNetDev/LightNet/pull/299) [`ecaf1f6`](https://github.com/LightNetDev/LightNet/commit/ecaf1f698231c3e4eec700ca07cb78d1dbf4378c) Thanks [@smn-cds](https://github.com/smn-cds)! - ⚠️ Added new translations
|
|
22
|
+
- ln.previous: "Previous"
|
|
23
|
+
- ln.next: "Next"
|
|
24
|
+
|
|
25
|
+
- [#299](https://github.com/LightNetDev/LightNet/pull/299) [`ecaf1f6`](https://github.com/LightNetDev/LightNet/commit/ecaf1f698231c3e4eec700ca07cb78d1dbf4378c) Thanks [@smn-cds](https://github.com/smn-cds)! - Improved CategorySection
|
|
26
|
+
- added `carousel` layout
|
|
27
|
+
- replaced `image-grid` layout with `grid`
|
|
28
|
+
- ⚠️ removed `button-grid`
|
|
29
|
+
- ⚠️ changed the default layout from `button-grid` to `carousel`
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- [#299](https://github.com/LightNetDev/LightNet/pull/299) [`ecaf1f6`](https://github.com/LightNetDev/LightNet/commit/ecaf1f698231c3e4eec700ca07cb78d1dbf4378c) Thanks [@smn-cds](https://github.com/smn-cds)! - ⚠️ Removed `disableHorizontalPadding` property from Section component.
|
|
34
|
+
|
|
35
|
+
- [#299](https://github.com/LightNetDev/LightNet/pull/299) [`ecaf1f6`](https://github.com/LightNetDev/LightNet/commit/ecaf1f698231c3e4eec700ca07cb78d1dbf4378c) Thanks [@smn-cds](https://github.com/smn-cds)! - Accessibility improvements
|
|
36
|
+
- set `role=search` for SearchInput component
|
|
37
|
+
- set aria-label for Section component
|
|
38
|
+
|
|
3
39
|
## 3.7.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
Share the
|
|
3
|
+
Share the gospel and strengthen believers in your community.
|
|
4
4
|
|
|
5
|
-
LightNet
|
|
5
|
+
LightNet makes it easy to **run your own digital media library**, so more people can find what they need and grow in faith.
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
8
|
+
|
|
9
|
+
Learn more on the [LightNet homepage](https://lightnet.community).
|
|
10
|
+
|
|
11
|
+
## Start Your Own Library
|
|
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
|
+
```
|
|
20
|
+
|
|
21
|
+
This will set up the demo site that you can customize and expand to meet your community’s needs.
|
|
8
22
|
|
|
9
23
|
## Documentation
|
|
10
24
|
|
|
11
|
-
[
|
|
25
|
+
Need help? [Explore the LightNet developer docs](https://docs.lightnet.community) for everything you need to get started and make the most of LightNet.
|
|
12
26
|
|
|
13
|
-
##
|
|
27
|
+
## Showcase
|
|
14
28
|
|
|
15
|
-
[
|
|
29
|
+
Want to see LightNet in action? [Visit the MediaWorks digital library](https://library.mediaworks.global) and see how it powers a real-world digital library.
|
|
16
30
|
|
|
17
31
|
## Contributing
|
|
18
32
|
|
|
19
|
-
|
|
33
|
+
Want to help improve LightNet? [Check out the contribution guide](https://github.com/LightNetDev/lightnet/blob/main/CONTRIBUTING.md) to learn how you can get involved and make a difference!
|
|
20
34
|
|
|
21
35
|
## License
|
|
22
36
|
|
|
23
|
-
MIT
|
|
24
|
-
|
|
25
|
-
Copyright (c) 2024–present [LightNet contributors](https://github.com/LightNetDev/LightNet/graphs/contributors)
|
|
37
|
+
LightNet is licensed under the MIT License. See the full details in the [LICENSE](https://github.com/LightNetDev/lightnet/blob/main/LICENSE) 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.12.8_@types+node@24.2.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.46.2_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.12.8_@types+node@24.2.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.46.2_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.12.8_@types+node@24.2.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.46.2_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.12.8_@types+node@24.2.0_jiti@2.4.2_lightningcss@1.29.1_rollup@4.46.2_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" "$@"
|
|
@@ -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/typescript@5.
|
|
13
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/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/typescript@5.
|
|
15
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/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/../typescript/bin/tsc" "$@"
|
|
@@ -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/typescript@5.
|
|
13
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/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/typescript@5.
|
|
15
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.9.2/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/../typescript/bin/tsserver" "$@"
|
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@astrojs/react": "^4.3.0",
|
|
8
8
|
"@astrojs/tailwind": "^6.0.2",
|
|
9
|
-
"@lightnet/decap-admin": "^3.1.
|
|
10
|
-
"astro": "^5.
|
|
11
|
-
"lightnet": "^3.
|
|
12
|
-
"react": "^19.1.
|
|
13
|
-
"react-dom": "^19.1.
|
|
14
|
-
"sharp": "^0.
|
|
9
|
+
"@lightnet/decap-admin": "^3.1.2",
|
|
10
|
+
"astro": "^5.12.8",
|
|
11
|
+
"lightnet": "^3.8.0",
|
|
12
|
+
"react": "^19.1.1",
|
|
13
|
+
"react-dom": "^19.1.1",
|
|
14
|
+
"sharp": "^0.34.3",
|
|
15
15
|
"tailwindcss": "^3.4.17",
|
|
16
|
-
"typescript": "^5.
|
|
16
|
+
"typescript": "^5.9.2"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22"
|
|
17
20
|
}
|
|
18
21
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import config from "virtual:lightnet/config"
|
|
2
|
+
import projectContext from "virtual:lightnet/project-context"
|
|
3
|
+
import { expect, test } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { isExternalUrl } from "../../src/utils/urls"
|
|
6
|
+
|
|
7
|
+
// relative path should be treated as internal
|
|
8
|
+
test("Should treat relative paths as internal", () => {
|
|
9
|
+
expect(isExternalUrl("/page")).toBe(false)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
// absolute url that matches the configured site should be internal
|
|
13
|
+
test("Should treat URLs matching projectContext.site as internal", () => {
|
|
14
|
+
expect(isExternalUrl(`${projectContext.site}/page`)).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// domains listed in internalDomains should be treated as internal
|
|
18
|
+
test("Should treat configured internalDomains as internal", () => {
|
|
19
|
+
config.internalDomains.push("internal.test")
|
|
20
|
+
expect(isExternalUrl("https://internal.test/foo")).toBe(false)
|
|
21
|
+
config.internalDomains.pop()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// any other absolute url should be external
|
|
25
|
+
test("Should treat other absolute URLs as external", () => {
|
|
26
|
+
expect(isExternalUrl("https://example.com")).toBe(true)
|
|
27
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CarouselSection } from "../src/components/CarouselSection.astro"
|
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.8.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/LightNetDev/lightnet",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"./content": "./exports/content.ts",
|
|
21
21
|
"./utils": "./exports/utils.ts",
|
|
22
22
|
"./components": "./exports/components.ts",
|
|
23
|
+
"./experimental-components": "./exports/experimental-components.ts",
|
|
23
24
|
"./experimental-details-page": "./exports/details-page.ts",
|
|
24
25
|
"./i18n": "./exports/i18n.ts",
|
|
25
26
|
"./locals": "./src/i18n/locals.ts",
|
|
@@ -29,7 +30,8 @@
|
|
|
29
30
|
"./pages/RootRoute.astro": "./src/pages/RootRoute.astro",
|
|
30
31
|
"./pages/SearchPageRoute.astro": "./src/pages/search-page/SearchPageRoute.astro",
|
|
31
32
|
"./pages/DetailsPageRoute.astro": "./src/pages/details-page/DetailsPageRoute.astro",
|
|
32
|
-
"./pages/api/search.ts": "./src/pages/api/search.ts"
|
|
33
|
+
"./pages/api/search.ts": "./src/pages/api/search.ts",
|
|
34
|
+
"./pages/api/versions.ts": "./src/pages/api/versions.ts"
|
|
33
35
|
},
|
|
34
36
|
"peerDependencies": {
|
|
35
37
|
"astro": "^5.1.0",
|
|
@@ -38,25 +40,31 @@
|
|
|
38
40
|
"tailwindcss": ">=3.4.0 <4.0.0"
|
|
39
41
|
},
|
|
40
42
|
"dependencies": {
|
|
43
|
+
"@astrojs/react": "^4.3.0",
|
|
44
|
+
"@astrojs/tailwind": "^6.0.2",
|
|
41
45
|
"@iconify-json/mdi": "^1.2.3",
|
|
42
46
|
"@iconify/tailwind": "^1.2.0",
|
|
43
|
-
"@astrojs/react": "^4.1.0",
|
|
44
|
-
"@astrojs/tailwind": "^6.0.0",
|
|
45
47
|
"@tailwindcss/typography": "^0.5.16",
|
|
46
48
|
"@tanstack/react-virtual": "^3.13.12",
|
|
47
49
|
"daisyui": "^4.12.24",
|
|
50
|
+
"embla-carousel": "^8.6.0",
|
|
51
|
+
"embla-carousel-auto-height": "^8.6.0",
|
|
52
|
+
"embla-carousel-wheel-gestures": "^8.0.2",
|
|
48
53
|
"fuse.js": "^7.1.0",
|
|
49
54
|
"i18next": "^25.3.2",
|
|
50
|
-
"marked": "^16.
|
|
51
|
-
"yaml": "^2.8.
|
|
55
|
+
"marked": "^16.1.2",
|
|
56
|
+
"yaml": "^2.8.1"
|
|
52
57
|
},
|
|
53
58
|
"devDependencies": {
|
|
54
|
-
"
|
|
55
|
-
"@types/
|
|
56
|
-
"@
|
|
57
|
-
"
|
|
59
|
+
"@playwright/test": "^1.54.2",
|
|
60
|
+
"@types/node": "^22.17.0",
|
|
61
|
+
"@types/react": "^19.1.9",
|
|
62
|
+
"typescript": "^5.9.2",
|
|
58
63
|
"vitest": "^3.2.4"
|
|
59
64
|
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22"
|
|
67
|
+
},
|
|
60
68
|
"scripts": {
|
|
61
69
|
"test": "vitest",
|
|
62
70
|
"e2e": "playwright install --with-deps chromium && playwright test"
|
|
@@ -51,6 +51,12 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
|
51
51
|
prerender: true,
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
+
injectRoute({
|
|
55
|
+
pattern: "/api/versions.json",
|
|
56
|
+
entrypoint: "lightnet/pages/api/versions.ts",
|
|
57
|
+
prerender: true,
|
|
58
|
+
})
|
|
59
|
+
|
|
54
60
|
injectRoute({
|
|
55
61
|
pattern: "/[locale]/media/[mediaId]",
|
|
56
62
|
entrypoint: "lightnet/pages/DetailsPageRoute.astro",
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon"
|
|
3
|
+
import Section, { type Props as SectionProps } from "./Section.astro"
|
|
4
|
+
|
|
5
|
+
type Props = SectionProps
|
|
6
|
+
|
|
7
|
+
const { titleClass = "", ...props } = Astro.props
|
|
8
|
+
const { t, direction } = Astro.locals.i18n
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<Section {...props} titleClass={"!mb-8 sm:!-mb-1 " + titleClass}>
|
|
12
|
+
<ln-carousel data-direction={direction} aria-roledescription="carousel">
|
|
13
|
+
<div class="mb-2 hidden justify-end gap-1 sm:flex">
|
|
14
|
+
<button
|
|
15
|
+
data-button-prev
|
|
16
|
+
aria-label={t("ln.previous")}
|
|
17
|
+
class="flex h-9 w-9 items-center justify-center rounded-full border-2 border-gray-200 bg-gray-200 text-gray-600 transition-colors ease-in-out hover:border-gray-400 disabled:bg-transparent disabled:text-gray-300 disabled:hover:border-gray-200"
|
|
18
|
+
><Icon
|
|
19
|
+
className="mdi--chevron-left"
|
|
20
|
+
ariaLabel=""
|
|
21
|
+
flipIcon={direction === "rtl"}
|
|
22
|
+
/></button
|
|
23
|
+
>
|
|
24
|
+
<button
|
|
25
|
+
data-button-next
|
|
26
|
+
aria-label={t("ln.next")}
|
|
27
|
+
class="flex h-9 w-9 items-center justify-center rounded-full border-2 border-gray-200 bg-gray-200 text-gray-600 transition-colors ease-in-out hover:border-gray-400 disabled:bg-transparent disabled:text-gray-300 disabled:hover:border-gray-200"
|
|
28
|
+
><Icon
|
|
29
|
+
className="mdi--chevron-right"
|
|
30
|
+
ariaLabel=""
|
|
31
|
+
flipIcon={direction === "rtl"}
|
|
32
|
+
/></button
|
|
33
|
+
>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div data-carousel class="-m-4 overflow-hidden p-4">
|
|
37
|
+
<ol
|
|
38
|
+
class="flex items-end gap-4 md:gap-8"
|
|
39
|
+
aria-atomic="false"
|
|
40
|
+
aria-live="polite"
|
|
41
|
+
data-carousel-container
|
|
42
|
+
>
|
|
43
|
+
<slot />
|
|
44
|
+
</ol>
|
|
45
|
+
</div>
|
|
46
|
+
</ln-carousel>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
import EmblaCarousel from "embla-carousel"
|
|
50
|
+
import AutoHeightPlugin from "embla-carousel-auto-height"
|
|
51
|
+
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures"
|
|
52
|
+
|
|
53
|
+
class Carousel extends HTMLElement {
|
|
54
|
+
preloadIndex = 0
|
|
55
|
+
|
|
56
|
+
connectedCallback() {
|
|
57
|
+
const carouselNode = this.querySelector(
|
|
58
|
+
"[data-carousel]",
|
|
59
|
+
) as HTMLElement
|
|
60
|
+
const direction = this.dataset.direction as "ltr" | "rtl"
|
|
61
|
+
const carousel = EmblaCarousel(
|
|
62
|
+
carouselNode,
|
|
63
|
+
{
|
|
64
|
+
slidesToScroll: "auto",
|
|
65
|
+
skipSnaps: true,
|
|
66
|
+
direction,
|
|
67
|
+
},
|
|
68
|
+
[AutoHeightPlugin(), WheelGesturesPlugin({ forceWheelAxis: "x" })],
|
|
69
|
+
)
|
|
70
|
+
const prevBtn = this.querySelector(
|
|
71
|
+
"[data-button-prev]",
|
|
72
|
+
) as HTMLButtonElement
|
|
73
|
+
const nextBtn = this.querySelector(
|
|
74
|
+
"[data-button-next]",
|
|
75
|
+
) as HTMLButtonElement
|
|
76
|
+
|
|
77
|
+
prevBtn.addEventListener("click", () => carousel.scrollPrev())
|
|
78
|
+
nextBtn.addEventListener("click", () => carousel.scrollNext())
|
|
79
|
+
|
|
80
|
+
const updateArrowButtons = () => {
|
|
81
|
+
if (carousel.canScrollPrev()) {
|
|
82
|
+
prevBtn.removeAttribute("disabled")
|
|
83
|
+
} else {
|
|
84
|
+
prevBtn.setAttribute("disabled", "disabled")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (carousel.canScrollNext()) {
|
|
88
|
+
nextBtn.removeAttribute("disabled")
|
|
89
|
+
} else {
|
|
90
|
+
nextBtn.setAttribute("disabled", "disabled")
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
carousel
|
|
94
|
+
.on("init", updateArrowButtons)
|
|
95
|
+
.on("select", updateArrowButtons)
|
|
96
|
+
.on("reInit", updateArrowButtons)
|
|
97
|
+
|
|
98
|
+
// when images are set to loading=lazy, safari and
|
|
99
|
+
// firefox fail to preload them before they are
|
|
100
|
+
// visible. We improve the browser heuristics
|
|
101
|
+
// by setting the next chunk of images to load
|
|
102
|
+
// eagerly.
|
|
103
|
+
const slideNodes =
|
|
104
|
+
this.querySelector("[data-carousel-container]")?.children ?? []
|
|
105
|
+
const preloadNextSlides = () => {
|
|
106
|
+
const slidesInView = carousel.slidesInView()
|
|
107
|
+
slidesInView.forEach((slideIndex) => {
|
|
108
|
+
const preloadIndex = slideIndex + slidesInView.length
|
|
109
|
+
if (preloadIndex > this.preloadIndex) {
|
|
110
|
+
this.preloadIndex = preloadIndex
|
|
111
|
+
const node = slideNodes[preloadIndex]
|
|
112
|
+
node?.querySelectorAll("img").forEach((img) => {
|
|
113
|
+
img.loading = "eager"
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// start preloading once the carousel enters the viewport
|
|
120
|
+
const observer = new IntersectionObserver(([target]) => {
|
|
121
|
+
if (!target.isIntersecting) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
preloadNextSlides()
|
|
125
|
+
carousel.on("slidesInView", () => preloadNextSlides())
|
|
126
|
+
observer.unobserve(target.target)
|
|
127
|
+
})
|
|
128
|
+
observer.observe(carouselNode)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
customElements.define("ln-carousel", Carousel)
|
|
132
|
+
</script>
|
|
133
|
+
</Section>
|
|
134
|
+
<style is:global>
|
|
135
|
+
.carousel-item--wide {
|
|
136
|
+
flex: 0 0 auto;
|
|
137
|
+
/* 1 column + part of the next */
|
|
138
|
+
width: 85%;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.carousel-item--narrow {
|
|
142
|
+
flex: 0 0 auto;
|
|
143
|
+
/* 2 columns + part of the next */
|
|
144
|
+
width: calc((100% - 1rem) / 2.3);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* sm */
|
|
148
|
+
@media (min-width: 640px) {
|
|
149
|
+
.carousel-item--wide {
|
|
150
|
+
/* 2 columns */
|
|
151
|
+
width: calc((100% - 1rem) / 2);
|
|
152
|
+
}
|
|
153
|
+
.carousel-item--narrow {
|
|
154
|
+
/* 3 columns */
|
|
155
|
+
width: calc((100% - 2rem) / 3);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* md */
|
|
160
|
+
@media (min-width: 768px) {
|
|
161
|
+
.carousel-item--wide {
|
|
162
|
+
/* 3 columns */
|
|
163
|
+
width: calc((100% - 4rem) / 3);
|
|
164
|
+
}
|
|
165
|
+
.carousel-item--narrow {
|
|
166
|
+
/* 4 columns */
|
|
167
|
+
width: calc((100% - 6rem) / 4);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* lg */
|
|
172
|
+
@media (min-width: 1024px) {
|
|
173
|
+
.carousel-item--wide {
|
|
174
|
+
/* 4 columns */
|
|
175
|
+
width: calc((100% - 6rem) / 4);
|
|
176
|
+
}
|
|
177
|
+
.carousel-item--narrow {
|
|
178
|
+
/* 5 columns */
|
|
179
|
+
width: calc((100% - 8rem) / 5);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -4,15 +4,16 @@ import { Image } from "astro:assets"
|
|
|
4
4
|
|
|
5
5
|
import { getUsedCategories } from "../content/get-categories"
|
|
6
6
|
import { searchPagePath } from "../utils/paths"
|
|
7
|
-
import
|
|
7
|
+
import CarouselSection from "./CarouselSection.astro"
|
|
8
|
+
import Section, { type Props as SectionProps } from "./Section.astro"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
layout?: "button-grid" | "image-grid"
|
|
10
|
+
type Props = SectionProps & {
|
|
11
|
+
layout?: "grid" | "carousel"
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const { title, layout = "
|
|
14
|
+
const { title, layout = "carousel", ...props } = Astro.props
|
|
15
15
|
const { t, currentLocale } = Astro.locals.i18n
|
|
16
|
+
const resolvedTitle = title ?? t("ln.categories")
|
|
16
17
|
|
|
17
18
|
const categories = await getUsedCategories(currentLocale, t)
|
|
18
19
|
type Category = (typeof categories)[number]
|
|
@@ -20,8 +21,8 @@ type Category = (typeof categories)[number]
|
|
|
20
21
|
function getImage({ image, id }: Category) {
|
|
21
22
|
if (!image) {
|
|
22
23
|
throw new AstroError(
|
|
23
|
-
`
|
|
24
|
-
`To resolve this issue,
|
|
24
|
+
`Expected an image for category "${id}".`,
|
|
25
|
+
`To resolve this issue, provide a valid image path in /src/content/categories/${id}.json.`,
|
|
25
26
|
)
|
|
26
27
|
}
|
|
27
28
|
return image
|
|
@@ -29,40 +30,27 @@ function getImage({ image, id }: Category) {
|
|
|
29
30
|
---
|
|
30
31
|
|
|
31
32
|
{
|
|
32
|
-
categories.length && (
|
|
33
|
-
<Section title={
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
!!categories.length && layout === "grid" && (
|
|
34
|
+
<Section {...props} title={resolvedTitle}>
|
|
35
|
+
<ol class="grid grid-cols-2 items-end justify-between gap-4 sm:grid-cols-3 md:grid-cols-4 md:gap-8 lg:grid-cols-5">
|
|
36
|
+
{categories.map((category) => (
|
|
37
|
+
<li class="h-full">
|
|
38
|
+
<a
|
|
39
|
+
href={searchPagePath(currentLocale, { category: category.id })}
|
|
40
|
+
class="group flex h-full flex-col gap-3"
|
|
41
|
+
>
|
|
42
|
+
<div
|
|
43
|
+
class="relative overflow-hidden rounded-2xl shadow-md outline-2 outline-gray-400 transition-colors duration-75 ease-in-out sm:group-hover:outline"
|
|
44
|
+
class:list={[
|
|
45
|
+
!category.image && "h-full min-h-28 w-full bg-gray-300",
|
|
46
|
+
]}
|
|
43
47
|
>
|
|
44
|
-
|
|
45
|
-
{category.name}
|
|
46
|
-
</div>
|
|
47
|
-
</a>
|
|
48
|
-
</li>
|
|
49
|
-
))}
|
|
50
|
-
</ul>
|
|
51
|
-
)}
|
|
52
|
-
{layout === "image-grid" && (
|
|
53
|
-
<ol class="grid grid-cols-2 items-end justify-between gap-4 sm:grid-cols-3 md:grid-cols-4 md:gap-8 lg:grid-cols-5">
|
|
54
|
-
{categories.map((category) => (
|
|
55
|
-
<li>
|
|
56
|
-
<a
|
|
57
|
-
href={searchPagePath(currentLocale, { category: category.id })}
|
|
58
|
-
class="group flex flex-col gap-3"
|
|
59
|
-
>
|
|
60
|
-
<div class="relative overflow-hidden rounded-md shadow-md outline-2 outline-gray-400 transition-all duration-75 ease-in-out sm:group-hover:outline">
|
|
48
|
+
{category.image && (
|
|
61
49
|
<Image
|
|
62
|
-
class="h-full w-full object-contain"
|
|
50
|
+
class="h-full w-full bg-gray-500 object-contain"
|
|
63
51
|
src={getImage(category)}
|
|
64
52
|
alt=""
|
|
65
|
-
widths={[256, 512, 768
|
|
53
|
+
widths={[128, 256, 388, 512, 768]}
|
|
66
54
|
sizes={
|
|
67
55
|
"(max-width: 640px) calc(calc(100vw - 3.5rem ) / 2), " +
|
|
68
56
|
"(max-width: 768px) calc(calc(100vw - 5rem ) / 3), " +
|
|
@@ -71,17 +59,78 @@ function getImage({ image, id }: Category) {
|
|
|
71
59
|
"217px"
|
|
72
60
|
}
|
|
73
61
|
/>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
<div
|
|
65
|
+
class="absolute start-0 top-0 flex h-full w-full flex-col justify-end bg-gradient-to-t p-4 text-gray-50"
|
|
66
|
+
class:list={[
|
|
67
|
+
category.image
|
|
68
|
+
? "from-black/80 via-black/30 via-50% to-transparent"
|
|
69
|
+
: "from-black/35 to-transparent",
|
|
70
|
+
]}
|
|
71
|
+
>
|
|
72
|
+
<span class="line-clamp-3 select-none text-balance font-bold">
|
|
73
|
+
{category.name}
|
|
74
|
+
</span>
|
|
79
75
|
</div>
|
|
80
|
-
</
|
|
81
|
-
</
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
</div>
|
|
77
|
+
</a>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ol>
|
|
85
81
|
</Section>
|
|
86
82
|
)
|
|
87
83
|
}
|
|
84
|
+
{
|
|
85
|
+
!!categories.length && layout === "carousel" && (
|
|
86
|
+
<CarouselSection {...props} title={resolvedTitle}>
|
|
87
|
+
{categories.map((category) => (
|
|
88
|
+
<li
|
|
89
|
+
class="carousel-item--narrow h-full"
|
|
90
|
+
role="group"
|
|
91
|
+
aria-roledescription="slide"
|
|
92
|
+
>
|
|
93
|
+
<a
|
|
94
|
+
href={searchPagePath(currentLocale, { category: category.id })}
|
|
95
|
+
class="group flex h-full flex-col gap-3"
|
|
96
|
+
>
|
|
97
|
+
<div
|
|
98
|
+
class="relative overflow-hidden rounded-2xl shadow-md outline-2 outline-gray-400 transition-all duration-75 ease-in-out sm:group-hover:outline"
|
|
99
|
+
class:list={[
|
|
100
|
+
!category.image && "h-full min-h-28 w-full bg-gray-300",
|
|
101
|
+
]}
|
|
102
|
+
>
|
|
103
|
+
{category.image && (
|
|
104
|
+
<Image
|
|
105
|
+
class="h-full w-full bg-gray-300 object-contain"
|
|
106
|
+
src={getImage(category)}
|
|
107
|
+
alt=""
|
|
108
|
+
widths={[128, 256, 388, 512, 768]}
|
|
109
|
+
sizes={
|
|
110
|
+
"(max-width: 640px) calc(calc(100vw - 3rem ) / 2.3), " +
|
|
111
|
+
"(max-width: 768px) calc(calc(100vw - 5rem ) / 3), " +
|
|
112
|
+
"(max-width: 1024px) calc(calc(100vw - 10rem ) / 4), " +
|
|
113
|
+
"(max-width: 1280px) calc(calc(100vw - 12rem ) / 5), " +
|
|
114
|
+
"217px"
|
|
115
|
+
}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
<div
|
|
119
|
+
class="absolute start-0 top-0 flex h-full w-full flex-col justify-end bg-gradient-to-t p-4 text-gray-50"
|
|
120
|
+
class:list={[
|
|
121
|
+
category.image
|
|
122
|
+
? "from-black/80 via-black/30 via-50% to-transparent"
|
|
123
|
+
: "from-black/35 to-transparent",
|
|
124
|
+
]}
|
|
125
|
+
>
|
|
126
|
+
<span class="line-clamp-3 select-none text-balance font-bold">
|
|
127
|
+
{category.name}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</a>
|
|
132
|
+
</li>
|
|
133
|
+
))}
|
|
134
|
+
</CarouselSection>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -81,7 +81,7 @@ const items = itemsInput.filter((item) => !!item)
|
|
|
81
81
|
{
|
|
82
82
|
(layout === "video" || layout === "landscape") && (
|
|
83
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
|
|
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
85
|
class:list={[layout === "landscape" && "items-end"]}
|
|
86
86
|
>
|
|
87
87
|
{items.map((item) => (
|
|
@@ -11,6 +11,7 @@ const { t } = Astro.locals.i18n
|
|
|
11
11
|
<form
|
|
12
12
|
action={`/${Astro.currentLocale}/media`}
|
|
13
13
|
method="get"
|
|
14
|
+
role="search"
|
|
14
15
|
class="dy-join group w-full rounded-2xl shadow-md outline-2 outline-offset-2 outline-gray-400 group-focus-within:outline"
|
|
15
16
|
class:list={[Astro.props.className]}
|
|
16
17
|
>
|
|
@@ -3,12 +3,17 @@ import SearchFilter from "../pages/search-page/components/SearchFilter.astro"
|
|
|
3
3
|
import SearchList from "../pages/search-page/components/SearchList.astro"
|
|
4
4
|
import Section, { type Props as SectionProps } from "./Section.astro"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
type Props = Omit<SectionProps, "maxWidth">
|
|
7
|
+
|
|
8
|
+
const { className = "", titleClass = "", ...props } = Astro.props
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
<Section
|
|
11
|
+
<Section
|
|
12
|
+
{...props}
|
|
13
|
+
className={"px-0 md:px-0 " + className}
|
|
14
|
+
titleClass={"px-4 md:px-8 " + titleClass}
|
|
15
|
+
maxWidth="narrow"
|
|
16
|
+
>
|
|
12
17
|
<div class="px-4 md:px-8">
|
|
13
18
|
<SearchFilter />
|
|
14
19
|
</div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import Icon from "./Icon"
|
|
3
|
+
|
|
2
4
|
export interface Props {
|
|
3
5
|
/**
|
|
4
6
|
* Id to set to the section element.
|
|
@@ -21,30 +23,35 @@ export interface Props {
|
|
|
21
23
|
* @default "lg"
|
|
22
24
|
*/
|
|
23
25
|
marginTop?: "sm" | "lg" | "none"
|
|
24
|
-
/**
|
|
25
|
-
* Remove padding from the left and right side of the content.
|
|
26
|
-
* This will not apply to the section title.
|
|
27
|
-
*
|
|
28
|
-
* @default false
|
|
29
|
-
*/
|
|
30
|
-
disableHorizontalPadding?: boolean
|
|
31
26
|
/**
|
|
32
27
|
* Title on top of the section.
|
|
33
28
|
*/
|
|
34
29
|
title?: string
|
|
30
|
+
/**
|
|
31
|
+
* A link to add to the section title. This link can be used to show
|
|
32
|
+
* more content related to the section.
|
|
33
|
+
*
|
|
34
|
+
* The link will not be transformed. You need to make sure to prepend the current locale.
|
|
35
|
+
*/
|
|
36
|
+
titleHref?: string
|
|
35
37
|
/**
|
|
36
38
|
* Css classes to set to the section element.
|
|
37
39
|
*/
|
|
38
40
|
className?: string
|
|
41
|
+
/**
|
|
42
|
+
* Css classes to set to the title element.
|
|
43
|
+
*/
|
|
44
|
+
titleClass?: string
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
const {
|
|
42
48
|
id,
|
|
43
49
|
maxWidth = "wide",
|
|
44
50
|
marginTop = "lg",
|
|
45
|
-
|
|
51
|
+
titleHref,
|
|
46
52
|
title,
|
|
47
53
|
className,
|
|
54
|
+
titleClass,
|
|
48
55
|
} = Astro.props
|
|
49
56
|
|
|
50
57
|
const maxWidths = {
|
|
@@ -60,22 +67,29 @@ const marginTopValues = {
|
|
|
60
67
|
---
|
|
61
68
|
|
|
62
69
|
<section
|
|
63
|
-
class="mx-auto"
|
|
64
|
-
class:list={[
|
|
65
|
-
maxWidths[maxWidth],
|
|
66
|
-
marginTopValues[marginTop],
|
|
67
|
-
!disableHorizontalPadding && "px-4 md:px-8",
|
|
68
|
-
className,
|
|
69
|
-
]}
|
|
70
|
+
class="mx-auto px-4 md:px-8"
|
|
71
|
+
class:list={[maxWidths[maxWidth], marginTopValues[marginTop], className]}
|
|
70
72
|
id={id}
|
|
73
|
+
aria-label={title}
|
|
71
74
|
>
|
|
72
75
|
{
|
|
73
76
|
title && (
|
|
74
77
|
<h2
|
|
75
78
|
class="mb-10 text-balance text-2xl font-bold text-gray-700 sm:mb-12 sm:text-3xl"
|
|
76
|
-
class:list={[
|
|
79
|
+
class:list={[titleClass]}
|
|
77
80
|
>
|
|
78
|
-
{
|
|
81
|
+
{titleHref ? (
|
|
82
|
+
<a href={titleHref} class="group flex items-center">
|
|
83
|
+
{title}
|
|
84
|
+
<Icon
|
|
85
|
+
className="mdi--chevron-right group-hover:text-primary transition-colors ease-in-out duration-75 text-4xl sm:text-5xl -ms-2"
|
|
86
|
+
ariaLabel=""
|
|
87
|
+
flipIcon={Astro.locals.i18n.direction === "rtl"}
|
|
88
|
+
/>
|
|
89
|
+
</a>
|
|
90
|
+
) : (
|
|
91
|
+
<>{title}</>
|
|
92
|
+
)}
|
|
79
93
|
</h2>
|
|
80
94
|
)
|
|
81
95
|
}
|
|
@@ -68,6 +68,6 @@ function parseCategory(item: unknown) {
|
|
|
68
68
|
categoryEntrySchema,
|
|
69
69
|
item,
|
|
70
70
|
(id) => `Invalid category: ${id}`,
|
|
71
|
-
(id) => `Fix these issues inside "src/content/
|
|
71
|
+
(id) => `Fix these issues inside "src/content/categories/${id}.json":`,
|
|
72
72
|
)
|
|
73
73
|
}
|
|
@@ -8,7 +8,7 @@ Check the [translation status](TRANSLATION-STATUS.md) for an overview of complet
|
|
|
8
8
|
Have you translated LightNet into a new language? Have you fixed a incorrect translation? Great! How about sharing
|
|
9
9
|
your work with others, by adding it to this folder?
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
These are the ways you can contribute your translations:
|
|
12
12
|
|
|
13
13
|
- [Open a GitHub pull-request](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project), if you are a git-native. 🤓
|
|
14
14
|
- [Use the translation-update form](https://github.com/LightNetDev/LightNet/issues/new?template=---03-translations-update.yml) to share your work. ⭐️
|
|
@@ -48,6 +48,18 @@ ln.languages: Languages
|
|
|
48
48
|
# Used on: https://sk8-ministries.pages.dev/en/media
|
|
49
49
|
ln.type: Type
|
|
50
50
|
|
|
51
|
+
# Accessibility label for buttons to show the previous items.
|
|
52
|
+
# This is used on the carousel arrow button.
|
|
53
|
+
#
|
|
54
|
+
# English: Previous
|
|
55
|
+
ln.previous: Previous
|
|
56
|
+
|
|
57
|
+
# Accessibility label for buttons to show the next items.
|
|
58
|
+
# This is used on the carousel arrow button.
|
|
59
|
+
#
|
|
60
|
+
# English: Next
|
|
61
|
+
ln.next: Next
|
|
62
|
+
|
|
51
63
|
# Accessibility label for an external link.
|
|
52
64
|
# This is only "visible" to a screen-reader.
|
|
53
65
|
#
|
package/src/i18n/translations.ts
CHANGED
package/src/layouts/Page.astro
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { ClientRouter } from "astro:transitions"
|
|
3
2
|
import CustomFooter from "virtual:lightnet/components/CustomFooter"
|
|
4
3
|
import CustomHead from "virtual:lightnet/components/CustomHead"
|
|
5
4
|
import config from "virtual:lightnet/config"
|
|
@@ -8,6 +7,7 @@ import { resolveLanguage } from "../i18n/resolve-language"
|
|
|
8
7
|
import Favicon from "./components/Favicon.astro"
|
|
9
8
|
import Header from "./components/Header.astro"
|
|
10
9
|
import PreloadReact from "./components/PreloadReact"
|
|
10
|
+
import ViewTransition from "./components/ViewTransition.astro"
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
title?: string
|
|
@@ -32,10 +32,9 @@ const language = resolveLanguage(currentLocale)
|
|
|
32
32
|
{config.manifest && <link rel="manifest" href={config.manifest} />}
|
|
33
33
|
<link rel="prefetch" href="/api/search.json" />
|
|
34
34
|
<Favicon />
|
|
35
|
-
<
|
|
35
|
+
<ViewTransition />
|
|
36
36
|
</head>
|
|
37
37
|
<body
|
|
38
|
-
transition:animate="none"
|
|
39
38
|
class="flex min-h-screen flex-col overflow-y-scroll bg-gray-50 text-gray-900"
|
|
40
39
|
>
|
|
41
40
|
<Header />
|
|
@@ -3,10 +3,7 @@ import PageNavigation from "./PageNavigation.astro"
|
|
|
3
3
|
import PageTitle from "./PageTitle.astro"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<header
|
|
7
|
-
class="fixed top-0 z-50 h-14 w-full bg-white shadow-lg sm:h-20"
|
|
8
|
-
transition:animate="none"
|
|
9
|
-
>
|
|
6
|
+
<header class="fixed top-0 z-50 h-14 w-full bg-white shadow-lg sm:h-20">
|
|
10
7
|
<div class="mx-auto flex h-full max-w-screen-xl justify-between px-4 md:px-8">
|
|
11
8
|
<PageTitle />
|
|
12
9
|
<PageNavigation />
|
|
@@ -42,32 +42,23 @@ if (!content.length) {
|
|
|
42
42
|
}
|
|
43
43
|
</ol>
|
|
44
44
|
<script>
|
|
45
|
-
document.
|
|
46
|
-
|
|
47
|
-
})
|
|
48
|
-
initAudioPanel()
|
|
49
|
-
function initAudioPanel() {
|
|
50
|
-
const audioPanel = document.getElementById("audio-panel")
|
|
51
|
-
if (!audioPanel) {
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
const audios = Array.from(audioPanel.querySelectorAll("audio"))
|
|
55
|
-
|
|
56
|
-
audios.forEach((audio, index) => {
|
|
57
|
-
audio.addEventListener("play", () => {
|
|
58
|
-
audios.forEach((otherAudio) => {
|
|
59
|
-
if (otherAudio !== audio) {
|
|
60
|
-
otherAudio.pause()
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
})
|
|
45
|
+
const audioPanel = document.getElementById("audio-panel")
|
|
46
|
+
const audios = Array.from(audioPanel?.querySelectorAll("audio") ?? [])
|
|
64
47
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
audios.forEach((audio, index) => {
|
|
49
|
+
audio.addEventListener("play", () => {
|
|
50
|
+
audios.forEach((otherAudio) => {
|
|
51
|
+
if (otherAudio !== audio) {
|
|
52
|
+
otherAudio.pause()
|
|
69
53
|
}
|
|
70
54
|
})
|
|
71
55
|
})
|
|
72
|
-
|
|
56
|
+
|
|
57
|
+
audio.addEventListener("ended", () => {
|
|
58
|
+
const nextAudio = audios[index + 1]
|
|
59
|
+
if (nextAudio) {
|
|
60
|
+
nextAudio.play()
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
})
|
|
73
64
|
</script>
|
|
@@ -22,37 +22,25 @@ interface Props {
|
|
|
22
22
|
</div>
|
|
23
23
|
</div>
|
|
24
24
|
<script>
|
|
25
|
-
document.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const btn = document.querySelector("#share-btn")
|
|
26
|
+
btn?.addEventListener("click", () => {
|
|
27
|
+
if (navigator.share) {
|
|
28
|
+
navigator
|
|
29
|
+
.share({
|
|
30
|
+
url: window.location.href,
|
|
31
|
+
})
|
|
32
|
+
.catch((e) => console.debug("Could not share", e))
|
|
33
|
+
} else {
|
|
34
|
+
navigator.clipboard
|
|
35
|
+
.writeText(window.location.href)
|
|
36
|
+
.then(() => {
|
|
37
|
+
const toast = document.querySelector<HTMLElement>("#share-success")!
|
|
38
|
+
toast.style.opacity = "100%"
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
toast.style.opacity = "0%"
|
|
41
|
+
}, 3000)
|
|
42
|
+
})
|
|
43
|
+
.catch((error) => console.log("Error copying URL to clipboard:", error))
|
|
34
44
|
}
|
|
35
|
-
|
|
36
|
-
if (navigator.share) {
|
|
37
|
-
navigator
|
|
38
|
-
.share({
|
|
39
|
-
url: window.location.href,
|
|
40
|
-
})
|
|
41
|
-
.catch((e) => console.debug("Could not share", e))
|
|
42
|
-
} else {
|
|
43
|
-
navigator.clipboard
|
|
44
|
-
.writeText(window.location.href)
|
|
45
|
-
.then(() => {
|
|
46
|
-
const toast = document.querySelector<HTMLElement>("#share-success")!
|
|
47
|
-
toast.style.opacity = "100%"
|
|
48
|
-
setTimeout(() => {
|
|
49
|
-
toast.style.opacity = "0%"
|
|
50
|
-
}, 3000)
|
|
51
|
-
})
|
|
52
|
-
.catch((error) =>
|
|
53
|
-
console.log("Error copying URL to clipboard:", error),
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
}
|
|
45
|
+
})
|
|
58
46
|
</script>
|
|
@@ -4,29 +4,13 @@ import { useEffect, useMemo, useRef, useState } from "react"
|
|
|
4
4
|
import type { SearchItem, SearchResponse } from "../../api/search-response"
|
|
5
5
|
import { observeSearchQuery, type SearchQuery } from "../utils/search-query"
|
|
6
6
|
|
|
7
|
-
declare global {
|
|
8
|
-
interface Window {
|
|
9
|
-
lnSearchState?: {
|
|
10
|
-
fuse: Fuse<SearchItem>
|
|
11
|
-
items: SearchItem[]
|
|
12
|
-
locale?: string
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
7
|
interface Context {
|
|
18
8
|
categories: Record<string, string>
|
|
19
9
|
mediaTypes: Record<string, { name: string }>
|
|
20
10
|
languages: Record<string, { name: string }>
|
|
21
|
-
currentLocale?: string
|
|
22
11
|
}
|
|
23
12
|
|
|
24
|
-
export function useSearch({
|
|
25
|
-
currentLocale,
|
|
26
|
-
categories,
|
|
27
|
-
mediaTypes,
|
|
28
|
-
languages,
|
|
29
|
-
}: Context) {
|
|
13
|
+
export function useSearch({ categories, mediaTypes, languages }: Context) {
|
|
30
14
|
const fuse = useRef<Fuse<SearchItem>>(undefined)
|
|
31
15
|
const [allItems, setAllItems] = useState<SearchItem[]>([])
|
|
32
16
|
const [isLoading, setIsLoading] = useState(true)
|
|
@@ -83,27 +67,12 @@ export function useSearch({
|
|
|
83
67
|
ignoreLocation: true,
|
|
84
68
|
})
|
|
85
69
|
setAllItems(items)
|
|
86
|
-
window.lnSearchState = {
|
|
87
|
-
locale: currentLocale,
|
|
88
|
-
items,
|
|
89
|
-
fuse: fuse.current,
|
|
90
|
-
}
|
|
91
70
|
} catch (error) {
|
|
92
71
|
console.error(error)
|
|
93
72
|
}
|
|
94
73
|
setIsLoading(false)
|
|
95
74
|
}
|
|
96
|
-
|
|
97
|
-
// locale is still the same because we add translated values to the
|
|
98
|
-
// search index
|
|
99
|
-
const { lnSearchState } = window
|
|
100
|
-
if (lnSearchState && lnSearchState.locale === currentLocale) {
|
|
101
|
-
fuse.current = lnSearchState.fuse
|
|
102
|
-
setAllItems(lnSearchState.items)
|
|
103
|
-
setIsLoading(false)
|
|
104
|
-
} else {
|
|
105
|
-
fetchData()
|
|
106
|
-
}
|
|
75
|
+
fetchData()
|
|
107
76
|
return removeSearchQueryObserver
|
|
108
77
|
}, [])
|
|
109
78
|
|
package/src/utils/paths.ts
CHANGED
|
@@ -21,12 +21,26 @@ export function detailsPagePath(
|
|
|
21
21
|
*/
|
|
22
22
|
export function searchPagePath(
|
|
23
23
|
locale: string | undefined,
|
|
24
|
-
filter?: {
|
|
24
|
+
filter?: {
|
|
25
|
+
category?: string
|
|
26
|
+
language?: string
|
|
27
|
+
search?: string
|
|
28
|
+
type?: string
|
|
29
|
+
},
|
|
25
30
|
) {
|
|
26
31
|
const searchParams = new URLSearchParams()
|
|
27
32
|
if (filter?.category) {
|
|
28
33
|
searchParams.append("category", filter.category)
|
|
29
34
|
}
|
|
35
|
+
if (filter?.language) {
|
|
36
|
+
searchParams.append("language", filter.language)
|
|
37
|
+
}
|
|
38
|
+
if (filter?.search) {
|
|
39
|
+
searchParams.append("search", filter.search)
|
|
40
|
+
}
|
|
41
|
+
if (filter?.type) {
|
|
42
|
+
searchParams.append("type", filter.type)
|
|
43
|
+
}
|
|
30
44
|
const query = searchParams.size ? `?${searchParams.toString()}` : ""
|
|
31
45
|
return `/${locale}/media${query}`
|
|
32
46
|
}
|