Moved assert helpers into their own folder + activated some tsconfig strict options

This commit is contained in:
DrMint 2022-12-09 23:03:09 +01:00
parent e9950602c4
commit 0ddd46643b
65 changed files with 314 additions and 302 deletions

View File

@ -3,7 +3,7 @@ import { useSwipeable } from "react-swipeable";
import { layout } from "../../design.config";
import { Ico, Icon } from "./Ico";
import { MainPanel } from "./Panels/MainPanel";
import { isDefined, isUndefined } from "helpers/others";
import { isDefined, isUndefined } from "helpers/asserts";
import { cIf, cJoin } from "helpers/className";
import { OpenGraph, TITLE_PREFIX, TITLE_SEPARATOR } from "helpers/openGraph";
import { Ids } from "types/ids";
@ -239,7 +239,7 @@ const ContentPlaceholder = ({ message, icon }: ContentPlaceholderProps): JSX.Ele
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
border-dark p-8 text-dark opacity-40">
{isDefined(icon) && <Ico icon={icon} className="!text-[300%]" />}
<p className={cJoin("w-64 text-2xl", cIf(!isDefined(icon), "text-center"))}>{message}</p>
<p className={cJoin("w-64 text-2xl", cIf(isUndefined(icon), "text-center"))}>{message}</p>
</div>
</div>
);

View File

