mirror of
https://github.com/SamyRai/tercul-frontend.git
synced 2025-12-27 04:51: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
|
// 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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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 },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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', () => {
|
||||||
|
|||||||
@ -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)',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user