Skip to content

Commit 8021309

Browse files
committed
revenue range
1 parent 603a5a6 commit 8021309

File tree

4 files changed

+155
-86
lines changed

4 files changed

+155
-86
lines changed

src/pages/nonprofit-outreach/api.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ class Filter {
7575
});
7676
}
7777
}
78+
range<T extends string>(kv: { [key in T]: T }) {
79+
const [[k, v]] = Object.entries(kv);
80+
if (!v) return;
81+
const range = this.extract_blank_exists(k, v as string);
82+
if (range.length > 1) {
83+
this.filter.$and ||= [];
84+
this.filter.$and.push({
85+
[k]: { $gte: Number(range[0]), $lte: Number(range[1]) },
86+
});
87+
}
88+
}
7889

7990
get all() {
8091
return this.filter;
@@ -102,6 +113,9 @@ export const loader: LoaderFunction = async ({ request }) => {
102113
exempt_organization_status_code,
103114
organization_code,
104115
filing_requirement_code,
116+
income_amount,
117+
asset_amount,
118+
revenue_amount,
105119
} = Object.fromEntries(url.searchParams.entries());
106120

107121
const filter = new Filter();
@@ -121,6 +135,9 @@ export const loader: LoaderFunction = async ({ request }) => {
121135
filter.opts({ exempt_organization_status_code });
122136
filter.opts({ filing_requirement_code });
123137
filter.starts_with({ ntee_code });
138+
filter.range({ income_amount });
139+
filter.range({ asset_amount });
140+
filter.range({ revenue_amount });
124141

125142
const skip = (+page - 1) * +limit;
126143

src/pages/nonprofit-outreach/index.tsx

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Link, useSearchParams } from "@remix-run/react";
22
import type { LoaderData } from "./api";
33
import { ListFilter } from "./list-filter";
44
import { Paginator } from "./paginator";
5+
import { RangeFilter } from "./range-filter";
56
export { loader } from "./api";
67
export { clientLoader } from "api/cache";
78
import { useCachedLoaderData } from "api/cache";
@@ -97,7 +98,23 @@ export default function Page() {
9798
}}
9899
/>
99100
</th>
100-
<th>Assets</th>
101+
<th>
102+
<RangeFilter
103+
_key="asset_amount"
104+
label="Assets"
105+
values={(k) => params.get(k)?.split(",") || []}
106+
onChange={(vs, k) => {
107+
setParams((p) => {
108+
if (vs.length === 0) {
109+
p.delete(k);
110+
return p;
111+
}
112+
p.set(k, vs.join(","));
113+
return p;
114+
});
115+
}}
116+
/>
117+
</th>
101118
<th>
102119
<ListFilter
103120
_key="income_code"
@@ -117,8 +134,40 @@ export default function Page() {
117134
}}
118135
/>
119136
</th>
120-
<th>Income</th>
121-
<th>Revenue</th>
137+
<th>
138+
<RangeFilter
139+
_key="income_amount"
140+
label="Income"
141+
values={(k) => params.get(k)?.split(",") || []}
142+
onChange={(vs, k) => {
143+
setParams((p) => {
144+
if (vs.length === 0) {
145+
p.delete(k);
146+
return p;
147+
}
148+
p.set(k, vs.join(","));
149+
return p;
150+
});
151+
}}
152+
/>
153+
</th>
154+
<th>
155+
<RangeFilter
156+
_key="revenue_amount"
157+
label="Revenue"
158+
values={(k) => params.get(k)?.split(",") || []}
159+
onChange={(vs, k) => {
160+
setParams((p) => {
161+
if (vs.length === 0) {
162+
p.delete(k);
163+
return p;
164+
}
165+
p.set(k, vs.join(","));
166+
return p;
167+
});
168+
}}
169+
/>
170+
</th>
122171
<th>City</th>
123172
<th>
124173
<ListFilter
Lines changed: 85 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,102 @@
1-
import { valibotResolver } from "@hookform/resolvers/valibot";
2-
import { NativeField as Field, Form } from "components/form";
3-
import { Modal } from "components/modal";
4-
import { FilterIcon } from "lucide-react";
5-
import { useState } from "react";
1+
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
2+
import {
3+
NativeCheckField as CheckField,
4+
NativeField as Field,
5+
} from "components/form";
6+
import { ListFilterIcon } from "lucide-react";
67
import { useForm } from "react-hook-form";
7-
import * as v from "valibot";
8-
9-
type Val = [number, number];
10-
interface Init {
11-
min: number;
12-
max: number;
13-
}
148

159
interface Props {
16-
value: Val | Init;
17-
onChange: (value: [number, number]) => void;
10+
label: string;
11+
_key: string;
12+
values?: (k: string) => string[];
13+
onChange?: (v: string[], k: string) => void;
1814
classes?: string;
1915
}
2016

