lightnet 3.10.7 → 3.11.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 +25 -0
- package/__e2e__/admin.spec.ts +54 -20
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
- package/__e2e__/fixtures/basics/package.json +5 -5
- package/__tests__/pages/details-page/create-content-metadata.spec.ts +23 -3
- package/package.json +10 -10
- package/src/admin/components/form/DynamicArray.tsx +82 -30
- package/src/admin/components/form/Input.tsx +17 -3
- package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +2 -2
- package/src/admin/components/form/MarkdownEditor.tsx +12 -4
- package/src/admin/components/form/Select.tsx +25 -13
- package/src/admin/components/form/SubmitButton.tsx +3 -3
- package/src/admin/components/form/atoms/Button.tsx +27 -0
- package/src/admin/components/form/atoms/ErrorMessage.tsx +1 -1
- package/src/admin/components/form/atoms/FileUpload.tsx +190 -0
- package/src/admin/components/form/atoms/Hint.tsx +3 -3
- package/src/admin/components/form/atoms/Label.tsx +18 -7
- package/src/admin/components/form/hooks/use-field-error.tsx +23 -2
- package/src/admin/components/form/utils/get-border-class.ts +22 -0
- package/src/admin/i18n/admin-i18n.ts +21 -0
- package/src/admin/i18n/translations/en.yml +24 -2
- package/src/admin/pages/AdminRoute.astro +1 -3
- package/src/admin/pages/media/EditForm.tsx +35 -11
- package/src/admin/pages/media/EditRoute.astro +33 -17
- package/src/admin/pages/media/fields/Authors.tsx +15 -5
- package/src/admin/pages/media/fields/Categories.tsx +17 -5
- package/src/admin/pages/media/fields/Collections.tsx +21 -11
- package/src/admin/pages/media/fields/Content.tsx +150 -0
- package/src/admin/pages/media/fields/Image.tsx +119 -0
- package/src/admin/pages/media/file-system.ts +6 -2
- package/src/admin/pages/media/media-item-store.ts +46 -3
- package/src/admin/types/media-item.ts +29 -0
- package/src/astro-integration/config.ts +10 -0
- package/src/astro-integration/integration.ts +7 -3
- package/src/components/SearchInput.astro +3 -3
- package/src/content/get-media-items.ts +2 -1
- package/src/i18n/react/i18n-context.ts +16 -5
- package/src/i18n/react/prepare-i18n-config.ts +1 -1
- package/src/i18n/react/{useI18n.ts → use-i18n.ts} +1 -1
- package/src/i18n/translations/TRANSLATION-STATUS.md +4 -0
- package/src/i18n/translations/ur.yml +25 -0
- package/src/i18n/translations.ts +1 -0
- package/src/layouts/Page.astro +5 -6
- package/src/layouts/components/LanguagePicker.astro +11 -5
- package/src/layouts/components/Menu.astro +76 -10
- package/src/pages/404Route.astro +3 -1
- package/src/pages/details-page/components/main-details/EditButton.astro +1 -1
- package/src/pages/details-page/utils/create-content-metadata.ts +2 -1
- package/src/pages/search-page/components/LoadingSkeleton.tsx +21 -14
- package/src/pages/search-page/components/SearchFilter.tsx +2 -2
- package/src/pages/search-page/components/SearchList.tsx +33 -29
- package/src/pages/search-page/components/SearchListItem.tsx +1 -1
- package/src/pages/search-page/components/Select.tsx +15 -13
- package/src/layouts/components/PreloadReact.tsx +0 -3
|
@@ -72,12 +72,12 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
injectRoute({
|
|
75
|
-
pattern: "/
|
|
75
|
+
pattern: "/admin",
|
|
76
76
|
entrypoint: "lightnet/admin/pages/AdminRoute.astro",
|
|
77
77
|
prerender: true,
|
|
78
78
|
})
|
|
79
79
|
injectRoute({
|
|
80
|
-
pattern: "/
|
|
80
|
+
pattern: "/admin/media/[mediaId]",
|
|
81
81
|
entrypoint: "lightnet/admin/pages/media/EditRoute.astro",
|
|
82
82
|
prerender: true,
|
|
83
83
|
})
|
|
@@ -114,7 +114,11 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
|
114
114
|
locales: resolveLocales(config),
|
|
115
115
|
routing: {
|
|
116
116
|
redirectToDefaultLocale: false,
|
|
117
|
-
|
|
117
|
+
// We need to set this to false to allow for
|
|
118
|
+
// admin paths without locale. But actually
|
|
119
|
+
// the default locale will be prefixed for regular
|
|
120
|
+
// LightNet pages.
|
|
121
|
+
prefixDefaultLocale: false,
|
|
118
122
|
fallbackType: "rewrite",
|
|
119
123
|
},
|
|
120
124
|
},
|
|
@@ -12,11 +12,11 @@ const { t } = Astro.locals.i18n
|
|
|
12
12
|
action={`/${Astro.currentLocale}/media`}
|
|
13
13
|
method="get"
|
|
14
14
|
role="search"
|
|
15
|
-
class="group
|
|
15
|
+
class="group flex w-full rounded-2xl shadow-sm outline outline-2 outline-offset-2 outline-transparent transition-all ease-in-out group-focus-within:outline-gray-400"
|
|
16
16
|
class:list={[Astro.props.className]}
|
|
17
17
|
>
|
|
18
18
|
<input
|
|
19
|
-
class="
|
|
19
|
+
class="grow rounded-s-2xl bg-gray-100/95 px-4 py-3 text-gray-900 placeholder-gray-500 shadow-inner focus:outline-none"
|
|
20
20
|
enterkeyhint="search"
|
|
21
21
|
type="search"
|
|
22
22
|
name="search"
|
|
@@ -24,7 +24,7 @@ const { t } = Astro.locals.i18n
|
|
|
24
24
|
/>
|
|
25
25
|
<button
|
|
26
26
|
type="submit"
|
|
27
|
-
class="
|
|
27
|
+
class="flex items-center rounded-e-2xl border border-gray-100/95 bg-gray-800 px-4 py-3 text-gray-50 hover:bg-gray-950 hover:text-gray-300"
|
|
28
28
|
>
|
|
29
29
|
<Icon className="mdi--magnify" ariaLabel={t("ln.search.title")} />
|
|
30
30
|
</button>
|
|
@@ -59,7 +59,8 @@ async function revertMediaItemEntry({ id, data: mediaItem }: MediaItemEntry) {
|
|
|
59
59
|
...collection,
|
|
60
60
|
collection: collection.collection.id,
|
|
61
61
|
}))
|
|
62
|
-
const image =
|
|
62
|
+
const image =
|
|
63
|
+
(await getEntry("internal-media-image-path", id))?.data.image ?? ""
|
|
63
64
|
return {
|
|
64
65
|
id,
|
|
65
66
|
data: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createContext, useMemo } from "react"
|
|
2
2
|
|
|
3
3
|
export type I18n = {
|
|
4
|
-
t: (key: string) => string
|
|
4
|
+
t: (key: string, params?: Record<string, unknown>) => string
|
|
5
5
|
currentLocale: string
|
|
6
6
|
direction: "rtl" | "ltr"
|
|
7
7
|
}
|
|
@@ -12,6 +12,17 @@ export type I18nConfig = Omit<I18n, "t"> & {
|
|
|
12
12
|
|
|
13
13
|
export const I18nContext = createContext<I18n | undefined>(undefined)
|
|
14
14
|
|
|
15
|
+
const interpolate = (value: string, params?: Record<string, unknown>) => {
|
|
16
|
+
if (!params) {
|
|
17
|
+
return value
|
|
18
|
+
}
|
|
19
|
+
return Object.entries(params ?? {}).reduce(
|
|
20
|
+
(prev, [paramName, paramValue]) =>
|
|
21
|
+
prev.replaceAll(`{{${paramName}}}`, `${paramValue}`),
|
|
22
|
+
value,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
/**
|
|
16
27
|
* Creates the runtime i18n helpers given a prepared configuration.
|
|
17
28
|
* Wraps the raw translation dictionary with a lookup that throws on missing keys.
|
|
@@ -22,16 +33,16 @@ export const createI18n = ({
|
|
|
22
33
|
direction,
|
|
23
34
|
}: I18nConfig) => {
|
|
24
35
|
return useMemo(() => {
|
|
25
|
-
const t = (key: string) => {
|
|
36
|
+
const t = (key: string, params?: Record<string, unknown>) => {
|
|
26
37
|
const value = translations[key]
|
|
27
38
|
if (value) {
|
|
28
|
-
return value
|
|
39
|
+
return interpolate(value, params)
|
|
29
40
|
}
|
|
30
|
-
if (key.match(/^(?:ln|x)\../i)) {
|
|
41
|
+
if (!key || key.match(/^(?:ln|x)\../i)) {
|
|
31
42
|
console.error(`Missing translation for key ${key}`)
|
|
32
43
|
return ""
|
|
33
44
|
}
|
|
34
|
-
return key
|
|
45
|
+
return interpolate(key, params)
|
|
35
46
|
}
|
|
36
47
|
return { t, currentLocale, direction }
|
|
37
48
|
}, [])
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prepares the configuration object passed from an Astro page to the
|
|
2
|
+
* Prepares the configuration object passed from an Astro page to the react i18n context.
|
|
3
3
|
* Resolves every requested translation key (supporting wildcard suffixes like `ln.dashboard.*`)
|
|
4
4
|
* so the React island receives only the strings it needs.
|
|
5
5
|
*
|
|
@@ -4,7 +4,7 @@ import { I18nContext } from "./i18n-context"
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Retrieves the current i18n helpers from context.
|
|
7
|
-
* Must be called inside a
|
|
7
|
+
* Must be called inside a react tree wrapped with `I18nContext.Provider`, otherwise throws.
|
|
8
8
|
*/
|
|
9
9
|
export const useI18n = () => {
|
|
10
10
|
const i18n = useContext(I18nContext)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
ln.header.open-main-menu: مرکزی مینو کھولیں
|
|
2
|
+
ln.header.select-language: زبان منتخب کریں
|
|
3
|
+
ln.home.title: ہوم
|
|
4
|
+
ln.category: زمرہ
|
|
5
|
+
ln.categories: زمروں
|
|
6
|
+
ln.language: زبان
|
|
7
|
+
ln.languages: زبانیں
|
|
8
|
+
ln.type: قسم
|
|
9
|
+
ln.previous: پچھلا
|
|
10
|
+
ln.next: اگلا
|
|
11
|
+
ln.external-link: بیرونی لنک
|
|
12
|
+
ln.search.title: تلاش
|
|
13
|
+
ln.search.placeholder: میڈیا تلاش کریں
|
|
14
|
+
ln.search.all-languages: تمام زبانیں
|
|
15
|
+
ln.search.all-types: تمام اقسام
|
|
16
|
+
ln.search.all-categories: تمام زمرے
|
|
17
|
+
ln.search.no-results: کوئی نتیجہ نہیں ملا
|
|
18
|
+
ln.details.open: کھولیں
|
|
19
|
+
ln.details.share: شیئر کریں
|
|
20
|
+
ln.details.part-of-collection: مجموعے کا حصہ
|
|
21
|
+
ln.details.download: ڈاؤن لوڈ کریں
|
|
22
|
+
ln.share.url-copied-to-clipboard: لنک کاپی ہو گیا
|
|
23
|
+
ln.404.page-not-found: صفحہ نہیں ملا
|
|
24
|
+
ln.404.go-to-the-home-page: ہوم پیج پر جائیں
|
|
25
|
+
ln.footer.powered-by-lightnet: لائٹ نیٹ کے ذریعے چلایا گیا
|
package/src/i18n/translations.ts
CHANGED
|
@@ -18,6 +18,7 @@ const builtInTranslations = {
|
|
|
18
18
|
pt: () => import("./translations/pt.yml?raw"),
|
|
19
19
|
ru: () => import("./translations/ru.yml?raw"),
|
|
20
20
|
uk: () => import("./translations/uk.yml?raw"),
|
|
21
|
+
ur: () => import("./translations/ur.yml?raw"),
|
|
21
22
|
zh: () => import("./translations/zh.yml?raw"),
|
|
22
23
|
} as const
|
|
23
24
|
|
package/src/layouts/Page.astro
CHANGED
|
@@ -7,24 +7,24 @@ import { resolveLanguage } from "../i18n/resolve-language"
|
|
|
7
7
|
import Favicon from "./components/Favicon.astro"
|
|
8
8
|
import Footer from "./components/Footer.astro"
|
|
9
9
|
import Header from "./components/Header.astro"
|
|
10
|
-
import PreloadReact from "./components/PreloadReact"
|
|
11
10
|
import ViewTransition from "./components/ViewTransition.astro"
|
|
12
11
|
|
|
13
12
|
interface Props {
|
|
14
13
|
title?: string
|
|
15
14
|
description?: string
|
|
16
15
|
mainClass?: string
|
|
16
|
+
locale?: string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const { title, description, mainClass } = Astro.props
|
|
19
|
+
const { title, description, mainClass, locale } = Astro.props
|
|
20
20
|
const configTitle = Astro.locals.i18n.t(config.title)
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
const
|
|
22
|
+
const currentLocale = locale ?? Astro.locals.i18n.currentLocale
|
|
23
|
+
const { direction } = resolveLanguage(currentLocale)
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
<!doctype html>
|
|
27
|
-
<html lang={currentLocale} dir={
|
|
27
|
+
<html lang={currentLocale} dir={direction}>
|
|
28
28
|
<head>
|
|
29
29
|
<meta charset="UTF-8" />
|
|
30
30
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -44,6 +44,5 @@ const language = resolveLanguage(currentLocale)
|
|
|
44
44
|
<slot />
|
|
45
45
|
</main>
|
|
46
46
|
{CustomFooter ? <CustomFooter /> : <Footer />}
|
|
47
|
-
<PreloadReact client:idle />
|
|
48
47
|
</body>
|
|
49
48
|
</html>
|
|
@@ -6,6 +6,11 @@ import MenuItem from "./MenuItem.astro"
|
|
|
6
6
|
|
|
7
7
|
const { t, locales } = Astro.locals.i18n
|
|
8
8
|
|
|
9
|
+
const hasLocale =
|
|
10
|
+
Astro.currentLocale &&
|
|
11
|
+
(Astro.url.pathname.startsWith(`/${Astro.currentLocale}/`) ||
|
|
12
|
+
Astro.url.pathname === `/${Astro.currentLocale}`)
|
|
13
|
+
|
|
9
14
|
const translations = locales
|
|
10
15
|
.map((locale) => ({
|
|
11
16
|
locale,
|
|
@@ -17,17 +22,18 @@ const translations = locales
|
|
|
17
22
|
|
|
18
23
|
function currentPathWithLocale(locale: string) {
|
|
19
24
|
const currentPath = Astro.url.pathname
|
|
20
|
-
const currentPathWithoutLocale =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
: currentPath
|
|
25
|
+
const currentPathWithoutLocale = hasLocale
|
|
26
|
+
? currentPath.slice(Astro.currentLocale.length + 1)
|
|
27
|
+
: currentPath
|
|
24
28
|
return localizePath(locale, currentPathWithoutLocale)
|
|
25
29
|
}
|
|
30
|
+
|
|
31
|
+
const disabled = !hasLocale && Astro.url.pathname !== "/"
|
|
26
32
|
---
|
|
27
33
|
|
|
28
34
|
{
|
|
29
35
|
translations.length > 1 && (
|
|
30
|
-
<Menu icon="mdi--web" label="ln.header.select-language">
|
|
36
|
+
<Menu disabled={disabled} icon="mdi--web" label="ln.header.select-language">
|
|
31
37
|
{translations.map(({ label, locale, active, href }) => (
|
|
32
38
|
<MenuItem href={href} hreflang={locale} active={active}>
|
|
33
39
|
{label}
|
|
@@ -4,25 +4,91 @@ import Icon from "../../components/Icon"
|
|
|
4
4
|
interface Props {
|
|
5
5
|
icon: string
|
|
6
6
|
label: string
|
|
7
|
+
disabled?: boolean
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
const { icon, label } = Astro.props
|
|
10
|
+
const { icon, label, disabled } = Astro.props
|
|
10
11
|
---
|
|
11
12
|
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
<ln-menu class="relative flex h-full items-center">
|
|
14
|
+
<button
|
|
15
|
+
disabled={disabled}
|
|
16
|
+
aria-disabled={disabled}
|
|
16
17
|
aria-label={Astro.locals.i18n.t(label)}
|
|
17
|
-
class="flex
|
|
18
|
+
class="flex rounded-md p-3 text-gray-600 hover:text-primary disabled:text-gray-300"
|
|
18
19
|
>
|
|
19
20
|
<Icon className={icon} ariaLabel="" />
|
|
20
|
-
</
|
|
21
|
+
</button>
|
|
21
22
|
|
|
22
23
|
<ul
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
data-menu-panel
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
inert
|
|
27
|
+
class="pointer-events-none absolute end-3 top-full mt-px flex w-48 origin-top scale-y-90 flex-col overflow-hidden rounded-b-md bg-white py-3 opacity-0 shadow-lg transition-all duration-100 ease-out"
|
|
25
28
|
>
|
|
26
29
|
<slot />
|
|
27
30
|
</ul>
|
|
28
|
-
</
|
|
31
|
+
</ln-menu>
|
|
32
|
+
<script>
|
|
33
|
+
class Menu extends HTMLElement {
|
|
34
|
+
menuPanel = this.querySelector<HTMLElement>("[data-menu-panel]")!
|
|
35
|
+
isMenuOpened = false
|
|
36
|
+
handleOutsideClick = (event: MouseEvent) => {
|
|
37
|
+
if (!this.isMenuOpened) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
const target = event.target as Node | null
|
|
41
|
+
if (target && this.contains(target)) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
this.closeMenu()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
toggleMenu() {
|
|
48
|
+
if (this.isMenuOpened) {
|
|
49
|
+
this.closeMenu()
|
|
50
|
+
} else {
|
|
51
|
+
this.openMenu()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
openMenu() {
|
|
56
|
+
if (this.isMenuOpened) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
this.menuPanel.style.opacity = "1"
|
|
60
|
+
this.menuPanel.style.pointerEvents = "auto"
|
|
61
|
+
this.menuPanel.style.transform = "scaleY(1)"
|
|
62
|
+
|
|
63
|
+
this.menuPanel.setAttribute("aria-hidden", "false")
|
|
64
|
+
this.menuPanel.removeAttribute("inert")
|
|
65
|
+
document.addEventListener("click", this.handleOutsideClick)
|
|
66
|
+
this.isMenuOpened = true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
closeMenu() {
|
|
70
|
+
if (!this.isMenuOpened) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
this.menuPanel.style.opacity = "0"
|
|
74
|
+
this.menuPanel.style.pointerEvents = "none"
|
|
75
|
+
this.menuPanel.style.transform = "scaleY(0.9)"
|
|
76
|
+
|
|
77
|
+
this.menuPanel.setAttribute("aria-hidden", "true")
|
|
78
|
+
this.menuPanel.setAttribute("inert", "")
|
|
79
|
+
document.removeEventListener("click", this.handleOutsideClick)
|
|
80
|
+
this.isMenuOpened = false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
connectedCallback() {
|
|
84
|
+
this.querySelector("button")?.addEventListener("click", () =>
|
|
85
|
+
this.toggleMenu(),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
window.addEventListener("beforeunload", () => {
|
|
89
|
+
this.closeMenu()
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
customElements.define("ln-menu", Menu)
|
|
94
|
+
</script>
|
package/src/pages/404Route.astro
CHANGED
|
@@ -7,7 +7,9 @@ import Page from "../layouts/Page.astro"
|
|
|
7
7
|
<p class="pb-8 pt-20 text-center text-3xl opacity-80">
|
|
8
8
|
{Astro.locals.i18n.t("ln.404.page-not-found")}
|
|
9
9
|
</p>
|
|
10
|
-
<a
|
|
10
|
+
<a
|
|
11
|
+
href={`/${Astro.locals.i18n.defaultLocale}`}
|
|
12
|
+
class="rounded-2xl bg-gray-200 px-8 py-4 text-sm font-bold transition-colors ease-in-out hover:bg-gray-400"
|
|
11
13
|
>{Astro.locals.i18n.t("ln.404.go-to-the-home-page")}</a
|
|
12
14
|
>
|
|
13
15
|
</div>
|
|
@@ -14,7 +14,7 @@ const { mediaId } = Astro.props
|
|
|
14
14
|
class="hidden cursor-pointer items-center gap-2 font-bold text-gray-700 underline"
|
|
15
15
|
id="edit-btn"
|
|
16
16
|
data-admin-enabled={config.experimental?.admin?.enabled}
|
|
17
|
-
href={
|
|
17
|
+
href={`/admin/media/${mediaId}`}
|
|
18
18
|
><Icon className="mdi--square-edit-outline" ariaLabel="" />
|
|
19
19
|
{Astro.locals.i18n.t("ln.admin.edit")}</a
|
|
20
20
|
>
|
|
@@ -18,6 +18,7 @@ const KNOWN_EXTENSIONS: Record<
|
|
|
18
18
|
php: { type: "link", canBeOpened: true },
|
|
19
19
|
json: { type: "source", canBeOpened: true },
|
|
20
20
|
xml: { type: "source", canBeOpened: true },
|
|
21
|
+
md: { type: "source", canBeOpened: true },
|
|
21
22
|
svg: { type: "image", canBeOpened: true },
|
|
22
23
|
jpg: { type: "image", canBeOpened: true },
|
|
23
24
|
jpeg: { type: "image", canBeOpened: true },
|
|
@@ -62,7 +63,7 @@ export function createContentMetadata({
|
|
|
62
63
|
const fileName = hasExtension
|
|
63
64
|
? lastPathSegment.slice(0, -(extension.length + 1))
|
|
64
65
|
: undefined
|
|
65
|
-
const label = customLabel
|
|
66
|
+
const label = customLabel || fileName || linkName
|
|
66
67
|
const type = KNOWN_EXTENSIONS[extension]?.type ?? "link"
|
|
67
68
|
const canBeOpened =
|
|
68
69
|
!hasExtension || !!KNOWN_EXTENSIONS[extension]?.canBeOpened
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import Icon from "../../../components/Icon"
|
|
2
|
-
import { useI18n } from "../../../i18n/react/
|
|
2
|
+
import { useI18n } from "../../../i18n/react/use-i18n"
|
|
3
3
|
|
|
4
4
|
export default function LoadingSkeleton() {
|
|
5
5
|
const { direction } = useI18n()
|
|
6
6
|
return (
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
<ul>
|
|
8
|
+
{Array.from({ length: 8 }, (_, index) => (
|
|
9
|
+
<li
|
|
10
|
+
key={index}
|
|
11
|
+
className="flex h-52 animate-pulse items-center overflow-hidden py-2 sm:h-64"
|
|
12
|
+
>
|
|
13
|
+
<div className="h-36 w-36 shrink-0 rounded-md bg-gray-200"></div>
|
|
14
|
+
<div className="ms-5 flex grow flex-col gap-3">
|
|
15
|
+
<div className="h-4 w-1/2 rounded-md bg-gray-200 md:h-6"></div>
|
|
16
|
+
<div className="h-4 w-3/4 rounded-md bg-gray-200 md:h-6"></div>
|
|
17
|
+
<div className="h-4 w-5/6 rounded-md bg-gray-200 md:h-6"></div>
|
|
18
|
+
</div>
|
|
19
|
+
<Icon
|
|
20
|
+
className="my-auto me-4 ms-2 hidden shrink-0 text-2xl text-gray-300 mdi--chevron-right sm:block"
|
|
21
|
+
flipIcon={direction === "rtl"}
|
|
22
|
+
ariaLabel=""
|
|
23
|
+
/>
|
|
24
|
+
</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
20
27
|
)
|
|
21
28
|
}
|
|
@@ -45,10 +45,10 @@ export default function SearchFilter({
|
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
47
|
<>
|
|
48
|
-
<label className="
|
|
48
|
+
<label className="mb-2 flex items-center gap-2 rounded-2xl border border-gray-300 bg-white px-4 py-3 shadow-inner outline outline-2 outline-offset-2 outline-transparent transition-all ease-in-out focus-within:outline-gray-300">
|
|
49
49
|
<input
|
|
50
50
|
type="search"
|
|
51
|
-
className="grow placeholder-gray-500"
|
|
51
|
+
className="grow placeholder-gray-500 focus:outline-none"
|
|
52
52
|
name="search"
|
|
53
53
|
ref={searchInput}
|
|
54
54
|
placeholder={t("ln.search.placeholder")}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
2
|
-
import { useEffect, useRef, useState } from "react"
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react"
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
createI18n,
|
|
@@ -38,11 +38,15 @@ export default function SearchList({
|
|
|
38
38
|
mediaTypes,
|
|
39
39
|
})
|
|
40
40
|
const count = isLoading ? mediaItemsTotal : results.length
|
|
41
|
+
const getItemKey = useCallback(
|
|
42
|
+
(index: number) => (isLoading ? index : results[index].id),
|
|
43
|
+
[isLoading, results],
|
|
44
|
+
)
|
|
41
45
|
|
|
42
46
|
const virtualizer = useWindowVirtualizer({
|
|
43
47
|
count,
|
|
44
48
|
estimateSize: () => rowHeight,
|
|
45
|
-
getItemKey
|
|
49
|
+
getItemKey,
|
|
46
50
|
overscan: 2,
|
|
47
51
|
scrollMargin: listRef.current?.offsetTop ?? 0,
|
|
48
52
|
})
|
|
@@ -68,28 +72,28 @@ export default function SearchList({
|
|
|
68
72
|
return (
|
|
69
73
|
<I18nContext.Provider value={i18n}>
|
|
70
74
|
<div ref={listRef} className="px-4 md:px-8">
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
75
|
+
{isLoading ? (
|
|
76
|
+
<LoadingSkeleton />
|
|
77
|
+
) : (
|
|
78
|
+
<ol
|
|
79
|
+
className="relative w-full divide-y divide-gray-200"
|
|
80
|
+
style={{
|
|
81
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{virtualizer.getVirtualItems().map((virtualRow) => {
|
|
85
|
+
const item = results[virtualRow.index]
|
|
86
|
+
return (
|
|
87
|
+
<li
|
|
88
|
+
key={virtualRow.key}
|
|
89
|
+
className="absolute left-0 top-0 block w-full"
|
|
90
|
+
style={{
|
|
91
|
+
height: `${virtualRow.size}px`,
|
|
92
|
+
transform: `translateY(${
|
|
93
|
+
virtualRow.start - virtualizer.options.scrollMargin
|
|
94
|
+
}px)`,
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
93
97
|
<SearchListItem
|
|
94
98
|
item={item}
|
|
95
99
|
showLanguage={showLanguage}
|
|
@@ -97,11 +101,11 @@ export default function SearchList({
|
|
|
97
101
|
languages={languages}
|
|
98
102
|
mediaTypes={mediaTypes}
|
|
99
103
|
/>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
</li>
|
|
105
|
+
)
|
|
106
|
+
})}
|
|
107
|
+
</ol>
|
|
108
|
+
)}
|
|
105
109
|
</div>
|
|
106
110
|
{!results.length && !isLoading && (
|
|
107
111
|
<div className="mt-24 text-center font-bold text-gray-500">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import CoverImageDecorator from "../../../components/CoverImageDecorator"
|
|
2
2
|
import Icon from "../../../components/Icon"
|
|
3
|
-
import { useI18n } from "../../../i18n/react/
|
|
3
|
+
import { useI18n } from "../../../i18n/react/use-i18n"
|
|
4
4
|
import { detailsPagePath } from "../../../utils/paths"
|
|
5
5
|
import type { SearchItem } from "../api/search-response"
|
|
6
6
|
|
|
@@ -12,21 +12,23 @@ export default function Select({
|
|
|
12
12
|
options,
|
|
13
13
|
}: Props) {
|
|
14
14
|
return (
|
|
15
|
-
<label
|
|
16
|
-
<span className="
|
|
15
|
+
<label>
|
|
16
|
+
<span className="mb-1 mt-2 block text-xs font-bold uppercase text-gray-600">
|
|
17
17
|
{label}
|
|
18
18
|
</span>
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
<div className="rounded-2xl border border-gray-300 bg-white px-4 py-3 shadow-sm outline outline-2 outline-offset-2 outline-transparent transition-all ease-in-out focus-within:outline-gray-300 sm:p-2">
|
|
20
|
+
<select
|
|
21
|
+
className="w-full bg-white focus:outline-none sm:text-sm"
|
|
22
|
+
value={initialValue}
|
|
23
|
+
onChange={(e) => valueChange(e.currentTarget.value)}
|
|
24
|
+
>
|
|
25
|
+
{options.map(({ id, labelText }) => (
|
|
26
|
+
<option key={id} value={id}>
|
|
27
|
+
{labelText}
|
|
28
|
+
</option>
|
|
29
|
+
))}
|
|
30
|
+
</select>
|
|
31
|
+
</div>
|
|
30
32
|
</label>
|
|
31
33
|
)
|
|
32
34
|
}
|