From f25e5e05975db4766995c8f2a80940ceac718aad Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 19 Jan 2026 23:55:01 +0000 Subject: [PATCH] feat: refactor ESLint configuration and update dependencies; improve error handling and code structure --- .eslintrc.json | 3 -- eslint.config.mjs | 36 ++++++++++++++++++ next.config.js | 1 + package-lock.json | 17 +++------ package.json | 4 +- src/components/demo/PersistenceExample.tsx | 4 +- src/components/error/AIErrorHelper.tsx | 2 +- .../namespace-manager/NamespaceSelector.tsx | 14 +++---- .../features/python-runner/PythonOutput.tsx | 4 +- .../features/snippet-display/SnippetCard.tsx | 38 +++++++------------ .../features/snippet-editor/ReactPreview.tsx | 17 +++------ src/components/layout/BackendIndicator.tsx | 11 +----- src/components/organisms/OrganismsSection.tsx | 2 - .../settings/OpenAISettingsCard.tsx | 12 +----- .../templates/DashboardTemplate.tsx | 1 - .../ui/sidebar-menu/SidebarMenuSkeleton.tsx | 8 ++-- src/hooks/use-mobile.ts | 6 ++- src/hooks/usePythonTerminal.ts | 2 - src/lib/db.ts | 2 +- src/lib/monaco-config.ts | 8 ++-- src/lib/parse-parameters.ts | 4 +- src/lib/pyodide-runner.ts | 4 +- src/lib/storage.ts | 6 +-- src/store/hooks/usePersistenceConfig.ts | 2 +- src/store/slices/snippetsSlice.ts | 4 +- 25 files changed, 102 insertions(+), 110 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 eslint.config.mjs diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3722418..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next/core-web-vitals", "next/typescript"] -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..49f8452 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,36 @@ +import js from '@eslint/js' +import tseslint from 'typescript-eslint' +import nextPlugin from '@next/eslint-plugin-next' +import reactHooks from 'eslint-plugin-react-hooks' +import globals from 'globals' + +export default [ + { + ignores: ['node_modules', '.next', 'dist', 'coverage'], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + nextPlugin.configs['core-web-vitals'], + { + name: 'react-hooks/custom', + plugins: { + 'react-hooks': reactHooks, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-hooks/set-state-in-effect': 'off', + 'react-hooks/purity': 'off', + }, + }, + { + rules: { + '@next/next/no-page-custom-font': 'off', + }, + }, + { + files: ['*.config.js', '*.config.cjs', '*.config.mjs', 'next.config.js'], + languageOptions: { + globals: globals.node, + }, + }, +] diff --git a/next.config.js b/next.config.js index 93718cc..a788b26 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,4 @@ +/* eslint-env node */ /** @type {import('next').NextConfig} */ const nextConfig = { output: process.env.BUILD_STATIC ? 'export' : 'standalone', diff --git a/package-lock.json b/package-lock.json index 9f9d986..4b74a5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "marked": "^15.0.7", "next": "16.1.3", "next-themes": "^0.4.6", - "pyodide": "^0.29.1", + "pyodide": "^0.27.0", "react": "^19.2.3", "react-dom": "^19.0.0", "react-error-boundary": "^6.0.0", @@ -3450,12 +3450,6 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, - "node_modules/@types/emscripten": { - "version": "1.41.5", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", - "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==", - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -7406,12 +7400,11 @@ } }, "node_modules/pyodide": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.29.1.tgz", - "integrity": "sha512-mpk9jtkiM7Ugh1r9P9dbR8vKrmf0lED32hBZq+Fn1kkkBiUoOjSsJEWcyprugICpiFpIXpUOf80ZrvFXkQMk2g==", - "license": "MPL-2.0", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.27.0.tgz", + "integrity": "sha512-0O992noCKqv8lPw4+QFWw8d8yrNWVtQ6zPhWNg/RNYbFohiwmV6SGlVh5fWk/4pOxyhgHbayq+ur8JoR/r5U9A==", + "license": "Apache-2.0", "dependencies": { - "@types/emscripten": "^1.41.4", "ws": "^8.5.0" }, "engines": { diff --git a/package.json b/package.json index d0b6c7b..e458b71 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint .", "kill": "fuser -k 5000/tcp" }, "dependencies": { @@ -52,7 +52,7 @@ "marked": "^15.0.7", "next": "16.1.3", "next-themes": "^0.4.6", - "pyodide": "^0.29.1", + "pyodide": "^0.27.0", "react": "^19.2.3", "react-dom": "^19.0.0", "react-error-boundary": "^6.0.0", diff --git a/src/components/demo/PersistenceExample.tsx b/src/components/demo/PersistenceExample.tsx index d7c4c22..25d09db 100644 --- a/src/components/demo/PersistenceExample.tsx +++ b/src/components/demo/PersistenceExample.tsx @@ -4,8 +4,8 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { useAppDispatch } from '@/store/hooks' -import { createSnippet, updateSnippet, deleteSnippet } from '@/store/slices/snippetsSlice' -import { FloppyDisk, Plus, Pencil, Trash } from '@phosphor-icons/react' +import { createSnippet } from '@/store/slices/snippetsSlice' +import { FloppyDisk, Plus } from '@phosphor-icons/react' import { toast } from 'sonner' export function PersistenceExample() { diff --git a/src/components/error/AIErrorHelper.tsx b/src/components/error/AIErrorHelper.tsx index 4920729..130ce49 100644 --- a/src/components/error/AIErrorHelper.tsx +++ b/src/components/error/AIErrorHelper.tsx @@ -39,6 +39,7 @@ export function AIErrorHelper({ error, context, className }: AIErrorHelperProps) const result = await analyzeErrorWithAI(errorMessage, errorStack, context) setAnalysis(result) } catch (err) { + console.error('AI analysis failed', err) setAnalysisError('Unable to analyze error. The AI service may be temporarily unavailable.') } finally { setIsAnalyzing(false) @@ -105,4 +106,3 @@ export function AIErrorHelper({ error, context, className }: AIErrorHelperProps) ) } - diff --git a/src/components/features/namespace-manager/NamespaceSelector.tsx b/src/components/features/namespace-manager/NamespaceSelector.tsx index 629ba05..4425442 100644 --- a/src/components/features/namespace-manager/NamespaceSelector.tsx +++ b/src/components/features/namespace-manager/NamespaceSelector.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback } from 'react' import { Select, SelectContent, @@ -30,11 +30,7 @@ export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: Na const [namespaceToDelete, setNamespaceToDelete] = useState(null) const [loading, setLoading] = useState(false) - useEffect(() => { - loadNamespaces() - }, []) - - const loadNamespaces = async () => { + const loadNamespaces = useCallback(async () => { try { const loadedNamespaces = await getAllNamespaces() setNamespaces(loadedNamespaces) @@ -49,7 +45,11 @@ export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: Na console.error('Failed to load namespaces:', error) toast.error('Failed to load namespaces') } - } + }, [onNamespaceChange, selectedNamespaceId]) + + useEffect(() => { + loadNamespaces() + }, [loadNamespaces]) const handleCreateNamespace = async () => { if (!newNamespaceName.trim()) { diff --git a/src/components/features/python-runner/PythonOutput.tsx b/src/components/features/python-runner/PythonOutput.tsx index cc52802..11fa02c 100644 --- a/src/components/features/python-runner/PythonOutput.tsx +++ b/src/components/features/python-runner/PythonOutput.tsx @@ -59,8 +59,8 @@ export function PythonOutput({ code }: PythonOutputProps) { }, [initializePyodide]) useEffect(() => { - const codeToCheck = code.toLowerCase() - setHasInput(codeToCheck.includes('input(')) + const hasInputCall = /\binput\s*\(/i.test(code) + setHasInput(hasInputCall) }, [code]) const statusTone = initError diff --git a/src/components/features/snippet-display/SnippetCard.tsx b/src/components/features/snippet-display/SnippetCard.tsx index a065df4..de0ce38 100644 --- a/src/components/features/snippet-display/SnippetCard.tsx +++ b/src/components/features/snippet-display/SnippetCard.tsx @@ -43,34 +43,24 @@ export function SnippetCard({ try { const loadedNamespaces = await getAllNamespaces() setNamespaces(loadedNamespaces) - } catch (error) { - console.error('Failed to load namespaces:', error) + } catch { + console.error('Failed to load namespaces') } } const snippetData = useMemo(() => { - try { - const code = snippet?.code || '' - const description = snippet?.description || '' - const maxLength = appConfig.codePreviewMaxLength - const isTruncated = code.length > maxLength - const displayCode = isTruncated ? code.slice(0, maxLength) + '...' : code + const code = snippet?.code || '' + const description = snippet?.description || '' + const maxLength = appConfig.codePreviewMaxLength + const isTruncated = code.length > maxLength + const displayCode = isTruncated ? code.slice(0, maxLength) + '...' : code - return { - description, - displayCode, - fullCode: code, - isTruncated, - hasPreview: snippet?.hasPreview || false - } - } catch (error) { - return { - description: '', - displayCode: '', - fullCode: '', - isTruncated: false, - hasPreview: false - } + return { + description, + displayCode, + fullCode: code, + isTruncated, + hasPreview: snippet?.hasPreview || false } }, [snippet]) @@ -92,6 +82,7 @@ export function SnippetCard({ } const handleView = (e: React.MouseEvent) => { + e.stopPropagation() if (selectionMode) { handleToggleSelect() } else { @@ -135,7 +126,6 @@ export function SnippetCard({ ) } - const currentNamespace = namespaces.find(n => n.id === snippet.namespaceId) const availableNamespaces = namespaces.filter(n => n.id !== snippet.namespaceId) return ( diff --git a/src/components/features/snippet-editor/ReactPreview.tsx b/src/components/features/snippet-editor/ReactPreview.tsx index 897ac54..50b4155 100644 --- a/src/components/features/snippet-editor/ReactPreview.tsx +++ b/src/components/features/snippet-editor/ReactPreview.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useMemo } from 'react' +import { useMemo } from 'react' import * as React from 'react' import { Alert, AlertDescription } from '@/components/ui/alert' import { AIErrorHelper } from '@/components/error/AIErrorHelper' @@ -15,24 +15,17 @@ interface ReactPreviewProps { } export function ReactPreview({ code, language, functionName, inputParameters }: ReactPreviewProps) { - const [error, setError] = useState(null) - const [Component, setComponent] = useState(null) - - useEffect(() => { - setError(null) - setComponent(null) - + const { Component, error } = useMemo(() => { const isReactCode = ['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language) - if (!isReactCode) { - return + return { Component: null, error: null } } try { const transformedComponent = transformReactCode(code, functionName) - setComponent(() => transformedComponent) + return { Component: transformedComponent, error: null } } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to render preview') + return { Component: null, error: err instanceof Error ? err.message : 'Failed to render preview' } } }, [code, language, functionName]) diff --git a/src/components/layout/BackendIndicator.tsx b/src/components/layout/BackendIndicator.tsx index c6918d2..dff5686 100644 --- a/src/components/layout/BackendIndicator.tsx +++ b/src/components/layout/BackendIndicator.tsx @@ -1,17 +1,10 @@ -import { useState, useEffect } from 'react' import { Database, CloudCheck } from '@phosphor-icons/react' import { getStorageConfig } from '@/lib/storage' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' export function BackendIndicator() { - const [backend, setBackend] = useState<'indexeddb' | 'flask'>('indexeddb') - const [isEnvConfigured, setIsEnvConfigured] = useState(false) - - useEffect(() => { - const config = getStorageConfig() - setBackend(config.backend) - setIsEnvConfigured(Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL)) - }, []) + const { backend } = getStorageConfig() + const isEnvConfigured = Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL) if (backend === 'indexeddb') { return ( diff --git a/src/components/organisms/OrganismsSection.tsx b/src/components/organisms/OrganismsSection.tsx index 29aa1d6..864c513 100644 --- a/src/components/organisms/OrganismsSection.tsx +++ b/src/components/organisms/OrganismsSection.tsx @@ -1,5 +1,3 @@ -import { ComponentShowcase } from '@/components/demo/ComponentShowcase' -import { organismsCodeSnippets } from '@/lib/component-code-snippets' import { Snippet } from '@/lib/types' import { NavigationBarsShowcase } from './showcases/NavigationBarsShowcase' import { DataTablesShowcase } from './showcases/DataTablesShowcase' diff --git a/src/components/settings/OpenAISettingsCard.tsx b/src/components/settings/OpenAISettingsCard.tsx index 9c26896..0d00bf1 100644 --- a/src/components/settings/OpenAISettingsCard.tsx +++ b/src/components/settings/OpenAISettingsCard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; @@ -8,18 +8,10 @@ import { Label } from '@/components/ui/label'; import { Eye, EyeClosed, Key } from '@phosphor-icons/react'; export function OpenAISettingsCard() { - const [apiKey, setApiKey] = useState(''); + const [apiKey, setApiKey] = useState(() => (typeof window !== 'undefined' ? localStorage.getItem('openai_api_key') || '' : '')); const [showKey, setShowKey] = useState(false); const [saved, setSaved] = useState(false); - useEffect(() => { - // Load the API key from localStorage on mount - const storedKey = localStorage.getItem('openai_api_key'); - if (storedKey) { - setApiKey(storedKey); - } - }, []); - const handleSave = () => { if (apiKey.trim()) { localStorage.setItem('openai_api_key', apiKey.trim()); diff --git a/src/components/templates/DashboardTemplate.tsx b/src/components/templates/DashboardTemplate.tsx index 42a3b37..379cb47 100644 --- a/src/components/templates/DashboardTemplate.tsx +++ b/src/components/templates/DashboardTemplate.tsx @@ -1,6 +1,5 @@ import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Bell, diff --git a/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx b/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx index 8a3d870..7084a7c 100644 --- a/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx +++ b/src/components/ui/sidebar-menu/SidebarMenuSkeleton.tsx @@ -1,6 +1,6 @@ "use client" -import { CSSProperties, ComponentProps, useMemo } from "react" +import { CSSProperties, ComponentProps } from "react" import { cn } from "@/lib/utils" import { Skeleton } from "@/components/ui/skeleton" @@ -11,10 +11,8 @@ export function SidebarMenuSkeleton({ }: ComponentProps<"div"> & { showIcon?: boolean }) { - // Random width between 50 to 90%. - const width = useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%` - }, []) + // Use a stable width so skeletons don't change across renders. + const width = "70%" return (
(undefined) + const [isMobile, setIsMobile] = useState(() => { + if (typeof window === "undefined") return undefined + return window.innerWidth < MOBILE_BREAKPOINT + }) useEffect(() => { const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) @@ -11,7 +14,6 @@ export function useIsMobile() { setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) } mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) return () => mql.removeEventListener("change", onChange) }, []) diff --git a/src/hooks/usePythonTerminal.ts b/src/hooks/usePythonTerminal.ts index e16a8b1..4fbfbd5 100644 --- a/src/hooks/usePythonTerminal.ts +++ b/src/hooks/usePythonTerminal.ts @@ -14,7 +14,6 @@ export function usePythonTerminal() { const [isInitializing, setIsInitializing] = useState(!isPyodideReady()) const [inputValue, setInputValue] = useState('') const [waitingForInput, setWaitingForInput] = useState(false) - const [inputPrompt, setInputPrompt] = useState('') const inputResolveRef = useRef<((value: string) => void) | null>(null) useEffect(() => { @@ -42,7 +41,6 @@ export function usePythonTerminal() { const handleInputPrompt = (prompt: string): Promise => { return new Promise((resolve) => { - setInputPrompt(prompt) addLine('input-prompt', prompt) setWaitingForInput(true) inputResolveRef.current = resolve diff --git a/src/lib/db.ts b/src/lib/db.ts index d693302..c360235 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -98,7 +98,7 @@ export async function createTemplate(snippet: Omit { +export async function syncTemplatesFromJSON(templates: unknown[]): Promise { // This would sync predefined templates - implement as needed console.log('Syncing templates', templates.length); } diff --git a/src/lib/monaco-config.ts b/src/lib/monaco-config.ts index d2bf314..34b0439 100644 --- a/src/lib/monaco-config.ts +++ b/src/lib/monaco-config.ts @@ -13,7 +13,7 @@ import type { Monaco } from '@monaco-editor/react' * - module: 99 = ModuleKind.ESNext * - jsx: 2 = JsxEmit.React */ -export const compilerOptions = { +export const compilerOptions: Monaco['languages']['typescript']['CompilerOptions'] = { target: 2, // ScriptTarget.Latest allowNonTsExtensions: true, moduleResolution: 2, // ModuleResolutionKind.NodeJs @@ -29,7 +29,7 @@ export const compilerOptions = { /** * Diagnostics options for TypeScript/JavaScript validation */ -export const diagnosticsOptions = { +export const diagnosticsOptions: Monaco['languages']['typescript']['DiagnosticsOptions'] = { noSemanticValidation: false, noSyntaxValidation: false, } @@ -160,10 +160,10 @@ export const shadcnTypes = ` */ export function configureMonacoTypeScript(monaco: Monaco) { // Set compiler options for TypeScript - monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions as any) + monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions) // Set compiler options for JavaScript - monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions as any) + monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions) // Set diagnostics options for TypeScript monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(diagnosticsOptions) diff --git a/src/lib/parse-parameters.ts b/src/lib/parse-parameters.ts index bfba0ac..12761b5 100644 --- a/src/lib/parse-parameters.ts +++ b/src/lib/parse-parameters.ts @@ -1,11 +1,11 @@ import { InputParameter } from '@/lib/types' -export function parseInputParameters(inputParameters?: InputParameter[]): Record { +export function parseInputParameters(inputParameters?: InputParameter[]): Record { if (!inputParameters || inputParameters.length === 0) { return {} } - const parsedProps: Record = {} + const parsedProps: Record = {} inputParameters.forEach((param) => { try { diff --git a/src/lib/pyodide-runner.ts b/src/lib/pyodide-runner.ts index 7f4322a..c140d78 100644 --- a/src/lib/pyodide-runner.ts +++ b/src/lib/pyodide-runner.ts @@ -46,7 +46,9 @@ sys.stderr = StringIO() `) try { - const result = pyodide.runPython(code) + const result = await pyodide.runPythonAsync(code) + + // Flush and collect stdout/stderr after async execution const stdout = pyodide.runPython('sys.stdout.getvalue()') const stderr = pyodide.runPython('sys.stderr.getvalue()') diff --git a/src/lib/storage.ts b/src/lib/storage.ts index dbce4b5..fb6d855 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -89,7 +89,7 @@ export class FlaskStorageAdapter { signal: AbortSignal.timeout(5000) }) return response.ok - } catch (error) { + } catch { return false } } @@ -102,8 +102,8 @@ export class FlaskStorageAdapter { if (!response.ok) { throw new Error(`Failed to fetch snippets: ${response.statusText}`) } - const data = await response.json() - return data.map((s: any) => ({ + const data: Snippet[] = await response.json() + return data.map((s) => ({ ...s, createdAt: typeof s.createdAt === 'string' ? new Date(s.createdAt).getTime() : s.createdAt, updatedAt: typeof s.updatedAt === 'string' ? new Date(s.updatedAt).getTime() : s.updatedAt diff --git a/src/store/hooks/usePersistenceConfig.ts b/src/store/hooks/usePersistenceConfig.ts index 91eefc5..fc2993b 100644 --- a/src/store/hooks/usePersistenceConfig.ts +++ b/src/store/hooks/usePersistenceConfig.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState } from 'react' import { type PersistenceConfig, getPersistenceConfig, diff --git a/src/store/slices/snippetsSlice.ts b/src/store/slices/snippetsSlice.ts index 76aaca1..216bb7f 100644 --- a/src/store/slices/snippetsSlice.ts +++ b/src/store/slices/snippetsSlice.ts @@ -154,11 +154,11 @@ const snippetsSlice = createSlice({ state.items = state.items.filter(s => s.id !== action.payload) }) .addCase(moveSnippet.fulfilled, (state, action) => { - const { snippetId, targetNamespaceId } = action.payload + const { snippetId } = action.payload state.items = state.items.filter(s => s.id !== snippetId) }) .addCase(bulkMoveSnippets.fulfilled, (state, action) => { - const { snippetIds, targetNamespaceId } = action.payload + const { snippetIds } = action.payload state.items = state.items.filter(s => !snippetIds.includes(s.id)) state.selectedIds = [] state.selectionMode = false