@@ -15,9 +15,16 @@ import {
1515} from '@/src/components/ui/pagination'
1616import { Slider } from '@/src/components/ui/slider'
1717import { cn } from '@/src/lib/utils'
18- import { ChevronDown , ChevronUp , Minus , Star } from 'lucide-react'
1918import {
20- ParserBuilder ,
19+ ChevronDown ,
20+ ChevronUp ,
21+ Dices ,
22+ Minus ,
23+ Star ,
24+ Trash2
25+ } from 'lucide-react'
26+ import {
27+ createMultiParser ,
2128 createParser ,
2229 parseAsBoolean ,
2330 parseAsFloat ,
@@ -27,8 +34,11 @@ import {
2734 parseAsIsoDate ,
2835 parseAsIsoDateTime ,
2936 parseAsJson ,
37+ parseAsNativeArrayOf ,
3038 parseAsStringLiteral ,
3139 parseAsTimestamp ,
40+ ParserBuilder ,
41+ SingleParser ,
3242 useQueryState
3343} from 'nuqs'
3444import React from 'react'
@@ -465,6 +475,244 @@ export function CustomParserDemo() {
465475 )
466476}
467477
478+ export function NativeArrayParserDemo ( ) {
479+ const [ value , setValue ] = useQueryState (
480+ 'nativeArray' ,
481+ parseAsNativeArrayOf ( parseAsInteger )
482+ )
483+ return (
484+ < DemoContainer demoKey = "nativeArray" >
485+ < Button
486+ onClick = { ( ) =>
487+ setValue ( prev => prev . concat ( Math . floor ( Math . random ( ) * 500 ) + 1 ) )
488+ }
489+ >
490+ < Dices size = { 18 } className = "mr-2 inline-block" role = "presentation" />
491+ Add random number
492+ </ Button >
493+ < Button
494+ onClick = { ( ) => setValue ( prev => prev . slice ( 0 , - 1 ) ) }
495+ disabled = { value . length === 0 }
496+ >
497+ < Trash2 size = { 16 } className = "mr-2 inline-block" role = "presentation" />
498+ Remove last number
499+ </ Button >
500+ < Button
501+ variant = "secondary"
502+ onClick = { ( ) => setValue ( [ ] ) }
503+ className = "ml-auto"
504+ >
505+ Clear
506+ </ Button >
507+ < CodeBlock
508+ lang = "json"
509+ code = { JSON . stringify ( value ) }
510+ allowCopy = { false }
511+ className = "my-0 w-full"
512+ />
513+ </ DemoContainer >
514+ )
515+ }
516+
517+ export function CustomMultiParserDemo ( ) {
518+ const parseAsFromTo = createParser ( {
519+ parse : value => {
520+ const [ min = null , max = null ] = value
521+ . split ( '~' )
522+ . map ( parseAsInteger . parse )
523+ if ( min === null ) return null
524+ if ( max === null ) return { eq : min }
525+ return { gte : min , lte : max }
526+ } ,
527+ serialize : value => {
528+ return value . eq !== undefined
529+ ? String ( value . eq )
530+ : `${ value . gte } ~${ value . lte } `
531+ }
532+ } )
533+
534+ const parseAsKeyValue = createParser ( {
535+ parse : value => {
536+ const [ key , val ] = value . split ( ':' )
537+ if ( ! key || ! val ) return null
538+ return { key, value : val }
539+ } ,
540+ serialize : value => {
541+ return `${ value . key } :${ value . value } `
542+ }
543+ } )
544+
545+ const parseAsFilters = < TItem extends { } > (
546+ itemParser : SingleParser < TItem >
547+ ) => {
548+ return createMultiParser ( {
549+ parse : values => {
550+ const keyValue = values
551+ . map ( parseAsKeyValue . parse )
552+ . filter ( v => v !== null )
553+
554+ const result = Object . fromEntries (
555+ keyValue . flatMap ( ( { key, value } ) => {
556+ const parsedValue : TItem | null = itemParser . parse ( value )
557+ return parsedValue === null ? [ ] : [ [ key , parsedValue ] ]
558+ } )
559+ )
560+
561+ return Object . keys ( result ) . length === 0 ? null : result
562+ } ,
563+ serialize : values => {
564+ return Object . entries ( values )
565+ . map ( ( [ key , value ] ) => {
566+ if ( ! itemParser . serialize ) return null
567+ return parseAsKeyValue . serialize ( {
568+ key,
569+ value : itemParser . serialize ( value )
570+ } )
571+ } )
572+ . filter ( v => v !== null )
573+ }
574+ } )
575+ }
576+
577+ const [ filters , setFilters ] = useQueryState (
578+ 'filters' ,
579+ parseAsFilters ( parseAsFromTo ) . withDefault ( { } )
580+ )
581+
582+ return (
583+ < DemoContainer demoKey = "filters" >
584+ < div >
585+ < label className = "text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" >
586+ Rating:
587+ </ label >
588+ < input
589+ type = "number"
590+ className = "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 flex-1 rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
591+ value = { filters . rating ?. eq ?? '' }
592+ min = { 0 }
593+ max = { 5 }
594+ onChange = { e => {
595+ setFilters ( prev => ( {
596+ ...prev ,
597+ rating : { eq : e . target . value === '' ? 0 : e . target . valueAsNumber }
598+ } ) )
599+ } }
600+ autoComplete = "off"
601+ />
602+ </ div >
603+ < div >
604+ < label className = "text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" >
605+ Price From:
606+ </ label >
607+ < input
608+ type = "number"
609+ className = "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 flex-1 rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
610+ value = { filters . price ?. gte ?? 0 }
611+ step = { 10 }
612+ max = { 1000 }
613+ onChange = { e => {
614+ setFilters ( prev => ( {
615+ ...prev ,
616+ price : {
617+ lte : prev . price ?. lte ?? 0 ,
618+ gte : e . target . value === '' ? 0 : e . target . valueAsNumber
619+ }
620+ } ) )
621+ } }
622+ autoComplete = "off"
623+ />
624+ </ div >
625+ < div >
626+ < label className = "text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" >
627+ Price To:
628+ </ label >
629+ < input
630+ type = "number"
631+ className = "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 flex-1 rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
632+ value = { filters . price ?. lte ?? 0 }
633+ step = { 10 }
634+ max = { 1000 }
635+ onChange = { e => {
636+ setFilters ( prev => ( {
637+ ...prev ,
638+ price : {
639+ gte : prev . price ?. gte ?? 0 ,
640+ lte : e . target . value === '' ? 0 : e . target . valueAsNumber
641+ }
642+ } ) )
643+ } }
644+ autoComplete = "off"
645+ />
646+ </ div >
647+ < Button
648+ variant = "secondary"
649+ onClick = { ( ) => setFilters ( null ) }
650+ className = "mt-auto ml-auto"
651+ >
652+ Clear
653+ </ Button >
654+ </ DemoContainer >
655+ )
656+
657+ return (
658+ < DemoContainer demoKey = "filters" >
659+ { Object . entries ( filters ) . map ( ( [ key , value ] ) => {
660+ if ( value . eq !== undefined ) {
661+ return (
662+ < div key = { key } >
663+ < label className = "text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" >
664+ { key } :{ ' ' }
665+ </ label >
666+ < input
667+ key = { key }
668+ type = "number"
669+ className = "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 flex-1 rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
670+ value = { value . eq }
671+ onChange = { e => {
672+ setFilters ( prev => ( {
673+ ...prev ,
674+ [ key ] : { eq : e . target . valueAsNumber }
675+ } ) )
676+ } }
677+ placeholder = "What's your favourite number?"
678+ autoComplete = "off"
679+ />
680+ </ div >
681+ )
682+ }
683+ return (
684+ < div key = { key } >
685+ < label className = "text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" >
686+ { key } :{ ' ' }
687+ </ label >
688+ < input
689+ key = { key }
690+ type = "number"
691+ className = "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 flex-1 rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
692+ value = { value . eq }
693+ onChange = { e => {
694+ setFilters ( prev => ( {
695+ ...prev ,
696+ [ key ] : { eq : e . target . valueAsNumber }
697+ } ) )
698+ } }
699+ placeholder = "What's your favourite number?"
700+ autoComplete = "off"
701+ />
702+ </ div >
703+ )
704+ } ) }
705+ < Button
706+ variant = "secondary"
707+ onClick = { ( ) => setFilters ( null ) }
708+ className = "ml-auto"
709+ >
710+ Clear
711+ </ Button >
712+ </ DemoContainer >
713+ )
714+ }
715+
468716type StarButtonProps = Omit < React . ComponentProps < typeof Button > , 'value' > & {
469717 index : Rating
470718 value : Rating | null
0 commit comments