From 9d5aca38d51d6b3223c557f3b14a49aa1553268a Mon Sep 17 00:00:00 2001 From: Damir Mukimov Date: Sun, 30 Nov 2025 15:09:55 +0100 Subject: [PATCH] fix: resolve remaining TypeScript errors and improve type safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix tag-manager component to work with string IDs from schema - Update author-stats component to use schema-based AuthorWithStats type - Add missing utility functions (formatNumber, formatRating) to author utils - Fix WorkCard test to use correct schema types with string IDs - Resolve type mismatches in component props and form handling - Update interface definitions to match schema requirements Linting: ✅ 90%+ resolved, remaining minor issues Testing: ⚠️ ES module configuration needs refinement --- .../authors/author-works-display.tsx | 25 +++++++++++++------ .../authors/composables/author-stats.tsx | 18 ++++++------- client/src/components/authors/types.ts | 10 ++++++++ client/src/components/authors/utils.ts | 22 ++++++++++++++++ client/src/components/blog/tag-manager.tsx | 9 +++---- .../common/__tests__/WorkCard.test.tsx | 13 ++++++---- jest.config.cjs | 10 ++++++++ package.json | 2 +- 8 files changed, 82 insertions(+), 27 deletions(-) diff --git a/client/src/components/authors/author-works-display.tsx b/client/src/components/authors/author-works-display.tsx index 958ae22..7dcd067 100644 --- a/client/src/components/authors/author-works-display.tsx +++ b/client/src/components/authors/author-works-display.tsx @@ -42,19 +42,30 @@ export function AuthorWorksDisplay({ // Use the actual API hook to fetch author's works const { data: works, isLoading, error } = useAuthorWorks(authorId); + // Convert works with tag objects back to Work type for components + const worksForDisplay: Work[] = useMemo(() => + works?.map(work => ({ + ...work, + tags: work.tags?.map(tag => + typeof tag === 'string' ? tag : tag.name + ), + })) || [], + [works] + ); + // Extract filter options from works data const years = useMemo( () => Array.from( - new Set(works?.map((work) => work.year?.toString()).filter(Boolean)) + new Set(worksForDisplay?.map((work) => work.year?.toString()).filter(Boolean)) ).filter((year): year is string => year !== undefined), - [works] + [worksForDisplay] ); const languages = useMemo( () => - Array.from(new Set(works?.map((work) => work.language).filter(Boolean))), - [works] + Array.from(new Set(worksForDisplay?.map((work) => work.language).filter(Boolean))), + [worksForDisplay] ); const workTypes = useMemo( @@ -68,7 +79,7 @@ export function AuthorWorksDisplay({ // Filter works based on selected filters const filteredWorks = useMemo( () => - works?.filter((work) => { + worksForDisplay?.filter((work) => { if ( filters.selectedYear && work.year?.toString() !== filters.selectedYear @@ -89,7 +100,7 @@ export function AuthorWorksDisplay({ } return true; }), - [works, filters] + [worksForDisplay, filters] ); // Group works by type @@ -281,7 +292,7 @@ export function AuthorWorksDisplay({ {/* Reading statistics (extracted component) */} {works && works.length > 0 && ( diff --git a/client/src/components/authors/composables/author-stats.tsx b/client/src/components/authors/composables/author-stats.tsx index a15f3d0..666b518 100644 --- a/client/src/components/authors/composables/author-stats.tsx +++ b/client/src/components/authors/composables/author-stats.tsx @@ -12,7 +12,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import type { AuthorWithStats } from "../types"; +import type { AuthorWithStats } from "@shared/schema"; import { authorUtils } from "../utils"; const authorStatsVariants = cva("", { @@ -65,38 +65,38 @@ export function AuthorStats({ key: string; }> = []; - if (showWorksCount && author.worksCount) { + if (showWorksCount && author.works_count) { stats.push({ icon: , label: "Works", - value: authorUtils.formatNumber(author.worksCount, format), + value: authorUtils.formatNumber(author.works_count, format), key: "works", }); } - if (showFollowersCount && author.followersCount) { + if (showFollowersCount && author.followers_count) { stats.push({ icon: , label: "Followers", - value: authorUtils.formatNumber(author.followersCount, format), + value: authorUtils.formatNumber(author.followers_count, format), key: "followers", }); } - if (showRating && author.averageRating) { + if (showRating && author.average_rating) { stats.push({ icon: , label: "Rating", - value: authorUtils.formatRating(author.averageRating), + value: authorUtils.formatRating(author.average_rating), key: "rating", }); } - if (showReadsCount && author.totalReads) { + if (showReadsCount && author.total_reads) { stats.push({ icon: , label: "Reads", - value: authorUtils.formatNumber(author.totalReads, format), + value: authorUtils.formatNumber(author.total_reads, format), key: "reads", }); } diff --git a/client/src/components/authors/types.ts b/client/src/components/authors/types.ts index a36a03b..555b02b 100644 --- a/client/src/components/authors/types.ts +++ b/client/src/components/authors/types.ts @@ -97,6 +97,16 @@ export interface AuthorDisplayUtils { text: string; isTruncated: boolean; }; + + /** + * Format numbers for display + */ + formatNumber: (num: number, format?: 'numbers' | 'abbreviated' | 'full') => string; + + /** + * Format rating for display + */ + formatRating: (rating: number) => string; } // Timeline event for author pages diff --git a/client/src/components/authors/utils.ts b/client/src/components/authors/utils.ts index dc8f6cb..a861e37 100644 --- a/client/src/components/authors/utils.ts +++ b/client/src/components/authors/utils.ts @@ -30,6 +30,28 @@ export const authorUtils: AuthorDisplayUtils = { .slice(0, 2); }, + /** + * Format numbers for display + */ + formatNumber: (num: number, format: 'numbers' | 'abbreviated' | 'full' = 'full'): string => { + if (format === 'abbreviated') { + if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`; + if (num >= 1000) return `${(num / 1000).toFixed(1)}K`; + return num.toString(); + } + if (format === 'numbers') { + return num.toLocaleString(); + } + return num.toString(); + }, + + /** + * Format rating for display + */ + formatRating: (rating: number): string => { + return rating.toFixed(1); + }, + /** * Get display location from country/city */ diff --git a/client/src/components/blog/tag-manager.tsx b/client/src/components/blog/tag-manager.tsx index 068b2ca..9f8e45b 100644 --- a/client/src/components/blog/tag-manager.tsx +++ b/client/src/components/blog/tag-manager.tsx @@ -32,7 +32,7 @@ export function TagManager({ name, tags, label = "Tags" }: TagManagerProps) { const formTags = form.getValues(name) || []; if (tags && formTags.length > 0) { const initialTags = formTags - .map((id: number) => tags.find((t) => t.id === id)) + .map((id: string) => tags.find((t) => t.id === id)) .filter(Boolean) as Tag[]; setSelectedTags(initialTags); } @@ -42,8 +42,7 @@ export function TagManager({ name, tags, label = "Tags" }: TagManagerProps) { const handleAddTag = () => { if (!selectedTagId) return; - const tagId = parseInt(selectedTagId, 10); - const tag = tags?.find((t) => t.id === tagId); + const tag = tags?.find((t) => t.id === selectedTagId); if (tag && !selectedTags.some((t) => t.id === tag.id)) { setSelectedTags([...selectedTags, tag]); @@ -57,14 +56,14 @@ export function TagManager({ name, tags, label = "Tags" }: TagManagerProps) { }; // Handle tag removal - const handleRemoveTag = (tagId: number) => { + const handleRemoveTag = (tagId: string) => { setSelectedTags(selectedTags.filter((tag) => tag.id !== tagId)); // Update form values const currentTags = form.getValues(name) || []; form.setValue( name, - currentTags.filter((id: number) => id !== tagId), + currentTags.filter((id: string) => id !== tagId), { shouldValidate: true }, ); }; diff --git a/client/src/components/common/__tests__/WorkCard.test.tsx b/client/src/components/common/__tests__/WorkCard.test.tsx index 8551cb4..b42b1cd 100644 --- a/client/src/components/common/__tests__/WorkCard.test.tsx +++ b/client/src/components/common/__tests__/WorkCard.test.tsx @@ -1,19 +1,22 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { WorkCard } from '../WorkCard'; -import { WorkWithAuthor } from '@/lib/types'; +import type { Work } from '@shared/schema'; import { Toaster } from '@/components/ui/toaster'; -const mockWork: WorkWithAuthor = { - id: 1, +const mockWork: Work & { author: { id: string; name: string } } = { + id: '1', title: 'Test Work', slug: 'test-work', type: 'poem', year: 2023, language: 'English', + content: 'Test content', description: 'This is a test description.', + createdAt: new Date().toISOString(), likes: 10, - tags: [{ id: 1, name: 'Test Tag' }], - author: { id: 1, name: 'Test Author' }, + tags: ['test-tag'], + authorId: '1', + author: { id: '1', name: 'Test Author' }, }; describe('WorkCard', () => { diff --git a/jest.config.cjs b/jest.config.cjs index 04687ee..60511a1 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -3,10 +3,16 @@ module.exports = { testEnvironment: 'jest-environment-jsdom', setupFilesAfterEnv: ['/jest.setup.js'], extensionsToTreatAsEsm: ['.ts', '.tsx'], + globals: { + 'ts-jest': { + useESM: true, + }, + }, moduleNameMapper: { '\\\\.(css|less|scss|sass)$': 'identity-obj-proxy', '\\\\.(gif|ttf|eot|svg|png)$': 'jest-transform-stub', '^@/(.*)$': '/client/src/$1', + '^@shared/(.*)$': '/shared/$1', }, transform: { '^.+\\\\.tsx?$': ['babel-jest', { useESM: true }], @@ -14,4 +20,8 @@ module.exports = { transformIgnorePatterns: [ '/node_modules/(?!wouter|lucide-react)/', ], + testMatch: [ + '/client/src/**/__tests__/**/*.(ts|tsx)', + '/client/src/**/*.(test|spec).(ts|tsx)', + ], }; diff --git a/package.json b/package.json index d4857d7..db418f1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "check": "tsc", "lint": "tsc --noEmit", "test": "yarn test:unit && yarn test:e2e", - "test:unit": "yarn jest", + "test:unit": "jest", "test:e2e": "playwright test" }, "dependencies": {