lightnet 3.10.1 → 3.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/src/admin/api/fs/{writeText.ts → write-file.ts} +9 -6
- package/src/admin/components/form/Select.tsx +39 -0
- package/src/admin/components/form/SubmitButton.tsx +6 -4
- package/src/admin/components/form/TextInput.tsx +34 -0
- package/src/admin/components/form/{FieldErrors.tsx → atoms/FieldErrors.tsx} +4 -1
- package/src/admin/components/form/atoms/Hint.tsx +10 -0
- package/src/admin/components/form/atoms/Label.tsx +12 -0
- package/src/admin/components/form/index.ts +4 -2
- package/src/admin/i18n/translations/en.yml +15 -0
- package/src/admin/pages/media/EditForm.tsx +71 -24
- package/src/admin/pages/media/EditRoute.astro +31 -4
- package/src/admin/pages/media/file-system.ts +3 -3
- package/src/admin/types/media-item.ts +9 -2
- package/src/astro-integration/integration.ts +2 -2
- package/src/components/Toast.tsx +1 -1
- package/src/i18n/locals.d.ts +35 -28
- package/src/i18n/locals.ts +2 -1
- package/src/i18n/react/i18n-context.ts +36 -0
- package/src/i18n/react/prepare-i18n-config.ts +31 -0
- package/src/i18n/react/useI18n.ts +15 -0
- package/src/i18n/translate.ts +15 -3
- package/src/pages/search-page/components/LoadingSkeleton.tsx +3 -1
- package/src/pages/search-page/components/SearchFilter.astro +12 -3
- package/src/pages/search-page/components/SearchFilter.tsx +4 -7
- package/src/pages/search-page/components/SearchList.astro +7 -6
- package/src/pages/search-page/components/SearchList.tsx +12 -15
- package/src/pages/search-page/components/SearchListItem.tsx +2 -4
- package/src/admin/components/form/TextField.tsx +0 -24
- package/src/pages/search-page/utils/search-filter-translations.ts +0 -20
- package/src/pages/search-page/utils/search-translations.ts +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# lightnet
|
|
2
2
|
|
|
3
|
+
## 3.10.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#327](https://github.com/LightNetDev/LightNet/pull/327) [`9f1e468`](https://github.com/LightNetDev/LightNet/commit/9f1e468cc296e62788586a3f3c6f245c8bc0cec9) Thanks [@smn-cds](https://github.com/smn-cds)! - Gracefully handle missing translations in React components to avoid runtime crashes.
|
|
8
|
+
|
|
9
|
+
## 3.10.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#325](https://github.com/LightNetDev/LightNet/pull/325) [`07b71f4`](https://github.com/LightNetDev/LightNet/commit/07b71f4d44f8e005aafb48bedc347f14c71e182a) Thanks [@smn-cds](https://github.com/smn-cds)! - Refactor internal react i18n logic.
|
|
14
|
+
|
|
3
15
|
## 3.10.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "LightNet makes it easy to run your own digital media library.",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "3.10.
|
|
6
|
+
"version": "3.10.3",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/LightNetDev/lightnet",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"./api/internal/search.ts": "./src/pages/search-page/api/search.ts",
|
|
35
35
|
"./api/versions.ts": "./src/api/versions.ts",
|
|
36
36
|
"./api/media/[mediaId].ts": "./src/api/media/[mediaId].ts",
|
|
37
|
-
"./api/internal/fs/
|
|
37
|
+
"./api/internal/fs/write-file.ts": "./src/admin/api/fs/write-file.ts",
|
|
38
38
|
"./admin/pages/AdminRoute.astro": "./src/admin/pages/AdminRoute.astro",
|
|
39
39
|
"./admin/pages/media/EditRoute.astro": "./src/admin/pages/media/EditRoute.astro"
|
|
40
40
|
},
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createWriteStream } from "node:fs"
|
|
2
|
+
import { mkdir, rename, rm } from "node:fs/promises"
|
|
2
3
|
import { dirname, isAbsolute, relative, resolve } from "node:path"
|
|
4
|
+
import { Writable } from "node:stream"
|
|
3
5
|
import { fileURLToPath } from "node:url"
|
|
4
6
|
|
|
5
7
|
import type { APIRoute } from "astro"
|
|
@@ -26,16 +28,17 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
26
28
|
) {
|
|
27
29
|
throw new Error("Path escapes project root.")
|
|
28
30
|
}
|
|
31
|
+
const { body } = request
|
|
32
|
+
if (!body) {
|
|
33
|
+
throw new Error("Request body missing.")
|
|
34
|
+
}
|
|
29
35
|
|
|
30
36
|
const targetDir = dirname(targetPath)
|
|
31
37
|
await mkdir(targetDir, { recursive: true })
|
|
32
38
|
|
|
33
|
-
const
|
|
34
|
-
const timestamp = Date.now()
|
|
35
|
-
const tmpPath = `${targetPath}.tmp-${timestamp}`
|
|
39
|
+
const tmpPath = `${targetPath}.tmp-${Date.now()}`
|
|
36
40
|
try {
|
|
37
|
-
|
|
38
|
-
await writeFile(tmpPath, body, "utf-8")
|
|
41
|
+
await body.pipeTo(Writable.toWeb(createWriteStream(tmpPath)))
|
|
39
42
|
await rename(tmpPath, targetPath)
|
|
40
43
|
} finally {
|
|
41
44
|
await rm(tmpPath, { force: true }).catch(() => {})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useI18n } from "../../../i18n/react/useI18n"
|
|
2
|
+
import { FieldErrors } from "./atoms/FieldErrors"
|
|
3
|
+
import Hint from "./atoms/Hint"
|
|
4
|
+
import Label from "./atoms/Label"
|
|
5
|
+
import { useFieldContext } from "./form-context"
|
|
6
|
+
|
|
7
|
+
export default function Select({
|
|
8
|
+
label,
|
|
9
|
+
hint,
|
|
10
|
+
options,
|
|
11
|
+
}: {
|
|
12
|
+
label: string
|
|
13
|
+
hint?: string
|
|
14
|
+
options: { id: string; label?: string }[]
|
|
15
|
+
}) {
|
|
16
|
+
const field = useFieldContext<string>()
|
|
17
|
+
const { t } = useI18n()
|
|
18
|
+
return (
|
|
19
|
+
<label className="dy-form-control w-full">
|
|
20
|
+
<Label label={label} />
|
|
21
|
+
<select
|
|
22
|
+
id={field.name}
|
|
23
|
+
name={field.name}
|
|
24
|
+
value={field.state.value}
|
|
25
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
26
|
+
onBlur={field.handleBlur}
|
|
27
|
+
className={`dy-select dy-select-bordered ${field.state.meta.errors.length ? "dy-select-error" : ""}"`}
|
|
28
|
+
>
|
|
29
|
+
{options.map(({ id, label }) => (
|
|
30
|
+
<option key={id} value={id}>
|
|
31
|
+
{label ? t(label) : id}
|
|
32
|
+
</option>
|
|
33
|
+
))}
|
|
34
|
+
</select>
|
|
35
|
+
<FieldErrors meta={field.state.meta} />
|
|
36
|
+
<Hint hint={hint} />
|
|
37
|
+
</label>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useStore } from "@tanstack/react-form"
|
|
|
2
2
|
import { useEffect, useRef, useState } from "react"
|
|
3
3
|
|
|
4
4
|
import Icon from "../../../components/Icon"
|
|
5
|
+
import { useI18n } from "../../../i18n/react/useI18n"
|
|
5
6
|
import { useFormContext } from "./form-context"
|
|
6
7
|
|
|
7
8
|
const SUCCESS_DURATION_MS = 2000
|
|
@@ -18,9 +19,9 @@ const buttonStateClasses = {
|
|
|
18
19
|
} as const
|
|
19
20
|
|
|
20
21
|
const buttonLabels = {
|
|
21
|
-
idle: "
|
|
22
|
-
success: "
|
|
23
|
-
error: "
|
|
22
|
+
idle: "ln.admin.save",
|
|
23
|
+
success: "ln.admin.saved",
|
|
24
|
+
error: "ln.admin.failed",
|
|
24
25
|
} as const
|
|
25
26
|
|
|
26
27
|
const icons = {
|
|
@@ -31,6 +32,7 @@ const icons = {
|
|
|
31
32
|
|
|
32
33
|
export default function SubmitButton() {
|
|
33
34
|
const form = useFormContext()
|
|
35
|
+
const { t } = useI18n()
|
|
34
36
|
const { submissionAttempts, isSubmitting, isSubmitSuccessful } = useStore(
|
|
35
37
|
form.store,
|
|
36
38
|
(state) => ({
|
|
@@ -48,7 +50,7 @@ export default function SubmitButton() {
|
|
|
48
50
|
return (
|
|
49
51
|
<button className={buttonClass} type="submit" disabled={isSubmitting}>
|
|
50
52
|
{icon && <Icon className={icon} ariaLabel="" />}
|
|
51
|
-
{label}
|
|
53
|
+
{t(label)}
|
|
52
54
|
</button>
|
|
53
55
|
)
|
|
54
56
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FieldErrors } from "./atoms/FieldErrors"
|
|
2
|
+
import Hint from "./atoms/Hint"
|
|
3
|
+
import Label from "./atoms/Label"
|
|
4
|
+
import { useFieldContext } from "./form-context"
|
|
5
|
+
|
|
6
|
+
export default function TextInput({
|
|
7
|
+
label,
|
|
8
|
+
hint,
|
|
9
|
+
type = "text",
|
|
10
|
+
}: {
|
|
11
|
+
label: string
|
|
12
|
+
hint?: string
|
|
13
|
+
type?: "text" | "date"
|
|
14
|
+
}) {
|
|
15
|
+
const field = useFieldContext<string>()
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<label className="dy-form-control w-full">
|
|
19
|
+
<Label label={label} />
|
|
20
|
+
<input
|
|
21
|
+
id={field.name}
|
|
22
|
+
name={field.name}
|
|
23
|
+
type={type}
|
|
24
|
+
value={field.state.value}
|
|
25
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
26
|
+
onBlur={field.handleBlur}
|
|
27
|
+
className={`dy-input dy-input-bordered ${field.state.meta.errors.length ? "dy-input-error" : ""}`}
|
|
28
|
+
/>
|
|
29
|
+
<FieldErrors meta={field.state.meta} />
|
|
30
|
+
<Hint hint={hint} />
|
|
31
|
+
</label>
|
|
32
|
+
</>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import type { AnyFieldMeta } from "@tanstack/react-form"
|
|
2
2
|
|
|
3
|
+
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
4
|
+
|
|
3
5
|
type FieldErrorsProps = {
|
|
4
6
|
meta: AnyFieldMeta
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
export const FieldErrors = ({ meta }: FieldErrorsProps) => {
|
|
10
|
+
const { t } = useI18n()
|
|
8
11
|
if (!meta.isTouched || meta.isValid) return null
|
|
9
12
|
|
|
10
13
|
return (
|
|
11
14
|
<ul className="my-2 flex flex-col gap-1" role="alert">
|
|
12
15
|
{meta.errors.map((error) => (
|
|
13
16
|
<li className="text-sm text-rose-800" key={error.code}>
|
|
14
|
-
{error.message}
|
|
17
|
+
{t(error.message)}
|
|
15
18
|
</li>
|
|
16
19
|
))}
|
|
17
20
|
</ul>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
2
|
+
|
|
3
|
+
export default function Hint({ hint }: { hint?: string }) {
|
|
4
|
+
const { t } = useI18n()
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex h-8 w-full items-center justify-end">
|
|
7
|
+
{hint && <span className="dy-label-text-alt">{t(hint)}</span>}
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
2
|
+
|
|
3
|
+
export default function Label({ label }: { label: string }) {
|
|
4
|
+
const { t } = useI18n()
|
|
5
|
+
return (
|
|
6
|
+
<div className="dy-label">
|
|
7
|
+
<span className="text-sm font-bold uppercase text-gray-600">
|
|
8
|
+
{t(label)}
|
|
9
|
+
</span>
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createFormHook } from "@tanstack/react-form"
|
|
2
2
|
|
|
3
3
|
import { fieldContext, formContext } from "./form-context"
|
|
4
|
+
import Select from "./Select"
|
|
4
5
|
import SubmitButton from "./SubmitButton"
|
|
5
|
-
import
|
|
6
|
+
import TextInput from "./TextInput"
|
|
6
7
|
|
|
7
8
|
export const { useAppForm, withForm } = createFormHook({
|
|
8
9
|
fieldComponents: {
|
|
9
|
-
|
|
10
|
+
TextInput,
|
|
11
|
+
Select,
|
|
10
12
|
},
|
|
11
13
|
formComponents: {
|
|
12
14
|
SubmitButton,
|
|
@@ -1 +1,16 @@
|
|
|
1
1
|
ln.admin.edit: Edit
|
|
2
|
+
ln.admin.save: Save
|
|
3
|
+
ln.admin.saved: Saved
|
|
4
|
+
ln.admin.failed: Failed
|
|
5
|
+
ln.admin.edit-media-item: Edit media item
|
|
6
|
+
ln.admin.back-to-details-page: Back to details page
|
|
7
|
+
ln.admin.title: Title
|
|
8
|
+
ln.admin.common-id: Common ID
|
|
9
|
+
ln.admin.created-on: Created on
|
|
10
|
+
ln.admin.created-on-hint: When has this item been created on this library?
|
|
11
|
+
ln.admin.common-id-hint: The english title, all lowercase, words separated with hyphens.
|
|
12
|
+
ln.admin.toast.invalid-data.title: Invalid form data
|
|
13
|
+
ln.admin.toast.invalid-data.hint: Check the fields and try again.
|
|
14
|
+
ln.admin.errors.non-empty-string: String must contain at least 1 character(s)
|
|
15
|
+
ln.admin.errors.invalid-date: Invalid date
|
|
16
|
+
ln.admin.errors.required: Required field
|
|
@@ -2,6 +2,11 @@ import { revalidateLogic } from "@tanstack/react-form"
|
|
|
2
2
|
|
|
3
3
|
import { showToastById } from "../../../components/showToast"
|
|
4
4
|
import Toast from "../../../components/Toast"
|
|
5
|
+
import {
|
|
6
|
+
createI18n,
|
|
7
|
+
type I18nConfig,
|
|
8
|
+
I18nContext,
|
|
9
|
+
} from "../../../i18n/react/i18n-context"
|
|
5
10
|
import { useAppForm } from "../../components/form"
|
|
6
11
|
import { type MediaItem, mediaItemSchema } from "../../types/media-item"
|
|
7
12
|
import { updateMediaItem } from "./media-item-store"
|
|
@@ -9,12 +14,18 @@ import { updateMediaItem } from "./media-item-store"
|
|
|
9
14
|
export default function EditForm({
|
|
10
15
|
mediaId,
|
|
11
16
|
mediaItem,
|
|
17
|
+
i18nConfig,
|
|
18
|
+
mediaTypes,
|
|
19
|
+
languages,
|
|
12
20
|
}: {
|
|
13
21
|
mediaId: string
|
|
14
22
|
mediaItem: MediaItem
|
|
23
|
+
i18nConfig: I18nConfig
|
|
24
|
+
mediaTypes: { id: string; label: string }[]
|
|
25
|
+
languages: { id: string; label: string }[]
|
|
15
26
|
}) {
|
|
16
27
|
const form = useAppForm({
|
|
17
|
-
defaultValues: mediaItem,
|
|
28
|
+
defaultValues: { ...mediaItem },
|
|
18
29
|
validators: {
|
|
19
30
|
onDynamic: mediaItemSchema,
|
|
20
31
|
},
|
|
@@ -29,30 +40,66 @@ export default function EditForm({
|
|
|
29
40
|
showToastById("invalid-form-data-toast")
|
|
30
41
|
},
|
|
31
42
|
})
|
|
43
|
+
const i18n = createI18n(i18nConfig)
|
|
44
|
+
const { t } = i18n
|
|
32
45
|
|
|
33
46
|
return (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
e
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
<I18nContext.Provider value={i18n}>
|
|
48
|
+
<form
|
|
49
|
+
onSubmit={(e) => {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
form.handleSubmit()
|
|
52
|
+
}}
|
|
53
|
+
className="flex flex-col items-start"
|
|
54
|
+
>
|
|
55
|
+
<form.AppField
|
|
56
|
+
name="title"
|
|
57
|
+
children={(field) => <field.TextInput label="ln.admin.title" />}
|
|
58
|
+
/>
|
|
59
|
+
<form.AppField
|
|
60
|
+
name="commonId"
|
|
61
|
+
children={(field) => (
|
|
62
|
+
<field.TextInput
|
|
63
|
+
label="ln.admin.common-id"
|
|
64
|
+
hint="ln.admin.common-id-hint"
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
/>
|
|
68
|
+
<form.AppField
|
|
69
|
+
name="type"
|
|
70
|
+
children={(field) => (
|
|
71
|
+
<field.Select label="ln.type" options={mediaTypes} />
|
|
72
|
+
)}
|
|
73
|
+
/>
|
|
74
|
+
<form.AppField
|
|
75
|
+
name="language"
|
|
76
|
+
children={(field) => (
|
|
77
|
+
<field.Select label="ln.language" options={languages} />
|
|
78
|
+
)}
|
|
79
|
+
/>
|
|
80
|
+
<form.AppField
|
|
81
|
+
name="dateCreated"
|
|
82
|
+
children={(field) => (
|
|
83
|
+
<field.TextInput
|
|
84
|
+
type="date"
|
|
85
|
+
label="ln.admin.created-on"
|
|
86
|
+
hint="ln.admin.created-on-hint"
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<div className="mt-8">
|
|
92
|
+
<form.AppForm>
|
|
93
|
+
<form.SubmitButton />
|
|
94
|
+
<Toast id="invalid-form-data-toast" variant="error">
|
|
95
|
+
<div className="font-bold text-gray-700">
|
|
96
|
+
{t("ln.admin.toast.invalid-data.title")}
|
|
97
|
+
</div>
|
|
98
|
+
{t("ln.admin.toast.invalid-data.hint")}
|
|
99
|
+
</Toast>
|
|
100
|
+
</form.AppForm>
|
|
101
|
+
</div>
|
|
102
|
+
</form>
|
|
103
|
+
</I18nContext.Provider>
|
|
57
104
|
)
|
|
58
105
|
}
|
|
@@ -4,6 +4,8 @@ import { getCollection } from "astro:content"
|
|
|
4
4
|
import config from "virtual:lightnet/config"
|
|
5
5
|
|
|
6
6
|
import { getRawMediaItem } from "../../../content/get-media-items"
|
|
7
|
+
import { getMediaTypes } from "../../../content/get-media-types"
|
|
8
|
+
import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
|
|
7
9
|
import { resolveLocales } from "../../../i18n/resolve-locales"
|
|
8
10
|
import Page from "../../../layouts/Page.astro"
|
|
9
11
|
import EditForm from "./EditForm"
|
|
@@ -17,17 +19,42 @@ export const getStaticPaths = (async () => {
|
|
|
17
19
|
|
|
18
20
|
const { mediaId } = Astro.params
|
|
19
21
|
const mediaItemEntry = await getRawMediaItem(mediaId)
|
|
22
|
+
|
|
23
|
+
const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
|
|
24
|
+
"ln.admin.*",
|
|
25
|
+
"ln.type",
|
|
26
|
+
"ln.language",
|
|
27
|
+
])
|
|
28
|
+
const { t } = Astro.locals.i18n
|
|
29
|
+
|
|
30
|
+
const mediaTypes = (await getMediaTypes()).map(({ id, data: { label } }) => ({
|
|
31
|
+
id,
|
|
32
|
+
label: t(label),
|
|
33
|
+
}))
|
|
34
|
+
const languages = config.languages.map(({ code, label }) => ({
|
|
35
|
+
id: code,
|
|
36
|
+
label: t(label),
|
|
37
|
+
}))
|
|
20
38
|
---
|
|
21
39
|
|
|
22
40
|
<Page>
|
|
23
|
-
<div class="mx-auto max-w-screen-
|
|
41
|
+
<div class="mx-auto max-w-screen-md px-4 pt-12 md:px-8">
|
|
24
42
|
<a
|
|
25
43
|
class="underline"
|
|
26
44
|
href=`/${Astro.currentLocale}/media/faithful-freestyle--en`
|
|
27
|
-
>
|
|
45
|
+
>{t("ln.admin.back-to-details-page")}</a
|
|
28
46
|
>
|
|
29
|
-
<h1 class="mb-
|
|
47
|
+
<h1 class="mb-10 mt-10 text-2xl">
|
|
48
|
+
{t("ln.admin.edit-media-item")}
|
|
49
|
+
</h1>
|
|
30
50
|
|
|
31
|
-
<EditForm
|
|
51
|
+
<EditForm
|
|
52
|
+
mediaId={mediaId}
|
|
53
|
+
mediaItem={mediaItemEntry.data}
|
|
54
|
+
i18nConfig={i18nConfig}
|
|
55
|
+
mediaTypes={mediaTypes}
|
|
56
|
+
languages={languages}
|
|
57
|
+
client:load
|
|
58
|
+
/>
|
|
32
59
|
</div>
|
|
33
60
|
</Page>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const writeFile = (path: string, body: string) => {
|
|
2
2
|
return fetch(
|
|
3
|
-
`/api/internal/fs/
|
|
3
|
+
`/api/internal/fs/write-file?path=${encodeURIComponent(path.replace(/^\//, ""))}`,
|
|
4
4
|
{
|
|
5
5
|
method: "POST",
|
|
6
6
|
headers: { "Content-Type": resolveContentType(path) },
|
|
@@ -10,7 +10,7 @@ export const writeText = (path: string, body: string) => {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const writeJson = async (path: string, object: unknown) => {
|
|
13
|
-
return
|
|
13
|
+
return writeFile(path, JSON.stringify(sortObject(object), null, 2))
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const resolveContentType = (path: string) => {
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { z } from "astro/zod"
|
|
2
2
|
|
|
3
|
+
const NON_EMPTY_STRING = "ln.admin.errors.non-empty-string"
|
|
4
|
+
const INVALID_DATE = "ln.admin.errors.invalid-date"
|
|
5
|
+
const REQUIRED = "ln.admin.errors.required"
|
|
6
|
+
|
|
3
7
|
export const mediaItemSchema = z.object({
|
|
4
|
-
commonId: z.string().nonempty(),
|
|
5
|
-
title: z.string().nonempty(),
|
|
8
|
+
commonId: z.string().nonempty(NON_EMPTY_STRING),
|
|
9
|
+
title: z.string().nonempty(NON_EMPTY_STRING),
|
|
10
|
+
type: z.string().nonempty(REQUIRED),
|
|
11
|
+
language: z.string().nonempty(REQUIRED),
|
|
12
|
+
dateCreated: z.string().date(INVALID_DATE),
|
|
6
13
|
})
|
|
7
14
|
|
|
8
15
|
export type MediaItem = z.infer<typeof mediaItemSchema>
|
|
@@ -87,8 +87,8 @@ export function lightnet(lightnetConfig: LightnetConfig): AstroIntegration {
|
|
|
87
87
|
// this endpoints to write files.
|
|
88
88
|
if (config.experimental?.admin?.enabled && command === "dev") {
|
|
89
89
|
injectRoute({
|
|
90
|
-
pattern: "/api/internal/fs/
|
|
91
|
-
entrypoint: "lightnet/api/internal/fs/
|
|
90
|
+
pattern: "/api/internal/fs/write-file",
|
|
91
|
+
entrypoint: "lightnet/api/internal/fs/write-file.ts",
|
|
92
92
|
prerender: false,
|
|
93
93
|
})
|
|
94
94
|
// Add empty adapter to avoid warning
|
package/src/components/Toast.tsx
CHANGED
|
@@ -46,7 +46,7 @@ export default function Toast({
|
|
|
46
46
|
}}
|
|
47
47
|
>
|
|
48
48
|
<div
|
|
49
|
-
className={`pointer-events-auto flex max-w-sm flex-col items-start gap-1 rounded-
|
|
49
|
+
className={`pointer-events-auto flex max-w-sm flex-col items-start gap-1 rounded-md border p-4 text-base shadow-md backdrop-blur-sm ${alertClasses}`}
|
|
50
50
|
>
|
|
51
51
|
{children}
|
|
52
52
|
</div>
|
package/src/i18n/locals.d.ts
CHANGED
|
@@ -3,36 +3,43 @@ declare namespace App {
|
|
|
3
3
|
/**
|
|
4
4
|
* Provides internationalization helpers.
|
|
5
5
|
*/
|
|
6
|
-
i18n:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
* @param TranslationKey to be translated.
|
|
11
|
-
*/
|
|
12
|
-
t: import("./translate").TranslateFn
|
|
6
|
+
i18n: I18n
|
|
7
|
+
}
|
|
8
|
+
}
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
type I18n = {
|
|
11
|
+
/**
|
|
12
|
+
* Translate a key to the language of the current locale.
|
|
13
|
+
*
|
|
14
|
+
* @param TranslationKey to be translated.
|
|
15
|
+
*/
|
|
16
|
+
t: import("./translate").TranslateFn
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The current locale or the default locale if the current locale is not available.
|
|
20
|
+
*
|
|
21
|
+
* In comparison to Astro.currentLocale this will always return a locale.
|
|
22
|
+
* Use Astro.currentLocale if you want to know the locale that is included in the current path.
|
|
23
|
+
*/
|
|
24
|
+
currentLocale: string
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The current text direction. Left-to-right or right-to-left.
|
|
28
|
+
*/
|
|
29
|
+
direction: "ltr" | "rtl"
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The default locale as defined in the project configuration.
|
|
33
|
+
*/
|
|
34
|
+
defaultLocale: string
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The available locales as defined in the project configuration.
|
|
38
|
+
*/
|
|
39
|
+
locales: string[]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* All available translation keys.
|
|
43
|
+
*/
|
|
44
|
+
translationKeys: string[]
|
|
38
45
|
}
|
package/src/i18n/locals.ts
CHANGED
|
@@ -4,7 +4,7 @@ import config from "virtual:lightnet/config"
|
|
|
4
4
|
import { resolveDefaultLocale } from "./resolve-default-locale"
|
|
5
5
|
import { resolveLanguage } from "./resolve-language"
|
|
6
6
|
import { resolveLocales } from "./resolve-locales"
|
|
7
|
-
import { useTranslate } from "./translate"
|
|
7
|
+
import { translationKeys, useTranslate } from "./translate"
|
|
8
8
|
|
|
9
9
|
export const onRequest: MiddlewareHandler = (
|
|
10
10
|
{ locals, currentLocale: astroCurrentLocale },
|
|
@@ -22,6 +22,7 @@ export const onRequest: MiddlewareHandler = (
|
|
|
22
22
|
defaultLocale,
|
|
23
23
|
direction,
|
|
24
24
|
locales,
|
|
25
|
+
translationKeys,
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
return next()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createContext } from "react"
|
|
2
|
+
|
|
3
|
+
export type I18n = {
|
|
4
|
+
t: (key: string) => string
|
|
5
|
+
currentLocale: string
|
|
6
|
+
direction: "rtl" | "ltr"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type I18nConfig = Omit<I18n, "t"> & {
|
|
10
|
+
translations: Record<string, string>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const I18nContext = createContext<I18n | undefined>(undefined)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates the runtime i18n helpers given a prepared configuration.
|
|
17
|
+
* Wraps the raw translation dictionary with a lookup that throws on missing keys.
|
|
18
|
+
*/
|
|
19
|
+
export const createI18n = ({
|
|
20
|
+
translations,
|
|
21
|
+
currentLocale,
|
|
22
|
+
direction,
|
|
23
|
+
}: I18nConfig) => {
|
|
24
|
+
const t = (key: string) => {
|
|
25
|
+
const value = translations[key]
|
|
26
|
+
if (value) {
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
if (key.match(/^(?:ln|x)\../i)) {
|
|
30
|
+
console.error(`Missing translation for key ${key}`)
|
|
31
|
+
return ""
|
|
32
|
+
}
|
|
33
|
+
return key
|
|
34
|
+
}
|
|
35
|
+
return { t, currentLocale, direction }
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepares the configuration object passed from an Astro page to the React i18n context.
|
|
3
|
+
* Resolves every requested translation key (supporting wildcard suffixes like `ln.dashboard.*`)
|
|
4
|
+
* so the React island receives only the strings it needs.
|
|
5
|
+
*
|
|
6
|
+
* @param i18n i18n helpers sourced from `Astro.locals`.
|
|
7
|
+
* @param translationKeys Specific keys (or wildcard groups) required by the React component.
|
|
8
|
+
* @returns A configuration object containing the resolved config.
|
|
9
|
+
*/
|
|
10
|
+
export const prepareI18nConfig = (
|
|
11
|
+
{ t, translationKeys: allKeys, currentLocale, direction }: I18n,
|
|
12
|
+
translationKeys: string[],
|
|
13
|
+
) => {
|
|
14
|
+
const resolveTranslations = (key: string) => {
|
|
15
|
+
if (key.endsWith("*")) {
|
|
16
|
+
const keyPrefix = key.slice(0, -1)
|
|
17
|
+
return allKeys
|
|
18
|
+
.filter((k) => k.startsWith(keyPrefix))
|
|
19
|
+
.map((k) => [k, t(k)])
|
|
20
|
+
}
|
|
21
|
+
return [[key, t(key)]]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
translations: Object.fromEntries(
|
|
26
|
+
translationKeys.flatMap(resolveTranslations),
|
|
27
|
+
) as Record<string, string>,
|
|
28
|
+
currentLocale,
|
|
29
|
+
direction,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useContext } from "react"
|
|
2
|
+
|
|
3
|
+
import { I18nContext } from "./i18n-context"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrieves the current i18n helpers from context.
|
|
7
|
+
* Must be called inside a React tree wrapped with `I18nContext.Provider`, otherwise throws.
|
|
8
|
+
*/
|
|
9
|
+
export const useI18n = () => {
|
|
10
|
+
const i18n = useContext(I18nContext)
|
|
11
|
+
if (!i18n) {
|
|
12
|
+
throw new Error("No i18n context has been provided")
|
|
13
|
+
}
|
|
14
|
+
return i18n
|
|
15
|
+
}
|
package/src/i18n/translate.ts
CHANGED
|
@@ -22,18 +22,30 @@ const languageCodes = [
|
|
|
22
22
|
]
|
|
23
23
|
const defaultLocale = resolveDefaultLocale(config)
|
|
24
24
|
|
|
25
|
-
await
|
|
25
|
+
const translations = await prepareI18nextTranslations()
|
|
26
|
+
export const translationKeys = [
|
|
27
|
+
...new Set(
|
|
28
|
+
Object.values(translations)
|
|
29
|
+
.map(({ translation }) => translation)
|
|
30
|
+
.flatMap((oneLanguageTranslations) =>
|
|
31
|
+
Object.keys(oneLanguageTranslations),
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const i18n = i18next.createInstance()
|
|
37
|
+
await i18n.init({
|
|
26
38
|
lng: defaultLocale,
|
|
27
39
|
// don't use name spacing
|
|
28
40
|
nsSeparator: false,
|
|
29
41
|
// only use flat keys
|
|
30
42
|
keySeparator: false,
|
|
31
|
-
resources:
|
|
43
|
+
resources: translations,
|
|
32
44
|
})
|
|
33
45
|
|
|
34
46
|
export function useTranslate(bcp47: string | undefined): TranslateFn {
|
|
35
47
|
const resolvedLocale = bcp47 ?? defaultLocale
|
|
36
|
-
const t =
|
|
48
|
+
const t = i18n.getFixedT<TranslationKey>(resolvedLocale)
|
|
37
49
|
const fallbackLng = [
|
|
38
50
|
...resolveLanguage(resolvedLocale).fallbackLanguages,
|
|
39
51
|
defaultLocale,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Icon from "../../../components/Icon"
|
|
2
|
+
import { useI18n } from "../../../i18n/react/useI18n"
|
|
2
3
|
|
|
3
|
-
export default function LoadingSkeleton(
|
|
4
|
+
export default function LoadingSkeleton() {
|
|
5
|
+
const { direction } = useI18n()
|
|
4
6
|
return (
|
|
5
7
|
<div className="flex h-52 animate-pulse items-center overflow-hidden py-2 sm:h-64">
|
|
6
8
|
<div className="h-36 w-36 shrink-0 rounded-md bg-gray-200"></div>
|
|
@@ -4,7 +4,7 @@ import config from "virtual:lightnet/config"
|
|
|
4
4
|
import { getUsedCategories } from "../../../content/get-categories"
|
|
5
5
|
import { contentLanguages } from "../../../content/get-languages"
|
|
6
6
|
import { getMediaTypes } from "../../../content/get-media-types"
|
|
7
|
-
import {
|
|
7
|
+
import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
|
|
8
8
|
import SearchFilterReact from "./SearchFilter.tsx"
|
|
9
9
|
|
|
10
10
|
const { t, currentLocale } = Astro.locals.i18n
|
|
@@ -49,7 +49,16 @@ if (
|
|
|
49
49
|
) {
|
|
50
50
|
initialLanguageFilter = currentLocale
|
|
51
51
|
}
|
|
52
|
-
const
|
|
52
|
+
const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
|
|
53
|
+
"ln.search.title",
|
|
54
|
+
"ln.language",
|
|
55
|
+
"ln.search.placeholder",
|
|
56
|
+
"ln.search.all-languages",
|
|
57
|
+
"ln.type",
|
|
58
|
+
"ln.search.all-types",
|
|
59
|
+
"ln.category",
|
|
60
|
+
"ln.search.all-categories",
|
|
61
|
+
])
|
|
53
62
|
---
|
|
54
63
|
|
|
55
64
|
<SearchFilterReact
|
|
@@ -57,7 +66,7 @@ const translations = provideTranslations(t)
|
|
|
57
66
|
languages={languages}
|
|
58
67
|
mediaTypes={mediaTypes}
|
|
59
68
|
categories={categories}
|
|
60
|
-
|
|
69
|
+
i18nConfig={i18nConfig}
|
|
61
70
|
languageFilterEnabled={languageFilterEnabled}
|
|
62
71
|
typesFilterEnabled={typesFilterEnabled}
|
|
63
72
|
categoriesFilterEnabled={categoriesFilterEnabled}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { useRef } from "react"
|
|
2
2
|
|
|
3
3
|
import Icon from "../../../components/Icon"
|
|
4
|
+
import { createI18n, type I18nConfig } from "../../../i18n/react/i18n-context"
|
|
4
5
|
import { useDebounce } from "../hooks/use-debounce"
|
|
5
6
|
import { useSearchQueryParam } from "../hooks/use-search-query-param"
|
|
6
|
-
import type {
|
|
7
|
-
TranslationKey,
|
|
8
|
-
Translations,
|
|
9
|
-
} from "../utils/search-filter-translations"
|
|
10
7
|
import { CATEGORY, LANGUAGE, SEARCH, TYPE } from "../utils/search-query"
|
|
11
8
|
import Select from "./Select"
|
|
12
9
|
|
|
@@ -16,7 +13,7 @@ interface Props {
|
|
|
16
13
|
languages: FilterValue[]
|
|
17
14
|
categories: FilterValue[]
|
|
18
15
|
mediaTypes: FilterValue[]
|
|
19
|
-
|
|
16
|
+
i18nConfig: I18nConfig
|
|
20
17
|
languageFilterEnabled: boolean
|
|
21
18
|
typesFilterEnabled: boolean
|
|
22
19
|
categoriesFilterEnabled: boolean
|
|
@@ -26,7 +23,7 @@ interface Props {
|
|
|
26
23
|
export default function SearchFilter({
|
|
27
24
|
categories,
|
|
28
25
|
mediaTypes,
|
|
29
|
-
|
|
26
|
+
i18nConfig,
|
|
30
27
|
languages,
|
|
31
28
|
languageFilterEnabled,
|
|
32
29
|
typesFilterEnabled,
|
|
@@ -40,7 +37,7 @@ export default function SearchFilter({
|
|
|
40
37
|
|
|
41
38
|
const searchInput = useRef<HTMLInputElement | null>(null)
|
|
42
39
|
|
|
43
|
-
const t = (
|
|
40
|
+
const { t } = createI18n(i18nConfig)
|
|
44
41
|
|
|
45
42
|
const debouncedSetSearch = useDebounce((value: string) => {
|
|
46
43
|
setSearch(value)
|
|
@@ -4,10 +4,10 @@ import { getCollection } from "astro:content"
|
|
|
4
4
|
import { getUsedCategories } from "../../../content/get-categories"
|
|
5
5
|
import { contentLanguages } from "../../../content/get-languages"
|
|
6
6
|
import { getMediaTypes } from "../../../content/get-media-types"
|
|
7
|
-
import {
|
|
7
|
+
import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
|
|
8
8
|
import SearchListReact from "./SearchList.tsx"
|
|
9
9
|
|
|
10
|
-
const { t, currentLocale
|
|
10
|
+
const { t, currentLocale } = Astro.locals.i18n
|
|
11
11
|
|
|
12
12
|
const categories: Record<string, string> = {}
|
|
13
13
|
for (const { id, name } of await getUsedCategories(currentLocale, t)) {
|
|
@@ -34,15 +34,16 @@ const languages = Object.fromEntries(
|
|
|
34
34
|
|
|
35
35
|
const mediaItemsTotal = (await getCollection("media")).length
|
|
36
36
|
|
|
37
|
-
const translations = provideTranslations(t)
|
|
38
37
|
const showLanguage = contentLanguages.length > 1
|
|
38
|
+
|
|
39
|
+
const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
|
|
40
|
+
"ln.search.no-results",
|
|
41
|
+
])
|
|
39
42
|
---
|
|
40
43
|
|
|
41
44
|
<SearchListReact
|
|
42
45
|
client:load
|
|
43
|
-
|
|
44
|
-
direction={direction}
|
|
45
|
-
translations={translations}
|
|
46
|
+
i18nConfig={i18nConfig}
|
|
46
47
|
categories={categories}
|
|
47
48
|
mediaTypes={mediaTypes}
|
|
48
49
|
languages={languages}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
2
2
|
import { useEffect, useRef, useState } from "react"
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
createI18n,
|
|
6
|
+
type I18nConfig,
|
|
7
|
+
I18nContext,
|
|
8
|
+
} from "../../../i18n/react/i18n-context"
|
|
4
9
|
import { useSearch } from "../hooks/use-search"
|
|
5
|
-
import type { TranslationKey, Translations } from "../utils/search-translations"
|
|
6
10
|
import LoadingSkeleton from "./LoadingSkeleton"
|
|
7
11
|
import SearchListItem, {
|
|
8
12
|
type MediaType,
|
|
@@ -10,9 +14,7 @@ import SearchListItem, {
|
|
|
10
14
|
} from "./SearchListItem"
|
|
11
15
|
|
|
12
16
|
interface Props {
|
|
13
|
-
|
|
14
|
-
translations: Translations
|
|
15
|
-
direction: "rtl" | "ltr"
|
|
17
|
+
i18nConfig: I18nConfig
|
|
16
18
|
categories: Record<string, string>
|
|
17
19
|
languages: Record<string, TranslatedLanguage>
|
|
18
20
|
showLanguage: boolean
|
|
@@ -21,11 +23,9 @@ interface Props {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export default function SearchList({
|
|
24
|
-
currentLocale,
|
|
25
26
|
categories,
|
|
26
|
-
|
|
27
|
+
i18nConfig,
|
|
27
28
|
languages,
|
|
28
|
-
direction,
|
|
29
29
|
showLanguage,
|
|
30
30
|
mediaTypes,
|
|
31
31
|
mediaItemsTotal,
|
|
@@ -63,11 +63,10 @@ export default function SearchList({
|
|
|
63
63
|
observer.disconnect()
|
|
64
64
|
}
|
|
65
65
|
}, [])
|
|
66
|
-
|
|
67
|
-
const t = (key: TranslationKey) => translations[key]
|
|
66
|
+
const i18n = createI18n(i18nConfig)
|
|
68
67
|
|
|
69
68
|
return (
|
|
70
|
-
|
|
69
|
+
<I18nContext.Provider value={i18n}>
|
|
71
70
|
<div ref={listRef} className="px-4 md:px-8">
|
|
72
71
|
<ol
|
|
73
72
|
className="relative w-full divide-y divide-gray-200"
|
|
@@ -89,13 +88,11 @@ export default function SearchList({
|
|
|
89
88
|
}}
|
|
90
89
|
>
|
|
91
90
|
{isLoading ? (
|
|
92
|
-
<LoadingSkeleton
|
|
91
|
+
<LoadingSkeleton />
|
|
93
92
|
) : (
|
|
94
93
|
<SearchListItem
|
|
95
94
|
item={item}
|
|
96
|
-
direction={direction}
|
|
97
95
|
showLanguage={showLanguage}
|
|
98
|
-
currentLocale={currentLocale}
|
|
99
96
|
categories={categories}
|
|
100
97
|
languages={languages}
|
|
101
98
|
mediaTypes={mediaTypes}
|
|
@@ -108,9 +105,9 @@ export default function SearchList({
|
|
|
108
105
|
</div>
|
|
109
106
|
{!results.length && !isLoading && (
|
|
110
107
|
<div className="mt-24 text-center font-bold text-gray-500">
|
|
111
|
-
{t("ln.search.no-results")}
|
|
108
|
+
{i18n.t("ln.search.no-results")}
|
|
112
109
|
</div>
|
|
113
110
|
)}
|
|
114
|
-
|
|
111
|
+
</I18nContext.Provider>
|
|
115
112
|
)
|
|
116
113
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import CoverImageDecorator from "../../../components/CoverImageDecorator"
|
|
2
2
|
import Icon from "../../../components/Icon"
|
|
3
|
+
import { useI18n } from "../../../i18n/react/useI18n"
|
|
3
4
|
import { detailsPagePath } from "../../../utils/paths"
|
|
4
5
|
import type { SearchItem } from "../api/search-response"
|
|
5
6
|
|
|
@@ -16,8 +17,6 @@ export type TranslatedLanguage = {
|
|
|
16
17
|
|
|
17
18
|
interface Props {
|
|
18
19
|
item: SearchItem
|
|
19
|
-
currentLocale: string | undefined
|
|
20
|
-
direction: "rtl" | "ltr"
|
|
21
20
|
categories: Record<string, string>
|
|
22
21
|
languages: Record<string, TranslatedLanguage>
|
|
23
22
|
showLanguage: boolean
|
|
@@ -26,13 +25,12 @@ interface Props {
|
|
|
26
25
|
|
|
27
26
|
export default function SearchListItem({
|
|
28
27
|
item,
|
|
29
|
-
currentLocale,
|
|
30
28
|
categories,
|
|
31
29
|
languages,
|
|
32
|
-
direction,
|
|
33
30
|
showLanguage,
|
|
34
31
|
mediaTypes,
|
|
35
32
|
}: Props) {
|
|
33
|
+
const { currentLocale, direction } = useI18n()
|
|
36
34
|
const coverImageStyle = mediaTypes[item.type].coverImageStyle
|
|
37
35
|
return (
|
|
38
36
|
<a
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { FieldErrors } from "./FieldErrors"
|
|
2
|
-
import { useFieldContext } from "./form-context"
|
|
3
|
-
|
|
4
|
-
export default function TextField({ label }: { label: string }) {
|
|
5
|
-
const field = useFieldContext<string>()
|
|
6
|
-
return (
|
|
7
|
-
<>
|
|
8
|
-
<label className="dy-form-control w-full max-w-sm">
|
|
9
|
-
<div className="dy-label">
|
|
10
|
-
<span className="dy-label-text">{label}</span>
|
|
11
|
-
</div>
|
|
12
|
-
<input
|
|
13
|
-
id={field.name}
|
|
14
|
-
name={field.name}
|
|
15
|
-
value={field.state.value}
|
|
16
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
17
|
-
onBlur={field.handleBlur}
|
|
18
|
-
className={`dy-input dy-input-bordered dy-input-sm w-full max-w-sm ${field.state.meta.errors.length ? "dy-input-error" : ""}`}
|
|
19
|
-
/>
|
|
20
|
-
<FieldErrors meta={field.state.meta} />
|
|
21
|
-
</label>
|
|
22
|
-
</>
|
|
23
|
-
)
|
|
24
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
const translationKeys = [
|
|
2
|
-
"ln.search.title",
|
|
3
|
-
"ln.language",
|
|
4
|
-
"ln.search.placeholder",
|
|
5
|
-
"ln.search.all-languages",
|
|
6
|
-
"ln.type",
|
|
7
|
-
"ln.search.all-types",
|
|
8
|
-
"ln.category",
|
|
9
|
-
"ln.search.all-categories",
|
|
10
|
-
] as const
|
|
11
|
-
|
|
12
|
-
export type TranslationKey = (typeof translationKeys)[number]
|
|
13
|
-
|
|
14
|
-
export type Translations = Record<TranslationKey, string>
|
|
15
|
-
|
|
16
|
-
export const provideTranslations = (translate: (key: string) => string) => {
|
|
17
|
-
return Object.fromEntries(
|
|
18
|
-
translationKeys.map((key) => [key, translate(key)]),
|
|
19
|
-
) as Translations
|
|
20
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const translationKeys = ["ln.search.no-results"] as const
|
|
2
|
-
|
|
3
|
-
export type TranslationKey = (typeof translationKeys)[number]
|
|
4
|
-
|
|
5
|
-
export type Translations = Record<TranslationKey, string>
|
|
6
|
-
|
|
7
|
-
export const provideTranslations = (translate: (key: string) => string) => {
|
|
8
|
-
return Object.fromEntries(
|
|
9
|
-
translationKeys.map((key) => [key, translate(key)]),
|
|
10
|
-
) as Translations
|
|
11
|
-
}
|