sendou.ink/app/components/BuildCard.tsx
William Lam 924ae8bfff
Fix abilities appearing clickable when they aren't (#1057)
* Issue 1036: non-draggable ability icons will no longer have its cursor morphed to a pointing hand icon

* Revert "Issue 1036: non-draggable ability icons will no longer have its cursor morphed to a pointing hand icon"

This reverts commit a0de4e0047.

* Issue 1036: Non-draggable ability icons will no longer have its cursor morphed to a pointing hand icon.
- Done correctly by conditionally choosing to render a <button /> or <div /> HTML element for ability icons everywhere Ability() was invoked by passing in readonly

* Removed stray console.warn() line, added a brief comment

* Removed unnecessary tabIndex adjustment for AbilityTag element if it is readonly (since it is no longer rendered as a button if it is readonly)
- We now also condtionally give type="button" to AbilityTag only if it is actually a button

* Conditionally set AbilitiesSelector's Ability JSX elements to readonly if the ability is "UNKNOWN"

* Fixed failed check for Prettier in CI pipeline

* Ability component now only chooses the readonly class if no onClick function is passed into it
- Exception: if the ability is "UNKNOWN", it will be forced to readonly

* Fixed Prettier error in CI pipeline

* Fixed Typecheck error in BuildCard.tsx

* Fixed Prettier CI pipeline error again

* Was checking for an undefined type incorrectly. Now the logic works properly

* Fixed Prettier error

* Cleaned up classname declaration syntax to React's shorthand
2022-10-30 10:38:03 +02:00

225 lines
6.0 KiB
TypeScript

import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import type { Build, BuildWeapon, GearType, User } from "~/db/types";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useUser } from "~/modules/auth";
import type {
Ability as AbilityType,
ModeShort,
} from "~/modules/in-game-lists";
import type { BuildAbilitiesTuple } from "~/modules/in-game-lists/types";
import { databaseTimestampToDate } from "~/utils/dates";
import { discordFullName, gearTypeToInitial } from "~/utils/strings";
import {
analyzerPage,
gearImageUrl,
mainWeaponImageUrl,
modeImageUrl,
navIconUrl,
userBuildsPage,
} from "~/utils/urls";
import { Ability } from "./Ability";
import { Button, LinkButton } from "./Button";
import { FormWithConfirm } from "./FormWithConfirm";
import { TrashIcon } from "./icons/Trash";
import { EditIcon } from "./icons/Edit";
import { Image } from "./Image";
import { Popover } from "./Popover";
import { InfoIcon } from "./icons/Info";
interface BuildProps {
build: Pick<
Build,
| "id"
| "title"
| "description"
| "clothesGearSplId"
| "headGearSplId"
| "shoesGearSplId"
| "updatedAt"
> & {
abilities: BuildAbilitiesTuple;
modes: ModeShort[] | null;
weapons: Array<BuildWeapon["weaponSplId"]>;
};
owner?: Pick<User, "discordId" | "discordDiscriminator" | "discordName">;
canEdit?: boolean;
}
export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
const user = useUser();
const { t } = useTranslation(["weapons", "builds", "common"]);
const { i18n } = useTranslation();
const isMounted = useIsMounted();
const {
id,
title,
description,
clothesGearSplId,
headGearSplId,
shoesGearSplId,
updatedAt,
abilities,
modes,
weapons,
} = build;
return (
<div className="build">
<div className="stack xxs">
<div className="build__top-row">
<h2 className="build__title">{title}</h2>
{modes && modes.length > 0 && (
<div className="build__modes">
{modes.map((mode) => (
<Image
key={mode}
alt=""
path={modeImageUrl(mode)}
width={18}
height={18}
/>
))}
</div>
)}
</div>
<div className="build__date-author-row">
{owner && (
<>
<Link to={userBuildsPage(owner)}>{discordFullName(owner)}</Link>
<div></div>
</>
)}
<time className={clsx({ invisible: !isMounted })}>
{isMounted
? databaseTimestampToDate(updatedAt).toLocaleDateString(
i18n.language,
{
day: "numeric",
month: "numeric",
year: "numeric",
}
)
: "t"}
</time>
</div>
</div>
<div className="build__weapons">
{weapons.map((weaponSplId) => (
<div key={weaponSplId} className="build__weapon">
<Image
path={mainWeaponImageUrl(weaponSplId)}
alt={t(`weapons:MAIN_${weaponSplId}` as any)}
title={t(`weapons:MAIN_${weaponSplId}` as any)}
height={36}
width={36}
/>
</div>
))}
{weapons.length === 1 && (
<div className="build__weapon-text">
{t(`weapons:MAIN_${weapons[0]!}` as any)}
</div>
)}
</div>
<div className="build__gear-abilities">
<AbilitiesRowWithGear
gearType="HEAD"
abilities={abilities[0]}
gearId={headGearSplId}
/>
<AbilitiesRowWithGear
gearType="CLOTHES"
abilities={abilities[1]}
gearId={clothesGearSplId}
/>
<AbilitiesRowWithGear
gearType="SHOES"
abilities={abilities[2]}
gearId={shoesGearSplId}
/>
</div>
<div className="build__bottom-row">
<Link
to={analyzerPage({
weaponId: weapons[0]!,
abilities: abilities.flat(),
})}
>
<Image
alt={t("common:pages.analyzer")}
className="build__icon"
path={navIconUrl("analyzer")}
/>
</Link>
{description && (
<Popover
buttonChildren={<InfoIcon className="build__icon" />}
triggerClassName="minimal tiny build__small-text"
>
{description}
</Popover>
)}
{canEdit && (
<>
<LinkButton
className="build__small-text"
variant="minimal"
tiny
to={`new?buildId=${id}&userId=${user!.id}`}
>
<EditIcon className="build__icon" />
</LinkButton>
<FormWithConfirm
dialogHeading={t("builds:deleteConfirm", { title })}
fields={[["buildToDeleteId", id]]}
>
<Button
className="build__small-text"
variant="minimal-destructive"
tiny
type="submit"
>
<TrashIcon className="build__icon" />
</Button>
</FormWithConfirm>
</>
)}
</div>
</div>
);
}
function AbilitiesRowWithGear({
gearType,
abilities,
gearId,
}: {
gearType: GearType;
abilities: AbilityType[];
gearId: number;
}) {
const { t } = useTranslation(["gear"]);
const translatedGearName = t(
`gear:${gearTypeToInitial(gearType)}_${gearId}` as any
);
return (
<>
<Image
height={64}
width={64}
alt={translatedGearName}
title={translatedGearName}
path={gearImageUrl(gearType, gearId)}
className="build__gear"
/>
{abilities.map((ability, i) => (
<Ability key={i} ability={ability} size={i === 0 ? "MAIN" : "SUB"} />
))}
</>
);
}