@ -3,7 +3,7 @@ import { DatePickerFragment } from "graphql/generated";
import { TranslatedProps } from "types/TranslatedProps";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { DownPressable } from "components/Containers/DownPressable";
import { isDefined } from "helpers/others";
import { isDefined } from "helpers/asserts";
import { cIf, cJoin } from "helpers/className";
/*

View File

@ -2,7 +2,7 @@ import { useCallback } from "react";
import { useBoolean } from "usehooks-ts";
import { TranslatedChroniclePreview } from "./ChroniclePreview";
import { GetChroniclesChaptersQuery } from "graphql/generated";
import { filterHasAttributes } from "helpers/others";
import { filterHasAttributes } from "helpers/asserts";
import { prettyInlineTitle, prettySlug, sJoin } from "helpers/formatters";
import { Ico, Icon } from "components/Ico";
import { compareDate } from "helpers/date";

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/router";
import { cJoin, cIf } from "helpers/className";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { atoms } from "contexts/atoms";
import { useAtomSetter, useAtomPair } from "helpers/atoms";

View File

@ -2,7 +2,7 @@ import { MouseEventHandler, useCallback } from "react";
import { Link } from "./Link";
import { Ico, Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { TranslatedProps } from "types/TranslatedProps";
import { useSmartLanguage } from "hooks/useSmartLanguage";

View File

@ -2,7 +2,7 @@ import { Button } from "./Button";
import { ToolTip } from "components/ToolTip";
import { cJoin } from "helpers/className";
import { ConditionalWrapper, Wrapper } from "helpers/component";
import { isDefinedAndNotEmpty } from "helpers/others";
import { isDefinedAndNotEmpty } from "helpers/asserts";
/*
*

View File

@ -1,7 +1,7 @@
import React, { MouseEventHandler } from "react";
import NextLink from "next/link";
import { ConditionalWrapper, Wrapper } from "helpers/component";
import { isDefinedAndNotEmpty } from "helpers/others";
import { isDefinedAndNotEmpty } from "helpers/asserts";
import { cIf, cJoin } from "helpers/className";
interface Props {

View File

@ -1,6 +1,7 @@
import { Fragment, useCallback } from "react";
import { Ico, Icon } from "components/Ico";
import { arrayMove, isDefinedAndNotEmpty } from "helpers/others";
import { arrayMove } from "helpers/others";
import { isDefinedAndNotEmpty } from "helpers/asserts";
/*
*
@ -16,6 +17,14 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface InsertedLabelProps {
label?: string;
}
const InsertedLabel = ({ label }: InsertedLabelProps) => (
<>{isDefinedAndNotEmpty(label) && <p>{label}</p>}</>
);
export const OrderableList = ({ onChange, items, insertLabels }: Props): JSX.Element => {
const updateOrder = useCallback(
(sourceIndex: number, targetIndex: number) => {
@ -29,9 +38,8 @@ export const OrderableList = ({ onChange, items, insertLabels }: Props): JSX.Ele
<div className="grid gap-2">
{items.map((item, index) => (
<Fragment key={index}>
{insertLabels && isDefinedAndNotEmpty(insertLabels[index]?.name) && (
<p>{insertLabels[index].name}</p>
)}
<InsertedLabel label={insertLabels?.[index]?.name} />
<div
onDragStart={(event) => {
const source = event.target as HTMLElement;

View File

@ -1,6 +1,6 @@
import { Ico, Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
import { isDefinedAndNotEmpty } from "helpers/asserts";
/*
*

View File

@ -1,5 +1,5 @@
import { cIf, cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
import { isDefinedAndNotEmpty } from "helpers/asserts";
/*
*

View File

@ -9,7 +9,7 @@ import { useFullscreen } from "hooks/useFullscreen";
import { Ids } from "types/ids";
import { UploadImageFragment } from "graphql/generated";
import { ImageQuality } from "helpers/img";
import { isDefined } from "helpers/others";
import { isDefined } from "helpers/asserts";
import { useAtomGetter } from "helpers/atoms";
import { atoms } from "contexts/atoms";

View File

@ -7,7 +7,7 @@ import { InsetBox } from "components/Containers/InsetBox";
import { cIf, cJoin } from "helpers/className";
import { slugify } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img";
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others";
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
import { AnchorShare } from "components/AnchorShare";
import { useIntersectionList } from "hooks/useIntersectionList";
import { Ico, Icon } from "components/Ico";
@ -459,7 +459,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h5 = -1;
scenebreak = 0;
} else if (h2 >= 0 && line.startsWith('<Header level="3"')) {
toc.children[h2].children.push({
toc.children[h2]?.children.push({
title: getTitle(line),
slug: getSlug(line),
children: [],
@ -469,7 +469,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h5 = -1;
scenebreak = 0;
} else if (h3 >= 0 && line.startsWith('<Header level="4"')) {
toc.children[h2].children[h3].children.push({
toc.children[h2]?.children[h3]?.children.push({
title: getTitle(line),
slug: getSlug(line),
children: [],
@ -478,7 +478,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h5 = -1;
scenebreak = 0;
} else if (h4 >= 0 && line.startsWith('<Header level="5"')) {
toc.children[h2].children[h3].children[h4].children.push({
toc.children[h2]?.children[h3]?.children[h4]?.children.push({
title: getTitle(line),
slug: getSlug(line),
children: [],
@ -486,7 +486,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h5++;
scenebreak = 0;
} else if (h5 >= 0 && line.startsWith('<Header level="6"')) {
toc.children[h2].children[h3].children[h4].children[h5].children.push({
toc.children[h2]?.children[h3]?.children[h4]?.children[h5]?.children.push({
title: getTitle(line),
slug: getSlug(line),
children: [],
@ -502,13 +502,13 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
};
if (h5 >= 0) {
toc.children[h2].children[h3].children[h4].children[h5].children.push(child);
toc.children[h2]?.children[h3]?.children[h4]?.children[h5]?.children.push(child);
} else if (h4 >= 0) {
toc.children[h2].children[h3].children[h4].children.push(child);
toc.children[h2]?.children[h3]?.children[h4]?.children.push(child);
} else if (h3 >= 0) {
toc.children[h2].children[h3].children.push(child);
toc.children[h2]?.children[h3]?.children.push(child);
} else if (h2 >= 0) {
toc.children[h2].children.push(child);
toc.children[h2]?.children.push(child);
} else {
toc.children.push(child);
}

View File

@ -3,7 +3,7 @@ import { MouseEventHandler, useCallback } from "react";
import { Ico, Icon } from "components/Ico";
import { ToolTip } from "components/ToolTip";
import { cIf, cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
import { isDefinedAndNotEmpty } from "helpers/asserts";
import { TranslatedProps } from "types/TranslatedProps";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { DownPressable } from "components/Containers/DownPressable";

View File

@ -1,5 +1,5 @@
import { Ico, Icon } from "components/Ico";
import { isDefinedAndNotEmpty } from "helpers/others";
import { isDefinedAndNotEmpty } from "helpers/asserts";
/*
*

View File

@ -3,7 +3,7 @@ import { Icon } from "components/Ico";
import { Button } from "components/Inputs/Button";
import { TranslatedProps } from "types/TranslatedProps";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { isDefined } from "helpers/others";
import { isUndefined } from "helpers/asserts";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
@ -30,7 +30,7 @@ export const ReturnButton = ({ href, title, displayOnlyOn, className }: Props):
<>
{((is3ColumnsLayout && displayOnlyOn === "3ColumnsLayout") ||
(!is3ColumnsLayout && displayOnlyOn === "1ColumnLayout") ||
!isDefined(displayOnlyOn)) && (
isUndefined(displayOnlyOn)) && (
<div className={className}>
<Button href={href} text={`${langui.return_to} ${title}`} icon={Icon.NavigateBefore} />
</div>

View File

@ -4,7 +4,7 @@ import { NavOption } from "components/PanelComponents/NavOption";
import { ToolTip } from "components/ToolTip";
import { Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
import { isDefinedAndNotEmpty } from "helpers/asserts";
import { Link } from "components/Inputs/Link";
import { sendAnalytics } from "helpers/analytics";
import { ColoredSvg } from "components/ColoredSvg";

View File

@ -10,7 +10,7 @@ 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/others";
import { filterHasAttributes, isDefined } from "helpers/asserts";
import { atoms } from "contexts/atoms";
import { useAtomGetter, useAtomPair } from "helpers/atoms";
import { ThemeMode } from "contexts/settings";
@ -39,15 +39,10 @@ export const SettingsPopup = (): JSX.Element => {
);
const [currencySelect, setCurrencySelect] = useState<number>(-1);
useEffect(() => {
if (isDefined(currency)) setCurrencySelect(currencyOptions.indexOf(currency));
}, [currency, currencyOptions]);
useEffect(() => {
if (currencySelect >= 0) setCurrency(currencyOptions[currencySelect]);
}, [currencyOptions, currencySelect, setCurrency]);
return (
<Popup
isVisible={isSettingsOpened}
@ -134,8 +129,11 @@ export const SettingsPopup = (): JSX.Element => {
options={currencyOptions}
value={currencySelect}
onChange={(newCurrency) => {
setCurrencySelect(newCurrency);
sendAnalytics("Settings", `Change currency (${currencyOptions[newCurrency]})}`);
const newCurrencyName = currencyOptions[newCurrency];
if (isDefined(newCurrencyName)) {
setCurrency(newCurrencyName);
sendAnalytics("Settings", `Change currency (${currencyOptions[newCurrency]})}`);
}
}}
className="w-28"
/>

View File

@ -11,7 +11,8 @@ import { ThumbnailHeader } from "./ThumbnailHeader";
import { ToolTip } from "./ToolTip";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { PostWithTranslations } from "types/types";
import { filterHasAttributes, getStatusDescription } from "helpers/others";
import { getStatusDescription } from "helpers/others";
import { filterHasAttributes } from "helpers/asserts";
import { prettySlug } from "helpers/formatters";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";

View File

@ -7,7 +7,7 @@ import { ImageQuality } from "helpers/img";
import { TranslatedProps } from "types/TranslatedProps";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { cIf, cJoin } from "helpers/className";
import { isDefined } from "helpers/others";
import { isDefined } from "helpers/asserts";
/*
*

View File

@ -5,7 +5,7 @@ import { ToolTip } from "./ToolTip";
import { Chip } from "components/Chip";
import { RecorderChipFragment } from "graphql/generated";
import { ImageQuality } from "helpers/img";
import { filterHasAttributes } from "helpers/others";
import { filterHasAttributes } from "helpers/asserts";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";

View File

@ -5,7 +5,7 @@ import { Chip } from "./Chip";
import { PageSelector } from "./Inputs/PageSelector";
import { Ico, Icon } from "./Ico";
import { cJoin } from "helpers/className";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { Ids } from "types/ids";
import { atoms } from "contexts/atoms";
@ -95,17 +95,17 @@ export const SmartList = <T,>({
const memo: Group<T>[] = [];
sortedItem.forEach((item) => {
groupingFunction(item).forEach((category) => {
const index = memo.findIndex((group) => group.name === category);
if (index === -1) {
groupingFunction(item).forEach((groupName) => {
const group = memo.find((elem) => elem.name === groupName);
if (isDefined(group)) {
group.items.push(item);
group.totalCount += groupCountingFunction(item);
} else {
memo.push({
name: category,
name: groupName,
items: [item],
totalCount: groupCountingFunction(item),
});
} else {
memo[index].items.push(item);
memo[index].totalCount += groupCountingFunction(item);
}
});
});
@ -170,7 +170,7 @@ export const SmartList = <T,>({
)}
<div className="mb-8">
{pages[page]?.length > 0 ? (
{(pages[page]?.length ?? 0) > 0 ? (
pages[page]?.map(
(group) =>
group.items.length > 0 && (

View File

@ -5,7 +5,7 @@ import { Markdawn } from "components/Markdown/Markdawn";
import { GetContentTextQuery, UploadImageFragment } from "graphql/generated";
import { prettyInlineTitle, prettySlug, slugify } from "helpers/formatters";
import { ImageQuality } from "helpers/img";
import { filterHasAttributes } from "helpers/others";
import { filterHasAttributes } from "helpers/asserts";
import { useAtomGetter } from "helpers/atoms";
import { atoms } from "contexts/atoms";

View File

@ -3,7 +3,7 @@ import { useEffectOnce } from "usehooks-ts";
import { atom } from "jotai";
import { UploadImageFragment } from "graphql/generated";
import { LightBox } from "components/LightBox";
import { filterDefined } from "helpers/others";
import { filterDefined } from "helpers/asserts";
import { atomPairing, useAtomSetter } from "helpers/atoms";
const lightBoxAtom = atomPairing(

View File

@ -4,7 +4,7 @@ import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { atomPairing, useAtomGetter, useAtomPair } from "helpers/atoms";
import { getDefaultPreferredLanguages } from "helpers/locales";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { usePrefersDarkMode } from "hooks/useMediaQuery";
export enum ThemeMode {

View File

@ -1,5 +1,5 @@
import { useLayoutEffect } from "react";
import { isDefined } from "helpers/others";
import { isDefined } from "helpers/asserts";
import { useIsWebkit } from "hooks/useIsWebkit";
export const useWebkitFixes = (): void => {

View File

@ -5,7 +5,7 @@ import { PostWithTranslations } from "types/types";
import { getOpenGraph } from "helpers/openGraph";
import { prettyDate, prettySlug } from "helpers/formatters";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { filterHasAttributes, isDefined } from "helpers/others";
import { filterHasAttributes, isDefined } from "helpers/asserts";
import { getDescription } from "helpers/description";
import { AppLayoutRequired } from "components/AppLayout";
@ -25,7 +25,7 @@ export const getPostStaticProps =
if (
post.posts?.data &&
post.posts.data.length > 0 &&
post.posts.data[0].attributes?.translations &&
post.posts.data[0]?.attributes?.translations &&
isDefined(context.locale) &&
isDefined(context.locales)
) {

79
src/helpers/asserts.ts Normal file
View File

@ -0,0 +1,79 @@
type JoinDot<K extends string, P extends string> = `${K}${"" extends K ? "" : "."}${P}`;
type PathDot<T, Acc extends string = ""> = T extends object
? {
[K in keyof T]: K extends string ? JoinDot<Acc, K> | PathDot<T[K], JoinDot<Acc, K>> : never;
}[keyof T]
: Acc;
type PathHead<T extends unknown[]> = T extends [infer head]
? head
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
T extends [infer head, ...infer rest]
? head
: "";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type PathRest<T extends unknown[]> = T extends [infer head, ...infer rest]
? rest extends []
? never
: rest
: never;
type PathLast<T extends unknown[]> = T["length"] extends 1 ? true : false;
type Recursive<T, Path extends unknown[]> = PathHead<Path> extends keyof T
? Omit<T, PathHead<Path>> & {
[P in PathHead<Path>]-?: PathLast<Path> extends true
? NonNullable<T[P]>
: Recursive<NonNullable<T[P]>, PathRest<Path>>;
}
: T;
type Split<Str, Acc extends string[] = []> = Str extends `${infer Head}.${infer Rest}`
? Split<Rest, [...Acc, Head]>
: Str extends `${infer Last}`
? [...Acc, Last]
: never;
export type SelectiveNonNullable<T, P extends PathDot<T>> = Recursive<NonNullable<T>, Split<P>>;
export const isDefined = <T>(t: T): t is NonNullable<T> => t !== null && t !== undefined;
export const isUndefined = <T>(t: T | null | undefined): t is null | undefined => !isDefined(t);
export const isDefinedAndNotEmpty = (string: string | null | undefined): string is string =>
isDefined(string) && string.length > 0;
export const filterDefined = <T>(t: T[] | null | undefined): NonNullable<T>[] =>
isUndefined(t) ? [] : (t.filter((item) => isDefined(item)) as NonNullable<T>[]);
export const filterHasAttributes = <T, P extends PathDot<T>>(
t: T[] | null | undefined,
paths: readonly P[]
): SelectiveNonNullable<T, typeof paths[number]>[] =>
isDefined(t)
? (t.filter((item) => hasAttributes(item, paths)) as unknown as SelectiveNonNullable<
T,
typeof paths[number]
>[])
: [];
const hasAttributes = <T>(item: T, paths: readonly PathDot<T>[]): boolean =>
isDefined(item) && paths.every((path) => hasAttribute(item, path));
const hasAttribute = <T>(item: T, path: string): boolean => {
if (isDefined(item)) {
const [head, ...rest] = path.split(".");
if (isDefined(head) && Object.keys(item).includes(head)) {
const attribute = head as keyof T;
if (isDefined(item[attribute])) {
if (rest.length > 0) {
return hasAttribute(item[attribute], rest.join("."));
}
return true;
}
}
}
return false;
};

View File

@ -1,4 +1,4 @@
import { isDefined } from "./others";
import { isDefined } from "./asserts";
export interface Wrapper {
children: React.ReactNode;

View File

@ -1,4 +1,4 @@
import { isUndefined } from "./others";
import { isUndefined } from "./asserts";
import { DatePickerFragment } from "graphql/generated";
export const compareDate = (

View File

@ -1,4 +1,4 @@
import { filterDefined, isDefined, isDefinedAndNotEmpty } from "./others";
import { filterDefined, isDefined, isDefinedAndNotEmpty } from "./asserts";
export const getDescription = (
description: string | null | undefined,

View File

@ -1,5 +1,5 @@
import { convertPrice } from "./numbers";
import { isDefinedAndNotEmpty, isUndefined } from "./others";
import { isDefinedAndNotEmpty, isUndefined } from "./asserts";
import { datePickerToDate } from "./date";
import { Currencies, Languages, Langui } from "./localData";
import { DatePickerFragment, PricePickerFragment } from "graphql/generated";
@ -180,7 +180,7 @@ export const prettyItemSubType = (
case "ComponentMetadataGame":
return metadata.platforms?.data &&
metadata.platforms.data.length > 0 &&
metadata.platforms.data[0].attributes
metadata.platforms.data[0]?.attributes
? metadata.platforms.data[0].attributes.short
: "";
case "ComponentMetadataGroup": {

View File

@ -27,13 +27,11 @@ const imageQualityProperties: Record<ImageQuality, ImageProperties> = {
};
export const getAssetFilename = (path: string): string => {
let result = path.split("/");
result = result[result.length - 1].split(".");
result = result
.splice(0, result.length - 1)
.join(".")
.split("_");
return result[0];
// /uploads/329_7f41d09a98.webp -> 329_7f41d09a98.webp
let result = path.substring(path.lastIndexOf("/") + 1);
// 329_7f41d09a98.webp -> 329
result = result.substring(0, result.indexOf("_"));
return result;
};
export const getAssetURL = (url: string, quality: ImageQuality): string => {

View File

@ -1,4 +1,4 @@
import { isDefined } from "./others";
import { isDefined } from "./asserts";
export const isUntangibleGroupItem = (
metadata:

View File

@ -1,4 +1,4 @@
import { isDefined } from "./others";
import { isDefined } from "./asserts";
export const getDefaultPreferredLanguages = (routerLocal: string, locales: string[]): string[] => {
let defaultPreferredLanguages: string[] = [];

View File

@ -1,5 +1,5 @@
import { OgImage, getImgSizesByQuality, ImageQuality, getAssetURL } from "./img";
import { isDefinedAndNotEmpty } from "./others";
import { isDefinedAndNotEmpty } from "./asserts";
import { Langui } from "./localData";
import { UploadImageFragment } from "graphql/generated";

View File

@ -1,5 +1,5 @@
import { PathDot, SelectiveNonNullable } from "../types/SelectiveNonNullable";
import { Langui } from "./localData";
import { isDefined } from "./asserts";
import {
Enum_Componentsetstextset_Status,
GetLibraryItemQuery,
@ -45,47 +45,6 @@ export const getStatusDescription = (status: string, langui: Langui): string | n
}
};
export const isDefined = <T>(t: T): t is NonNullable<T> => t !== null && t !== undefined;
export const isUndefined = <T>(t: T | null | undefined): t is null | undefined =>
t === null || t === undefined;
export const isDefinedAndNotEmpty = (string: string | null | undefined): string is string =>
isDefined(string) && string.length > 0;
export const filterDefined = <T>(t: T[] | null | undefined): NonNullable<T>[] =>
isUndefined(t) ? [] : (t.filter((item) => isDefined(item)) as NonNullable<T>[]);
export const filterHasAttributes = <T, P extends PathDot<T>>(
t: T[] | null | undefined,
paths: readonly P[]
): SelectiveNonNullable<T, typeof paths[number]>[] =>
isDefined(t)
? (t.filter((item) => hasAttributes(item, paths)) as unknown as SelectiveNonNullable<
T,
typeof paths[number]
>[])
: [];
const hasAttributes = <T>(item: T, paths: readonly PathDot<T>[]): boolean =>
isDefined(item) && paths.every((path) => hasAttribute(item, path));
const hasAttribute = <T>(item: T, path: string): boolean => {
if (isDefined(item)) {
const [head, ...rest] = path.split(".");
if (Object.keys(item).includes(head)) {
const attribute = head as keyof T;
if (isDefined(item[attribute])) {
if (rest.length > 0) {
return hasAttribute(item[attribute], rest.join("."));
}
return true;
}
}
}
return false;
};
export const iterateMap = <K, V, U>(
map: Map<K, V>,
callbackfn: (key: K, value: V, index: number) => U,
@ -99,7 +58,10 @@ export const iterateMap = <K, V, U>(
};
export const arrayMove = <T>(arr: T[], sourceIndex: number, targetIndex: number): T[] => {
arr.splice(targetIndex, 0, arr.splice(sourceIndex, 1)[0]);
const sourceItem = arr.splice(sourceIndex, 1)[0];
if (isDefined(sourceItem)) {
arr.splice(targetIndex, 0, sourceItem);
}
return arr;
};

View File

@ -1,4 +1,4 @@
import { isDefinedAndNotEmpty } from "./others";
import { isDefinedAndNotEmpty } from "./asserts";
export const prettyTerminalUnderlinedTitle = (string: string | null | undefined): string =>
isDefinedAndNotEmpty(string)

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { useIsClient } from "usehooks-ts";
import { isDefined } from "helpers/others";
import { isDefined } from "helpers/asserts";
export const useFullscreen = (
id: string

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import { throttle } from "throttle-debounce";
import { useIsClient } from "usehooks-ts";
import { useOnScroll } from "./useOnScroll";
import { isDefined } from "helpers/others";
import { isDefined, isUndefined } from "helpers/asserts";
import { Ids } from "types/ids";
export const useIntersectionList = (ids: string[]): number => {
@ -16,21 +16,21 @@ export const useIntersectionList = (ids: string[]): number => {
(scroll: number) => {
console.log("useIntersectionList");
if (!isDefined(contentPanel)) {
if (isUndefined(contentPanel)) {
setCurrentIntersection(-1);
return;
}
for (let idIndex = 0; idIndex < ids.length; idIndex++) {
const elem = document.getElementById(ids[ids.length - 1 - idIndex]);
for (const [index, id] of [...ids].reverse().entries()) {
const elem = document.getElementById(id);
const halfScreenOffset = window.screen.height / 2;
if (isDefined(elem) && scroll > elem.offsetTop - halfScreenOffset) {
setCurrentIntersection(ids.length - 1 - idIndex);
const unreversedIndex = ids.length - 1 - index;
setCurrentIntersection(unreversedIndex);
return;
}
}
setCurrentIntersection(-1);
},
[ids, contentPanel]
);

View File

@ -1,6 +1,6 @@
import { useEffect } from "react";
import { useIsClient } from "usehooks-ts";
import { isDefined } from "helpers/others";
import { isDefined } from "helpers/asserts";
export const useOnResize = (
id: string,
@ -11,9 +11,12 @@ export const useOnResize = (
useEffect(() => {
console.log("[useOnResize]", id);
const elem = isClient ? document.querySelector(`#${id}`) : null;
const ro = new ResizeObserver((resizeObserverEntry) =>
onResize(resizeObserverEntry[0].contentRect.width, resizeObserverEntry[0].contentRect.height)
);
const ro = new ResizeObserver((resizeObserverEntry) => {
const entry = resizeObserverEntry[0];
if (isDefined(entry)) {
onResize(entry.contentRect.width, entry.contentRect.height);
}
});
if (isDefined(elem)) {
ro.observe(elem);
}

View File

@ -1,7 +1,7 @@
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useCounter } from "usehooks-ts";
import { isDefined } from "helpers/others";
import { isDefined } from "helpers/asserts";
const NUM_RETRIES = 10;

View File

@ -1,7 +1,7 @@
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher";
import { filterDefined, isDefined } from "helpers/others";
import { filterDefined, isDefined } from "helpers/asserts";
import { getPreferredLanguage } from "helpers/locales";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";

View File

@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { i18n } from "../../../next.config";
import { cartesianProduct, filterHasAttributes, isDefined } from "helpers/others";
import { cartesianProduct } from "helpers/others";
import { filterHasAttributes, isDefined } from "helpers/asserts";
import { fetchLocalData } from "graphql/fetchLocalData";
import { getReadySdk } from "graphql/sdk";
@ -183,7 +184,7 @@ const Revalidate = async (
language_code: "en",
slug: body.entry.slug,
});
filterHasAttributes(libraryItem.libraryItems?.data[0].attributes?.subitem_of?.data, [
filterHasAttributes(libraryItem.libraryItems?.data[0]?.attributes?.subitem_of?.data, [
"attributes.slug",
] as const).forEach((parentItem) => paths.push(`/library/${parentItem.attributes.slug}`));
}
@ -202,12 +203,12 @@ const Revalidate = async (
slug: body.entry.slug,
});
const folderSlug = content.contents?.data[0].attributes?.folder?.data?.attributes?.slug;
const folderSlug = content.contents?.data[0]?.attributes?.folder?.data?.attributes?.slug;
if (folderSlug) {
paths.push(`/contents/folder/${folderSlug}`);
}
filterHasAttributes(content.contents?.data[0].attributes?.ranged_contents?.data, [
filterHasAttributes(content.contents?.data[0]?.attributes?.ranged_contents?.data, [
"attributes.library_item.data.attributes.slug",
] as const).forEach((ranged_content) => {
const parentSlug = ranged_content.attributes.library_item.data.attributes.slug;
@ -260,16 +261,16 @@ const Revalidate = async (
slug: body.entry.slug,
});
const parentSlug =
folder.contentsFolders?.data[0].attributes?.parent_folder?.data?.attributes?.slug;
folder.contentsFolders?.data[0]?.attributes?.parent_folder?.data?.attributes?.slug;
if (parentSlug) {
paths.push(`/contents/folder/${parentSlug}`);
}
filterHasAttributes(folder.contentsFolders?.data[0].attributes?.subfolders?.data, [
filterHasAttributes(folder.contentsFolders?.data[0]?.attributes?.subfolders?.data, [
"attributes.slug",
] as const).forEach((subfolder) =>
paths.push(`/contents/folder/${subfolder.attributes.slug}`)
);
filterHasAttributes(folder.contentsFolders?.data[0].attributes?.contents?.data, [
filterHasAttributes(folder.contentsFolders?.data[0]?.attributes?.contents?.data, [
"attributes.slug",
] as const).forEach((content) => paths.push(`/contents/${content.attributes.slug}`));
}
@ -308,7 +309,7 @@ const Revalidate = async (
paths.push(`/archives/videos`);
paths.push(`/archives/videos/v/${body.entry.uid}`);
const video = await sdk.getVideo({ uid: body.entry.uid });
const channelUid = video.videos?.data[0].attributes?.channel?.data?.attributes?.uid;
const channelUid = video.videos?.data[0]?.attributes?.channel?.data?.attributes?.uid;
if (isDefined(channelUid)) {
paths.push(`/archives/videos/c/${channelUid}`);
}

View File

@ -14,7 +14,7 @@ import { getVideoThumbnailURL } from "helpers/videos";
import { Icon } from "components/Ico";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { WithLabel } from "components/Inputs/WithLabel";
import { filterHasAttributes, isDefined } from "helpers/others";
import { filterHasAttributes, isDefined } from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph";
import { compareDate } from "helpers/date";
import { HorizontalLine } from "components/HorizontalLine";
@ -136,7 +136,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
const channel = await sdk.getVideoChannel({
channel: context.params && isDefined(context.params.uid) ? context.params.uid.toString() : "",
});
if (!channel.videoChannels?.data[0].attributes) return { notFound: true };
if (!channel.videoChannels?.data[0]?.attributes) return { notFound: true };
channel.videoChannels.data[0].attributes.videos?.data
.sort((a, b) => compareDate(a.attributes?.published_date, b.attributes?.published_date))

View File

@ -14,7 +14,7 @@ import { SubPanel } from "components/Containers/SubPanel";
import { PreviewCard } from "components/PreviewCard";
import { GetVideosPreviewQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes } from "helpers/others";
import { filterHasAttributes } from "helpers/asserts";
import { getVideoThumbnailURL } from "helpers/videos";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { getOpenGraph } from "helpers/openGraph";

View File

@ -12,7 +12,7 @@ import { SubPanel } from "components/Containers/SubPanel";
import { GetVideoQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { prettyDate, prettyShortenNumber } from "helpers/formatters";
import { filterHasAttributes, isDefined } from "helpers/others";
import { filterHasAttributes, isDefined } from "helpers/asserts";
import { getVideoFile } from "helpers/videos";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";

View File

@ -1,7 +1,7 @@
import { GetStaticProps, GetStaticPaths, GetStaticPathsResult } from "next";
import { useCallback, useMemo } from "react";
import { useCallback } from "react";
import { getReadySdk } from "graphql/sdk";
import { isDefined, filterHasAttributes } from "helpers/others";
import { isDefined, filterHasAttributes } from "helpers/asserts";
import { ChronicleWithTranslations } from "types/types";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { useSmartLanguage } from "hooks/useSmartLanguage";

View File

@ -5,7 +5,7 @@ import { SubPanel } from "components/Containers/SubPanel";
import { Icon } from "components/Ico";
import { getReadySdk } from "graphql/sdk";
import { GetChroniclesChaptersQuery } from "graphql/generated";
import { filterHasAttributes } from "helpers/others";
import { filterHasAttributes } from "helpers/asserts";
import { prettySlug } from "helpers/formatters";
import { getOpenGraph } from "helpers/openGraph";
import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList";

View File

@ -21,7 +21,8 @@ import {
prettySlug,
} from "helpers/formatters";
import { isUntangibleGroupItem } from "helpers/libraryItem";
import { filterHasAttributes, getStatusDescription, isDefinedAndNotEmpty } from "helpers/others";
import { getStatusDescription } from "helpers/others";
import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/asserts";
import { ContentWithTranslations } from "types/types";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { useSmartLanguage } from "hooks/useSmartLanguage";
@ -466,7 +467,7 @@ type FolderContents = NonNullable<
const getPreviousContent = (contents: FolderContents, currentSlug: string) => {
for (let index = 0; index < contents.length; index++) {
const content = contents[index];
if (content.attributes?.slug === currentSlug && index > 0) {
if (content?.attributes?.slug === currentSlug && index > 0) {
return contents[index - 1];
}
}
@ -478,7 +479,7 @@ const getPreviousContent = (contents: FolderContents, currentSlug: string) => {
const getNextContent = (contents: FolderContents, currentSlug: string) => {
for (let index = 0; index < contents.length; index++) {
const content = contents[index];
if (content.attributes?.slug === currentSlug && index < contents.length - 1) {
if (content?.attributes?.slug === currentSlug && index < contents.length - 1) {
return contents[index + 1];
}
}

View File

@ -15,10 +15,14 @@ import { WithLabel } from "components/Inputs/WithLabel";
import { Button } from "components/Inputs/Button";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { Icon } from "components/Ico";
import { filterDefined, filterHasAttributes, isDefinedAndNotEmpty } from "helpers/others";
import {
filterDefined,
filterHasAttributes,
isDefinedAndNotEmpty,
SelectiveNonNullable,
} from "helpers/asserts";
import { GetContentsQuery } from "graphql/generated";
import { SmartList } from "components/SmartList";
import { SelectiveNonNullable } from "types/SelectiveNonNullable";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { TranslatedPreviewCard } from "components/PreviewCard";

View File

@ -4,7 +4,7 @@ import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { getOpenGraph } from "helpers/openGraph";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes } from "helpers/others";
import { filterHasAttributes } from "helpers/asserts";
import { GetContentsFolderQuery } from "graphql/generated";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { prettySlug } from "helpers/formatters";

View File

@ -6,7 +6,7 @@ import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/Cont
import { ToolTip } from "components/ToolTip";
import { DevGetContentsQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { filterDefined, filterHasAttributes } from "helpers/others";
import { filterDefined, filterHasAttributes } from "helpers/asserts";
import { Report, Severity } from "types/Report";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
@ -61,7 +61,8 @@ const CheckupContents = ({ contents, ...otherProps }: Props): JSX.Element => {
? "bg-[#fff344] !opacity-100"
: ""
}
text={Severity[line.severity]}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
text={Severity[line.severity] ?? "Unknown"}
/>
<ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p>

View File

@ -47,7 +47,7 @@ const CheckupLibraryItems = ({ libraryItems, ...otherProps }: Props): JSX.Elemen
<div
key={index}
className="mb-2 grid
grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2">
grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2">
<Button href={line.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab />
<Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab />
<p>{line.subitems.join(" -> ")}</p>
@ -63,7 +63,8 @@ const CheckupLibraryItems = ({ libraryItems, ...otherProps }: Props): JSX.Elemen
? "bg-[#fff344] !opacity-100"
: ""
}
text={Severity[line.severity]}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
text={Severity[line.severity] ?? "Unknown"}
/>
<ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p>

View File

@ -431,12 +431,7 @@ const DesignSystem = (props: Props): JSX.Element => {
value={sliderState}
max={100}
onChange={(event) => {
let value = 0;
if (Array.isArray(event)) {
value = event[0];
} else {
value = event;
}
const value = (Array.isArray(event) ? event[0] : event) ?? 0;
setSliderState(() => value);
}}
/>

View File

@ -32,7 +32,7 @@ const replaceSelection = (
const swapChar = (char: string, swaps: string[]): string => {
for (let index = 0; index < swaps.length; index++) {
if (char === swaps[index]) {
return swaps[(index + 1) % swaps.length];
return swaps[(index + 1) % swaps.length] ?? char;
}
}
return char;

View File

@ -31,13 +31,13 @@ import {
} from "helpers/formatters";
import { ImageQuality } from "helpers/img";
import { convertMmToInch } from "helpers/numbers";
import { sortRangedContent } from "helpers/others";
import {
filterDefined,
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
sortRangedContent,
} from "helpers/others";
} from "helpers/asserts";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { isUntangibleGroupItem } from "helpers/libraryItem";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";

View File

@ -11,13 +11,8 @@ import {
UploadImageFragment,
} from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import {
filterHasAttributes,
getStatusDescription,
isDefined,
isDefinedAndNotEmpty,
sortRangedContent,
} from "helpers/others";
import { getStatusDescription, sortRangedContent } from "helpers/others";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
@ -305,12 +300,7 @@ const LibrarySlug = ({
max={10}
value={filterSettings.teint * 10}
onChange={(event) => {
let value = 0;
if (Array.isArray(event)) {
value = event[0];
} else {
value = event;
}
const value = (Array.isArray(event) ? event[0] : event) ?? 0;
setTeint(value / 10);
}}
/>
@ -390,7 +380,7 @@ const LibrarySlug = ({
? CUSTOM_DARK_DROPSHADOW
: CUSTOM_LIGHT_DROPSHADOW,
}}>
{effectiveDisplayMode === "single" ? (
{effectiveDisplayMode === "single" && isDefined(firstPage) ? (
<div
className={cJoin(
"relative grid grid-flow-col",
@ -412,70 +402,73 @@ const LibrarySlug = ({
/>
</div>
) : (
<>
<div
className={cJoin(
"relative grid grid-flow-col",
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
)}
onClick={() => currentZoom <= 1 && handlePageNavigation("left")}
style={{
clipPath: leftSideClipPath,
}}>
{isSidePagesEnabled && (
<div
style={{
width: leftSidePagesWidth,
backgroundImage: `url(/reader/sidepages-${bookType}.webp)`,
backgroundSize: `${
(SIDEPAGES_PAGE_COUNT_ON_TEXTURE / leftSidePagesCount) * 100
}% 100%`,
}}
/>
)}
<Img
style={{ maxHeight: pageHeight, width: "auto" }}
src={pageOrder === PageOrder.LeftToRight ? firstPage : secondPage}
quality={pageQuality}
/>
<PageFilters page="left" bookType={bookType} options={filterSettings} />
</div>
<div
className={cJoin(
"relative grid grid-flow-col",
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
)}
onClick={() => currentZoom <= 1 && handlePageNavigation("right")}
style={{
clipPath: rightSideClipPath,
}}>
<Img
style={{ maxHeight: pageHeight, width: "auto" }}
className={cIf(
is1ColumnLayout,
`max-h-[calc(100vh-5rem)]`,
"max-h-[calc(100vh-4rem)]"
isDefined(firstPage) &&
isDefined(secondPage) && (
<>
<div
className={cJoin(
"relative grid grid-flow-col",
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
)}
onClick={() => currentZoom <= 1 && handlePageNavigation("left")}
style={{
clipPath: leftSideClipPath,
}}>
{isSidePagesEnabled && (
<div
style={{
width: leftSidePagesWidth,
backgroundImage: `url(/reader/sidepages-${bookType}.webp)`,
backgroundSize: `${
(SIDEPAGES_PAGE_COUNT_ON_TEXTURE / leftSidePagesCount) * 100
}% 100%`,
}}
/>
)}
src={pageOrder === PageOrder.LeftToRight ? secondPage : firstPage}
quality={pageQuality}
/>
{isSidePagesEnabled && (
<div
style={{
width: rightSidePagesWidth,
backgroundImage: `url(/reader/sidepages-${bookType}.webp)`,
backgroundPositionX: "right",
backgroundSize: `${
(SIDEPAGES_PAGE_COUNT_ON_TEXTURE / rightSidePagesCount) * 100
}% 100%`,
}}
/>
)}
<PageFilters page="right" bookType={bookType} options={filterSettings} />
</div>
</>
<Img
style={{ maxHeight: pageHeight, width: "auto" }}
src={pageOrder === PageOrder.LeftToRight ? firstPage : secondPage}
quality={pageQuality}
/>
<PageFilters page="left" bookType={bookType} options={filterSettings} />
</div>
<div
className={cJoin(
"relative grid grid-flow-col",
cIf(currentZoom <= 1, "cursor-pointer", "cursor-move")
)}
onClick={() => currentZoom <= 1 && handlePageNavigation("right")}
style={{
clipPath: rightSideClipPath,
}}>
<Img
style={{ maxHeight: pageHeight, width: "auto" }}
className={cIf(
is1ColumnLayout,
`max-h-[calc(100vh-5rem)]`,
"max-h-[calc(100vh-4rem)]"
)}
src={pageOrder === PageOrder.LeftToRight ? secondPage : firstPage}
quality={pageQuality}
/>
{isSidePagesEnabled && (
<div
style={{
width: rightSidePagesWidth,
backgroundImage: `url(/reader/sidepages-${bookType}.webp)`,
backgroundPositionX: "right",
backgroundSize: `${
(SIDEPAGES_PAGE_COUNT_ON_TEXTURE / rightSidePagesCount) * 100
}% 100%`,
}}
/>
)}
<PageFilters page="right" bookType={bookType} options={filterSettings} />
</div>
</>
)
)}
</TransformComponent>
</TransformWrapper>
@ -498,12 +491,7 @@ const LibrarySlug = ({
max={pages.length - 1}
value={currentPageIndex - 1}
onChange={(event) => {
let value = 0;
if (Array.isArray(event)) {
value = event[0];
} else {
value = event;
}
const value = (Array.isArray(event) ? event[0] : event) ?? 0;
changeCurrentPageIndex(() => value);
}}
/>
@ -657,7 +645,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
bookType,
pageWidth,
itemSlug: item.libraryItems.data[0].attributes.slug,
pageRatio: `${pages[0].width ?? 21} / ${pages[0].height ?? 29.7}`,
pageRatio: `${pages[0]?.width ?? 21} / ${pages[0]?.height ?? 29.7}`,
openGraph: getOpenGraph(
langui,
item.libraryItems.data[0].attributes.title,

View File

@ -21,10 +21,15 @@ import { isUntangibleGroupItem } from "helpers/libraryItem";
import { PreviewCard } from "components/PreviewCard";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others";
import {
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
isUndefined,
SelectiveNonNullable,
} from "helpers/asserts";
import { convertPrice } from "helpers/numbers";
import { SmartList } from "components/SmartList";
import { SelectiveNonNullable } from "types/SelectiveNonNullable";
import { getOpenGraph } from "helpers/openGraph";
import { compareDate } from "helpers/date";
import { HorizontalLine } from "components/HorizontalLine";
@ -142,11 +147,14 @@ const Library = ({ items, ...otherProps }: Props): JSX.Element => {
return naturalCompare(titleA, titleB);
}
case 1: {
const commonCurrency = currencies[0];
if (isUndefined(commonCurrency)) return 0;
const priceA = a.attributes.price
? convertPrice(a.attributes.price, currencies[0])
? convertPrice(a.attributes.price, commonCurrency)
: Infinity;
const priceB = b.attributes.price
? convertPrice(b.attributes.price, currencies[0])
? convertPrice(b.attributes.price, commonCurrency)
: Infinity;
return priceA - priceB;
}

View File

@ -3,7 +3,7 @@ import { NextRouter, useRouter } from "next/router";
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { Terminal } from "components/Cli/Terminal";
import { PostWithTranslations } from "types/types";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";

View File

@ -14,7 +14,7 @@ import { WithLabel } from "components/Inputs/WithLabel";
import { TextInput } from "components/Inputs/TextInput";
import { Button } from "components/Inputs/Button";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/others";
import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/asserts";
import { SmartList } from "components/SmartList";
import { getOpenGraph } from "helpers/openGraph";
import { compareDate } from "helpers/date";

View File

@ -10,7 +10,7 @@ import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/Cont
import { SubPanel } from "components/Containers/SubPanel";
import DefinitionCard from "components/Wiki/DefinitionCard";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { WikiPageWithTranslations } from "types/types";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { prettySlug, sJoin } from "helpers/formatters";

View File

@ -13,12 +13,8 @@ import {
} from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { prettySlug } from "helpers/formatters";
import {
filterHasAttributes,
getStatusDescription,
isDefined,
isDefinedAndNotEmpty,
} from "helpers/others";
import { getStatusDescription } from "helpers/others";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { ToolTip } from "components/ToolTip";
@ -156,7 +152,7 @@ const ChronologyEra = ({ id, title, description, chronologyItems }: ChronologyEr
let currentYear = -Infinity;
filterHasAttributes(chronologyItems, ["attributes"] as const).forEach((item) => {
if (currentYear === item.attributes.year) {
memo[memo.length - 1].push(item);
memo[memo.length - 1]?.push(item);
} else {
currentYear = item.attributes.year;
memo.push([item]);
@ -214,7 +210,7 @@ interface ChronologyYearProps {
const ChronologyYear = ({ items }: ChronologyYearProps) => (
<div
className="rounded-2xl target:my-4 target:bg-mid target:py-4"
id={generateAnchor(items[0].attributes?.year)}>
id={generateAnchor(items[0]?.attributes?.year)}>
{filterHasAttributes(items, ["attributes.events"] as const).map((item, index) => (
<ChronologyDate
key={index}

View File

@ -15,10 +15,14 @@ import { Switch } from "components/Inputs/Switch";
import { TextInput } from "components/Inputs/TextInput";
import { WithLabel } from "components/Inputs/WithLabel";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { filterDefined, filterHasAttributes, isDefinedAndNotEmpty } from "helpers/others";
import {
filterDefined,
filterHasAttributes,
isDefinedAndNotEmpty,
SelectiveNonNullable,
} from "helpers/asserts";
import { SmartList } from "components/SmartList";
import { Select } from "components/Inputs/Select";
import { SelectiveNonNullable } from "types/SelectiveNonNullable";
import { prettySlug } from "helpers/formatters";
import { getOpenGraph } from "helpers/openGraph";
import { TranslatedPreviewCard } from "components/PreviewCard";

View File

@ -1,39 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
type JoinDot<K extends string, P extends string> = `${K}${"" extends K ? "" : "."}${P}`;
export type PathDot<T, Acc extends string = ""> = T extends object
? {
[K in keyof T]: K extends string ? JoinDot<Acc, K> | PathDot<T[K], JoinDot<Acc, K>> : never;
}[keyof T]
: Acc;
type PathHead<T extends unknown[]> = T extends [infer head]
? head
: T extends [infer head, ...infer rest]
? head
: "";
type PathRest<T extends unknown[]> = T extends [infer head, ...infer rest]
? rest extends []
? never
: rest
: never;
type PathLast<T extends unknown[]> = T["length"] extends 1 ? true : false;
type Recursive<T, Path extends unknown[]> = PathHead<Path> extends keyof T
? Omit<T, PathHead<Path>> & {
[P in PathHead<Path>]-?: PathLast<Path> extends true
? NonNullable<T[P]>
: Recursive<NonNullable<T[P]>, PathRest<Path>>;
}
: T;
type Split<Str, Acc extends string[] = []> = Str extends `${infer Head}.${infer Rest}`
? Split<Rest, [...Acc, Head]>
: Str extends `${infer Last}`
? [...Acc, Last]
: never;
export type SelectiveNonNullable<T, P extends PathDot<T>> = Recursive<NonNullable<T>, Split<P>>;

View File

@ -1,11 +1,15 @@
{
"compilerOptions": {
// Type Checking
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"strict": true,
"target": "ES6",
"lib": ["dom", "dom.iterable", "esnext"],
"importHelpers": true,
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
@ -16,7 +20,6 @@
"jsx": "preserve",
"incremental": true,
"baseUrl": "src"
// "noUncheckedIndexedAccess": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]