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