mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-22 02:16:45 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import clsx from "clsx";
|
||||
import { Check } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Divider } from "~/components/Divider";
|
||||
import { ModeImage } from "~/components/Image";
|
||||
import { BANNED_MAPS } from "~/features/match-profile/banned-maps";
|
||||
import { shortStageName, stageIds } from "~/modules/in-game-lists/stage-ids";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import { nullFilledArray } from "~/utils/arrays";
|
||||
import { stageImageUrl } from "~/utils/urls";
|
||||
import styles from "./ModeMapPoolPicker.module.css";
|
||||
|
||||
export function ModeMapPoolPicker({
|
||||
mode,
|
||||
amountToPick,
|
||||
pool,
|
||||
tiebreaker,
|
||||
onChange,
|
||||
modeTabs,
|
||||
onModeChange,
|
||||
disabled,
|
||||
}: {
|
||||
mode: ModeShort;
|
||||
amountToPick: number;
|
||||
pool: StageId[];
|
||||
tiebreaker?: StageId;
|
||||
onChange: (stages: StageId[]) => void;
|
||||
/** When provided, the divider becomes a tab switcher between these modes. */
|
||||
modeTabs?: ModeShort[];
|
||||
onModeChange?: (mode: ModeShort) => void;
|
||||
/** When true, stages can't be picked or removed (view-only). */
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const [wigglingStageId, setWigglingStageId] = React.useState<StageId | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const stages: (StageId | null)[] = [
|
||||
...pool,
|
||||
...nullFilledArray(amountToPick - pool.length),
|
||||
];
|
||||
|
||||
const handlePickedStageClick = (stageId: StageId) => {
|
||||
onChange(pool.filter((s) => s !== stageId));
|
||||
};
|
||||
|
||||
const handleUnpickedStageClick = (stageId: StageId) => {
|
||||
// is there space left?
|
||||
if (stages[amountToPick - 1] !== null) {
|
||||
setWigglingStageId(stageId);
|
||||
return;
|
||||
}
|
||||
|
||||
// was it already picked?
|
||||
if (pool.includes(stageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange([...pool, stageId].sort((a, b) => a - b));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.container, "stack sm")}>
|
||||
<div className="stack sm horizontal justify-center">
|
||||
{nullFilledArray(amountToPick).map((_, index) => {
|
||||
return (
|
||||
<MapSlot
|
||||
key={index}
|
||||
number={index + 1}
|
||||
picked={stages[index] !== null}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Divider className={styles.divider}>
|
||||
{modeTabs && onModeChange ? (
|
||||
<div className={styles.modeTabs}>
|
||||
{modeTabs.map((tabMode) => {
|
||||
const active = tabMode === mode;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tabMode}
|
||||
type="button"
|
||||
className={clsx(styles.modeTab, {
|
||||
[styles.modeTabActive]: active,
|
||||
})}
|
||||
onClick={() => onModeChange(tabMode)}
|
||||
aria-pressed={active}
|
||||
data-testid={`map-pool-mode-tab-${tabMode}`}
|
||||
>
|
||||
<ModeImage mode={tabMode} size={24} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<ModeImage mode={mode} size={32} />
|
||||
)}
|
||||
</Divider>
|
||||
<div className="stack sm horizontal flex-wrap justify-center mt-1">
|
||||
{stageIds.map((stageId) => {
|
||||
const isTiebreaker = tiebreaker === stageId;
|
||||
const banned = BANNED_MAPS[mode].includes(stageId);
|
||||
const selected = stages.includes(stageId);
|
||||
|
||||
const onClick = () => {
|
||||
if (disabled) return;
|
||||
if (isTiebreaker) return;
|
||||
if (banned) return;
|
||||
if (selected) return handlePickedStageClick(stageId);
|
||||
|
||||
handleUnpickedStageClick(stageId);
|
||||
};
|
||||
|
||||
return (
|
||||
<MapButton
|
||||
key={stageId}
|
||||
stageId={stageId}
|
||||
onClick={onClick}
|
||||
selected={selected}
|
||||
banned={banned}
|
||||
tiebreaker={isTiebreaker}
|
||||
wiggle={wigglingStageId === stageId}
|
||||
onWiggleEnd={() => setWigglingStageId(null)}
|
||||
disabled={disabled}
|
||||
testId={`map-pool-${mode}-${stageId}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MapSlot({ number, picked }: { number: number; picked: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.slot, {
|
||||
[styles.slotPicked]: picked,
|
||||
})}
|
||||
>
|
||||
{picked ? <Check className={styles.slotIcon} /> : number}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MapButton({
|
||||
stageId,
|
||||
onClick,
|
||||
selected,
|
||||
banned,
|
||||
tiebreaker,
|
||||
wiggle,
|
||||
onWiggleEnd,
|
||||
disabled,
|
||||
testId,
|
||||
}: {
|
||||
stageId: StageId;
|
||||
onClick: () => void;
|
||||
selected?: boolean;
|
||||
banned?: boolean;
|
||||
tiebreaker?: boolean;
|
||||
wiggle?: boolean;
|
||||
onWiggleEnd?: () => void;
|
||||
disabled?: boolean;
|
||||
testId: string;
|
||||
}) {
|
||||
const { t } = useTranslation(["game-misc"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("stack items-center relative", styles.mapButtonContainer)}
|
||||
>
|
||||
<button
|
||||
className={clsx(styles.mapButton, {
|
||||
[styles.mapButtonWiggle]: wiggle,
|
||||
[styles.mapButtonGreyedOut]: selected || banned || tiebreaker,
|
||||
})}
|
||||
style={{ "--map-image-url": `url("${stageImageUrl(stageId)}.avif")` }}
|
||||
onClick={onClick}
|
||||
onAnimationEnd={wiggle ? onWiggleEnd : undefined}
|
||||
disabled={disabled || banned}
|
||||
type="button"
|
||||
data-testid={testId}
|
||||
/>
|
||||
{selected ? (
|
||||
<Check
|
||||
className={styles.mapButtonIcon}
|
||||
onClick={onClick}
|
||||
data-testid={`${testId}-picked`}
|
||||
/>
|
||||
) : null}
|
||||
{tiebreaker ? (
|
||||
<div className={styles.mapButtonText}>Tiebreak</div>
|
||||
) : banned ? (
|
||||
<div className={clsx(styles.mapButtonText, "text-error")}>Banned</div>
|
||||
) : null}
|
||||
<div className={styles.mapButtonLabel}>
|
||||
{shortStageName(t(`game-misc:STAGE_${stageId}`))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user