lightnet 3.10.2 → 3.10.4

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/__e2e__/admin.spec.ts +297 -100
  3. package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
  4. package/__e2e__/fixtures/basics/package.json +5 -5
  5. package/package.json +10 -9
  6. package/src/admin/api/fs/{writeText.ts → write-file.ts} +9 -6
  7. package/src/admin/components/form/Input.tsx +36 -0
  8. package/src/admin/components/form/Select.tsx +43 -0
  9. package/src/admin/components/form/SubmitButton.tsx +21 -16
  10. package/src/admin/components/form/atoms/ErrorMessage.tsx +34 -0
  11. package/src/admin/components/form/atoms/Hint.tsx +10 -0
  12. package/src/admin/components/form/atoms/Label.tsx +19 -0
  13. package/src/admin/components/form/atoms/Legend.tsx +10 -0
  14. package/src/admin/components/form/hooks/use-field-error.tsx +21 -0
  15. package/src/admin/i18n/translations/en.yml +8 -2
  16. package/src/admin/pages/media/EditForm.tsx +43 -43
  17. package/src/admin/pages/media/EditRoute.astro +28 -5
  18. package/src/admin/pages/media/fields/Authors.tsx +58 -0
  19. package/src/admin/pages/media/file-system.ts +3 -3
  20. package/src/admin/pages/media/media-item-store.ts +9 -7
  21. package/src/admin/types/media-item.ts +10 -1
  22. package/src/astro-integration/integration.ts +2 -2
  23. package/src/components/HeroSection.astro +1 -1
  24. package/src/components/HighlightSection.astro +1 -1
  25. package/src/components/SearchInput.astro +1 -1
  26. package/src/components/Toast.tsx +1 -1
  27. package/src/i18n/react/i18n-context.ts +14 -8
  28. package/src/layouts/MarkdownPage.astro +1 -1
  29. package/src/layouts/components/Menu.astro +1 -1
  30. package/src/layouts/components/PageNavigation.astro +1 -1
  31. package/src/pages/details-page/components/main-details/OpenButton.astro +1 -1
  32. package/src/pages/search-page/components/LoadingSkeleton.tsx +1 -1
  33. package/src/pages/search-page/components/SearchFilter.tsx +1 -1
  34. package/src/pages/search-page/components/SearchListItem.tsx +1 -1
  35. package/src/pages/search-page/components/Select.tsx +1 -1
  36. package/src/admin/components/form/FieldErrors.tsx +0 -22
  37. package/src/admin/components/form/TextField.tsx +0 -24
  38. package/src/admin/components/form/form-context.ts +0 -4
  39. package/src/admin/components/form/index.ts +0 -16
@@ -1,9 +1,9 @@
1
- import { useStore } from "@tanstack/react-form"
2
1
  import { useEffect, useRef, useState } from "react"
2
+ import { type Control, useFormState } from "react-hook-form"
3
3
 
4
4
  import Icon from "../../../components/Icon"
5
5
  import { useI18n } from "../../../i18n/react/useI18n"
6
- import { useFormContext } from "./form-context"
6
+ import type { MediaItem } from "../../types/media-item"
7
7
 
8
8
  const SUCCESS_DURATION_MS = 2000
9
9
 
@@ -30,25 +30,30 @@ const icons = {
30
30
  error: "mdi--error-outline",
31
31
  } as const
32
32
 
