Umami events + natural sort
This commit is contained in:
parent
bd0185358c
commit
40d893eba8
|
@ -23,6 +23,7 @@
|
|||
"react-dom": "18.2.0",
|
||||
"react-hot-keys": "^2.7.2",
|
||||
"react-swipeable": "^7.0.0",
|
||||
"string-natural-compare": "^3.0.1",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"turndown": "^7.1.1",
|
||||
|
@ -39,6 +40,7 @@
|
|||
"@types/nodemailer": "^6.4.5",
|
||||
"@types/react": "18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/string-natural-compare": "^3.0.2",
|
||||
"@types/throttle-debounce": "^5.0.0",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
||||
|
@ -2660,6 +2662,12 @@
|
|||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/string-natural-compare": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/string-natural-compare/-/string-natural-compare-3.0.2.tgz",
|
||||
"integrity": "sha512-teA6gjoKrX+eeweVnk3bbQsbKUQyrP6CDJGsu0x33U4wyq2aF4y6CwPC/ygnLL0rf8twMXRmLdXo7DVhp1XBBw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/throttle-debounce": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
|
||||
|
@ -8160,6 +8168,11 @@
|
|||
"integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/string-natural-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz",
|
||||
"integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw=="
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
|
@ -11115,6 +11128,12 @@
|
|||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/string-natural-compare": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/string-natural-compare/-/string-natural-compare-3.0.2.tgz",
|
||||
"integrity": "sha512-teA6gjoKrX+eeweVnk3bbQsbKUQyrP6CDJGsu0x33U4wyq2aF4y6CwPC/ygnLL0rf8twMXRmLdXo7DVhp1XBBw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/throttle-debounce": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
|
||||
|
@ -15107,6 +15126,11 @@
|
|||
"integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==",
|
||||
"dev": true
|
||||
},
|
||||
"string-natural-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz",
|
||||
"integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"react-dom": "18.2.0",
|
||||
"react-hot-keys": "^2.7.2",
|
||||
"react-swipeable": "^7.0.0",
|
||||
"string-natural-compare": "^3.0.1",
|
||||
"throttle-debounce": "^5.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"turndown": "^7.1.1",
|
||||
|
@ -50,6 +51,7 @@
|
|||
"@types/nodemailer": "^6.4.5",
|
||||
"@types/react": "18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/string-natural-compare": "^3.0.2",
|
||||
"@types/throttle-debounce": "^5.0.0",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
||||
|
|
|
@ -382,13 +382,19 @@ export const AppLayout = ({
|
|||
<Button
|
||||
text="Let me in regardless"
|
||||
className="mt-8"
|
||||
onClick={disgardSafariWarning}
|
||||
onClick={() => {
|
||||
disgardSafariWarning();
|
||||
umami("[Safari] Disgard warning");
|
||||
}}
|
||||
/>
|
||||
</Popup>
|
||||
|
||||
<Popup
|
||||
state={configPanelOpen}
|
||||
onClose={() => setConfigPanelOpen(false)}
|
||||
onClose={() => {
|
||||
setConfigPanelOpen(false);
|
||||
umami("[Settings] Close settings");
|
||||
}}
|
||||
>
|
||||
<h2 className="text-2xl">{langui.settings}</h2>
|
||||
|
||||
|
@ -423,6 +429,7 @@ export const AppLayout = ({
|
|||
(item) => item.code
|
||||
);
|
||||
setPreferredLanguages(newPreferredLanguages);
|
||||
umami("[Settings] Change preferred languages");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -442,6 +449,7 @@ export const AppLayout = ({
|
|||
onClick: () => {
|
||||
setDarkMode(false);
|
||||
setSelectedThemeMode(true);
|
||||
umami("[Settings] Change theme (light)");
|
||||
},
|
||||
active: selectedThemeMode && !darkMode,
|
||||
text: langui.light,
|
||||
|
@ -449,6 +457,7 @@ export const AppLayout = ({
|
|||
{
|
||||
onClick: () => {
|
||||
setSelectedThemeMode(false);
|
||||
umami("[Settings] Change theme (auto)");
|
||||
},
|
||||
active: !selectedThemeMode,
|
||||
text: langui.auto,
|
||||
|
@ -457,6 +466,7 @@ export const AppLayout = ({
|
|||
onClick: () => {
|
||||
setDarkMode(true);
|
||||
setSelectedThemeMode(true);
|
||||
umami("[Settings] Change theme (dark)");
|
||||
},
|
||||
active: selectedThemeMode && darkMode,
|
||||
text: langui.dark,
|
||||
|
@ -471,7 +481,12 @@ export const AppLayout = ({
|
|||
<Select
|
||||
options={currencyOptions}
|
||||
value={currencySelect}
|
||||
onChange={setCurrencySelect}
|
||||
onChange={(newCurrency) => {
|
||||
setCurrencySelect(newCurrency);
|
||||
umami(
|
||||
`[Settings] Change currency (${currencyOptions[newCurrency]})}`
|
||||
);
|
||||
}}
|
||||
className="w-28"
|
||||
/>
|
||||
</div>
|
||||
|
@ -482,17 +497,41 @@ export const AppLayout = ({
|
|||
<ButtonGroup
|
||||
buttonsProps={[
|
||||
{
|
||||
onClick: () => setFontSize(fontSize / 1.05),
|
||||
onClick: () => {
|
||||
setFontSize((current) => current / 1.05);
|
||||
umami(
|
||||
`[Settings] Change font size (${(
|
||||
(fontSize / 1.05) *
|
||||
100
|
||||
).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
})}%)`
|
||||
);
|
||||
},
|
||||
icon: Icon.TextDecrease,
|
||||
},
|
||||
{
|
||||
onClick: () => setFontSize(1),
|
||||
onClick: () => {
|
||||
setFontSize(1);
|
||||
umami("[Settings] Change font size (100%)");
|
||||
},
|
||||
text: `${(fontSize * 100).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
})}%`,
|
||||
},
|
||||
{
|
||||
onClick: () => setFontSize(fontSize * 1.05),
|
||||
onClick: () => {
|
||||
setFontSize((current) => current * 1.05);
|
||||
umami(
|
||||
`[Settings] Change font size (${(
|
||||
fontSize *
|
||||
1.05 *
|
||||
100
|
||||
).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
})}%)`
|
||||
);
|
||||
},
|
||||
icon: Icon.TextIncrease,
|
||||
},
|
||||
]}
|
||||
|
@ -504,13 +543,19 @@ export const AppLayout = ({
|
|||
<div className="grid gap-2">
|
||||
<Button
|
||||
active={!dyslexic}
|
||||
onClick={() => setDyslexic(false)}
|
||||
onClick={() => {
|
||||
setDyslexic(false);
|
||||
umami("[Settings] Change font (Zen Maru Gothic)");
|
||||
}}
|
||||
className="font-zenMaruGothic"
|
||||
text="Zen Maru Gothic"
|
||||
/>
|
||||
<Button
|
||||
active={dyslexic}
|
||||
onClick={() => setDyslexic(true)}
|
||||
onClick={() => {
|
||||
setDyslexic(true);
|
||||
umami("[Settings] Change font (OpenDyslexic)");
|
||||
}}
|
||||
className="font-openDyslexic"
|
||||
text="OpenDyslexic"
|
||||
/>
|
||||
|
@ -523,7 +568,10 @@ export const AppLayout = ({
|
|||
placeholder="<player>"
|
||||
className="w-48"
|
||||
value={playerName}
|
||||
onChange={setPlayerName}
|
||||
onChange={(newName) => {
|
||||
setPlayerName(newName);
|
||||
umami("[Settings] Change username");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -40,7 +40,10 @@ export const LanguageSwitcher = ({
|
|||
<Fragment key={index}>
|
||||
<Button
|
||||
active={value === localesIndex}
|
||||
onClick={() => onLanguageChanged(value)}
|
||||
onClick={() => {
|
||||
onLanguageChanged(value);
|
||||
umami(`[Language Switcher] Switch language (${locale})`);
|
||||
}}
|
||||
text={prettyLanguage(locale, languages)}
|
||||
/>
|
||||
</Fragment>
|
||||
|
|
|
@ -38,9 +38,16 @@ export const MainPanel = (): JSX.Element => {
|
|||
"fixed top-1/2",
|
||||
cIf(mainPanelReduced, "left-[4.65rem]", "left-[18.65rem]")
|
||||
)}
|
||||
onClick={toggleMainPanelReduced}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (mainPanelReduced) {
|
||||
umami("[MainPanel] Expand");
|
||||
} else {
|
||||
umami("[MainPanel] Reduce");
|
||||
}
|
||||
toggleMainPanelReduced();
|
||||
}}
|
||||
className="bg-light !px-2"
|
||||
icon={mainPanelReduced ? Icon.ChevronRight : Icon.ChevronLeft}
|
||||
/>
|
||||
|
@ -83,24 +90,11 @@ export const MainPanel = (): JSX.Element => {
|
|||
<Button
|
||||
onClick={() => {
|
||||
setConfigPanelOpen(true);
|
||||
umami("[Settings] Open settings");
|
||||
}}
|
||||
icon={Icon.Settings}
|
||||
/>
|
||||
</ToolTip>
|
||||
|
||||
{/* <ToolTip
|
||||
content={<h3 className="text-2xl">{langui.open_search}</h3>}
|
||||
placement="right"
|
||||
className="text-left"
|
||||
disabled={!mainPanelReduced}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSearchPanelOpen(true);
|
||||
}}
|
||||
icon={Icon.Search}
|
||||
/>
|
||||
</ToolTip> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -193,6 +187,7 @@ export const MainPanel = (): JSX.Element => {
|
|||
)}
|
||||
<div className="mt-4 mb-8 grid place-content-center">
|
||||
<a
|
||||
onClick={() => umami("[MainPanel] Visit license")}
|
||||
aria-label="Read more about the license we use for this website"
|
||||
className="group grid grid-flow-col place-content-center gap-1 transition-[filter]"
|
||||
href="https://creativecommons.org/licenses/by-sa/4.0/"
|
||||
|
@ -222,6 +217,7 @@ export const MainPanel = (): JSX.Element => {
|
|||
<div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8">
|
||||
<a
|
||||
aria-label="Browse our GitHub repository, which include this website source code"
|
||||
onClick={() => umami("[MainPanel] Visit GitHub")}
|
||||
className="aspect-square w-10
|
||||
bg-black transition-colors [mask:url('/icons/github-brands.svg')]
|
||||
![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] hover:bg-dark"
|
||||
|
@ -231,6 +227,7 @@ export const MainPanel = (): JSX.Element => {
|
|||
></a>
|
||||
<a
|
||||
aria-label="Follow us on Twitter"
|
||||
onClick={() => umami("[MainPanel] Visit Twitter")}
|
||||
className="aspect-square w-10
|
||||
bg-black transition-colors [mask:url('/icons/twitter-brands.svg')]
|
||||
![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] hover:bg-dark"
|
||||
|
@ -240,6 +237,7 @@ export const MainPanel = (): JSX.Element => {
|
|||
></a>
|
||||
<a
|
||||
aria-label="Join our Discord server!"
|
||||
onClick={() => umami("[MainPanel] Visit Discord")}
|
||||
className="aspect-square w-10
|
||||
bg-black transition-colors [mask:url('/icons/discord-brands.svg')]
|
||||
![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] hover:bg-dark"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { Chip } from "./Chip";
|
||||
import { PageSelector } from "./Inputs/PageSelector";
|
||||
import { Ico, Icon } from "./Ico";
|
||||
|
@ -15,7 +16,7 @@ interface Group<T> {
|
|||
}
|
||||
|
||||
const defaultGroupSortingFunction = <T,>(a: Group<T>, b: Group<T>) =>
|
||||
a.name.localeCompare(b.name);
|
||||
naturalCompare(a.name, b.name);
|
||||
const defaultGroupCountingFunction = () => 1;
|
||||
const defaultFilteringFunction = () => true;
|
||||
const defaultSortingFunction = () => 0;
|
||||
|
|
|
@ -75,17 +75,19 @@ const AboutUs = (props: PostStaticProps): JSX.Element => {
|
|||
case "OKAY":
|
||||
setFormResponse(langui.response_email_success ?? "");
|
||||
setFormState("completed");
|
||||
|
||||
umami("[Contact] Send email (success)");
|
||||
break;
|
||||
|
||||
case "EENVELOPE":
|
||||
setFormResponse(langui.response_invalid_email ?? "");
|
||||
setFormState("stale");
|
||||
umami("[Contact] Send email (invalid email)");
|
||||
break;
|
||||
|
||||
default:
|
||||
setFormResponse(response.message ?? "");
|
||||
setFormState("stale");
|
||||
umami("[Contact] Send email (error)");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Select } from "components/Inputs/Select";
|
||||
import { Switch } from "components/Inputs/Switch";
|
||||
|
@ -17,7 +18,11 @@ 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 } from "helpers/others";
|
||||
import {
|
||||
filterDefined,
|
||||
filterHasAttributes,
|
||||
isDefinedAndNotEmpty,
|
||||
} from "helpers/others";
|
||||
import { GetContentsQuery } from "graphql/generated";
|
||||
import { SmartList } from "components/SmartList";
|
||||
import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable";
|
||||
|
@ -150,7 +155,14 @@ const Contents = ({ contents, ...otherProps }: Props): JSX.Element => {
|
|||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
umami("[Contents/All] Change search term");
|
||||
} else {
|
||||
umami("[News] Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WithLabel label={langui.group_by}>
|
||||
|
@ -158,14 +170,31 @@ const Contents = ({ contents, ...otherProps }: Props): JSX.Element => {
|
|||
className="w-full"
|
||||
options={[langui.category ?? "Category", langui.type ?? "Type"]}
|
||||
value={groupingMethod}
|
||||
onChange={setGroupingMethod}
|
||||
onChange={(value) => {
|
||||
setGroupingMethod(value);
|
||||
umami(
|
||||
`[Contents/All] Change grouping method (${
|
||||
["none", "category", "type"][value + 1]
|
||||
})`
|
||||
);
|
||||
}}
|
||||
allowEmpty
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
umami(
|
||||
`[Contents/All] Always ${
|
||||
keepInfoVisible ? "hide" : "show"
|
||||
} info`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
|
@ -177,6 +206,7 @@ const Contents = ({ contents, ...otherProps }: Props): JSX.Element => {
|
|||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
umami("[Contents/All] Reset all filters");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
|
@ -299,7 +329,7 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|||
contents.contents.data.sort((a, b) => {
|
||||
const titleA = a.attributes?.slug ?? "";
|
||||
const titleB = b.attributes?.slug ?? "";
|
||||
return titleA.localeCompare(titleB);
|
||||
return naturalCompare(titleA, titleB);
|
||||
});
|
||||
|
||||
const props: Props = {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import {
|
||||
ContentPanel,
|
||||
|
@ -237,6 +238,23 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|||
|
||||
const folder = contentsFolder.contentsFolders.data[0].attributes;
|
||||
|
||||
const subFolders = {
|
||||
// eslint-disable-next-line id-denylist
|
||||
data: filterHasAttributes(folder.subfolders?.data, [
|
||||
"attributes.slug",
|
||||
]).sort((a, b) => naturalCompare(a.attributes.slug, b.attributes.slug)),
|
||||
};
|
||||
|
||||
const contents = {
|
||||
// eslint-disable-next-line id-denylist
|
||||
data: filterHasAttributes(folder.contents?.data, ["attributes.slug"]).sort(
|
||||
(a, b) => naturalCompare(a.attributes.slug, b.attributes.slug)
|
||||
),
|
||||
};
|
||||
|
||||
folder.contents = contents;
|
||||
folder.subfolders = subFolders;
|
||||
|
||||
const title = (() => {
|
||||
if (slug === "root") {
|
||||
return langui.contents ?? "Contents";
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export {};
|
||||
|
||||
declare global {
|
||||
function umami(eventName: string): void;
|
||||
}
|
|
@ -1,15 +1,19 @@
|
|||
import { PostPage } from "components/PostPage";
|
||||
import { useAppLayout } from "contexts/AppLayoutContext";
|
||||
import {
|
||||
getPostStaticProps,
|
||||
PostStaticProps,
|
||||
} from "graphql/getPostStaticProps";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
||||
*/
|
||||
|
||||
const Home = ({ ...otherProps }: PostStaticProps): JSX.Element => (
|
||||
const Home = ({ ...otherProps }: PostStaticProps): JSX.Element => {
|
||||
const { langui } = useAppLayout();
|
||||
return (
|
||||
<PostPage
|
||||
{...otherProps}
|
||||
prependBody={
|
||||
|
@ -25,9 +29,11 @@ const Home = ({ ...otherProps }: PostStaticProps): JSX.Element => (
|
|||
</div>
|
||||
}
|
||||
displayTitle={false}
|
||||
openGraph={getOpenGraph(langui)}
|
||||
displayLanguageSwitcher
|
||||
/>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
|
|
|
@ -471,7 +471,7 @@ const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
|
|||
)}
|
||||
>
|
||||
<h3 className="text-xl">{langui.type_information}</h3>
|
||||
<div className="grid w-full grid-cols-2 place-content-between">
|
||||
<div className="flex flex-wrap place-content-between gap-x-8">
|
||||
{item.metadata?.[0]?.__typename ===
|
||||
"ComponentMetadataBooks" && (
|
||||
<>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
||||
import { Fragment, useCallback, useEffect, useMemo } from "react";
|
||||
import { Fragment, useCallback, useMemo } from "react";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { ReturnButton } from "components/PanelComponents/ReturnButton";
|
||||
import {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { GetStaticProps } from "next";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
||||
import { Select } from "components/Inputs/Select";
|
||||
import { Switch } from "components/Inputs/Switch";
|
||||
|
@ -23,7 +24,12 @@ import { isUntangibleGroupItem } from "helpers/libraryItem";
|
|||
import { PreviewCard } from "components/PreviewCard";
|
||||
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
|
||||
import { ButtonGroup } from "components/Inputs/ButtonGroup";
|
||||
import { filterHasAttributes, isDefined, isUndefined } from "helpers/others";
|
||||
import {
|
||||
filterHasAttributes,
|
||||
isDefined,
|
||||
isDefinedAndNotEmpty,
|
||||
isUndefined,
|
||||
} from "helpers/others";
|
||||
import { useAppLayout } from "contexts/AppLayoutContext";
|
||||
import { convertPrice } from "helpers/numbers";
|
||||
import { SmartList } from "components/SmartList";
|
||||
|
@ -162,7 +168,7 @@ const Library = ({ items, ...otherProps }: Props): JSX.Element => {
|
|||
b.attributes.title,
|
||||
b.attributes.subtitle
|
||||
);
|
||||
return titleA.localeCompare(titleB);
|
||||
return naturalCompare(titleA, titleB);
|
||||
}
|
||||
case 1: {
|
||||
const priceA = a.attributes.price
|
||||
|
@ -269,7 +275,14 @@ const Library = ({ items, ...otherProps }: Props): JSX.Element => {
|
|||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
umami("[Library] Change search term");
|
||||
} else {
|
||||
umami("[Library] Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WithLabel label={langui.group_by}>
|
||||
|
@ -281,7 +294,14 @@ const Library = ({ items, ...otherProps }: Props): JSX.Element => {
|
|||
langui.release_year ?? "Year",
|
||||
]}
|
||||
value={groupingMethod}
|
||||
onChange={setGroupingMethod}
|
||||
onChange={(value) => {
|
||||
setGroupingMethod(value);
|
||||
umami(
|
||||
`[Library] Change grouping method (${
|
||||
["none", "category", "type", "year"][value + 1]
|
||||
})`
|
||||
);
|
||||
}}
|
||||
allowEmpty
|
||||
/>
|
||||
</WithLabel>
|
||||
|
@ -295,28 +315,64 @@ const Library = ({ items, ...otherProps }: Props): JSX.Element => {
|
|||
langui.release_date ?? "Release date",
|
||||
]}
|
||||
value={sortingMethod}
|
||||
onChange={setSortingMethod}
|
||||
onChange={(value) => {
|
||||
setSortingMethod(value);
|
||||
umami(
|
||||
`[Library] Change sorting method (${
|
||||
["name", "price", "release date"][value]
|
||||
})`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.show_subitems}>
|
||||
<Switch value={showSubitems} onClick={toggleShowSubitems} />
|
||||
<Switch
|
||||
value={showSubitems}
|
||||
onClick={() => {
|
||||
toggleShowSubitems();
|
||||
umami(`[Library] ${showSubitems ? "Hide" : "Show"} subitems`);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.show_primary_items}>
|
||||
<Switch value={showPrimaryItems} onClick={toggleShowPrimaryItems} />
|
||||
<Switch
|
||||
value={showPrimaryItems}
|
||||
onClick={() => {
|
||||
toggleShowPrimaryItems();
|
||||
umami(
|
||||
`[Library] ${showPrimaryItems ? "Hide" : "Show"} primary items`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel label={langui.show_secondary_items}>
|
||||
<Switch
|
||||
value={showSecondaryItems}
|
||||
onClick={toggleShowSecondaryItems}
|
||||
onClick={() => {
|
||||
toggleShowSecondaryItems();
|
||||
umami(
|
||||
`[Library] ${
|
||||
showSecondaryItems ? "Hide" : "Show"
|
||||
} secondary items`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
umami(
|
||||
`[Library] Always ${keepInfoVisible ? "hide" : "show"} info`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
|
@ -326,25 +382,37 @@ const Library = ({ items, ...otherProps }: Props): JSX.Element => {
|
|||
{
|
||||
tooltip: langui.only_display_items_i_want,
|
||||
icon: Icon.Favorite,
|
||||
onClick: () => setFilterUserStatus(LibraryItemUserStatus.Want),
|
||||
onClick: () => {
|
||||
setFilterUserStatus(LibraryItemUserStatus.Want);
|
||||
umami("[Library] Set filter status (I want)");
|
||||
},
|
||||
active: filterUserStatus === LibraryItemUserStatus.Want,
|
||||
},
|
||||
{
|
||||
tooltip: langui.only_display_items_i_have,
|
||||
icon: Icon.BackHand,
|
||||
onClick: () => setFilterUserStatus(LibraryItemUserStatus.Have),
|
||||
onClick: () => {
|
||||
setFilterUserStatus(LibraryItemUserStatus.Have);
|
||||
umami("[Library] Set filter status (I have)");
|
||||
},
|
||||
active: filterUserStatus === LibraryItemUserStatus.Have,
|
||||
},
|
||||
{
|
||||
tooltip: langui.only_display_unmarked_items,
|
||||
icon: Icon.RadioButtonUnchecked,
|
||||
onClick: () => setFilterUserStatus(LibraryItemUserStatus.None),
|
||||
onClick: () => {
|
||||
setFilterUserStatus(LibraryItemUserStatus.None);
|
||||
umami("[Library] Set filter status (unmarked)");
|
||||
},
|
||||
active: filterUserStatus === LibraryItemUserStatus.None,
|
||||
},
|
||||
{
|
||||
tooltip: langui.only_display_unmarked_items,
|
||||
text: langui.all,
|
||||
onClick: () => setFilterUserStatus(undefined),
|
||||
onClick: () => {
|
||||
setFilterUserStatus(undefined);
|
||||
umami("[Library] Set filter status (all)");
|
||||
},
|
||||
active: isUndefined(filterUserStatus),
|
||||
},
|
||||
]}
|
||||
|
@ -363,6 +431,7 @@ const Library = ({ items, ...otherProps }: Props): JSX.Element => {
|
|||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
setFilterUserStatus(DEFAULT_FILTERS_STATE.filterUserStatus);
|
||||
umami("[Library] Reset all filters");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
|
|
|
@ -17,7 +17,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 } from "helpers/others";
|
||||
import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { SmartList } from "components/SmartList";
|
||||
import { getOpenGraph } from "helpers/openGraph";
|
||||
import { compareDate } from "helpers/date";
|
||||
|
@ -75,12 +75,27 @@ const News = ({ posts, ...otherProps }: Props): JSX.Element => {
|
|||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
umami("[News] Change search term");
|
||||
} else {
|
||||
umami("[News] Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
umami(
|
||||
`[News] Always ${keepInfoVisible ? "hide" : "show"} info`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
|
@ -91,6 +106,7 @@ const News = ({ posts, ...otherProps }: Props): JSX.Element => {
|
|||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
umami("[News] Reset all filters");
|
||||
}}
|
||||
/>
|
||||
</SubPanel>
|
||||
|
|
|
@ -18,7 +18,11 @@ 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 } from "helpers/others";
|
||||
import {
|
||||
filterDefined,
|
||||
filterHasAttributes,
|
||||
isDefinedAndNotEmpty,
|
||||
} from "helpers/others";
|
||||
import { SmartList } from "components/SmartList";
|
||||
import { Select } from "components/Inputs/Select";
|
||||
import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable";
|
||||
|
@ -84,7 +88,14 @@ const Wiki = ({ pages, ...otherProps }: Props): JSX.Element => {
|
|||
className="mb-6 w-full"
|
||||
placeholder={langui.search_title ?? "Search..."}
|
||||
value={searchName}
|
||||
onChange={setSearchName}
|
||||
onChange={(name) => {
|
||||
setSearchName(name);
|
||||
if (isDefinedAndNotEmpty(name)) {
|
||||
umami("[Wiki] Change search term");
|
||||
} else {
|
||||
umami("[Wiki] Clear search term");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<WithLabel label={langui.group_by}>
|
||||
|
@ -92,14 +103,29 @@ const Wiki = ({ pages, ...otherProps }: Props): JSX.Element => {
|
|||
className="w-full"
|
||||
options={[langui.category ?? "Category"]}
|
||||
value={groupingMethod}
|
||||
onChange={setGroupingMethod}
|
||||
onChange={(value) => {
|
||||
setGroupingMethod(value);
|
||||
umami(
|
||||
`[Wiki] Change grouping method (${
|
||||
["none", "category"][value + 1]
|
||||
})`
|
||||
);
|
||||
}}
|
||||
allowEmpty
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
{hoverable && (
|
||||
<WithLabel label={langui.always_show_info}>
|
||||
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
|
||||
<Switch
|
||||
value={keepInfoVisible}
|
||||
onClick={() => {
|
||||
toggleKeepInfoVisible();
|
||||
umami(
|
||||
`[Wiki] Always ${keepInfoVisible ? "hide" : "show"} info`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
|
||||
|
@ -109,7 +135,9 @@ const Wiki = ({ pages, ...otherProps }: Props): JSX.Element => {
|
|||
icon={Icon.Replay}
|
||||
onClick={() => {
|
||||
setSearchName(DEFAULT_FILTERS_STATE.searchName);
|
||||
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
|
||||
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
|
||||
umami("[Wiki] Reset all filters");
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
|
@ -164,7 +164,7 @@ input[type="submit"] {
|
|||
@apply grid cursor-pointer place-content-center place-items-center rounded-full border-[1px]
|
||||
border-dark px-4 pt-[0.4rem] pb-[0.5rem] text-dark transition-all hover:bg-dark hover:text-light
|
||||
hover:drop-shadow-shade-lg active:border-black active:bg-black active:text-light
|
||||
active:drop-shadow-black-lg;
|
||||
active:drop-shadow-black-lg outline-none;
|
||||
}
|
||||
|
||||
.texture-paper-dots {
|
||||
|
|
Loading…
Reference in New Issue