lightnet 3.4.6 → 3.5.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 +42 -10
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +2 -2
- package/exports/components.ts +1 -0
- package/package.json +8 -7
- package/src/astro-integration/config.ts +6 -3
- package/src/components/HeroSection.astro +35 -7
- package/src/components/SearchSection.astro +16 -0
- package/src/components/Section.astro +20 -4
- package/src/i18n/translations/TRANSLATION-STATUS.md +1 -1
- package/src/i18n/translations/de.yml +1 -2
- package/src/i18n/translations/en.yml +3 -8
- package/src/i18n/translations/ru.yml +1 -2
- package/src/i18n/translations/uk.yml +1 -2
- package/src/i18n/translations.ts +0 -1
- package/src/layouts/Page.astro +1 -0
- package/src/layouts/components/PageNavigation.astro +13 -7
- package/src/pages/details-page/components/main-details/AudioPlayer.astro +1 -1
- package/src/pages/details-page/components/main-details/Cover.astro +1 -0
- package/src/pages/search-page/SearchPageRoute.astro +10 -40
- package/src/pages/search-page/components/LoadingSkeleton.tsx +19 -0
- package/src/pages/search-page/components/SearchFilter.astro +65 -0
- package/src/pages/search-page/components/SearchFilter.tsx +33 -84
- package/src/pages/search-page/components/SearchList.astro +50 -0
- package/src/pages/search-page/components/SearchList.tsx +117 -0
- package/src/pages/search-page/components/SearchListItem.tsx +105 -0
- package/src/pages/search-page/components/Select.tsx +5 -5
- package/src/pages/search-page/hooks/use-search-query-param.ts +31 -0
- package/src/pages/search-page/hooks/use-search.ts +103 -49
- package/src/pages/search-page/utils/search-filter-translations.ts +20 -0
- package/src/pages/search-page/utils/search-query.ts +76 -0
- package/src/pages/search-page/utils/search-translations.ts +1 -12
- package/src/pages/search-page/Search.tsx +0 -71
- package/src/pages/search-page/components/ResultList.tsx +0 -135
- package/src/pages/search-page/types.ts +0 -11
- package/src/pages/search-page/utils/use-provided-translations.ts +0 -5
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
import config from "virtual:lightnet/config"
|
|
3
|
+
|
|
4
|
+
import { getCategories } from "../../../content/get-categories"
|
|
5
|
+
import { contentLanguages } from "../../../content/get-languages"
|
|
6
|
+
import { getMediaTypes } from "../../../content/get-media-types"
|
|
7
|
+
import { provideTranslations } from "../utils/search-filter-translations"
|
|
8
|
+
import SearchFilterReact from "./SearchFilter.tsx"
|
|
9
|
+
|
|
10
|
+
const { t, currentLocale } = Astro.locals.i18n
|
|
11
|
+
|
|
12
|
+
const sortByName = (array: { id: string; name: string }[]) =>
|
|
13
|
+
array.sort((a, b) => a.name.localeCompare(b.name, currentLocale))
|
|
14
|
+
|
|
15
|
+
const categories = (await getCategories(currentLocale, t)).map(
|
|
16
|
+
({ id, name }) => ({ id, name }),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const mediaTypes = sortByName(
|
|
20
|
+
(await getMediaTypes()).map((type) => ({
|
|
21
|
+
id: type.id,
|
|
22
|
+
name: t(type.data.label),
|
|
23
|
+
})),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const languages = sortByName(
|
|
27
|
+
contentLanguages.map((language) => ({
|
|
28
|
+
id: language.code,
|
|
29
|
+
name: t(language.label),
|
|
30
|
+
})),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const languageFilterEnabled = contentLanguages.length > 1
|
|
34
|
+
const typesFilterEnabled = mediaTypes.length > 1
|
|
35
|
+
// Not every media item has a category. So it makes
|
|
36
|
+
// sense to have the filter when there is only one category.
|
|
37
|
+
const categoriesFilterEnabled = categories.length > 0
|
|
38
|
+
|
|
39
|
+
const filterByLocale = !!config.searchPage?.filterByLocale
|
|
40
|
+
let initialLanguageFilter: string | undefined = undefined
|
|
41
|
+
const hasContentLanguage = contentLanguages.find(
|
|
42
|
+
({ code }) => code === currentLocale,
|
|
43
|
+
)
|
|
44
|
+
if (
|
|
45
|
+
filterByLocale &&
|
|
46
|
+
currentLocale &&
|
|
47
|
+
hasContentLanguage &&
|
|
48
|
+
languageFilterEnabled
|
|
49
|
+
) {
|
|
50
|
+
initialLanguageFilter = currentLocale
|
|
51
|
+
}
|
|
52
|
+
const translations = provideTranslations(t)
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<SearchFilterReact
|
|
56
|
+
client:load
|
|
57
|
+
languages={languages}
|
|
58
|
+
mediaTypes={mediaTypes}
|
|
59
|
+
categories={categories}
|
|
60
|
+
translations={translations}
|
|
61
|
+
languageFilterEnabled={languageFilterEnabled}
|
|
62
|
+
typesFilterEnabled={typesFilterEnabled}
|
|
63
|
+
categoriesFilterEnabled={categoriesFilterEnabled}
|
|
64
|
+
initialLanguage={initialLanguageFilter}
|
|
65
|
+
/>
|
|
@@ -1,104 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useRef } from "react"
|
|
2
2
|
|
|
3
3
|
import Icon from "../../../components/Icon"
|
|
4
4
|
import { useDebounce } from "../hooks/use-debounce"
|
|
5
|
-
import
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { useSearchQueryParam } from "../hooks/use-search-query-param"
|
|
6
|
+
import type {
|
|
7
|
+
TranslationKey,
|
|
8
|
+
Translations,
|
|
9
|
+
} from "../utils/search-filter-translations"
|
|
10
|
+
import { CATEGORY, LANGUAGE, SEARCH, TYPE } from "../utils/search-query"
|
|
9
11
|
import Select from "./Select"
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
const SEARCH = "search"
|
|
13
|
-
const LANGUAGE = "language"
|
|
14
|
-
const TYPE = "type"
|
|
15
|
-
const CATEGORY = "category"
|
|
13
|
+
type FilterValue = { id: string; name: string }
|
|
16
14
|
|
|
17
15
|
interface Props {
|
|
18
|
-
|
|
19
|
-
categories:
|
|
20
|
-
mediaTypes:
|
|
21
|
-
locale?: string
|
|
16
|
+
languages: FilterValue[]
|
|
17
|
+
categories: FilterValue[]
|
|
18
|
+
mediaTypes: FilterValue[]
|
|
22
19
|
translations: Translations
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
languageFilterEnabled: boolean
|
|
21
|
+
typesFilterEnabled: boolean
|
|
22
|
+
categoriesFilterEnabled: boolean
|
|
23
|
+
initialLanguage?: string
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export default function SearchFilter({
|
|
28
27
|
categories,
|
|
29
28
|
mediaTypes,
|
|
30
|
-
updateQuery,
|
|
31
29
|
translations,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
languages,
|
|
31
|
+
languageFilterEnabled,
|
|
32
|
+
typesFilterEnabled,
|
|
33
|
+
categoriesFilterEnabled,
|
|
34
|
+
initialLanguage,
|
|
35
35
|
}: Props) {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const categoriesFilterEnabled = Object.keys(categories).length > 0
|
|
41
|
-
|
|
42
|
-
const getSearchParam = (name: string, defaultValue = "") => {
|
|
43
|
-
// be lazy to avoid parsing search params all the time
|
|
44
|
-
return () => {
|
|
45
|
-
const searchParams = new URLSearchParams(window.location.search)
|
|
46
|
-
return searchParams.get(name) ?? defaultValue
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const [search, setSearch] = useState(getSearchParam(SEARCH))
|
|
51
|
-
const [language, setLanguage] = useState(() => {
|
|
52
|
-
let initialLanguageFilter = ""
|
|
53
|
-
const hasContentLanguage = contentLanguages.find(
|
|
54
|
-
({ code }) => code === locale,
|
|
55
|
-
)
|
|
56
|
-
if (
|
|
57
|
-
filterByLocale &&
|
|
58
|
-
locale &&
|
|
59
|
-
hasContentLanguage &&
|
|
60
|
-
languageFilterEnabled
|
|
61
|
-
) {
|
|
62
|
-
initialLanguageFilter = locale
|
|
63
|
-
}
|
|
64
|
-
return getSearchParam(LANGUAGE, initialLanguageFilter)()
|
|
65
|
-
})
|
|
66
|
-
const [type, setType] = useState(getSearchParam(TYPE))
|
|
67
|
-
const [category, setCategory] = useState(getSearchParam(CATEGORY))
|
|
36
|
+
const [search, setSearch] = useSearchQueryParam(SEARCH)
|
|
37
|
+
const [language, setLanguage] = useSearchQueryParam(LANGUAGE, initialLanguage)
|
|
38
|
+
const [type, setType] = useSearchQueryParam(TYPE)
|
|
39
|
+
const [category, setCategory] = useSearchQueryParam(CATEGORY)
|
|
68
40
|
|
|
69
41
|
const searchInput = useRef<HTMLInputElement | null>(null)
|
|
70
42
|
|
|
71
|
-
const t =
|
|
43
|
+
const t = (key: TranslationKey) => translations[key]
|
|
72
44
|
|
|
73
45
|
const debouncedSetSearch = useDebounce((value: string) => {
|
|
74
46
|
setSearch(value)
|
|
75
47
|
}, 300)
|
|
76
48
|
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
const url = new URL(window.location.href)
|
|
79
|
-
const updateSearchParam = (name: string, value: string) => {
|
|
80
|
-
// Only update when value before and after are different and both are non empty.
|
|
81
|
-
if (value === (url.searchParams.get(name) ?? "")) {
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
url.searchParams.set(name, value)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
updateSearchParam(SEARCH, search)
|
|
88
|
-
updateSearchParam(LANGUAGE, language)
|
|
89
|
-
updateSearchParam(TYPE, type)
|
|
90
|
-
updateSearchParam(CATEGORY, category)
|
|
91
|
-
history.replaceState({ ...history.state }, "", url.toString())
|
|
92
|
-
|
|
93
|
-
updateQuery({ search, language, type, category })
|
|
94
|
-
}, [search, language, type, category])
|
|
95
|
-
|
|
96
49
|
return (
|
|
97
50
|
<>
|
|
98
|
-
<label className="dy-input dy-input-bordered mb-2 flex items-center gap-2">
|
|
51
|
+
<label className="dy-input dy-input-bordered mb-2 flex items-center gap-2 rounded-2xl">
|
|
99
52
|
<input
|
|
100
53
|
type="search"
|
|
101
|
-
className="grow"
|
|
54
|
+
className="grow placeholder-gray-500"
|
|
55
|
+
name="search"
|
|
102
56
|
ref={searchInput}
|
|
103
57
|
placeholder={t("ln.search.placeholder")}
|
|
104
58
|
enterKeyHint="search"
|
|
@@ -115,11 +69,8 @@ export default function SearchFilter({
|
|
|
115
69
|
initialValue={language}
|
|
116
70
|
valueChange={(val) => setLanguage(val)}
|
|
117
71
|
options={[
|
|
118
|
-
{ id: "",
|
|
119
|
-
...
|
|
120
|
-
id,
|
|
121
|
-
label,
|
|
122
|
-
})),
|
|
72
|
+
{ id: "", name: t("ln.search.all-languages") },
|
|
73
|
+
...languages,
|
|
123
74
|
]}
|
|
124
75
|
/>
|
|
125
76
|
)}
|
|
@@ -130,7 +81,7 @@ export default function SearchFilter({
|
|
|
130
81
|
initialValue={type}
|
|
131
82
|
valueChange={(val) => setType(val)}
|
|
132
83
|
options={[
|
|
133
|
-
{ id: "",
|
|
84
|
+
{ id: "", name: t("ln.search.all-types") },
|
|
134
85
|
...mediaTypes,
|
|
135
86
|
]}
|
|
136
87
|
/>
|
|
@@ -142,10 +93,8 @@ export default function SearchFilter({
|
|
|
142
93
|
initialValue={category}
|
|
143
94
|
valueChange={(val) => setCategory(val)}
|
|
144
95
|
options={[
|
|
145
|
-
{ id: "",
|
|
146
|
-
...
|
|
147
|
-
.sort((a, b) => a[1].localeCompare(b[1], locale))
|
|
148
|
-
.map(([id, label]) => ({ id, label })),
|
|
96
|
+
{ id: "", name: t("ln.search.all-categories") },
|
|
97
|
+
...categories,
|
|
149
98
|
]}
|
|
150
99
|
/>
|
|
151
100
|
)}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from "astro:content"
|
|
3
|
+
|
|
4
|
+
import { getCategories } from "../../../content/get-categories"
|
|
5
|
+
import { contentLanguages } from "../../../content/get-languages"
|
|
6
|
+
import { getMediaTypes } from "../../../content/get-media-types"
|
|
7
|
+
import { provideTranslations } from "../utils/search-translations"
|
|
8
|
+
import SearchListReact from "./SearchList.tsx"
|
|
9
|
+
|
|
10
|
+
const { t, currentLocale, direction } = Astro.locals.i18n
|
|
11
|
+
|
|
12
|
+
const categories: Record<string, string> = {}
|
|
13
|
+
for (const { id, name } of await getCategories(currentLocale, t)) {
|
|
14
|
+
categories[id] = name
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const mediaTypes = Object.fromEntries(
|
|
18
|
+
(await getMediaTypes()).map((type) => [
|
|
19
|
+
type.id,
|
|
20
|
+
{
|
|
21
|
+
name: t(type.data.label),
|
|
22
|
+
icon: type.data.icon,
|
|
23
|
+
},
|
|
24
|
+
]),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const languages = Object.fromEntries(
|
|
28
|
+
contentLanguages.map((language) => [
|
|
29
|
+
language.code,
|
|
30
|
+
{ direction: language.direction, name: t(language.label) },
|
|
31
|
+
]),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const mediaItemsTotal = (await getCollection("media")).length
|
|
35
|
+
|
|
36
|
+
const translations = provideTranslations(t)
|
|
37
|
+
const showLanguage = contentLanguages.length > 1
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<SearchListReact
|
|
41
|
+
client:load
|
|
42
|
+
currentLocale={currentLocale}
|
|
43
|
+
direction={direction}
|
|
44
|
+
translations={translations}
|
|
45
|
+
categories={categories}
|
|
46
|
+
mediaTypes={mediaTypes}
|
|
47
|
+
languages={languages}
|
|
48
|
+
showLanguage={showLanguage}
|
|
49
|
+
mediaItemsTotal={mediaItemsTotal}
|
|
50
|
+
/>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
2
|
+
import { useEffect, useRef, useState } from "react"
|
|
3
|
+
|
|
4
|
+
import { useSearch } from "../hooks/use-search"
|
|
5
|
+
import type { TranslationKey, Translations } from "../utils/search-translations"
|
|
6
|
+
import LoadingSkeleton from "./LoadingSkeleton"
|
|
7
|
+
import SearchListItem, {
|
|
8
|
+
type MediaType,
|
|
9
|
+
type TranslatedLanguage,
|
|
10
|
+
} from "./SearchListItem"
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
currentLocale: string | undefined
|
|
14
|
+
translations: Translations
|
|
15
|
+
direction: "rtl" | "ltr"
|
|
16
|
+
categories: Record<string, string>
|
|
17
|
+
languages: Record<string, TranslatedLanguage>
|
|
18
|
+
showLanguage: boolean
|
|
19
|
+
mediaTypes: Record<string, MediaType>
|
|
20
|
+
mediaItemsTotal: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function SearchList({
|
|
24
|
+
currentLocale,
|
|
25
|
+
categories,
|
|
26
|
+
translations,
|
|
27
|
+
languages,
|
|
28
|
+
direction,
|
|
29
|
+
showLanguage,
|
|
30
|
+
mediaTypes,
|
|
31
|
+
mediaItemsTotal,
|
|
32
|
+
}: Props) {
|
|
33
|
+
const listRef = useRef<HTMLDivElement | null>(null)
|
|
34
|
+
const [rowHeight, setRowHeight] = useState(256)
|
|
35
|
+
const { results, isLoading } = useSearch({
|
|
36
|
+
currentLocale,
|
|
37
|
+
categories,
|
|
38
|
+
languages,
|
|
39
|
+
mediaTypes,
|
|
40
|
+
})
|
|
41
|
+
const count = isLoading ? mediaItemsTotal : results.length
|
|
42
|
+
|
|
43
|
+
const virtualizer = useWindowVirtualizer({
|
|
44
|
+
count,
|
|
45
|
+
estimateSize: () => rowHeight,
|
|
46
|
+
getItemKey: (index) => (isLoading ? index : results[index].id),
|
|
47
|
+
overscan: 2,
|
|
48
|
+
scrollMargin: listRef.current?.offsetTop ?? 0,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const updateRowHeight = () => {
|
|
53
|
+
// This is the fixed row heights that have a responsive breakpoint
|
|
54
|
+
// If you update here, also update the initial height
|
|
55
|
+
const newRowHeight = window.matchMedia("(min-width: 640px)").matches
|
|
56
|
+
? 256
|
|
57
|
+
: 208
|
|
58
|
+
setRowHeight(newRowHeight)
|
|
59
|
+
}
|
|
60
|
+
const observer = new ResizeObserver(() => updateRowHeight())
|
|
61
|
+
observer.observe(document.body)
|
|
62
|
+
updateRowHeight()
|
|
63
|
+
return () => {
|
|
64
|
+
observer.disconnect()
|
|
65
|
+
}
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const t = (key: TranslationKey) => translations[key]
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<div ref={listRef} className="px-4 md:px-8">
|
|
73
|
+
<ol
|
|
74
|
+
className="relative w-full divide-y divide-gray-200"
|
|
75
|
+
style={{
|
|
76
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{virtualizer.getVirtualItems().map((virtualRow) => {
|
|
80
|
+
const item = results[virtualRow.index]
|
|
81
|
+
return (
|
|
82
|
+
<li
|
|
83
|
+
key={virtualRow.key}
|
|
84
|
+
className="absolute left-0 top-0 block w-full"
|
|
85
|
+
style={{
|
|
86
|
+
height: `${virtualRow.size}px`,
|
|
87
|
+
transform: `translateY(${
|
|
88
|
+
virtualRow.start - virtualizer.options.scrollMargin
|
|
89
|
+
}px)`,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{isLoading ? (
|
|
93
|
+
<LoadingSkeleton direction={direction} />
|
|
94
|
+
) : (
|
|
95
|
+
<SearchListItem
|
|
96
|
+
item={item}
|
|
97
|
+
direction={direction}
|
|
98
|
+
showLanguage={showLanguage}
|
|
99
|
+
currentLocale={currentLocale}
|
|
100
|
+
categories={categories}
|
|
101
|
+
languages={languages}
|
|
102
|
+
mediaTypes={mediaTypes}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
</li>
|
|
106
|
+
)
|
|
107
|
+
})}
|
|
108
|
+
</ol>
|
|
109
|
+
</div>
|
|
110
|
+
{!results.length && !isLoading && (
|
|
111
|
+
<div className="mt-24 text-center font-bold text-gray-500">
|
|
112
|
+
{t("ln.search.no-results")}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import Icon from "../../../components/Icon"
|
|
2
|
+
import { detailsPagePath } from "../../../utils/paths"
|
|
3
|
+
import type { SearchItem } from "../../api/search-response"
|
|
4
|
+
|
|
5
|
+
export type MediaType = {
|
|
6
|
+
name: string
|
|
7
|
+
icon: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type TranslatedLanguage = {
|
|
11
|
+
name: string
|
|
12
|
+
direction: "rtl" | "ltr"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
item: SearchItem
|
|
17
|
+
currentLocale: string | undefined
|
|
18
|
+
direction: "rtl" | "ltr"
|
|
19
|
+
categories: Record<string, string>
|
|
20
|
+
languages: Record<string, TranslatedLanguage>
|
|
21
|
+
showLanguage: boolean
|
|
22
|
+
mediaTypes: Record<string, MediaType>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function SearchListItem({
|
|
26
|
+
item,
|
|
27
|
+
currentLocale,
|
|
28
|
+
categories,
|
|
29
|
+
languages,
|
|
30
|
+
direction,
|
|
31
|
+
showLanguage,
|
|
32
|
+
mediaTypes,
|
|
33
|
+
}: Props) {
|
|
34
|
+
return (
|
|
35
|
+
<a
|
|
36
|
+
href={detailsPagePath(currentLocale, {
|
|
37
|
+
id: item.id,
|
|
38
|
+
})}
|
|
39
|
+
lang={item.language}
|
|
40
|
+
className="group flex h-52 overflow-hidden py-2 transition-colors ease-in-out sm:h-64 md:rounded-sm md:hover:bg-gray-100"
|
|
41
|
+
>
|
|
42
|
+
<div className="flex h-full w-36 shrink-0 flex-col items-start justify-center">
|
|
43
|
+
<img
|
|
44
|
+
className="max-h-36 w-auto max-w-36 rounded-sm object-contain shadow-md"
|
|
45
|
+
src={item.image.src}
|
|
46
|
+
width={item.image.width}
|
|
47
|
+
height={item.image.height}
|
|
48
|
+
alt=""
|
|
49
|
+
decoding="async"
|
|
50
|
+
loading="eager"
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="ms-5 flex grow flex-col justify-center text-xs sm:ms-8">
|
|
55
|
+
<h2 className="mb-1 line-clamp-2 text-balance text-sm font-bold text-gray-700 md:mb-3 md:text-base">
|
|
56
|
+
<Icon
|
|
57
|
+
className={`${mediaTypes[item.type].icon} me-2 align-bottom text-2xl text-gray-700`}
|
|
58
|
+
ariaLabel={mediaTypes[item.type].name}
|
|
59
|
+
/>
|
|
60
|
+
<span>{item.title}</span>
|
|
61
|
+
</h2>
|
|
62
|
+
<div className="flex flex-col flex-wrap items-start gap-2 sm:mb-3 md:flex-row md:items-center md:gap-3">
|
|
63
|
+
{!!item.authors?.length && (
|
|
64
|
+
<p className="mb-1 line-clamp-2 sm:line-clamp-1 md:mb-0 md:text-base">
|
|
65
|
+
{item.authors.join(", ")}
|
|
66
|
+
</p>
|
|
67
|
+
)}
|
|
68
|
+
{showLanguage && (
|
|
69
|
+
<span className="rounded-lg border border-gray-300 px-2 py-1 text-gray-500">
|
|
70
|
+
{languages[item.language].name}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
<ul
|
|
74
|
+
lang={currentLocale}
|
|
75
|
+
className="flex max-h-14 flex-wrap gap-1 overflow-hidden sm:max-h-6"
|
|
76
|
+
>
|
|
77
|
+
{item.categories?.map((category) => (
|
|
78
|
+
<li
|
|
79
|
+
key={category}
|
|
80
|
+
className="rounded-lg bg-gray-200 px-2 py-1 text-gray-600"
|
|
81
|
+
>
|
|
82
|
+
{categories[category]}
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="hidden sm:block">
|
|
89
|
+
<p
|
|
90
|
+
className="line-clamp-3 max-w-screen-sm text-xs"
|
|
91
|
+
lang={item.language}
|
|
92
|
+
dir={languages[item.language].direction}
|
|
93
|
+
>
|
|
94
|
+
{item.description}
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<Icon
|
|
99
|
+
className="mdi--chevron-right md:group-hover:text-primary my-auto me-4 ms-2 hidden shrink-0 text-2xl text-gray-300 sm:block"
|
|
100
|
+
flipIcon={direction === "rtl"}
|
|
101
|
+
ariaLabel=""
|
|
102
|
+
/>
|
|
103
|
+
</a>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
type Props = {
|
|
2
2
|
label: string
|
|
3
|
-
initialValue: string
|
|
3
|
+
initialValue: string | undefined
|
|
4
4
|
valueChange: (value: string) => void
|
|
5
|
-
options: { id: string;
|
|
5
|
+
options: { id: string; name: string }[]
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export default function Select({
|
|
@@ -17,13 +17,13 @@ export default function Select({
|
|
|
17
17
|
{label}
|
|
18
18
|
</span>
|
|
19
19
|
<select
|
|
20
|
-
className="dy-select dy-select-bordered sm:dy-select-sm w-full"
|
|
20
|
+
className="dy-select dy-select-bordered sm:dy-select-sm w-full rounded-xl"
|
|
21
21
|
value={initialValue}
|
|
22
22
|
onChange={(e) => valueChange(e.currentTarget.value)}
|
|
23
23
|
>
|
|
24
|
-
{options.map(({ id,
|
|
24
|
+
{options.map(({ id, name }) => (
|
|
25
25
|
<option key={id} value={id}>
|
|
26
|
-
{
|
|
26
|
+
{name}
|
|
27
27
|
</option>
|
|
28
28
|
))}
|
|
29
29
|
</select>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getSearchQueryParam,
|
|
5
|
+
type SearchQueryParam,
|
|
6
|
+
setSearchQueryParam,
|
|
7
|
+
} from "../utils/search-query"
|
|
8
|
+
|
|
9
|
+
export function useSearchQueryParam(
|
|
10
|
+
paramName: SearchQueryParam,
|
|
11
|
+
initialValue?: string,
|
|
12
|
+
) {
|
|
13
|
+
const [state, updateState] = useState(initialValue)
|
|
14
|
+
const updateParam = (value: string) => {
|
|
15
|
+
setSearchQueryParam(paramName, value)
|
|
16
|
+
updateState(value)
|
|
17
|
+
}
|
|
18
|
+
// sync with url query param
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
// if there is already a parameter set on the url
|
|
21
|
+
// override the initial state
|
|
22
|
+
if (getSearchQueryParam(paramName) !== undefined) {
|
|
23
|
+
updateState(getSearchQueryParam(paramName))
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
if (initialValue !== undefined) {
|
|
27
|
+
setSearchQueryParam(paramName, initialValue)
|
|
28
|
+
}
|
|
29
|
+
}, [])
|
|
30
|
+
return [state, updateParam] as const
|
|
31
|
+
}
|