Add language filter for a lot of pages
This commit is contained in:
		
							parent
							
								
									3c7b9aa2d6
								
							
						
					
					
						commit
						872f31a6a3
					
				| @ -2,10 +2,7 @@ import { Chip } from "components/Chip"; | ||||
| import { Markdawn } from "components/Markdown/Markdawn"; | ||||
| import { RecorderChip } from "components/RecorderChip"; | ||||
| import { ToolTip } from "components/ToolTip"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts"; | ||||
| import { useAtomGetter } from "helpers/atoms"; | ||||
| import { prettyLanguage } from "helpers/formatters"; | ||||
| import { ContentStatus, useFormat } from "hooks/useFormat"; | ||||
| 
 | ||||
| /* | ||||
| @ -40,8 +37,7 @@ export const Credits = ({ | ||||
|   authors = [], | ||||
|   notes, | ||||
| }: Props): JSX.Element => { | ||||
|   const { format, formatStatusDescription, formatStatusLabel } = useFormat(); | ||||
|   const languages = useAtomGetter(atoms.localData.languages); | ||||
|   const { format, formatStatusDescription, formatStatusLabel, formatLanguage } = useFormat(); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="grid place-items-center gap-5"> | ||||
| @ -54,7 +50,7 @@ export const Credits = ({ | ||||
|               <h2 className="text-xl">{format("translation_notice")}</h2> | ||||
|               <div className="flex flex-wrap place-content-center place-items-center gap-2"> | ||||
|                 <p className="font-headers font-bold">{format("source_language")}:</p> | ||||
|                 <Chip text={prettyLanguage(sourceLanguageCode, languages)} /> | ||||
|                 <Chip text={formatLanguage(sourceLanguageCode)} /> | ||||
|               </div> | ||||
|             </> | ||||
|           )} | ||||
|  | ||||
| @ -2,11 +2,9 @@ import { Fragment } from "react"; | ||||
| import { ToolTip } from "../ToolTip"; | ||||
| import { Button } from "./Button"; | ||||
| import { cJoin } from "helpers/className"; | ||||
| import { prettyLanguage } from "helpers/formatters"; | ||||
| import { iterateMap } from "helpers/others"; | ||||
| import { sendAnalytics } from "helpers/analytics"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { useAtomGetter } from "helpers/atoms"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -32,7 +30,7 @@ export const LanguageSwitcher = ({ | ||||
|   onLanguageChanged, | ||||
|   showBadge = true, | ||||
| }: Props): JSX.Element => { | ||||
|   const languages = useAtomGetter(atoms.localData.languages); | ||||
|   const { formatLanguage } = useFormat(); | ||||
|   return ( | ||||
|     <ToolTip | ||||
|       content={ | ||||
| @ -45,7 +43,7 @@ export const LanguageSwitcher = ({ | ||||
|                   onLanguageChanged(value); | ||||
|                   sendAnalytics("Language Switcher", `Switch language (${locale})`); | ||||
|                 }} | ||||
|                 text={prettyLanguage(locale, languages)} | ||||
|                 text={formatLanguage(locale)} | ||||
|               /> | ||||
|             </Fragment> | ||||
|           ))} | ||||
|  | ||||
| @ -7,7 +7,6 @@ import { TextInput } from "components/Inputs/TextInput"; | ||||
| import { Popup } from "components/Containers/Popup"; | ||||
| import { sendAnalytics } from "helpers/analytics"; | ||||
| import { cJoin, cIf } from "helpers/className"; | ||||
| import { prettyLanguage } from "helpers/formatters"; | ||||
| import { filterHasAttributes, isDefined } from "helpers/asserts"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms"; | ||||
| @ -36,8 +35,7 @@ export const SettingsPopup = (): JSX.Element => { | ||||
|   const perfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled); | ||||
|   const isPerfModeToggleable = useAtomGetter(atoms.settings.isPerfModeToggleable); | ||||
| 
 | ||||
|   const languages = useAtomGetter(atoms.localData.languages); | ||||
|   const { format } = useFormat(); | ||||
|   const { format, formatLanguage } = useFormat(); | ||||
|   const currencies = useAtomGetter(atoms.localData.currencies); | ||||
| 
 | ||||
|   const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout); | ||||
| @ -77,7 +75,7 @@ export const SettingsPopup = (): JSX.Element => { | ||||
|               <OrderableList | ||||
|                 items={preferredLanguages.map((locale) => ({ | ||||
|                   code: locale, | ||||
|                   name: prettyLanguage(locale, languages), | ||||
|                   name: formatLanguage(locale), | ||||
|                 }))} | ||||
|                 insertLabels={[ | ||||
|                   { | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| import { MouseEventHandler, useCallback } from "react"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { Markdown } from "./Markdown/Markdown"; | ||||
| import { Chip } from "components/Chip"; | ||||
| import { Ico } from "components/Ico"; | ||||
| @ -7,13 +6,14 @@ import { Img } from "components/Img"; | ||||
| import { UpPressable } from "components/Containers/UpPressable"; | ||||
| import { DatePickerFragment, PricePickerFragment, UploadImageFragment } from "graphql/generated"; | ||||
| import { cIf, cJoin } from "helpers/className"; | ||||
| import { prettyDate, prettyDuration, prettyPrice, prettyShortenNumber } from "helpers/formatters"; | ||||
| import { prettyDuration, prettyShortenNumber } from "helpers/formatters"; | ||||
| import { ImageQuality } from "helpers/img"; | ||||
| import { useDeviceSupportsHover } from "hooks/useMediaQuery"; | ||||
| import { useSmartLanguage } from "hooks/useSmartLanguage"; | ||||
| import { TranslatedProps } from "types/TranslatedProps"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { useAtomGetter } from "helpers/atoms"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| 
 | ||||
| /* | ||||
|  *                                        ╭─────────────╮ | ||||
| @ -77,11 +77,10 @@ export const PreviewCard = ({ | ||||
|   disabled = false, | ||||
|   onClick, | ||||
| }: Props): JSX.Element => { | ||||
|   const { formatPrice, formatDate } = useFormat(); | ||||
|   const isPerfModeEnabled = useAtomGetter(atoms.settings.isPerfModeEnabled); | ||||
|   const currency = useAtomGetter(atoms.settings.currency); | ||||
|   const currencies = useAtomGetter(atoms.localData.currencies); | ||||
|   const preferredCurrency = useAtomGetter(atoms.settings.currency); | ||||
|   const isHoverable = useDeviceSupportsHover(); | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   const metadataJSX = ( | ||||
|     <> | ||||
| @ -90,13 +89,13 @@ export const PreviewCard = ({ | ||||
|           {metadata.releaseDate && ( | ||||
|             <p className="text-sm"> | ||||
|               <Ico icon="event" className="mr-1 translate-y-[.15em] !text-base" /> | ||||
|               {prettyDate(metadata.releaseDate, router.locale)} | ||||
|               {formatDate(metadata.releaseDate)} | ||||
|             </p> | ||||
|           )} | ||||
|           {metadata.price && ( | ||||
|             <p className="justify-self-end text-sm"> | ||||
|               <Ico icon="shopping_cart" className="mr-1 translate-y-[.15em] !text-base" /> | ||||
|               {prettyPrice(metadata.price, currencies, currency, router.locale)} | ||||
|               {formatPrice(metadata.price, preferredCurrency)} | ||||
|             </p> | ||||
|           )} | ||||
|           {metadata.views && ( | ||||
|  | ||||
| @ -4,6 +4,8 @@ import { readFileSync, writeFileSync } from "fs"; | ||||
| import { config } from "dotenv"; | ||||
| import { getReadySdk } from "./sdk"; | ||||
| import { | ||||
|   LocalDataGetCurrenciesQuery, | ||||
|   LocalDataGetLanguagesQuery, | ||||
|   LocalDataGetTypesTranslationsQuery, | ||||
|   LocalDataGetWebsiteInterfacesQuery, | ||||
| } from "./generated"; | ||||
| @ -12,6 +14,10 @@ import { | ||||
|   Langui, | ||||
|   TypesTranslations, | ||||
|   processTypesTranslations, | ||||
|   Currencies, | ||||
|   processCurrencies, | ||||
|   Languages, | ||||
|   processLanguages, | ||||
| } from "helpers/localData"; | ||||
| import { getLogger } from "helpers/logger"; | ||||
| 
 | ||||
| @ -86,3 +92,13 @@ export const getTypesTranslations = (): TypesTranslations => { | ||||
|   const typesTranslations = readLocalData<LocalDataGetTypesTranslationsQuery>("typesTranslations"); | ||||
|   return processTypesTranslations(typesTranslations); | ||||
| }; | ||||
| 
 | ||||
| export const getCurrencies = (): Currencies => { | ||||
|   const currencies = readLocalData<LocalDataGetCurrenciesQuery>("currencies"); | ||||
|   return processCurrencies(currencies); | ||||
| }; | ||||
| 
 | ||||
| export const getLanguages = (): Languages => { | ||||
|   const languages = readLocalData<LocalDataGetLanguagesQuery>("languages"); | ||||
|   return processLanguages(languages); | ||||
| }; | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { GetStaticProps } from "next"; | ||||
| import { getReadySdk } from "./sdk"; | ||||
| import { PostWithTranslations } from "types/types"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| import { prettyDate, prettySlug } from "helpers/formatters"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales"; | ||||
| import { filterHasAttributes } from "helpers/asserts"; | ||||
| import { getDescription } from "helpers/description"; | ||||
| @ -17,7 +17,7 @@ export const getPostStaticProps = | ||||
|   (slug: string): GetStaticProps => | ||||
|   async (context) => { | ||||
|     const sdk = getReadySdk(); | ||||
|     const { format, formatCategory } = getFormat(context.locale); | ||||
|     const { format, formatCategory, formatDate } = getFormat(context.locale); | ||||
|     const post = await sdk.getPost({ | ||||
|       slug: slug, | ||||
|     }); | ||||
| @ -35,7 +35,7 @@ export const getPostStaticProps = | ||||
|     const title = selectedTranslation?.title ?? prettySlug(slug); | ||||
| 
 | ||||
|     const description = getDescription(selectedTranslation?.excerpt ?? selectedTranslation?.body, { | ||||
|       [format("release_date")]: [prettyDate(post.posts.data[0].attributes.date, context.locale)], | ||||
|       [format("release_date")]: [formatDate(post.posts.data[0].attributes.date)], | ||||
|       [format("category", { count: Infinity })]: filterHasAttributes( | ||||
|         post.posts.data[0].attributes.categories?.data, | ||||
|         ["attributes"] | ||||
|  | ||||
| @ -1,43 +1,7 @@ | ||||
| import { convert } from "html-to-text"; | ||||
| import { sanitize } from "isomorphic-dompurify"; | ||||
| import { marked } from "marked"; | ||||
| import { convertPrice } from "./numbers"; | ||||
| import { isDefinedAndNotEmpty, isUndefined } from "./asserts"; | ||||
| import { datePickerToDate } from "./date"; | ||||
| import { Currencies, Languages } from "./localData"; | ||||
| import { DatePickerFragment, PricePickerFragment } from "graphql/generated"; | ||||
| 
 | ||||
| export const prettyDate = ( | ||||
|   datePicker: DatePickerFragment, | ||||
|   locale = "en", | ||||
|   dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium" | ||||
| ): string => datePickerToDate(datePicker).toLocaleString(locale, { dateStyle }); | ||||
| 
 | ||||
| export const prettyPrice = ( | ||||
|   pricePicker: PricePickerFragment, | ||||
|   currencies: Currencies, | ||||
|   targetCurrencyCode?: string, | ||||
|   locale = "en" | ||||
| ): string => { | ||||
|   if (!targetCurrencyCode) return ""; | ||||
|   if (isUndefined(pricePicker.amount)) return ""; | ||||
| 
 | ||||
|   const targetCurrency = currencies.find( | ||||
|     (currency) => currency.attributes?.code === targetCurrencyCode | ||||
|   ); | ||||
| 
 | ||||
|   if (targetCurrency?.attributes) { | ||||
|     const amountInTargetCurrency = convertPrice(pricePicker, targetCurrency); | ||||
|     return amountInTargetCurrency.toLocaleString(locale, { | ||||
|       style: "currency", | ||||
|       currency: targetCurrency.attributes.code, | ||||
|     }); | ||||
|   } | ||||
|   return pricePicker.amount.toLocaleString(locale, { | ||||
|     style: "currency", | ||||
|     currency: pricePicker.currency?.data?.attributes?.code, | ||||
|   }); | ||||
| }; | ||||
| import { isDefinedAndNotEmpty } from "./asserts"; | ||||
| 
 | ||||
| export const prettySlug = (slug?: string, parentSlug?: string): string => { | ||||
|   let newSlug = slug; | ||||
| @ -94,14 +58,6 @@ export const prettyDuration = (seconds: number): string => { | ||||
|   return result; | ||||
| }; | ||||
| 
 | ||||
| export const prettyLanguage = (code: string, languages: Languages): string => { | ||||
|   let result = code; | ||||
|   languages.forEach((language) => { | ||||
|     if (language.attributes?.code === code) result = language.attributes.localized_name; | ||||
|   }); | ||||
|   return result; | ||||
| }; | ||||
| 
 | ||||
| export const prettyURL = (url: string): string => { | ||||
|   const domain = new URL(url); | ||||
|   return domain.hostname.replace("www.", ""); | ||||
|  | ||||
| @ -1,10 +1,21 @@ | ||||
| import { IntlMessageFormat } from "intl-messageformat"; | ||||
| import { LibraryItemMetadataDynamicZone } from "graphql/generated"; | ||||
| import { | ||||
|   DatePickerFragment, | ||||
|   LibraryItemMetadataDynamicZone, | ||||
|   PricePickerFragment, | ||||
| } from "graphql/generated"; | ||||
| import { ICUParams } from "graphql/icuParams"; | ||||
| import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts"; | ||||
| import { getLangui, getTypesTranslations } from "graphql/fetchLocalData"; | ||||
| import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts"; | ||||
| import { | ||||
|   getCurrencies, | ||||
|   getLanguages, | ||||
|   getLangui, | ||||
|   getTypesTranslations, | ||||
| } from "graphql/fetchLocalData"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { LibraryItemMetadata } from "types/types"; | ||||
| import { datePickerToDate } from "helpers/date"; | ||||
| import { convertPrice } from "helpers/numbers"; | ||||
| 
 | ||||
| type WordingKey = keyof ICUParams; | ||||
| type LibraryItemType = Exclude<LibraryItemMetadataDynamicZone["__typename"], undefined>; | ||||
| @ -45,10 +56,18 @@ export const getFormat = ( | ||||
|   formatContentType: (slug: string) => string; | ||||
|   formatWikiTag: (slug: string) => string; | ||||
|   formatWeaponType: (slug: string) => string; | ||||
|   formatLanguage: (code: string) => string; | ||||
|   formatPrice: (price: PricePickerFragment, targetCurrencyCode?: string) => string; | ||||
|   formatDate: ( | ||||
|     datePicker: DatePickerFragment, | ||||
|     dateStyle?: Intl.DateTimeFormatOptions["dateStyle"] | ||||
|   ) => string; | ||||
| } => { | ||||
|   const langui = getLangui(locale); | ||||
|   const fallbackLangui = getLangui("en"); | ||||
|   const typesTranslations = getTypesTranslations(); | ||||
|   const currencies = getCurrencies(); | ||||
|   const languages = getLanguages(); | ||||
| 
 | ||||
|   const format = ( | ||||
|     key: WordingKey, | ||||
| @ -214,6 +233,35 @@ export const getFormat = ( | ||||
|     return findTranslation(locale) ?? findTranslation("en") ?? prettySlug(slug); | ||||
|   }; | ||||
| 
 | ||||
|   const formatLanguage = (code: string) => | ||||
|     languages.find((language) => language.attributes?.code === code)?.attributes?.localized_name ?? | ||||
|     code.toUpperCase(); | ||||
| 
 | ||||
|   const formatPrice = (price: PricePickerFragment, targetCurrencyCode?: string) => { | ||||
|     if (isUndefined(price.amount)) return ""; | ||||
| 
 | ||||
|     const targetCurrency = currencies.find( | ||||
|       (currency) => currency.attributes?.code === targetCurrencyCode | ||||
|     ); | ||||
| 
 | ||||
|     if (targetCurrency?.attributes) { | ||||
|       const amountInTargetCurrency = convertPrice(price, targetCurrency); | ||||
|       return amountInTargetCurrency.toLocaleString(locale, { | ||||
|         style: "currency", | ||||
|         currency: targetCurrency.attributes.code, | ||||
|       }); | ||||
|     } | ||||
|     return price.amount.toLocaleString(locale, { | ||||
|       style: "currency", | ||||
|       currency: price.currency?.data?.attributes?.code, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const formatDate = ( | ||||
|     datePicker: DatePickerFragment, | ||||
|     dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium" | ||||
|   ): string => datePickerToDate(datePicker).toLocaleString(locale, { dateStyle }); | ||||
| 
 | ||||
|   return { | ||||
|     format, | ||||
|     formatLibraryItemType, | ||||
| @ -224,5 +272,8 @@ export const getFormat = ( | ||||
|     formatContentType, | ||||
|     formatWikiTag, | ||||
|     formatWeaponType, | ||||
|     formatLanguage, | ||||
|     formatPrice, | ||||
|     formatDate, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -3,12 +3,18 @@ import { useCallback } from "react"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { useAtomGetter } from "helpers/atoms"; | ||||
| import { LibraryItemMetadataDynamicZone } from "graphql/generated"; | ||||
| import { | ||||
|   DatePickerFragment, | ||||
|   LibraryItemMetadataDynamicZone, | ||||
|   PricePickerFragment, | ||||
| } from "graphql/generated"; | ||||
| import { ICUParams } from "graphql/icuParams"; | ||||
| import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts"; | ||||
| import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts"; | ||||
| import { getLogger } from "helpers/logger"; | ||||
| import { prettySlug } from "helpers/formatters"; | ||||
| import { LibraryItemMetadata } from "types/types"; | ||||
| import { convertPrice } from "helpers/numbers"; | ||||
| import { datePickerToDate } from "helpers/date"; | ||||
| 
 | ||||
| const logger = getLogger("🗺️ [I18n]"); | ||||
| 
 | ||||
| @ -69,10 +75,18 @@ export const useFormat = (): { | ||||
|   formatContentType: (slug: string) => string; | ||||
|   formatWikiTag: (slug: string) => string; | ||||
|   formatWeaponType: (slug: string) => string; | ||||
|   formatLanguage: (code: string) => string; | ||||
|   formatPrice: (price: PricePickerFragment, targetCurrencyCode?: string) => string; | ||||
|   formatDate: ( | ||||
|     datePicker: DatePickerFragment, | ||||
|     dateStyle?: Intl.DateTimeFormatOptions["dateStyle"] | ||||
|   ) => string; | ||||
| } => { | ||||
|   const langui = useAtomGetter(atoms.localData.langui); | ||||
|   const fallbackLangui = useAtomGetter(atoms.localData.fallbackLangui); | ||||
|   const typesTranslations = useAtomGetter(atoms.localData.typesTranslations); | ||||
|   const languages = useAtomGetter(atoms.localData.languages); | ||||
|   const currencies = useAtomGetter(atoms.localData.currencies); | ||||
|   const { locale = "en" } = useRouter(); | ||||
| 
 | ||||
|   const format = useCallback( | ||||
| @ -280,6 +294,44 @@ Falling back to en translation.` | ||||
|     [locale, typesTranslations.weaponTypes] | ||||
|   ); | ||||
| 
 | ||||
|   const formatLanguage = useCallback( | ||||
|     (code: string) => | ||||
|       languages.find((language) => language.attributes?.code === code)?.attributes | ||||
|         ?.localized_name ?? code.toUpperCase(), | ||||
|     [languages] | ||||
|   ); | ||||
| 
 | ||||
|   const formatPrice = useCallback( | ||||
|     (price: PricePickerFragment, targetCurrencyCode?: string) => { | ||||
|       if (isUndefined(price.amount)) return ""; | ||||
| 
 | ||||
|       const targetCurrency = currencies.find( | ||||
|         (currency) => currency.attributes?.code === targetCurrencyCode | ||||
|       ); | ||||
| 
 | ||||
|       if (targetCurrency?.attributes) { | ||||
|         const amountInTargetCurrency = convertPrice(price, targetCurrency); | ||||
|         return amountInTargetCurrency.toLocaleString(locale, { | ||||
|           style: "currency", | ||||
|           currency: targetCurrency.attributes.code, | ||||
|         }); | ||||
|       } | ||||
|       return price.amount.toLocaleString(locale, { | ||||
|         style: "currency", | ||||
|         currency: price.currency?.data?.attributes?.code, | ||||
|       }); | ||||
|     }, | ||||
|     [currencies, locale] | ||||
|   ); | ||||
| 
 | ||||
|   const formatDate = useCallback( | ||||
|     ( | ||||
|       datePicker: DatePickerFragment, | ||||
|       dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium" | ||||
|     ) => datePickerToDate(datePicker).toLocaleString(locale, { dateStyle }), | ||||
|     [locale] | ||||
|   ); | ||||
| 
 | ||||
|   return { | ||||
|     format, | ||||
|     formatLibraryItemType, | ||||
| @ -290,5 +342,8 @@ Falling back to en translation.` | ||||
|     formatContentType, | ||||
|     formatWikiTag, | ||||
|     formatWeaponType, | ||||
|     formatLanguage, | ||||
|     formatPrice, | ||||
|     formatDate, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { useCallback } from "react"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { HorizontalLine } from "components/HorizontalLine"; | ||||
| @ -12,7 +11,7 @@ import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/Cont | ||||
| import { SubPanel } from "components/Containers/SubPanel"; | ||||
| import { GetVideoQuery } from "graphql/generated"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { prettyDate, prettyShortenNumber } from "helpers/formatters"; | ||||
| import { prettyShortenNumber } from "helpers/formatters"; | ||||
| import { filterHasAttributes, isDefined } from "helpers/asserts"; | ||||
| import { getVideoFile, getVideoThumbnailURL } from "helpers/videos"; | ||||
| import { getOpenGraph } from "helpers/openGraph"; | ||||
| @ -37,8 +36,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => { | ||||
|   const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
|   const closeSubPanel = useCallback(() => setSubPanelOpened(false), [setSubPanelOpened]); | ||||
|   const { format } = useFormat(); | ||||
|   const router = useRouter(); | ||||
|   const { format, formatDate } = useFormat(); | ||||
| 
 | ||||
|   const subPanel = ( | ||||
|     <SubPanel> | ||||
| @ -85,7 +83,7 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => { | ||||
|             <div className="flex w-full flex-row flex-wrap place-items-center gap-x-6"> | ||||
|               <p> | ||||
|                 <Ico icon="event" className="mr-1 translate-y-[.15em] !text-base" /> | ||||
|                 {prettyDate(video.published_date, router.locale)} | ||||
|                 {formatDate(video.published_date)} | ||||
|               </p> | ||||
|               <p> | ||||
|                 <Ico icon="visibility" className="mr-1 translate-y-[.15em] !text-base" /> | ||||
|  | ||||
| @ -42,12 +42,14 @@ const DEFAULT_FILTERS_STATE = { | ||||
|   keepInfoVisible: true, | ||||
|   query: "", | ||||
|   page: 1, | ||||
|   lang: 0, | ||||
| }; | ||||
| 
 | ||||
| const queryParamSchema = z.object({ | ||||
|   query: z.coerce.string().optional(), | ||||
|   page: z.coerce.number().positive().optional(), | ||||
|   sort: z.coerce.number().min(0).max(5).optional(), | ||||
|   lang: z.coerce.number().min(0).optional(), | ||||
| }); | ||||
| 
 | ||||
| /* | ||||
| @ -59,7 +61,7 @@ interface Props extends AppLayoutRequired {} | ||||
| 
 | ||||
| const Contents = (props: Props): JSX.Element => { | ||||
|   const hoverable = useDeviceSupportsHover(); | ||||
|   const { format, formatCategory, formatContentType } = useFormat(); | ||||
|   const { format, formatCategory, formatContentType, formatLanguage } = useFormat(); | ||||
|   const router = useTypedRouter(queryParamSchema); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
| 
 | ||||
| @ -72,6 +74,17 @@ const Contents = (props: Props): JSX.Element => { | ||||
|     [format] | ||||
|   ); | ||||
| 
 | ||||
|   const languageOptions = useMemo(() => { | ||||
|     const memo = | ||||
|       router.locales?.map((language) => ({ | ||||
|         meiliAttribute: language, | ||||
|         displayedName: formatLanguage(language), | ||||
|       })) ?? []; | ||||
| 
 | ||||
|     memo.unshift({ meiliAttribute: "", displayedName: format("all") }); | ||||
|     return memo; | ||||
|   }, [router.locales, formatLanguage, format]); | ||||
| 
 | ||||
|   const [sortingMethod, setSortingMethod] = useState<number>( | ||||
|     router.query.sort ?? DEFAULT_FILTERS_STATE.sortingMethod | ||||
|   ); | ||||
| @ -82,25 +95,41 @@ const Contents = (props: Props): JSX.Element => { | ||||
|     setValue: setKeepInfoVisible, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
| 
 | ||||
|   const [page, setPage] = useState<number>(router.query.page ?? DEFAULT_FILTERS_STATE.page); | ||||
|   const [contents, setContents] = useState<CustomSearchResponse<MeiliContent>>(); | ||||
|   const [page, setPage] = useState<number>(router.query.page ?? DEFAULT_FILTERS_STATE.page); | ||||
|   const [query, setQuery] = useState(router.query.query ?? DEFAULT_FILTERS_STATE.query); | ||||
|   const [languageOption, setLanguageOption] = useState( | ||||
|     router.query.lang ?? DEFAULT_FILTERS_STATE.lang | ||||
|   ); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const fetchPosts = async () => { | ||||
|       const currentSortingMethod = sortingMethods[sortingMethod]; | ||||
|       const currentSortingMethod = sortingMethods[sortingMethod]?.meiliAttribute; | ||||
|       const currentLanguageOption = languageOptions[languageOption]?.meiliAttribute; | ||||
| 
 | ||||
|       const filter: string[] = []; | ||||
|       if (languageOption !== 0) { | ||||
|         filter.push(`filterable_languages = ${currentLanguageOption}`); | ||||
|       } | ||||
| 
 | ||||
|       const searchResult = await meiliSearch(MeiliIndices.CONTENT, query, { | ||||
|         attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"], | ||||
|         attributesToHighlight: ["translations"], | ||||
|         attributesToCrop: ["translations.displayable_description"], | ||||
|         filter, | ||||
|         hitsPerPage: 25, | ||||
|         page, | ||||
|         sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined, | ||||
|         sort: isDefined(currentSortingMethod) ? [currentSortingMethod] : undefined, | ||||
|       }); | ||||
|       setContents(filterHitsWithHighlight<MeiliContent>(searchResult, "translations")); | ||||
| 
 | ||||
|       setContents( | ||||
|         languageOption === 0 | ||||
|           ? filterHitsWithHighlight<MeiliContent>(searchResult, "translations") | ||||
|           : searchResult | ||||
|       ); | ||||
|     }; | ||||
|     fetchPosts(); | ||||
|   }, [query, page, sortingMethod, sortingMethods]); | ||||
|   }, [query, page, sortingMethod, sortingMethods, languageOption, languageOptions]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (router.isReady) | ||||
| @ -108,15 +137,17 @@ const Contents = (props: Props): JSX.Element => { | ||||
|         page, | ||||
|         query, | ||||
|         sort: sortingMethod, | ||||
|         lang: languageOption, | ||||
|       }); | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [page, query, sortingMethod, router.isReady]); | ||||
|   }, [page, query, languageOption, sortingMethod, router.isReady]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (router.isReady) { | ||||
|       if (isDefined(router.query.page)) setPage(router.query.page); | ||||
|       if (isDefined(router.query.query)) setQuery(router.query.query); | ||||
|       if (isDefined(router.query.sort)) setSortingMethod(router.query.sort); | ||||
|       if (isDefined(router.query.lang)) setLanguageOption(router.query.lang); | ||||
|     } | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [router.isReady]); | ||||
| @ -173,6 +204,24 @@ const Contents = (props: Props): JSX.Element => { | ||||
|         /> | ||||
|       </WithLabel> | ||||
| 
 | ||||
|       <WithLabel label={format("language", { count: Infinity })}> | ||||
|         <Select | ||||
|           className="w-full" | ||||
|           options={languageOptions.map((item) => item.displayedName)} | ||||
|           value={languageOption} | ||||
|           onChange={(newLanguageOption) => { | ||||
|             setPage(1); | ||||
|             setLanguageOption(newLanguageOption); | ||||
|             sendAnalytics( | ||||
|               "Contents/All", | ||||
|               `Change language filter (${ | ||||
|                 languageOptions.map((item) => item.meiliAttribute)[newLanguageOption] | ||||
|               })` | ||||
|             ); | ||||
|           }} | ||||
|         /> | ||||
|       </WithLabel> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel label={format("always_show_info")}> | ||||
|           <Switch | ||||
| @ -190,10 +239,11 @@ const Contents = (props: Props): JSX.Element => { | ||||
|         text={format("reset_all_filters")} | ||||
|         icon="settings_backup_restore" | ||||
|         onClick={() => { | ||||
|           setPage(1); | ||||
|           setPage(DEFAULT_FILTERS_STATE.page); | ||||
|           setQuery(DEFAULT_FILTERS_STATE.query); | ||||
|           setSortingMethod(DEFAULT_FILTERS_STATE.sortingMethod); | ||||
|           setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
|           setLanguageOption(DEFAULT_FILTERS_STATE.lang); | ||||
|           sendAnalytics("Contents/All", "Reset all filters"); | ||||
|         }} | ||||
|       /> | ||||
| @ -212,13 +262,19 @@ const Contents = (props: Props): JSX.Element => { | ||||
|               href={`/contents/${item.slug}`} | ||||
|               translations={filterHasAttributes(item._formatted.translations, [ | ||||
|                 "language.data.attributes.code", | ||||
|               ]).map(({ displayable_description, language, ...otherAttributes }) => ({ | ||||
|                 ...otherAttributes, | ||||
|                 description: containsHighlight(displayable_description) | ||||
|                   ? displayable_description | ||||
|                   : undefined, | ||||
|                 language: language.data.attributes.code, | ||||
|               }))} | ||||
|               ]) | ||||
|                 .map(({ displayable_description, language, ...otherAttributes }) => ({ | ||||
|                   ...otherAttributes, | ||||
|                   description: containsHighlight(displayable_description) | ||||
|                     ? displayable_description | ||||
|                     : undefined, | ||||
|                   language: language.data.attributes.code, | ||||
|                 })) | ||||
|                 .filter( | ||||
|                   ({ language }) => | ||||
|                     languageOption === 0 || | ||||
|                     language === languageOptions[languageOption]?.meiliAttribute | ||||
|                 )} | ||||
|               fallback={{ title: prettySlug(item.slug) }} | ||||
|               thumbnail={item.thumbnail?.data?.attributes} | ||||
|               thumbnailAspectRatio="3/2" | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import { Fragment, useCallback, useMemo } from "react"; | ||||
| import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { Chip } from "components/Chip"; | ||||
| @ -20,13 +19,7 @@ import { | ||||
|   GetLibraryItemQuery, | ||||
| } from "graphql/generated"; | ||||
| import { getReadySdk } from "graphql/sdk"; | ||||
| import { | ||||
|   prettyDate, | ||||
|   prettyInlineTitle, | ||||
|   prettyPrice, | ||||
|   prettySlug, | ||||
|   prettyURL, | ||||
| } from "helpers/formatters"; | ||||
| import { prettyInlineTitle, prettySlug, prettyURL } from "helpers/formatters"; | ||||
| import { ImageQuality } from "helpers/img"; | ||||
| import { convertMmToInch } from "helpers/numbers"; | ||||
| import { sortRangedContent } from "helpers/others"; | ||||
| @ -96,15 +89,15 @@ const LibrarySlug = ({ | ||||
|     formatCategory, | ||||
|     formatContentType, | ||||
|     formatLibraryItemSubType, | ||||
|     formatPrice, | ||||
|     formatDate, | ||||
|   } = useFormat(); | ||||
|   const currencies = useAtomGetter(atoms.localData.currencies); | ||||
| 
 | ||||
|   const isContentPanelAtLeast3xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast3xl); | ||||
|   const isContentPanelAtLeastSm = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastSm); | ||||
|   const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout); | ||||
| 
 | ||||
|   const hoverable = useDeviceSupportsHover(); | ||||
|   const router = useRouter(); | ||||
|   const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = useBoolean(false); | ||||
| 
 | ||||
|   const { showLightBox } = useAtomGetter(atoms.lightBox); | ||||
| @ -320,24 +313,17 @@ const LibrarySlug = ({ | ||||
|               {item.release_date && ( | ||||
|                 <div className="grid place-content-start place-items-center"> | ||||
|                   <h3 className="text-xl">{format("release_date")}</h3> | ||||
|                   <p>{prettyDate(item.release_date, router.locale)}</p> | ||||
|                   <p>{formatDate(item.release_date)}</p> | ||||
|                 </div> | ||||
|               )} | ||||
| 
 | ||||
|               {item.price && ( | ||||
|                 <div className="grid place-content-start place-items-center text-center"> | ||||
|                   <h3 className="text-xl">{format("price")}</h3> | ||||
|                   <p> | ||||
|                     {prettyPrice( | ||||
|                       item.price, | ||||
|                       currencies, | ||||
|                       item.price.currency?.data?.attributes?.code, | ||||
|                       router.locale | ||||
|                     )} | ||||
|                   </p> | ||||
|                   <p>{formatPrice(item.price)}</p> | ||||
|                   {item.price.currency?.data?.attributes?.code !== currency && ( | ||||
|                     <p> | ||||
|                       {prettyPrice(item.price, currencies, currency, router.locale)} | ||||
|                       {formatPrice(item.price, currency)} | ||||
|                       <br />({format("calculated").toLowerCase()}) | ||||
|                     </p> | ||||
|                   )} | ||||
| @ -627,7 +613,9 @@ export default LibrarySlug; | ||||
| 
 | ||||
| export const getStaticProps: GetStaticProps = async (context) => { | ||||
|   const sdk = getReadySdk(); | ||||
|   const { format, formatCategory, formatLibraryItemSubType } = getFormat(context.locale); | ||||
|   const { format, formatCategory, formatLibraryItemSubType, formatDate } = getFormat( | ||||
|     context.locale | ||||
|   ); | ||||
|   const item = await sdk.getLibraryItem({ | ||||
|     slug: context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "", | ||||
|   }); | ||||
| @ -648,7 +636,7 @@ export const getStaticProps: GetStaticProps = async (context) => { | ||||
|         : [], | ||||
|       [format("release_date")]: [ | ||||
|         item.libraryItems.data[0].attributes.release_date | ||||
|           ? prettyDate(item.libraryItems.data[0].attributes.release_date, context.locale) | ||||
|           ? formatDate(item.libraryItems.data[0].attributes.release_date) | ||||
|           : undefined, | ||||
|       ], | ||||
|     } | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { GetStaticProps } from "next"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useEffect, useMemo, useState } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { z } from "zod"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| @ -31,6 +31,7 @@ import { prettySlug } from "helpers/formatters"; | ||||
| import { Paginator } from "components/Containers/Paginator"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| import { getFormat } from "helpers/i18n"; | ||||
| import { Select } from "components/Inputs/Select"; | ||||
| 
 | ||||
| /* | ||||
|  *                                         ╭─────────────╮ | ||||
| @ -41,11 +42,13 @@ const DEFAULT_FILTERS_STATE = { | ||||
|   query: "", | ||||
|   keepInfoVisible: true, | ||||
|   page: 1, | ||||
|   lang: 0, | ||||
| }; | ||||
| 
 | ||||
| const queryParamSchema = z.object({ | ||||
|   query: z.coerce.string().optional(), | ||||
|   page: z.coerce.number().positive().optional(), | ||||
|   lang: z.coerce.number().min(0).optional(), | ||||
| }); | ||||
| 
 | ||||
| /* | ||||
| @ -56,9 +59,10 @@ const queryParamSchema = z.object({ | ||||
| interface Props extends AppLayoutRequired {} | ||||
| 
 | ||||
| const News = ({ ...otherProps }: Props): JSX.Element => { | ||||
|   const { format, formatCategory } = useFormat(); | ||||
|   const { format, formatCategory, formatLanguage } = useFormat(); | ||||
|   const hoverable = useDeviceSupportsHover(); | ||||
|   const router = useTypedRouter(queryParamSchema); | ||||
|   const isTerminalMode = useAtomGetter(atoms.layout.terminalMode); | ||||
| 
 | ||||
|   const [query, setQuery] = useState(router.query.query ?? DEFAULT_FILTERS_STATE.query); | ||||
| 
 | ||||
| @ -68,13 +72,32 @@ const News = ({ ...otherProps }: Props): JSX.Element => { | ||||
|     setValue: setKeepInfoVisible, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
| 
 | ||||
|   const isTerminalMode = useAtomGetter(atoms.layout.terminalMode); | ||||
|   const languageOptions = useMemo(() => { | ||||
|     const memo = | ||||
|       router.locales?.map((language) => ({ | ||||
|         meiliAttribute: language, | ||||
|         displayedName: formatLanguage(language), | ||||
|       })) ?? []; | ||||
| 
 | ||||
|     memo.unshift({ meiliAttribute: "", displayedName: format("all") }); | ||||
|     return memo; | ||||
|   }, [router.locales, formatLanguage, format]); | ||||
| 
 | ||||
|   const [page, setPage] = useState<number>(router.query.page ?? DEFAULT_FILTERS_STATE.page); | ||||
|   const [posts, setPosts] = useState<CustomSearchResponse<MeiliPost>>(); | ||||
|   const [languageOption, setLanguageOption] = useState( | ||||
|     router.query.lang ?? DEFAULT_FILTERS_STATE.lang | ||||
|   ); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const fetchPosts = async () => { | ||||
|       const currentLanguageOption = languageOptions[languageOption]?.meiliAttribute; | ||||
|       const filter = ["hidden = false"]; | ||||
| 
 | ||||
|       if (languageOption !== 0) { | ||||
|         filter.push(`filterable_languages = ${currentLanguageOption}`); | ||||
|       } | ||||
| 
 | ||||
|       const searchResult = await meiliSearch(MeiliIndices.POST, query, { | ||||
|         hitsPerPage: 25, | ||||
|         page, | ||||
| @ -82,26 +105,33 @@ const News = ({ ...otherProps }: Props): JSX.Element => { | ||||
|         attributesToHighlight: ["translations.title", "translations.displayable_description"], | ||||
|         attributesToCrop: ["translations.displayable_description"], | ||||
|         sort: ["sortable_date:desc"], | ||||
|         filter: ["hidden = false"], | ||||
|         filter, | ||||
|       }); | ||||
|       setPosts(filterHitsWithHighlight<MeiliPost>(searchResult, "translations")); | ||||
| 
 | ||||
|       setPosts( | ||||
|         languageOption === 0 | ||||
|           ? filterHitsWithHighlight<MeiliPost>(searchResult, "translations") | ||||
|           : searchResult | ||||
|       ); | ||||
|     }; | ||||
|     fetchPosts(); | ||||
|   }, [query, page]); | ||||
|   }, [query, page, languageOption, languageOptions]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (router.isReady) | ||||
|       router.updateQuery({ | ||||
|         page, | ||||
|         query, | ||||
|         lang: languageOption, | ||||
|       }); | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [page, query, router.isReady]); | ||||
|   }, [page, query, languageOption, router.isReady]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (router.isReady) { | ||||
|       if (isDefined(router.query.page)) setPage(router.query.page); | ||||
|       if (isDefined(router.query.query)) setQuery(router.query.query); | ||||
|       if (isDefined(router.query.lang)) setLanguageOption(router.query.lang); | ||||
|     } | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [router.isReady]); | ||||
| @ -131,6 +161,24 @@ const News = ({ ...otherProps }: Props): JSX.Element => { | ||||
|         }} | ||||
|       /> | ||||
| 
 | ||||
|       <WithLabel label={format("language", { count: Infinity })}> | ||||
|         <Select | ||||
|           className="w-full" | ||||
|           options={languageOptions.map((item) => item.displayedName)} | ||||
|           value={languageOption} | ||||
|           onChange={(newLanguageOption) => { | ||||
|             setPage(1); | ||||
|             setLanguageOption(newLanguageOption); | ||||
|             sendAnalytics( | ||||
|               "News", | ||||
|               `Change language filter (${ | ||||
|                 languageOptions.map((item) => item.meiliAttribute)[newLanguageOption] | ||||
|               })` | ||||
|             ); | ||||
|           }} | ||||
|         /> | ||||
|       </WithLabel> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel label={format("always_show_info")}> | ||||
|           <Switch | ||||
| @ -148,8 +196,10 @@ const News = ({ ...otherProps }: Props): JSX.Element => { | ||||
|         text={format("reset_all_filters")} | ||||
|         icon="settings_backup_restore" | ||||
|         onClick={() => { | ||||
|           setPage(DEFAULT_FILTERS_STATE.page); | ||||
|           setQuery(DEFAULT_FILTERS_STATE.query); | ||||
|           setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
|           setLanguageOption(DEFAULT_FILTERS_STATE.lang); | ||||
|           sendAnalytics("News", "Reset all filters"); | ||||
|         }} | ||||
|       /> | ||||
| @ -168,13 +218,19 @@ const News = ({ ...otherProps }: Props): JSX.Element => { | ||||
|               href={`/news/${item.slug}`} | ||||
|               translations={filterHasAttributes(item._formatted.translations, [ | ||||
|                 "language.data.attributes.code", | ||||
|               ]).map(({ excerpt, displayable_description, language, ...otherAttributes }) => ({ | ||||
|                 ...otherAttributes, | ||||
|                 description: containsHighlight(displayable_description) | ||||
|                   ? displayable_description | ||||
|                   : excerpt, | ||||
|                 language: language.data.attributes.code, | ||||
|               }))} | ||||
|               ]) | ||||
|                 .map(({ excerpt, displayable_description, language, ...otherAttributes }) => ({ | ||||
|                   ...otherAttributes, | ||||
|                   description: containsHighlight(displayable_description) | ||||
|                     ? displayable_description | ||||
|                     : excerpt, | ||||
|                   language: language.data.attributes.code, | ||||
|                 })) | ||||
|                 .filter( | ||||
|                   ({ language }) => | ||||
|                     languageOption === 0 || | ||||
|                     language === languageOptions[languageOption]?.meiliAttribute | ||||
|                 )} | ||||
|               fallback={{ title: prettySlug(item.slug) }} | ||||
|               thumbnail={item.thumbnail?.data?.attributes} | ||||
|               thumbnailAspectRatio="3/2" | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { GetStaticProps } from "next"; | ||||
| import { useCallback, useEffect, useState } from "react"; | ||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { z } from "zod"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| @ -20,12 +20,18 @@ import { TranslatedPreviewCard } from "components/PreviewCard"; | ||||
| import { sendAnalytics } from "helpers/analytics"; | ||||
| import { useTypedRouter } from "hooks/useTypedRouter"; | ||||
| import { MeiliIndices, MeiliWikiPage } from "shared/meilisearch-graphql-typings/meiliTypes"; | ||||
| import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search"; | ||||
| import { | ||||
|   containsHighlight, | ||||
|   CustomSearchResponse, | ||||
|   filterHitsWithHighlight, | ||||
|   meiliSearch, | ||||
| } from "helpers/search"; | ||||
| import { Paginator } from "components/Containers/Paginator"; | ||||
| import { useFormat } from "hooks/useFormat"; | ||||
| import { getFormat } from "helpers/i18n"; | ||||
| import { useAtomSetter } from "helpers/atoms"; | ||||
| import { atoms } from "contexts/atoms"; | ||||
| import { Select } from "components/Inputs/Select"; | ||||
| 
 | ||||
| /* | ||||
|  *                                         ╭─────────────╮ | ||||
| @ -36,11 +42,13 @@ const DEFAULT_FILTERS_STATE = { | ||||
|   query: "", | ||||
|   keepInfoVisible: true, | ||||
|   page: 1, | ||||
|   lang: 0, | ||||
| }; | ||||
| 
 | ||||
| const queryParamSchema = z.object({ | ||||
|   query: z.coerce.string().optional(), | ||||
|   page: z.coerce.number().positive().optional(), | ||||
|   lang: z.coerce.number().min(0).optional(), | ||||
| }); | ||||
| 
 | ||||
| /* | ||||
| @ -51,24 +59,44 @@ const queryParamSchema = z.object({ | ||||
| interface Props extends AppLayoutRequired {} | ||||
| 
 | ||||
| const Wiki = (props: Props): JSX.Element => { | ||||
|   const { format, formatCategory, formatWikiTag } = useFormat(); | ||||
|   const { format, formatCategory, formatWikiTag, formatLanguage } = useFormat(); | ||||
|   const hoverable = useDeviceSupportsHover(); | ||||
|   const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); | ||||
|   const closeSubPanel = useCallback(() => setSubPanelOpened(false), [setSubPanelOpened]); | ||||
|   const router = useTypedRouter(queryParamSchema); | ||||
|   const [query, setQuery] = useState(router.query.query ?? DEFAULT_FILTERS_STATE.query); | ||||
| 
 | ||||
|   const languageOptions = useMemo(() => { | ||||
|     const memo = | ||||
|       router.locales?.map((language) => ({ | ||||
|         meiliAttribute: language, | ||||
|         displayedName: formatLanguage(language), | ||||
|       })) ?? []; | ||||
| 
 | ||||
|     memo.unshift({ meiliAttribute: "", displayedName: format("all") }); | ||||
|     return memo; | ||||
|   }, [router.locales, formatLanguage, format]); | ||||
| 
 | ||||
|   const [wikiPages, setWikiPages] = useState<CustomSearchResponse<MeiliWikiPage>>(); | ||||
|   const [query, setQuery] = useState(router.query.query ?? DEFAULT_FILTERS_STATE.query); | ||||
|   const [page, setPage] = useState<number>(router.query.page ?? DEFAULT_FILTERS_STATE.page); | ||||
|   const { | ||||
|     value: keepInfoVisible, | ||||
|     toggle: toggleKeepInfoVisible, | ||||
|     setValue: setKeepInfoVisible, | ||||
|   } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
| 
 | ||||
|   const [page, setPage] = useState<number>(router.query.page ?? DEFAULT_FILTERS_STATE.page); | ||||
|   const [wikiPages, setWikiPages] = useState<CustomSearchResponse<MeiliWikiPage>>(); | ||||
|   const [languageOption, setLanguageOption] = useState( | ||||
|     router.query.lang ?? DEFAULT_FILTERS_STATE.lang | ||||
|   ); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const fetchWikiPages = async () => { | ||||
|       const currentLanguageOption = languageOptions[languageOption]?.meiliAttribute; | ||||
| 
 | ||||
|       const filter: string[] = []; | ||||
|       if (languageOption !== 0) { | ||||
|         filter.push(`filterable_languages = ${currentLanguageOption}`); | ||||
|       } | ||||
| 
 | ||||
|       const searchResult = await meiliSearch(MeiliIndices.WIKI_PAGE, query, { | ||||
|         hitsPerPage: 25, | ||||
|         page, | ||||
| @ -79,25 +107,32 @@ const Wiki = (props: Props): JSX.Element => { | ||||
|           "translations.displayable_description", | ||||
|         ], | ||||
|         attributesToCrop: ["translations.displayable_description"], | ||||
|         filter, | ||||
|       }); | ||||
|       setWikiPages(searchResult); | ||||
|       setWikiPages( | ||||
|         languageOption === 0 | ||||
|           ? filterHitsWithHighlight<MeiliWikiPage>(searchResult, "translations") | ||||
|           : searchResult | ||||
|       ); | ||||
|     }; | ||||
|     fetchWikiPages(); | ||||
|   }, [query, page]); | ||||
|   }, [query, page, languageOptions, languageOption]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (router.isReady) | ||||
|       router.updateQuery({ | ||||
|         page, | ||||
|         query, | ||||
|         lang: languageOption, | ||||
|       }); | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [page, query, router.isReady]); | ||||
|   }, [page, query, router.isReady, languageOption]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (router.isReady) { | ||||
|       if (isDefined(router.query.page)) setPage(router.query.page); | ||||
|       if (isDefined(router.query.query)) setQuery(router.query.query); | ||||
|       if (isDefined(router.query.lang)) setLanguageOption(router.query.lang); | ||||
|     } | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [router.isReady]); | ||||
| @ -127,6 +162,24 @@ const Wiki = (props: Props): JSX.Element => { | ||||
|         }} | ||||
|       /> | ||||
| 
 | ||||
|       <WithLabel label={format("language", { count: Infinity })}> | ||||
|         <Select | ||||
|           className="w-full" | ||||
|           options={languageOptions.map((item) => item.displayedName)} | ||||
|           value={languageOption} | ||||
|           onChange={(newLanguageOption) => { | ||||
|             setPage(1); | ||||
|             setLanguageOption(newLanguageOption); | ||||
|             sendAnalytics( | ||||
|               "Wiki", | ||||
|               `Change language filter (${ | ||||
|                 languageOptions.map((item) => item.meiliAttribute)[newLanguageOption] | ||||
|               })` | ||||
|             ); | ||||
|           }} | ||||
|         /> | ||||
|       </WithLabel> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel label={format("always_show_info")}> | ||||
|           <Switch | ||||
| @ -144,9 +197,10 @@ const Wiki = (props: Props): JSX.Element => { | ||||
|         text={format("reset_all_filters")} | ||||
|         icon="settings_backup_restore" | ||||
|         onClick={() => { | ||||
|           setPage(1); | ||||
|           setPage(DEFAULT_FILTERS_STATE.page); | ||||
|           setQuery(DEFAULT_FILTERS_STATE.query); | ||||
|           setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
|           setLanguageOption(DEFAULT_FILTERS_STATE.lang); | ||||
|           sendAnalytics("Wiki", "Reset all filters"); | ||||
|         }} | ||||
|       /> | ||||
| @ -182,19 +236,31 @@ const Wiki = (props: Props): JSX.Element => { | ||||
|               href={`/wiki/${item.slug}`} | ||||
|               translations={filterHasAttributes(item._formatted.translations, [ | ||||
|                 "language.data.attributes.code", | ||||
|               ]).map( | ||||
|                 ({ aliases, summary, displayable_description, language, ...otherAttributes }) => ({ | ||||
|                   ...otherAttributes, | ||||
|                   subtitle: | ||||
|                     aliases && aliases.length > 0 | ||||
|                       ? aliases.map((alias) => alias?.alias).join("・") | ||||
|                       : undefined, | ||||
|                   description: containsHighlight(displayable_description) | ||||
|                     ? displayable_description | ||||
|                     : summary, | ||||
|                   language: language.data.attributes.code, | ||||
|                 }) | ||||
|               )} | ||||
|               ]) | ||||
|                 .map( | ||||
|                   ({ | ||||
|                     aliases, | ||||
|                     summary, | ||||
|                     displayable_description, | ||||
|                     language, | ||||
|                     ...otherAttributes | ||||
|                   }) => ({ | ||||
|                     ...otherAttributes, | ||||
|                     subtitle: | ||||
|                       aliases && aliases.length > 0 | ||||
|                         ? aliases.map((alias) => alias?.alias).join("・") | ||||
|                         : undefined, | ||||
|                     description: containsHighlight(displayable_description) | ||||
|                       ? displayable_description | ||||
|                       : summary, | ||||
|                     language: language.data.attributes.code, | ||||
|                   }) | ||||
|                 ) | ||||
|                 .filter( | ||||
|                   ({ language }) => | ||||
|                     languageOption === 0 || | ||||
|                     language === languageOptions[languageOption]?.meiliAttribute | ||||
|                 )} | ||||
|               fallback={{ title: prettySlug(item.slug) }} | ||||
|               thumbnail={item.thumbnail?.data?.attributes} | ||||
|               thumbnailAspectRatio={"4/3"} | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { GetStaticProps } from "next"; | ||||
| import { z } from "zod"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useEffect, useMemo, useState } from "react"; | ||||
| import { useBoolean } from "usehooks-ts"; | ||||
| import { AppLayout, AppLayoutRequired } from "components/AppLayout"; | ||||
| import { getFormat } from "helpers/i18n"; | ||||
| @ -29,6 +29,7 @@ import { useDeviceSupportsHover } from "hooks/useMediaQuery"; | ||||
| import { WithLabel } from "components/Inputs/WithLabel"; | ||||
| import { Switch } from "components/Inputs/Switch"; | ||||
| import { ReturnButton } from "components/PanelComponents/ReturnButton"; | ||||
| import { Select } from "components/Inputs/Select"; | ||||
| 
 | ||||
| /* | ||||
|  *                                         ╭─────────────╮ | ||||
| @ -39,11 +40,13 @@ const DEFAULT_FILTERS_STATE = { | ||||
|   query: "", | ||||
|   keepInfoVisible: true, | ||||
|   page: 1, | ||||
|   lang: 0, | ||||
| }; | ||||
| 
 | ||||
| const queryParamSchema = z.object({ | ||||
|   query: z.coerce.string().optional(), | ||||
|   page: z.coerce.number().positive().optional(), | ||||
|   lang: z.coerce.number().min(0).optional(), | ||||
| }); | ||||
| 
 | ||||
| /* | ||||
| @ -54,12 +57,26 @@ const queryParamSchema = z.object({ | ||||
| interface Props extends AppLayoutRequired {} | ||||
| 
 | ||||
| const Weapons = (props: Props): JSX.Element => { | ||||
|   const { format, formatCategory, formatWeaponType } = useFormat(); | ||||
|   const { format, formatCategory, formatWeaponType, formatLanguage } = useFormat(); | ||||
|   const hoverable = useDeviceSupportsHover(); | ||||
|   const router = useTypedRouter(queryParamSchema); | ||||
| 
 | ||||
|   const languageOptions = useMemo(() => { | ||||
|     const memo = | ||||
|       router.locales?.map((language) => ({ | ||||
|         meiliAttribute: language, | ||||
|         displayedName: formatLanguage(language), | ||||
|       })) ?? []; | ||||
| 
 | ||||
|     memo.unshift({ meiliAttribute: "", displayedName: format("all") }); | ||||
|     return memo; | ||||
|   }, [router.locales, formatLanguage, format]); | ||||
| 
 | ||||
|   const [query, setQuery] = useState(router.query.query ?? DEFAULT_FILTERS_STATE.query); | ||||
|   const [page, setPage] = useState<number>(router.query.page ?? DEFAULT_FILTERS_STATE.page); | ||||
|   const [languageOption, setLanguageOption] = useState( | ||||
|     router.query.lang ?? DEFAULT_FILTERS_STATE.lang | ||||
|   ); | ||||
| 
 | ||||
|   const { | ||||
|     value: keepInfoVisible, | ||||
| @ -71,6 +88,13 @@ const Weapons = (props: Props): JSX.Element => { | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const fetchPosts = async () => { | ||||
|       const currentLanguageOption = languageOptions[languageOption]?.meiliAttribute; | ||||
| 
 | ||||
|       const filter: string[] = []; | ||||
|       if (languageOption !== 0) { | ||||
|         filter.push(`filterable_languages = ${currentLanguageOption}`); | ||||
|       } | ||||
| 
 | ||||
|       const searchResult = await meiliSearch(MeiliIndices.WEAPON, query, { | ||||
|         hitsPerPage: 25, | ||||
|         page, | ||||
| @ -78,25 +102,32 @@ const Weapons = (props: Props): JSX.Element => { | ||||
|         attributesToHighlight: ["translations.description", "translations.names"], | ||||
|         attributesToCrop: ["translations.description"], | ||||
|         sort: ["slug:asc"], | ||||
|         filter, | ||||
|       }); | ||||
|       setWeapons(filterHitsWithHighlight<MeiliWeapon>(searchResult, "translations")); | ||||
|       setWeapons( | ||||
|         languageOption === 0 | ||||
|           ? filterHitsWithHighlight<MeiliWeapon>(searchResult, "translations") | ||||
|           : searchResult | ||||
|       ); | ||||
|     }; | ||||
|     fetchPosts(); | ||||
|   }, [query, page]); | ||||
|   }, [query, page, languageOptions, languageOption]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (router.isReady) | ||||
|       router.updateQuery({ | ||||
|         page, | ||||
|         query, | ||||
|         lang: languageOption, | ||||
|       }); | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [page, query, router.isReady]); | ||||
|   }, [page, query, languageOption, router.isReady]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (router.isReady) { | ||||
|       if (isDefined(router.query.page)) setPage(router.query.page); | ||||
|       if (isDefined(router.query.query)) setQuery(router.query.query); | ||||
|       if (isDefined(router.query.lang)) setLanguageOption(router.query.lang); | ||||
|     } | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [router.isReady]); | ||||
| @ -132,6 +163,24 @@ const Weapons = (props: Props): JSX.Element => { | ||||
|         }} | ||||
|       /> | ||||
| 
 | ||||
|       <WithLabel label={format("language", { count: Infinity })}> | ||||
|         <Select | ||||
|           className="w-full" | ||||
|           options={languageOptions.map((item) => item.displayedName)} | ||||
|           value={languageOption} | ||||
|           onChange={(newLanguageOption) => { | ||||
|             setPage(1); | ||||
|             setLanguageOption(newLanguageOption); | ||||
|             sendAnalytics( | ||||
|               "Weapons", | ||||
|               `Change language filter (${ | ||||
|                 languageOptions.map((item) => item.meiliAttribute)[newLanguageOption] | ||||
|               })` | ||||
|             ); | ||||
|           }} | ||||
|         /> | ||||
|       </WithLabel> | ||||
| 
 | ||||
|       {hoverable && ( | ||||
|         <WithLabel label={format("always_show_info")}> | ||||
|           <Switch | ||||
| @ -149,8 +198,10 @@ const Weapons = (props: Props): JSX.Element => { | ||||
|         text={format("reset_all_filters")} | ||||
|         icon="settings_backup_restore" | ||||
|         onClick={() => { | ||||
|           setPage(DEFAULT_FILTERS_STATE.page); | ||||
|           setQuery(DEFAULT_FILTERS_STATE.query); | ||||
|           setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible); | ||||
|           setLanguageOption(DEFAULT_FILTERS_STATE.lang); | ||||
|           sendAnalytics("Weapons", "Reset all filters"); | ||||
|         }} | ||||
|       /> | ||||
| @ -176,12 +227,18 @@ const Weapons = (props: Props): JSX.Element => { | ||||
|               href={`/wiki/weapons/${item.slug}`} | ||||
|               translations={filterHasAttributes(item._formatted.translations, [ | ||||
|                 "language.data.attributes.code", | ||||
|               ]).map(({ description, language, names: [primaryName, ...aliases] }) => ({ | ||||
|                 language: language.data.attributes.code, | ||||
|                 title: primaryName, | ||||
|                 subtitle: aliases.join("・"), | ||||
|                 description: containsHighlight(description) ? description : undefined, | ||||
|               }))} | ||||
|               ]) | ||||
|                 .map(({ description, language, names: [primaryName, ...aliases] }) => ({ | ||||
|                   language: language.data.attributes.code, | ||||
|                   title: primaryName, | ||||
|                   subtitle: aliases.join("・"), | ||||
|                   description: containsHighlight(description) ? description : undefined, | ||||
|                 })) | ||||
|                 .filter( | ||||
|                   ({ language }) => | ||||
|                     languageOption === 0 || | ||||
|                     language === languageOptions[languageOption]?.meiliAttribute | ||||
|                 )} | ||||
|               fallback={{ title: prettySlug(item.slug) }} | ||||
|               thumbnail={item.thumbnail?.data?.attributes} | ||||
|               thumbnailAspectRatio="1/1" | ||||
|  | ||||
| @ -33,6 +33,7 @@ export interface MeiliContent | ||||
|     displayable_description?: string | null; | ||||
|   })[]; | ||||
|   sortable_updated_date: number; | ||||
|   filterable_languages: string[]; | ||||
| } | ||||
| 
 | ||||
| export interface MeiliVideo extends VideoAttributesFragment { | ||||
| @ -50,6 +51,7 @@ export interface MeiliPost extends Omit<PostAttributesFragment, "translations"> | ||||
|   > & { | ||||
|     displayable_description?: string | null; | ||||
|   })[]; | ||||
|   filterable_languages: string[]; | ||||
| } | ||||
| 
 | ||||
| export interface MeiliWikiPage extends Omit<WikiPageAttributesFragment, "translations"> { | ||||
| @ -60,6 +62,7 @@ export interface MeiliWikiPage extends Omit<WikiPageAttributesFragment, "transla | ||||
|   > & { | ||||
|     displayable_description?: string | null; | ||||
|   })[]; | ||||
|   filterable_languages: string[]; | ||||
| } | ||||
| 
 | ||||
| type WeaponAttributesTranslation = NonNullable< | ||||
| @ -79,6 +82,7 @@ export interface MeiliWeapon extends Omit<WeaponAttributesFragment, "name" | "st | ||||
|     description: string; | ||||
|     language: WeaponAttributesTranslation["language"]; | ||||
|   }[]; | ||||
|   filterable_languages: string[]; | ||||
| } | ||||
| 
 | ||||
| export enum MeiliIndices { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint