@@ -4,17 +4,96 @@ import useWorkspace from "@/lib/swr/use-workspace";
44import { GroupColorCircle } from "@/ui/partners/groups/group-color-circle" ;
55import { PartnerStatusBadges } from "@/ui/partners/partner-status-badges" ;
66import { ProgramEnrollmentStatus } from "@dub/prisma/client" ;
7- import { useRouterStuff } from "@dub/ui" ;
8- import { CircleDotted , FlagWavy , Users6 } from "@dub/ui/icons" ;
9- import { cn , COUNTRIES , nFormatter } from "@dub/utils" ;
7+ import { encodeRangeToken , parseRangeToken , useRouterStuff } from "@dub/ui" ;
8+ import {
9+ CircleDotted ,
10+ CursorRays ,
11+ FlagWavy ,
12+ InvoiceDollar ,
13+ MarketingTarget ,
14+ MoneyBills2 ,
15+ UserPlus ,
16+ Users6 ,
17+ } from "@dub/ui/icons" ;
18+ import { cn , COUNTRIES , currencyFormatter , nFormatter } from "@dub/utils" ;
1019import { useMemo } from "react" ;
1120
21+ const PARTNER_METRIC_RANGE = [
22+ {
23+ filterKey : "totalClicks" ,
24+ minParam : "totalClicksMin" ,
25+ maxParam : "totalClicksMax" ,
26+ metric : "totalClicks" as const ,
27+ label : "Clicks" ,
28+ icon : CursorRays ,
29+ } ,
30+ {
31+ filterKey : "totalLeads" ,
32+ minParam : "totalLeadsMin" ,
33+ maxParam : "totalLeadsMax" ,
34+ metric : "totalLeads" as const ,
35+ label : "Leads" ,
36+ icon : UserPlus ,
37+ } ,
38+ {
39+ filterKey : "totalConversions" ,
40+ minParam : "totalConversionsMin" ,
41+ maxParam : "totalConversionsMax" ,
42+ metric : "totalConversions" as const ,
43+ label : "Conversions" ,
44+ icon : MarketingTarget ,
45+ } ,
46+ {
47+ filterKey : "totalSaleAmount" ,
48+ minParam : "totalSaleAmountMin" ,
49+ maxParam : "totalSaleAmountMax" ,
50+ metric : "totalSaleAmount" as const ,
51+ label : "Revenue" ,
52+ icon : InvoiceDollar ,
53+ formatRangeBound : ( n : number ) => currencyFormatter ( n ) ,
54+ parseRangeInput : ( raw : string ) => {
55+ const n = Number . parseFloat ( raw . replace ( / [ ^ 0 - 9 . - ] / g, "" ) ) ;
56+ if ( ! Number . isFinite ( n ) ) {
57+ return Number . NaN ;
58+ }
59+ return Math . round ( n * 100 ) ;
60+ } ,
61+ } ,
62+ {
63+ filterKey : "totalCommissions" ,
64+ minParam : "totalCommissionsMin" ,
65+ maxParam : "totalCommissionsMax" ,
66+ metric : "totalCommissions" as const ,
67+ label : "Commissions" ,
68+ icon : MoneyBills2 ,
69+ formatRangeBound : ( n : number ) => currencyFormatter ( n ) ,
70+ parseRangeInput : ( raw : string ) => {
71+ const n = Number . parseFloat ( raw . replace ( / [ ^ 0 - 9 . - ] / g, "" ) ) ;
72+ if ( ! Number . isFinite ( n ) ) {
73+ return Number . NaN ;
74+ }
75+ return Math . round ( n * 100 ) ;
76+ } ,
77+ } ,
78+ ] as const ;
79+
80+ export type PartnerFilterKey =
81+ | "groupId"
82+ | "status"
83+ | "country"
84+ | ( typeof PARTNER_METRIC_RANGE ) [ number ] [ "filterKey" ] ;
85+
1286export function usePartnerFilters (
1387 extraSearchParams : Record < string , string > ,
14- enabledFilters : ( "groupId" | "status" | "country" ) [ ] = [
88+ enabledFilters : PartnerFilterKey [ ] = [
1589 "groupId" ,
1690 "status" ,
1791 "country" ,
92+ "totalClicks" ,
93+ "totalLeads" ,
94+ "totalConversions" ,
95+ "totalSaleAmount" ,
96+ "totalCommissions" ,
1897 ] ,
1998) {
2099 const { searchParamsObj, queryParams } = useRouterStuff ( ) ;
@@ -25,6 +104,15 @@ export function usePartnerFilters(
25104
26105 const { groups } = useGroups ( ) ;
27106
107+ const cohortParams = useMemo (
108+ ( ) => ( {
109+ ...( searchParamsObj . groupId && { groupId : searchParamsObj . groupId } ) ,
110+ ...( searchParamsObj . country && { country : searchParamsObj . country } ) ,
111+ ...( searchParamsObj . search && { search : searchParamsObj . search } ) ,
112+ } ) ,
113+ [ searchParamsObj . groupId , searchParamsObj . country , searchParamsObj . search ] ,
114+ ) ;
115+
28116 const { partnersCount : countriesCount } = usePartnersCount <
29117 | {
30118 country : string ;
@@ -34,6 +122,7 @@ export function usePartnerFilters(
34122 > ( {
35123 groupBy : "country" ,
36124 status,
125+ ...cohortParams ,
37126 enabled : enabledFilters . includes ( "country" ) ,
38127 } ) ;
39128
@@ -44,7 +133,9 @@ export function usePartnerFilters(
44133 } [ ]
45134 | undefined
46135 > ( {
47- groupBy : "status" , // here we include all statuses to get the groupBy count
136+ groupBy : "status" ,
137+ status,
138+ ...cohortParams ,
48139 enabled : enabledFilters . includes ( "status" ) ,
49140 } ) ;
50141
@@ -57,6 +148,7 @@ export function usePartnerFilters(
57148 > ( {
58149 groupBy : "groupId" ,
59150 status,
151+ ...cohortParams ,
60152 enabled : enabledFilters . includes ( "groupId" ) ,
61153 } ) ;
62154
@@ -128,14 +220,17 @@ export function usePartnerFilters(
128220 key : "country" ,
129221 icon : FlagWavy ,
130222 label : "Location" ,
131- getOptionIcon : ( value ) => (
223+ separatorAfter : PARTNER_METRIC_RANGE . some ( ( m ) =>
224+ enabledFilters . includes ( m . filterKey ) ,
225+ ) ,
226+ getOptionIcon : ( value : string ) => (
132227 < img
133228 alt = { value }
134229 src = { `https://hatscripts.github.io/circle-flags/flags/${ value . toLowerCase ( ) } .svg` }
135230 className = "size-4 shrink-0"
136231 />
137232 ) ,
138- getOptionLabel : ( value ) => COUNTRIES [ value ] ,
233+ getOptionLabel : ( value : string ) => COUNTRIES [ value ] ,
139234 options :
140235 countriesCount
141236 ?. filter ( ( { country } ) => COUNTRIES [ country ] )
@@ -147,62 +242,187 @@ export function usePartnerFilters(
147242 } ,
148243 ]
149244 : [ ] ) ,
245+ ...PARTNER_METRIC_RANGE . filter ( ( m ) =>
246+ enabledFilters . includes ( m . filterKey ) ,
247+ ) . map ( ( m ) => {
248+ const formatRangeBound =
249+ "formatRangeBound" in m && m . formatRangeBound
250+ ? m . formatRangeBound
251+ : ( n : number ) => nFormatter ( n , { full : true } ) ;
252+ const parseRangeInput =
253+ "parseRangeInput" in m && m . parseRangeInput
254+ ? m . parseRangeInput
255+ : ( raw : string ) => {
256+ const n = Number . parseInt ( raw . replace ( / [ ^ \d - ] / g, "" ) , 10 ) ;
257+ return Number . isFinite ( n ) ? n : Number . NaN ;
258+ } ;
259+ return {
260+ key : m . filterKey ,
261+ icon : m . icon ,
262+ label : m . label ,
263+ type : "range" as const ,
264+ options : null ,
265+ ...( m . metric === "totalCommissions"
266+ ? {
267+ rangeDisplayScale : 100 ,
268+ rangeNumberStep : 0.01 ,
269+ }
270+ : { } ) ,
271+ formatRangeBound,
272+ parseRangeInput,
273+ formatRangePillLabel : ( token : string ) => {
274+ const { min, max } = parseRangeToken ( token ) ;
275+ if ( min != null && max != null ) {
276+ return `${ formatRangeBound ( min ) } – ${ formatRangeBound ( max ) } ` ;
277+ }
278+ if ( min != null ) {
279+ return `${ formatRangeBound ( min ) } – No max` ;
280+ }
281+ if ( max != null ) {
282+ return `No min – ${ formatRangeBound ( max ) } ` ;
283+ }
284+ return token ;
285+ } ,
286+ } ;
287+ } ) ,
150288 ] ,
151- [ groupsCount , groups , statusCount , countriesCount ] ,
289+ [ groupsCount , groups , statusCount , countriesCount , slug , enabledFilters ] ,
152290 ) ;
153291
154292 const activeFilters = useMemo ( ( ) => {
155- const { groupId, status, country } = searchParamsObj ;
293+ const { groupId, status : statusParam , country } = searchParamsObj ;
156294
157295 return [
158296 ...( enabledFilters . includes ( "groupId" ) && groupId
159297 ? [ { key : "groupId" , value : groupId } ]
160298 : [ ] ) ,
161- ...( enabledFilters . includes ( "status" ) && status
162- ? [ { key : "status" , value : status } ]
299+ ...( enabledFilters . includes ( "status" ) && statusParam
300+ ? [ { key : "status" , value : statusParam } ]
163301 : [ ] ) ,
164302 ...( enabledFilters . includes ( "country" ) && country
165303 ? [ { key : "country" , value : country } ]
166304 : [ ] ) ,
305+ ...PARTNER_METRIC_RANGE . filter ( ( m ) =>
306+ enabledFilters . includes ( m . filterKey ) ,
307+ ) . flatMap ( ( m ) => {
308+ const minRaw = searchParamsObj [ m . minParam ] ;
309+ const maxRaw = searchParamsObj [ m . maxParam ] ;
310+ const min =
311+ minRaw !== undefined && minRaw !== "" ? Number ( minRaw ) : undefined ;
312+ const max =
313+ maxRaw !== undefined && maxRaw !== "" ? Number ( maxRaw ) : undefined ;
314+ const minOk = min !== undefined && Number . isFinite ( min ) ;
315+ const maxOk = max !== undefined && Number . isFinite ( max ) ;
316+ if ( ! minOk && ! maxOk ) {
317+ return [ ] ;
318+ }
319+ return [
320+ {
321+ key : m . filterKey ,
322+ value : encodeRangeToken (
323+ minOk ? min : undefined ,
324+ maxOk ? max : undefined ,
325+ ) ,
326+ } ,
327+ ] ;
328+ } ) ,
167329 ] ;
168- } , [ searchParamsObj ] ) ;
330+ } , [ searchParamsObj , enabledFilters ] ) ;
331+
332+ const onSelect = ( key : string , value : unknown ) => {
333+ const metric = PARTNER_METRIC_RANGE . find ( ( m ) => m . filterKey === key ) ;
334+ if ( metric ) {
335+ const { min, max } = parseRangeToken ( String ( value ) ) ;
336+ queryParams ( {
337+ set : {
338+ ...( min != null ? { [ metric . minParam ] : String ( min ) } : { } ) ,
339+ ...( max != null ? { [ metric . maxParam ] : String ( max ) } : { } ) ,
340+ } ,
341+ del : [
342+ ...( min == null ? [ metric . minParam ] : [ ] ) ,
343+ ...( max == null ? [ metric . maxParam ] : [ ] ) ,
344+ "page" ,
345+ ] ,
346+ } ) ;
347+ return ;
348+ }
169349
170- const onSelect = ( key : string , value : any ) =>
171350 queryParams ( {
172- set : {
173- [ key ] : value ,
174- } ,
351+ set : { [ key ] : value as string } ,
175352 del : "page" ,
176353 } ) ;
354+ } ;
355+
356+ const onRemove = ( key : string , _value ?: unknown ) => {
357+ const metric = PARTNER_METRIC_RANGE . find ( ( m ) => m . filterKey === key ) ;
358+ if ( metric ) {
359+ queryParams ( {
360+ del : [ metric . minParam , metric . maxParam , "page" ] ,
361+ } ) ;
362+ return ;
363+ }
177364
178- const onRemove = ( key : string ) =>
179365 queryParams ( {
180366 del : [ key , "page" ] ,
181367 } ) ;
368+ } ;
369+
370+ const onRemoveFilter = ( key : string ) => {
371+ onRemove ( key ) ;
372+ } ;
182373
183374 const onRemoveAll = ( ) =>
184375 queryParams ( {
185- del : [ "status" , "country" , "groupId" , "search" ] ,
376+ del : [
377+ "status" ,
378+ "country" ,
379+ "groupId" ,
380+ "search" ,
381+ "totalClicksMin" ,
382+ "totalClicksMax" ,
383+ "totalLeadsMin" ,
384+ "totalLeadsMax" ,
385+ "totalConversionsMin" ,
386+ "totalConversionsMax" ,
387+ "totalSaleAmountMin" ,
388+ "totalSaleAmountMax" ,
389+ "totalCommissionsMin" ,
390+ "totalCommissionsMax" ,
391+ "page" ,
392+ ] ,
186393 } ) ;
187394
188- const searchQuery = useMemo (
189- ( ) =>
190- new URLSearchParams ( {
191- ...Object . fromEntries (
192- activeFilters . map ( ( { key, value } ) => [ key , value ] ) ,
193- ) ,
194- ...( searchParamsObj . search && { search : searchParamsObj . search } ) ,
195- workspaceId : workspaceId || "" ,
196- ...extraSearchParams ,
197- } ) . toString ( ) ,
198- [ activeFilters , workspaceId , extraSearchParams ] ,
199- ) ;
395+ const searchQuery = useMemo ( ( ) => {
396+ const acc : Record < string , string > = {
397+ workspaceId : workspaceId || "" ,
398+ ...extraSearchParams ,
399+ } ;
400+ if ( searchParamsObj . search ) {
401+ acc . search = searchParamsObj . search ;
402+ }
403+ for ( const { key, value } of activeFilters ) {
404+ const metric = PARTNER_METRIC_RANGE . find ( ( m ) => m . filterKey === key ) ;
405+ if ( metric ) {
406+ const { min, max } = parseRangeToken ( String ( value ) ) ;
407+ if ( min != null ) {
408+ acc [ metric . minParam ] = String ( min ) ;
409+ }
410+ if ( max != null ) {
411+ acc [ metric . maxParam ] = String ( max ) ;
412+ }
413+ } else {
414+ acc [ key ] = String ( value ) ;
415+ }
416+ }
417+ return new URLSearchParams ( acc ) . toString ( ) ;
418+ } , [ activeFilters , workspaceId , extraSearchParams , searchParamsObj . search ] ) ;
200419
201420 return {
202421 filters,
203422 activeFilters,
204423 onSelect,
205424 onRemove,
425+ onRemoveFilter,
206426 onRemoveAll,
207427 searchQuery,
208428 } ;
0 commit comments