Skip to content

Commit 2ee485b

Browse files
[STA-175] Filters & Searches for Bundles
Added filtering by complete/incomplete and searching to the bundles page. Searching is as permissive as possible so that searches like "pantry", "summer", and "egg" all work as expected. Text searching has the following qualities: - Matching a room name matches all bundles and items in that room - If the room isn't matched, matichng a bundle name matches all items in that bundle - If none of the above have matched, match against the item name - If a bundle is not matched or has no matched items, exclude it - If a room is not matched or has no matched bundles, exclude it
1 parent fa040fd commit 2ee485b

2 files changed

Lines changed: 169 additions & 37 deletions

File tree

apps/stardew.app/src/components/cards/bundle-item-card.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Dispatch, SetStateAction } from "react";
55
import { usePlayers } from "@/contexts/players-context";
66

77
import { categoryIcons, goldIcons } from "@/lib/constants";
8-
import { BundleItemWithLocation } from "@/types/bundles";
8+
import { BundleItem, BundleItemWithLocation } from "@/types/bundles";
99
import { BooleanCard } from "./boolean-card";
1010

1111
interface BundleItemCardProps {
@@ -29,6 +29,23 @@ interface BundleItemCardProps {
2929
setPromptOpen?: Dispatch<SetStateAction<boolean>>;
3030
}
3131

32+
const categoryItems: Record<string, string> = {
33+
"-4": "Any Fish",
34+
"-5": "Any Egg",
35+
"-6": "Any Milk",
36+
"-777": "Wild Seeds (Any)",
37+
};
38+
39+
export function bundleItemName<T extends BundleItem>(item: T): string {
40+
if (item.itemID == "-1") {
41+
return "Gold";
42+
} else if (item.itemID in categoryItems) {
43+
return categoryItems[item.itemID];
44+
}
45+
46+
return objects[item.itemID as keyof typeof objects]?.name;
47+
}
48+
3249
export const BundleItemCard = ({
3350
item,
3451
show,
@@ -52,13 +69,6 @@ export const BundleItemCard = ({
5269
let overrides: Record<string, string | number | boolean | undefined> = {};
5370
let unknownItem: Boolean = false;
5471

55-
const categoryItems: Record<string, string> = {
56-
"-4": "Any Fish",
57-
"-5": "Any Egg",
58-
"-6": "Any Milk",
59-
"-777": "Wild Seeds (Any)",
60-
};
61-
6272
if (item.itemID == "-1") {
6373
//Special case for handling gold in Vault bundles
6474
iconURL = goldIcons[item.itemQuantity.toString()];

apps/stardew.app/src/pages/bundles.tsx

Lines changed: 151 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ import { PlayerType, usePlayers } from "@/contexts/players-context";
3737
import { usePreferences } from "@/contexts/preferences-context";
3838

3939
import { AchievementCard } from "@/components/cards/achievement-card";
40-
import { BundleItemCard } from "@/components/cards/bundle-item-card";
40+
import {
41+
BundleItemCard,
42+
bundleItemName,
43+
} from "@/components/cards/bundle-item-card";
4144
import { UnblurDialog } from "@/components/dialogs/unblur-dialog";
4245
import BundleSheet from "@/components/sheets/bundle-sheet";
4346
import {
@@ -47,7 +50,10 @@ import {
4750
AccordionTrigger,
4851
AccordionTriggerNoToggle,
4952
} from "@/components/ui/accordion";
53+
import { Command, CommandInput } from "@/components/ui/command";
5054
import { Progress } from "@/components/ui/progress";
55+
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
56+
import { cn } from "@/lib/utils";
5157
import { useMediaQuery } from "@react-hook/media-query";
5258
import { IconSettings } from "@tabler/icons-react";
5359
import clsx from "clsx";
@@ -60,6 +66,12 @@ export const ItemQualityToString = {
6066
"3": "Iridium",
6167
};
6268

69+
const bubbleColors: Record<string, string> = {
70+
"0": "border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-950", // unknown or not completed
71+
"1": "border-yellow-900 bg-yellow-500/20", // known, but not completed
72+
"2": "border-green-900 bg-green-500/20", // completed
73+
};
74+
6375
type BundleAccordionProps = {
6476
bundleWithStatus: BundleWithStatus;
6577
children: JSX.Element | JSX.Element[];
@@ -73,6 +85,12 @@ type AccordionSectionProps = {
7385
completedCount?: number;
7486
};
7587

88+
type FilteredBundle = {
89+
bundleWithStatus: BundleWithStatus;
90+
items: (BundleItem | Randomizer)[];
91+
matchesSearch: boolean;
92+
};
93+
7694
const CommunityCenterRooms: CommunityCenterRoomName[] = [
7795
"Pantry",
7896
"Crafts Room",
@@ -417,6 +435,11 @@ export default function Bundles() {
417435
let [open, setIsOpen] = useState(false);
418436
let [object, setObject] = useState<BundleItemWithLocation | null>(null);
419437
let [bundles, setBundles] = useState<BundleWithStatus[]>([]);
438+
let [completeCount, setCompleteCount] = useState(0);
439+
let [incompleteCount, setIncompleteCount] = useState(0);
440+
let [filter, setFilter] = useState("all");
441+
let [search, setSearch] = useState("");
442+
420443
const { activePlayer, patchPlayer } = usePlayers();
421444

422445
function GetActiveBundles(
@@ -477,7 +500,13 @@ export default function Bundles() {
477500
}
478501

479502
useEffect(() => {
480-
setBundles(GetActiveBundles(activePlayer));
503+
const activeBundles = GetActiveBundles(activePlayer);
504+
const _completeCount = activeBundles.filter(BundleCompleted).length;
505+
const _incompleteCount = activeBundles.length - _completeCount;
506+
507+
setBundles(activeBundles);
508+
setCompleteCount(_completeCount);
509+
setIncompleteCount(_incompleteCount);
481510
}, [activePlayer]);
482511

483512
const getAchievementProgress = (name: string) => {
@@ -563,9 +592,53 @@ export default function Bundles() {
563592
);
564593
})}
565594
</AccordionSection>
595+
{/* Filters and Actions Row */}
596+
<div className="flex w-full flex-row items-center justify-between">
597+
<ToggleGroup
598+
variant="outline"
599+
type="single"
600+
value={filter}
601+
onValueChange={(val: string) =>
602+
setFilter(val === filter ? "all" : val)
603+
}
604+
className="gap-2"
605+
>
606+
<ToggleGroupItem value="0" aria-label="Show Incomplete">
607+
<span
608+
className={cn(
609+
"inline-block h-4 w-4 rounded-full border align-middle",
610+
bubbleColors["0"],
611+
)}
612+
/>
613+
<span className="align-middle">
614+
Incomplete ({incompleteCount})
615+
</span>
616+
</ToggleGroupItem>
617+
<ToggleGroupItem value="2" aria-label="Show Complete">
618+
<span
619+
className={cn(
620+
"inline-block h-4 w-4 rounded-full border align-middle",
621+
bubbleColors["2"],
622+
)}
623+
/>
624+
<span className="align-middle">Complete ({completeCount})</span>
625+
</ToggleGroupItem>
626+
</ToggleGroup>
627+
</div>
628+
{/* Search Bar Row */}
629+
<div className="mt-2 w-full">
630+
<Command className="w-full border border-b-0 dark:border-neutral-800">
631+
<CommandInput
632+
onValueChange={(v) => setSearch(v?.toLowerCase())}
633+
placeholder="Search Bundles"
634+
/>
635+
</Command>
636+
</div>
566637
{CommunityCenterRooms.map((roomName: CommunityCenterRoomName) => {
567638
let roomBundles: BundleWithStatus[] = [];
568639
let completedCount = 0;
640+
const roomMatched =
641+
!search || roomName.toLowerCase().includes(search);
569642
if (activePlayer && Array.isArray(activePlayer.bundles)) {
570643
roomBundles = activePlayer.bundles.filter((bundleWithStatus) => {
571644
if (bundleWithStatus?.bundle) {
@@ -584,13 +657,64 @@ export default function Bundles() {
584657
bundleWithStatus.bundle.areaName === roomName,
585658
);
586659
}
660+
const filteredBundles: FilteredBundle[] = roomBundles
661+
.filter((bundleWithStatus) => {
662+
switch (filter) {
663+
case "0":
664+
return !BundleCompleted(bundleWithStatus);
665+
case "2":
666+
return BundleCompleted(bundleWithStatus);
667+
case "all":
668+
default:
669+
return true;
670+
}
671+
})
672+
.map((bundleWithStatus): FilteredBundle => {
673+
const bundleMatched =
674+
roomMatched ||
675+
bundleWithStatus.bundle.name.toLowerCase().includes(search);
676+
677+
return {
678+
bundleWithStatus: bundleWithStatus,
679+
items: bundleWithStatus.bundle.items
680+
.filter((_, idx) => {
681+
switch (filter) {
682+
case "0":
683+
return !bundleWithStatus.bundleStatus[idx];
684+
case "2":
685+
return bundleWithStatus.bundleStatus[idx];
686+
case "all":
687+
default:
688+
return true;
689+
}
690+
})
691+
.filter((item) => {
692+
if (bundleMatched) {
693+
return true;
694+
}
695+
696+
return (
697+
!isRandomizer(item) &&
698+
bundleItemName(item).toLowerCase().includes(search)
699+
);
700+
}),
701+
matchesSearch: bundleMatched,
702+
};
703+
})
704+
.filter((filteredBundle) => filteredBundle.items.length !== 0);
705+
706+
if (filteredBundles.length === 0) {
707+
return;
708+
}
709+
587710
return (
588711
<AccordionSection
589712
key={roomName}
590713
title={roomName}
591714
completedCount={completedCount}
592715
>
593-
{roomBundles.map((bundleWithStatus: BundleWithStatus) => {
716+
{filteredBundles.map((filteredBundle: FilteredBundle) => {
717+
const bundleWithStatus = filteredBundle.bundleWithStatus;
594718
return (
595719
<BundleAccordion
596720
key={bundleWithStatus.bundle.localizedName}
@@ -606,32 +730,30 @@ export default function Bundles() {
606730
})}
607731
onChangeBundle={SwapBundle}
608732
>
609-
{bundleWithStatus.bundle.items.map ? (
610-
bundleWithStatus.bundle.items.map(
611-
(item, index: number) => {
612-
if (isRandomizer(item)) {
613-
// Guard clause for type coercion
614-
return <></>;
615-
}
616-
const BundleItemWithLocation: BundleItemWithLocation =
617-
{
618-
...item,
619-
index: index,
620-
bundleID: bundleWithStatus.bundle.name,
621-
};
622-
return (
623-
<BundleItemCard
624-
key={item.itemID + "-" + index}
625-
item={BundleItemWithLocation}
626-
setIsOpen={setIsOpen}
627-
completed={bundleWithStatus.bundleStatus[index]}
628-
setObject={setObject}
629-
show={show}
630-
setPromptOpen={setPromptOpen}
631-
/>
632-
);
633-
},
634-
)
733+
{filteredBundle.items ? (
734+
filteredBundle.items.map((item, index: number) => {
735+
if (isRandomizer(item)) {
736+
// Guard clause for type coercion
737+
return <></>;
738+
}
739+
const BundleItemWithLocation: BundleItemWithLocation =
740+
{
741+
...item,
742+
index: index,
743+
bundleID: bundleWithStatus.bundle.name,
744+
};
745+
return (
746+
<BundleItemCard
747+
key={item.itemID + "-" + index}
748+
item={BundleItemWithLocation}
749+
setIsOpen={setIsOpen}
750+
completed={bundleWithStatus.bundleStatus[index]}
751+
setObject={setObject}
752+
show={show}
753+
setPromptOpen={setPromptOpen}
754+
/>
755+
);
756+
})
635757
) : (
636758
<>error</>
637759
)}

0 commit comments

Comments
 (0)