21-
const amnt = v.pipe(
22-
v.string(),
23-
v.transform((x) => +x),
24-
v.minValue(0),
25-
v.transform((x) => +x)
26-
);
27-
const schema = v.pipe(
28-
v.object({
29-
min: v.optional(amnt),
30-
max: v.optional(amnt),
31-
}),
32-
v.forward(
33-
v.partialCheck(
34-
[["min"], ["max"]],
35-
({ min, max }) => (min && max ? min <= max : true),
36-
"min must be less than or equal to max"
37-
),
38-
["min"]
39-
)
40-
);
41-
42-
interface Fv extends v.InferInput<typeof schema> {}
43-
interface FvParsed extends v.InferOutput<typeof schema> {}
44-
4517
export function RangeFilter(props: Props) {
46-
const [open, setOpen] = useState(false);
47-
const isInit = !props.value || !Array.isArray(props.value);
48-
49-
const vmin =
50-
props.value && Array.isArray(props.value)
51-
? props.value[0]
52-
: props.value?.min;
53-
const vmax =
54-
props.value && Array.isArray(props.value)
55-
? props.value[1]
56-
: props.value?.max;
18+
const raw = props.values?.(props._key) || [];
19+
const is_active = raw.length > 0;
20+
const is_blank = raw.includes("blank");
21+
const is_exists = raw.includes("exists");
22+
const range = raw.filter((t) => t !== "blank" && t !== "exists");
5723

5824
const {
59-
handleSubmit,
6025
register,
61-
formState: { errors },
62-
} = useForm<Fv>({
63-
resolver: valibotResolver(schema),
64-
defaultValues: { min: vmin?.toString(), max: vmax?.toString() },
26+
handleSubmit,
27+
reset,
28+
formState: { isDirty, errors },
29+
watch,
30+
} = useForm({
31+
values: {
32+
blank: is_blank,
33+
exists: is_exists,
34+
min: range[0] || "",
35+
max: range[1] || "",
36+
},
6537
});
38+
39+
const min = watch("min");
6640
return (
67-
<div className={`flex items-center ${props.classes}`}>
68-
<button type="button">
69-
<FilterIcon
70-
size={16}
71-
className={`${isInit ? "text-gray-l1" : "text-blue-d1"}`}
41+
<Popover className="relative flex items-start justify-between gap-x-2">
42+
<p>{props.label}</p>
43+
<PopoverButton className="mt-1">
44+
<ListFilterIcon
45+
size={14}
46+
className={`${is_active ? "text-green stroke-3" : ""}`}
7247
/>
73-
</button>
74-
<Modal
75-
open={open}
76-
onClose={() => setOpen(false)}
77-
classes="max-w-xl rounded"
48+
</PopoverButton>
49+
<PopoverPanel
50+
as="form"
51+
anchor={{ to: "bottom", gap: 8 }}
52+
className="bg-white w-max border border-gray-l3 p-2 grid rounded-sm gap-2"
53+
onReset={(e) => {
54+
e.preventDefault();
55+
reset(undefined, { keepDirtyValues: false });
56+
props.onChange?.([], props._key);
57+
}}
58+
onSubmit={handleSubmit(({ blank, exists, ...texts }) => {
59+
const text = Object.values(texts) as string[];
60+
const vals = text
61+
.concat([blank ? "blank" : "", exists ? "exists" : ""])
62+
.filter((x) => x);
63+
props.onChange?.(vals, props._key);
64+
})}
7865
>
79-
<Form
80-
onSubmit={handleSubmit((fv) => {
81-
const { min, max } = fv as FvParsed;
66+
<CheckField {...register("blank")} classes="text-xs">
67+
Blank
68+
</CheckField>
69+
<CheckField {...register("exists")} classes="mb-2 text-xs">
70+
Exists
71+
</CheckField>
8272

83-
props.onChange([min, max]);
84-
setOpen(false);
85-
})}
86-
className="rounded p-4"
87-
>
88-
<Field label="Min" {...register("min")} error={errors.min?.message} />
89-
<Field label="Max" {...register("min")} error={errors.max?.message} />
90-
<button className="btn btn-blue px-4 py-1 text-sm">Apply</button>
91-
</Form>
92-
</Modal>
93-
</div>
73+
<Field
74+
label="Min"
75+
type="number"
76+
error={errors.min?.message}
77+
{...register("min", { min: 0 })}
78+
classes={{ input: "text-xs py-1 px-2", label: "mb-0! text-xs" }}
79+
/>
80+
<Field
81+
label="Max"
82+
type="number"
83+
error={errors.max?.message}
84+
{...register("max", { min, max: 10_000_000_000 })}
85+
classes={{ input: "text-xs py-1 px-2", label: "mb-0! text-xs" }}
86+
/>
87+
<div className="flex justify-end space-x-2 mt-4">
88+
<button type="reset" className="btn btn-outline text-xs px-2 py-1">
89+
clear
90+
</button>
91+
<button
92+
disabled={!isDirty}
93+
type="submit"
94+
className="btn btn-blue text-xs px-2 py-1"
95+
>
96+
apply
97+
</button>
98+
</div>
99+
</PopoverPanel>
100+
</Popover>
94101
);
95102
}

src/pages/nonprofit-outreach/text-filter.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,7 @@ export function TextFilter(props: Props) {
8080
/>
8181
))}
8282
<div className="flex justify-end space-x-2 mt-4">
83-
<button
84-
disabled={!isDirty}
85-
type="reset"
86-
className="btn btn-outline text-xs px-2 py-1"
87-
>
83+
<button type="reset" className="btn btn-outline text-xs px-2 py-1">
8884
clear
8985
</button>
9086
<button

0 commit comments

Comments
 (0)