diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cc90e1f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.next +.git +.env* +npm-debug.log* +README.md +.gitignore diff --git a/.eslintrc.json b/.eslintrc.json index bffb357..38bb94e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,7 @@ { - "extends": "next/core-web-vitals" + "extends": "next/core-web-vitals", + "rules": { + "react/no-unescaped-entities": "off", + "react-hooks/exhaustive-deps": "off" + } } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..662fa3b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# Use Node.js LTS version +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy project files +COPY . . + +# Build the application +RUN npm run build + +# Expose port +EXPOSE 3000 + +# Start the application +CMD ["npm", "start"] diff --git a/README.md b/README.md index 14172d4..0ac3e5a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,183 @@ -### Code Visualizer: +# Code Timeline Preview -**Transforming Code into Engaging Visual Timelines** +A modern, interactive code visualization tool that helps developers understand and analyze code structure and complexity over time. -Code Visualizer is a web application that generates colorful visualizations from code inputs, highlighting different segments for improved readability and engagement. +## Live Demo + +Try out the live demo at: [codevi.netlify.app](https://codevi.netlify.app/) Thumbnail -Find Code Visualizer at the following URL: +## Features + +### Core Visualization +- **Interactive Timeline**: Visualize code structure with an intuitive timeline interface +- **Syntax Highlighting**: Clear color-coding for different code elements: + - Keywords (Deep Red) + - Classes (Rich Green) + - Functions (Deep Purple) + - Variables (Rich Blue) + - Operators (Warm Orange) + - Strings (Ocean Blue) + - Numbers (Ruby Red) + - Comments (Neutral Gray) + - Decorators (Bright Orange) + +### Code Analysis +- **Complexity Visualization**: Toggle between syntax highlighting and complexity view +- **Complexity Metrics**: + - Low Complexity (Green): Simple, straightforward code + - Medium Complexity (Yellow): Moderate control flow and nesting + - High Complexity (Red): Complex logic and deep nesting +- **Code Structure Analysis**: Automatic analysis of: + - Control flow statements + - Nesting levels + - Logical operators + - Dependencies + +### Navigation & Controls +- **Search Functionality**: Filter code segments based on text search +- **Filter Dialog**: Show/hide specific code types: + - Keywords + - Classes + - Functions + - Variables + - Operators + - Strings + - Numbers + - Comments +- **Zoom Controls**: Adjust timeline view scale +- **Mini-map Navigation**: Quick navigation through large codebases with visual preview +- **Scrolling**: Smooth scrolling with proper viewport management + +### UI Features +- **Dark/Light Mode**: Optimized color schemes for both themes +- **Responsive Layout**: Adapts to different screen sizes +- **Interactive Tooltips**: Detailed information on hover +- **Modern Design**: + - Clean, minimalist interface + - Subtle shadows and transitions + - Professional color palette + - High contrast for readability + +### Editor Integration +- **Code Input**: Built-in code editor with syntax highlighting +- **Real-time Updates**: Immediate visualization of code changes +- **Error Handling**: Validation and error reporting for code input + +## Getting Started + +1. Clone the repository: +```bash +git clone https://github.com/yourusername/code_timeline_preview.git +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Run the development server: +```bash +npm run dev +``` + +4. Open [http://localhost:3000](http://localhost:3000) in your browser + +## Usage + +1. **Input Code**: Paste your code in the left editor panel +2. **Explore Visualization**: + - Use the timeline view on the right + - Toggle between syntax and complexity views + - Use search and filters to focus on specific code elements +3. **Navigate**: + - Use the mini-map for quick navigation + - Zoom in/out to adjust detail level + - Scroll through longer code files + +## 🐳 Docker Setup + +You can run the application using Docker. Here's how: + +### Prerequisites + +- Docker installed on your machine +- Docker Compose (optional, for development) + +### Building the Docker Image + +```bash +# Build the image +docker build -t code-timeline . --load +``` + +### Running the Container + +```bash +# Run the container +docker run -p 3000:3000 code-timeline +``` + +The application will be available at `http://localhost:3000` + +### Development with Docker + +For development, you can use volume mounts to reflect changes immediately: + +```bash +docker run -p 3000:3000 -v $(pwd):/app code-timeline npm run dev +``` + +### Docker Commands Reference + +```bash +# Stop the container +docker stop + +# Remove the container +docker rm + +# List running containers +docker ps + +# View container logs +docker logs + +# Rebuild the image after changes +docker build -t code-timeline . --load +``` + +### Troubleshooting + +If you encounter any issues: + +1. Make sure ports are not in use: + ```bash + lsof -i :3000 + ``` + +2. Clean up Docker resources: + ```bash + docker system prune + ``` + +3. Check container logs: + ```bash + docker logs + ``` + +## Technologies + +- **Frontend**: Next.js, React +- **Styling**: Tailwind CSS +- **Code Editor**: Ace Editor +- **Icons**: Lucide React + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License -[codevi.netlify.com](https://codevi.netlify.app/)
\ No newline at end of file +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b5daed2..a76585e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,15 @@ "@nextui-org/button": "^2.0.38", "@nextui-org/system": "^2.2.6", "@nextui-org/theme": "^2.2.11", + "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-toggle": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", - "framer-motion": "^11.11.2", + "framer-motion": "^11.15.0", "html2canvas": "^1.4.1", + "jspdf": "^2.5.2", "lucide-react": "^0.451.0", "next": "14.2.14", "react": "^18", @@ -44,6 +46,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -703,6 +717,289 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", + "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", + "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-icons": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", @@ -711,6 +1008,143 @@ "react": "^16.x || ^17.x || ^18.x" } }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-primitive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", @@ -833,6 +1267,24 @@ } } }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", @@ -1203,6 +1655,13 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", @@ -1507,6 +1966,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", @@ -1672,6 +2143,18 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1750,6 +2233,18 @@ "node": ">=8" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1816,6 +2311,33 @@ } ] }, + "node_modules/canvg": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", + "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/canvg/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1954,6 +2476,18 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/core-js": { + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2140,6 +2674,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2167,6 +2707,13 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", + "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2858,6 +3405,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2950,16 +3503,19 @@ } }, "node_modules/framer-motion": { - "version": "11.11.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.2.tgz", - "integrity": "sha512-Sj2xao1t5MskcCVJawfAhbTOP4IGEjFWE7ebiyd4Lo5yzKP3pkSMyaCtzM6D0uAT7ozNTgZcUorEsCWAIxHfZA==", + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz", + "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==", + "license": "MIT", "dependencies": { + "motion-dom": "^11.14.3", + "motion-utils": "^11.14.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/is-prop-valid": { @@ -3046,6 +3602,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -3844,6 +4409,24 @@ "json5": "lib/cli.js" } }, + "node_modules/jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4036,6 +4619,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz", + "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==", + "license": "MIT" + }, + "node_modules/motion-utils": { + "version": "11.14.3", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz", + "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4407,6 +5002,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -4642,6 +5244,16 @@ } ] }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -4686,6 +5298,75 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-remove-scroll": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", + "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4726,6 +5407,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", @@ -4787,6 +5474,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4997,6 +5694,16 @@ "node": ">=0.10.0" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -5282,6 +5989,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwind-merge": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.3.tgz", @@ -5581,6 +6298,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index bb3aa79..dd412c8 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,15 @@ "@nextui-org/button": "^2.0.38", "@nextui-org/system": "^2.2.6", "@nextui-org/theme": "^2.2.11", + "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-toggle": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", - "framer-motion": "^11.11.2", + "framer-motion": "^11.15.0", "html2canvas": "^1.4.1", + "jspdf": "^2.5.2", "lucide-react": "^0.451.0", "next": "14.2.14", "react": "^18", diff --git a/src/app/globals.css b/src/app/globals.css index 1dcb0fc..08f3d97 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -10,6 +10,19 @@ body { .text-balance { text-wrap: balance; } + .animate-gradient-x { + background-size: 200% 200%; + animation: gradient-x 15s ease infinite; + } + + .animate-typing::after { + content: '|'; + animation: typing 1.2s infinite; + } + + .animate-blink { + animation: blink 1s step-end infinite; + } } @layer base { @@ -71,8 +84,141 @@ body { @layer base { * { @apply border-border; + transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; } body { @apply bg-background text-foreground; } } + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideIn { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes gradient-x { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +@keyframes typing { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} + +@keyframes blink { + from, to { + opacity: 1; + } + 50% { + opacity: 0; + } +} + +.timeline-segment { + transition: all 0.2s ease; +} + +.timeline-segment:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Custom scrollbar for timeline */ +.timeline-container::-webkit-scrollbar { + width: 8px; +} + +.timeline-container::-webkit-scrollbar-track { + background: transparent; +} + +.timeline-container::-webkit-scrollbar-thumb { + background-color: rgba(156, 163, 175, 0.5); + border-radius: 4px; +} + +.timeline-container::-webkit-scrollbar-thumb:hover { + background-color: rgba(156, 163, 175, 0.7); +} + +/* Dark mode scrollbar */ +.dark .timeline-container::-webkit-scrollbar-thumb { + background-color: rgba(75, 85, 99, 0.5); +} + +.dark .timeline-container::-webkit-scrollbar-thumb:hover { + background-color: rgba(75, 85, 99, 0.7); +} + +/* Loading dots animation */ +.loading-dots { + display: flex; + gap: 4px; +} + +.loading-dots .dot { + width: 4px; + height: 4px; + border-radius: 50%; + animation: bounce 1.4s infinite ease-in-out; +} + +.loading-dots .dot:nth-child(1) { + animation-delay: -0.32s; +} + +.loading-dots .dot:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes bounce { + 0%, 80%, 100% { + transform: scale(0); + } + 40% { + transform: scale(1); + } +} + +/* Text fade animation */ +.text-fade-in { + animation: fadeIn 0.5s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(2px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/app/page.js b/src/app/page.js index a0e2e01..a6553f4 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,9 +1,11 @@ "use client"; -import React, { useState, useRef } from "react"; -import { Download ,Github} from "lucide-react"; +import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"; +import { Download, Github, BarChart2, MinusCircle, PlusCircle, Activity, ChevronDown, Sun, Moon, History, Trash2, X, Share2, AlertTriangle, Filter } from "lucide-react"; import html2canvas from "html2canvas"; import AceEditor from "react-ace"; +import { validateCodeInput } from "@/lib/utils"; +import jsPDF from 'jspdf'; import "ace-builds/src-noconflict/mode-dart"; import "ace-builds/src-noconflict/theme-dracula"; @@ -11,303 +13,1255 @@ import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/ext-language_tools"; const CodeTimeline = () => { - const [codeInput, setCodeInput] = useState(""); + // Load initial state from localStorage + const loadFromStorage = (key, defaultValue) => { + if (typeof window === 'undefined') return defaultValue; + const stored = localStorage.getItem(key); + return stored ? JSON.parse(stored) : defaultValue; + }; + + const [mounted, setMounted] = useState(false); + const [darkMode, setDarkMode] = useState(false); + const [codeInput, setCodeInput] = useState(''); const [timelineData, setTimelineData] = useState([]); - const [darkMode, setDarkMode] = useState(true); - const timelineRef = useRef(null); - const [elementTypes, setElementTypes] = useState({ - keyword: "#FF6B6B", // Soft Red - class: "#4ECDC4", // Teal - function: "#45B7D1", // Sky Blue - variable: "#96CEB4", // Sage Green - operator: darkMode ? "#FFD93D" : "#FFD700", // Soft yellow - string: "#FF8C42", // Soft Orange - number: "#6A0572", // Deep Purple - boolean: "#FF4081", // Pink - comment: "#78909C", // Blue Grey - import: "#26A69A", // Green Teal - decorator: "#BA68C8", // Light Purple - punctuation: "#B0BEC5", // Light Blue Grey - bracket: "#00BCD4", // Cyan - property: "#8BC34A", // Light Green - space: "transparent", - default: darkMode ? "#E0E0E0" : "#424242", // Light Grey / Dark Grey + const [filteredTimelineData, setFilteredTimelineData] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [error, setError] = useState(null); + const [showComplexity, setShowComplexity] = useState(false); + const [zoom, setZoom] = useState(1); + const [visibleRange, setVisibleRange] = useState({ start: 0, end: 0 }); + const [filters, setFilters] = useState({ + keyword: true, + class: true, + function: true, + variable: true, + operator: true, + string: true, + number: true, + boolean: true, + comment: true, + decorator: true, + bracket: true, + punctuation: true, + property: true }); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [selectedSegment, setSelectedSegment] = useState(null); + const [isDiffModalOpen, setIsDiffModalOpen] = useState(false); + const [history, setHistory] = useState([]); + const [showHistory, setShowHistory] = useState(false); + const [loadingText, setLoadingText] = useState('analyzing'); + + // Initialize state from localStorage after mount + useEffect(() => { + setMounted(true); + try { + const savedDarkMode = localStorage.getItem('darkMode'); + const savedTimelineData = localStorage.getItem('timelineData'); + const savedHistory = localStorage.getItem('codeHistory'); + + if (savedDarkMode !== null) { + setDarkMode(JSON.parse(savedDarkMode)); + } + if (savedTimelineData) { + setTimelineData(JSON.parse(savedTimelineData)); + } + if (savedHistory) { + setHistory(JSON.parse(savedHistory)); + } + } catch (error) { + console.error('Error loading saved data:', error); + } + }, []); + + // Save state to localStorage + useEffect(() => { + if (!mounted) return; + + localStorage.setItem('darkMode', JSON.stringify(darkMode)); + localStorage.setItem('timelineData', JSON.stringify(timelineData)); + localStorage.setItem('codeHistory', JSON.stringify(history)); + }, [darkMode, timelineData, history, mounted]); + + const timelineRef = useRef(null); + const timelineContainerRef = useRef(null); + const tooltipRef = useRef(null); + + const theme = { + background: darkMode ? '#1a1a1a' : '#ffffff', + surface: darkMode ? '#2d2d2d' : '#f8f9fa', + text: { + primary: darkMode ? '#ffffff' : '#000000', + secondary: darkMode ? '#a0aec0' : '#4a5568', + }, + border: darkMode ? '#404040' : '#e2e8f0', + accent: '#3182ce', + severity: { + low: '#48bb78', + medium: '#ecc94b', + high: '#e53e3e', + }, + complexity: { + low: '#48bb78', + medium: '#ecc94b', + high: '#e53e3e', + } + }; + + const elementTypes = { + keyword: '#C678DD', // Purple + class: '#E5C07B', // Yellow + function: '#61AFEF', // Blue + variable: '#E06C75', // Red + operator: '#56B6C2', // Cyan + string: '#98C379', // Green + number: '#D19A66', // Orange + boolean: '#C678DD', // Purple (same as keyword) + comment: '#7F848E', // Gray + import: '#C678DD', // Purple (same as keyword) + decorator: '#61AFEF', // Blue (same as function) + punctuation: '#ABB2BF', // Light gray + bracket: '#ABB2BF', // Light gray + property: '#E06C75', // Red (same as variable) + space: 'transparent', + default: '#ABB2BF' // Light gray + }; + + const getSegmentColor = (segment, analysis) => { + if (showComplexity) { + const complexity = analysis.complexity || 0; + if (complexity > 0.7) return theme.severity.high; + if (complexity > 0.4) return theme.severity.medium; + return theme.severity.low; + } + return elementTypes[segment.type] || elementTypes.default; + }; + + const getSegmentOpacity = (analysis) => { + if (showComplexity) { + const complexity = analysis.complexity || 0; + return 0.3 + (complexity * 0.7); + } + return 1; + }; + + const getSegmentHeight = (complexity) => { + const minHeight = 20; + const maxHeight = 40; + return minHeight + (complexity * (maxHeight - minHeight)); + }; + + const getSegmentWidth = (text) => { + const baseWidth = 10; // Increased from 6 to 10 for wider blocks + return Math.max(text.length * baseWidth, 15); // Minimum width of 15px + }; - const keywords = [ - "class", - "function", - "const", - "let", - "var", - "if", - "else", - "for", - "while", - "return", - "import", - "from", - "async", - "await", - "try", - "catch", - "throw", - "new", - "this", - "super", - ]; + const analyzeCodeSegment = (code) => { + if (!code) return { complexity: 0, codeSmells: [] }; + + let complexity = 0; + const codeSmells = []; + + // Complexity factors + const keywordComplexity = (code.match(/\b(if|else|for|while|switch|case|try|catch)\b/g) || []).length * 0.1; + const operatorComplexity = (code.match(/[&|=!<>+\-*/%]+/g) || []).length * 0.05; + const nestingComplexity = (code.match(/[{[(]/g) || []).length * 0.1; + const lengthComplexity = Math.min(code.length / 100, 0.5); + + complexity = keywordComplexity + operatorComplexity + nestingComplexity + lengthComplexity; + + // Code smells detection + if (code.length > 80) { + codeSmells.push({ message: 'Line is too long (> 80 characters)' }); + } + if ((code.match(/\t/g) || []).length > 0) { + codeSmells.push({ message: 'Uses tabs instead of spaces' }); + } + if (code.match(/console\.(log|debug|info)/)) { + codeSmells.push({ message: 'Contains console statement' }); + } + if (code.match(/var\s/)) { + codeSmells.push({ message: 'Uses var instead of const/let' }); + } + + return { + complexity: Math.min(complexity, 1), + codeSmells + }; + }; + + const handleDelete = useCallback(() => { + setTimelineData([]); + setCodeInput(''); + addToHistory(codeInput, 'deleted'); + }, [codeInput]); + + const parseCodeChanges = useCallback((code) => { + if (!code) return []; + + const lines = code.split('\n'); + return lines.map((line, index) => ({ + id: index + 1, + segments: tokenizeLine(line) + })); + }, []); + + const handleCodeInput = useCallback((value) => { + setCodeInput(value); + try { + const parsedData = parseCodeChanges(value); + setTimelineData(parsedData); + setFilteredTimelineData(parsedData); + } catch (err) { + console.error('Error parsing code:', err); + } + }, []); const tokenizeLine = (line) => { - const tokens = line.split(/(\s+|[{}()[\],;.])/); + if (!line.trim()) { + return [{ type: 'space', text: ' ' }]; + } - return tokens - .map((token) => { - if (!token) return null; + const segments = []; + let currentToken = ''; + let currentType = ''; + let inString = false; + let stringChar = ''; + let inComment = false; + + const processToken = () => { + if (currentToken) { + segments.push({ + type: currentType || getTokenType(currentToken), + text: currentToken + }); + currentToken = ''; + currentType = ''; + } + }; - let color = elementTypes.default; - let width = token.length * 8; + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + // Handle comments + if (char === '/' && line[i + 1] === '/') { + processToken(); + segments.push({ type: 'comment', text: line.slice(i) }); + break; + } - if (token.trim() === "") { - return { - text: token, - color: elementTypes.space, - width: token.length * 8, - }; + // Handle strings + if ((char === '"' || char === "'" || char === '`') && !inComment) { + if (!inString) { + processToken(); + inString = true; + stringChar = char; + currentToken = char; + } else if (char === stringChar && line[i - 1] !== '\\') { + currentToken += char; + segments.push({ type: 'string', text: currentToken }); + currentToken = ''; + inString = false; + continue; } + } - if (keywords.includes(token)) { - color = elementTypes.keyword; - } else if (token.match(/^[A-Z][a-zA-Z0-9]*$/)) { - color = elementTypes.class; - } else if (token.match(/^[a-z][a-zA-Z0-9]*(?=\()/)) { - color = elementTypes.function; - } else if (token.match(/^[a-z][a-zA-Z0-9]*$/)) { - color = elementTypes.variable; - } else if (token.match(/[+\-*/%=<>!&|^~]/)) { - color = elementTypes.operator; - } else if (token.match(/^(['"]).*\1$/)) { - color = elementTypes.string; - } else if (token.match(/^\d+$/)) { - color = elementTypes.number; - } else if (token === "true" || token === "false") { - color = elementTypes.boolean; - } else if (token.startsWith("//")) { - color = elementTypes.comment; - } else if (token === "import" || token === "from") { - color = elementTypes.import; - } else if (token.startsWith("@")) { - color = elementTypes.decorator; - } else if (token.match(/[{}()[\]]/)) { - color = elementTypes.bracket; - } else if (token.match(/[.,;]/)) { - color = elementTypes.punctuation; - } + if (inString) { + currentToken += char; + continue; + } + + // Handle spaces + if (/\s/.test(char)) { + processToken(); + segments.push({ type: 'space', text: char }); + continue; + } + + // Handle brackets + if ('(){}[]'.includes(char)) { + processToken(); + segments.push({ type: 'bracket', text: char }); + continue; + } + + // Handle operators + if ('+-*/%=<>!&|^~'.includes(char)) { + processToken(); + segments.push({ type: 'operator', text: char }); + continue; + } - return { text: token, color, width }; - }) - .filter(Boolean); + // Handle punctuation + if ('.,;:'.includes(char)) { + processToken(); + segments.push({ type: 'punctuation', text: char }); + continue; + } + + currentToken += char; + } + + processToken(); + return segments; }; - const generateTimelineFromCode = (code) => { - const lines = code.split("\n"); - return lines - .map((line, index) => ({ - id: index + 1, - segments: tokenizeLine(line), - })) - .filter((line) => line.segments.length > 0); + const getTokenType = (token) => { + // Keywords + if (/^(function|return|const|let|var|if|else|for|while|do|switch|case|break|continue|class|extends|new|this|import|export|from|default|null|undefined|true|false)$/.test(token)) { + return 'keyword'; + } + // Classes (capitalized words) + if (/^[A-Z][a-zA-Z0-9]*$/.test(token)) { + return 'class'; + } + // Functions (words followed by parentheses) + if (/^[a-zA-Z_$][a-zA-Z0-9_$]*\(.*\)$/.test(token)) { + return 'function'; + } + // Numbers + if (/^[0-9]+(\.[0-9]+)?$/.test(token)) { + return 'number'; + } + // Booleans + if (/^(true|false)$/.test(token)) { + return 'boolean'; + } + // Strings (quoted text) + if (/^["'`].*["'`]$/.test(token)) { + return 'string'; + } + // Comments + if (/^\/\/.*$/.test(token) || /^\/\*[\s\S]*\*\/$/.test(token)) { + return 'comment'; + } + // Operators + if (/^[+\-*/%=<>!&|^~]+$/.test(token)) { + return 'operator'; + } + // Decorators + if (/^@[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(token)) { + return 'decorator'; + } + // Properties + if (/^\.[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(token)) { + return 'property'; + } + // Punctuation + if (/^[.,;:]$/.test(token)) { + return 'punctuation'; + } + // Brackets + if (/^[(){}\[\]]$/.test(token)) { + return 'bracket'; + } + // Variables (identifiers) + if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(token)) { + return 'variable'; + } + return 'default'; }; - const handleInputChange = (newCode) => { - setCodeInput(newCode); - setTimelineData(generateTimelineFromCode(newCode)); + const handleSearch = (term) => { + setSearchTerm(term); }; - const toggleDarkMode = () => { - setDarkMode(!darkMode); - localStorage.setItem("darkMode", !darkMode); + const handleZoomIn = () => { + setZoom(prev => Math.min(prev + 0.2, 2)); }; - const exportImage = async () => { - if (timelineRef.current) { - const clone = timelineRef.current.cloneNode(true); + const handleZoomOut = () => { + setZoom(prev => Math.max(prev - 0.2, 0.5)); + }; + + const handleFilterChange = (newFilters) => { + setFilters(newFilters); + }; + + // Filter timeline data based on search term and filters + const filteredTimelineDataMemo = useMemo(() => { + if (!timelineData || !Array.isArray(timelineData)) return []; + + return timelineData + .filter(row => row && Array.isArray(row.segments)) + .map(row => ({ + ...row, + segments: row.segments.filter(segment => { + if (!segment || !segment.type) return false; + const type = segment.type; + return filters[type] && + (!searchTerm || segment.text.toLowerCase().includes(searchTerm.toLowerCase())); + }) + })) + .filter(row => row.segments.length > 0); + }, [timelineData, searchTerm, filters]); + + const handleDownload = async () => { + try { + // Get the timeline element + const timelineElement = timelineRef.current; + if (!timelineElement) return; + + // Use html2canvas to create an image + const canvas = await html2canvas(timelineElement); + + // Convert canvas to blob + canvas.toBlob((blob) => { + if (!blob) return; + + // Create download link + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'code-timeline.png'; + + // Trigger download + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Cleanup + URL.revokeObjectURL(url); + }, 'image/png'); + } catch (error) { + console.error('Error downloading timeline:', error); + } + }; + + const handleFileUpload = useCallback((event) => { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const content = e.target.result; + setCodeInput(content); + const parsedData = parseCodeChanges(content); + setTimelineData(parsedData); + setFilteredTimelineData(parsedData); + // Reset scroll position + if (timelineRef.current) { + timelineRef.current.scrollTop = 0; + } + } catch (err) { + setError(`Error reading file: ${err.message}`); + } + }; + reader.onerror = () => { + setError('Error reading file'); + }; + reader.readAsText(file); + }, []); + + const handleScroll = useCallback(() => { + if (!timelineRef.current || !timelineData) return; + + const container = timelineRef.current; + const { scrollTop, clientHeight, scrollHeight } = container; + + // Calculate visible lines based on scroll position and container height + const totalLines = timelineData.length; + const lineHeight = scrollHeight / totalLines; + + const start = Math.floor(scrollTop / lineHeight); + const end = Math.ceil((scrollTop + clientHeight) / lineHeight); + + setVisibleRange({ start, end }); + }, [timelineData]); + + const handleTooltipPosition = (e, tooltip) => { + const rect = tooltip.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const spaceAbove = rect.top; + const spaceBelow = viewportHeight - rect.bottom; + + setTooltipPosition({ top: spaceBelow < 100 && spaceAbove > spaceBelow }); + }; - document.body.appendChild(clone); + useEffect(() => { + const timeline = timelineRef.current; + if (timeline) { + timeline.addEventListener('wheel', (e) => { + e.preventDefault(); + timeline.scrollTop += e.deltaY; + }, { passive: false }); + } + }, []); - const { scrollWidth, scrollHeight } = clone; + useEffect(() => { + const container = timelineContainerRef.current; + if (container) { + container.addEventListener('scroll', handleScroll); + // Initial calculation + handleScroll(); + return () => container.removeEventListener('scroll', handleScroll); + } + }, [timelineData?.length]); - const canvas = await html2canvas(clone, { - width: 922, - height: scrollHeight, - backgroundColor: darkMode ? "#2D2D2D" : "#FFFFFF", - scale: window.devicePixelRatio, - }); + useEffect(() => { + handleScroll(); + }, [zoom]); - document.body.removeChild(clone); + // History management + const addToHistory = (code, type = 'deleted') => { + const newHistoryEntry = { + id: Date.now(), + timestamp: new Date().toISOString(), + code, + type, + timelineData: timelineData + }; + + setHistory(prevHistory => { + const updatedHistory = [newHistoryEntry, ...prevHistory].slice(0, 50); + localStorage.setItem('codeHistory', JSON.stringify(updatedHistory)); + return updatedHistory; + }); + }; - const image = canvas - .toDataURL("image/png") - .replace("image/png", "image/octet-stream"); - const link = document.createElement("a"); - link.download = "code-timeline.png"; - link.href = image; - link.click(); + const restoreFromHistory = (entry) => { + if (window.confirm('This will replace your current code. Continue?')) { + setCodeInput(entry.code); + setTimelineData(entry.timelineData); + setShowHistory(false); } }; - // useEffect(() => { - // const darkModeSetting = localStorage.getItem("darkMode"); - // const isDarkMode = darkModeSetting === "true" || darkModeSetting === null; - // setDarkMode(isDarkMode); + const clearHistory = () => { + if (window.confirm('Are you sure you want to clear all history?')) { + setHistory([]); + localStorage.removeItem('codeHistory'); + } + }; - // setElementTypes((prev) => ({ - // ...prev, - // operator: !isDarkMode ? "#FFD93D" : "#FFD700", - // default: !isDarkMode ? "#E0E0E0" : "#424242", - // })); - // setTimelineData(generateTimelineFromCode(codeInput)); - // }, [codeInput, darkMode]); + const handleEditorScroll = (editor) => { + const firstVisibleRow = editor.getFirstVisibleRow(); + const lastVisibleRow = editor.getLastVisibleRow(); + const middleRow = Math.floor((firstVisibleRow + lastVisibleRow) / 2); + + if (timelineRef.current) { + const lineHeight = 24; // Approximate height of each line in the visualizer + const scrollPosition = middleRow * lineHeight; + timelineRef.current.scrollTop = scrollPosition; + } + }; - return ( -
-
-

{ + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} > - Code Timeline Visualizer -

-
- {/*
- - {darkMode ? ( - - ) : ( - - )} - -
*/} - - - - - - +

+ Filter Code Elements +

+ +
+ {Object.entries(filters).map(([key, value]) => ( + + ))} +
+ +
+ +
+ ); + }; + + const DiffModal = ({ isOpen, onClose, segment, darkMode }) => { + if (!isOpen || !segment) return null; -

- See your code come to life! -

- -
-
- +
e.stopPropagation()} + style={{ + animation: 'slideIn 0.3s ease-out' + }} + > +
+ {/* Header */} +
+

+ Code Segment Analysis +

+ +
+ + {/* Content */} +
+ {/* Left Column - Context */} +
+
+

+ Code Context +

+
+
+                      
+                        {segment.context}
+                      
+                    
+
+
+
+ + {/* Right Column - Details & Analysis */} +
+ {/* Segment Details */} +
+

+ Segment Details +

+
+
+
+

+ Type + + {segment.type.charAt(0).toUpperCase() + segment.type.slice(1)} + +

+
+
+
+
+

+ Position + + Line {segment.line}, Pos {segment.position} + +

+
+
+
+
+ + {/* Complexity Analysis */} +
+

+ Complexity Analysis +

+
+
+
+
+ + Complexity Score + + 0.7 ? "text-red-500" : + segment.complexity > 0.4 ? "text-yellow-500" : + "text-green-500" + }`}> + {(segment.complexity * 100).toFixed(0)}% + +
+
+
0.7 ? "bg-red-500" : + segment.complexity > 0.4 ? "bg-yellow-500" : + "bg-green-500" + }`} + style={{ + width: `${segment.complexity * 100}%`, + transition: 'width 0.5s ease-out' + }} + /> +
+
+
+
+
+ + {/* Code Smells */} + {segment.codeSmells?.length > 0 && ( +
+

+ Code Smells +

+
+
    + {segment.codeSmells.map((smell, index) => ( +
  • + + {smell.message} +
  • + ))} +
+
+
+ )} +
+
+
+
+ ); + }; -
-
-
- {timelineData.map((row) => ( -
- - {row.id} + useEffect(() => { + const texts = [ + 'parsing code', + 'finding patterns', + 'analyzing complexity', + 'detecting smells', + 'optimizing view', + 'brewing coffee ☕', + 'reading minds 🤔', + 'doing magic ✨' + ]; + let index = 0; + + const interval = setInterval(() => { + index = (index + 1) % texts.length; + setLoadingText(texts[index]); + }, 2000); + + return () => clearInterval(interval); + }, []); + + return ( +
+ {mounted ? ( + <> +
+
+

+ Code Timeline +

+
+ + Visualize your code's structure + +
+
+
+
+
+
+ + {loadingText} -
- {row.segments.map((segment, segIndex) => ( -
- ))} +
+
+
+ +
+
+
+ setSearchTerm(e.target.value)} + className={`w-64 px-4 py-2 rounded border ${ + darkMode + ? "bg-gray-800 border-gray-700 text-gray-200" + : "bg-white border-gray-300" + }`} + /> +
+ + + +
+ +
+ + + + +
+
+
+ + {error && ( +
+ + No code changes detected. Please input some code to analyze. +
+ )} + +
+
+
+ +
+
+ +
+
+
+
+ {filteredTimelineDataMemo.map((row) => { + const analysis = analyzeCodeSegment(row.segments.map(s => s.text).join('')); + return ( +
+ + {row.id} + +
+ {row.segments.map((segment, segIndex) => ( +
+
{ + setSelectedSegment({ + ...segment, + complexity: analysis.complexity, + codeSmells: analysis.codeSmells, + line: row.id, + position: segIndex + 1, + context: timelineData[row.id - 2]?.segments.map(s => s.text).join('') + '\n' + + timelineData[row.id - 1]?.segments.map(s => s.text).join('') + '\n' + + timelineData[row.id]?.segments.map(s => s.text).join('') + '\n' + + timelineData[row.id + 1]?.segments.map(s => s.text).join('') + '\n' + + timelineData[row.id + 2]?.segments.map(s => s.text).join('') + }); + setIsDiffModalOpen(true); + }} + /> +
+ ))} +
+
+ ); + })}
- ))} +
+ +
+
+ {showComplexity ? ( +
+
+
+ + Low Severity + +
+
+
+ + Medium Severity + +
+
+
+ + High Severity + +
+
+ ) : ( + Object.entries(elementTypes).map(([key, color]) => + key !== "space" && + key !== "default" && ( +
+
+ + {key.charAt(0).toUpperCase() + key.slice(1)} + +
+ ) + ) + )} +
+
-
-
- {Object.entries(elementTypes).map( - ([key, color]) => - key !== "space" && - key !== "default" && ( -
+ setIsFilterOpen(false)} + onApply={handleFilterChange} + filters={filters} + darkMode={darkMode} + /> + setIsDiffModalOpen(false)} + segment={selectedSegment} + darkMode={darkMode} + /> + + {/* History Modal */} + {showHistory && ( +
setShowHistory(false)} + > +
e.stopPropagation()} + > +
+

+ Code History +

+
+ + +
+
+ +
+ {history.length === 0 ? ( +

+ No history available +

+ ) : ( + history.map((entry) => (
- - {key.charAt(0).toUpperCase() + key.slice(1)} - -
- ) - )} +
+ + {new Date(entry.timestamp).toLocaleString()} + +
+ + {entry.type} + + +
+
+
+                          {entry.code}
+                        
+
+ )) + )} +
+
-
-
-
+ )} + + ) : null}
); }; diff --git a/src/components/ui/alert.jsx b/src/components/ui/alert.jsx new file mode 100644 index 0000000..534a30b --- /dev/null +++ b/src/components/ui/alert.jsx @@ -0,0 +1,31 @@ +"use client"; + +import React from 'react'; +import { cn } from '@/lib/utils'; + +export function Alert({ type = 'info', message, className, onClose }) { + const baseStyles = 'p-4 mb-4 rounded-lg flex justify-between items-center'; + const typeStyles = { + error: 'bg-red-100 text-red-800 dark:bg-red-200 dark:text-red-900', + success: 'bg-green-100 text-green-800 dark:bg-green-200 dark:text-green-900', + warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-200 dark:text-yellow-900', + info: 'bg-blue-100 text-blue-800 dark:bg-blue-200 dark:text-blue-900', + }; + + return ( +
+ {message} + {onClose && ( + + )} +
+ ); +} diff --git a/src/components/ui/code-smells.jsx b/src/components/ui/code-smells.jsx new file mode 100644 index 0000000..487c4eb --- /dev/null +++ b/src/components/ui/code-smells.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { AlertTriangle, AlertCircle, Info } from 'lucide-react'; + +export function CodeSmells({ smells, darkMode }) { + if (!smells || smells.length === 0) { + return ( +
+ No code smells detected +
+ ); + } + + const getSeverityIcon = (severity) => { + switch (severity) { + case 'error': + return ; + case 'warning': + return ; + default: + return ; + } + }; + + const getSeverityColor = (severity) => { + switch (severity) { + case 'error': + return 'text-red-500 bg-red-500/10 border-red-500/20'; + case 'warning': + return 'text-yellow-500 bg-yellow-500/10 border-yellow-500/20'; + default: + return 'text-blue-500 bg-blue-500/10 border-blue-500/20'; + } + }; + + // Group smells by severity + const groupedSmells = smells.reduce((acc, smell) => { + acc[smell.severity] = acc[smell.severity] || []; + acc[smell.severity].push(smell); + return acc; + }, {}); + + const severityOrder = ['error', 'warning', 'info']; + + return ( +
+ {severityOrder.map(severity => + groupedSmells[severity] && ( +
+

+ {getSeverityIcon(severity)} + {severity.charAt(0).toUpperCase() + severity.slice(1)}s + + {groupedSmells[severity].length} + +

+
+ {groupedSmells[severity].map((smell, index) => ( +
+
+ {smell.type.split('_').map(word => + word.charAt(0) + word.slice(1).toLowerCase() + ).join(' ')} +
+

+ {smell.message} +

+ {smell.line && ( +
+ Line {smell.line} +
+ )} + {smell.code && ( +
+                      {smell.code}
+                    
+ )} +
+ ))} +
+
+ ) + )} +
+ ); +} diff --git a/src/components/ui/diff-modal.js b/src/components/ui/diff-modal.js new file mode 100644 index 0000000..4d9629a --- /dev/null +++ b/src/components/ui/diff-modal.js @@ -0,0 +1,219 @@ +'use client'; + +import React from 'react'; +import * as Dialog from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; + +const overlayVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { duration: 0.3 } + }, + exit: { + opacity: 0, + transition: { duration: 0.2 } + } +}; + +const contentVariants = { + hidden: { + opacity: 0, + scale: 0.95, + }, + visible: { + opacity: 1, + scale: 1, + transition: { + type: "spring", + duration: 0.5, + bounce: 0.3 + } + }, + exit: { + opacity: 0, + scale: 0.95, + transition: { duration: 0.2 } + } +}; + +const listItemVariants = { + hidden: { opacity: 0, x: -20 }, + visible: i => ({ + opacity: 1, + x: 0, + transition: { + delay: i * 0.1, + duration: 0.3 + } + }), + exit: i => ({ + opacity: 0, + x: -20, + transition: { + delay: i * 0.05, + duration: 0.2 + } + }) +}; + +export function DiffModal({ isOpen, onClose, segment, darkMode }) { + return ( + + + {isOpen && ( + + + + + + + + {/* Header */} +
+ + Code Details + + + + Close + +
+ + {/* Content */} +
+ {/* Code Information */} +
+ +

Code Segment

+
+                        {segment?.text || ''}
+                      
+
+ + {/* Metadata */} + +
+

Type

+

+ {segment?.type || 'Unknown'} +

+
+
+

Complexity

+

+ {segment?.complexity?.toFixed(1) || 'N/A'} +

+
+
+ + {/* Code Smells */} + {segment?.codeSmells?.length > 0 && ( + +

Code Smells

+
    + + {segment.codeSmells.map((smell, index) => ( + + + {smell.message} + + ))} + +
+
+ )} + + {/* Context */} + +

Context

+
+

Line: {segment?.line || 'Unknown'}

+

Position: {segment?.position || 'Unknown'}

+ {segment?.context && ( + +

Surrounding Code:

+
+                              {segment.context}
+                            
+
+ )} +
+
+
+
+
+
+
+ )} +
+
+ ); +} diff --git a/src/components/ui/file-upload.jsx b/src/components/ui/file-upload.jsx new file mode 100644 index 0000000..c3969c1 --- /dev/null +++ b/src/components/ui/file-upload.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Upload } from 'lucide-react'; + +export function FileUpload({ onFileContent, className = '' }) { + const handleFileChange = async (e) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = async (e) => { + const text = e.target.result; + onFileContent(text); + }; + reader.readAsText(file); + } + }; + + return ( + + ); +} diff --git a/src/components/ui/filter-dialog.jsx b/src/components/ui/filter-dialog.jsx new file mode 100644 index 0000000..8f02f44 --- /dev/null +++ b/src/components/ui/filter-dialog.jsx @@ -0,0 +1,64 @@ +"use client"; + +import React from 'react'; + +export function FilterDialog({ + isOpen, + onClose, + onApply, + filters, + darkMode +}) { + if (!isOpen) return null; + + return ( +
+
+
+

+ Filter Timeline +

+
+ +
+ {Object.entries(filters).map(([key, value]) => ( + + ))} +
+ +
+ +
+
+
+ ); +} diff --git a/src/components/ui/mini-map.jsx b/src/components/ui/mini-map.jsx new file mode 100644 index 0000000..4e7a677 --- /dev/null +++ b/src/components/ui/mini-map.jsx @@ -0,0 +1,95 @@ +"use client"; + +import React, { useEffect, useRef } from 'react'; + +export function MiniMap({ + timelineData, + visibleRange, + onNavigate, + darkMode, + width = 150, + height = 100 +}) { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas || !timelineData.length) return; + + const ctx = canvas.getContext('2d'); + const totalLines = timelineData.length; + + // Clear canvas + ctx.fillStyle = darkMode ? '#1f2937' : '#f9fafb'; + ctx.fillRect(0, 0, width, height); + + // Calculate dimensions + const lineHeight = height / totalLines; + + // Draw timeline segments + timelineData.forEach((row, index) => { + const y = (index / totalLines) * height; + + // Draw background for the entire line + ctx.fillStyle = darkMode ? '#374151' : '#e5e7eb'; + ctx.fillRect(0, y, width, Math.max(1, lineHeight)); + + // Draw segments + let currentX = 0; + row.segments.forEach(segment => { + if (segment.color !== 'transparent') { + const segmentWidth = (segment.width / 8) * (width / 100); + ctx.fillStyle = segment.color; + ctx.fillRect(currentX, y, Math.max(1, segmentWidth), Math.max(1, lineHeight)); + currentX += segmentWidth; + } + }); + }); + + // Draw visible range indicator + if (visibleRange) { + const { start, end } = visibleRange; + const visibleStart = (start / totalLines) * height; + const visibleHeight = ((end - start) / totalLines) * height; + + // Draw semi-transparent overlay for non-visible areas + ctx.fillStyle = `rgba(0, 0, 0, ${darkMode ? 0.5 : 0.2})`; + ctx.fillRect(0, 0, width, visibleStart); + ctx.fillRect(0, visibleStart + visibleHeight, width, height - (visibleStart + visibleHeight)); + + // Draw border around visible area + ctx.strokeStyle = darkMode ? '#60a5fa' : '#3b82f6'; + ctx.lineWidth = 2; + ctx.strokeRect(0, visibleStart, width, visibleHeight); + } + }, [timelineData, visibleRange, darkMode, width, height]); + + const handleClick = (e) => { + if (!timelineData.length) return; + + const rect = canvasRef.current.getBoundingClientRect(); + const y = e.clientY - rect.top; + const clickedPosition = Math.floor((y / height) * timelineData.length); + + onNavigate(Math.max(0, Math.min(clickedPosition, timelineData.length - 1))); + }; + + return ( +
+ +
+ ); +} diff --git a/src/components/ui/timeline-controls.jsx b/src/components/ui/timeline-controls.jsx new file mode 100644 index 0000000..515bbbc --- /dev/null +++ b/src/components/ui/timeline-controls.jsx @@ -0,0 +1,68 @@ +"use client"; + +import React from 'react'; +import { Search, ZoomIn, ZoomOut, Filter } from 'lucide-react'; + +export function TimelineControls({ + onZoomIn, + onZoomOut, + onSearch, + onFilter, + darkMode +}) { + return ( +
+
+ onSearch(e.target.value)} + className={`w-full pl-10 pr-4 py-2 rounded-lg border ${ + darkMode + ? 'bg-gray-800 border-gray-700 text-gray-200' + : 'bg-white border-gray-200 text-gray-700' + }`} + /> + +
+ + + + + + +
+ ); +} diff --git a/src/lib/analysis.js b/src/lib/analysis.js new file mode 100644 index 0000000..032a312 --- /dev/null +++ b/src/lib/analysis.js @@ -0,0 +1,415 @@ +// Code complexity analysis utilities + +// Code Analysis Constants +const COMPLEXITY_WEIGHTS = { + CONTROL_FLOW: 2, // if, for, while, etc. + NESTING: 1.5, // Nested blocks + LOGICAL_OPS: 1, // &&, ||, ! + TERNARY: 1.5, // ? : + FUNCTION_CALLS: 1, // Function invocations + RECURSION: 2, // Recursive calls +}; + +const CODE_SMELL_PATTERNS = { + // ESLint-inspired patterns + NO_UNUSED_VARS: { + pattern: /(?:let|const|var)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*=.*?(?![\s\S]*\1)/, + message: 'Unused variable detected', + severity: 'warning' + }, + NO_CONSOLE: { + pattern: /console\.(log|debug|info|warn|error)/, + message: 'Unexpected console statement', + severity: 'warning' + }, + MAX_LEN: { + pattern: /.{120,}/, + message: 'Line exceeds maximum length (120 characters)', + severity: 'warning' + }, + NO_EVAL: { + pattern: /\beval\(/, + message: 'eval() is dangerous and should be avoided', + severity: 'error' + }, + NO_ALERT: { + pattern: /\b(alert|confirm|prompt)\(/, + message: 'Unexpected alert/confirm/prompt', + severity: 'warning' + }, + NO_NESTED_TERNARY: { + pattern: /\?.*\?/, + message: 'Nested ternary expressions are hard to read', + severity: 'warning' + }, + PREFER_CONST: { + pattern: /let\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*=\s*[^;,\n]*(?![\s\S]*\1\s*=)/, + message: 'Use const instead of let for values that are never reassigned', + severity: 'info' + }, + NO_MULTIPLE_EMPTY_LINES: { + pattern: /\n\s*\n\s*\n/, + message: 'Multiple empty lines detected', + severity: 'info' + }, + NO_DEBUGGER: { + pattern: /debugger;?/, + message: 'Unexpected debugger statement', + severity: 'error' + }, + CALLBACK_RETURN: { + pattern: /function.*callback.*\{(?![^}]*return)/, + message: 'Expected return in callback function', + severity: 'warning' + }, + NO_SHADOW: { + pattern: /(?:let|const|var)\s+([a-zA-Z_$][0-9a-zA-Z_$]*).+?(?:let|const|var)\s+\1/, + message: 'Variable shadows another variable', + severity: 'warning' + }, + CAMELCASE: { + pattern: /(?:let|const|var)\s+[a-z]+[_][a-z]+/, + message: 'Use camelCase for variable names', + severity: 'info' + }, + NO_MAGIC_NUMBERS: { + pattern: /(? 0 instead of === 0" }, + { regex: /for\s*\(.*\{[\s\S]*\}/g, message: "Consider using array methods instead of for loops" }, + { regex: /catch\s*\(\s*e\s*\)/g, message: "Use more descriptive error variable names" }, + { regex: /setTimeout\s*\(\s*function\s*\(\)/g, message: "Consider using async/await instead of setTimeout" } + ] + }, + python: { + patterns: [ + { regex: /print\s*\([^)]*\)/g, message: "Consider using logging instead of print" }, + { regex: /except:/g, message: "Avoid bare except clause" }, + { regex: /import \*/g, message: "Avoid wildcard imports" }, + { regex: /global\s+[a-zA-Z_]/g, message: "Avoid global variables" }, + { regex: /lambda/g, message: "Consider using a regular function instead of lambda" } + ] + }, + java: { + patterns: [ + { regex: /System\.out\.println/g, message: "Use a logger instead of System.out.println" }, + { regex: /catch\s*\(\s*Exception\s+e\s*\)/g, message: "Avoid catching generic Exception" }, + { regex: /null\s*==/g, message: "Use Objects.isNull() or Optional" }, + { regex: /synchronized/g, message: "Consider using concurrent collections instead" } + ] + }, + csharp: { + patterns: [ + { regex: /Console\.(Write|WriteLine)/g, message: "Use logging framework instead of Console" }, + { regex: /catch\s*\(\s*Exception\s+e\s*\)/g, message: "Avoid catching generic Exception" }, + { regex: /goto/g, message: "Avoid using goto statements" } + ] + }, + ruby: { + patterns: [ + { regex: /puts/g, message: "Use Rails.logger instead of puts" }, + { regex: /rescue\s*$/g, message: "Avoid rescuing without specifying an error class" }, + { regex: /eval/g, message: "Avoid using eval" } + ] + }, + php: { + patterns: [ + { regex: /var_dump|print_r/g, message: "Use proper logging instead of debug functions" }, + { regex: /\$_GET|\$_POST/g, message: "Validate input data before usage" }, + { regex: /mysql_/g, message: "Use PDO or mysqli instead of mysql_* functions" } + ] + }, + go: { + patterns: [ + { regex: /panic\(/g, message: "Avoid using panic" }, + { regex: /\.Error\(\)\s*==\s*""/g, message: "Check error type instead of error string" }, + { regex: /time.Sleep/g, message: "Consider using contexts for timeouts" } + ] + }, + rust: { + patterns: [ + { regex: /unwrap\(\)/g, message: "Handle Result/Option explicitly instead of unwrap" }, + { regex: /panic!\(/g, message: "Avoid using panic!" }, + { regex: /unsafe\s*\{/g, message: "Minimize usage of unsafe blocks" } + ] + }, + swift: { + patterns: [ + { regex: /print\(/g, message: "Use logging framework instead of print" }, + { regex: /try\!/g, message: "Avoid force try" }, + { regex: /as\!/g, message: "Avoid force casting" } + ] + }, + kotlin: { + patterns: [ + { regex: /println\(/g, message: "Use logging framework instead of println" }, + { regex: /!!/g, message: "Avoid using not-null assertion operator" }, + { regex: /lateinit/g, message: "Consider using nullable or lazy properties" } + ] + }, + typescript: { + patterns: [ + { regex: /any/g, message: "Avoid using 'any' type" }, + { regex: /console\.(log|warn|error|info|debug)/g, message: "Unexpected console statement" }, + { regex: /\!=/g, message: "Use !== instead of !=" } + ] + } +}; + +// Detect language based on code content and file extension +function detectLanguage(code, fileExtension = '') { + // Map file extensions to languages + const extensionMap = { + js: 'javascript', + jsx: 'javascript', + ts: 'typescript', + tsx: 'typescript', + py: 'python', + java: 'java', + cs: 'csharp', + rb: 'ruby', + php: 'php', + go: 'go', + rs: 'rust', + swift: 'swift', + kt: 'kotlin' + }; + + // Try to detect from extension first + if (fileExtension && extensionMap[fileExtension.toLowerCase()]) { + return extensionMap[fileExtension.toLowerCase()]; + } + + // Fallback to content-based detection + const languagePatterns = { + python: /(def|import|from|class|if __name__ == ['"]__main__['"]:)/, + javascript: /(const|let|var|function|=>|require\(|import\s+.*\s+from)/, + java: /(public class|private|protected|package|import java)/, + csharp: /(using System|namespace|public class|private|protected)/, + ruby: /(require|def|class|module|puts|attr_)/, + php: /(<\?php|\$[a-zA-Z_]|namespace|use\s+.*?;)/, + go: /(package main|import \(|func|type struct)/, + rust: /(fn main|let mut|impl|pub struct)/, + swift: /(import Foundation|var|func|class|struct)/, + kotlin: /(fun|val|var|class|package)/, + typescript: /(interface|type|export|implements)/ + }; + + for (const [lang, pattern] of Object.entries(languagePatterns)) { + if (pattern.test(code)) { + return lang; + } + } + + return 'javascript'; // Default to JavaScript if no match +} + +// Detect code smells in a code segment +function detectCodeSmells(code, fileExtension = '') { + const language = detectLanguage(code, fileExtension); + const rules = languageRules[language] || languageRules.javascript; + const smells = []; + + // Apply language-specific rules + rules.patterns.forEach(pattern => { + if (pattern.regex.test(code)) { + smells.push({ + type: 'smell', + message: pattern.message, + severity: 'warning' + }); + } + }); + + // Common patterns across languages + const commonSmells = [ + { regex: /TODO|FIXME/g, message: "Remove TODO/FIXME comments before committing" }, + { regex: /\/\/\s*hack/gi, message: "Remove hack comments" }, + { regex: /function.*\{[\s\S]{100,}\}/g, message: "Function is too long" }, + { regex: /(if|while).*\{[\s\S]*\1.*\{/g, message: "Nested control structures detected" }, + { regex: /[^\w\s\(\)]{3,}/g, message: "Complex expression detected" } + ]; + + commonSmells.forEach(pattern => { + if (pattern.regex.test(code)) { + smells.push({ + type: 'smell', + message: pattern.message, + severity: 'info' + }); + } + }); + + return smells; +} + +/** + * Calculate cognitive complexity of a code segment + */ +export function calculateComplexity(code) { + let score = 0; + + // Control flow statements + const controlFlow = (code.match(/if|else|for|while|do|switch|case|try|catch|finally/g) || []).length; + score += controlFlow * COMPLEXITY_WEIGHTS.CONTROL_FLOW; + + // Nesting level (count brackets and indentation) + const nesting = (code.match(/{/g) || []).length; + score += nesting * COMPLEXITY_WEIGHTS.NESTING; + + // Logical operators + const logicalOps = (code.match(/&&|\|\||!(?!=)/g) || []).length; + score += logicalOps * COMPLEXITY_WEIGHTS.LOGICAL_OPS; + + // Ternary operators + const ternary = (code.match(/\?.*:/g) || []).length; + score += ternary * COMPLEXITY_WEIGHTS.TERNARY; + + // Function calls + const functionCalls = (code.match(/\w+\(/g) || []).length; + score += functionCalls * COMPLEXITY_WEIGHTS.FUNCTION_CALLS; + + // Recursion detection + const functionName = code.match(/function\s+(\w+)/)?.[1]; + if (functionName && code.includes(functionName + '(')) { + score += COMPLEXITY_WEIGHTS.RECURSION; + } + + return score; +} + +/** + * Analyze dependencies and imports + */ +export function analyzeDependencies(code) { + const imports = (code.match(/import.*from|require\(.*\)/g) || []); + const exports = (code.match(/export\s+(default\s+)?(\w+|\{.*\})/g) || []); + const functionCalls = (code.match(/\w+\(/g) || []).map(call => call.slice(0, -1)); + + return { + imports: imports.length, + exports: exports.length, + functionCalls, + totalDependencies: imports.length + exports.length + functionCalls.length + }; +} + +/** + * Calculate size score based on code length and structure + */ +export function calculateSizeScore(code) { + const lines = code.split('\n').length; + const chars = code.length; + return Math.log10(lines * chars) / 2; +} + +/** + * Get color based on complexity score + */ +export function getComplexityColor(complexity) { + if (complexity <= 3) { + return '#4CAF50'; // Low complexity - Green + } else if (complexity <= 6) { + return '#FFC107'; // Medium complexity - Yellow + } else if (complexity <= 9) { + return '#FF9800'; // High complexity - Orange + } else { + return '#F44336'; // Very high complexity - Red + } +} + +/** + * Main analysis function for code segments + */ +export function analyzeCodeSegment(code, fileExtension = '') { + const complexity = calculateComplexity(code); + const dependencies = analyzeDependencies(code); + const sizeScore = calculateSizeScore(code); + const codeSmells = detectCodeSmells(code, fileExtension); + const performanceImpact = analyzePerformanceImpact(code); + const changeImpact = calculateChangeImpact(code); + + return { + complexity, + dependencies, + sizeScore, + codeSmells, + performanceImpact, + changeImpact, + color: getComplexityColor(complexity) + }; +} + +/** + * Analyze performance impact + */ +export function analyzePerformanceImpact(code) { + const impacts = []; + + Object.entries(PERFORMANCE_PATTERNS).forEach(([type, { pattern, message, impact }]) => { + if (pattern.test(code)) { + impacts.push({ type, message, impact }); + } + }); + + return impacts; +} + +/** + * Calculate change impact score + */ +export function calculateChangeImpact(code) { + const complexity = calculateComplexity(code); + const { totalDependencies } = analyzeDependencies(code); + const sizeScore = calculateSizeScore(code); + + return { + score: (complexity * 0.4) + (totalDependencies * 0.4) + (sizeScore * 0.2), + riskLevel: complexity > 7 || totalDependencies > 5 ? 'high' : + complexity > 4 || totalDependencies > 3 ? 'medium' : 'low' + }; +} diff --git a/src/lib/utils.js b/src/lib/utils.js index b20bf01..5ce74b1 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -4,3 +4,56 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs) { return twMerge(clsx(inputs)); } + +export const validateCodeInput = (input) => { + if (!input || typeof input !== 'string') { + throw new Error('Input must be a non-empty string'); + } + + // Basic code structure validation + const lines = input.split('\n'); + if (lines.length === 0) { + throw new Error('Input must contain at least one line of code'); + } + + return true; +}; + +export const parseCodeChanges = (input) => { + try { + // Remove any BOM and normalize line endings + const normalizedInput = input.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n'); + + // Split into lines and filter out empty lines + return normalizedInput.split('\n'); + } catch (error) { + throw new Error(`Failed to parse code changes: ${error.message}`); + } +}; + +export const getTokenType = (token) => { + if (!token) return 'default'; + + const patterns = { + keyword: /^(class|function|const|let|var|if|else|for|while|return|import|from|async|await|try|catch|throw|new|this|super)$/, + class: /^[A-Z][a-zA-Z0-9]*$/, + function: /^[a-z][a-zA-Z0-9]*(?=\()/, + variable: /^[a-z][a-zA-Z0-9]*$/, + operator: /[+\-*/%=<>!&|^~]/, + string: /^(['"]).*\1$/, + number: /^\d+$/, + boolean: /^(true|false)$/, + comment: /^\/\//, + decorator: /^@/, + bracket: /[{}()[\]]/, + punctuation: /[.,;]/ + }; + + for (const [type, pattern] of Object.entries(patterns)) { + if (pattern.test(token)) { + return type; + } + } + + return 'default'; +};