@@ -37,7 +37,10 @@ import { PlayerType, usePlayers } from "@/contexts/players-context";
3737import { usePreferences } from "@/contexts/preferences-context" ;
3838
3939import { 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" ;
4144import { UnblurDialog } from "@/components/dialogs/unblur-dialog" ;
4245import BundleSheet from "@/components/sheets/bundle-sheet" ;
4346import {
@@ -47,7 +50,10 @@ import {
4750 AccordionTrigger ,
4851 AccordionTriggerNoToggle ,
4952} from "@/components/ui/accordion" ;
53+ import { Command , CommandInput } from "@/components/ui/command" ;
5054import { Progress } from "@/components/ui/progress" ;
55+ import { ToggleGroup , ToggleGroupItem } from "@/components/ui/toggle-group" ;
56+ import { cn } from "@/lib/utils" ;
5157import { useMediaQuery } from "@react-hook/media-query" ;
5258import { IconSettings } from "@tabler/icons-react" ;
5359import 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+
6375type 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+
7694const 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