mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Planned to be replaced with Playwright maybe? Just removing in the meanwhile so they don't confuse people. Or that people won't accidentally develop new.
230 lines
6.1 KiB
TypeScript
230 lines
6.1 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"}
|
|
readonly
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|