| 152 | * |
| 153 | */ |
| 154 | export function useStructuredFilters(options: UseStructuredFiltersOptions) { |
| 155 | const route = useRoute() |
| 156 | const router = useRouter() |
| 157 | const { packages, initialFilters, initialSort, searchQueryModel } = options |
| 158 | const { t } = useI18n() |
| 159 | |
| 160 | const searchQuery = shallowRef(normalizeSearchParam(route.query.q)) |
| 161 | |
| 162 | // Filter state - must be declared before the watcher that uses it |
| 163 | const filters = ref<StructuredFilters>({ |
| 164 | ...DEFAULT_FILTERS, |
| 165 | ...initialFilters, |
| 166 | }) |
| 167 | |
| 168 | // Watch route query changes and sync filter state |
| 169 | watch( |
| 170 | () => route.query.q, |
| 171 | urlQuery => { |
| 172 | const value = normalizeSearchParam(urlQuery) |
| 173 | if (searchQuery.value !== value) { |
| 174 | searchQuery.value = value |
| 175 | } |
| 176 | |
| 177 | // Sync filters with URL |
| 178 | // When URL changes (e.g. from search input or navigation), |
| 179 | // we need to update our local filter state to match |
| 180 | const parsed = parseSearchOperators(value) |
| 181 | |
| 182 | filters.value.text = parsed.text ?? '' |
| 183 | // Deduplicate keywords (in case of both kw: and keyword: for same value) |
| 184 | filters.value.keywords = parsed.keywords ?? [] |
| 185 | |
| 186 | // Note: We intentionally don't reset other filters (security, downloadRange, etc.) |
| 187 | // as those are not typically driven by the search query string structure |
| 188 | }, |
| 189 | { immediate: true }, |
| 190 | ) |
| 191 | |
| 192 | // Sort state |
| 193 | const sortOption = shallowRef<SortOption>(initialSort ?? 'updated-desc') |
| 194 | |
| 195 | // Available keywords extracted from all packages |
| 196 | const availableKeywords = computed(() => { |
| 197 | const keywordCounts = new Map<string, number>() |
| 198 | for (const pkg of packages.value) { |
| 199 | const keywords = pkg.package.keywords ?? [] |
| 200 | for (const keyword of keywords) { |
| 201 | keywordCounts.set(keyword, (keywordCounts.get(keyword) ?? 0) + 1) |
| 202 | } |
| 203 | } |
| 204 | // Sort by count descending |
| 205 | return Array.from(keywordCounts.entries()) |
| 206 | .sort((a, b) => b[1] - a[1]) |
| 207 | .map(([keyword]) => keyword) |
| 208 | }) |
| 209 | |
| 210 | // Filter predicates |
| 211 | function matchesTextFilter(pkg: NpmSearchResult, text: string, scope: SearchScope): boolean { |