lightnet 3.10.4 → 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 +6 -0
- package/__e2e__/admin.spec.ts +71 -12
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +2 -2
- package/package.json +4 -5
- package/src/admin/components/form/DynamicArray.tsx +74 -0
- package/src/admin/components/form/Input.tsx +5 -5
- package/src/admin/components/form/Select.tsx +8 -10
- package/src/admin/components/form/SubmitButton.tsx +9 -12
- package/src/admin/components/form/atoms/ErrorMessage.tsx +7 -28
- package/src/admin/components/form/atoms/Hint.tsx +2 -2
- package/src/admin/components/form/atoms/Label.tsx +5 -1
- package/src/admin/components/form/atoms/Legend.tsx +12 -2
- package/src/admin/components/form/hooks/use-field-error.tsx +3 -11
- package/src/admin/i18n/translations/en.yml +15 -5
- package/src/admin/pages/media/EditForm.tsx +20 -4
- package/src/admin/pages/media/EditRoute.astro +28 -9
- package/src/admin/pages/media/fields/Authors.tsx +35 -50
- 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 +6 -1
- package/src/admin/types/media-item.ts +34 -2
- package/src/components/CategoriesSection.astro +2 -2
- package/src/components/MediaGallerySection.astro +3 -3
- package/src/components/MediaList.astro +2 -2
- package/src/content/get-categories.ts +18 -3
- package/src/i18n/resolve-language.ts +1 -1
- package/src/layouts/Page.astro +3 -2
- package/src/layouts/components/LanguagePicker.astro +1 -1
- package/src/pages/details-page/components/more-details/Languages.astro +2 -2
- package/src/pages/search-page/components/SearchFilter.astro +7 -7
- package/src/pages/search-page/components/SearchFilter.tsx +4 -4
- package/src/pages/search-page/components/SearchList.astro +4 -4
- package/src/pages/search-page/components/SearchListItem.tsx +4 -4
- package/src/pages/search-page/components/Select.tsx +3 -3
- package/src/pages/search-page/hooks/use-search.ts +4 -4
|
@@ -3,6 +3,7 @@ import type { GetStaticPaths } from "astro"
|
|
|
3
3
|
import { getCollection } from "astro:content"
|
|
4
4
|
import config from "virtual:lightnet/config"
|
|
5
5
|
|
|
6
|
+
import { getCategories } from "../../../content/get-categories"
|
|
6
7
|
import { getRawMediaItem } from "../../../content/get-media-items"
|
|
7
8
|
import { getMediaTypes } from "../../../content/get-media-types"
|
|
8
9
|
import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
|
|
@@ -23,42 +24,60 @@ const { data: mediaItem } = await getRawMediaItem(mediaId)
|
|
|
23
24
|
const formData = {
|
|
24
25
|
...mediaItem,
|
|
25
26
|
authors: mediaItem.authors?.map((value) => ({ value })) ?? [],
|
|
27
|
+
categories:
|
|
28
|
+
mediaItem.categories?.map((value) => ({
|
|
29
|
+
value,
|
|
30
|
+
})) ?? [],
|
|
31
|
+
collections: mediaItem.collections ?? [],
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
|
|
29
35
|
"ln.admin.*",
|
|
30
36
|
"ln.type",
|
|
31
37
|
"ln.language",
|
|
38
|
+
"ln.categories",
|
|
32
39
|
])
|
|
33
|
-
const { t } = Astro.locals.i18n
|
|
40
|
+
const { t, currentLocale } = Astro.locals.i18n
|
|
34
41
|
|
|
35
42
|
const mediaTypes = (await getMediaTypes()).map(({ id, data: { label } }) => ({
|
|
36
43
|
id,
|
|
37
|
-
|
|
44
|
+
labelText: t(label),
|
|
38
45
|
}))
|
|
46
|
+
|
|
47
|
+
const categories = (await getCategories(currentLocale, t)).map(
|
|
48
|
+
({ id, labelText }) => ({ id, labelText }),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const collections = (await getCollection("media-collections")).map(
|
|
52
|
+
({ id, data: { label } }) => ({ id, labelText: t(label) }),
|
|
53
|
+
)
|
|
54
|
+
|
|
39
55
|
const languages = config.languages.map(({ code, label }) => ({
|
|
40
56
|
id: code,
|
|
41
|
-
|
|
57
|
+
labelText: t(label),
|
|
42
58
|
}))
|
|
43
59
|
---
|
|
44
60
|
|
|
45
|
-
<Page>
|
|
46
|
-
<div class="mx-auto max-w-screen-md px-4
|
|
61
|
+
<Page mainClass="bg-slate-500">
|
|
62
|
+
<div class="mx-auto block max-w-screen-md px-4 md:px-8">
|
|
47
63
|
<a
|
|
48
|
-
class="underline"
|
|
64
|
+
class="block py-4 text-gray-200 underline"
|
|
49
65
|
href=`/${Astro.currentLocale}/media/faithful-freestyle--en`
|
|
50
66
|
>{t("ln.admin.back-to-details-page")}</a
|
|
51
67
|
>
|
|
52
|
-
|
|
53
|
-
{t("ln.admin.edit-media-item")}
|
|
54
|
-
</h1>
|
|
68
|
+
</div>
|
|
55
69
|
|
|
70
|
+
<div
|
|
71
|
+
class="mx-auto max-w-screen-md rounded-2xl bg-gray-200 px-4 py-8 shadow-md md:px-8"
|
|
72
|
+
>
|
|
56
73
|
<EditForm
|
|
57
74
|
mediaId={mediaId}
|
|
58
75
|
mediaItem={formData}
|
|
59
76
|
i18nConfig={i18nConfig}
|
|
60
77
|
mediaTypes={mediaTypes}
|
|
61
78
|
languages={languages}
|
|
79
|
+
categories={categories}
|
|
80
|
+
collections={collections}
|
|
62
81
|
client:load
|
|
63
82
|
/>
|
|
64
83
|
</div>
|
|
@@ -1,58 +1,43 @@
|
|
|
1
|
-
import { type Control
|
|
1
|
+
import { type Control } from "react-hook-form"
|
|
2
2
|
|
|
3
|
-
import Icon from "../../../../components/Icon"
|
|
4
|
-
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
5
3
|
import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
|
|
6
|
-
import
|
|
7
|
-
import
|
|
4
|
+
import DynamicArray from "../../../components/form/DynamicArray"
|
|
5
|
+
import { useFieldError } from "../../../components/form/hooks/use-field-error"
|
|
8
6
|
import type { MediaItem } from "../../../types/media-item"
|
|
9
7
|
|
|
10
8
|
export default function Authors({ control }: { control: Control<MediaItem> }) {
|
|
11
|
-
const { fields, append, remove } = useFieldArray({
|
|
12
|
-
name: "authors",
|
|
13
|
-
control,
|
|
14
|
-
})
|
|
15
|
-
const { t } = useI18n()
|
|
16
9
|
return (
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}}
|
|
50
|
-
>
|
|
51
|
-
{t("ln.admin.add-author")}
|
|
52
|
-
</button>
|
|
53
|
-
</div>
|
|
54
|
-
<ErrorMessage name="authors" control={control} />
|
|
55
|
-
<Hint />
|
|
56
|
-
</fieldset>
|
|
10
|
+
<DynamicArray
|
|
11
|
+
control={control}
|
|
12
|
+
name="authors"
|
|
13
|
+
label="ln.admin.authors"
|
|
14
|
+
renderElement={(index) => <AuthorInput index={index} control={control} />}
|
|
15
|
+
addButton={{
|
|
16
|
+
label: "ln.admin.add-author",
|
|
17
|
+
onClick: (append, index) =>
|
|
18
|
+
append({ value: "" }, { focusName: `authors.${index}.value` }),
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function AuthorInput({
|
|
25
|
+
index,
|
|
26
|
+
control,
|
|
27
|
+
}: {
|
|
28
|
+
index: number
|
|
29
|
+
control: Control<MediaItem>
|
|
30
|
+
}) {
|
|
31
|
+
const name = `authors.${index}.value` as const
|
|
32
|
+
const errorMessage = useFieldError({ name, control })
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<input
|
|
36
|
+
className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
|
|
37
|
+
aria-invalid={!!errorMessage}
|
|
38
|
+
{...control.register(name)}
|
|
39
|
+
/>
|
|
40
|
+
<ErrorMessage message={errorMessage} />
|
|
41
|
+
</>
|
|
57
42
|
)
|
|
58
43
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Control } from "react-hook-form"
|
|
2
|
+
|
|
3
|
+
import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
|
|
4
|
+
import DynamicArray from "../../../components/form/DynamicArray"
|
|
5
|
+
import { useFieldError } from "../../../components/form/hooks/use-field-error"
|
|
6
|
+
import type { MediaItem } from "../../../types/media-item"
|
|
7
|
+
|
|
8
|
+
export default function Categories({
|
|
9
|
+
control,
|
|
10
|
+
categories,
|
|
11
|
+
}: {
|
|
12
|
+
control: Control<MediaItem>
|
|
13
|
+
categories: { id: string; labelText: string }[]
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<DynamicArray
|
|
17
|
+
control={control}
|
|
18
|
+
name="categories"
|
|
19
|
+
label="ln.categories"
|
|
20
|
+
renderElement={(index) => (
|
|
21
|
+
<CategorySelect
|
|
22
|
+
categories={categories}
|
|
23
|
+
control={control}
|
|
24
|
+
index={index}
|
|
25
|
+
/>
|
|
26
|
+
)}
|
|
27
|
+
addButton={{
|
|
28
|
+
label: "ln.admin.add-category",
|
|
29
|
+
onClick: (append, index) =>
|
|
30
|
+
append({ value: "" }, { focusName: `categories.${index}.value` }),
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CategorySelect({
|
|
37
|
+
control,
|
|
38
|
+
categories,
|
|
39
|
+
index,
|
|
40
|
+
}: {
|
|
41
|
+
control: Control<MediaItem>
|
|
42
|
+
categories: { id: string; labelText: string }[]
|
|
43
|
+
index: number
|
|
44
|
+
}) {
|
|
45
|
+
const name = `categories.${index}.value` as const
|
|
46
|
+
const errorMessage = useFieldError({ name, control })
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<select
|
|
50
|
+
{...control.register(name)}
|
|
51
|
+
id={name}
|
|
52
|
+
aria-invalid={!!errorMessage}
|
|
53
|
+
className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
|
|
54
|
+
>
|
|
55
|
+
{categories.map(({ id, labelText }) => (
|
|
56
|
+
<option key={id} value={id}>
|
|
57
|
+
{labelText}
|
|
58
|
+
</option>
|
|
59
|
+
))}
|
|
60
|
+
</select>
|
|
61
|
+
<ErrorMessage message={errorMessage} />
|
|
62
|
+
</>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -8,6 +8,11 @@ export const updateMediaItem = async (id: string, item: MediaItem) => {
|
|
|
8
8
|
const mapToContentSchema = (item: MediaItem) => {
|
|
9
9
|
return {
|
|
10
10
|
...item,
|
|
11
|
-
authors: item.authors
|
|
11
|
+
authors: flatten(item.authors),
|
|
12
|
+
categories: flatten(item.categories),
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
|
|
16
|
+
const flatten = <TValue>(valueArray: { value: TValue }[]) => {
|
|
17
|
+
return valueArray.map(({ value }) => value)
|
|
18
|
+
}
|
|
@@ -1,8 +1,27 @@
|
|
|
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),
|
|
@@ -12,7 +31,20 @@ export const mediaItemSchema = z.object({
|
|
|
12
31
|
authors: z
|
|
13
32
|
.object({ value: z.string().nonempty(NON_EMPTY_STRING) })
|
|
14
33
|
.array()
|
|
15
|
-
.
|
|
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")),
|
|
16
48
|
dateCreated: z.string().date(INVALID_DATE),
|
|
17
49
|
})
|
|
18
50
|
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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) {
|
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
|
}))
|
|
@@ -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
|
))
|
|
@@ -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[]
|
|
@@ -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
|
|