diff --git a/package.json b/package.json index fee7b58..39d6abb 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,15 @@ "private": true, "scripts": { "dev": "vite", - "build": "vue-tsc && vite build", + "build": "node scripts/generate-sprite.js && vue-tsc && vite build", + "icons": "node scripts/generate-sprite.js", "preview": "vite preview", "tauri": "tauri", "lint": "eslint . --fix", "gen:api": "npx swagger-typescript-api@12.0.4 -p http://localhost:1337/api/docs-json -o ./src/api -n api.ts --axios --unwrap-response-data --extract-request-params --extract-request-body --single-http-client" }, "dependencies": { + "@tanstack/vue-query": "^5.101.0", "@floating-ui/vue": "^1.1.5", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ee07cb..59c8019 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,9 @@ importers: '@floating-ui/vue': specifier: ^1.1.5 version: 1.1.11(vue@3.5.35(typescript@5.9.3)) + '@tanstack/vue-query': + specifier: ^5.101.0 + version: 5.101.0(vue@3.5.35(typescript@5.9.3)) '@tauri-apps/api': specifier: ^2 version: 2.11.0 @@ -1086,6 +1089,22 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/match-sorter-utils@8.19.4': + resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} + engines: {node: '>=12'} + + '@tanstack/query-core@5.101.0': + resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} + + '@tanstack/vue-query@5.101.0': + resolution: {integrity: sha512-sZeW0RvfEZ9QRiaXirE/HQZsFT5saMlPZVfeYvjPX6lqSBS9lkD7wfnCfzOBns6HD2f34Gx9cazkuU3Xj6hl/A==} + peerDependencies: + '@vue/composition-api': ^1.1.2 + vue: ^2.6.0 || ^3.3.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + '@tauri-apps/api@2.11.0': resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} @@ -2457,6 +2476,9 @@ packages: resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} hasBin: true + remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + reserved-identifiers@1.2.0: resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} engines: {node: '>=18'} @@ -3373,6 +3395,20 @@ snapshots: tailwindcss: 4.3.0 vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.100.0)(yaml@2.9.0) + '@tanstack/match-sorter-utils@8.19.4': + dependencies: + remove-accents: 0.5.0 + + '@tanstack/query-core@5.101.0': {} + + '@tanstack/vue-query@5.101.0(vue@3.5.35(typescript@5.9.3))': + dependencies: + '@tanstack/match-sorter-utils': 8.19.4 + '@tanstack/query-core': 5.101.0 + '@vue/devtools-api': 6.6.4 + vue: 3.5.35(typescript@5.9.3) + vue-demi: 0.14.10(vue@3.5.35(typescript@5.9.3)) + '@tauri-apps/api@2.11.0': {} '@tauri-apps/cli-darwin-arm64@2.11.2': @@ -4998,6 +5034,8 @@ snapshots: dependencies: jsesc: 3.1.0 + remove-accents@0.5.0: {} + reserved-identifiers@1.2.0: {} resolve-pkg-maps@1.0.0: {} diff --git a/scripts/generate-sprite.js b/scripts/generate-sprite.js new file mode 100644 index 0000000..5e79593 --- /dev/null +++ b/scripts/generate-sprite.js @@ -0,0 +1,49 @@ +import { readFileSync, writeFileSync, readdirSync } from 'fs' +import { join, basename } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const iconsDir = join(__dirname, '../src/assets/icons') + +const svgFiles = readdirSync(iconsDir) + .filter(f => f.endsWith('.svg')) + .sort() + +if (!svgFiles.length) { + console.error('No SVG files found in', iconsDir) + process.exit(1) +} + +// Extract viewBox and inner content from each SVG file +const symbols = svgFiles.map((file) => { + const name = basename(file, '.svg') + const raw = readFileSync(join(iconsDir, file), 'utf-8') + + const viewBox = (raw.match(/viewBox="([^"]+)"/) ?? [])[1] ?? '0 0 24 24' + + // Everything between and + const inner = (raw.match(/]*>([\s\S]*?)<\/svg>/i) ?? [])[1] + ?.trim() + .replace(/\n\s*/g, '\n ') ?? '' + + return ` \n ${inner}\n ` +}) + +// --- sprite.svg --- +const sprite = ` + +${symbols.join('\n')} + +` +writeFileSync(join(iconsDir, 'sprite.svg'), sprite) + +// --- icon-names.ts (IconName union type) --- +const names = svgFiles.map(f => `'${basename(f, '.svg')}'`).join('\n | ') +const meta = `// Generated by scripts/generate-sprite.js — do not edit manually +export type IconName = + | ${names} +` +writeFileSync(join(iconsDir, 'icon-names.ts'), meta) + +console.log(`✓ sprite.svg — ${svgFiles.length} icons`) +console.log('✓ icon-names.ts') diff --git a/src/App.vue b/src/App.vue index d7af000..a63d9b1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,34 +15,37 @@ diff --git a/src/components/feed/FeedCardStack.vue b/src/components/feed/FeedCardStack.vue index adb32d1..bf3fa19 100644 --- a/src/components/feed/FeedCardStack.vue +++ b/src/components/feed/FeedCardStack.vue @@ -56,14 +56,14 @@ async function handleLike(profileId: string) { const activeProfile = authStore.activeProfile if (!activeProfile) return + feedStore.removeCard(profileId) + checkRefill() try { await apiClient.api.likesControllerCreateLike({ sourceProfileId: activeProfile.id, targetProfileId: profileId, type: 'like', }) - feedStore.removeCard(profileId) - checkRefill() } catch { uiStore.addToast('Не удалось отправить лайк', 'error') @@ -74,26 +74,23 @@ async function handleDislike(profileId: string) { const activeProfile = authStore.activeProfile if (!activeProfile) return + feedStore.removeCard(profileId) + checkRefill() try { await apiClient.api.likesControllerCreateLike({ sourceProfileId: activeProfile.id, targetProfileId: profileId, type: 'dislike', }) - feedStore.removeCard(profileId) - checkRefill() } catch { - feedStore.removeCard(profileId) + // Dislike failures are silent — card is already removed } } function checkRefill() { - const activeProfile = authStore.activeProfile - if (!activeProfile) - return if (feedStore.cards.length < 5 && feedStore.hasMore) { - feedStore.fetchNextPage(activeProfile.id) + feedStore.fetchNextPage() } } diff --git a/src/components/feed/FeedFilters.vue b/src/components/feed/FeedFilters.vue index e2580df..32cf654 100644 --- a/src/components/feed/FeedFilters.vue +++ b/src/components/feed/FeedFilters.vue @@ -122,9 +122,7 @@ function toggleTag(tagId: string) { function apply() { feedStore.applyFilters({ ...filters }) - const profileId = authStore.activeProfile?.id - if (profileId) - feedStore.fetchNextPage(profileId) + feedStore.fetchNextPage() emit('close') } diff --git a/src/components/layout/BottomNav.vue b/src/components/layout/BottomNav.vue index baa5597..0776026 100644 --- a/src/components/layout/BottomNav.vue +++ b/src/components/layout/BottomNav.vue @@ -10,7 +10,7 @@ :aria-current="isActive(item.path) ? 'page' : undefined" > - + {{ item.label }} @@ -19,10 +19,12 @@ - -