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.
Files changed (41) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +23 -11
  3. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  4. package/__e2e__/fixtures/basics/node_modules/.bin/tsc +2 -2
  5. package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +2 -2
  6. package/__e2e__/fixtures/basics/package.json +10 -7
  7. package/__tests__/utils/markdown.spec.ts +4 -0
  8. package/__tests__/utils/urls.spec.ts +27 -0
  9. package/exports/experimental-components.ts +1 -0
  10. package/package.json +18 -10
  11. package/src/astro-integration/integration.ts +6 -0
  12. package/src/components/CarouselSection.astro +182 -0
  13. package/src/components/CategoriesSection.astro +96 -47
  14. package/src/components/MediaGallery.astro +1 -1
  15. package/src/components/SearchInput.astro +1 -0
  16. package/src/components/SearchSection.astro +9 -4
  17. package/src/components/Section.astro +31 -17
  18. package/src/content/get-categories.ts +1 -1
  19. package/src/i18n/translations/README.md +1 -1
  20. package/src/i18n/translations/ar.yml +2 -0
  21. package/src/i18n/translations/bn.yml +2 -0
  22. package/src/i18n/translations/de.yml +2 -0
  23. package/src/i18n/translations/en.yml +12 -0
  24. package/src/i18n/translations/es.yml +2 -0
  25. package/src/i18n/translations/fi.yml +2 -0
  26. package/src/i18n/translations/fr.yml +2 -0
  27. package/src/i18n/translations/hi.yml +2 -0
  28. package/src/i18n/translations/pt.yml +2 -0
  29. package/src/i18n/translations/ru.yml +2 -0
  30. package/src/i18n/translations/uk.yml +2 -0
  31. package/src/i18n/translations/zh.yml +2 -0
  32. package/src/i18n/translations.ts +2 -0
  33. package/src/layouts/Page.astro +2 -3
  34. package/src/layouts/components/Header.astro +1 -4
  35. package/src/layouts/components/ViewTransition.astro +9 -0
  36. package/src/pages/api/versions.ts +7 -0
  37. package/src/pages/details-page/components/AudioPanel.astro +15 -24
  38. package/src/pages/details-page/components/main-details/ShareButton.astro +20 -32
  39. package/src/pages/search-page/components/SearchList.tsx +0 -1
  40. package/src/pages/search-page/hooks/use-search.ts +2 -33
  41. 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
- # LightNet
1
+ ![LightNet](https://github.com/LightNetDev/lightnet/blob/main/lightnet-banner.webp)
2
2
 
3
- Share the message of Jesus and strengthen believers worldwide.
3
+ Share the gospel and strengthen believers in your community.
4
4
 
5
- LightNet empowers ministries to run their own digital media libraries. They can easily share content in the heart language of the communities they serve - including videos, audio, images, and documents.
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
- It is built as an integration for the [Astro framework](https://astro.build), enabling the creation of statically generated sites that can be hosted on any file server. These sites are fast, easily extendable, and fully support internationalization.
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
- [Read the LightNet docs](https://docs.lightnet.community) to learn how to use LightNet.
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
- ## Example site
27
+ ## Showcase
14
28
 
15
- [Check out the example site](/examples/sk8-ministries/) showcasing LightNet in action for a fictional skateboard ministry.
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
- We would love to partner with you. [Visit the contribution guide](https://github.com/LightNetDev/lightnet/blob/main/CONTRIBUTING.md) to learn how to help with LightNet development.
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.11.0_@types+node@24.0.12_jiti@2.4.2_lightningcss@1.29.1_rollup@4.44.2_terser@5.39.0_typescript@5.8.3_yaml@2.8.0/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.11.0_@types+node@24.0.12_jiti@2.4.2_lightningcss@1.29.1_rollup@4.44.2_terser@5.39.0_typescript@5.8.3_yaml@2.8.0/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
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.11.0_@types+node@24.0.12_jiti@2.4.2_lightningcss@1.29.1_rollup@4.44.2_terser@5.39.0_typescript@5.8.3_yaml@2.8.0/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.11.0_@types+node@24.0.12_jiti@2.4.2_lightningcss@1.29.1_rollup@4.44.2_terser@5.39.0_typescript@5.8.3_yaml@2.8.0/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
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.8.3/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.8.3/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
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.8.3/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.8.3/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
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.8.3/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.8.3/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
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.8.3/node_modules/typescript/bin/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/typescript@5.8.3/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
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.1",
10
- "astro": "^5.11.0",
11
- "lightnet": "^3.7.0",
12
- "react": "^19.1.0",
13
- "react-dom": "^19.1.0",
14
- "sharp": "^0.33.5",
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.8.3"
16
+ "typescript": "^5.9.2"
17
+ },
18
+ "engines": {
19
+ "node": ">=22"
17
20
  }
18
21
  }
@@ -52,3 +52,7 @@ test("Should remove block quotes", () => {
52
52
  "block quote\nmore quote",
53
53
  )
54
54
  })
55
+
56
+ test("Should return undefined when markdown is undefined", () => {
57
+ expect(markdownToText(undefined)).toBeUndefined()
58
+ })
@@ -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.7.0",
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.0.0",
51
- "yaml": "^2.8.0"
55
+ "marked": "^16.1.2",
56
+ "yaml": "^2.8.1"
52
57
  },
