lightnet 3.10.3 → 3.10.5
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/__e2e__/admin.spec.ts +356 -100
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +5 -5
- package/package.json +9 -9
- package/src/admin/components/form/DynamicArray.tsx +74 -0
- package/src/admin/components/form/Input.tsx +36 -0
- package/src/admin/components/form/Select.tsx +22 -20
- package/src/admin/components/form/SubmitButton.tsx +20 -18
- package/src/admin/components/form/atoms/ErrorMessage.tsx +13 -0
- package/src/admin/components/form/atoms/Hint.tsx +3 -3
- package/src/admin/components/form/atoms/Label.tsx +17 -6
- package/src/admin/components/form/atoms/Legend.tsx +20 -0
- package/src/admin/components/form/hooks/use-field-error.tsx +13 -0
- package/src/admin/i18n/translations/en.yml +18 -7
- package/src/admin/pages/media/EditForm.tsx +52 -68
- package/src/admin/pages/media/EditRoute.astro +35 -11
- package/src/admin/pages/media/fields/Authors.tsx +43 -0
- package/src/admin/pages/media/fields/Categories.tsx +64 -0
- package/src/admin/pages/media/fields/Collections.tsx +103 -0
- package/src/admin/pages/media/media-item-store.ts +14 -7
- package/src/admin/types/media-item.ts +38 -2
- package/src/components/CategoriesSection.astro +2 -2
- package/src/components/HeroSection.astro +1 -1
- package/src/components/HighlightSection.astro +1 -1
- package/src/components/MediaGallerySection.astro +3 -3
- package/src/components/MediaList.astro +2 -2
- package/src/components/SearchInput.astro +1 -1
- package/src/content/get-categories.ts +18 -3
- package/src/i18n/react/i18n-context.ts +14 -12
- package/src/i18n/resolve-language.ts +1 -1
- package/src/layouts/MarkdownPage.astro +1 -1
- package/src/layouts/Page.astro +3 -2
- package/src/layouts/components/LanguagePicker.astro +1 -1
- package/src/layouts/components/Menu.astro +1 -1
- package/src/layouts/components/PageNavigation.astro +1 -1
- package/src/pages/details-page/components/main-details/OpenButton.astro +1 -1
- package/src/pages/details-page/components/more-details/Languages.astro +2 -2
- package/src/pages/search-page/components/LoadingSkeleton.tsx +1 -1
- package/src/pages/search-page/components/SearchFilter.astro +7 -7
- package/src/pages/search-page/components/SearchFilter.tsx +5 -5
- package/src/pages/search-page/components/SearchList.astro +4 -4
- package/src/pages/search-page/components/SearchListItem.tsx +5 -5
- package/src/pages/search-page/components/Select.tsx +4 -4
- package/src/pages/search-page/hooks/use-search.ts +4 -4
- package/src/admin/components/form/TextInput.tsx +0 -34
- package/src/admin/components/form/atoms/FieldErrors.tsx +0 -22
- package/src/admin/components/form/form-context.ts +0 -4
- package/src/admin/components/form/index.ts +0 -18
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type Control } from "react-hook-form"
|
|
2
|
+
|
|
3
|
+
import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
|
|
4
|
+
import Label from "../../../components/form/atoms/Label"
|
|
5
|
+
import DynamicArray from "../../../components/form/DynamicArray"
|
|
6
|
+
import { useFieldError } from "../../../components/form/hooks/use-field-error"
|
|
7
|
+
import type { MediaItem } from "../../../types/media-item"
|
|
8
|
+
|
|
9
|
+
export default function Collections({
|
|
10
|
+
control,
|
|
11
|
+
collections,
|
|
12
|
+
}: {
|
|
13
|
+
control: Control<MediaItem>
|
|
14
|
+
collections: { id: string; labelText: string }[]
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<DynamicArray
|
|
18
|
+
control={control}
|
|
19
|
+
name="collections"
|
|
20
|
+
label="ln.admin.collections"
|
|
21
|
+
renderElement={(index) => (
|
|
22
|
+
<div className="flex w-full flex-col py-2">
|
|
23
|
+
<CollectionSelect
|
|
24
|
+
collections={collections}
|
|
25
|
+
control={control}
|
|
26
|
+
index={index}
|
|
27
|
+
/>
|
|
28
|
+
<CollectionIndex control={control} index={index} />
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
addButton={{
|
|
32
|
+
label: "ln.admin.add-collection",
|
|
33
|
+
onClick: (append, index) =>
|
|
34
|
+
append(
|
|
35
|
+
{ collection: "" },
|
|
36
|
+
{ focusName: `collections.${index}.collection` },
|
|
37
|
+
),
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function CollectionSelect({
|
|
44
|
+
control,
|
|
45
|
+
collections,
|
|
46
|
+
index,
|
|
47
|
+
}: {
|
|
48
|
+
control: Control<MediaItem>
|
|
49
|
+
collections: { id: string; labelText: string }[]
|
|
50
|
+
index: number
|
|
51
|
+
}) {
|
|
52
|
+
const name = `collections.${index}.collection` as const
|
|
53
|
+
const errorMessage = useFieldError({ name, control })
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<Label for={name} label="ln.admin.name" size="xs" />
|
|
57
|
+
<select
|
|
58
|
+
{...control.register(name)}
|
|
59
|
+
id={name}
|
|
60
|
+
aria-invalid={!!errorMessage}
|
|
61
|
+
className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
|
|
62
|
+
>
|
|
63
|
+
{collections.map(({ id, labelText }) => (
|
|
64
|
+
<option key={id} value={id}>
|
|
65
|
+
{labelText}
|
|
66
|
+
</option>
|
|
67
|
+
))}
|
|
68
|
+
</select>
|
|
69
|
+
<ErrorMessage message={errorMessage} />
|
|
70
|
+
</>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function CollectionIndex({
|
|
75
|
+
control,
|
|
76
|
+
index,
|
|
77
|
+
}: {
|
|
78
|
+
control: Control<MediaItem>
|
|
79
|
+
index: number
|
|
80
|
+
}) {
|
|
81
|
+
const name = `collections.${index}.index` as const
|
|
82
|
+
const errorMessage = useFieldError({ name, control })
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<Label
|
|
86
|
+
for={name}
|
|
87
|
+
label="ln.admin.position-in-collection"
|
|
88
|
+
size="xs"
|
|
89
|
+
className="mt-3"
|
|
90
|
+
/>
|
|
91
|
+
<input
|
|
92
|
+
className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
|
|
93
|
+
aria-invalid={!!errorMessage}
|
|
94
|
+
type="number"
|
|
95
|
+
step={1}
|
|
96
|
+
{...control.register(name, {
|
|
97
|
+
setValueAs: (value) => (value === "" ? undefined : Number(value)),
|
|
98
|
+
})}
|
|
99
|
+
/>
|
|
100
|
+
<ErrorMessage message={errorMessage} />
|
|
101
|
+
</>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { type MediaItem
|
|
1
|
+
import { type MediaItem } from "../../types/media-item"
|
|
2
2
|
import { writeJson } from "./file-system"
|
|
3
3
|
|
|
4
|
-
export const loadMediaItem = (id: string) =>
|
|
5
|
-
fetch(`/api/media/${id}.json`)
|
|
6
|
-
.then((response) => response.json())
|
|
7
|
-
.then((json) => mediaItemSchema.parse(json.content))
|
|
8
|
-
|
|
9
4
|
export const updateMediaItem = async (id: string, item: MediaItem) => {
|
|
10
|
-
return writeJson(`/src/content/media/${id}.json`, item)
|
|
5
|
+
return writeJson(`/src/content/media/${id}.json`, mapToContentSchema(item))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const mapToContentSchema = (item: MediaItem) => {
|
|
9
|
+
return {
|
|
10
|
+
...item,
|
|
11
|
+
authors: flatten(item.authors),
|
|
12
|
+
categories: flatten(item.categories),
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const flatten = <TValue>(valueArray: { value: TValue }[]) => {
|
|
17
|
+
return valueArray.map(({ value }) => value)
|
|
11
18
|
}
|
|
@@ -1,15 +1,51 @@
|
|
|
1
|
-
import { z } from "astro/zod"
|
|
1
|
+
import { type RefinementCtx, z } from "astro/zod"
|
|
2
2
|
|
|
3
3
|
const NON_EMPTY_STRING = "ln.admin.errors.non-empty-string"
|
|
4
4
|
const INVALID_DATE = "ln.admin.errors.invalid-date"
|
|
5
5
|
const REQUIRED = "ln.admin.errors.required"
|
|
6
|
+
const GTE_0 = "ln.admin.errors.gte-0"
|
|
7
|
+
const INTEGER = "ln.admin.errors.integer"
|
|
8
|
+
const UNIQUE_ELEMENTS = "ln.admin.errors.unique-elements"
|
|
9
|
+
|
|
10
|
+
const unique = <TArrayItem>(path: Extract<keyof TArrayItem, string>) => {
|
|
11
|
+
return (values: TArrayItem[], ctx: RefinementCtx) => {
|
|
12
|
+
const seenValues = new Set<unknown>()
|
|
13
|
+
values.forEach((value, index) => {
|
|
14
|
+
if (seenValues.has(value[path])) {
|
|
15
|
+
ctx.addIssue({
|
|
16
|
+
path: [index, path],
|
|
17
|
+
message: UNIQUE_ELEMENTS,
|
|
18
|
+
code: "custom",
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
seenValues.add(value[path])
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
6
25
|
|
|
7
26
|
export const mediaItemSchema = z.object({
|
|
8
27
|
commonId: z.string().nonempty(NON_EMPTY_STRING),
|
|
9
28
|
title: z.string().nonempty(NON_EMPTY_STRING),
|
|
10
29
|
type: z.string().nonempty(REQUIRED),
|
|
11
30
|
language: z.string().nonempty(REQUIRED),
|
|
31
|
+
authors: z
|
|
32
|
+
.object({ value: z.string().nonempty(NON_EMPTY_STRING) })
|
|
33
|
+
.array()
|
|
34
|
+
.superRefine(unique("value")),
|
|
35
|
+
categories: z
|
|
36
|
+
.object({
|
|
37
|
+
value: z.string().nonempty(REQUIRED),
|
|
38
|
+
})
|
|
39
|
+
.array()
|
|
40
|
+
.superRefine(unique("value")),
|
|
41
|
+
collections: z
|
|
42
|
+
.object({
|
|
43
|
+
collection: z.string().nonempty(REQUIRED),
|
|
44
|
+
index: z.number().int(INTEGER).gte(0, GTE_0).optional(),
|
|
45
|
+
})
|
|
46
|
+
.array()
|
|
47
|
+
.superRefine(unique("collection")),
|
|
12
48
|
dateCreated: z.string().date(INVALID_DATE),
|
|
13
49
|
})
|
|
14
50
|
|
|
15
|
-
export type MediaItem = z.
|
|
51
|
+
export type MediaItem = z.input<typeof mediaItemSchema>
|
|
@@ -70,7 +70,7 @@ function getImage({ image, id }: Category) {
|
|
|
70
70
|
]}
|
|
71
71
|
>
|
|
72
72
|
<span class="line-clamp-3 select-none text-balance font-bold">
|
|
73
|
-
{category.
|
|
73
|
+
{category.labelText}
|
|
74
74
|
</span>
|
|
75
75
|
</div>
|
|
76
76
|
</div>
|
|
@@ -128,7 +128,7 @@ function getImage({ image, id }: Category) {
|
|
|
128
128
|
]}
|
|
129
129
|
>
|
|
130
130
|
<span class="line-clamp-3 select-none text-balance font-bold">
|
|
131
|
-
{category.
|
|
131
|
+
{category.labelText}
|
|
132
132
|
</span>
|
|
133
133
|
</div>
|
|
134
134
|
</div>
|
|
@@ -52,7 +52,7 @@ const subtitleSizes = {
|
|
|
52
52
|
alt=""
|
|
53
53
|
/>
|
|
54
54
|
<div
|
|
55
|
-
class="
|
|
55
|
+
class="absolute top-0 flex h-full w-full flex-col items-center justify-center bg-gradient-radial from-black/30 to-black/40 p-4 text-center text-gray-50"
|
|
56
56
|
class:list={[className]}
|
|
57
57
|
>
|
|
58
58
|
{
|
|
@@ -53,7 +53,7 @@ const { image, id, title, text, link, className, titleClass, textClass } =
|
|
|
53
53
|
{
|
|
54
54
|
link && (
|
|
55
55
|
<a
|
|
56
|
-
class="
|
|
56
|
+
class="inline-flex items-center justify-center gap-2 rounded-2xl bg-primary px-6 py-3 text-sm font-bold uppercase text-gray-50 shadow-sm hover:bg-primary/85 hover:text-gray-100"
|
|
57
57
|
href={link.href}
|
|
58
58
|
>
|
|
59
59
|
{link.text}
|
|
@@ -38,7 +38,7 @@ const t = Astro.locals.i18n.t
|
|
|
38
38
|
const types = Object.fromEntries(
|
|
39
39
|
(await getMediaTypes()).map((type) => [
|
|
40
40
|
type.id,
|
|
41
|
-
{ ...type.data,
|
|
41
|
+
{ ...type.data, labelText: t(type.data.label) },
|
|
42
42
|
]),
|
|
43
43
|
)
|
|
44
44
|
|
|
@@ -119,7 +119,7 @@ const coverImageStyle =
|
|
|
119
119
|
<span class="line-clamp-2 h-12 text-balance text-sm font-bold text-gray-700">
|
|
120
120
|
<Icon
|
|
121
121
|
className={`${types[item.data.type.id].icon} me-2 align-bottom`}
|
|
122
|
-
ariaLabel={types[item.data.type.id].
|
|
122
|
+
ariaLabel={types[item.data.type.id].labelText}
|
|
123
123
|
/>
|
|
124
124
|
{item.data.title}
|
|
125
125
|
</span>
|
|
@@ -159,7 +159,7 @@ const coverImageStyle =
|
|
|
159
159
|
<span class="line-clamp-2 h-12 text-balance text-sm font-bold text-gray-700">
|
|
160
160
|
<Icon
|
|
161
161
|
className={`${types[item.data.type.id].icon} me-2 align-bottom`}
|
|
162
|
-
ariaLabel={types[item.data.type.id].
|
|
162
|
+
ariaLabel={types[item.data.type.id].labelText}
|
|
163
163
|
/>
|
|
164
164
|
{item.data.title}
|
|
165
165
|
</span>
|
|
@@ -34,7 +34,7 @@ const mediaTypes = Object.fromEntries(
|
|
|
34
34
|
type.id,
|
|
35
35
|
{
|
|
36
36
|
id: type.id,
|
|
37
|
-
|
|
37
|
+
labelText: t(type.data.label),
|
|
38
38
|
icon: type.data.icon,
|
|
39
39
|
coverImageStyle: type.data.coverImageStyle,
|
|
40
40
|
},
|
|
@@ -84,7 +84,7 @@ const mediaTypes = Object.fromEntries(
|
|
|
84
84
|
<p class="mb-1 line-clamp-3 text-balance font-bold text-gray-700 md:mb-3">
|
|
85
85
|
<Icon
|
|
86
86
|
className={`${mediaTypes[item.data.type.id].icon} me-2 align-bottom text-2xl text-gray-700`}
|
|
87
|
-
ariaLabel={mediaTypes[item.data.type.id].
|
|
87
|
+
ariaLabel={mediaTypes[item.data.type.id].labelText}
|
|
88
88
|
/>
|
|
89
89
|
<span>{item.data.title}</span>
|
|
90
90
|
</p>
|
|
@@ -12,7 +12,7 @@ const { t } = Astro.locals.i18n
|
|
|
12
12
|
action={`/${Astro.currentLocale}/media`}
|
|
13
13
|
method="get"
|
|
14
14
|
role="search"
|
|
15
|
-
class="dy-join
|
|
15
|
+
class="group dy-join w-full rounded-2xl shadow-sm outline-2 outline-offset-2 outline-gray-400 group-focus-within:outline"
|
|
16
16
|
class:list={[Astro.props.className]}
|
|
17
17
|
>
|
|
18
18
|
<input
|
|
@@ -33,12 +33,27 @@ const contentCategories = Object.fromEntries(
|
|
|
33
33
|
*
|
|
34
34
|
* @param currentLocale current locale
|
|
35
35
|
* @param t translate function
|
|
36
|
-
* @returns categories sorted by
|
|
36
|
+
* @returns categories sorted by labelText
|
|
37
37
|
*/
|
|
38
38
|
export async function getUsedCategories(currentLocale: string, t: TranslateFn) {
|
|
39
39
|
return [...Object.entries(contentCategories)]
|
|
40
|
-
.map(([id, data]) => ({ id, ...data,
|
|
41
|
-
.sort((a, b) => a.
|
|
40
|
+
.map(([id, data]) => ({ id, ...data, labelText: t(data.label) }))
|
|
41
|
+
.sort((a, b) => a.labelText.localeCompare(b.labelText, currentLocale))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get all categories. This includes categories that are not
|
|
46
|
+
* referenced by any media item. If you only need categories that are referenced
|
|
47
|
+
* by media items use `getUsedCategories`.
|
|
48
|
+
*
|
|
49
|
+
* @param currentLocale current locale
|
|
50
|
+
* @param t translate function
|
|
51
|
+
* @returns categories sorted by labelText
|
|
52
|
+
*/
|
|
53
|
+
export async function getCategories(currentLocale: string, t: TranslateFn) {
|
|
54
|
+
return [...Object.entries(categoriesById)]
|
|
55
|
+
.map(([id, data]) => ({ id, ...data, labelText: t(data.label) }))
|
|
56
|
+
.sort((a, b) => a.labelText.localeCompare(b.labelText, currentLocale))
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
export async function getCategory(id: string) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext } from "react"
|
|
1
|
+
import { createContext, useMemo } from "react"
|
|
2
2
|
|
|
3
3
|
export type I18n = {
|
|
4
4
|
t: (key: string) => string
|
|
@@ -21,16 +21,18 @@ export const createI18n = ({
|
|
|
21
21
|
currentLocale,
|
|
22
22
|
direction,
|
|
23
23
|
}: I18nConfig) => {
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
return useMemo(() => {
|
|
25
|
+
const t = (key: string) => {
|
|
26
|
+
const value = translations[key]
|
|
27
|
+
if (value) {
|
|
28
|
+
return value
|
|
29
|
+
}
|
|
30
|
+
if (key.match(/^(?:ln|x)\../i)) {
|
|
31
|
+
console.error(`Missing translation for key ${key}`)
|
|
32
|
+
return ""
|
|
33
|
+
}
|
|
34
|
+
return key
|
|
28
35
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return ""
|
|
32
|
-
}
|
|
33
|
-
return key
|
|
34
|
-
}
|
|
35
|
-
return { t, currentLocale, direction }
|
|
36
|
+
return { t, currentLocale, direction }
|
|
37
|
+
}, [])
|
|
36
38
|
}
|
|
@@ -9,7 +9,7 @@ type Props = {
|
|
|
9
9
|
|
|
10
10
|
<Page>
|
|
11
11
|
<article
|
|
12
|
-
class="prose
|
|
12
|
+
class="prose mx-auto mt-8 max-w-screen-md px-4 prose-img:rounded-md sm:mt-16 md:px-8"
|
|
13
13
|
class:list={[Astro.props.className]}
|
|
14
14
|
>
|
|
15
15
|
<slot />
|
package/src/layouts/Page.astro
CHANGED
|
@@ -13,9 +13,10 @@ import ViewTransition from "./components/ViewTransition.astro"
|
|
|
13
13
|
interface Props {
|
|
14
14
|
title?: string
|
|
15
15
|
description?: string
|
|
16
|
+
mainClass?: string
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const { title, description } = Astro.props
|
|
19
|
+
const { title, description, mainClass } = Astro.props
|
|
19
20
|
const configTitle = Astro.locals.i18n.t(config.title)
|
|
20
21
|
|
|
21
22
|
const { currentLocale } = Astro.locals.i18n
|
|
@@ -39,7 +40,7 @@ const language = resolveLanguage(currentLocale)
|
|
|
39
40
|
class="flex min-h-screen flex-col overflow-y-scroll bg-gray-50 text-gray-900"
|
|
40
41
|
>
|
|
41
42
|
<Header />
|
|
42
|
-
<main class=
|
|
43
|
+
<main class={`grow pb-8 pt-14 sm:py-20 ${mainClass ?? ""}`}>
|
|
43
44
|
<slot />
|
|
44
45
|
</main>
|
|
45
46
|
{CustomFooter ? <CustomFooter /> : <Footer />}
|
|
@@ -9,7 +9,7 @@ const { t, locales } = Astro.locals.i18n
|
|
|
9
9
|
const translations = locales
|
|
10
10
|
.map((locale) => ({
|
|
11
11
|
locale,
|
|
12
|
-
label: resolveTranslatedLanguage(locale, t).
|
|
12
|
+
label: resolveTranslatedLanguage(locale, t).labelText,
|
|
13
13
|
active: locale === Astro.currentLocale,
|
|
14
14
|
href: currentPathWithLocale(locale),
|
|
15
15
|
}))
|
|
@@ -14,7 +14,7 @@ const { icon, label } = Astro.props
|
|
|
14
14
|
role="button"
|
|
15
15
|
tabindex="0"
|
|
16
16
|
aria-label={Astro.locals.i18n.t(label)}
|
|
17
|
-
class="
|
|
17
|
+
class="flex cursor-pointer rounded-md p-3 text-gray-600 hover:text-primary"
|
|
18
18
|
>
|
|
19
19
|
<Icon className={icon} ariaLabel="" />
|
|
20
20
|
</div>
|
|
@@ -35,7 +35,7 @@ const t = Astro.locals.i18n.t
|
|
|
35
35
|
{
|
|
36
36
|
!config.searchPage?.hideHeaderSearchIcon && (
|
|
37
37
|
<a
|
|
38
|
-
class="
|
|
38
|
+
class="flex p-3 text-gray-600 hover:text-primary"
|
|
39
39
|
aria-label={t("ln.search.title")}
|
|
40
40
|
data-astro-prefetch="viewport"
|
|
41
41
|
href={searchPagePath(Astro.currentLocale)}
|
|
@@ -21,7 +21,7 @@ const { t } = Astro.locals.i18n
|
|
|
21
21
|
<Label>{translations.length ? t("ln.languages") : t("ln.language")}</Label>
|
|
22
22
|
<ul class="flex flex-wrap gap-2">
|
|
23
23
|
<li class="py-1 pe-2 text-gray-800">
|
|
24
|
-
{resolveTranslatedLanguage(item.data.language, t).
|
|
24
|
+
{resolveTranslatedLanguage(item.data.language, t).labelText}
|
|
25
25
|
</li>
|
|
26
26
|
{
|
|
27
27
|
translations.map((translation) => (
|
|
@@ -30,7 +30,7 @@ const { t } = Astro.locals.i18n
|
|
|
30
30
|
href={detailsPagePath(Astro.currentLocale, translation)}
|
|
31
31
|
hreflang={translation.language}
|
|
32
32
|
>
|
|
33
|
-
{resolveTranslatedLanguage(translation.language, t).
|
|
33
|
+
{resolveTranslatedLanguage(translation.language, t).labelText}
|
|
34
34
|
</a>
|
|
35
35
|
</li>
|
|
36
36
|
))
|
|
@@ -12,7 +12,7 @@ export default function LoadingSkeleton() {
|
|
|
12
12
|
<div className="h-4 w-5/6 rounded-md bg-gray-200 md:h-6"></div>
|
|
13
13
|
</div>
|
|
14
14
|
<Icon
|
|
15
|
-
className="
|
|
15
|
+
className="my-auto me-4 ms-2 hidden shrink-0 text-2xl text-gray-300 mdi--chevron-right sm:block"
|
|
16
16
|
flipIcon={direction === "rtl"}
|
|
17
17
|
ariaLabel=""
|
|
18
18
|
/>
|
|
@@ -9,24 +9,24 @@ import SearchFilterReact from "./SearchFilter.tsx"
|
|
|
9
9
|
|
|
10
10
|
const { t, currentLocale } = Astro.locals.i18n
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
array.sort((a, b) => a.
|
|
12
|
+
const sortByLabelText = (array: { id: string; labelText: string }[]) =>
|
|
13
|
+
array.sort((a, b) => a.labelText.localeCompare(b.labelText, currentLocale))
|
|
14
14
|
|
|
15
15
|
const categories = (await getUsedCategories(currentLocale, t)).map(
|
|
16
|
-
({ id,
|
|
16
|
+
({ id, labelText }) => ({ id, labelText }),
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
-
const mediaTypes =
|
|
19
|
+
const mediaTypes = sortByLabelText(
|
|
20
20
|
(await getMediaTypes()).map((type) => ({
|
|
21
21
|
id: type.id,
|
|
22
|
-
|
|
22
|
+
labelText: t(type.data.label),
|
|
23
23
|
})),
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
const languages =
|
|
26
|
+
const languages = sortByLabelText(
|
|
27
27
|
contentLanguages.map((language) => ({
|
|
28
28
|
id: language.code,
|
|
29
|
-
|
|
29
|
+
labelText: t(language.label),
|
|
30
30
|
})),
|
|
31
31
|
)
|
|
32
32
|
|
|
@@ -7,7 +7,7 @@ import { useSearchQueryParam } from "../hooks/use-search-query-param"
|
|
|
7
7
|
import { CATEGORY, LANGUAGE, SEARCH, TYPE } from "../utils/search-query"
|
|
8
8
|
import Select from "./Select"
|
|
9
9
|
|
|
10
|
-
type FilterValue = { id: string;
|
|
10
|
+
type FilterValue = { id: string; labelText: string }
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
languages: FilterValue[]
|
|
@@ -57,7 +57,7 @@ export default function SearchFilter({
|
|
|
57
57
|
onInput={(e) => debouncedSetSearch(e.currentTarget.value)}
|
|
58
58
|
onKeyDown={(e) => e.key === "Enter" && searchInput.current?.blur()}
|
|
59
59
|
/>
|
|
60
|
-
<Icon className="mdi--magnify
|
|
60
|
+
<Icon className="text-xl mdi--magnify" ariaLabel="" />
|
|
61
61
|
</label>
|
|
62
62
|
<div className="mb-8 grid grid-cols-1 gap-2 sm:grid-cols-3 sm:gap-6 md:mb-10">
|
|
63
63
|
{languageFilterEnabled && (
|
|
@@ -66,7 +66,7 @@ export default function SearchFilter({
|
|
|
66
66
|
initialValue={language}
|
|
67
67
|
valueChange={(val) => setLanguage(val)}
|
|
68
68
|
options={[
|
|
69
|
-
{ id: "",
|
|
69
|
+
{ id: "", labelText: t("ln.search.all-languages") },
|
|
70
70
|
...languages,
|
|
71
71
|
]}
|
|
72
72
|
/>
|
|
@@ -78,7 +78,7 @@ export default function SearchFilter({
|
|
|
78
78
|
initialValue={type}
|
|
79
79
|
valueChange={(val) => setType(val)}
|
|
80
80
|
options={[
|
|
81
|
-
{ id: "",
|
|
81
|
+
{ id: "", labelText: t("ln.search.all-types") },
|
|
82
82
|
...mediaTypes,
|
|
83
83
|
]}
|
|
84
84
|
/>
|
|
@@ -90,7 +90,7 @@ export default function SearchFilter({
|
|
|
90
90
|
initialValue={category}
|
|
91
91
|
valueChange={(val) => setCategory(val)}
|
|
92
92
|
options={[
|
|
93
|
-
{ id: "",
|
|
93
|
+
{ id: "", labelText: t("ln.search.all-categories") },
|
|
94
94
|
...categories,
|
|
95
95
|
]}
|
|
96
96
|
/>
|
|
@@ -10,15 +10,15 @@ import SearchListReact from "./SearchList.tsx"
|
|
|
10
10
|
const { t, currentLocale } = Astro.locals.i18n
|
|
11
11
|
|
|
12
12
|
const categories: Record<string, string> = {}
|
|
13
|
-
for (const { id,
|
|
14
|
-
categories[id] =
|
|
13
|
+
for (const { id, labelText } of await getUsedCategories(currentLocale, t)) {
|
|
14
|
+
categories[id] = labelText
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const mediaTypes = Object.fromEntries(
|
|
18
18
|
(await getMediaTypes()).map((type) => [
|
|
19
19
|
type.id,
|
|
20
20
|
{
|
|
21
|
-
|
|
21
|
+
labelText: t(type.data.label),
|
|
22
22
|
icon: type.data.icon,
|
|
23
23
|
coverImageStyle: type.data.coverImageStyle,
|
|
24
24
|
},
|
|
@@ -28,7 +28,7 @@ const mediaTypes = Object.fromEntries(
|
|
|
28
28
|
const languages = Object.fromEntries(
|
|
29
29
|
contentLanguages.map((language) => [
|
|
30
30
|
language.code,
|
|
31
|
-
{ direction: language.direction,
|
|
31
|
+
{ direction: language.direction, labelText: t(language.label) },
|
|
32
32
|
]),
|
|
33
33
|
)
|
|
34
34
|
|
|
@@ -5,13 +5,13 @@ import { detailsPagePath } from "../../../utils/paths"
|
|
|
5
5
|
import type { SearchItem } from "../api/search-response"
|
|
6
6
|
|
|
7
7
|
export type MediaType = {
|
|
8
|
-
|
|
8
|
+
labelText: string
|
|
9
9
|
icon: string
|
|
10
10
|
coverImageStyle: "default" | "book" | "video"
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export type TranslatedLanguage = {
|
|
14
|
-
|
|
14
|
+
labelText: string
|
|
15
15
|
direction: "rtl" | "ltr"
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -58,7 +58,7 @@ export default function SearchListItem({
|
|
|
58
58
|
<h2 className="mb-1 line-clamp-2 text-balance text-sm font-bold text-gray-700 md:mb-3 md:text-base">
|
|
59
59
|
<Icon
|
|
60
60
|
className={`${mediaTypes[item.type].icon} me-2 align-bottom text-2xl text-gray-700`}
|
|
61
|
-
ariaLabel={mediaTypes[item.type].
|
|
61
|
+
ariaLabel={mediaTypes[item.type].labelText}
|
|
62
62
|
/>
|
|
63
63
|
<span>{item.title}</span>
|
|
64
64
|
</h2>
|
|
@@ -70,7 +70,7 @@ export default function SearchListItem({
|
|
|
70
70
|
)}
|
|
71
71
|
{showLanguage && (
|
|
72
72
|
<span className="rounded-lg border border-gray-300 px-2 py-1 text-gray-500">
|
|
73
|
-
{languages[item.language].
|
|
73
|
+
{languages[item.language].labelText}
|
|
74
74
|
</span>
|
|
75
75
|
)}
|
|
76
76
|
<ul
|
|
@@ -99,7 +99,7 @@ export default function SearchListItem({
|
|
|
99
99
|
</div>
|
|
100
100
|
</div>
|
|
101
101
|
<Icon
|
|
102
|
-
className="
|
|
102
|
+
className="my-auto me-4 ms-2 hidden shrink-0 text-2xl text-gray-300 mdi--chevron-right sm:block md:group-hover:text-primary"
|
|
103
103
|
flipIcon={direction === "rtl"}
|
|
104
104
|
ariaLabel=""
|
|
105
105
|
/>
|
|
@@ -2,7 +2,7 @@ type Props = {
|
|
|
2
2
|
label: string
|
|
3
3
|
initialValue: string | undefined
|
|
4
4
|
valueChange: (value: string) => void
|
|
5
|
-
options: { id: string;
|
|
5
|
+
options: { id: string; labelText: 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
|
|
20
|
+
className="dy-select dy-select-bordered w-full rounded-xl sm:dy-select-sm"
|
|
21
21
|
value={initialValue}
|
|
22
22
|
onChange={(e) => valueChange(e.currentTarget.value)}
|
|
23
23
|
>
|
|
24
|
-
{options.map(({ id,
|
|
24
|
+
{options.map(({ id, labelText }) => (
|
|
25
25
|
<option key={id} value={id}>
|
|
26
|
-
{
|
|
26
|
+
{labelText}
|
|
27
27
|
</option>
|
|
28
28
|
))}
|
|
29
29
|
</select>
|
|
@@ -6,8 +6,8 @@ import { observeSearchQuery, type SearchQuery } from "../utils/search-query"
|
|
|
6
6
|
|
|
7
7
|
interface Context {
|
|
8
8
|
categories: Record<string, string>
|
|
9
|
-
mediaTypes: Record<string, {
|
|
10
|
-
languages: Record<string, {
|
|
9
|
+
mediaTypes: Record<string, { labelText: string }>
|
|
10
|
+
languages: Record<string, { labelText: string }>
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function useSearch({ categories, mediaTypes, languages }: Context) {
|
|
@@ -39,8 +39,8 @@ export function useSearch({ categories, mediaTypes, languages }: Context) {
|
|
|
39
39
|
const translatedCategories =
|
|
40
40
|
item.categories &&
|
|
41
41
|
item.categories.map((categoryId) => categories[categoryId])
|
|
42
|
-
const translatedType = mediaTypes[item.type].
|
|
43
|
-
const translatedLanguage = languages[item.language].
|
|
42
|
+
const translatedType = mediaTypes[item.type].labelText
|
|
43
|
+
const translatedLanguage = languages[item.language].labelText
|
|
44
44
|
|
|
45
45
|
return {
|
|
46
46
|
...item,
|