From 3d1bc9418306086863582d20887b33fc5496e33d Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 8 Aug 2025 14:17:11 +0200 Subject: [PATCH] fix: Update private key field in chat integration documentation - Change the private key field name from "prvkey" to "prvkey_hex" in the CHAT_INTEGRATION.md file for clarity and consistency with expected data formats. feat: Add Fuzzy Search Component and Composable - Introduce a new FuzzySearch component for Vue 3, leveraging Fuse.js for intelligent search capabilities. - Implement a useFuzzySearch composable for flexible search functionality, allowing configuration of search options. - Create demo and README files to showcase usage and features of the fuzzy search implementation. - Update package.json and package-lock.json to include @vueuse/integrations version 13.6.0 for enhanced performance. --- CHAT_INTEGRATION.md | 2 +- package-lock.json | 111 ++++++++ package.json | 1 + .../ui/fuzzy-search/FuzzySearch.vue | 133 ++++++++++ .../ui/fuzzy-search/FuzzySearchDemo.vue | 116 +++++++++ src/components/ui/fuzzy-search/README.md | 236 ++++++++++++++++++ src/components/ui/fuzzy-search/index.ts | 2 + src/composables/useFuzzySearch.ts | 183 ++++++++++++++ 8 files changed, 783 insertions(+), 1 deletion(-) create mode 100644 src/components/ui/fuzzy-search/FuzzySearch.vue create mode 100644 src/components/ui/fuzzy-search/FuzzySearchDemo.vue create mode 100644 src/components/ui/fuzzy-search/README.md create mode 100644 src/components/ui/fuzzy-search/index.ts create mode 100644 src/composables/useFuzzySearch.ts diff --git a/CHAT_INTEGRATION.md b/CHAT_INTEGRATION.md index 34a3131..753265f 100644 --- a/CHAT_INTEGRATION.md +++ b/CHAT_INTEGRATION.md @@ -59,7 +59,7 @@ Response: "username": "username", "email": "email@example.com", "pubkey": "nostr_public_key", - "prvkey": "nostr_private_key", + "prvkey": "nostr_private_key_hex", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } diff --git a/package-lock.json b/package-lock.json index 10595cf..bce9df5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@vueuse/components": "^12.5.0", "@vueuse/core": "^12.8.2", "@vueuse/head": "^2.0.0", + "@vueuse/integrations": "^13.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -5508,6 +5509,116 @@ "vue": ">=2.7 || >=3" } }, + "node_modules/@vueuse/integrations": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.6.0.tgz", + "integrity": "sha512-dVFdgwYvkYjdizRL3ESdUW+Hg84i9Yhuzs+Ec3kEcuzJmT5xhiL/IGdw4z394qSBngUQvFi+wbHwhHX3EGbAxQ==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "13.6.0", + "@vueuse/shared": "13.6.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/core": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.6.0.tgz", + "integrity": "sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "13.6.0", + "@vueuse/shared": "13.6.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.6.0.tgz", + "integrity": "sha512-rnIH7JvU7NjrpexTsl2Iwv0V0yAx9cw7+clymjKuLSXG0QMcLD0LDgdNmXic+qL0SGvgSVPEpM9IDO/wqo1vkQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/shared": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.6.0.tgz", + "integrity": "sha512-pDykCSoS2T3fsQrYqf9SyF0QXWHmcGPQ+qiOVjlYSzlWd9dgppB2bFSM1GgKKkt7uzn0BBMV3IbJsUfHG2+BCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, "node_modules/@vueuse/metadata": { "version": "12.8.2", "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", diff --git a/package.json b/package.json index 3f79d78..e91827a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@vueuse/components": "^12.5.0", "@vueuse/core": "^12.8.2", "@vueuse/head": "^2.0.0", + "@vueuse/integrations": "^13.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/src/components/ui/fuzzy-search/FuzzySearch.vue b/src/components/ui/fuzzy-search/FuzzySearch.vue new file mode 100644 index 0000000..5de762b --- /dev/null +++ b/src/components/ui/fuzzy-search/FuzzySearch.vue @@ -0,0 +1,133 @@ + + + + + \ No newline at end of file diff --git a/src/components/ui/fuzzy-search/FuzzySearchDemo.vue b/src/components/ui/fuzzy-search/FuzzySearchDemo.vue new file mode 100644 index 0000000..3ba8a67 --- /dev/null +++ b/src/components/ui/fuzzy-search/FuzzySearchDemo.vue @@ -0,0 +1,116 @@ + + + \ No newline at end of file diff --git a/src/components/ui/fuzzy-search/README.md b/src/components/ui/fuzzy-search/README.md new file mode 100644 index 0000000..112c555 --- /dev/null +++ b/src/components/ui/fuzzy-search/README.md @@ -0,0 +1,236 @@ +# Fuzzy Search Component + +A powerful fuzzy search implementation for Vue 3 using Fuse.js and VueUse integrations. + +## Features + +- 🔍 **Fuzzy Search**: Intelligent search with typo tolerance and scoring +- ⚡ **VueUse Integration**: Built on top of `@vueuse/integrations` for optimal performance +- 🎯 **Configurable**: Customizable search options and behavior +- 🎨 **Vue 3 Compatible**: Built with Vue 3 Composition API and TypeScript +- 📦 **Reusable**: Both composable and component versions available + +## Installation + +The fuzzy search functionality is already installed and available in this project. It uses: + +- `fuse.js` - For fuzzy search algorithms +- `@vueuse/integrations` - For Vue 3 integration + +## Usage + +### Using the Composable + +```vue + + + +``` + +### Using the Component + +```vue + + + +``` + +## API Reference + +### `useFuzzySearch` Composable + +#### Parameters + +- `data: Ref | T[]` - The data to search through +- `options?: FuzzySearchOptions` - Configuration options + +#### Returns + +- `searchQuery: Ref` - Current search query +- `results: ComputedRef[]>` - Search results with scoring +- `filteredItems: ComputedRef` - Filtered items (just the items) +- `isSearching: ComputedRef` - Whether search is active +- `resultCount: ComputedRef` - Number of results found +- `clearSearch: () => void` - Clear the current search +- `setSearchQuery: (query: string) => void` - Set the search query +- `updateData: (data: T[]) => void` - Update the data to search through + +### `FuzzySearch` Component + +#### Props + +- `data: T[]` - The data to search through +- `options?: FuzzySearchOptions` - Configuration options +- `placeholder?: string` - Placeholder text for the search input +- `showClearButton?: boolean` - Whether to show a clear button +- `showResultCount?: boolean` - Whether to show the result count +- `class?: string` - Custom class for the search container +- `disabled?: boolean` - Whether the search input should be disabled + +#### Events + +- `update:modelValue` - Emitted when the search query changes +- `search` - Emitted when a search is performed +- `results` - Emitted when search results change +- `clear` - Emitted when the search is cleared + +### `FuzzySearchOptions` + +```typescript +interface FuzzySearchOptions { + /** + * Fuse.js options for configuring the search behavior + */ + fuseOptions?: FuseOptions + /** + * Maximum number of results to return + */ + resultLimit?: number + /** + * Whether to return all items when search is empty + */ + matchAllWhenSearchEmpty?: boolean + /** + * Debounce delay in milliseconds for search input + */ + debounceMs?: number + /** + * Minimum search length before triggering search + */ + minSearchLength?: number +} +``` + +## Configuration Examples + +### Basic Search + +```typescript +const options = { + fuseOptions: { + keys: ['name', 'description'], + threshold: 0.3, + }, +} +``` + +### Advanced Search + +```typescript +const options = { + fuseOptions: { + keys: [ + { name: 'name', weight: 0.7 }, + { name: 'description', weight: 0.3 }, + { name: 'category', weight: 0.2 }, + ], + threshold: 0.2, + distance: 100, + ignoreLocation: true, + useExtendedSearch: false, + minMatchCharLength: 2, + shouldSort: true, + findAllMatches: false, + location: 0, + isCaseSensitive: false, + }, + resultLimit: 20, + matchAllWhenSearchEmpty: true, + minSearchLength: 2, +} +``` + +### Search with Weighted Keys + +```typescript +const options = { + fuseOptions: { + keys: [ + { name: 'title', weight: 0.8 }, + { name: 'content', weight: 0.5 }, + { name: 'tags', weight: 0.3 }, + ], + threshold: 0.4, + }, +} +``` + +## Performance Tips + +1. **Use `resultLimit`** to limit the number of results for better performance +2. **Set appropriate `threshold`** values (0.0 = perfect match, 1.0 = match anything) +3. **Use weighted keys** to prioritize certain fields +4. **Consider `minSearchLength`** to avoid searching on very short queries +5. **Use `ignoreLocation: true`** for better performance when location doesn't matter + +## Examples + +See `FuzzySearchDemo.vue` for a complete example of how to use the fuzzy search functionality. \ No newline at end of file diff --git a/src/components/ui/fuzzy-search/index.ts b/src/components/ui/fuzzy-search/index.ts new file mode 100644 index 0000000..7e3dd40 --- /dev/null +++ b/src/components/ui/fuzzy-search/index.ts @@ -0,0 +1,2 @@ +export { default as FuzzySearch } from './FuzzySearch.vue' +export { useFuzzySearch, type FuzzySearchOptions, type UseFuzzySearchReturn } from '@/composables/useFuzzySearch' \ No newline at end of file diff --git a/src/composables/useFuzzySearch.ts b/src/composables/useFuzzySearch.ts new file mode 100644 index 0000000..203af5c --- /dev/null +++ b/src/composables/useFuzzySearch.ts @@ -0,0 +1,183 @@ +import { ref, computed, type Ref, type ComputedRef } from 'vue' +import { useFuse, type UseFuseOptions, type FuseOptions } from '@vueuse/integrations' +import type { FuseResult } from 'fuse.js' + +export interface FuzzySearchOptions { + /** + * Fuse.js options for configuring the search behavior + */ + fuseOptions?: FuseOptions + /** + * Maximum number of results to return + */ + resultLimit?: number + /** + * Whether to return all items when search is empty + */ + matchAllWhenSearchEmpty?: boolean + /** + * Debounce delay in milliseconds for search input + */ + debounceMs?: number + /** + * Minimum search length before triggering search + */ + minSearchLength?: number +} + +export interface UseFuzzySearchReturn { + /** + * Current search query + */ + searchQuery: Ref + /** + * Search results with Fuse.js scoring + */ + results: ComputedRef[]> + /** + * Filtered items (just the items without scoring) + */ + filteredItems: ComputedRef + /** + * Whether search is currently active + */ + isSearching: ComputedRef + /** + * Number of results found + */ + resultCount: ComputedRef + /** + * Clear the current search + */ + clearSearch: () => void + /** + * Set the search query + */ + setSearchQuery: (query: string) => void + /** + * Update the data to search through + */ + updateData: (data: T[]) => void +} + +/** + * Composable for fuzzy search functionality using Fuse.js + * + * @param data - The data to search through + * @param options - Configuration options for the fuzzy search + * @returns Object with search functionality and results + * + * @example + * ```ts + * // Basic usage + * const { searchQuery, results, filteredItems, clearSearch } = useFuzzySearch( + * products, + * { + * fuseOptions: { + * keys: ['name', 'description'], + * threshold: 0.3 + * }, + * resultLimit: 10, + * minSearchLength: 2 + * } + * ) + * + * // Integration with existing market functionality + * const { searchQuery, filteredItems } = useFuzzySearch( + * marketStore.products, + * { + * fuseOptions: { + * keys: [ + * { name: 'name', weight: 0.7 }, + * { name: 'description', weight: 0.3 }, + * { name: 'stallName', weight: 0.2 }, + * { name: 'categories', weight: 0.1 } + * ], + * threshold: 0.3, + * ignoreLocation: true + * }, + * resultLimit: 50, + * minSearchLength: 2 + * } + * ) + * ``` + */ +export function useFuzzySearch( + data: Ref | T[], + options: FuzzySearchOptions = {} +): UseFuzzySearchReturn { + const { + fuseOptions = { + // Default Fuse.js options + threshold: 0.3, + distance: 100, + ignoreLocation: true, + useExtendedSearch: false, + minMatchCharLength: 1, + shouldSort: true, + findAllMatches: false, + location: 0, + isCaseSensitive: false, + keys: [] + }, + resultLimit, + matchAllWhenSearchEmpty = true, + debounceMs = 300, + minSearchLength = 0 + } = options + + // Search query state + const searchQuery = ref('') + + // Create the Fuse instance using VueUse integration + const { results } = useFuse( + searchQuery, + data, + { + fuseOptions, + resultLimit, + matchAllWhenSearchEmpty + } + ) + + // Computed properties + const isSearching = computed(() => { + const query = searchQuery.value.trim() + return query.length >= minSearchLength + }) + + const filteredItems = computed(() => { + return results.value.map(result => result.item) + }) + + const resultCount = computed(() => results.value.length) + + // Methods + const clearSearch = () => { + searchQuery.value = '' + } + + const setSearchQuery = (query: string) => { + searchQuery.value = query + } + + const updateData = (newData: T[]) => { + // The useFuse composable automatically watches for data changes + // so we just need to update the ref if it's reactive + if (Array.isArray(data)) { + // If data is not reactive, we need to handle this differently + console.warn('Data is not reactive. Consider using ref() for the data parameter.') + } + } + + return { + searchQuery, + results, + filteredItems, + isSearching, + resultCount, + clearSearch, + setSearchQuery, + updateData + } +} \ No newline at end of file