lightnet 4.1.0 → 4.1.1
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 +12 -0
- package/package.json +4 -4
- package/src/components/VideoPlayer.astro +4 -4
- package/src/i18n/translations/en.yml +24 -21
- package/src/layouts/components/Footer.astro +5 -14
- package/src/layouts/components/MenuItem.astro +4 -4
- package/src/layouts/components/PageNavigation.astro +12 -16
- package/src/pages/details-page/components/AudioPlayer.astro +4 -4
- package/src/pages/details-page/components/ContentSection.astro +37 -39
- package/src/pages/details-page/components/main-details/OpenButton.astro +8 -4
- package/src/pages/details-page/components/main-details/ShareButton.astro +1 -1
- package/src/pages/details-page/utils/create-content-metadata.ts +32 -34
- package/src/utils/is-external-url.ts +34 -0
- package/src/utils/link-attributes.ts +10 -0
- package/src/utils/paths.ts +6 -0
- package/src/utils/urls.ts +12 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# lightnet
|
|
2
2
|
|
|
3
|
+
## 4.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#391](https://github.com/LightNetDev/LightNet/pull/391) [`384e125`](https://github.com/LightNetDev/LightNet/commit/384e1259e3b0d18f7f93e2c83ec2e40952bbe5fd) - Support protocol-less external URLs such as `example.com`
|
|
8
|
+
|
|
9
|
+
- [#391](https://github.com/LightNetDev/LightNet/pull/391) [`384e125`](https://github.com/LightNetDev/LightNet/commit/384e1259e3b0d18f7f93e2c83ec2e40952bbe5fd) - Flip external link icon and share icon on rtl locales.
|
|
10
|
+
|
|
11
|
+
- [#391](https://github.com/LightNetDev/LightNet/pull/391) [`384e125`](https://github.com/LightNetDev/LightNet/commit/384e1259e3b0d18f7f93e2c83ec2e40952bbe5fd) - Details page default to "Open" if they do not know a file extension
|
|
12
|
+
|
|
13
|
+
- [#391](https://github.com/LightNetDev/LightNet/pull/391) [`384e125`](https://github.com/LightNetDev/LightNet/commit/384e1259e3b0d18f7f93e2c83ec2e40952bbe5fd) - Update dependencies
|
|
14
|
+
|
|
3
15
|
## 4.1.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "LightNet makes it easy to run your own digital media library.",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "4.1.
|
|
6
|
+
"version": "4.1.1",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/LightNetDev/lightnet",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@astrojs/react": "^5.0.4",
|
|
55
|
-
"@iconify-json/lucide": "^1.2.
|
|
55
|
+
"@iconify-json/lucide": "^1.2.106",
|
|
56
56
|
"@iconify-json/mdi": "^1.2.3",
|
|
57
57
|
"@iconify/tailwind": "^1.2.0",
|
|
58
58
|
"@tailwindcss/typography": "^0.5.19",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"embla-carousel": "^8.6.0",
|
|
62
62
|
"embla-carousel-wheel-gestures": "^8.1.0",
|
|
63
63
|
"fuse.js": "^7.3.0",
|
|
64
|
-
"i18next": "^26.0.
|
|
64
|
+
"i18next": "^26.0.10",
|
|
65
65
|
"lucide-react": "^1.14.0",
|
|
66
66
|
"marked": "^18.0.3",
|
|
67
67
|
"postcss": "^8.5.14",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@playwright/test": "^1.59.1",
|
|
73
73
|
"@types/react": "^19.2.14",
|
|
74
|
-
"astro": "^6.
|
|
74
|
+
"astro": "^6.3.1",
|
|
75
75
|
"typescript": "^6.0.3",
|
|
76
76
|
"vitest": "^4.1.5",
|
|
77
77
|
"@internal/e2e-test-utils": "^0.0.1"
|
|
@@ -4,6 +4,8 @@ import { getImage } from "astro:assets"
|
|
|
4
4
|
import { Image } from "astro:assets"
|
|
5
5
|
import { ExternalLinkIcon } from "lucide-react"
|
|
6
6
|
|
|
7
|
+
import { getLinkAttributes } from "../utils/link-attributes"
|
|
8
|
+
|
|
7
9
|
interface Props {
|
|
8
10
|
/**
|
|
9
11
|
* Url of the video. This can be YouTube, vimeo, or a mp4 file.
|
|
@@ -116,9 +118,7 @@ async function parseUrl(
|
|
|
116
118
|
) : (
|
|
117
119
|
<a
|
|
118
120
|
class="group relative flex h-full w-full items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 focus-visible:ring-offset-2 focus-visible:ring-offset-black"
|
|
119
|
-
|
|
120
|
-
target="_blank"
|
|
121
|
-
rel="noreferrer noopener"
|
|
121
|
+
{...getLinkAttributes(href ?? url)}
|
|
122
122
|
>
|
|
123
123
|
{imageMetadata ? (
|
|
124
124
|
<Image
|
|
@@ -145,7 +145,7 @@ async function parseUrl(
|
|
|
145
145
|
</span>
|
|
146
146
|
</div>
|
|
147
147
|
<div class="absolute bottom-0 end-0 start-0 z-20 flex items-center gap-2 rounded bg-black/70 px-4 py-4 text-sm font-medium text-white backdrop-blur md:text-sm">
|
|
148
|
-
<ExternalLinkIcon className="shrink-0" />
|
|
148
|
+
<ExternalLinkIcon className="shrink-0 rtl:scale-x-[-1]" />
|
|
149
149
|
{t("ln.external-link")}
|
|
150
150
|
</div>
|
|
151
151
|
</a>
|
|
@@ -2,104 +2,106 @@
|
|
|
2
2
|
# This is only "visible" to a screen-reader.
|
|
3
3
|
#
|
|
4
4
|
# English: Open main menu
|
|
5
|
-
# Used on: https://
|
|
5
|
+
# Used on: https://kuuluu.org/en (not visible)
|
|
6
6
|
ln.header.open-main-menu: Open main menu
|
|
7
7
|
|
|
8
8
|
# Accessibility label for the language menu.
|
|
9
9
|
# This is only "visible" to a screen-reader.
|
|
10
10
|
#
|
|
11
11
|
# English: Select language
|
|
12
|
-
# Used on: https://
|
|
12
|
+
# Used on: https://kuuluu.org/en (not visible)
|
|
13
13
|
ln.header.select-language: Select language
|
|
14
14
|
|
|
15
15
|
# Display name for the home page.
|
|
16
16
|
#
|
|
17
17
|
# English: Home
|
|
18
|
-
# Used on: https://
|
|
18
|
+
# Used on: https://kuuluu.org/en (first item in the main menu)
|
|
19
19
|
ln.home.title: Home
|
|
20
20
|
|
|
21
21
|
# Label for a single media item category.
|
|
22
22
|
#
|
|
23
23
|
# English: Category
|
|
24
|
-
# Used on: https://
|
|
24
|
+
# Used on: https://kuuluu.org/en/media
|
|
25
25
|
ln.category: Category
|
|
26
26
|
|
|
27
27
|
# Label for multiple media item categories.
|
|
28
28
|
#
|
|
29
29
|
# English: Categories
|
|
30
|
-
# Used on: https://
|
|
30
|
+
# Used on: https://kuuluu.org/en/media/how-to-360-flip--en/
|
|
31
31
|
ln.categories: Categories
|
|
32
32
|
|
|
33
33
|
# Label for a single language.
|
|
34
34
|
#
|
|
35
35
|
# English: Language
|
|
36
|
-
# Used on: https://
|
|
36
|
+
# Used on: https://kuuluu.org/en/media
|
|
37
37
|
ln.language: Language
|
|
38
38
|
|
|
39
39
|
# Label for multiple languages.
|
|
40
40
|
#
|
|
41
41
|
# English: Languages
|
|
42
|
-
# Used on: https://
|
|
42
|
+
# Used on: https://kuuluu.org/en/media/how-to-kickflip--en/
|
|
43
43
|
ln.languages: Languages
|
|
44
44
|
|
|
45
45
|
# Label for a single media type.
|
|
46
46
|
#
|
|
47
47
|
# English: Type
|
|
48
|
-
# Used on: https://
|
|
48
|
+
# Used on: https://kuuluu.org/en/media
|
|
49
49
|
ln.type: Type
|
|
50
50
|
|
|
51
51
|
# Accessibility label for buttons to show the previous items.
|
|
52
52
|
# This is used on the carousel arrow button.
|
|
53
53
|
#
|
|
54
54
|
# English: Previous
|
|
55
|
+
# Used on: https://kuuluu.org/en (not visible)
|
|
55
56
|
ln.previous: Previous
|
|
56
57
|
|
|
57
58
|
# Accessibility label for buttons to show the next items.
|
|
58
59
|
# This is used on the carousel arrow button.
|
|
59
60
|
#
|
|
60
61
|
# English: Next
|
|
62
|
+
# Used on: https://kuuluu.org/en (not visible)
|
|
61
63
|
ln.next: Next
|
|
62
64
|
|
|
63
65
|
# Hint for an external link.
|
|
64
66
|
#
|
|
65
67
|
# English: External link - opens in a new tab
|
|
66
|
-
# Used on: https://
|
|
68
|
+
# Used on: https://kuuluu.org/en/media/ollie-with-integrity--en/
|
|
67
69
|
ln.external-link: External link - opens in a new tab
|
|
68
70
|
|
|
69
71
|
# Title for the search page.
|
|
70
72
|
#
|
|
71
73
|
# English: Search
|
|
72
|
-
# Used on: https://
|
|
74
|
+
# Used on: https://kuuluu.org/en/media/ (not visible)
|
|
73
75
|
ln.search.title: Search
|
|
74
76
|
|
|
75
77
|
# Placeholder text for the search input to search for media items.
|
|
76
78
|
# English: Search media
|
|
77
79
|
#
|
|
78
|
-
# Used on: https://
|
|
80
|
+
# Used on: https://kuuluu.org/en/media/
|
|
79
81
|
ln.search.placeholder: Search media
|
|
80
82
|
|
|
81
83
|
# Filter option to display content in all languages.
|
|
82
84
|
#
|
|
83
85
|
# English: All languages
|
|
84
|
-
# Used on: https://
|
|
86
|
+
# Used on: https://kuuluu.org/en/media/
|
|
85
87
|
ln.search.all-languages: All languages
|
|
86
88
|
|
|
87
89
|
# Filter option to display all media types.
|
|
88
90
|
#
|
|
89
91
|
# English: All types
|
|
90
|
-
# Used on: https://
|
|
92
|
+
# Used on: https://kuuluu.org/en/media/
|
|
91
93
|
ln.search.all-types: All types
|
|
92
94
|
|
|
93
95
|
# Filter option to display all media item categories.
|
|
94
96
|
#
|
|
95
97
|
# English: All categories
|
|
96
|
-
# Used on: https://
|
|
98
|
+
# Used on: https://kuuluu.org/en/media/
|
|
97
99
|
ln.search.all-categories: All categories
|
|
98
100
|
|
|
99
101
|
# Message displayed when no search results are found.
|
|
100
102
|
#
|
|
101
103
|
# English: No results
|
|
102
|
-
# Used on: https://
|
|
104
|
+
# Used on: https://kuuluu.org/en/media/?language=de&type=book
|
|
103
105
|
ln.search.no-results: No results
|
|
104
106
|
|
|
105
107
|
# Button label to open a media item's main content.
|
|
@@ -110,7 +112,7 @@ ln.details.open: Open
|
|
|
110
112
|
# Button label to share a media item.
|
|
111
113
|
#
|
|
112
114
|
# English: Share
|
|
113
|
-
# Used on: https://
|
|
115
|
+
# Used on: https://kuuluu.org/en/media/ollie-with-integrity--en/
|
|
114
116
|
ln.details.share: Share
|
|
115
117
|
|
|
116
118
|
# Button label to start editing an item
|
|
@@ -121,32 +123,33 @@ ln.details.edit: Edit
|
|
|
121
123
|
# Label for indicating that the media item belongs to a collection.
|
|
122
124
|
#
|
|
123
125
|
# English: Part of collection
|
|
124
|
-
# Used on: https://
|
|
126
|
+
# Used on: https://kuuluu.org/en/media/ollie-with-integrity--en/
|
|
125
127
|
ln.details.part-of-collection: Part of collection
|
|
126
128
|
|
|
127
129
|
# Button label for downloading a media item.
|
|
128
130
|
#
|
|
129
131
|
# English: Download
|
|
130
|
-
# Used on: https://
|
|
132
|
+
# Used on: https://kuuluu.org/en/media/ride-the-ramp-of-faith--en/
|
|
131
133
|
ln.details.download: Download
|
|
132
134
|
|
|
133
135
|
# Notification message indicating that the link has been copied to the clipboard.
|
|
134
136
|
#
|
|
135
137
|
# English: Link copied to clipboard
|
|
136
|
-
# Used on: https://
|
|
138
|
+
# Used on: https://kuuluu.org/en/media/ride-the-ramp-of-faith--en/ (Firefox)
|
|
137
139
|
ln.share.url-copied-to-clipboard: Link copied to clipboard
|
|
138
140
|
|
|
139
141
|
# Title for the 404-error page. This page is displayed when a path does not exist.
|
|
140
142
|
#
|
|
141
143
|
# English: Page not found
|
|
142
|
-
# Used on: https://
|
|
144
|
+
# Used on: https://kuuluu.org/unexisting-path
|
|
143
145
|
ln.404.page-not-found: Page not found
|
|
144
146
|
|
|
145
147
|
# Button label on the 404-error page for returning to the home page.
|
|
146
148
|
#
|
|
147
149
|
# English: Go to Home page
|
|
148
|
-
# Used on: https://
|
|
150
|
+
# Used on: https://kuuluu.org/unexisting-path
|
|
149
151
|
ln.404.go-to-the-home-page: Go to Home page
|
|
150
152
|
|
|
151
153
|
# Footer text to give credits to LightNet
|
|
154
|
+
# Used on: https://kuuluu.org/en
|
|
152
155
|
ln.footer.powered-by-lightnet: Powered by LightNet
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import config from "virtual:lightnet/config"
|
|
3
3
|
|
|
4
|
+
import { getLinkAttributes } from "../../utils/link-attributes"
|
|
4
5
|
import { localizePath } from "../../utils/paths"
|
|
5
|
-
import { isExternalUrl } from "../../utils/urls"
|
|
6
6
|
import LightNetLogo from "./LightNetLogo.svg"
|
|
7
7
|
|
|
8
8
|
const { t, tConfigField, currentLocale } = Astro.locals.i18n
|
|
@@ -15,22 +15,16 @@ const footerText = config.footerText
|
|
|
15
15
|
// Prepare footer links using the same locale rules as the main menu.
|
|
16
16
|
const footerLinks = (config.footerLinks ?? []).map(
|
|
17
17
|
({ href, label, requiresLocale }) => {
|
|
18
|
-
const isExternal = isExternalUrl(href)
|
|
19
|
-
|
|
20
18
|
return {
|
|
21
|
-
href:
|
|
22
|
-
isExternal || !requiresLocale
|
|
23
|
-
? href
|
|
24
|
-
: localizePath(currentLocale, href),
|
|
19
|
+
href: requiresLocale ? localizePath(currentLocale, href) : href,
|
|
25
20
|
label: tConfigField(label, config),
|
|
26
|
-
isExternal,
|
|
27
21
|
}
|
|
28
22
|
},
|
|
29
23
|
)
|
|
30
24
|
|
|
31
25
|
// Hide the footer entirely when there is nothing to show.
|
|
32
26
|
const shouldRenderFooter =
|
|
33
|
-
config.credits ||
|
|
27
|
+
config.credits || !!footerText || footerLinks.length > 0
|
|
34
28
|
|
|
35
29
|
if (!shouldRenderFooter) {
|
|
36
30
|
return
|
|
@@ -51,9 +45,7 @@ if (!shouldRenderFooter) {
|
|
|
51
45
|
<Fragment>
|
|
52
46
|
{(footerText || index > 0) && <span class="text-gray-400">·</span>}
|
|
53
47
|
<a
|
|
54
|
-
|
|
55
|
-
target={link.isExternal ? "_blank" : undefined}
|
|
56
|
-
rel={link.isExternal ? "noopener noreferrer" : undefined}
|
|
48
|
+
{...getLinkAttributes(link.href)}
|
|
57
49
|
class="underline-offset-4 hover:underline"
|
|
58
50
|
>
|
|
59
51
|
{link.label}
|
|
@@ -68,8 +60,7 @@ if (!shouldRenderFooter) {
|
|
|
68
60
|
<div class="flex items-center text-sm">
|
|
69
61
|
<a
|
|
70
62
|
class="flex items-center gap-2 text-gray-800 underline-offset-4 hover:underline"
|
|
71
|
-
|
|
72
|
-
target="_blank"
|
|
63
|
+
{...getLinkAttributes("https://lightnet.community")}
|
|
73
64
|
>
|
|
74
65
|
<img
|
|
75
66
|
src={LightNetLogo.src}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { getLinkAttributes } from "../../utils/link-attributes"
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
href: string
|
|
4
6
|
active: boolean
|
|
5
7
|
hreflang?: string
|
|
6
|
-
target?: "_blank" | "_self"
|
|
7
8
|
}
|
|
8
|
-
const { href, hreflang, active
|
|
9
|
+
const { href, hreflang, active } = Astro.props
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
<li>
|
|
12
13
|
<a
|
|
13
|
-
|
|
14
|
+
{...getLinkAttributes(href)}
|
|
14
15
|
hreflang={hreflang}
|
|
15
|
-
target={target}
|
|
16
16
|
class="flex items-center gap-2 px-6 py-3 decoration-gray-800"
|
|
17
17
|
class:list={[active ? "font-bold" : "hover:underline"]}
|
|
18
18
|
>
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { ExternalLinkIcon, MenuIcon, SearchIcon } from "lucide-react"
|
|
3
3
|
import config from "virtual:lightnet/config"
|
|
4
4
|
|
|
5
|
+
import { isExternalUrl } from "../../utils/is-external-url"
|
|
5
6
|
import { localizePath, searchPagePath } from "../../utils/paths"
|
|
6
|
-
import { isExternalUrl } from "../../utils/urls"
|
|
7
7
|
import LanguagePicker from "./LanguagePicker.astro"
|
|
8
8
|
import Menu from "./Menu.astro"
|
|
9
9
|
import MenuItem from "./MenuItem.astro"
|
|
@@ -13,18 +13,15 @@ const { t, tConfigField, currentLocale } = Astro.locals.i18n
|
|
|
13
13
|
|
|
14
14
|
const items = (config.mainMenu ?? []).map(({ href, label, requiresLocale }) => {
|
|
15
15
|
const isExternal = isExternalUrl(href)
|
|
16
|
-
const path =
|
|
17
|
-
isExternal || !requiresLocale ? href : localizePath(currentLocale, href)
|
|
18
16
|
const isActive =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
currentPath === href)
|
|
17
|
+
currentPath === localizePath(currentLocale, href) ||
|
|
18
|
+
currentPath === localizePath(currentLocale, `${href}/`) ||
|
|
19
|
+
currentPath === href
|
|
23
20
|
return {
|
|
24
|
-
|
|
25
|
-
isExternal,
|
|
21
|
+
href: requiresLocale ? localizePath(currentLocale, href) : href,
|
|
26
22
|
labelText: tConfigField(label, config),
|
|
27
23
|
isActive,
|
|
24
|
+
isExternal,
|
|
28
25
|
}
|
|
29
26
|
})
|
|
30
27
|
---
|
|
@@ -49,15 +46,14 @@ const items = (config.mainMenu ?? []).map(({ href, label, requiresLocale }) => {
|
|
|
49
46
|
!!items.length && (
|
|
50
47
|
<Menu label="ln.header.open-main-menu">
|
|
51
48
|
<MenuIcon slot="icon" />
|
|
52
|
-
{items.map(({ labelText,
|
|
53
|
-
<MenuItem
|
|
54
|
-
href={path}
|
|
55
|
-
active={isActive}
|
|
56
|
-
target={isExternal ? "_blank" : "_self"}
|
|
57
|
-
>
|
|
49
|
+
{items.map(({ labelText, href, isActive, isExternal }) => (
|
|
50
|
+
<MenuItem href={href} active={isActive}>
|
|
58
51
|
{labelText}
|
|
59
52
|
{isExternal && (
|
|
60
|
-
<ExternalLinkIcon
|
|
53
|
+
<ExternalLinkIcon
|
|
54
|
+
size="1.2rem"
|
|
55
|
+
className="shrink-0 rtl:scale-x-[-1]"
|
|
56
|
+
/>
|
|
61
57
|
)}
|
|
62
58
|
</MenuItem>
|
|
63
59
|
))}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { ExternalLinkIcon } from "lucide-react"
|
|
3
3
|
|
|
4
|
+
import { getLinkAttributes } from "../../../utils/link-attributes"
|
|
5
|
+
|
|
4
6
|
type Props = {
|
|
5
7
|
src: string
|
|
6
8
|
className?: string
|
|
@@ -20,9 +22,7 @@ const isMp3 = audioExtension.endsWith(".mp3")
|
|
|
20
22
|
<a
|
|
21
23
|
class="group flex w-full items-center gap-4 rounded-2xl border border-gray-200 bg-white px-4 py-4 shadow-sm transition hover:border-gray-300 hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-900/70"
|
|
22
24
|
class:list={[className]}
|
|
23
|
-
|
|
24
|
-
target="_blank"
|
|
25
|
-
rel="noreferrer noopener"
|
|
25
|
+
{...getLinkAttributes(src)}
|
|
26
26
|
aria-label={t("ln.external-link")}
|
|
27
27
|
>
|
|
28
28
|
<span class="flex h-12 w-12 items-center justify-center rounded-full border border-gray-700 text-gray-700 shadow-sm transition group-hover:scale-105">
|
|
@@ -37,7 +37,7 @@ const isMp3 = audioExtension.endsWith(".mp3")
|
|
|
37
37
|
</span>
|
|
38
38
|
<div class="flex h-full grow flex-col gap-3">
|
|
39
39
|
<span class="flex items-center gap-2 font-semibold text-gray-900">
|
|
40
|
-
<ExternalLinkIcon className="shrink-0" />
|
|
40
|
+
<ExternalLinkIcon className="shrink-0 rtl:scale-x-[-1]" />
|
|
41
41
|
{t("ln.external-link")}
|
|
42
42
|
</span>
|
|
43
43
|
<div class="h-2 w-full rounded-full bg-gray-200">
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "lucide-react"
|
|
14
14
|
|
|
15
15
|
import { getMediaItem } from "../../../content/get-media-items"
|
|
16
|
+
import { getLinkAttributes } from "../../../utils/link-attributes"
|
|
16
17
|
import {
|
|
17
18
|
createContentMetadata,
|
|
18
19
|
type UrlType,
|
|
@@ -54,47 +55,44 @@ const iconDirectionClass = direction === "rtl" ? "scale-x-[-1]" : ""
|
|
|
54
55
|
class="mx-auto mt-16 max-w-screen-md overflow-hidden bg-gray-200 md:mt-20 md:rounded-xl"
|
|
55
56
|
>
|
|
56
57
|
{
|
|
57
|
-
content.map(
|
|
58
|
-
|
|
59
|
-
const TypeIcon = typeIcons[type]
|
|
58
|
+
content.map(({ extension, labelText, type, isDownload, url }, index) => {
|
|
59
|
+
const TypeIcon = typeIcons[type]
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
>
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
</span>
|
|
61
|
+
return (
|
|
62
|
+
<li class="group -mt-px px-4 transition-colors ease-in-out hover:bg-gray-300 md:px-8">
|
|
63
|
+
<a
|
|
64
|
+
{...getLinkAttributes(url)}
|
|
65
|
+
class="flex items-center justify-between py-8"
|
|
66
|
+
>
|
|
67
|
+
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gray-800 text-gray-200">
|
|
68
|
+
<TypeIcon size="1.2rem" />
|
|
69
|
+
</span>
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)}
|
|
91
|
-
</a>
|
|
92
|
-
{index !== content.length - 1 && (
|
|
93
|
-
<div class="h-px w-full bg-gray-300" />
|
|
71
|
+
<div class="ms-4 line-clamp-1 shrink grow overflow-hidden sm:ms-8">
|
|
72
|
+
{labelText}
|
|
73
|
+
</div>
|
|
74
|
+
<div class="me-4 ms-2 shrink-0 font-bold uppercase text-gray-600 sm:me-8">
|
|
75
|
+
{extension}
|
|
76
|
+
</div>
|
|
77
|
+
{isDownload ? (
|
|
78
|
+
<>
|
|
79
|
+
<DownloadIcon className="shrink-0 text-gray-600 group-hover:text-gray-800" />
|
|
80
|
+
<span class="sr-only">{t("ln.details.download")}</span>
|
|
81
|
+
</>
|
|
82
|
+
) : (
|
|
83
|
+
<>
|
|
84
|
+
<ChevronRightIcon
|
|
85
|
+
className={`shrink-0 text-gray-600 group-hover:text-gray-800 ${iconDirectionClass}`}
|
|
86
|
+
/>
|
|
87
|
+
<span class="sr-only">{t("ln.details.open")}</span>
|
|
88
|
+
</>
|
|
94
89
|
)}
|
|
95
|
-
</
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
</a>
|
|
91
|
+
{index !== content.length - 1 && (
|
|
92
|
+
<div class="h-px w-full bg-gray-300" />
|
|
93
|
+
)}
|
|
94
|
+
</li>
|
|
95
|
+
)
|
|
96
|
+
})
|
|
99
97
|
}
|
|
100
98
|
</ol>
|
|
@@ -3,6 +3,7 @@ import { ExternalLinkIcon } from "lucide-react"
|
|
|
3
3
|
|
|
4
4
|
import { getMediaItem } from "../../../../content/get-media-items"
|
|
5
5
|
import { getMediaType } from "../../../../content/get-media-types"
|
|
6
|
+
import { getLinkAttributes } from "../../../../utils/link-attributes"
|
|
6
7
|
import { createContentMetadata } from "../../utils/create-content-metadata"
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
@@ -33,11 +34,14 @@ const getOpenActionLabel = () => {
|
|
|
33
34
|
|
|
34
35
|
<a
|
|
35
36
|
class="flex min-w-52 items-center justify-center gap-2 rounded-2xl bg-gray-800 px-6 py-3 font-bold text-gray-100 shadow-sm hover:bg-gray-950 hover:text-gray-300"
|
|
36
|
-
|
|
37
|
-
target={content.target}
|
|
37
|
+
{...getLinkAttributes(content.url)}
|
|
38
38
|
hreflang={item.data.language}
|
|
39
39
|
class:list={[className]}
|
|
40
40
|
>
|
|
41
|
-
{
|
|
42
|
-
|
|
41
|
+
{
|
|
42
|
+
content.isExternal && (
|
|
43
|
+
<ExternalLinkIcon className="shrink-0 rtl:scale-x-[-1]" />
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
{content.isDownload ? t("ln.details.download") : getOpenActionLabel()}
|
|
43
47
|
</a>
|
|
@@ -14,7 +14,7 @@ const { t } = Astro.locals.i18n
|
|
|
14
14
|
class="flex cursor-pointer items-center gap-2 font-bold text-gray-700 underline"
|
|
15
15
|
class:list={[Astro.props.className]}
|
|
16
16
|
id="share-btn"
|
|
17
|
-
><ForwardIcon />
|
|
17
|
+
><ForwardIcon className="rtl:scale-x-[-1]" />
|
|
18
18
|
{t("ln.details.share")}</button
|
|
19
19
|
>
|
|
20
20
|
<Toast id="share-success" variant="success">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isExternalUrl } from "../../../utils/
|
|
1
|
+
import { isExternalUrl } from "../../../utils/is-external-url"
|
|
2
2
|
|
|
3
3
|
export type UrlType =
|
|
4
4
|
| "link"
|
|
@@ -11,36 +11,36 @@ export type UrlType =
|
|
|
11
11
|
|
|
12
12
|
const KNOWN_EXTENSIONS: Record<
|
|
13
13
|
string,
|
|
14
|
-
{ type: UrlType;
|
|
14
|
+
{ type: UrlType; isDownload?: boolean } | undefined
|
|
15
15
|
> = {
|
|
16
|
-
htm: { type: "link"
|
|
17
|
-
html: { type: "link"
|
|
18
|
-
php: { type: "link"
|
|
19
|
-
json: { type: "source"
|
|
20
|
-
xml: { type: "source"
|
|
21
|
-
md: { type: "source"
|
|
22
|
-
svg: { type: "image"
|
|
23
|
-
jpg: { type: "image"
|
|
24
|
-
jpeg: { type: "image"
|
|
25
|
-
png: { type: "image"
|
|
26
|
-
gif: { type: "image"
|
|
27
|
-
ico: { type: "image"
|
|
28
|
-
webp: { type: "image"
|
|
29
|
-
mp3: { type: "audio"
|
|
30
|
-
wav: { type: "audio"
|
|
31
|
-
aac: { type: "audio"
|
|
32
|
-
ogg: { type: "audio"
|
|
33
|
-
mp4: { type: "video"
|
|
34
|
-
webm: { type: "video"
|
|
35
|
-
ogv: { type: "video"
|
|
36
|
-
pdf: { type: "text"
|
|
37
|
-
txt: { type: "text"
|
|
38
|
-
epub: { type: "text" },
|
|
39
|
-
zip: { type: "package" },
|
|
40
|
-
ppt: { type: "text" },
|
|
41
|
-
pptx: { type: "text" },
|
|
42
|
-
doc: { type: "text" },
|
|
43
|
-
docx: { type: "text" },
|
|
16
|
+
htm: { type: "link" },
|
|
17
|
+
html: { type: "link" },
|
|
18
|
+
php: { type: "link" },
|
|
19
|
+
json: { type: "source" },
|
|
20
|
+
xml: { type: "source" },
|
|
21
|
+
md: { type: "source" },
|
|
22
|
+
svg: { type: "image" },
|
|
23
|
+
jpg: { type: "image" },
|
|
24
|
+
jpeg: { type: "image" },
|
|
25
|
+
png: { type: "image" },
|
|
26
|
+
gif: { type: "image" },
|
|
27
|
+
ico: { type: "image" },
|
|
28
|
+
webp: { type: "image" },
|
|
29
|
+
mp3: { type: "audio" },
|
|
30
|
+
wav: { type: "audio" },
|
|
31
|
+
aac: { type: "audio" },
|
|
32
|
+
ogg: { type: "audio" },
|
|
33
|
+
mp4: { type: "video" },
|
|
34
|
+
webm: { type: "video" },
|
|
35
|
+
ogv: { type: "video" },
|
|
36
|
+
pdf: { type: "text" },
|
|
37
|
+
txt: { type: "text" },
|
|
38
|
+
epub: { type: "text", isDownload: true },
|
|
39
|
+
zip: { type: "package", isDownload: true },
|
|
40
|
+
ppt: { type: "text", isDownload: true },
|
|
41
|
+
pptx: { type: "text", isDownload: true },
|
|
42
|
+
doc: { type: "text", isDownload: true },
|
|
43
|
+
docx: { type: "text", isDownload: true },
|
|
44
44
|
} as const
|
|
45
45
|
|
|
46
46
|
export function createContentMetadata({
|
|
@@ -66,16 +66,14 @@ export function createContentMetadata({
|
|
|
66
66
|
|
|
67
67
|
const labelText = customLabel ?? fileName ?? linkName
|
|
68
68
|
const type = KNOWN_EXTENSIONS[extension]?.type ?? "link"
|
|
69
|
-
const
|
|
70
|
-
!hasExtension || !!KNOWN_EXTENSIONS[extension]?.canBeOpened
|
|
69
|
+
const isDownload = KNOWN_EXTENSIONS[extension]?.isDownload
|
|
71
70
|
|
|
72
71
|
return {
|
|
73
72
|
url,
|
|
74
73
|
extension,
|
|
75
74
|
isExternal,
|
|
76
75
|
labelText,
|
|
77
|
-
|
|
76
|
+
isDownload,
|
|
78
77
|
type,
|
|
79
|
-
target: isExternal ? "_blank" : "_self",
|
|
80
78
|
} as const
|
|
81
79
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import config from "virtual:lightnet/config"
|
|
2
|
+
|
|
3
|
+
import { parseUrl } from "./urls"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Test if a given url is outside this site.
|
|
7
|
+
* Will return false if the url is relative or if it
|
|
8
|
+
* starts with the site config from astro config.
|
|
9
|
+
*
|
|
10
|
+
* @param url to test
|
|
11
|
+
* @returns is the url external?
|
|
12
|
+
*/
|
|
13
|
+
export function isExternalUrl(url: string) {
|
|
14
|
+
const parsedUrl = parseUrl(url)
|
|
15
|
+
if (!parsedUrl) {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (config.internalDomains.includes(parsedUrl.hostname)) {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { SITE: site } = import.meta.env
|
|
24
|
+
if (!site) {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parsedSiteUrl = parseUrl(site)
|
|
29
|
+
if (!parsedSiteUrl) {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return !parsedUrl.href.startsWith(parsedSiteUrl.href)
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { isExternalUrl } from "./is-external-url"
|
|
2
|
+
|
|
3
|
+
export function getLinkAttributes(href: string) {
|
|
4
|
+
const isExternal = isExternalUrl(href)
|
|
5
|
+
return {
|
|
6
|
+
href,
|
|
7
|
+
target: isExternal ? "_blank" : "_self",
|
|
8
|
+
rel: isExternal ? "noopener noreferrer" : undefined,
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/utils/paths.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isAbsoluteUrl } from "./urls"
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Prefix a site-internal path with Astro's configured base path.
|
|
3
5
|
*
|
|
@@ -91,6 +93,10 @@ export function searchPagePath(
|
|
|
91
93
|
* @returns resolved path. Eg. '/en/about' for input "en" and "/about"
|
|
92
94
|
*/
|
|
93
95
|
export function localizePath(locale: string | undefined, path: string) {
|
|
96
|
+
if (isAbsoluteUrl(path)) {
|
|
97
|
+
return path
|
|
98
|
+
}
|
|
99
|
+
|
|
94
100
|
return pathWithBase(
|
|
95
101
|
`${locale ? `/${locale}` : ""}/${path.replace(/^\//, "")}`,
|
|
96
102
|
)
|
package/src/utils/urls.ts
CHANGED
|
@@ -1,28 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Test if a given url is outside this site.
|
|
5
|
-
* Will return false if the url is relative or if it
|
|
6
|
-
* starts with the site config from astro config.
|
|
7
|
-
*
|
|
8
|
-
* @param url to test
|
|
9
|
-
* @returns is the url external?
|
|
10
|
-
*/
|
|
11
|
-
export function isExternalUrl(url: string) {
|
|
12
|
-
let parsedUrl
|
|
1
|
+
export function parseUrl(url: string) {
|
|
13
2
|
try {
|
|
14
|
-
|
|
15
|
-
parsedUrl = new URL(url)
|
|
3
|
+
return new URL(url)
|
|
16
4
|
} catch {
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const { SITE: site } = import.meta.env
|
|
24
|
-
if (!site) {
|
|
25
|
-
return true
|
|
5
|
+
// Support host-like values such as `example.com` without treating plain paths as external.
|
|
6
|
+
if (/^(localhost(?::\d+)?|[^/\s]+\.[^/\s]+)(?:[/?#]|$)/.test(url)) {
|
|
7
|
+
return new URL(`https://${url}`)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return null
|
|
26
11
|
}
|
|
27
|
-
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isAbsoluteUrl(url: string) {
|
|
15
|
+
return !!parseUrl(url)
|
|
28
16
|
}
|