diff --git a/src/features/FilterFonts/index.ts b/src/features/FilterFonts/index.ts index 3fc1009..18606e0 100644 --- a/src/features/FilterFonts/index.ts +++ b/src/features/FilterFonts/index.ts @@ -1,5 +1,11 @@ -export { categoryFilterStore } from './model/stores/categoryFilterStore'; -export { providersFilterStore } from './model/stores/providersFilterStore'; -export { subsetsFilterStore } from './model/stores/subsetsFilterStore'; - -export { clearAllFilters } from './model/services/clearAllFilters/clearAllFilters'; +export { + createFilterManager, + type FilterManager, +} from './lib/filterManager/filterManager.svelte'; +export { + FONT_CATEGORIES, + FONT_PROVIDERS, + FONT_SUBSETS, +} from './model/const/const'; +export type { FilterGroupConfig } from './model/const/types/common'; +export { filterManager } from './model/state/manager.svelte'; diff --git a/src/features/FilterFonts/lib/filterManager/filterManager.svelte.ts b/src/features/FilterFonts/lib/filterManager/filterManager.svelte.ts new file mode 100644 index 0000000..5ece71a --- /dev/null +++ b/src/features/FilterFonts/lib/filterManager/filterManager.svelte.ts @@ -0,0 +1,59 @@ +import { + type Filter, + createFilter, +} from '$shared/lib/utils'; +import type { FilterGroupConfig } from '../../model/const/types/common'; + +/** + * Create a filter manager instance. + */ +export function createFilterManager(configs: FilterGroupConfig[]) { + // Create filter instances upfront + const groups = $state( + configs.map(config => ({ + id: config.id, + label: config.label, + instance: createFilter({ properties: config.properties }), + })), + ); + + // Derived: any selection across all groups + const hasAnySelection = $derived( + groups.some(group => group.instance.selectedProperties.length > 0), + ); + + // Derived: total count across all groups + const totalSelectedCount = $derived( + groups.reduce( + (acc, group) => acc + group.instance.selectedProperties.length, + 0, + ), + ); + + return { + // Direct array reference (reactive) + get groups() { + return groups; + }, + + // Derived values + get hasAnySelection() { + return hasAnySelection; + }, + get totalSelectedCount() { + return totalSelectedCount; + }, + + // Global action + deselectAllGlobal: () => { + groups.forEach(group => group.instance.deselectAll()); + }, + + // Helper to get group by id + getGroup: (id: string) => { + return groups.find(g => g.id === id); + }, + }; +} + +export type FilterManager = ReturnType; diff --git a/src/features/FilterFonts/model/const/const.ts b/src/features/FilterFonts/model/const/const.ts index 75b54d7..691a756 100644 --- a/src/features/FilterFonts/model/const/const.ts +++ b/src/features/FilterFonts/model/const/const.ts @@ -1,4 +1,4 @@ -import type { Property } from '$shared/lib/store/createFilterStore/createFilterStore'; +import type { Property } from '$shared/lib/store'; export const FONT_CATEGORIES: Property[] = [ { diff --git a/src/features/FilterFonts/model/const/types/common.ts b/src/features/FilterFonts/model/const/types/common.ts new file mode 100644 index 0000000..c8527b4 --- /dev/null +++ b/src/features/FilterFonts/model/const/types/common.ts @@ -0,0 +1,7 @@ +import type { Property } from '$shared/lib/store'; + +export interface FilterGroupConfig { + id: string; + label: string; + properties: Property[]; +} diff --git a/src/features/FilterFonts/model/services/clearAllFilters/clearAllFilters.ts b/src/features/FilterFonts/model/services/clearAllFilters/clearAllFilters.ts deleted file mode 100644 index 260077c..0000000 --- a/src/features/FilterFonts/model/services/clearAllFilters/clearAllFilters.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { categoryFilterStore } from '../../stores/categoryFilterStore'; -import { providersFilterStore } from '../../stores/providersFilterStore'; -import { subsetsFilterStore } from '../../stores/subsetsFilterStore'; - -export function clearAllFilters() { - categoryFilterStore.deselectAllProperties(); - providersFilterStore.deselectAllProperties(); - subsetsFilterStore.deselectAllProperties(); -} diff --git a/src/features/FilterFonts/model/state/manager.svelte.ts b/src/features/FilterFonts/model/state/manager.svelte.ts new file mode 100644 index 0000000..6d3ed27 --- /dev/null +++ b/src/features/FilterFonts/model/state/manager.svelte.ts @@ -0,0 +1,27 @@ +import { createFilterManager } from '../../lib/filterManager/filterManager.svelte'; +import { + FONT_CATEGORIES, + FONT_PROVIDERS, + FONT_SUBSETS, +} from '../const/const'; +import type { FilterGroupConfig } from '../const/types/common'; + +const filtersData: FilterGroupConfig[] = [ + { + id: 'providers', + label: 'Font provider', + properties: FONT_PROVIDERS, + }, + { + id: 'subsets', + label: 'Font subset', + properties: FONT_SUBSETS, + }, + { + id: 'categories', + label: 'Font category', + properties: FONT_CATEGORIES, + }, +]; + +export const filterManager = createFilterManager(filtersData); diff --git a/src/features/FilterFonts/model/stores/categoryFilterStore.ts b/src/features/FilterFonts/model/stores/categoryFilterStore.ts deleted file mode 100644 index cf40782..0000000 --- a/src/features/FilterFonts/model/stores/categoryFilterStore.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - type FilterModel, - createFilterStore, -} from '$shared/lib/store/createFilterStore/createFilterStore'; -import { FONT_CATEGORIES } from '../const/const'; - -/** - * Initial state for CategoryFilter - */ -export const initialState: FilterModel = { - searchQuery: '', - properties: FONT_CATEGORIES, -}; - -/** - * CategoryFilter store - */ -export const categoryFilterStore = createFilterStore(initialState); diff --git a/src/features/FilterFonts/model/stores/providersFilterStore.ts b/src/features/FilterFonts/model/stores/providersFilterStore.ts deleted file mode 100644 index bd9af2b..0000000 --- a/src/features/FilterFonts/model/stores/providersFilterStore.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - type FilterModel, - createFilterStore, -} from '$shared/lib/store/createFilterStore/createFilterStore'; -import { FONT_PROVIDERS } from '../const/const'; - -/** - * Initial state for ProvidersFilter - */ -export const initialState: FilterModel = { - searchQuery: '', - properties: FONT_PROVIDERS, -}; - -/** - * ProvidersFilter store - */ -export const providersFilterStore = createFilterStore(initialState); diff --git a/src/features/FilterFonts/model/stores/subsetsFilterStore.ts b/src/features/FilterFonts/model/stores/subsetsFilterStore.ts deleted file mode 100644 index 7d99b50..0000000 --- a/src/features/FilterFonts/model/stores/subsetsFilterStore.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - type FilterModel, - createFilterStore, -} from '$shared/lib/store/createFilterStore/createFilterStore'; -import { FONT_SUBSETS } from '../const/const'; - -/** - * Initial state for SubsetsFilter - */ -const initialState: FilterModel = { - searchQuery: '', - properties: FONT_SUBSETS, -}; - -/** - * SubsetsFilter store - */ -export const subsetsFilterStore = createFilterStore(initialState); diff --git a/src/shared/lib/store/createFilterStore/createFilterStore.ts b/src/shared/lib/store/createFilterStore/createFilterStore.ts deleted file mode 100644 index a15ab0f..0000000 --- a/src/shared/lib/store/createFilterStore/createFilterStore.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { - type Readable, - type Writable, - derived, - writable, -} from 'svelte/store'; - -export interface Property { - /** - * Property identifier - */ - id: string; - /** - * Property name - */ - name: string; - /** - * Property selected state - */ - selected?: boolean; -} - -export interface FilterModel { - /** - * Search query - */ - searchQuery?: string; - /** - * Properties - */ - properties: Property[]; -} - -/** - * Model for reusable filter store with search support and property selection - */ -export interface FilterStore extends Writable { - /** - * Get the store. - * @returns Readable store with filter data - */ - getStore: () => Readable; - /** - * Get all properties. - * @returns Readable store with properties - */ - getAllProperties: () => Readable; - /** - * Get the selected properties. - * @returns Readable store with selected properties - */ - getSelectedProperties: () => Readable; - /** - * Get the filtered properties. - * @returns Readable store with filtered properties - */ - getFilteredProperties: () => Readable; - /** - * Update the search query filter. - * - * @param searchQuery - Search text (undefined to clear) - */ - setSearchQuery: (searchQuery: string | undefined) => void; - /** - * Clear the search query filter. - */ - clearSearchQuery: () => void; - /** - * Select a property. - * - * @param property - Property to select - */ - selectProperty: (propertyId: string) => void; - /** - * Deselect a property. - * - * @param property - Property to deselect - */ - deselectProperty: (propertyId: string) => void; - /** - * Toggle a property. - * - * @param propertyId - Property ID - */ - toggleProperty: (propertyId: string) => void; - /** - * Select all properties. - */ - selectAllProperties: () => void; - /** - * Deselect all properties. - */ - deselectAllProperties: () => void; -} - -/** - * Create a filter store. - * @param initialState - Initial state of the filter store - * @returns FilterStore - */ -export function createFilterStore( - initialState?: T, -): FilterStore { - const { subscribe, set, update } = writable(initialState); - - return { - /* - * Expose subscribe, set, and update from Writable. - * This makes FilterStore compatible with Writable interface. - */ - subscribe, - set, - update, - /** - * Get the current state of the filter store. - */ - getStore: () => { - return { - subscribe, - }; - }, - /** - * Get the filtered properties. - */ - getAllProperties: () => { - return derived({ subscribe }, $store => { - return $store.properties; - }); - }, - /** - * Get the selected properties. - */ - getSelectedProperties: () => { - return derived({ subscribe }, $store => { - return $store.properties.filter(property => property.selected); - }); - }, - /** - * Get the filtered properties. - */ - getFilteredProperties: () => { - return derived({ subscribe }, $store => { - return $store.properties.filter(property => - property.name.includes($store.searchQuery || '') - ); - }); - }, - /** - * Update the search query filter. - * - * @param searchQuery - Search text (undefined to clear) - */ - setSearchQuery: (searchQuery: string | undefined) => { - update(state => ({ - ...state, - searchQuery: searchQuery || undefined, - })); - }, - /** - * Clear the search query filter. - */ - clearSearchQuery: () => { - update(state => ({ - ...state, - searchQuery: undefined, - })); - }, - /** - * Select a property. - * - * @param propertyId - Property ID - */ - selectProperty: (propertyId: string) => { - update(state => ({ - ...state, - properties: state.properties.map(c => - c.id === propertyId ? { ...c, selected: true } : c - ), - })); - }, - /** - * Deselect a property. - * - * @param propertyId - Property ID - */ - deselectProperty: (propertyId: string) => { - update(state => ({ - ...state, - properties: state.properties.map(c => - c.id === propertyId ? { ...c, selected: false } : c - ), - })); - }, - /** - * Toggle a property. - * - * @param propertyId - Property ID - */ - toggleProperty: (propertyId: string) => { - update(state => ({ - ...state, - properties: state.properties.map(c => - c.id === propertyId ? { ...c, selected: !c.selected } : c - ), - })); - }, - /** - * Select all properties - */ - selectAllProperties: () => { - update(state => ({ - ...state, - properties: state.properties.map(c => ({ ...c, selected: true })), - })); - }, - /** - * Deselect all properties - */ - deselectAllProperties: () => { - update(state => ({ - ...state, - properties: state.properties.map(c => ({ ...c, selected: false })), - })); - }, - }; -} diff --git a/src/shared/lib/utils/filter/createFilter/createFilter.svelte.ts b/src/shared/lib/utils/filter/createFilter/createFilter.svelte.ts new file mode 100644 index 0000000..879e5c9 --- /dev/null +++ b/src/shared/lib/utils/filter/createFilter/createFilter.svelte.ts @@ -0,0 +1,120 @@ +import { SvelteSet } from 'svelte/reactivity'; + +export interface Property { + /** + * Property identifier + */ + id: string; + /** + * Property name + */ + name: string; + /** + * Property selected state + */ + selected?: boolean; +} + +export interface FilterModel { + /** + * Search query + */ + searchQuery?: string; + /** + * Properties + */ + properties: Property[]; +} + +/** + * Create a filter store. + * @param initialState - Initial state of the filter store + */ +export function createFilter( + initialState: T, +) { + let properties = $state( + initialState.properties.map(p => ({ + ...p, + selected: p.selected ?? false, + })), + ); + + const selectedProperties = $derived.by(() => { + const _ = properties; + return properties.filter(p => p.selected); + }); + + const selectedCount = $derived.by(() => { + const _ = properties; + return selectedProperties.length; + }); + + return { + /** + * Get all properties. + */ + get properties() { + return properties; + }, + /** + * Get selected properties. + */ + get selectedProperties() { + return selectedProperties; + }, + /** + * Get selected count. + */ + get selectedCount() { + return selectedCount; + }, + /** + * Toggle property selection. + */ + toggleProperty: (id: string) => { + properties = properties.map(p => ({ + ...p, + selected: p.id === id ? !p.selected : p.selected, + })); + }, + /** + * Select property. + */ + selectProperty(id: string) { + properties = properties.map(p => ({ + ...p, + selected: p.id === id ? true : p.selected, + })); + }, + /** + * Deselect property. + */ + deselectProperty(id: string) { + properties = properties.map(p => ({ + ...p, + selected: p.id === id ? false : p.selected, + })); + }, + /** + * Select all properties. + */ + selectAll: () => { + properties = properties.map(p => ({ + ...p, + selected: true, + })); + }, + /** + * Deselect all properties. + */ + deselectAll: () => { + properties = properties.map(p => ({ + ...p, + selected: false, + })); + }, + }; +} + +export type Filter = ReturnType; diff --git a/src/shared/lib/utils/index.ts b/src/shared/lib/utils/index.ts index c9cfbea..3e54985 100644 --- a/src/shared/lib/utils/index.ts +++ b/src/shared/lib/utils/index.ts @@ -7,4 +7,11 @@ export type { QueryParams, QueryParamValue, } from './buildQueryString'; -export { createVirtualizer } from './createVirtualizer/createVirtualizer'; +export { + createVirtualizer, + type Virtualizer, +} from './createVirtualizer/createVirtualizer.svelte'; +export { + createFilter, + type Filter, +} from './filter/createFilter/createFilter.svelte'; diff --git a/src/shared/ui/CheckboxFilter/CheckboxFilter.svelte b/src/shared/ui/CheckboxFilter/CheckboxFilter.svelte index 68150ed..fbebe2b 100644 --- a/src/shared/ui/CheckboxFilter/CheckboxFilter.svelte +++ b/src/shared/ui/CheckboxFilter/CheckboxFilter.svelte @@ -1,5 +1,5 @@ @@ -114,7 +115,7 @@ const hasSelection = $derived(selectedCount > 0);
- {#each properties as property (property.id)} + {#each filter.properties as property (property.id)}