diff --git a/app/components/WeaponSelect.tsx b/app/components/WeaponSelect.tsx index b159dd9ba..e17936d27 100644 --- a/app/components/WeaponSelect.tsx +++ b/app/components/WeaponSelect.tsx @@ -44,6 +44,8 @@ interface WeaponSelectProps< disabledWeaponIds?: Array; // 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; } // 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) { 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< ; +}) { 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, }; diff --git a/app/features/sendouq-match/routes/q.match.$id.tsx b/app/features/sendouq-match/routes/q.match.$id.tsx index 0f1bd7a74..fe21d442d 100644 --- a/app/features/sendouq-match/routes/q.match.$id.tsx +++ b/app/features/sendouq-match/routes/q.match.$id.tsx @@ -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() {
{ 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["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(); @@ -1243,10 +1257,15 @@ function MapListMap({ { const userId = user!.id; const groupMatchMapId = map.id; + if (typeof weaponSplId === "number") { + addRecentlyReportedWeapon?.(weaponSplId); + } + onOwnWeaponSelected( typeof weaponSplId === "number" ? { diff --git a/app/features/sendouq/q-hooks.ts b/app/features/sendouq/q-hooks.ts new file mode 100644 index 000000000..2699eb1fc --- /dev/null +++ b/app/features/sendouq/q-hooks.ts @@ -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; +}; diff --git a/locales/da/common.json b/locales/da/common.json index 5667e255f..46bf16843 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -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", diff --git a/locales/de/common.json b/locales/de/common.json index 01b7a1860..97110ff4c 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -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", diff --git a/locales/en/common.json b/locales/en/common.json index d70cd9525..398a74d61 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -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", diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index d473f680e..078e81482 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -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", diff --git a/locales/es-US/common.json b/locales/es-US/common.json index be43a95e7..3390cef34 100644 --- a/locales/es-US/common.json +++ b/locales/es-US/common.json @@ -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", diff --git a/locales/fr-CA/common.json b/locales/fr-CA/common.json index 6ac06af3f..e3a1501c4 100644 --- a/locales/fr-CA/common.json +++ b/locales/fr-CA/common.json @@ -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", diff --git a/locales/fr-EU/common.json b/locales/fr-EU/common.json index 374bda14b..9196d229e 100644 --- a/locales/fr-EU/common.json +++ b/locales/fr-EU/common.json @@ -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", diff --git a/locales/he/common.json b/locales/he/common.json index e932e51ef..2552b86a7 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -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": "חוקים מיוחדים", diff --git a/locales/it/common.json b/locales/it/common.json index 90f12333a..63d67d8dc 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -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", diff --git a/locales/ja/common.json b/locales/ja/common.json index c245b88d1..f65bf80d6 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -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": "特別ルール", diff --git a/locales/ko/common.json b/locales/ko/common.json index eafe63c57..3e931d958 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -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": "특별 규칙", diff --git a/locales/nl/common.json b/locales/nl/common.json index 85cce8b50..031630a0c 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -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", diff --git a/locales/pl/common.json b/locales/pl/common.json index a48191f6b..e6aa1f252 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -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", diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index e60acbc14..5dde1e314 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -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", diff --git a/locales/ru/common.json b/locales/ru/common.json index 0cb6d11ba..b56c3e0a1 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -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": "Особые правила", diff --git a/locales/zh/common.json b/locales/zh/common.json index b9d8930c7..4996a122d 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -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": "特殊规则",