Exported images w/ SQ season summary downloadable (#3272)

This commit is contained in:
Kalle
2026-07-31 17:34:49 +03:00
committed by GitHub
parent 83660a5b2f
commit 97619bdc05
114 changed files with 5780 additions and 292 deletions

View File

@@ -7,6 +7,7 @@ import styles from "./Ability.module.css";
import { Image } from "./Image";
const sizeMap = {
HUGE: 64,
MAIN: 42,
SUB: 32,
SUBTINY: 26,

View File

@@ -3,7 +3,7 @@ import * as React from "react";
import type { Tables } from "~/db/tables";
import { useHydrated } from "~/hooks/useHydrated";
import { LRUCache } from "~/modules/cache";
import { BLANK_IMAGE_URL, discordAvatarUrl } from "~/utils/urls";
import { BLANK_IMAGE_URL, resolveAvatarUrl } from "~/utils/urls";
import styles from "./Avatar.module.css";
const dimensions = {
@@ -132,19 +132,23 @@ export function Avatar({
const identiconSource = identiconInput ?? user?.discordId ?? "unknown";
const src = url
? url
: user?.customAvatarUrl && !isErrored
? user.customAvatarUrl
: user?.discordAvatar && !isErrored
? discordAvatarUrl({
discordAvatar: user.discordAvatar,
discordId: user.discordId,
size: size === "lg" || size === "xmd" ? "lg" : "sm",
})
: isClient
? generateIdenticon(identiconSource, dimensions[size], 7)
: BLANK_IMAGE_URL;
const userAvatarUrl = user
? resolveAvatarUrl({
customAvatarUrl: user.customAvatarUrl,
discordId: user.discordId,
discordAvatar: user.discordAvatar,
size: size === "lg" || size === "xmd" ? "lg" : "sm",
})
: undefined;
const avatarUrl = url ?? userAvatarUrl;
const src =
avatarUrl && !isErrored
? avatarUrl
: isClient
? generateIdenticon(identiconSource, dimensions[size], 7)
: BLANK_IMAGE_URL;
return (
<div className={clsx(styles.avatarWrapper, className)}>

View File

@@ -1,10 +1,22 @@
import clsx from "clsx";
import { Lock, MessageCircleMore, SquarePen, Trash } from "lucide-react";
import {
HardDriveDownload,
Lock,
MessageCircleMore,
SquarePen,
Trash,
} from "lucide-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import type { Tables } from "~/db/tables";
import { useUser } from "~/features/auth/core/user";
import type { BuildWeaponWithTop500Info } from "~/features/builds/builds-types";
import {
BuildGraphic,
type BuildGraphicOwner,
} from "~/features/img-export/components/BuildGraphic";
import { ImageExportDialog } from "~/features/img-export/components/ImageExportDialog";
import type {
Ability as AbilityType,
BuildAbilitiesTuple,
@@ -28,6 +40,7 @@ import { Ability } from "./Ability";
import styles from "./BuildCard.module.css";
import { LinkButton, SendouButton } from "./elements/Button";
import { SendouPopover } from "./elements/Popover";
import { SendouSwitch } from "./elements/Switch";
import { FormWithConfirm } from "./FormWithConfirm";
import { Image } from "./Image";
import { LocaleTime } from "./LocaleTime";
@@ -48,11 +61,21 @@ interface BuildProps {
modes: ModeShort[] | null;
weapons: Array<BuildWeaponWithTop500Info>;
};
owner?: Pick<UserWithPlusTier, "discordId" | "username" | "plusTier">;
owner?: Pick<UserWithPlusTier, "discordId" | "username" | "plusTier"> &
Partial<
Pick<BuildGraphicOwner, "customUrl" | "discordAvatar" | "customAvatarUrl">
>;
/** Set to false when the page context already shows the owner (e.g. their own builds page) */
showOwner?: boolean;
canEdit?: boolean;
}
export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
export function BuildCard({
build,
owner,
showOwner = true,
canEdit = false,
}: BuildProps) {
const user = useUser();
const { t } = useTranslation(["weapons", "builds", "common", "game-misc"]);
@@ -100,15 +123,15 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
</h2>
</div>
<div className={styles.dateAuthorRow}>
{owner && (
{owner && showOwner ? (
<>
<Link to={userBuildsPage(owner)} className={styles.ownerLink}>
{owner.username}
</Link>
<div></div>
</>
)}
{owner?.plusTier ? (
) : null}
{owner?.plusTier && showOwner ? (
<>
<span>+{owner.plusTier}</span>
<div></div>
@@ -180,6 +203,7 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
path={navIconUrl("analyzer")}
/>
</LinkButton>
{owner ? <BuildImageExportDialog build={build} owner={owner} /> : null}
{description ? (
<SendouPopover
trigger={
@@ -229,6 +253,67 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
);
}
function BuildImageExportDialog({
build,
owner,
}: {
build: BuildProps["build"];
owner: BuildGraphicOwner;
}) {
const { t } = useTranslation(["common"]);
const [showTitle, setShowTitle] = React.useState(true);
const [showAbilityPoints, setShowAbilityPoints] = React.useState(true);
const [showAbilityChunks, setShowAbilityChunks] = React.useState(false);
return (
<ImageExportDialog
trigger={
<SendouButton
shape="circle"
size="small"
variant="minimal"
icon={<HardDriveDownload />}
className={styles.smallText}
aria-label={t("common:imageExport.export")}
/>
}
heading={t("common:imageExport.export")}
filename={`build-${mySlugify(build.title)}`}
qrCodePath={analyzerPage({
weaponId: build.weapons[0].weaponSplId,
abilities: build.abilities.flat(),
})}
settings={
<>
<SendouSwitch isSelected={showTitle} onChange={setShowTitle}>
{t("common:imageExport.buildTitle")}
</SendouSwitch>
<SendouSwitch
isSelected={showAbilityPoints}
onChange={setShowAbilityPoints}
>
{t("common:imageExport.abilityPoints")}
</SendouSwitch>
<SendouSwitch
isSelected={showAbilityChunks}
onChange={setShowAbilityChunks}
>
{t("common:imageExport.abilityChunks")}
</SendouSwitch>
</>
}
>
<BuildGraphic
build={build}
owner={owner}
showTitle={showTitle}
showAbilityPoints={showAbilityPoints}
showAbilityChunks={showAbilityChunks}
/>
</ImageExportDialog>
);
}
function RoundWeaponImage({ weapon }: { weapon: BuildWeaponWithTop500Info }) {
const normalizedWeaponSplId = canonicalWeaponSplId(weapon.weaponSplId);

View File

@@ -12,7 +12,7 @@ export function Flag({
const { i18n } = useTranslation();
return (
<div
<span
className={clsx(`twf twf-${countryCode.toLowerCase()}`, {
"twf-s": tiny,
})}