fix: resolve remaining TypeScript errors and improve type safety

- 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
This commit is contained in:
Damir Mukimov 2025-11-30 15:09:55 +01:00
parent 470a0faa1b
commit 9d5aca38d5
No known key found for this signature in database
GPG Key ID: 42996CC7C73BC750
8 changed files with 82 additions and 27 deletions

View File

@ -42,19 +42,30 @@ export function AuthorWorksDisplay({
// Use the actual API hook to fetch author's works // Use the actual API hook to fetch author's works
const { data: works, isLoading, error } = useAuthorWorks(authorId); 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 // Extract filter options from works data
const years = useMemo( const years = useMemo(
() => () =>
Array.from( 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), ).filter((year): year is string => year !== undefined),
[works] [worksForDisplay]
); );
const languages = useMemo( const languages = useMemo(
() => () =>
Array.from(new Set(works?.map((work) => work.language).filter(Boolean))), Array.from(new Set(worksForDisplay?.map((work) => work.language).filter(Boolean))),
[works] [worksForDisplay]
); );
const workTypes = useMemo( const workTypes = useMemo(
@ -68,7 +79,7 @@ export function AuthorWorksDisplay({
// Filter works based on selected filters // Filter works based on selected filters
const filteredWorks = useMemo( const filteredWorks = useMemo(
() => () =>
works?.filter((work) => { worksForDisplay?.filter((work) => {
if ( if (
filters.selectedYear && filters.selectedYear &&
work.year?.toString() !== filters.selectedYear work.year?.toString() !== filters.selectedYear
@ -89,7 +100,7 @@ export function AuthorWorksDisplay({
} }
return true; return true;
}), }),
[works, filters] [worksForDisplay, filters]
); );
// Group works by type // Group works by type
@ -281,7 +292,7 @@ export function AuthorWorksDisplay({
{/* Reading statistics (extracted component) */} {/* Reading statistics (extracted component) */}
{works && works.length > 0 && ( {works && works.length > 0 && (
<AuthorReadingStats <AuthorReadingStats
works={works} works={worksForDisplay}
workTypes={workTypes} workTypes={workTypes}
languages={languages} languages={languages}
/> />

View File

@ -12,7 +12,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { AuthorWithStats } from "../types"; import type { AuthorWithStats } from "@shared/schema";
import { authorUtils } from "../utils"; import { authorUtils } from "../utils";
const authorStatsVariants = cva("", { const authorStatsVariants = cva("", {
@ -65,38 +65,38 @@ export function AuthorStats({
key: string; key: string;
}> = []; }> = [];
if (showWorksCount && author.worksCount) { if (showWorksCount && author.works_count) {
stats.push({ stats.push({
icon: <BookOpen className="h-4 w-4" />, icon: <BookOpen className="h-4 w-4" />,
label: "Works", label: "Works",
value: authorUtils.formatNumber(author.worksCount, format), value: authorUtils.formatNumber(author.works_count, format),
key: "works", key: "works",
}); });
} }
if (showFollowersCount && author.followersCount) { if (showFollowersCount && author.followers_count) {
stats.push({ stats.push({
icon: <Users className="h-4 w-4" />, icon: <Users className="h-4 w-4" />,
label: "Followers", label: "Followers",
value: authorUtils.formatNumber(author.followersCount, format), value: authorUtils.formatNumber(author.followers_count, format),
key: "followers", key: "followers",
}); });
} }
if (showRating && author.averageRating) { if (showRating && author.average_rating) {
stats.push({ stats.push({
icon: <Star className="h-4 w-4" />, icon: <Star className="h-4 w-4" />,
label: "Rating", label: "Rating",
value: authorUtils.formatRating(author.averageRating), value: authorUtils.formatRating(author.average_rating),
key: "rating", key: "rating",
}); });
} }
if (showReadsCount && author.totalReads) { if (showReadsCount && author.total_reads) {
stats.push({ stats.push({
icon: <TrendingUp className="h-4 w-4" />, icon: <TrendingUp className="h-4 w-4" />,
label: "Reads", label: "Reads",
value: authorUtils.formatNumber(author.totalReads, format), value: authorUtils.formatNumber(author.total_reads, format),
key: "reads", key: "reads",
}); });
} }

View File

@ -97,6 +97,16 @@ export interface AuthorDisplayUtils {
text: string; text: string;
isTruncated: boolean; 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 // Timeline event for author pages

View File

@ -30,6 +30,28 @@ export const authorUtils: AuthorDisplayUtils = {
.slice(0, 2); .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 * Get display location from country/city
*/ */

View File

@ -32,7 +32,7 @@ export function TagManager({ name, tags, label = "Tags" }: TagManagerProps) {
const formTags = form.getValues(name) || []; const formTags = form.getValues(name) || [];
if (tags && formTags.length > 0) { if (tags && formTags.length > 0) {
const initialTags = formTags 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[]; .filter(Boolean) as Tag[];
setSelectedTags(initialTags); setSelectedTags(initialTags);
} }
@ -42,8 +42,7 @@ export function TagManager({ name, tags, label = "Tags" }: TagManagerProps) {
const handleAddTag = () => { const handleAddTag = () => {
if (!selectedTagId) return; if (!selectedTagId) return;
const tagId = parseInt(selectedTagId, 10); const tag = tags?.find((t) => t.id === selectedTagId);
const tag = tags?.find((t) => t.id === tagId);
if (tag && !selectedTags.some((t) => t.id === tag.id)) { if (tag && !selectedTags.some((t) => t.id === tag.id)) {
setSelectedTags([...selectedTags, tag]); setSelectedTags([...selectedTags, tag]);
@ -57,14 +56,14 @@ export function TagManager({ name, tags, label = "Tags" }: TagManagerProps) {
}; };
// Handle tag removal // Handle tag removal
const handleRemoveTag = (tagId: number) => { const handleRemoveTag = (tagId: string) => {
setSelectedTags(selectedTags.filter((tag) => tag.id !== tagId)); setSelectedTags(selectedTags.filter((tag) => tag.id !== tagId));
// Update form values // Update form values
const currentTags = form.getValues(name) || []; const currentTags = form.getValues(name) || [];
form.setValue( form.setValue(
name, name,
currentTags.filter((id: number) => id !== tagId), currentTags.filter((id: string) => id !== tagId),
{ shouldValidate: true }, { shouldValidate: true },
); );
}; };

View File

@ -1,19 +1,22 @@
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import { WorkCard } from '../WorkCard'; import { WorkCard } from '../WorkCard';
import { WorkWithAuthor } from '@/lib/types'; import type { Work } from '@shared/schema';
import { Toaster } from '@/components/ui/toaster'; import { Toaster } from '@/components/ui/toaster';
const mockWork: WorkWithAuthor = { const mockWork: Work & { author: { id: string; name: string } } = {
id: 1, id: '1',
title: 'Test Work', title: 'Test Work',
slug: 'test-work', slug: 'test-work',
type: 'poem', type: 'poem',
year: 2023, year: 2023,
language: 'English', language: 'English',
content: 'Test content',
description: 'This is a test description.', description: 'This is a test description.',
createdAt: new Date().toISOString(),
likes: 10, likes: 10,
tags: [{ id: 1, name: 'Test Tag' }], tags: ['test-tag'],
author: { id: 1, name: 'Test Author' }, authorId: '1',
author: { id: '1', name: 'Test Author' },
}; };
describe('WorkCard', () => { describe('WorkCard', () => {

View File

@ -3,10 +3,16 @@ module.exports = {
testEnvironment: 'jest-environment-jsdom', testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
extensionsToTreatAsEsm: ['.ts', '.tsx'], extensionsToTreatAsEsm: ['.ts', '.tsx'],
globals: {
'ts-jest': {
useESM: true,
},
},
moduleNameMapper: { moduleNameMapper: {
'\\\\.(css|less|scss|sass)$': 'identity-obj-proxy', '\\\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\\\.(gif|ttf|eot|svg|png)$': 'jest-transform-stub', '\\\\.(gif|ttf|eot|svg|png)$': 'jest-transform-stub',
'^@/(.*)$': '<rootDir>/client/src/$1', '^@/(.*)$': '<rootDir>/client/src/$1',
'^@shared/(.*)$': '<rootDir>/shared/$1',
}, },
transform: { transform: {
'^.+\\\\.tsx?$': ['babel-jest', { useESM: true }], '^.+\\\\.tsx?$': ['babel-jest', { useESM: true }],
@ -14,4 +20,8 @@ module.exports = {
transformIgnorePatterns: [ transformIgnorePatterns: [
'/node_modules/(?!wouter|lucide-react)/', '/node_modules/(?!wouter|lucide-react)/',
], ],
testMatch: [
'<rootDir>/client/src/**/__tests__/**/*.(ts|tsx)',
'<rootDir>/client/src/**/*.(test|spec).(ts|tsx)',
],
}; };

View File

@ -10,7 +10,7 @@
"check": "tsc", "check": "tsc",
"lint": "tsc --noEmit", "lint": "tsc --noEmit",
"test": "yarn test:unit && yarn test:e2e", "test": "yarn test:unit && yarn test:e2e",
"test:unit": "yarn jest", "test:unit": "jest",
"test:e2e": "playwright test" "test:e2e": "playwright test"
}, },
"dependencies": { "dependencies": {