53
58
  "devDependencies": {
54
- "typescript": "^5.5.3",
55
- "@types/react": "^19.1.8",
56
- "@playwright/test": "^1.53.2",
57
- "@types/node": "^22.16.2",
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 Section from "./Section.astro"
7
+ import CarouselSection from "./CarouselSection.astro"
8
+ import Section, { type Props as SectionProps } from "./Section.astro"
8
9
 
9
- interface Props {
10
- title?: string
11
- layout?: "button-grid" | "image-grid"
10
+ type Props = SectionProps & {
11
+ layout?: "grid" | "carousel"
12
12
  }
13
13
 
14
- const { title, layout = "button-grid" } = Astro.props
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
- `The CategorySection with layout="image-grid" requires an image for category "${id}".`,
24
- `To resolve this issue, either change the layout to "button-grid" or provide an image path in /src/content/categories/${id}.json.`,
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={title ?? t("ln.categories")}>
34
- {layout === "button-grid" && (
35
- <ul class="grid w-full grid-cols-2 flex-wrap gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
36
- {categories.map((category) => (
37
- <li class="grow">
38
- <a
39
- class="flex h-20 w-full items-center justify-center rounded-2xl bg-gray-200 p-3 text-gray-600 shadow-sm transition-colors ease-in-out hover:bg-gray-300"
40
- href={searchPagePath(currentLocale, {
41
- category: category.id,
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
- <div class="line-clamp-3 overflow-hidden text-balance text-center text-xs font-bold">
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, 1024]}
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
- <div class="absolute start-0 top-0 flex h-full w-full flex-col justify-end bg-gradient-to-t from-black/80 via-black/35 via-30% to-transparent to-65% p-4 text-gray-50">
75
- <span class="line-clamp-3 text-balance font-bold">
76
- {category.name}
77
- </span>
78
- </div>
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
- </a>
81
- </li>
82
- ))}
83
- </ol>
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 xl: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
- // we need the semi-colon for the astro compiler :(
7
- // prettier-ignore
8
- type Props = Omit<SectionProps, "disableHorizontalPadding" | "maxWidth">;
6
+ type Props = Omit<SectionProps, "maxWidth">
7
+
8
+ const { className = "", titleClass = "", ...props } = Astro.props
9
9
  ---
10
10
 
11
- <Section {...Astro.props} disableHorizontalPadding={true} maxWidth="narrow">
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
- disableHorizontalPadding = false,
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={[disableHorizontalPadding && "px-4 md:px-8"]}
79
+ class:list={[titleClass]}
77
80
  >
78
- {title}
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/category/${id}.json":`,
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
- This are the ways how you can contribute your translations:
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. ⭐️
@@ -6,6 +6,8 @@ ln.categories: الفئات
6
6
  ln.language: اللغة
7
7
  ln.languages: اللغات
8
8
  ln.type: النوع
9
+ ln.previous: السابق
10
+ ln.next: التالي
9
11
  ln.external-link: رابط خارجي
10
12
  ln.search.title: بحث
11
13
  ln.search.placeholder: ابحث في الوسائط
@@ -6,6 +6,8 @@ ln.categories: বিভাগসমূহ
6
6
  ln.language: ভাষা
7
7
  ln.languages: ভাষাসমূহ
8
8
  ln.type: ধরন
9
+ ln.previous: পূর্ববর্তী
10
+ ln.next: পরবর্তী
9
11
  ln.external-link: বহিরাগত লিঙ্ক
10
12
  ln.search.title: অনুসন্ধান
11
13
  ln.search.placeholder: মিডিয়া অনুসন্ধান করুন
@@ -5,6 +5,8 @@ ln.category: Kategorie
5
5
  ln.language: Sprache
6
6
  ln.languages: Sprachen
7
7
  ln.type: Typ
8
+ ln.previous: Zurück
9
+ ln.next: Weiter
8
10
  ln.external-link: Externer Link
9
11
  ln.details.open: Öffnen
10
12
  ln.details.part-of-collection: Teil der Sammlung
@@ -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
  #
@@ -6,6 +6,8 @@ ln.categories: Categorías
6
6
  ln.language: Idioma
7
7
  ln.languages: Idiomas
8
8
  ln.type: Tipo
9
+ ln.previous: Anterior
10
+ ln.next: Siguiente
9
11
  ln.external-link: Enlace externo
10
12
  ln.search.title: Buscar
11
13
  ln.search.placeholder: Buscar medios
@@ -6,6 +6,8 @@ ln.categories: Kategoriat
6
6
  ln.language: Kieli
7
7
  ln.languages: Kielet
8
8
  ln.type: Mediatyyppi
9
+ ln.previous: Edellinen
10
+ ln.next: Seuraava
9
11
  ln.external-link: Ulkoinen linkki
10
12
  ln.search.title: Haku
11
13
  ln.search.placeholder: Hae mediaa
@@ -6,6 +6,8 @@ ln.categories: Catégories
6
6
  ln.language: Langue
7
7
  ln.languages: Langues
8
8
  ln.type: Type
9
+ ln.previous: Précédent
10
+ ln.next: Suivant
9
11
  ln.external-link: Lien externe
10
12
  ln.search.title: Recherche
11
13
  ln.search.placeholder: Rechercher des médias
@@ -6,6 +6,8 @@ ln.categories: श्रेणियाँ
6
6
  ln.language: भाषा
7
7
  ln.languages: भाषाएँ
8
8
  ln.type: प्रकार
9
+ ln.previous: पिछला
10
+ ln.next: अगला
9
11
  ln.external-link: बाहरी लिंक
10
12
  ln.search.title: खोज
11
13
  ln.search.placeholder: मीडिया खोजें
@@ -6,6 +6,8 @@ ln.categories: Categorias
6
6
  ln.language: Idioma
7
7
  ln.languages: Idiomas
8
8
  ln.type: Tipo
9
+ ln.previous: Anterior
10
+ ln.next: Seguinte
9
11
  ln.external-link: Ligação externa
10
12
  ln.search.title: Pesquisa
11
13
  ln.search.placeholder: Pesquisar conteúdos
@@ -6,6 +6,8 @@ ln.categories: Категории
6
6
  ln.language: Язык
7
7
  ln.languages: Языки
8
8
  ln.type: Тип
9
+ ln.previous: Предыдущий
10
+ ln.next: Следующий
9
11
  ln.external-link: Внешняя ссылка
10
12
  ln.search.title: Поиск
11
13
  ln.search.placeholder: Поиск медиа
@@ -6,6 +6,8 @@ ln.categories: Категорії
6
6
  ln.language: Мова
7
7
  ln.languages: Мови
8
8
  ln.type: Тип
9
+ ln.previous: Попередній
10
+ ln.next: Наступний
9
11
  ln.external-link: Зовнішнє посилання
10
12
  ln.search.title: Пошук
11
13
  ln.search.placeholder: Пошук медіа
@@ -6,6 +6,8 @@ ln.categories: 分类
6
6
  ln.language: 语言
7
7
  ln.languages: 语言
8
8
  ln.type: 类型
9
+ ln.previous: 上一个
10
+ ln.next: 下一个
9
11
  ln.external-link: 外部链接
10
12
  ln.search.title: 搜索
11
13
  ln.search.placeholder: 搜索媒体…
@@ -64,6 +64,8 @@ export type LightNetTranslationKey =
64
64
  | "ln.languages"
65
65
  | "ln.external-link"
66
66
  | "ln.type"
67
+ | "ln.previous"
68
+ | "ln.next"
67
69
  | "ln.details.open"
68
70
  | "ln.details.share"
69
71
  | "ln.details.part-of-collection"
@@ -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
- <ClientRouter />
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 />
@@ -0,0 +1,9 @@
1
+ ---
2
+
3
+ ---
4
+
5
+ <style is:global>
6
+ @view-transition {
7
+ navigation: auto;
8
+ }
9
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { APIRoute } from "astro"
2
+
3
+ import pkg from "../../../package.json" assert { type: "json" }
4
+
5
+ export const GET: APIRoute = () => {
6
+ return new Response(JSON.stringify({ lightnet: pkg.version }))
7
+ }
@@ -42,32 +42,23 @@ if (!content.length) {
42
42
  }
43
43
  </ol>
44
44
  <script>
45
- document.addEventListener("astro:after-swap", () => {
46
- initAudioPanel()
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
- audio.addEventListener("ended", () => {
66
- const nextAudio = audios[index + 1]
67
- if (nextAudio) {
68
- nextAudio.play()
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.addEventListener("astro:after-swap", () => {
26
- initShareButton()
27
- })
28
- initShareButton()
29
-
30
- function initShareButton() {
31
- const btn = document.querySelector("#share-btn")
32
- if (!btn) {
33
- return
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
- btn?.addEventListener("click", () => {
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>
@@ -33,7 +33,6 @@ export default function SearchList({
33
33
  const listRef = useRef<HTMLDivElement | null>(null)
34
34
  const [rowHeight, setRowHeight] = useState(256)
35
35
  const { results, isLoading } = useSearch({
36
- currentLocale,
37
36
  categories,
38
37
  languages,
39
38
  mediaTypes,
@@ -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
- // try restore old search index only if
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
 
@@ -21,12 +21,26 @@ export function detailsPagePath(
21
21
  */
22
22
  export function searchPagePath(
23
23
  locale: string | undefined,
24
- filter?: { category: string },
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
  }