sendou.ink/pages/maps.tsx
2021-02-09 01:29:13 +02:00

397 lines
12 KiB
TypeScript

import {
Alert,
AlertIcon,
Box,
Button,
Checkbox,
FormLabel,
Grid,
Input,
InputGroup,
InputRightElement,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Radio,
RadioGroup,
Stack,
Textarea,
} from "@chakra-ui/react";
import { t, Trans } from "@lingui/macro";
import { useLingui } from "@lingui/react";
import { RankedMode } from "@prisma/client";
import Breadcrumbs from "components/common/Breadcrumbs";
import ModeImage from "components/common/ModeImage";
import SubText from "components/common/SubText";
import { stages } from "lib/lists/stages";
import { setManySearchParams } from "lib/setSearchParams";
import { shuffleArray } from "lib/shuffleArray";
import { useRouter } from "next/router";
import { ChangeEvent, Fragment, useEffect, useState } from "react";
import { FiCheck, FiFilter, FiRotateCw } from "react-icons/fi";
const MapsGeneratorPage = () => {
const router = useRouter();
const { i18n } = useLingui();
const [stagesSelected, setStagesSelected] = useState<
Record<string, RankedMode[]>
>(getInitialStages());
const [generationMode, setGenerationMode] = useState<
"EQUAL" | "SZ_EVERY_OTHER"
>("EQUAL");
const [maplist, setMaplist] = useState("");
const [count, setCount] = useState(9);
const [editing, setEditing] = useState(false);
const [copied, setCopied] = useState<null | "URL" | "LIST">(null);
function getInitialStages() {
const filtersFromUrl = Object.entries(router.query).reduce(
(acc: Record<string, RankedMode[]>, cur) => {
if (stages.includes(cur[0]) && typeof cur[1] === "string") {
// @ts-ignore
acc[cur[0]] = cur[1]
.split(",")
.filter((mode) => ["SZ", "TC", "RM", "CB"].includes(mode));
}
return acc;
},
{}
);
return Object.keys(filtersFromUrl).length
? filtersFromUrl
: stages.reduce((acc: Record<string, RankedMode[]>, cur) => {
acc[cur] = ["SZ", "TC", "RM", "CB"];
return acc;
}, {});
}
const poolForUrl = (stagesSelectedForUrl: Record<string, string[]>) => {
return Object.entries(stagesSelectedForUrl).map(([key, stages]) => ({
key,
value: stages.join(","),
}));
};
useEffect(() => {
const maplist = localStorage.getItem("maplist");
if (!maplist) return;
setMaplist(maplist);
}, []);
useEffect(() => {
if (!copied) return;
const timer = setTimeout(() => {
setCopied(null);
}, 750);
return () => clearTimeout(timer);
}, [copied]);
const handleChange = (
e: ChangeEvent<HTMLInputElement>,
mode: RankedMode,
stage: string
) => {
const oldArray = stagesSelected[stage] ?? [];
const newArray = e.target.checked
? oldArray.concat(mode)
: oldArray.filter((modeInArray) => modeInArray !== mode);
const newStagesSelected = { ...stagesSelected, [stage]: newArray };
setStagesSelected(newStagesSelected);
setManySearchParams(poolForUrl(newStagesSelected), true);
};
const generateMaps = () => {
let modeStages = Object.entries(stagesSelected).reduce(
(acc: Record<RankedMode, string[]>, [stage, modes]) => {
modes.forEach((mode) => acc[mode].push(stage));
return acc;
},
{ SZ: [], TC: [], RM: [], CB: [] }
);
modeStages = {
SZ: shuffleArray(modeStages.SZ),
TC: shuffleArray(modeStages.TC),
RM: shuffleArray(modeStages.RM),
CB: shuffleArray(modeStages.CB),
};
const modesFromGenerationMode =
generationMode === "SZ_EVERY_OTHER"
? ["TC", "RM", "CB"]
: ["SZ", "TC", "RM", "CB"];
const modes = (shuffleArray(
modesFromGenerationMode
) as RankedMode[]).filter((mode) => modeStages[mode].length > 0);
if (modes.length === 0) {
return "I can't generate a maplist without any maps in it you know.";
}
const stagesAlreadyPicked = new Set<string>();
const isSZFirst = false;
return new Array(count)
.fill(null)
.map((_, i) => {
let modeOfRound: RankedMode = "SZ";
if (
generationMode !== "SZ_EVERY_OTHER" ||
i % 2 === Number(isSZFirst)
) {
modes.push(modes.shift() as RankedMode);
modeOfRound = modes[0];
}
const stageArray = modeStages[modeOfRound];
stageArray.push(stageArray.shift() as string);
let shifted = 0;
while (
stagesAlreadyPicked.has(stageArray[0]) &&
shifted < stageArray.length
) {
stageArray.push(stageArray.shift() as string);
shifted++;
}
stagesAlreadyPicked.add(stageArray[0]);
return `${i + 1}) ${modeOfRound} on ${stageArray[0]}`;
})
.join("\n");
};
return (
<>
<Breadcrumbs pages={[{ name: t`Maplist Generator` }]} />
{editing ? (
<>
<Alert status="info" mb={8}>
<AlertIcon />
<Trans>
Pro tip: bookmark this page after making your map list to save it
</Trans>
</Alert>
<Grid
templateColumns="2fr 2fr 1fr 1fr 1fr 1fr"
rowGap={4}
placeItems="center"
mx={["-20px", "0"]}
>
<Box />
<Box />
<ModeImage mode="SZ" />
<ModeImage mode="TC" />
<ModeImage mode="RM" />
<ModeImage mode="CB" />
{stages.map((stage) => {
const buttonIsAdd = (stagesSelected[stage]?.length ?? 0) < 4;
return (
<Fragment key={stage}>
<SubText textAlign="center">{i18n._(stage)}</SubText>
<Button
size="sm"
variant="outline"
colorScheme={buttonIsAdd ? "theme" : "red"}
onClick={() => {
const newStagesSelected = {
...stagesSelected,
[stage]: ["SZ", "TC", "RM", "CB"] as RankedMode[],
};
if (!buttonIsAdd) delete newStagesSelected[stage];
setStagesSelected(newStagesSelected);
setManySearchParams(poolForUrl(newStagesSelected), true);
}}
>
{buttonIsAdd ? <Trans>All</Trans> : <Trans>Clear</Trans>}
</Button>
<Checkbox
value="SZ"
isChecked={(stagesSelected[stage] ?? []).includes("SZ")}
onChange={(e) => handleChange(e, "SZ", stage)}
/>
<Checkbox
value="TC"
isChecked={(stagesSelected[stage] ?? []).includes("TC")}
onChange={(e) => handleChange(e, "TC", stage)}
/>
<Checkbox
value="RM"
isChecked={(stagesSelected[stage] ?? []).includes("RM")}
onChange={(e) => handleChange(e, "RM", stage)}
/>
<Checkbox
value="CB"
isChecked={(stagesSelected[stage] ?? []).includes("CB")}
onChange={(e) => handleChange(e, "CB", stage)}
/>
</Fragment>
);
})}
</Grid>
<FormLabel htmlFor="share" mt={4}>
<Trans>Share your map pool</Trans>
</FormLabel>
{window && (
<InputGroup size="md" mb={8}>
<Input name="share" value={window.location.href} readOnly />
<InputRightElement width="4.5rem">
<Button
onClick={() => {
navigator.clipboard.writeText(window.location.href);
setCopied("URL");
}}
h="1.75rem"
size="sm"
disabled={copied === "URL"}
>
{copied === "URL" ? <FiCheck /> : <Trans>Copy</Trans>}
</Button>
</InputRightElement>
</InputGroup>
)}
</>
) : (
<Grid
my={8}
templateColumns="repeat(4, 1fr)"
rowGap={4}
columnGap={4}
display={["none", null, "grid"]}
>
<Box textAlign="center">
<ModeImage mode="SZ" />
</Box>
<Box textAlign="center">
<ModeImage mode="TC" />
</Box>
<Box textAlign="center">
<ModeImage mode="RM" />
</Box>
<Box textAlign="center">
<ModeImage mode="CB" />
</Box>
<Box textAlign="center">
{Object.entries(stagesSelected).map(([stage, modes]) =>
modes.includes("SZ") ? (
<SubText key={stage}>{stage}</SubText>
) : null
)}
</Box>
<Box textAlign="center">
{Object.entries(stagesSelected).map(([stage, modes]) =>
modes.includes("TC") ? (
<SubText key={stage}>{stage}</SubText>
) : null
)}
</Box>
<Box textAlign="center">
{Object.entries(stagesSelected).map(([stage, modes]) =>
modes.includes("RM") ? (
<SubText key={stage}>{stage}</SubText>
) : null
)}
</Box>
<Box textAlign="center">
{Object.entries(stagesSelected).map(([stage, modes]) =>
modes.includes("CB") ? (
<SubText key={stage}>{stage}</SubText>
) : null
)}
</Box>
</Grid>
)}
<Stack direction={["column", "row"]} spacing={4} mb={4}>
<Button
leftIcon={<FiRotateCw />}
onClick={() => {
const maplist = generateMaps();
setMaplist(maplist);
localStorage.setItem("maplist", maplist);
}}
>
<Trans>Generate maps</Trans>
</Button>
<Button
leftIcon={<FiFilter />}
variant="outline"
onClick={() => setEditing(!editing)}
>
{editing ? <Trans>Hide maps</Trans> : <Trans>Change map pool</Trans>}
</Button>
</Stack>
<RadioGroup
onChange={(value) =>
setGenerationMode(value as "EQUAL" | "SZ_EVERY_OTHER")
}
value={generationMode}
>
<Stack direction="row" mb={4}>
<Radio value="EQUAL">
<Trans>All modes equally</Trans>
</Radio>
<Radio value="SZ_EVERY_OTHER">
<Trans>SZ every other</Trans>
</Radio>
</Stack>
</RadioGroup>
<FormLabel htmlFor="count" fontSize="sm">
<Trans>Amount of maps to generate</Trans>
</FormLabel>
<NumberInput
name="count"
size="sm"
value={count}
min={1}
max={100}
onChange={(_, value) => {
if (!Number.isNaN(value)) setCount(value);
}}
mb={4}
width={24}
>
<NumberInputField />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
{maplist && (
<>
<Textarea value={maplist} readOnly rows={9} />
<Button
mt={2}
variant="outline"
size="sm"
width={16}
disabled={copied === "LIST"}
onClick={() => {
navigator.clipboard.writeText(maplist);
setCopied("LIST");
}}
>
{copied === "LIST" ? <FiCheck /> : <Trans>Copy</Trans>}
</Button>
</>
)}
</>
);
};
export default MapsGeneratorPage;