mirror of
https://github.com/SamyRai/tercul-frontend.git
synced 2025-12-27 02:31:34 +00:00
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:
parent
470a0faa1b
commit
9d5aca38d5
@ -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}
|
||||
/>
|
||||
|
||||
@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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 },
|
||||
);
|
||||
};
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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)',
|
||||
],
|
||||
};
|
||||
|
||||
@ -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": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user