Add language filter for a lot of pages

This commit is contained in:
DrMint 2023-06-09 21:45:20 +02:00
parent 3c7b9aa2d6
commit 872f31a6a3
16 changed files with 460 additions and 166 deletions

View File

@ -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>
</>
)}

View File

@ -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>
))}

View File

@ -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={[
{

View File

@ -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 && (

View File

@ -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);
};

View File

@ -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"]

View File

@ -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.", "");

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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" />

View File

@ -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 }) => ({
])
.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"

View File

@ -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,
],
}

View File

@ -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 }) => ({
])
.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"

View File

@ -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,8 +236,15 @@ 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 }) => ({
])
.map(
({
aliases,
summary,
displayable_description,
language,
...otherAttributes
}) => ({
...otherAttributes,
subtitle:
aliases && aliases.length > 0
@ -194,6 +255,11 @@ const Wiki = (props: Props): JSX.Element => {
: summary,
language: language.data.attributes.code,
})
)
.filter(
({ language }) =>
languageOption === 0 ||
language === languageOptions[languageOption]?.meiliAttribute
)}
fallback={{ title: prettySlug(item.slug) }}
thumbnail={item.thumbnail?.data?.attributes}

View File

@ -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] }) => ({
])
.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"

View File

@ -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 {