-
Notifications
You must be signed in to change notification settings - Fork 255
Expand file tree
/
Copy pathuseFilterStore.ts
More file actions
242 lines (207 loc) · 7.43 KB
/
Copy pathuseFilterStore.ts
File metadata and controls
242 lines (207 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import { create } from 'zustand';
/**
* Filter Store
*
* Manages runtime filter input values for the published site.
* Tracks current values from filter layer inputs and provides
* them to collection layers for dynamic filtering.
*
* Structure: filterLayerId -> inputLayerId -> currentValue
*
* URL sync: filter values are synced to URL params using the input's
* `name` attribute (if set) or a stripped layer ID as the key.
* Example: `?search=Emily` if the input's name is "search",
* or `?mm1ue1e3iktexa=Emily` as fallback.
*/
/**
* Maps inputLayerId → URL param name.
* Built from input elements' `name` attributes or stripped layer IDs.
*/
type NameMap = Record<string, string>;
interface FilterStoreState {
values: Record<string, Record<string, string>>;
/** Maps inputLayerId → URL param name */
nameMap: NameMap;
setFilterValue: (filterLayerId: string, inputLayerId: string, value: string) => void;
setFilterValues: (filterLayerId: string, inputValues: Record<string, string>) => void;
clearFilter: (filterLayerId: string) => void;
getFilterValues: (filterLayerId: string) => Record<string, string>;
getAllFilterValues: () => Record<string, Record<string, string>>;
setNameMap: (map: NameMap) => void;
removeNameMapEntries: (inputLayerIds: string[]) => void;
syncToUrl: () => void;
loadFromUrl: () => void;
reset: () => void;
}
function stripLayerPrefix(layerId: string): string {
return layerId.startsWith('lyr-') ? layerId.slice(4) : layerId;
}
function getUrlParamName(inputLayerId: string, nameMap: NameMap): string {
return nameMap[inputLayerId] || stripLayerPrefix(inputLayerId);
}
function buildReverseMap(nameMap: NameMap): Record<string, string> {
const reverse: Record<string, string> = {};
for (const [layerId, paramName] of Object.entries(nameMap)) {
reverse[paramName] = layerId;
}
return reverse;
}
export const useFilterStore = create<FilterStoreState>((set, get) => ({
values: {},
nameMap: {},
setFilterValue: (filterLayerId, inputLayerId, value) => {
set(state => {
const newValues = { ...state.values };
// Remove stale _url entry for this input so buildApiFilters
// doesn't find the old URL-loaded value before the new one
if (newValues['_url'] && inputLayerId in newValues['_url']) {
const { [inputLayerId]: _, ...restUrl } = newValues['_url'];
if (Object.keys(restUrl).length === 0) {
delete newValues['_url'];
} else {
newValues['_url'] = restUrl;
}
}
newValues[filterLayerId] = {
...(newValues[filterLayerId] || {}),
[inputLayerId]: value,
};
return { values: newValues };
});
setTimeout(() => get().syncToUrl(), 0);
},
setFilterValues: (filterLayerId, inputValues) => {
set(state => {
const newValues = { ...state.values };
let changed = false;
// Remove stale _url entries for these inputs so buildApiFilters
// doesn't keep URL-loaded values after real input interaction.
if (newValues['_url']) {
let urlChanged = false;
const nextUrl = { ...newValues['_url'] };
for (const inputLayerId of Object.keys(inputValues)) {
if (inputLayerId in nextUrl) {
delete nextUrl[inputLayerId];
urlChanged = true;
}
}
if (urlChanged) {
changed = true;
if (Object.keys(nextUrl).length === 0) {
delete newValues['_url'];
} else {
newValues['_url'] = nextUrl;
}
}
}
const currentLayerValues = newValues[filterLayerId] || {};
const nextLayerValues = { ...currentLayerValues };
for (const [inputLayerId, value] of Object.entries(inputValues)) {
if (nextLayerValues[inputLayerId] !== value) {
nextLayerValues[inputLayerId] = value;
changed = true;
}
}
if (!changed) return state;
newValues[filterLayerId] = nextLayerValues;
return { values: newValues };
});
setTimeout(() => get().syncToUrl(), 0);
},
clearFilter: (filterLayerId) => {
set(state => {
const { [filterLayerId]: _, ...rest } = state.values;
return { values: rest };
});
setTimeout(() => get().syncToUrl(), 0);
},
getFilterValues: (filterLayerId) => {
return get().values[filterLayerId] || {};
},
getAllFilterValues: () => {
return get().values;
},
setNameMap: (map) => {
set(state => ({ nameMap: { ...state.nameMap, ...map } }));
},
removeNameMapEntries: (inputLayerIds) => {
if (inputLayerIds.length === 0) return;
set(state => {
const nextMap = { ...state.nameMap };
for (const inputLayerId of inputLayerIds) {
delete nextMap[inputLayerId];
}
return { nameMap: nextMap };
});
},
syncToUrl: () => {
if (typeof window === 'undefined') return;
const { values, nameMap } = get();
const url = new URL(window.location.href);
// Build set of current param names we manage (to know which to remove)
const managedParams = new Set<string>();
for (const filterLayerValues of Object.values(values)) {
for (const inputLayerId of Object.keys(filterLayerValues)) {
managedParams.add(getUrlParamName(inputLayerId, nameMap));
}
}
// Also remove any params that match known name map entries or stripped IDs
const allKnownParams = new Set<string>(managedParams);
for (const [layerId, paramName] of Object.entries(nameMap)) {
allKnownParams.add(paramName);
allKnownParams.add(stripLayerPrefix(layerId));
}
// Remove old filter params (both old filter_ format and new name format)
const keysToRemove: string[] = [];
url.searchParams.forEach((_, key) => {
if (key.startsWith('filter_') || allKnownParams.has(key)) {
keysToRemove.push(key);
}
});
keysToRemove.forEach(key => url.searchParams.delete(key));
// Add current filter values with friendly names
for (const filterLayerValues of Object.values(values)) {
for (const [inputLayerId, value] of Object.entries(filterLayerValues)) {
if (!value) continue;
if (/-(cb|rb)-.+-input$/.test(inputLayerId)) continue;
url.searchParams.set(getUrlParamName(inputLayerId, nameMap), value);
}
}
window.history.replaceState({}, '', url.toString());
},
loadFromUrl: () => {
if (typeof window === 'undefined') return;
const { nameMap } = get();
const reverseMap = buildReverseMap(nameMap);
const url = new URL(window.location.href);
const urlValues: Record<string, string> = {};
const knownStrippedIds = new Set<string>();
for (const layerId of Object.keys(nameMap)) {
knownStrippedIds.add(stripLayerPrefix(layerId));
}
url.searchParams.forEach((value, key) => {
if (!value) return;
let inputLayerId: string | null = null;
if (reverseMap[key]) {
inputLayerId = reverseMap[key];
} else if (knownStrippedIds.has(key)) {
inputLayerId = `lyr-${key}`;
} else if (key.startsWith('filter_')) {
inputLayerId = key.slice('filter_'.length);
}
if (inputLayerId) {
urlValues[inputLayerId] = value;
}
});
if (Object.keys(urlValues).length > 0) {
set(state => {
const merged = { ...state.values };
merged['_url'] = { ...(merged['_url'] || {}), ...urlValues };
return { values: merged };
});
}
},
reset: () => {
set({ values: {}, nameMap: {} });
},
}));