feat: update Angular cache with latest TypeScript definitions

This commit is contained in:
2025-09-22 22:17:38 -04:00
parent d78afda4cd
commit d372b7d509
27 changed files with 512 additions and 646 deletions
+204 -289
View File
@@ -1,26 +1,25 @@
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, signal, untracked, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, effect, inject, signal, ViewChild, ElementRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { YoutubeApiService } from '../../services/youtube-api.service';
import { Video } from '../../models/video.model';
import { InfiniteAnchorComponent } from '../shared/infinite-anchor/infinite-anchor.component';
import { formatRelativeFr } from '../../utils/date.util';
import { InstanceService, Provider } from '../../services/instance.service';
import { HistoryService } from '../../services/history.service';
import { Title } from '@angular/platform-browser';
import { TranslatePipe } from '../../pipes/translate.pipe';
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
import { AddToPlaylistComponent } from '../shared/components/add-to-playlist/add-to-playlist.component';
import { SearchResultGridComponent } from 'src/app/shared/components/search-result-grid/search-result-grid.component';
import { VideoItem } from 'src/app/shared/models/video-item.model';
import { SearchService, type SearchResponse } from '../../app/search/search.service';
import type { ProviderId } from '../../app/core/providers/provider-registry';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Observable, Subscription } from 'rxjs';
@Component({
selector: 'app-search',
standalone: true,
templateUrl: './search.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, RouterLink, InfiniteAnchorComponent, TranslatePipe, LikeButtonComponent, AddToPlaylistComponent]
imports: [CommonModule, TranslatePipe, SearchResultGridComponent]
})
export class SearchComponent {
private route = inject(ActivatedRoute);
@@ -33,16 +32,6 @@ export class SearchComponent {
q = signal<string>('');
loading = signal<boolean>(false);
results = signal<Video[]>([]);
busyMore = signal<boolean>(false);
nextCursor = signal<string | null>(null);
// Twitch-specific dual lists
twitchChannels = signal<Video[]>([]);
twitchVods = signal<Video[]>([]);
twitchCursorChannels = signal<string | null>(null);
twitchCursorVods = signal<string | null>(null);
twitchBusyChannels = signal<boolean>(false);
twitchBusyVods = signal<boolean>(false);
notice = signal<string | null>(null);
providerParam = signal<Provider | null>(null);
themeParam = signal<string | null>(null);
@@ -51,7 +40,7 @@ export class SearchComponent {
sortParam = signal<'relevance' | 'date' | 'views'>('relevance');
// Unified multi-provider response (grouped suggestions)
groups = signal<Record<ProviderId, any[]>>({} as any);
groups = signal<Record<string, any[]>>({});
showUnified = signal<boolean>(false);
error = signal<string | null>(null);
@@ -79,20 +68,52 @@ export class SearchComponent {
return ['yt','dm','tw','pt','od','ru'];
});
// Subtitle display: codes separated by middot
activeProvidersDisplay = computed(() => this.activeProviders().join(' · '));
// Map between Provider and ProviderId
private readonly providerIdMap: Record<string, Provider> = {
'yt': 'youtube',
'dm': 'dailymotion',
'tw': 'twitch',
'pt': 'peertube',
'od': 'odysee',
'ru': 'rumble'
};
// Helper to display provider label from registry/instances
providerDisplay(p: ProviderId): string {
try {
const found = (this.instances.providers() || []).find(x => (x.id as any) === p);
return found?.label || p.toUpperCase();
} catch { return p.toUpperCase(); }
// Get active providers with full info
activeProvidersInfo = computed(() => {
const activeIds = this.activeProviders();
return this.instances.providers().filter(p =>
activeIds.some(id => this.providerIdMap[id] === p.id)
);
});
// Brand colors for provider badge (inline styles to match History badges)
getProviderColors(providerId: string | null | undefined): { [key: string]: string } {
const id = (providerId || '').toLowerCase();
switch (id) {
case 'youtube':
return { backgroundColor: 'rgba(220, 38, 38, 0.15)', color: 'rgb(248, 113, 113)', borderColor: 'rgba(239, 68, 68, 0.3)' };
case 'dailymotion':
return { backgroundColor: 'rgba(37, 99, 235, 0.15)', color: 'rgb(147, 197, 253)', borderColor: 'rgba(59, 130, 246, 0.3)' };
case 'peertube':
return { backgroundColor: 'rgba(245, 158, 11, 0.15)', color: 'rgb(252, 211, 77)', borderColor: 'rgba(245, 158, 11, 0.3)' };
case 'rumble':
return { backgroundColor: 'rgba(22, 163, 74, 0.15)', color: 'rgb(134, 239, 172)', borderColor: 'rgba(34, 197, 94, 0.3)' };
case 'twitch':
return { backgroundColor: 'rgba(168, 85, 247, 0.15)', color: 'rgb(216, 180, 254)', borderColor: 'rgba(168, 85, 247, 0.3)' };
case 'odysee':
return { backgroundColor: 'rgba(236, 72, 153, 0.15)', color: 'rgb(251, 207, 232)', borderColor: 'rgba(236, 72, 153, 0.3)' };
default:
return { backgroundColor: 'rgba(30, 41, 59, 0.8)', color: 'rgb(203, 213, 225)', borderColor: 'rgb(51, 65, 85)' };
}
}
// Title: Search ${q ? ` — “${q}”` : ''}
// Page title with search query
pageHeading = computed(() => {
const q = this.q();
return `Search ${q ? ` — “${q}”` : ''}`;
if (q) {
return `Résultats pour « ${q} »`;
}
return 'Recherche';
});
// Public computed used by the template to avoid referencing private `instances`
selectedProviderForView = computed(() => this.providerParam() || this.instances.selectedProvider());
@@ -111,46 +132,19 @@ export class SearchComponent {
canPrev = computed(() => (this.pageParam() || 1) > 1);
// Helper computed: filtered list based on active tag (non-Twitch only)
// Flatten all unified results into a single list of VideoItems
allVideos = computed<any[]>(() => {
const g = this.groups();
if (!g) return [];
return Object.values(g).flat();
});
// Helper computed: filtered list based on active tag
filteredResults = computed(() => {
const tag = this.filterTag();
const list = this.allVideos();
const provider = this.selectedProviderForView();
// Use unified results if available
if (this.showUnified()) {
const groups = this.groups();
const list: Video[] = [];
// Convert unified groups to Video objects
Object.entries(groups).forEach(([providerId, items]) => {
items.forEach((item: any) => {
list.push({
videoId: item.id,
title: item.title,
thumbnail: item.thumbnail || '',
uploaderName: item.uploaderName || 'Unknown',
uploaderAvatar: '',
views: 0,
uploaded: Date.now(),
uploadedDate: new Date().toISOString(),
duration: 0,
url: item.url || '',
type: 'video',
provider: providerId as 'youtube'|'dailymotion'|'twitch'|'rumble'|'odysee'|'peertube'
});
});
});
// Apply tag filtering to unified results
if (provider === 'twitch') return list; // Not used for Twitch (separate sections)
if (tag === 'all') return list;
// For now, return all results since we don't have duration/uploaded info from unified search
return list;
}
// Fallback to legacy results
const list = this.results();
if (provider === 'twitch') return list; // Not used for Twitch (separate sections)
if (tag === 'all') return list;
@@ -162,84 +156,76 @@ export class SearchComponent {
// Duration filters
if (tag === 'short') return list.filter(v => {
const d = Number(v.duration || 0);
const d = Number(v.durationSec || 0);
return d > 0 && d < 4 * 60;
});
if (tag === 'medium') return list.filter(v => {
const d = Number(v.duration || 0);
const d = Number(v.durationSec || 0);
return d >= 4 * 60 && d < 20 * 60;
});
if (tag === 'long') return list.filter(v => {
const d = Number(v.duration || 0);
const d = Number(v.durationSec || 0);
return d >= 20 * 60;
});
// Date filters
if (tag === 'today') return list.filter(v => {
const uploadTime = typeof v.uploaded === 'number' ? v.uploaded : 0;
const uploadTime = v.publishedAt ? new Date(v.publishedAt).getTime() : 0;
return uploadTime > 0 && (now - uploadTime) <= oneDay;
});
if (tag === 'this_week') return list.filter(v => {
const uploadTime = typeof v.uploaded === 'number' ? v.uploaded : 0;
const uploadTime = v.publishedAt ? new Date(v.publishedAt).getTime() : 0;
return uploadTime > 0 && (now - uploadTime) <= sevenDays;
});
if (tag === 'this_month') return list.filter(v => {
const uploadTime = typeof v.uploaded === 'number' ? v.uploaded : 0;
const uploadTime = v.publishedAt ? new Date(v.publishedAt).getTime() : 0;
return uploadTime > 0 && (now - uploadTime) <= thirtyDays;
});
if (tag === 'this_year') return list.filter(v => {
const uploadTime = typeof v.uploaded === 'number' ? v.uploaded : 0;
const uploadTime = v.publishedAt ? new Date(v.publishedAt).getTime() : 0;
return uploadTime > 0 && (now - uploadTime) <= oneYear;
});
return list;
});
// Filter each provider group separately for sectioned rendering
filteredGroups = computed(() => {
const tag = this.filterTag();
const g = this.groups();
const result: Record<string, any[]> = {};
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
const oneYear = 365 * oneDay;
const applyFilter = (list: any[]): any[] => {
if (tag === 'all' || this.selectedProviderForView() === 'twitch') return list;
if (tag === 'short') return list.filter(v => { const d = Number(v.durationSec || 0); return d > 0 && d < 4 * 60; });
if (tag === 'medium') return list.filter(v => { const d = Number(v.durationSec || 0); return d >= 4 * 60 && d < 20 * 60; });
if (tag === 'long') return list.filter(v => { const d = Number(v.durationSec || 0); return d >= 20 * 60; });
if (tag === 'today') return list.filter(v => { const t = v.publishedAt ? new Date(v.publishedAt).getTime() : 0; return t > 0 && (now - t) <= oneDay; });
if (tag === 'this_week') return list.filter(v => { const t = v.publishedAt ? new Date(v.publishedAt).getTime() : 0; return t > 0 && (now - t) <= sevenDays; });
if (tag === 'this_month') return list.filter(v => { const t = v.publishedAt ? new Date(v.publishedAt).getTime() : 0; return t > 0 && (now - t) <= thirtyDays; });
if (tag === 'this_year') return list.filter(v => { const t = v.publishedAt ? new Date(v.publishedAt).getTime() : 0; return t > 0 && (now - t) <= oneYear; });
return list;
};
Object.entries(g || {}).forEach(([pid, list]) => {
result[pid] = Array.isArray(list) ? applyFilter(list) : [];
});
return result;
});
// Available tags computed from current results (only show tags that can apply)
availableTags = computed(() => {
const provider = this.selectedProviderForView();
const list = this.allVideos();
if (list.length === 0) return [];
const tags: { key: string; label: string; show: boolean }[] = [];
const now = Date.now();
// Use unified results if available
const list = this.showUnified() ?
(() => {
const groups = this.groups();
const results: Video[] = [];
Object.entries(groups).forEach(([_, items]) => {
items.forEach((item: any) => {
results.push({
videoId: item.id,
title: item.title,
thumbnail: item.thumbnail || '',
uploaderName: item.uploaderName || 'Unknown',
uploaderAvatar: '',
views: 0,
uploaded: Date.now(),
uploadedDate: new Date().toISOString(),
duration: 0,
url: item.url || '',
type: 'video',
provider: 'youtube' as any
});
});
});
return results;
})() :
this.results();
if (provider === 'twitch') {
const hasLive = this.twitchChannels().length > 0;
const hasVods = this.twitchVods().length > 0;
// Show the group if at least one of the sections is available
if (hasLive || hasVods) {
tags.push({ key: 'twitch_all', label: 'Tout', show: hasLive && hasVods });
tags.push({ key: 'twitch_live', label: 'En direct', show: hasLive });
tags.push({ key: 'twitch_vod', label: 'VOD', show: hasVods });
}
return tags.filter(t => t.show);
}
// Non-Twitch providers: duration + date filters
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
@@ -250,8 +236,8 @@ export class SearchComponent {
hasToday = false, hasThisWeek = false, hasThisMonth = false, hasThisYear = false;
for (const v of list) {
const d = Number(v.duration || 0);
const uploadTime = typeof v.uploaded === 'number' ? v.uploaded : 0;
const d = Number(v.durationSec || 0);
const uploadTime = v.publishedAt ? new Date(v.publishedAt).getTime() : 0;
const timeDiff = now - uploadTime;
// Duration filters
@@ -290,21 +276,32 @@ export class SearchComponent {
});
// Subscribe once to unified multi-provider results and reflect to UI
this.unified.request$.pipe(takeUntilDestroyed()).subscribe({
next: (resp: SearchResponse) => {
this.groups.set(resp.groups as any);
this.loading.set(false);
this.error.set(null);
},
error: () => {
this.groups.set({} as any);
this.loading.set(false);
this.error.set('Le service de recherche est temporairement indisponible. Réessayer.');
}
});
const subscription: Subscription = this.unified.request$
.pipe(takeUntilDestroyed())
.subscribe({
next: (resp: any) => {
// Convertir la réponse en un format compatible avec notre modèle
const groups: Record<string, any[]> = {};
if (resp.groups) {
Object.entries(resp.groups).forEach(([key, value]) => {
groups[key] = Array.isArray(value) ? value : [];
});
}
this.groups.set(groups);
this.loading.set(false);
this.error.set(null);
},
error: () => {
this.groups.set({} as any);
this.loading.set(false);
this.error.set('Le service de recherche est temporairement indisponible. Réessayer.');
}
});
// Listen to query param changes (so subsequent searches update)
this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((pm) => {
const paramSubscription: Subscription = this.route.queryParamMap
.pipe(takeUntilDestroyed())
.subscribe((pm: ParamMap) => {
const q = (pm.get('q') || '').trim();
this.q.set(q);
const prov = (pm.get('provider') as Provider) || (pm.get('p') as Provider) || null;
@@ -361,8 +358,6 @@ export class SearchComponent {
this.groups.set({} as any); // Clear previous results
this.error.set(null);
} else {
this.results.set([]);
this.nextCursor.set(null);
this.loading.set(false);
this.groups.set({} as any);
this.showUnified.set(false);
@@ -370,139 +365,8 @@ export class SearchComponent {
}
});
// React to provider/region/PeerTube instance changes to refresh search automatically
effect(() => {
const provider = this.instances.selectedProvider();
const region = this.instances.region();
const ptInstance = this.instances.activePeerTubeInstance();
untracked(() => {
// If provider is explicitly specified in query, do not override it with global changes
if (this.providerParam()) return;
if (!this.q()) return;
this.notice.set(null);
// If unified mode is active, re-emit providers to trigger a refresh instead of legacy reload
if (this.showUnified()) {
const provParam = this.providersListParam();
const provider = this.instances.selectedProvider();
const providersToUse = (provParam && (provParam === 'all' || (Array.isArray(provParam) && provParam.length > 0)))
? (provParam as any)
: [provider as unknown as ProviderId];
this.unified.setProviders(providersToUse);
this.loading.set(true);
} else {
this.reloadSearch();
}
});
}, { allowSignalWrites: true });
}
reloadSearch() {
const readiness = this.instances.getProviderReadiness(this.providerParam() || undefined as any);
if (!readiness.ready) {
this.notice.set(readiness.reason || 'Le provider sélectionné n\'est pas prêt.');
this.results.set([]);
this.nextCursor.set(null);
this.twitchChannels.set([]);
this.twitchVods.set([]);
this.twitchCursorChannels.set(null);
this.twitchCursorVods.set(null);
this.loading.set(false);
return;
}
this.loading.set(true);
this.results.set([]);
this.nextCursor.set(null);
this.twitchChannels.set([]);
this.twitchVods.set([]);
this.twitchCursorChannels.set(null);
this.twitchCursorVods.set(null);
const provider = this.providerParam() || this.instances.selectedProvider();
// Record search term with provider once per reload
try {
this.history.recordSearch(this.q(), { provider }).subscribe({
next: () => {},
error: (err) => console.error('Error recording search:', err)
});
} catch (err) {
console.error('Error in recordSearch:', err);
}
// Ensure default tag matches provider
this.filterTag.set(provider === 'twitch' ? 'twitch_all' : 'all');
if (provider === 'twitch') {
// Load both sections in parallel
this.twitchBusyChannels.set(true);
this.twitchBusyVods.set(true);
this.api.searchTwitchChannelsPage(this.q(), null).subscribe(res => {
this.twitchChannels.set(res.items);
this.twitchCursorChannels.set(res.nextCursor || null);
this.twitchBusyChannels.set(false);
this.loading.set(false);
});
this.api.searchTwitchVodsPage(this.q(), null).subscribe(res => {
this.twitchVods.set(res.items);
this.twitchCursorVods.set(res.nextCursor || null);
this.twitchBusyVods.set(false);
this.loading.set(false);
});
} else {
this.fetchNextPage();
}
}
fetchNextPage() {
if (this.busyMore() || !this.q()) return;
const readiness = this.instances.getProviderReadiness(this.providerParam() || undefined as any);
if (!readiness.ready) {
if (!this.notice()) this.notice.set(readiness.reason || 'Le provider sélectionné n\'est pas prêt.');
return;
}
this.busyMore.set(true);
const providerOverride = this.providerParam();
this.api.searchVideosPage(this.q(), this.nextCursor(), providerOverride as any).subscribe(res => {
const merged = [...this.results(), ...res.items];
this.results.set(merged);
this.nextCursor.set(res.nextCursor || null);
this.busyMore.set(false);
this.loading.set(false);
// Provider/instance availability notices (avoid overriding legitimate no-results unless clear)
if (merged.length === 0 && !this.notice()) {
const readiness2 = this.instances.getProviderReadiness();
if (!readiness2.ready) {
this.notice.set(readiness2.reason || 'Le provider sélectionné n\'est pas prêt.');
} else {
const provider = this.providerParam() || this.instances.selectedProvider();
if (provider === 'peertube') {
const inst = this.instances.activePeerTubeInstance();
this.notice.set(`PeerTube: les vidéos ne sont pas disponibles depuis l'instance "${inst}" pour le moment. Essayez une autre instance dans l'en-tête.`);
} else if (provider === 'rumble') {
const label = this.instances.selectedProviderLabel();
this.notice.set(`Les vidéos ne sont pas disponibles pour le provider "${label}" pour le moment. Réessayez plus tard ou choisissez un autre provider.`);
}
}
}
});
}
loadMoreTwitchChannels() {
if (this.twitchBusyChannels() || !this.twitchCursorChannels()) return;
this.twitchBusyChannels.set(true);
this.api.searchTwitchChannelsPage(this.q(), this.twitchCursorChannels()).subscribe(res => {
this.twitchChannels.set([...this.twitchChannels(), ...res.items]);
this.twitchCursorChannels.set(res.nextCursor || null);
this.twitchBusyChannels.set(false);
});
}
loadMoreTwitchVods() {
if (this.twitchBusyVods() || !this.twitchCursorVods()) return;
this.twitchBusyVods.set(true);
this.api.searchTwitchVodsPage(this.q(), this.twitchCursorVods()).subscribe(res => {
this.twitchVods.set([...this.twitchVods(), ...res.items]);
this.twitchCursorVods.set(res.nextCursor || null);
this.twitchBusyVods.set(false);
});
}
formatViews(views: number): string {
if (views >= 1_000_000_000) return (views / 1_000_000_000).toFixed(1) + 'B';
@@ -516,40 +380,56 @@ export class SearchComponent {
return formatRelativeFr(dateIso);
}
// Build query params for Watch page (provider + optional odysee slug)
watchQueryParams(v: Video): Record<string, any> | null {
const p = this.providerParam() || this.instances.selectedProvider();
const qp: any = { p };
if (p === 'odysee' && v.url?.startsWith('https://odysee.com/')) {
let slug = v.url.substring('https://odysee.com/'.length);
if (slug.startsWith('/')) slug = slug.slice(1);
qp.slug = slug;
// Méthode utilitaire pour nettoyer les paramètres de l'URL
private cleanUrlParams(params: any): any {
const cleanParams = { ...params };
// Supprimer explicitement le paramètre theme
if ('theme' in cleanParams) {
delete cleanParams.theme;
}
if (p === 'twitch' && v.type === 'channel') {
// Extract channel login from uploaderUrl if available
const url = v.uploaderUrl || v.url || '';
try {
const u = new URL(url);
const parts = u.pathname.split('/').filter(Boolean);
if (parts.length > 0) qp.channel = parts[0];
} catch {}
}
return qp;
// Nettoyer les valeurs null/undefined
Object.keys(cleanParams).forEach(key => {
if (cleanParams[key] === null || cleanParams[key] === undefined) {
delete cleanParams[key];
}
});
return cleanParams;
}
// Submit handler from the embedded SearchBox: keep URL as source of truth
onSearchBarSubmit(evt: { q: string; providers: ProviderId[] | 'all' }) {
const q = (evt?.q || '').trim();
if (!q) return;
const provider = this.providerParam() || this.instances.selectedProvider();
const qp: any = { q, p: provider, provider: null };
if (Array.isArray(evt.providers)) qp.providers = evt.providers.join(',');
else if (evt.providers === 'all') qp.providers = 'yt,dm,tw,pt,od,ru';
qp.page = 1; // reset pagination on new search
this.router.navigate([], { relativeTo: this.route, queryParams: qp, queryParamsHandling: 'merge' });
// Créer un nouvel objet de paramètres de requête
const queryParams: any = {
q,
p: this.providerParam() || this.instances.selectedProvider(),
page: 1 // reset pagination on new search
};
// Gérer les fournisseurs
if (Array.isArray(evt.providers)) {
queryParams.providers = evt.providers.join(',');
} else if (evt.providers === 'all') {
queryParams.providers = 'yt,dm,tw,pt,od,ru';
}
// Nettoyer les paramètres avant la navigation
const cleanParams = this.cleanUrlParams(queryParams);
// Utiliser replaceUrl: true pour éviter d'ajouter une entrée dans l'historique
// et forcer le rechargement avec les nouveaux paramètres
this.router.navigate([], {
relativeTo: this.route,
queryParams: cleanParams,
queryParamsHandling: '',
replaceUrl: true
});
}
@ViewChild('searchInput') searchInput?: ElementRef<HTMLInputElement>;
@ViewChild('searchInput', { static: false }) searchInput?: ElementRef<HTMLInputElement>;
// Legacy input handlers removed; SearchBox handles keyboard and submit
@@ -574,18 +454,47 @@ export class SearchComponent {
// Pagination controls
nextPage() {
const next = (this.pageParam() || 1) + 1;
this.router.navigate([], { relativeTo: this.route, queryParams: { page: next }, queryParamsHandling: 'merge' });
const cleanParams = this.cleanUrlParams({
...this.route.snapshot.queryParams,
page: next
});
this.router.navigate([], {
relativeTo: this.route,
queryParams: cleanParams,
queryParamsHandling: '',
replaceUrl: true
});
}
prevPage() {
const prev = Math.max(1, (this.pageParam() || 1) - 1);
this.router.navigate([], { relativeTo: this.route, queryParams: { page: prev }, queryParamsHandling: 'merge' });
const cleanParams = this.cleanUrlParams({
...this.route.snapshot.queryParams,
page: prev
});
this.router.navigate([], {
relativeTo: this.route,
queryParams: cleanParams,
queryParamsHandling: '',
replaceUrl: true
});
}
// Sort control
onSortChange(event: Event) {
const value = (event.target as HTMLSelectElement)?.value as 'relevance' | 'date' | 'views' | '';
const sort = (value === 'date' || value === 'views') ? value : 'relevance';
this.router.navigate([], { relativeTo: this.route, queryParams: { sort, page: 1 }, queryParamsHandling: 'merge' });
const cleanParams = this.cleanUrlParams({
...this.route.snapshot.queryParams,
sort,
page: 1
});
this.router.navigate([], {
relativeTo: this.route,
queryParams: cleanParams,
queryParamsHandling: '',
replaceUrl: true
});
}
// CTA handler: navigate to focus on a single provider while preserving q
@@ -593,6 +502,12 @@ export class SearchComponent {
const q = (this.q() || '').trim();
const qp: any = { provider: p, p, providers: p, page: 1 };
if (q) qp.q = q;
this.router.navigate([], { relativeTo: this.route, queryParams: qp, queryParamsHandling: 'merge' });
const cleanParams = this.cleanUrlParams(qp);
this.router.navigate([], {
relativeTo: this.route,
queryParams: cleanParams,
queryParamsHandling: '',
replaceUrl: true
});
}
}