33
- export default function SubmitButton() {
34
- const form = useFormContext()
33
+ export default function SubmitButton({
34
+ control,
35
+ className,
36
+ }: {
37
+ control: Control<MediaItem>
38
+ className?: string
39
+ }) {
35
40
  const { t } = useI18n()
36
- const { submissionAttempts, isSubmitting, isSubmitSuccessful } = useStore(
37
- form.store,
38
- (state) => ({
39
- canSubmit: state.canSubmit,
40
- isSubmitting: state.isSubmitting,
41
- isSubmitSuccessful: state.isSubmitSuccessful,
42
- submissionAttempts: state.submissionAttempts,
43
- }),
44
- )
45
- const buttonState = useButtonState(isSubmitSuccessful, submissionAttempts)
46
- const buttonClass = `${baseButtonClass} ${buttonStateClasses[buttonState]}`
41
+ const { isValid, isSubmitting, isSubmitSuccessful, submitCount } =
42
+ useFormState({
43
+ control,
44
+ })
45
+
46
+ const buttonState = useButtonState(isSubmitSuccessful, submitCount)
47
+ const buttonClass = `${baseButtonClass} ${buttonStateClasses[buttonState]} ${className}`
47
48
  const label = buttonLabels[buttonState]
48
49
  const icon = icons[buttonState]
49
50
 
50
51
  return (
51
- <button className={buttonClass} type="submit" disabled={isSubmitting}>
52
+ <button
53
+ className={buttonClass}
54
+ type="submit"
55
+ disabled={!isValid || isSubmitting}
56
+ >
52
57
  {icon && <Icon className={icon} ariaLabel="" />}
53
58
  {t(label)}
54
59
  </button>
@@ -0,0 +1,34 @@
1
+ import { ErrorMessage as RhfErrorMessage } from "@hookform/error-message"
2
+ import { type Control, useFormState } from "react-hook-form"
3
+
4
+ import { useI18n } from "../../../../i18n/react/useI18n"
5
+
6
+ export default function ErrorMessage({
7
+ name,
8
+ control,
9
+ }: {
10
+ name: string
11
+ control: Control<any>
12
+ }) {
13
+ const { t } = useI18n()
14
+ const { errors } = useFormState({ control, name, exact: true })
15
+ return (
16
+ <RhfErrorMessage
17
+ errors={errors}
18
+ name={name}
19
+ render={({ message }) => {
20
+ if (!message) {
21
+ return null
22
+ }
23
+ return (
24
+ <p
25
+ className="my-2 flex flex-col gap-1 text-sm text-rose-800"
26
+ role="alert"
27
+ >
28
+ {t(message)}
29
+ </p>
30
+ )
31
+ }}
32
+ />
33
+ )
34
+ }
@@ -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-12 w-full items-start justify-end p-2">
7
+ {hint && <span className="dy-label-text-alt">{t(hint)}</span>}
8
+ </div>
9
+ )
10
+ }
@@ -0,0 +1,19 @@
1
+ import { useI18n } from "../../../../i18n/react/useI18n"
2
+
3
+ export default function Label({
4
+ label,
5
+ for: htmlFor,
6
+ }: {
7
+ label: string
8
+ for: string
9
+ }) {
10
+ const { t } = useI18n()
11
+ return (
12
+ <label
13
+ htmlFor={htmlFor}
14
+ className="pb-2 text-sm font-bold uppercase text-gray-600"
15
+ >
16
+ {t(label)}
17
+ </label>
18
+ )
19
+ }
@@ -0,0 +1,10 @@
1
+ import { useI18n } from "../../../../i18n/react/useI18n"
2
+
3
+ export default function Label({ label }: { label: string }) {
4
+ const { t } = useI18n()
5
+ return (
6
+ <legend className="pb-2 text-sm font-bold uppercase text-gray-600">
7
+ {t(label)}
8
+ </legend>
9
+ )
10
+ }
@@ -0,0 +1,21 @@
1
+ import { type Control, type FieldError, useFormState } from "react-hook-form"
2
+
3
+ export function useFieldError({
4
+ control,
5
+ name,
6
+ index,
7
+ }: {
8
+ control: Control<any>
9
+ name: string
10
+ index?: number
11
+ }) {
12
+ const { errors } = useFormState({ control, name, exact: true })
13
+ const error = errors[name]
14
+ if (!error) {
15
+ return undefined
16
+ }
17
+ if (Array.isArray(error)) {
18
+ return index !== undefined ? (error[index] as FieldError) : undefined
19
+ }
20
+ return error as FieldError
21
+ }
@@ -2,10 +2,16 @@ ln.admin.edit: Edit
2
2
  ln.admin.save: Save
3
3
  ln.admin.saved: Saved
4
4
  ln.admin.failed: Failed
5
+ ln.admin.remove: Remove
6
+ ln.admin.add-author: Add Author
5
7
  ln.admin.edit-media-item: Edit media item
6
8
  ln.admin.back-to-details-page: Back to details page
7
9
  ln.admin.title: Title
8
10
  ln.admin.common-id: Common ID
9
- ln.admin.toast.invalid-data.title: Invalid form data
10
- ln.admin.toast.invalid-data.hint: Check the fields and try again.
11
+ ln.admin.authors: Authors
12
+ ln.admin.created-on: Created on
13
+ ln.admin.created-on-hint: When has this item been created on this library?
14
+ ln.admin.common-id-hint: The english title, all lowercase, words separated with hyphens.
11
15
  ln.admin.errors.non-empty-string: String must contain at least 1 character(s)
16
+ ln.admin.errors.invalid-date: Invalid date
17
+ ln.admin.errors.required: Required field
@@ -1,72 +1,72 @@
1
- import { revalidateLogic } from "@tanstack/react-form"
1
+ import { zodResolver } from "@hookform/resolvers/zod"
2
+ import { useForm } from "react-hook-form"
2
3
 
3
- import { showToastById } from "../../../components/showToast"
4
- import Toast from "../../../components/Toast"
5
4
  import {
6
5
  createI18n,
7
6
  type I18nConfig,
8
7
  I18nContext,
9
8
  } from "../../../i18n/react/i18n-context"
10
- import { useAppForm } from "../../components/form"
9
+ import Input from "../../components/form/Input"
10
+ import Select from "../../components/form/Select"
11
+ import SubmitButton from "../../components/form/SubmitButton"
11
12
  import { type MediaItem, mediaItemSchema } from "../../types/media-item"
13
+ import Authors from "./fields/Authors"
12
14
  import { updateMediaItem } from "./media-item-store"
13
15
 
14
16
  export default function EditForm({
15
17
  mediaId,
16
18
  mediaItem,
17
19
  i18nConfig,
20
+ mediaTypes,
21
+ languages,
18
22
  }: {
19
23
  mediaId: string
20
24
  mediaItem: MediaItem
21
25
  i18nConfig: I18nConfig
26
+ mediaTypes: { id: string; label: string }[]
27
+ languages: { id: string; label: string }[]
22
28
  }) {
23
- const form = useAppForm({
29
+ const { handleSubmit, control } = useForm({
24
30
  defaultValues: mediaItem,
25
- validators: {
26
- onDynamic: mediaItemSchema,
27
- },
28
- validationLogic: revalidateLogic({
29
- mode: "blur",
30
- modeAfterSubmission: "change",
31
- }),
32
- onSubmit: async ({ value }) => {
33
- await updateMediaItem(mediaId, { ...mediaItem, ...value })
34
- },
35
- onSubmitInvalid: () => {
36
- showToastById("invalid-form-data-toast")
37
- },
31
+ mode: "onTouched",
32
+ resolver: zodResolver(mediaItemSchema),
38
33
  })
34
+ const onSubmit = handleSubmit(
35
+ async (data) => await updateMediaItem(mediaId, { ...mediaItem, ...data }),
36
+ )
39
37
  const i18n = createI18n(i18nConfig)
40
- const { t } = i18n
41
-
42
38
  return (
43
39
  <I18nContext.Provider value={i18n}>
44
- <form
45
- onSubmit={(e) => {
46
- e.preventDefault()
47
- form.handleSubmit()
48
- }}
49
- className="flex flex-col items-start gap-4"
50
- >
51
- <form.AppField
40
+ <form onSubmit={onSubmit}>
41
+ <Input name="title" label="ln.admin.title" control={control} />
42
+ <Input
52
43
  name="commonId"
53
- children={(field) => (
54
- <field.TextField label={t("ln.admin.common-id")} />
55
- )}
44
+ label="ln.admin.common-id"
45
+ hint="ln.admin.common-id-hint"
46
+ control={control}
47
+ />
48
+ <Select
49
+ name="type"
50
+ label="ln.type"
51
+ options={mediaTypes}
52
+ control={control}
56
53
  />
57
- <form.AppField
58
- name="title"
59
- children={(field) => <field.TextField label={t("ln.admin.title")} />}
54
+ <Select
55
+ name="language"
56
+ label="ln.language"
57
+ options={languages}
58
+ control={control}
60
59
  />
61
- <form.AppForm>
62
- <form.SubmitButton />
63
- <Toast id="invalid-form-data-toast" variant="error">
64
- <div className="font-bold text-gray-700">
65
- {t("ln.admin.toast.invalid-data.title")}
66
- </div>
67
- {t("ln.admin.toast.invalid-data.hint")}
68
- </Toast>
69
- </form.AppForm>
60
+ <Authors control={control} />
61
+ <Input
62
+ name="dateCreated"
63
+ label="ln.admin.created-on"
64
+ hint="ln.admin.created-on-hint"
65
+ type="date"
66
+ control={control}
67
+ />
68
+
69
+ <SubmitButton className="mt-8" control={control} />
70
70
  </form>
71
71
  </I18nContext.Provider>
72
72
  )
@@ -4,6 +4,7 @@ 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"
7
8
  import { prepareI18nConfig } from "../../../i18n/react/prepare-i18n-config"
8
9
  import { resolveLocales } from "../../../i18n/resolve-locales"
9
10
  import Page from "../../../layouts/Page.astro"
@@ -17,25 +18,47 @@ export const getStaticPaths = (async () => {
17
18
  }) satisfies GetStaticPaths
18
19
 
19
20
  const { mediaId } = Astro.params
20
- const mediaItemEntry = await getRawMediaItem(mediaId)
21
+ const { data: mediaItem } = await getRawMediaItem(mediaId)
21
22
 
22
- const i18nConfig = prepareI18nConfig(Astro.locals.i18n, ["ln.admin.*"])
23
+ const formData = {
24
+ ...mediaItem,
25
+ authors: mediaItem.authors?.map((value) => ({ value })) ?? [],
26
+ }
27
+
28
+ const i18nConfig = prepareI18nConfig(Astro.locals.i18n, [
29
+ "ln.admin.*",
30
+ "ln.type",
31
+ "ln.language",
32
+ ])
23
33
  const { t } = Astro.locals.i18n
34
+
35
+ const mediaTypes = (await getMediaTypes()).map(({ id, data: { label } }) => ({
36
+ id,
37
+ label: t(label),
38
+ }))
39
+ const languages = config.languages.map(({ code, label }) => ({
40
+ id: code,
41
+ label: t(label),
42
+ }))
24
43
  ---
25
44
 
26
45
  <Page>
27
- <div class="mx-auto max-w-screen-lg px-4 pt-12 md:px-8">
46
+ <div class="mx-auto max-w-screen-md px-4 pt-12 md:px-8">
28
47
  <a
29
48
  class="underline"
30
49
  href=`/${Astro.currentLocale}/media/faithful-freestyle--en`
31
50
  >{t("ln.admin.back-to-details-page")}</a
32
51
  >
33
- <h1 class="mb-4 mt-8 text-lg">{t("ln.admin.edit-media-item")}</h1>
52
+ <h1 class="mb-10 mt-10 text-2xl">
53
+ {t("ln.admin.edit-media-item")}
54
+ </h1>
34
55
 
35
56
  <EditForm
36
57
  mediaId={mediaId}
37
- mediaItem={mediaItemEntry.data}
58
+ mediaItem={formData}
38
59
  i18nConfig={i18nConfig}
60
+ mediaTypes={mediaTypes}
61
+ languages={languages}
39
62
  client:load
40
63
  />
41
64
  </div>
@@ -0,0 +1,58 @@
1
+ import { type Control, useFieldArray } from "react-hook-form"
2
+
3
+ import Icon from "../../../../components/Icon"
4
+ import { useI18n } from "../../../../i18n/react/useI18n"
5
+ import ErrorMessage from "../../../components/form/atoms/ErrorMessage"
6
+ import Hint from "../../../components/form/atoms/Hint"
7
+ import Legend from "../../../components/form/atoms/Legend"
8
+ import type { MediaItem } from "../../../types/media-item"
9
+
10
+ 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
+ return (
17
+ <fieldset key="authors">
18
+ <Legend label="ln.admin.authors" />
19
+ <div className="flex w-full flex-col divide-y divide-gray-300 rounded-lg border border-gray-300">
20
+ {fields.map((author, index) => (
21
+ <div className="p-2" key={author.id}>
22
+ <div className="flex w-full items-center gap-2">
23
+ <input
24
+ className="dy-input dy-input-sm grow"
25
+ {...control.register(`authors.${index}.value`)}
26
+ />
27
+ <button
28
+ className="flex items-center p-2 text-gray-600 hover:text-gray-900"
29
+ type="button"
30
+ onClick={() => remove(index)}
31
+ >
32
+ <Icon
33
+ className="mdi--remove"
34
+ ariaLabel={t("ln.admin.remove")}
35
+ />
36
+ </button>
37
+ </div>
38
+ <ErrorMessage name={`authors.${index}.value`} control={control} />
39
+ </div>
40
+ ))}
41
+ <button
42
+ type="button"
43
+ className="p-4 text-sm font-bold text-gray-600 hover:bg-gray-200"
44
+ onClick={() => {
45
+ append(
46
+ { value: "" },
47
+ { focusName: `authors.${fields.length}.value` },
48
+ )
49
+ }}
50
+ >
51
+ {t("ln.admin.add-author")}
52
+ </button>
53
+ </div>
54
+ <ErrorMessage name="authors" control={control} />
55
+ <Hint />
56
+ </fieldset>
57
+ )
58
+ }
@@ -1,6 +1,6 @@
1
- export const writeText = (path: string, body: string) => {
1
+ export const writeFile = (path: string, body: string) => {
2
2
  return fetch(
3
- `/api/internal/fs/writeText?path=${encodeURIComponent(path.replace(/^\//, ""))}`,
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 writeText(path, JSON.stringify(sortObject(object), null, 2))
13
+ return writeFile(path, JSON.stringify(sortObject(object), null, 2))
14
14
  }
15
15
 
16
16
  const resolveContentType = (path: string) => {
@@ -1,11 +1,13 @@
1
- import { type MediaItem, mediaItemSchema } from "../../types/media-item"
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: item.authors.map(({ value }) => value),
12
+ }
11
13
  }
@@ -1,10 +1,19 @@
1
1
  import { z } from "astro/zod"
2
2
 
3
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"
4
6
 
5
7
  export const mediaItemSchema = z.object({
6
8
  commonId: z.string().nonempty(NON_EMPTY_STRING),
7
9
  title: z.string().nonempty(NON_EMPTY_STRING),
10
+ type: z.string().nonempty(REQUIRED),
11
+ language: z.string().nonempty(REQUIRED),
12
+ authors: z
13
+ .object({ value: z.string().nonempty(NON_EMPTY_STRING) })
14
+ .array()
15
+ .min(1),
16
+ dateCreated: z.string().date(INVALID_DATE),
8
17
  })
9
18
 
10
- export type MediaItem = z.infer<typeof mediaItemSchema>
19
+ export type MediaItem = z.input<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/writeText",
91
- entrypoint: "lightnet/api/internal/fs/writeText.ts",
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
@@ -52,7 +52,7 @@ const subtitleSizes = {
52
52
  alt=""
53
53
  />
54
54
  <div
55
- class="bg-gradient-radial absolute top-0 flex h-full w-full flex-col items-center justify-center from-black/30 to-black/40 p-4 text-center text-gray-50"
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="bg-primary hover:bg-primary/85 inline-flex items-center justify-center gap-2 rounded-2xl px-6 py-3 text-sm font-bold uppercase text-gray-50 shadow-sm hover:text-gray-100"
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}
@@ -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 group w-full rounded-2xl shadow-sm outline-2 outline-offset-2 outline-gray-400 group-focus-within:outline"
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
@@ -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-2xl border p-4 text-base shadow-md backdrop-blur-sm ${alertClasses}`}
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>
@@ -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,12 +21,18 @@ export const createI18n = ({
21
21
  currentLocale,
22
22
  direction,
23
23
  }: I18nConfig) => {
24
- const t = (key: string) => {
25
- const translated = translations[key]
26
- if (!translated) {
27
- throw new Error(`Missing translation for key ${key}`)
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
- return translated
30
- }
31
- return { t, currentLocale, direction }
36
+ return { t, currentLocale, direction }
37
+ }, [])
32
38
  }
@@ -9,7 +9,7 @@ type Props = {
9
9
 
10
10
  <Page>
11
11
  <article
12
- class="prose prose-img:rounded-md mx-auto mt-8 max-w-screen-md px-4 sm:mt-16 md:px-8"
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 />
@@ -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="hover:text-primary flex cursor-pointer rounded-md p-3 text-gray-600"
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="hover:text-primary flex p-3 text-gray-600"
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)}
@@ -24,7 +24,7 @@ const content = createContentMetadata(item.data.content[0])
24
24
  content.isExternal && (
25
25
  <Icon
26
26
  ariaLabel={Astro.locals.i18n.t("ln.external-link")}
27
- className={`mdi--external-link shrink-0`}
27
+ className={`shrink-0 mdi--external-link`}
28
28
  />
29
29
  )
30
30
  }
@@ -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="mdi--chevron-right my-auto me-4 ms-2 hidden shrink-0 text-2xl text-gray-300 sm:block"
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
  />
@@ -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 text-xl" ariaLabel="" />
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 && (
@@ -99,7 +99,7 @@ export default function SearchListItem({
99
99
  </div>
100
100
  </div>
101
101
  <Icon
102
- 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"
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
  />
@@ -17,7 +17,7 @@ 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 rounded-xl"
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
  >