Add SendouQ weapon report recently reported quick add

This commit is contained in:
Kalle 2025-07-02 21:04:20 +03:00
parent c9752bfc87
commit ea178d7d6a
19 changed files with 162 additions and 26 deletions

View File

@ -44,6 +44,8 @@ interface WeaponSelectProps<
disabledWeaponIds?: Array<MainWeaponId>; // TODO: implement for `AnyWeapon` if needed
testId?: string;
isRequired?: boolean;
/** If set, selection of weapons that user sees when search input is empty allowing for quick select for e.g. previous selections */
quickSelectWeaponsIds?: Array<MainWeaponId>;
}
// TODO: fix selected value disappears when filtered out. This is because `items` is filtered in a controlled manner and the selected key might not be included in the filtered items.
@ -60,10 +62,13 @@ export function WeaponSelect<
includeSubSpecial,
testId = "weapon-select",
isRequired,
quickSelectWeaponsIds,
}: WeaponSelectProps<Clearable, IncludeSubSpecial>) {
const { t } = useTranslation(["common"]);
const { items, filterValue, setFilterValue } =
useFilteredWeaponItems(includeSubSpecial);
const { items, filterValue, setFilterValue } = useFilteredWeaponItems({
includeSubSpecial,
quickSelectWeaponsIds,
});
const keyify = (value?: MainWeaponId | AnyWeapon | null) => {
if (typeof value === "number") return `MAIN_${value}`;
@ -111,11 +116,13 @@ export function WeaponSelect<
<SendouSelectItemSection
heading={name}
headingImgPath={
name === "subs"
? subWeaponImageUrl(SPLAT_BOMB_ID)
: name === "specials"
? specialWeaponImageUrl(TRIZOOKA_ID)
: weaponCategoryUrl(name)
key === "quick-select"
? undefined
: name === "subs"
? subWeaponImageUrl(SPLAT_BOMB_ID)
: name === "specials"
? specialWeaponImageUrl(TRIZOOKA_ID)
: weaponCategoryUrl(name)
}
className={idx === 0 ? "pt-0-5-forced" : undefined}
key={key}
@ -169,32 +176,72 @@ export function WeaponSelect<
);
}
function useFilteredWeaponItems(includeSubSpecial: boolean | undefined) {
function useFilteredWeaponItems({
includeSubSpecial,
quickSelectWeaponsIds,
}: {
includeSubSpecial: boolean | undefined;
quickSelectWeaponsIds?: Array<MainWeaponId>;
}) {
const items = useAllWeaponCategories(includeSubSpecial);
const [filterValue, setFilterValue] = React.useState("");
const { t } = useTranslation(["common"]);
const filtered = !filterValue
? items
: items
.map((category) => {
const filteredItems = category.items.filter((item) =>
filterWeapon({
weapon: item.weapon,
weaponName: item.name,
searchTerm: filterValue,
const showQuickSelectWeapons =
filterValue === "" && quickSelectWeaponsIds?.length;
const filteredItems = () => {
if (showQuickSelectWeapons) {
return [
{
idx: 0,
key: "quick-select" as const,
name: t("common:forms.weaponSearch.quickSelect"),
items: items
.flatMap((c) =>
c.items
.map((item) => (item.weapon.type === "MAIN" ? item : null))
.filter((val) => val !== null),
)
.filter((item) =>
quickSelectWeaponsIds.includes(item.weapon.id as MainWeaponId),
)
.sort((a, b) => {
const aIdx = quickSelectWeaponsIds.indexOf(
a.weapon.id as MainWeaponId,
);
const bIdx = quickSelectWeaponsIds.indexOf(
b.weapon.id as MainWeaponId,
);
return aIdx - bIdx;
}),
);
},
];
}
return {
...category,
items: filteredItems,
};
})
.filter((category) => category.items.length > 0)
.map((category, idx) => ({ ...category, idx }));
return !filterValue
? items
: items
.map((category) => {
const filteredItems = category.items.filter((item) =>
filterWeapon({
weapon: item.weapon,
weaponName: item.name,
searchTerm: filterValue,
}),
);
return {
...category,
items: filteredItems,
};
})
.filter((category) => category.items.length > 0)
.map((category, idx) => ({ ...category, idx }));
};
return {
items: filtered,
items: filteredItems(),
filterValue,
setFilterValue,
};

View File

@ -38,6 +38,7 @@ import { Chat, type ChatProps, useChat } from "~/features/chat/components/Chat";
import * as Seasons from "~/features/mmr/core/Seasons";
import { GroupCard } from "~/features/sendouq/components/GroupCard";
import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants";
import { useRecentlyReportedWeapons } from "~/features/sendouq/q-hooks";
import { AddPrivateNoteDialog } from "~/features/sendouq-match/components/AddPrivateNoteDialog";
import type { ReportedWeaponForMerging } from "~/features/sendouq-match/core/reported-weapons.server";
import { resolveRoomPass } from "~/features/tournament-bracket/tournament-bracket-utils";
@ -410,6 +411,8 @@ function ReportWeaponsForm() {
const [reportingMode, setReportingMode] = React.useState<
"ALL" | "MYSELF" | "MY_TEAM"
>("MYSELF");
const { recentlyReportedWeapons, addRecentlyReportedWeapon } =
useRecentlyReportedWeapons();
const playedMaps = data.match.mapList.filter((m) => m.winnerGroupId);
const winners = playedMaps.map((m) =>
@ -569,6 +572,7 @@ function ReportWeaponsForm() {
<div className="stack horizontal sm items-center">
<WeaponSelect
value={weaponSplId ?? undefined}
quickSelectWeaponsIds={recentlyReportedWeapons}
onChange={(weaponSplId) => {
setWeaponsUsage((val) => {
const result = val.filter(
@ -585,6 +589,8 @@ function ReportWeaponsForm() {
userId: member.id,
});
addRecentlyReportedWeapon(weaponSplId);
return result;
});
}}
@ -929,6 +935,8 @@ function MapList({
const [ownWeaponsUsage, setOwnWeaponsUsage] = React.useState<
ReportedWeaponForMerging[]
>([]);
const { recentlyReportedWeapons, addRecentlyReportedWeapon } =
useRecentlyReportedWeapons();
const previouslyReportedWinners = isResubmission
? data.match.mapList
@ -974,6 +982,8 @@ function MapList({
setWinners={setWinners}
weapons={data.reportedWeapons?.[i]}
showReportedOwnWeapon={!ownWeaponReported}
recentlyReportedWeapons={recentlyReportedWeapons}
addRecentlyReportedWeapon={addRecentlyReportedWeapon}
onOwnWeaponSelected={(newReportedWeapon) => {
if (!newReportedWeapon) return;
@ -1031,6 +1041,8 @@ function MapListMap({
weapons,
onOwnWeaponSelected,
showReportedOwnWeapon,
recentlyReportedWeapons,
addRecentlyReportedWeapon,
}: {
i: number;
map: Unpacked<SerializeFrom<typeof loader>["match"]["mapList"]>;
@ -1040,6 +1052,8 @@ function MapListMap({
weapons?: (MainWeaponId | null)[] | null;
onOwnWeaponSelected?: (weapon: ReportedWeaponForMerging | null) => void;
showReportedOwnWeapon: boolean;
recentlyReportedWeapons?: MainWeaponId[];
addRecentlyReportedWeapon?: (weapon: MainWeaponId) => void;
}) {
const user = useUser();
const data = useLoaderData<typeof loader>();
@ -1243,10 +1257,15 @@ function MapListMap({
</label>
<WeaponSelect
clearable
quickSelectWeaponsIds={recentlyReportedWeapons}
onChange={(weaponSplId) => {
const userId = user!.id;
const groupMatchMapId = map.id;
if (typeof weaponSplId === "number") {
addRecentlyReportedWeapon?.(weaponSplId);
}
onOwnWeaponSelected(
typeof weaponSplId === "number"
? {

View File

@ -0,0 +1,54 @@
import * as React from "react";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
const LOCAL_STORAGE_KEY = "sq__recently-reported-weapons";
const MAX_REPORTED_WEAPONS = 7;
/**
* This hook provides access to the list of recently reported weapons,
* which is persisted in local storage, and a function to add a new weapon
* to the list. The list is automatically loaded from local storage when
* the hook is first used.
*
* If a weapon is added that already exists in the list, it will be moved to the front of the list.
* If the list exceeds the maximum number of reported weapons, the oldest weapon will be removed.
*/
export function useRecentlyReportedWeapons() {
const [recentlyReportedWeapons, setReportedWeapons] = React.useState<
MainWeaponId[]
>([]);
React.useEffect(() => {
setReportedWeapons(getReportedWeaponsFromLocalStorage());
}, []);
const addRecentlyReportedWeapon = (weapon: MainWeaponId) => {
const newList = addReportedWeaponToLocalStorage(weapon);
setReportedWeapons(newList);
};
return { recentlyReportedWeapons, addRecentlyReportedWeapon };
}
const getReportedWeaponsFromLocalStorage = (): MainWeaponId[] => {
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
if (!stored) return [];
return JSON.parse(stored);
};
/** Adds weapon to list of recently reported weapons to local storage returning the current list */
const addReportedWeaponToLocalStorage = (weapon: MainWeaponId) => {
const stored = getReportedWeaponsFromLocalStorage();
const otherWeapons = stored.filter((storedWeapon) => storedWeapon !== weapon);
if (otherWeapons.length >= MAX_REPORTED_WEAPONS) {
otherWeapons.pop();
}
const newList = [weapon, ...otherWeapons];
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newList));
return newList;
};

View File

@ -157,6 +157,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Særregler",

View File

@ -157,6 +157,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Spezielle Regeln",

View File

@ -157,6 +157,7 @@
"forms.userSearch.noResults": "No users matching your search found",
"forms.weaponSearch.placeholder": "Select a weapon",
"forms.weaponSearch.search.placeholder": "Search weapons...",
"forms.weaponSearch.quickSelect": "Recent",
"forms.gearSearch.placeholder": "Select a gear",
"forms.gearSearch.search.placeholder": "Search gear...",
"tag.name.SPECIAL": "Special rules",

View File

@ -158,6 +158,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Reglas especiales",

View File

@ -158,6 +158,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Reglas especiales",

View File

@ -158,6 +158,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Règles spéciales",

View File

@ -158,6 +158,7 @@
"forms.userSearch.noResults": "Aucun utilisateur trouvé",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Règles spéciales",

View File

@ -157,6 +157,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "חוקים מיוחדים",

View File

@ -158,6 +158,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Regole speciali",

View File

@ -155,6 +155,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "特別ルール",

View File

@ -155,6 +155,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "특별 규칙",

View File

@ -157,6 +157,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Speciale spelregels",

View File

@ -158,6 +158,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Zasady specjalne",

View File

@ -158,6 +158,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Regras especiais",

View File

@ -158,6 +158,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "Особые правила",

View File

@ -155,6 +155,7 @@
"forms.userSearch.noResults": "",
"forms.weaponSearch.placeholder": "",
"forms.weaponSearch.search.placeholder": "",
"forms.weaponSearch.quickSelect": "",
"forms.gearSearch.placeholder": "",
"forms.gearSearch.search.placeholder": "",
"tag.name.SPECIAL": "特殊规则",