Enable Biome rules 4

This commit is contained in:
Kalle
2026-09-06 14:48:24 +03:00
parent e5196ca4d5
commit 71fb91935c
22 changed files with 239 additions and 206 deletions

View File

@@ -34,7 +34,7 @@
- state management is done via plain `useState` and React Context API
- avoid using `useEffect`
- all texts should be provided translations via the i18next library's `useTranslations` hook's `t` function
- instead of `&&` operator for conditional rendering, use the ternary operator
- instead of `&&` operator for conditional rendering, use the ternary operator; enforced by the `no-and-conditional-rendering` Biome plugin
- for localized user-readable time strings use `<LocaleTime />`, `<LocaleTimeRange>` or `useFormatDistanceToNow`. If needed use `useDateTimeFormat` directly. NEVER use e.g. `toLocaleString` directly as it does not include users' language selection.
## Forms

View File

@@ -103,7 +103,7 @@ export function BuildCard({
>
<div>
<div className={styles.topRow}>
{modes && modes.length > 0 && (
{modes && modes.length > 0 ? (
<div className={styles.modes}>
{modes.map((mode) => (
<Image
@@ -117,7 +117,7 @@ export function BuildCard({
/>
))}
</div>
)}
) : null}
<h2 className={styles.title} data-testid="build-title">
{title}
</h2>
@@ -159,11 +159,11 @@ export function BuildCard({
{weapons.map((weapon) => (
<RoundWeaponImage key={weapon.weaponSplId} weapon={weapon} />
))}
{weapons.length === 1 && (
{weapons.length === 1 ? (
<div className={styles.weaponText}>
{t(`weapons:MAIN_${weapons[0].weaponSplId}` as any)}
</div>
)}
) : null}
</div>
<div
className={clsx(styles.gearAbilities, {
@@ -219,7 +219,7 @@ export function BuildCard({
{description}
</SendouPopover>
) : null}
{canEdit && (
{canEdit ? (
<>
<LinkButton
shape="circle"
@@ -248,7 +248,7 @@ export function BuildCard({
/>
</FormWithConfirm>
</>
)}
) : null}
</div>
</div>
);

View File

@@ -296,7 +296,7 @@ export default function Chart({
},
}}
/>
{tooltipData && (
{tooltipData ? (
<div
className={styles.tooltip}
style={{
@@ -324,7 +324,7 @@ export default function Chart({
</div>
))}
</div>
)}
) : null}
</div>
);
}

View File

@@ -30,7 +30,7 @@ export function Label({
return (
<div className={clsx(styles.container, className, { "mb-0": !spaced })}>
<label htmlFor={htmlFor} className={labelClassName}>
{children} {required && <span className="text-error">*</span>}
{children} {required ? <span className="text-error">*</span> : null}
</label>
{valueLimits ? (
<div

View File

@@ -82,15 +82,15 @@ export function MapPoolSelector({
return (
<fieldset className={className}>
{Boolean(title) && <legend>{title}</legend>}
{Boolean(handleRemoval || allowBulkEdit) && (
{title ? <legend>{title}</legend> : null}
{handleRemoval || allowBulkEdit ? (
<div className="stack horizontal sm justify-end">
{handleRemoval && (
{handleRemoval ? (
<SendouButton variant="minimal" onClick={handleRemoval}>
{t("actions.remove")}
</SendouButton>
)}
{allowBulkEdit && (
) : null}
{allowBulkEdit ? (
<SendouButton
variant="minimal-destructive"
isDisabled={mapPool.isEmpty()}
@@ -98,16 +98,16 @@ export function MapPoolSelector({
>
{t("actions.clear")}
</SendouButton>
)}
) : null}
</div>
)}
) : null}
<div className="stack md">
{allowBulkEdit && (
{allowBulkEdit ? (
<MapPoolTemplateSelect
value={template}
handleChange={handleTemplateChange}
/>
)}
) : null}
{info}
<MapPoolStages
mapPool={mapPool}
@@ -275,9 +275,8 @@ export function MapPoolStages({
</button>
);
})}
{!isPresentational &&
allowBulkEdit &&
(mapPool.hasStage(stageId) ? (
{!isPresentational && allowBulkEdit ? (
mapPool.hasStage(stageId) ? (
<SendouButton
shape="circle"
key="clear"
@@ -297,7 +296,8 @@ export function MapPoolStages({
aria-label={t("common:actions.selectAll")}
size="small"
/>
))}
)
) : null}
</div>
</div>
</div>
@@ -358,7 +358,7 @@ function MapPoolTemplateSelect({
</option>
))}
</optgroup>
{recentEvents && recentEvents.length > 0 && (
{recentEvents && recentEvents.length > 0 ? (
<optgroup label={t("common:maps.template.yourRecentEvents")}>
{recentEvents.map((event) => (
<option key={event.id} value={`recent-event:${event.id}`}>
@@ -366,7 +366,7 @@ function MapPoolTemplateSelect({
</option>
))}
</optgroup>
)}
) : null}
</select>
</label>
);

View File

@@ -11,7 +11,7 @@ export function Section({
}) {
return (
<section className={styles.section}>
{title && <h2>{title}</h2>}
{title ? <h2>{title}</h2> : null}
<div className={className}>{children}</div>
</section>
);

View File

@@ -59,10 +59,11 @@ export function SendouButton({
}}
className={buttonClassName({ className, variant, size, shape })}
>
{icon &&
React.cloneElement(icon, {
className: iconClassName(icon.props.className, children, size),
})}
{icon
? React.cloneElement(icon, {
className: iconClassName(icon.props.className, children, size),
})
: null}
{children}
</button>
);
@@ -112,10 +113,11 @@ export function LinkButton({
data-testid={testId}
aria-label={ariaLabel}
>
{icon &&
React.cloneElement(icon, {
className: iconClassName(icon.props.className, children, size),
})}
{icon
? React.cloneElement(icon, {
className: iconClassName(icon.props.className, children, size),
})
: null}
{children}
</a>
);
@@ -132,10 +134,11 @@ export function LinkButton({
onClick={onClick}
aria-label={ariaLabel}
>
{icon &&
React.cloneElement(icon, {
className: iconClassName(icon.props.className, children, size),
})}
{icon
? React.cloneElement(icon, {
className: iconClassName(icon.props.className, children, size),
})
: null}
{children}
</Link>
);

View File

@@ -116,7 +116,9 @@ export function NotificationContent({
seen: Number(!stickyUnseenIds.has(notification.id)),
}}
/>
{i !== notifications.length - 1 && <NotificationItemDivider />}
{i !== notifications.length - 1 ? (
<NotificationItemDivider />
) : null}
</React.Fragment>
))}
</NotificationsList>

View File

@@ -167,7 +167,7 @@ function BuildAnalyzerPage() {
};
const mainWeaponCategoryItems = [
analyzed.stats.shotSpreadAir && (
analyzed.stats.shotSpreadAir ? (
<StatCard
context={context}
key="jumpShotSpread"
@@ -175,8 +175,8 @@ function BuildAnalyzerPage() {
title={t("analyzer:stat.jumpShotSpread")}
suffix="°"
/>
),
typeof analyzed.stats.shotSpreadGround === "number" && (
) : null,
typeof analyzed.stats.shotSpreadGround === "number" ? (
<StatCard
context={context}
key="groundShotSpread"
@@ -184,9 +184,9 @@ function BuildAnalyzerPage() {
title={t("analyzer:stat.groundShotSpread")}
suffix="°"
/>
),
) : null,
// Squeezer
analyzed.stats.shotAutofireSpreadAir && (
analyzed.stats.shotAutofireSpreadAir ? (
<StatCard
context={context}
key="shotAutofireSpreadAir"
@@ -194,8 +194,8 @@ function BuildAnalyzerPage() {
title={t("analyzer:stat.shotAutofireSpreadAir")}
suffix="°"
/>
),
typeof analyzed.stats.shotAutofireSpreadGround === "number" && (
) : null,
typeof analyzed.stats.shotAutofireSpreadGround === "number" ? (
<StatCard
context={context}
key="shotAutofireSpreadGround"
@@ -203,7 +203,7 @@ function BuildAnalyzerPage() {
title={t("analyzer:stat.shotAutofireSpreadGround")}
suffix="°"
/>
),
) : null,
...INK_CONSUME_TYPES.filter(
(type) => analyzed.stats[`mainWeaponInkConsumptionPercentage_${type}`],
@@ -216,7 +216,7 @@ function BuildAnalyzerPage() {
suffix="%"
/>
)),
typeof analyzed.stats.mainWeaponWhiteInkSeconds === "number" && (
typeof analyzed.stats.mainWeaponWhiteInkSeconds === "number" ? (
<StatCard
context={context}
key="whiteInkSeconds"
@@ -224,8 +224,8 @@ function BuildAnalyzerPage() {
title={t("analyzer:stat.whiteInk")}
suffix={t("analyzer:suffix.seconds")}
/>
),
typeof analyzed.weapon.brellaCanopyHp === "number" && (
) : null,
typeof analyzed.weapon.brellaCanopyHp === "number" ? (
<StatCard
context={context}
key="brellaCanopyHp"
@@ -233,8 +233,8 @@ function BuildAnalyzerPage() {
title={t("analyzer:stat.canopyHp")}
suffix={t("analyzer:suffix.hp")}
/>
),
typeof analyzed.weapon.fullChargeSeconds === "number" && (
) : null,
typeof analyzed.weapon.fullChargeSeconds === "number" ? (
<StatCard
context={context}
key="fullChargeSeconds"
@@ -242,8 +242,8 @@ function BuildAnalyzerPage() {
title={t("analyzer:stat.fullChargeSeconds")}
suffix={t("analyzer:suffix.seconds")}
/>
),
typeof analyzed.weapon.maxChargeHoldSeconds === "number" && (
) : null,
typeof analyzed.weapon.maxChargeHoldSeconds === "number" ? (
<StatCard
context={context}
key="maxChargeHoldSeconds"
@@ -251,7 +251,7 @@ function BuildAnalyzerPage() {
title={t("analyzer:stat.maxChargeHoldSeconds")}
suffix={t("analyzer:suffix.seconds")}
/>
),
) : null,
].filter(Boolean);
// a primary slot-only ability (e.g. Ninja Squid) or Ability Doubler alone leaves abilityPoints at 0
@@ -313,47 +313,46 @@ function BuildAnalyzerPage() {
{t("analyzer:compare")}
</SendouTab>
</SendouTabList>
{[1, 2].map(
(buildIndex) =>
focusedBuild && (
<SendouTabPanel
id={`build-${buildIndex}`}
key={`build-${buildIndex}`}
>
<AbilitiesSelector
selectedAbilities={focusedBuild}
onChange={(newBuild) => {
const firstBuildIsEmpty = build
{[1, 2].map((buildIndex) =>
focusedBuild ? (
<SendouTabPanel
id={`build-${buildIndex}`}
key={`build-${buildIndex}`}
>
<AbilitiesSelector
selectedAbilities={focusedBuild}
onChange={(newBuild) => {
const firstBuildIsEmpty = build
.flat()
.every((ability) => ability === "UNKNOWN");
const buildWasEmptied =
!firstBuildIsEmpty &&
newBuild
.flat()
.every((ability) => ability === "UNKNOWN");
const buildWasEmptied =
!firstBuildIsEmpty &&
newBuild
.flat()
.every((ability) => ability === "UNKNOWN") &&
focused === 1;
// otherwise build2 would be duplicated
if (buildWasEmptied) {
handleChange({
newBuild: build2,
newBuild2: newBuild,
newFocused: 1,
});
return;
}
.every((ability) => ability === "UNKNOWN") &&
focused === 1;
// otherwise build2 would be duplicated
if (buildWasEmptied) {
handleChange({
[focused === 1 || firstBuildIsEmpty
? "newBuild"
: "newBuild2"]: newBuild,
newFocused: firstBuildIsEmpty ? 1 : undefined,
newBuild: build2,
newBuild2: newBuild,
newFocused: 1,
});
}}
/>
</SendouTabPanel>
),
return;
}
handleChange({
[focused === 1 || firstBuildIsEmpty
? "newBuild"
: "newBuild2"]: newBuild,
newFocused: firstBuildIsEmpty ? 1 : undefined,
});
}}
/>
</SendouTabPanel>
) : null,
)}
<SendouTabPanel id="build-compare">
<APCompare
@@ -382,16 +381,16 @@ function BuildAnalyzerPage() {
}
effects={allEffects}
/>
{showAbilityChunksRequired && (
{showAbilityChunksRequired ? (
<AbilityChunksRequired build={build} />
)}
) : null}
</div>
<div className={styles.patch}>
{t("analyzer:patch")} {CURRENT_PATCH}
</div>
</div>
<div className="stack md">
{mainWeaponCategoryItems.length > 0 && (
{mainWeaponCategoryItems.length > 0 ? (
<StatCategory
title={t("analyzer:stat.category.main")}
summaryRightContent={
@@ -410,7 +409,7 @@ function BuildAnalyzerPage() {
>
{mainWeaponCategoryItems}
</StatCategory>
)}
) : null}
<StatCategory
title={t("analyzer:stat.category.sub")}
@@ -438,67 +437,67 @@ function BuildAnalyzerPage() {
title={t("analyzer:stat.whiteInk")}
suffix={t("analyzer:suffix.seconds")}
/>
{analyzed.stats.subVelocity && (
{analyzed.stats.subVelocity ? (
<StatCard
context={context}
stat={statKeyToTuple("subVelocity")}
title={t("analyzer:stat.sub.velocity")}
/>
)}
{analyzed.stats.subFirstPhaseDuration && (
) : null}
{analyzed.stats.subFirstPhaseDuration ? (
<StatCard
context={context}
stat={statKeyToTuple("subFirstPhaseDuration")}
title={t("analyzer:stat.sub.firstPhaseDuration")}
suffix={t("analyzer:suffix.seconds")}
/>
)}
{analyzed.stats.subSecondPhaseDuration && (
) : null}
{analyzed.stats.subSecondPhaseDuration ? (
<StatCard
context={context}
stat={statKeyToTuple("subSecondPhaseDuration")}
title={t("analyzer:stat.sub.secondPhaseDuration")}
suffix={t("analyzer:suffix.seconds")}
/>
)}
{analyzed.stats.subMarkingTimeInSeconds && (
) : null}
{analyzed.stats.subMarkingTimeInSeconds ? (
<StatCard
context={context}
stat={statKeyToTuple("subMarkingTimeInSeconds")}
title={t("analyzer:stat.sub.markingTimeInSeconds")}
suffix={t("analyzer:suffix.seconds")}
/>
)}
{analyzed.stats.subMarkingRadius && (
) : null}
{analyzed.stats.subMarkingRadius ? (
<StatCard
context={context}
stat={statKeyToTuple("subMarkingRadius")}
title={t("analyzer:stat.sub.markingRadius")}
/>
)}
{analyzed.stats.subExplosionRadius && (
) : null}
{analyzed.stats.subExplosionRadius ? (
<StatCard
context={context}
stat={statKeyToTuple("subExplosionRadius")}
title={t("analyzer:stat.sub.explosionRadius")}
/>
)}
{analyzed.stats.subHp && (
) : null}
{analyzed.stats.subHp ? (
<StatCard
context={context}
stat={statKeyToTuple("subHp")}
title={t("analyzer:stat.sub.hp")}
suffix={t("analyzer:suffix.hp")}
/>
)}
{analyzed.stats.subQsjBoost && (
) : null}
{analyzed.stats.subQsjBoost ? (
<StatCard
context={context}
stat={statKeyToTuple("subQsjBoost")}
title={t("analyzer:stat.sub.qsjBoost")}
suffix={t("analyzer:abilityPoints.short")}
/>
)}
) : null}
</StatCategory>
<StatCategory
@@ -567,7 +566,7 @@ function BuildAnalyzerPage() {
/>
))
: null}
{analyzed.stats.specialDurationInSeconds && (
{analyzed.stats.specialDurationInSeconds ? (
<StatCard
context={context}
stat={statKeyToTuple("specialDurationInSeconds")}
@@ -585,8 +584,8 @@ function BuildAnalyzerPage() {
: undefined
}
/>
)}
{analyzed.stats.specialDamageDistance && (
) : null}
{analyzed.stats.specialDamageDistance ? (
<StatCard
context={context}
stat={statKeyToTuple("specialDamageDistance")}
@@ -596,8 +595,8 @@ function BuildAnalyzerPage() {
),
})}
/>
)}
{analyzed.stats.specialPaintRadius && (
) : null}
{analyzed.stats.specialPaintRadius ? (
<StatCard
context={context}
stat={statKeyToTuple("specialPaintRadius")}
@@ -607,8 +606,8 @@ function BuildAnalyzerPage() {
),
})}
/>
)}
{analyzed.stats.specialFieldHp && (
) : null}
{analyzed.stats.specialFieldHp ? (
<StatCard
context={context}
stat={statKeyToTuple("specialFieldHp")}
@@ -619,8 +618,8 @@ function BuildAnalyzerPage() {
})}
suffix={t("analyzer:suffix.hp")}
/>
)}
{analyzed.stats.specialDeviceHp && (
) : null}
{analyzed.stats.specialDeviceHp ? (
<StatCard
context={context}
stat={statKeyToTuple("specialDeviceHp")}
@@ -631,8 +630,8 @@ function BuildAnalyzerPage() {
})}
suffix={t("analyzer:suffix.hp")}
/>
)}
{analyzed.stats.specialHookInkConsumptionPercentage && (
) : null}
{analyzed.stats.specialHookInkConsumptionPercentage ? (
<StatCard
context={context}
stat={statKeyToTuple("specialHookInkConsumptionPercentage")}
@@ -643,8 +642,8 @@ function BuildAnalyzerPage() {
})}
suffix="%"
/>
)}
{analyzed.stats.specialInkConsumptionPerSecondPercentage && (
) : null}
{analyzed.stats.specialInkConsumptionPerSecondPercentage ? (
<StatCard
context={context}
stat={statKeyToTuple(
@@ -657,8 +656,8 @@ function BuildAnalyzerPage() {
})}
suffix="%"
/>
)}
{analyzed.stats.specialReticleRadius && (
) : null}
{analyzed.stats.specialReticleRadius ? (
<StatCard
context={context}
stat={statKeyToTuple("specialReticleRadius")}
@@ -668,8 +667,8 @@ function BuildAnalyzerPage() {
),
})}
/>
)}
{analyzed.stats.specialThrowDistance && (
) : null}
{analyzed.stats.specialThrowDistance ? (
<StatCard
context={context}
stat={statKeyToTuple("specialThrowDistance")}
@@ -679,8 +678,8 @@ function BuildAnalyzerPage() {
),
})}
/>
)}
{analyzed.stats.specialMoveSpeed && (
) : null}
{analyzed.stats.specialMoveSpeed ? (
<StatCard
context={context}
stat={statKeyToTuple("specialMoveSpeed")}
@@ -690,8 +689,8 @@ function BuildAnalyzerPage() {
),
})}
/>
)}
{analyzed.stats.specialAutoChargeRate && (
) : null}
{analyzed.stats.specialAutoChargeRate ? (
<StatCard
context={context}
stat={statKeyToTuple("specialAutoChargeRate")}
@@ -701,8 +700,8 @@ function BuildAnalyzerPage() {
),
})}
/>
)}
{analyzed.stats.specialMaxRadius && (
) : null}
{analyzed.stats.specialMaxRadius ? (
<StatCard
context={context}
stat={statKeyToTuple("specialMaxRadius")}
@@ -713,8 +712,8 @@ function BuildAnalyzerPage() {
})}
popoverInfo={t("analyzer:stat.special.maxRadius.explanation")}
/>
)}
{analyzed.stats.specialRadiusRangeMin && (
) : null}
{analyzed.stats.specialRadiusRangeMin ? (
<StatCard
context={context}
stat={statKeyToTuple("specialRadiusRangeMin")}
@@ -724,8 +723,8 @@ function BuildAnalyzerPage() {
),
})}
/>
)}
{analyzed.stats.specialRadiusRangeMax && (
) : null}
{analyzed.stats.specialRadiusRangeMax ? (
<StatCard
context={context}
stat={statKeyToTuple("specialRadiusRangeMax")}
@@ -735,8 +734,8 @@ function BuildAnalyzerPage() {
),
})}
/>
)}
{analyzed.stats.specialPowerUpDuration && (
) : null}
{analyzed.stats.specialPowerUpDuration ? (
<StatCard
context={context}
stat={statKeyToTuple("specialPowerUpDuration")}
@@ -747,7 +746,7 @@ function BuildAnalyzerPage() {
})}
suffix={t("analyzer:suffix.seconds")}
/>
)}
) : null}
</StatCategory>
<StatCategory
title={t("analyzer:stat.category.subDef")}
@@ -787,7 +786,7 @@ function BuildAnalyzerPage() {
/>
</StatCategory>
{analyzed.stats.subWeaponDefenseDamages.length > 0 && (
{analyzed.stats.subWeaponDefenseDamages.length > 0 ? (
<StatCategory
title={t("analyzer:stat.category.subWeaponDefenseDamages")}
containerClassName={styles.tableContainer}
@@ -814,9 +813,9 @@ function BuildAnalyzerPage() {
multiShots={analyzed.weapon.multiShots}
/>
</StatCategory>
)}
) : null}
{analyzed.stats.damages.length > 0 && (
{analyzed.stats.damages.length > 0 ? (
<StatCategory
title={t("analyzer:stat.category.damage")}
containerClassName={styles.tableContainer}
@@ -826,9 +825,9 @@ function BuildAnalyzerPage() {
multiShots={analyzed.weapon.multiShots}
/>
</StatCategory>
)}
) : null}
{analyzed.stats.specialWeaponDamages.length > 0 && (
{analyzed.stats.specialWeaponDamages.length > 0 ? (
<StatCategory
title={t("analyzer:stat.category.special.damage", {
specialWeapon: t(
@@ -839,9 +838,9 @@ function BuildAnalyzerPage() {
>
<DamageTable values={analyzed.stats.specialWeaponDamages} />
</StatCategory>
)}
) : null}
{analyzed.stats.fullInkTankOptions.length > 0 && (
{analyzed.stats.fullInkTankOptions.length > 0 ? (
<StatCategory
title={t("analyzer:stat.category.actionsPerInkTank")}
containerClassName={styles.tableContainer}
@@ -863,7 +862,7 @@ function BuildAnalyzerPage() {
<PerInkTankGrid weaponSplId={mainWeaponId} />
</div>
</StatCategory>
)}
) : null}
<StatCategory
title={t("analyzer:stat.category.movement")}
@@ -890,34 +889,34 @@ function BuildAnalyzerPage() {
stat={statKeyToTuple("runSpeed")}
title={t("analyzer:stat.runSpeed")}
/>
{analyzed.stats.shootingRunSpeed && (
{analyzed.stats.shootingRunSpeed ? (
<StatCard
context={context}
stat={statKeyToTuple("shootingRunSpeed")}
title={t("analyzer:stat.shootingRunSpeed")}
/>
)}
{analyzed.stats.shootingRunSpeedCharging && (
) : null}
{analyzed.stats.shootingRunSpeedCharging ? (
<StatCard
context={context}
stat={statKeyToTuple("shootingRunSpeedCharging")}
title={t("analyzer:stat.shootingRunSpeedCharging")}
/>
)}
{analyzed.stats.shootingRunSpeedFullCharge && (
) : null}
{analyzed.stats.shootingRunSpeedFullCharge ? (
<StatCard
context={context}
stat={statKeyToTuple("shootingRunSpeedFullCharge")}
title={t("analyzer:stat.shootingRunSpeedFullCharge")}
/>
)}
{analyzed.stats.shootingRunSpeedSecondaryMode && (
) : null}
{analyzed.stats.shootingRunSpeedSecondaryMode ? (
<StatCard
context={context}
stat={statKeyToTuple("shootingRunSpeedSecondaryMode")}
title={t("analyzer:stat.shootingRunSpeedSecondaryMode")}
/>
)}
) : null}
<StatCard
context={context}
stat={statKeyToTuple("squidSurgeChargeFrames")}
@@ -989,7 +988,7 @@ function BuildAnalyzerPage() {
}
/>
</StatCategory>
{objectShredderSelected && (
{objectShredderSelected ? (
<Link
className={styles.noticeableLink}
to={objectDamageCalculatorPage(mainWeaponId)}
@@ -1002,7 +1001,7 @@ function BuildAnalyzerPage() {
/>
{t("analyzer:objCalcAd")}
</Link>
)}
) : null}
{user && focusedBuild && !buildIsEmpty(focusedBuild) ? (
<Link
className={styles.noticeableLink}
@@ -1443,9 +1442,9 @@ function StatCategory({
{summaryRightContent}
</summary>
<div className={containerClassName}>{children}</div>
{textBelow && (
{textBelow ? (
<div className={styles.statCategoryExplanation}>{textBelow}</div>
)}
) : null}
</details>
);
}
@@ -1540,7 +1539,7 @@ function StatCard({
<div className={styles.statCardTitleAndValueContainer}>
<h2 className={styles.statCardTitle}>
{title}{" "}
{popoverInfo && (
{popoverInfo ? (
<SendouPopover
trigger={
<SendouButton className={styles.statCardPopoverTrigger}>
@@ -1550,7 +1549,7 @@ function StatCard({
>
{popoverInfo}
</SendouPopover>
)}
) : null}
</h2>
<div className={styles.statCardValues}>
<div className={styles.statCardValue}>
@@ -1684,9 +1683,9 @@ function DamageTable({
<thead>
<tr>
<th>{t("analyzer:damage.header.type")}</th>
{showDistanceColumn && (
{showDistanceColumn ? (
<th>{t("analyzer:damage.header.distance")}</th>
)}
) : null}
{damageIsSubWeaponDamage(firstRow) ? (
<th>
{comparisonValues
@@ -1694,7 +1693,9 @@ function DamageTable({
: t("analyzer:damage.header.baseDamage")}
</th>
) : null}
{showDamageColumn && <th>{t("analyzer:damage.header.damage")}</th>}
{showDamageColumn ? (
<th>{t("analyzer:damage.header.damage")}</th>
) : null}
{showPopovers ? <th /> : null}
</tr>
</thead>
@@ -1731,27 +1732,27 @@ function DamageTable({
) : null}
</div>
</td>
{showDistanceColumn && (
{showDistanceColumn ? (
<td>
{typeof val.distance === "number"
? val.distance
: val.distance?.join("-")}
</td>
)}
{damageIsSubWeaponDamage(val) && <td>{val.baseValue}</td>}
{showDamageColumn && (
) : null}
{damageIsSubWeaponDamage(val) ? <td>{val.baseValue}</td> : null}
{showDamageColumn ? (
<td>
{damage(val)}
{comparisonVal ? `/${damage(comparisonVal)}` : null}{" "}
{val.shotsToSplat && (
{val.shotsToSplat ? (
<span className={styles.shotsToSplat}>
{t("analyzer:damage.toSplat", {
count: val.shotsToSplat,
})}
</span>
)}
) : null}
</td>
)}
) : null}
{showPopovers ? (
<td className={styles.popoverCell}>
{renderPopover(val, (val as SubWeaponDamage).subWeaponId) ? (
@@ -1858,11 +1859,11 @@ function ConsumptionTable({
})}
</tbody>
</Table>
{subWeaponId === TORPEDO_ID && (
{subWeaponId === TORPEDO_ID ? (
<div className={styles.consumptionTableExplanation}>
{t("analyzer:torpedoExplanation")}
</div>
)}
) : null}
</>
);
}

View File

@@ -191,7 +191,7 @@ function Results() {
return (
<Section title={t("calendar:results")} className={styles.resultsSection}>
{data.event.participantCount && (
{data.event.participantCount ? (
<div className={styles.resultsParticipantCount}>
{isTeamResults
? t("calendar:participatedCount", {
@@ -201,7 +201,7 @@ function Results() {
count: data.event.participantCount,
})}
</div>
)}
) : null}
<Table>
<thead>
<tr>
@@ -276,9 +276,9 @@ function Description() {
<Avatar user={data.event} size="xs" />
{data.event.username}
</div>
{data.event.description && (
{data.event.description ? (
<div className="whitespace-pre-wrap">{data.event.description}</div>
)}
) : null}
</div>
</Section>
);

View File

@@ -92,7 +92,9 @@ function TeamLFGPost({
<div className="stack horizontal items-center justify-between">
<PostTeamLogoHeader team={post.team} />
<div className="stack horizontal items-center sm">
{isHydrated && <PostTimezonePill timezone={post.timezone} />}
{isHydrated ? (
<PostTimezonePill timezone={post.timezone} />
) : null}
{post.languages ? (
<PostLanguagePill languages={post.languages} />
) : null}
@@ -273,12 +275,12 @@ function PostPills({
invisible: !isHydrated,
})}
>
{typeof timezone === "string" && isHydrated && (
{typeof timezone === "string" && isHydrated ? (
<PostTimezonePill timezone={timezone} />
)}
{!isHydrated && <PostTimezonePillPlaceholder />}
) : null}
{!isHydrated ? <PostTimezonePillPlaceholder /> : null}
{languages ? <PostLanguagePill languages={languages} /> : null}
{canEdit && <PostEditButton id={postId} />}
{canEdit ? <PostEditButton id={postId} /> : null}
</div>
);
}

View File

@@ -128,7 +128,7 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
<SendouButton onClick={handleCreateMaplist} isDisabled={disabled}>
{t("common:maps.createMapList")}
</SendouButton>
{mapList && (
{mapList ? (
<>
<ol className={styles.mapList}>
{mapList.map(({ mode, stageId }, i) => (
@@ -163,7 +163,7 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
{t("common:actions.copyToClipboard")}
</SendouButton>
</>
)}
) : null}
</div>
);
}

View File

@@ -61,9 +61,9 @@ export default function NotificationsPage() {
seen: Number(!unseenIds.has(notification.id)),
}}
/>
{i !== data.notifications.length - 1 && (
{i !== data.notifications.length - 1 ? (
<NotificationItemDivider />
)}
) : null}
</React.Fragment>
))}
</NotificationsList>

View File

@@ -321,7 +321,9 @@ function DamageReceiversGrid({
</div>
<div className="stack horizontal sm justify-center items-center">
{t(`analyzer:damage.${damage.type}` as any)}
{damage.objectShredder && <Ability ability="OS" size="TINY" />}
{damage.objectShredder ? (
<Ability ability="OS" size="TINY" />
) : null}
</div>
</div>
))}
@@ -332,8 +334,9 @@ function DamageReceiversGrid({
<div>
<Label htmlFor="ap">
<div className={styles.apLabel}>
{abilityPoints !== "0" &&
damageReceiverAp[damageToReceiver.receiver]}
{abilityPoints !== "0"
? damageReceiverAp[damageToReceiver.receiver]
: null}
</div>
</Label>
<SendouPopover

View File

@@ -356,7 +356,7 @@ function Groups() {
<SendouTab id="groups" number={neutralGroups.length}>
{t("q:looking.columns.groups")}
</SendouTab>
{isMobile && (
{isMobile ? (
<SendouTab
id="received"
number={groupsReceivedLikesFrom.length}
@@ -367,12 +367,12 @@ function Groups() {
: "q:looking.columns.invitations",
)}
</SendouTab>
)}
{isMobile && data.ownGroup && (
) : null}
{isMobile && data.ownGroup ? (
<SendouTab id="own" number={data.ownGroup.members.length}>
{t("q:looking.columns.myGroup")}
</SendouTab>
)}
) : null}
</SendouTabList>
<SendouTabPanel id="groups">
<div className="stack sm">

View File

@@ -98,7 +98,7 @@ export function SwissBracket({
return (
<div className="stack xl">
<div className="stack lg">
{groups.length > 1 && (
{groups.length > 1 ? (
<div className="stack horizontal">
{groups.map((g) => (
<SendouButton
@@ -118,7 +118,7 @@ export function SwissBracket({
</SendouButton>
))}
</div>
)}
) : null}
<div className="stack lg">
{rounds.map((round, roundI) => {
const matches = bracket.data.match.filter(

View File

@@ -293,9 +293,9 @@ function NewBadgeReceiversSelector({
<div className="stack horizontal sm items-end">
<ParticipationPill setResults={member.setResults} />
</div>
{i !== standingToReceive?.members.length - 1 && (
{i !== standingToReceive?.members.length - 1 ? (
<Divider className="mt-3" />
)}
) : null}
</div>
);
})}

View File

@@ -424,12 +424,12 @@ function SeriesView({
</div>
</SendouTabPanel>
<SendouTabPanel id="leaderboard">
{hasLeaderboard && (
{hasLeaderboard ? (
<EventLeaderboard
leaderboard={series.leaderboard!}
ownEntry={series.ownEntry}
/>
)}
) : null}
</SendouTabPanel>
</SendouTabs>
</div>

View File

@@ -37,7 +37,7 @@ export function UserResultsTable({
<Table>
<thead>
<tr>
{hasHighlightCheckboxes && <th />}
{hasHighlightCheckboxes ? <th /> : null}
<th id={placementHeaderId}>{t("results.placing")}</th>
<th>{t("results.tournament")}</th>
<th>{t("results.date")}</th>
@@ -59,7 +59,7 @@ export function UserResultsTable({
return (
<tr key={rowId}>
{hasHighlightCheckboxes && (
{hasHighlightCheckboxes ? (
<td>
<input
value={result.teamId}
@@ -73,7 +73,7 @@ export function UserResultsTable({
defaultChecked={Boolean(result.isHighlight)}
/>
</td>
)}
) : null}
<td className="pl-4 whitespace-nowrap" id={placementCellId}>
<div className="stack horizontal xs items-end">
<Placement placement={result.placement} />{" "}

View File

@@ -259,7 +259,7 @@ function Document({
<Fonts />
</head>
<body>
{IS_E2E_TEST_RUN && <HydrationTestIndicator />}
{IS_E2E_TEST_RUN ? <HydrationTestIndicator /> : null}
<React.StrictMode>
<SearchParamsProvider>
<SendouToastRegion />

View File

@@ -0,0 +1,18 @@
language js
// `cond && <X />` renders the left value when it is falsy but not `null` /
// `undefined` / `false`: a `0` count or an empty string ends up in the DOM as
// text. The ternary form has no such edge, so it is the repo-wide convention
// (AGENTS.md, "React"). Matches both `cond && <jsx/>` anywhere and any `&&`
// standing directly as a JSX child, which covers helpers like
// `{icon && React.cloneElement(icon, {...})}`.
`$cond && $rendered` as $expression where {
or {
$rendered <: or {
JsxTagExpression(),
JsParenthesizedExpression(expression=JsxTagExpression())
},
$expression <: within JsxExpressionChild(expression=$expression)
},
register_diagnostic(span=$expression, message="Do not use `&&` for conditional rendering: a falsy left side that is not `null`/`undefined`/`false` (a `0` count, an empty string, `NaN`) renders itself. Use a ternary instead: `cond ? <X /> : null`.", severity="error")
}

View File

@@ -221,6 +221,10 @@
{
"includes": ["**", "!app/utils/kysely.server.ts"],
"plugins": ["./biome-plugins/no-kysely-sqlite-helpers.grit"]
},
{
"includes": ["app/**"],
"plugins": ["./biome-plugins/no-and-conditional-rendering.grit"]
}
]
}