diff --git a/frontend/__tests__/unit/components/AnimatedCounter.test.tsx b/frontend/__tests__/unit/components/AnimatedCounter.test.tsx index 1f1a1e421f..b12061aa03 100644 --- a/frontend/__tests__/unit/components/AnimatedCounter.test.tsx +++ b/frontend/__tests__/unit/components/AnimatedCounter.test.tsx @@ -71,7 +71,7 @@ describe('AnimatedCounter', () => { }) // Should show intermediate value - const displayedValue = parseInt(screen.getByText(/\d+/).textContent || '0') + const displayedValue = Number.parseInt(screen.getByText(/\d+/).textContent || '0') expect(displayedValue).toBeGreaterThan(0) expect(displayedValue).toBeLessThanOrEqual(50) }) diff --git a/frontend/__tests__/unit/components/SearchPageLayout.test.tsx b/frontend/__tests__/unit/components/SearchPageLayout.test.tsx index 958a480535..fa9889a64f 100644 --- a/frontend/__tests__/unit/components/SearchPageLayout.test.tsx +++ b/frontend/__tests__/unit/components/SearchPageLayout.test.tsx @@ -251,7 +251,7 @@ describe('', () => { {}} onPageChange={handlePageChange} searchQuery="" @@ -346,7 +346,7 @@ describe('', () => { render( {}} onPageChange={() => {}} diff --git a/frontend/eslint-rules/no-global-isfinite.mjs b/frontend/eslint-rules/no-global-isfinite.mjs new file mode 100644 index 0000000000..86c186a4bf --- /dev/null +++ b/frontend/eslint-rules/no-global-isfinite.mjs @@ -0,0 +1,31 @@ +const noGlobalIsFiniteRule = { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow use of global isFinite, use Number.isFinite instead', + recommended: false, + }, + fixable: 'code', + messages: { + useNumberIsFinite: 'Use Number.isFinite() instead of global isFinite().', + }, + schema: [], + }, + create(context) { + return { + CallExpression(node) { + if (node.callee.type === 'Identifier' && node.callee.name === 'isFinite') { + context.report({ + node: node.callee, + messageId: 'useNumberIsFinite', + fix(fixer) { + return fixer.replaceText(node.callee, 'Number.isFinite') + }, + }) + } + }, + } + }, +} + +export default noGlobalIsFiniteRule diff --git a/frontend/eslint-rules/no-global-isnan.mjs b/frontend/eslint-rules/no-global-isnan.mjs new file mode 100644 index 0000000000..123f1ee758 --- /dev/null +++ b/frontend/eslint-rules/no-global-isnan.mjs @@ -0,0 +1,31 @@ +const noGlobalIsNaNRule = { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow use of global isNaN, use Number.isNaN instead', + recommended: false, + }, + fixable: 'code', + messages: { + useNumberIsNaN: 'Use Number.isNaN() instead of global isNaN().', + }, + schema: [], + }, + create(context) { + return { + CallExpression(node) { + if (node.callee.type === 'Identifier' && node.callee.name === 'isNaN') { + context.report({ + node: node.callee, + messageId: 'useNumberIsNaN', + fix(fixer) { + return fixer.replaceText(node.callee, 'Number.isNaN') + }, + }) + } + }, + } + }, +} + +export default noGlobalIsNaNRule diff --git a/frontend/eslint-rules/no-global-nan.mjs b/frontend/eslint-rules/no-global-nan.mjs new file mode 100644 index 0000000000..0a499b8faa --- /dev/null +++ b/frontend/eslint-rules/no-global-nan.mjs @@ -0,0 +1,44 @@ +const noGlobalNaNRule = { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow use of global NaN, use Number.NaN instead', + recommended: false, + }, + fixable: 'code', + messages: { + useNumberNaN: 'Use Number.NaN instead of global NaN.', + }, + schema: [], + }, + create(context) { + return { + Identifier(node) { + // Only report if it's 'NaN' and not already part of 'Number.NaN' + if (node.name === 'NaN') { + const parent = node.parent + if ( + parent && + parent.type === 'MemberExpression' && + parent.property === node && + parent.object && + parent.object.type === 'Identifier' && + parent.object.name === 'Number' + ) { + return + } + + context.report({ + node, + messageId: 'useNumberNaN', + fix(fixer) { + return fixer.replaceText(node, 'Number.NaN') + }, + }) + } + }, + } + }, +} + +export default noGlobalNaNRule diff --git a/frontend/eslint-rules/no-global-parsefloat.mjs b/frontend/eslint-rules/no-global-parsefloat.mjs new file mode 100644 index 0000000000..8e5dbd234a --- /dev/null +++ b/frontend/eslint-rules/no-global-parsefloat.mjs @@ -0,0 +1,31 @@ +const noGlobalParseFloatRule = { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow use of global parseFloat, use Number.parseFloat instead', + recommended: false, + }, + fixable: 'code', + messages: { + useNumberParseFloat: 'Use Number.parseFloat() instead of global parseFloat().', + }, + schema: [], + }, + create(context) { + return { + CallExpression(node) { + if (node.callee.type === 'Identifier' && node.callee.name === 'parseFloat') { + context.report({ + node: node.callee, + messageId: 'useNumberParseFloat', + fix(fixer) { + return fixer.replaceText(node.callee, 'Number.parseFloat') + }, + }) + } + }, + } + }, +} + +export default noGlobalParseFloatRule diff --git a/frontend/eslint-rules/no-global-parseint.mjs b/frontend/eslint-rules/no-global-parseint.mjs new file mode 100644 index 0000000000..75d26028c3 --- /dev/null +++ b/frontend/eslint-rules/no-global-parseint.mjs @@ -0,0 +1,31 @@ +const noGlobalParseIntRule = { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow use of global parseInt, use Number.parseInt instead', + recommended: false, + }, + fixable: 'code', + messages: { + useNumberParseInt: 'Use Number.parseInt() instead of global parseInt().', + }, + schema: [], + }, + create(context) { + return { + CallExpression(node) { + if (node.callee.type === 'Identifier' && node.callee.name === 'parseInt') { + context.report({ + node: node.callee, + messageId: 'useNumberParseInt', + fix(fixer) { + return fixer.replaceText(node.callee, 'Number.parseInt') + }, + }) + } + }, + } + }, +} + +export default noGlobalParseIntRule diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index a5045824dd..edf4c88d8b 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -15,6 +15,11 @@ import react from 'eslint-plugin-react' import reactHooks from 'eslint-plugin-react-hooks' import nextPlugin from '@next/eslint-plugin-next' import globals from 'globals' +import noGlobalIsFiniteRule from './eslint-rules/no-global-isfinite.mjs' +import noGlobalIsNaNRule from './eslint-rules/no-global-isnan.mjs' +import noGlobalNaNRule from './eslint-rules/no-global-nan.mjs' +import noGlobalParseFloatRule from './eslint-rules/no-global-parsefloat.mjs' +import noGlobalParseIntRule from './eslint-rules/no-global-parseint.mjs' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -61,6 +66,15 @@ const eslintConfig = [ react, 'jsx-a11y': jsxA11y, '@next/next': nextPlugin, + nest: { + rules: { + 'no-global-isfinite': noGlobalIsFiniteRule, + 'no-global-isnan': noGlobalIsNaNRule, + 'no-global-nan': noGlobalNaNRule, + 'no-global-parsefloat': noGlobalParseFloatRule, + 'no-global-parseint': noGlobalParseIntRule, + }, + }, }, settings: { 'import/resolver': { @@ -149,6 +163,11 @@ const eslintConfig = [ 'jsx-a11y/no-distracting-elements': 'warn', 'jsx-a11y/label-has-associated-control': 'error', 'jsx-a11y/click-events-have-key-events': 'warn', + 'nest/no-global-isfinite': 'error', + 'nest/no-global-isnan': 'error', + 'nest/no-global-nan': 'error', + 'nest/no-global-parsefloat': 'error', + 'nest/no-global-parseint': 'error', }, }, { diff --git a/frontend/src/app/my/mentorship/page.tsx b/frontend/src/app/my/mentorship/page.tsx index 3de19c9c32..e8271be1ad 100644 --- a/frontend/src/app/my/mentorship/page.tsx +++ b/frontend/src/app/my/mentorship/page.tsx @@ -24,7 +24,7 @@ const MyMentorshipPage: React.FC = () => { const username = (session as ExtendedSession)?.user?.login const initialQuery = searchParams.get('q') || '' - const initialPage = parseInt(searchParams.get('page') || '1', 10) + const initialPage = Number.parseInt(searchParams.get('page') || '1', 10) const [searchQuery, setSearchQuery] = useState(initialQuery) const [debouncedQuery, setDebouncedQuery] = useState(initialQuery) diff --git a/frontend/src/components/ModuleCard.tsx b/frontend/src/components/ModuleCard.tsx index 479e9ec136..4f70839e6c 100644 --- a/frontend/src/components/ModuleCard.tsx +++ b/frontend/src/components/ModuleCard.tsx @@ -98,7 +98,7 @@ export default ModuleCard export const getSimpleDuration = (start: string, end: string): string => { const startDate = new Date(start) const endDate = new Date(end) - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) { return 'Invalid duration' } diff --git a/frontend/src/hooks/useSearchPage.ts b/frontend/src/hooks/useSearchPage.ts index dd92aa9545..7846ea3684 100644 --- a/frontend/src/hooks/useSearchPage.ts +++ b/frontend/src/hooks/useSearchPage.ts @@ -37,7 +37,9 @@ export function useSearchPage({ const searchParams = useSearchParams() const [items, setItems] = useState([]) - const [currentPage, setCurrentPage] = useState(parseInt(searchParams.get('page') || '1')) + const [currentPage, setCurrentPage] = useState( + Number.parseInt(searchParams.get('page') || '1') + ) const [searchQuery, setSearchQuery] = useState(searchParams.get('q') || '') const [sortBy, setSortBy] = useState(searchParams.get('sortBy') || defaultSortBy) const [order, setOrder] = useState(searchParams.get('order') || defaultOrder) diff --git a/frontend/src/utils/dateFormatter.ts b/frontend/src/utils/dateFormatter.ts index 26b6e0a170..3c6ab04f36 100644 --- a/frontend/src/utils/dateFormatter.ts +++ b/frontend/src/utils/dateFormatter.ts @@ -8,7 +8,7 @@ export const formatDate = (input: number | string) => { ? new Date(input * 1000) // Unix timestamp in seconds : new Date(input) // ISO date string - if (isNaN(date.getTime())) { + if (Number.isNaN(date.getTime())) { throw new Error('Invalid date') } @@ -23,7 +23,7 @@ export const formatDateRange = (startDate: number | string, endDate: number | st const start = typeof startDate === 'number' ? new Date(startDate * 1000) : new Date(startDate) const end = typeof endDate === 'number' ? new Date(endDate * 1000) : new Date(endDate) - if (isNaN(start.getTime()) || isNaN(end.getTime())) { + if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { throw new Error('Invalid date') } @@ -63,7 +63,7 @@ export const formatDateRange = (startDate: number | string, endDate: number | st export const formatDateForInput = (dateStr: string) => { if (!dateStr) return '' const date = new Date(dateStr) - if (isNaN(date.getTime())) { + if (Number.isNaN(date.getTime())) { throw new Error('Invalid date') } return date.toISOString().slice(0, 10) diff --git a/frontend/src/utils/round.ts b/frontend/src/utils/round.ts index 7440658196..8b41f06654 100644 --- a/frontend/src/utils/round.ts +++ b/frontend/src/utils/round.ts @@ -1,5 +1,5 @@ export const round = (value: number, precision = 2): number => { - if (isNaN(value)) return 0 // Handle NaN values + if (Number.isNaN(value)) return 0 // Handle NaN values if (precision < 0) { throw new Error('Precision must be a non-negative integer') }