Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/frontend/src/lib/shortcuts/handlers/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { get } from 'svelte/store';
import { goto } from '$app/navigation';
import { friendListFilter } from '$lib/stores/friends';
import { buildSearchParams, friendListFilter } from '$lib/stores/friends';
import { NAVIGATION_PATHS } from '../config.js';
import type { HandlerContext } from '../types.js';

Expand All @@ -13,7 +13,7 @@ export function handleNavigation(e: KeyboardEvent, ctx: HandlerContext): boolean
if (e.key === 'f') {
// Restore filter state from store when navigating to friends
const filterState = get(friendListFilter);
const params = friendListFilter.buildSearchParams(filterState);
const params = buildSearchParams(filterState);
const queryString = params.toString();
goto(queryString ? `/friends?${queryString}` : '/friends');
return true;
Expand Down
102 changes: 102 additions & 0 deletions apps/frontend/src/lib/stores/friendListFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { derived, writable } from 'svelte/store';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buildSearchParams and parseSearchParams methods take an external FriendListFilterState argument rather than reading the store's own current state. This creates an unusual API where callers must subscribe and pass state back in manually. Consider either (a) making these pure utility functions exported separately from the store, or (b) using Svelte's get(friendListFilter) internally so callers invoke them without arguments. The current design leaks internal state shape to callers unnecessarily.

import type { FacetFilters } from '$shared';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FriendListFilterState interface is not exported. Consumers who want to type-annotate variables holding a snapshot of the filter state (e.g. in Svelte components using let state: FriendListFilterState) cannot do so without re-defining the type. Consider exporting it.

export interface FriendListFilterState {
query: string;
filters: FacetFilters;
}

const initialFilterState: FriendListFilterState = {
query: '',
filters: {},
};

function createFriendListFilterStore() {
const { subscribe, set, update } = writable<FriendListFilterState>(initialFilterState);

return {
subscribe,

setQuery: (query: string) => {
update((state) => ({ ...state, query }));
},

setFilters: (filters: FacetFilters) => {
update((state) => ({ ...state, filters }));
},

setState: (query: string, filters: FacetFilters) => {
set({ query, filters });
},

clear: () => {
set(initialFilterState);
},
};
}

export const friendListFilter = createFriendListFilterStore();

export const friendListQuery = derived(friendListFilter, ($filter) => $filter.query);

export const hasFriendListFilters = derived(
friendListFilter,
($filter) =>
$filter.query.trim().length > 0 ||
Object.values($filter.filters).some((arr) => arr && arr.length > 0),
);

/**
* Build URL search params from filter state.
* Pure utility — does not read the store; pass a state snapshot.
*/
export function buildSearchParams(state: FriendListFilterState): URLSearchParams {
const params = new URLSearchParams();

if (state.query.trim()) {
params.set('q', state.query.trim());
}

for (const [key, values] of Object.entries(state.filters)) {
if (values && values.length > 0) {
params.set(key, values.join(','));
}
}

return params;
}

/**
* Parse URL search params into filter state.
* Pure utility — does not read the store.
*/
export function parseSearchParams(params: URLSearchParams): FriendListFilterState {
const query = params.get('q') ?? '';
const filters: FacetFilters = {};

const stringArrayKeys = [
'country',
'city',
'organization',
'job_title',
'department',
'circles',
] as const;
for (const key of stringArrayKeys) {
const value = params.get(key);
if (value) {
filters[key] = value.split(',');
}
}

const relationshipCategory = params.get('relationship_category');
if (relationshipCategory) {
filters.relationship_category = relationshipCategory.split(',') as (
| 'family'
| 'professional'
| 'social'
)[];
}

return { query, filters };
}
Loading