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
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 && (
<AuthorReadingStats
works={works}
works={worksForDisplay}
workTypes={workTypes}
languages={languages}
/>

View File

@ -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: <BookOpen className="h-4 w-4" />,
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: <Users className="h-4 w-4" />,
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: <Star className="h-4 w-4" />,
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: <TrendingUp className="h-4 w-4" />,
label: "Reads",
value: authorUtils.formatNumber(author.totalReads, format),
value: authorUtils.formatNumber(author.total_reads, format),
key: "reads",
});
}

View File

@ -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

View File

@ -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
*/

View File

@ -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 },
);
};

View File

@ -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', () => {

View File

@ -3,10 +3,16 @@ module.exports = {
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['<rootDir>/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',
'^@/(.*)$': '<rootDir>/client/src/$1',
'^@shared/(.*)$': '<rootDir>/shared/$1',
},
transform: {
'^.+\\\\.tsx?$': ['babel-jest', { useESM: true }],
@ -14,4 +20,8 @@ module.exports = {
transformIgnorePatterns: [
'/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",
"lint": "tsc --noEmit",
"test": "yarn test:unit && yarn test:e2e",
"test:unit": "yarn jest",
"test:unit": "jest",
"test:e2e": "playwright test"
},
"dependencies": {