@@ -10,6 +10,7 @@ import {
1010 Clapperboard ,
1111 ListVideo ,
1212 Calendar ,
13+ Crosshair ,
1314 User ,
1415 X ,
1516} from " lucide-vue-next" ;
@@ -88,6 +89,15 @@ const SINCE_OPTIONS: Array<{ value: SincePreset; label: string }> = [
8889 { value: " 90d" , label: " Last 90 days" },
8990];
9091
92+ type KillsPreset = " any" | " 2" | " 3" | " 4" | " 5" ;
93+ const KILLS_OPTIONS: Array <{ value: KillsPreset ; label: string }> = [
94+ { value: " any" , label: " Any kills" },
95+ { value: " 2" , label: " 2+ kills" },
96+ { value: " 3" , label: " 3+ kills" },
97+ { value: " 4" , label: " 4+ kills" },
98+ { value: " 5" , label: " 5K (Ace)" },
99+ ];
100+
91101const playerFilter = computed <string | null >(() => {
92102 const v = route .query .player ;
93103 return typeof v === " string" && v .length > 0 ? v : null ;
@@ -100,6 +110,14 @@ const sinceFilter = computed<SincePreset>(() => {
100110 }
101111 return " all" ;
102112});
113+ const killsFilter = computed <KillsPreset >(() => {
114+ const v = route .query .kills ;
115+ if (typeof v === " string" ) {
116+ const match = KILLS_OPTIONS .find ((o ) => o .value === v );
117+ if (match ) return match .value ;
118+ }
119+ return " any" ;
120+ });
103121
104122function setSince(v : SincePreset ) {
105123 const next = { ... route .query } as Record <string , any >;
@@ -108,6 +126,13 @@ function setSince(v: SincePreset) {
108126 router .replace ({ path: route .path , query: next , hash: route .hash });
109127}
110128
129+ function setKills(v : KillsPreset ) {
130+ const next = { ... route .query } as Record <string , any >;
131+ if (v === " any" ) delete next .kills ;
132+ else next .kills = v ;
133+ router .replace ({ path: route .path , query: next , hash: route .hash });
134+ }
135+
111136function clearPlayer() {
112137 pickedPlayerName .value = null ;
113138 const next = { ... route .query } as Record <string , any >;
@@ -209,6 +234,11 @@ function subscribe() {
209234 if (cutoff ) {
210235 conditions .push ({ created_at: { _gte: cutoff } });
211236 }
237+ if (killsFilter .value !== " any" ) {
238+ conditions .push ({
239+ kills_count: { _gte: parseInt (killsFilter .value , 10 ) },
240+ });
241+ }
212242 const where = conditions .length === 0 ? {} : { _and: conditions };
213243 const obs = getGraphqlClient ().subscribe ({
214244 query: generateSubscription ({
@@ -235,7 +265,7 @@ function subscribe() {
235265}
236266subscribe ();
237267
238- watch ([playerFilter , sinceFilter , isAdmin ], () => subscribe ());
268+ watch ([playerFilter , sinceFilter , killsFilter , isAdmin ], () => subscribe ());
239269onBeforeUnmount (() => {
240270 activeSub ?.unsubscribe ();
241271 pendingSub ?.unsubscribe ();
@@ -290,11 +320,24 @@ const hasClips = computed(() => filteredClips.value.length > 0);
290320
291321// Multi-clip matches collapse to a MatchClipsGroupCard, singletons
292322// stay as HighlightCards. Subscription capped at 200, so client-side
293- // grouping is fine.
323+ // grouping is fine. When the user has filtered to a specific player,
324+ // skip grouping entirely — they're browsing that player's individual
325+ // plays, not the match-by-match digest.
294326type GridItem =
295327 | { kind: " single" ; clip: Clip ; sortKey: string }
296328 | { kind: " group" ; matchId: string ; clips: Clip []; sortKey: string };
297329const gridItems = computed <GridItem []>(() => {
330+ if (playerFilter .value ) {
331+ return filteredClips .value
332+ .map <GridItem >((clip ) => ({
333+ kind: " single" ,
334+ clip ,
335+ sortKey: clip .created_at ,
336+ }))
337+ .sort ((a , b ) =>
338+ a .sortKey < b .sortKey ? 1 : a .sortKey > b .sortKey ? - 1 : 0 ,
339+ );
340+ }
298341 const byMatch = new Map <string , Clip []>();
299342 const orphans: Clip [] = [];
300343 for (const c of filteredClips .value ) {
@@ -491,6 +534,39 @@ const adminFilters: Array<{ value: Filter; label: string; icon?: any }> = [
491534 </SelectContent >
492535 </Select >
493536
537+ <Select
538+ :model-value =" killsFilter "
539+ @update :model-value =" (v ) => setKills (v as KillsPreset ) "
540+ >
541+ <SelectTrigger
542+ class="h-8 w-auto min-w-[9rem] gap-2 rounded-full border-border/60 bg-muted/30 px-3 text-xs"
543+ :class ="
544+ killsFilter !== ' any'
545+ ? ' border-[hsl(var(--tac-amber)/0.5)] bg-[hsl(var(--tac-amber)/0.12)] text-[hsl(var(--tac-amber))]'
546+ : ' '
547+ "
548+ >
549+ <Crosshair
550+ class="h-3.5 w-3.5 "
551+ :class ="
552+ killsFilter !== ' any'
553+ ? ' text-[hsl(var(--tac-amber))]'
554+ : ' text-muted-foreground'
555+ "
556+ />
557+ <SelectValue />
558+ </SelectTrigger >
559+ <SelectContent >
560+ <SelectItem
561+ v-for =" opt in KILLS_OPTIONS "
562+ :key =" opt .value "
563+ :value =" opt .value "
564+ >
565+ {{ opt.label }}
566+ </SelectItem >
567+ </SelectContent >
568+ </Select >
569+
494570 <span
495571 v-if =" !loading"
496572 class =" ml-auto font-mono text-[0.62rem] uppercase tracking-[0.16em] text-muted-foreground tabular-nums"
@@ -517,14 +593,16 @@ const adminFilters: Array<{ value: Filter; label: string; icon?: any }> = [
517593 <Empty >
518594 <EmptyTitle >
519595 {{
520- playerFilter || sinceFilter !== "all"
596+ playerFilter || sinceFilter !== "all" || killsFilter !== "any"
521597 ? "No clips match these filters"
522598 : "No clips here yet"
523599 }}
524600 </EmptyTitle >
525601 <EmptyDescription >
526- <template v-if =" playerFilter || sinceFilter !== ' all' " >
527- Try widening the date range or clearing the player filter.
602+ <template
603+ v-if =" playerFilter || sinceFilter !== ' all' || killsFilter !== ' any' "
604+ >
605+ Try widening the filters or clearing one.
528606 </template >
529607 <template v-else-if =" isAdmin " >
530608 Try a different filter — or wait for new clips to render.
@@ -534,7 +612,7 @@ const adminFilters: Array<{ value: Filter; label: string; icon?: any }> = [
534612 </template >
535613 </EmptyDescription >
536614 <div
537- v-if =" playerFilter || sinceFilter !== 'all'"
615+ v-if =" playerFilter || sinceFilter !== 'all' || killsFilter !== 'any' "
538616 class =" mt-3 flex flex-wrap items-center justify-center gap-2"
539617 >
540618 <button
@@ -555,6 +633,15 @@ const adminFilters: Array<{ value: Filter; label: string; icon?: any }> = [
555633 <X class="h-3 w-3" />
556634 Clear date
557635 </button >
636+ <button
637+ v-if =" killsFilter !== 'any'"
638+ type =" button"
639+ :class =" [tacticalFilterPillClasses]"
640+ @click =" setKills('any')"
641+ >
642+ <X class="h-3 w-3" />
643+ Clear kills
644+ </button >
558645 </div >
559646 </Empty >
560647 </PageTransition >
0 commit comments