● Zion Boggan
repos/Docket/components/search-filter-bar.tsx
zionboggan.com ↗
180 lines · tsx
History for this file →
1
 
2
 
3
import { clsx } from "@/lib/cx";
4
import { Card } from "@/components/ui";
5
import type { ReactNode } from "react";
6
 
7
export function SearchFilterBar({
8
  children,
9
  count,
10
  total,
11
  countLabel = "result",
12
  onClear,
13
  clearHref,
14
  className,
15
}: {
16
  children: ReactNode;
17
  count?: number;
18
  total?: number;
19
  countLabel?: string;
20
  onClear?: () => void;
21
  clearHref?: string;
22
  className?: string;
23
}) {
24
  const showClear = !!(onClear || clearHref);
25
  return (
26
    <Card className={clsx("p-4 sm:p-5", className)}>
27
      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">{children}</div>
28
      {(count != null || showClear) && (
29
        <div className="mt-3 flex flex-wrap items-center justify-between gap-2">
30
          {count != null && (
31
            <p className="label text-ash-2">
32
              {count.toLocaleString()}&nbsp;{countLabel}
33
              {count !== 1 ? "s" : ""}
34
              {total != null && count !== total
35
                ? ` of ${total.toLocaleString()}`
36
                : ""}
37
            </p>
38
          )}
39
          {onClear && (
40
            <button
41
              type="button"
42
              onClick={onClear}
43
              className="focus-ring label rounded-sm px-2 py-1 text-ash underline underline-offset-4 hover:text-paper"
44
            >
45
              Clear filters
46
            </button>
47
          )}
48
          {clearHref && !onClear && (
49
            <a
50
              href={clearHref}
51
              className="focus-ring label rounded-sm px-2 py-1 text-ash underline underline-offset-4 hover:text-paper"
52
            >
53
              Clear filters
54
            </a>
55
          )}
56
        </div>
57
      )}
58
    </Card>
59
  );
60
}
61
 
62
export function FilterField({
63
  label,
64
  span,
65
  children,
66
}: {
67
  label: string;
68
  span?: "2";
69
  children: ReactNode;
70
}) {
71
  return (
72
    <label className={clsx("block", span === "2" && "sm:col-span-2")}>
73
      <span className="label mb-1.5 block text-ash">{label}</span>
74
      {children}
75
    </label>
76
  );
77
}
78
 
79
export function FilterInput({
80
  name,
81
  value,
82
  defaultValue,
83
  onChange,
84
  placeholder,
85
  "aria-label": ariaLabel,
86
}: {
87
  name?: string;
88
  value?: string;
89
  defaultValue?: string;
90
  onChange?: (v: string) => void;
91
  placeholder?: string;
92
  "aria-label"?: string;
93
}) {
94
  const className =
95
    "focus-ring w-full min-h-[52px] rounded-[3px] border border-line bg-ink px-3 py-2 text-[18px] leading-snug text-paper placeholder:text-ash-2";
96
  if (onChange !== undefined) {
97
    return (
98
      <input
99
        type="search"
100
        value={value ?? ""}
101
        onChange={(e) => onChange(e.target.value)}
102
        placeholder={placeholder}
103
        aria-label={ariaLabel}
104
        className={className}
105
      />
106
    );
107
  }
108
  return (
109
    <input
110
      type="search"
111
      name={name}
112
      defaultValue={defaultValue ?? ""}
113
      placeholder={placeholder}
114
      aria-label={ariaLabel}
115
      className={className}
116
    />
117
  );
118
}
119
 
120
export function FilterSelect({
121
  name,
122
  value,
123
  defaultValue,
124
  onChange,
125
  "aria-label": ariaLabel,
126
  options,
127
}: {
128
  name?: string;
129
  value?: string;
130
  defaultValue?: string;
131
  onChange?: (v: string) => void;
132
  "aria-label"?: string;
133
  options: Array<{ value: string; label: string }>;
134
}) {
135
  const className =
136
    "focus-ring w-full min-h-[44px] rounded-[3px] border border-line bg-ink px-3 py-2 font-mono text-[13px] text-paper";
137
  if (onChange !== undefined) {
138
    return (
139
      <select
140
        value={value ?? ""}
141
        onChange={(e) => onChange(e.target.value)}
142
        aria-label={ariaLabel}
143
        className={className}
144
      >
145
        {options.map((o) => (
146
          <option key={o.value} value={o.value} className="bg-surface text-paper">
147
            {o.label}
148
          </option>
149
        ))}
150
      </select>
151
    );
152
  }
153
  return (
154
    <select
155
      name={name}
156
      defaultValue={defaultValue ?? ""}
157
      aria-label={ariaLabel}
158
      className={className}
159
    >
160
      {options.map((o) => (
161
        <option key={o.value} value={o.value} className="bg-surface text-paper">
162
          {o.label}
163
        </option>
164
      ))}
165
    </select>
166
  );
167
}
168
 
169
export function FilterSubmit({ label = "Filter" }: { label?: string }) {
170
  return (
171
    <div className="flex items-end">
172
      <button
173
        type="submit"
174
        className="focus-ring label inline-flex min-h-[44px] w-full items-center justify-center rounded-[3px] border border-line bg-surface px-4 text-paper hover:bg-graphite"
175
      >
176
        {label}
177
      </button>
178
    </div>
179
  );
180
}