diff --git a/.gitignore b/.gitignore index 2fc81b60..c78eed20 100644 --- a/.gitignore +++ b/.gitignore @@ -266,6 +266,10 @@ out .nuxt dist +# Vite cache directories +.vite +**/.vite + # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js @@ -279,6 +283,10 @@ dist .temp .cache +# Temporary build files +.tmp +**/.tmp + # vitepress build output **/.vitepress/dist @@ -288,6 +296,14 @@ dist # Docusaurus cache and generated files .docusaurus +# Turbo cache +.turbo +**/.turbo + +# Vite SSR dist +dist-ssr +**/dist-ssr + # Serverless directories .serverless/ @@ -346,3 +362,7 @@ apps/backend/logs/ # GitHub instructions .github/instructions/sonarqube_mcp.instructions.md + +# VS Code MCP configuration +.vscode/mcp.json +.playwright-mcp/ diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 4d26384b..0a21b5cc 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -38,7 +38,8 @@ gitignore: true globs: - '**/*.{md,markdown}' -# 5) Ignore patterns - exclude prompts folder and Serena memories +# 5) Ignore patterns - exclude prompts folder, Serena memories, and new frontend docs ignores: - 'prompts/**/*.{md,markdown}' - '.serena/memories/**/*.{md,markdown}' + - 'apps/frontend/src/**/*.{md,markdown}' diff --git a/.serena/memories/architecture_overview.md b/.serena/memories/architecture_overview.md index f56fc031..3422be98 100644 --- a/.serena/memories/architecture_overview.md +++ b/.serena/memories/architecture_overview.md @@ -43,45 +43,55 @@ External Systems (Redis, Git, Filesystem) ### Key Services #### 1. **gitService** (`services/gitService.ts`) + - Git operations: clone, log extraction, repository analysis - Streaming support for large repositories (50k+ commits) - Batch processing with configurable batch sizes - Integration with `repositoryCoordinator` for shared repository access **Key Methods:** + - `getCommits(repoPath)` - Extract commits from local repository - `cloneRepository(repoUrl, options)` - Clone with configurable depth - Streaming capabilities for memory-efficient large repo handling #### 2. **cache** (`services/cache.ts`) + Multi-tier caching strategy: + - **Tier 1 - Raw Commits** (60% memory): Direct Git extraction results, TTL 1h - **Tier 2 - Filtered Commits** (25% memory): Author/date filtered, TTL 30min - **Tier 3 - Aggregated Data** (15% memory): Processed visualizations, TTL 15min **Backends:** + - **Redis**: Primary distributed cache (via ioredis) - **hybridLruCache**: In-memory LRU + disk persistence fallback - Automatic fallback and health checks **Key Functions:** + - `getFromCache(key)` - Multi-tier read with fallback - `setInCache(key, value, ttl)` - Multi-tier write with replication - `isCacheHealthy()` - Health status of cache backends - `switchCacheBackend(backend)` - Runtime backend switching #### 3. **repositoryCoordinator** (`services/repositoryCoordinator.ts`) + Prevents duplicate repository clones and manages shared access: + - **Operation Coalescing**: Combines identical concurrent operations - **Reference Counting**: Tracks active users of each repository - **Automatic Cleanup**: Removes unused repositories - **Lock Management**: Deadlock-free concurrent access via `lockManager` **Key Functions:** + - `withSharedRepository(repoUrl, operation)` - Execute with shared repo access - `coordinatedOperation(repoUrl, operationType, operation)` - Coordinated execution **Architecture:** + ``` Request 1 ─┐ Request 2 ─┼─→ Coordinator ─→ Single Clone ─→ Shared Access @@ -89,21 +99,27 @@ Request 3 ─┘ (Reference Counted) ``` #### 4. **repositoryCache** (`services/repositoryCache.ts`) + Physical repository caching on disk: + - Max repositories: 50 (configurable) - Max age: 24 hours (configurable) - LRU eviction when limits reached - Integration with coordinator for reuse #### 5. **fileAnalysisService** (`services/fileAnalysisService.ts`) + File type distribution analysis: + - Categorizes files (code, documentation, config, assets, other) - Extension-based statistics - Directory-level breakdown - Performance optimized with streaming #### 6. **repositorySummaryService** (`services/repositorySummaryService.ts`) + Repository metadata extraction: + - Sparse clone approach (95-99% bandwidth savings) - Creation date determination (first commit or API) - Last commit info with relative time @@ -111,7 +127,9 @@ Repository metadata extraction: - Total commits and contributor count #### 7. **metrics** (`services/metrics.ts`) + Prometheus metrics collection: + - Request counters and latencies - Cache hit rates - Memory usage @@ -119,7 +137,9 @@ Prometheus metrics collection: - Custom business metrics #### 8. **logger** (`services/logger.ts`) + Winston logging with: + - Daily log rotation - Multiple log levels (error, warn, info, debug) - Structured logging with context @@ -128,34 +148,44 @@ Winston logging with: ### Utilities #### **hybridLruCache** (`utils/hybridLruCache.ts`) + Hierarchical LRU cache: + - In-memory primary cache - Disk-based secondary cache - Automatic tier promotion/demotion - Memory pressure-aware eviction #### **lockManager** (`utils/lockManager.ts`) + Distributed locking: + - Redis-based locks with TTL - Lock cleanup on timeout - Prevents race conditions in coordinator - Supports lock renewal #### **memoryPressureManager** (`utils/memoryPressureManager.ts`) + Memory monitoring and protection: + - Thresholds: Warning (75%), Critical (85%), Emergency (95%) - Circuit breakers for memory protection - Request throttling under pressure - Emergency cache eviction #### **urlSecurity** (`utils/urlSecurity.ts`) + Repository URL validation: + - Blocks malicious URLs (file://, javascript:, etc.) - Validates Git hosting platforms (GitHub, GitLab, Bitbucket) - Normalizes URLs #### **routeHelpers** (`utils/routeHelpers.ts`) + Common route patterns extracted for reuse: + - `setupRouteRequest()` - Initialize request context - `recordRouteSuccess()` - Success response with metrics - `recordRouteError()` - Error handling with logging @@ -173,6 +203,7 @@ Common route patterns extracted for reuse: ### Routes #### **repositoryRoutes** (`routes/repositoryRoutes.ts`) + - `GET /repositories/summary` - Repository metadata - `GET /repositories/churn` - Code churn analysis - `GET /repositories/commits` - All commits @@ -181,9 +212,11 @@ Common route patterns extracted for reuse: - `GET /repositories/full-data` - Complete repository data #### **commitRoutes** (`routes/commitRoutes.ts`) + - Legacy commit endpoints (being refactored) #### **healthRoutes** (`routes/healthRoutes.ts`) + - `GET /health` - Basic health check - `GET /health/detailed` - Comprehensive system status - `GET /health/memory` - Memory pressure status @@ -204,23 +237,24 @@ Request → Tier 1 (Raw Commits, 60%) ``` ### Cache Key Design + ```typescript // Tier 1: Raw commits `commits:${repoUrlHash}` - // Tier 2: Filtered commits `commits:filtered:${repoUrlHash}:${filterHash}` - // Tier 3: Aggregated data -`heatmap:${repoUrlHash}:${timePeriod}:${filterHash}` +`heatmap:${repoUrlHash}:${timePeriod}:${filterHash}`; ``` ### TTL Strategy + - **Raw data**: 1 hour (highest reusability) - **Filtered data**: 30 minutes (medium reusability) - **Aggregated data**: 15 minutes (specific use case) ### Backends Priority + 1. **Redis** (primary) - Distributed, fast, persistent 2. **Memory** (fallback) - Local, fastest, volatile 3. **Disk** (last resort) - Local, slow, persistent @@ -250,6 +284,7 @@ Request → Coordinator.withSharedRepository() ``` ### Benefits + - **Efficiency**: Single clone for concurrent identical requests - **Resource Management**: Reference counting prevents premature cleanup - **Consistency**: Lock-based coordination prevents race conditions @@ -258,6 +293,7 @@ Request → Coordinator.withSharedRepository() ## Memory Management ### Monitoring + ``` Normal (< 75%) → Allow all operations Warning (75-85%) → Log warnings, continue @@ -266,11 +302,13 @@ Emergency (> 95%) → Reject new requests, aggressive eviction ``` ### Emergency Eviction Order + 1. Tier 3 cache (aggregated data) - least reusable 2. Tier 2 cache (filtered data) - medium reusability 3. Tier 1 cache (raw commits) - highest reusability ### Circuit Breakers + - Automatic request rejection at emergency threshold - Prevents system overload and crashes - Self-recovery when memory drops below threshold @@ -278,50 +316,101 @@ Emergency (> 95%) → Reject new requests, aggressive eviction ## Streaming for Large Repositories ### Activation + - Automatically enabled for repositories with 50k+ commits - Configurable threshold via `STREAMING_COMMIT_THRESHOLD` ### Batch Processing + - Default batch size: 1000 commits - Configurable via `STREAMING_BATCH_SIZE` - Memory-efficient processing of massive histories ### Benefits + - Handles repositories with 100k+ commits - Prevents memory exhaustion - Progressive data delivery to frontend ## Frontend Architecture -### Component Structure +### Component Structure (Redesigned with shadcn/ui) + ``` App.tsx (Root) ↓ -MainPage.tsx (Main layout) - ├─ RepoInput.tsx (URL input) - ├─ ActivityHeatmap.tsx (Visualization) - ├─ CommitList.tsx (Commit display) - └─ RiveLoader.tsx (Loading animation) +├─ Header.tsx (Top navigation with theme toggle) +├─ Footer.tsx (Bottom footer) +├─ LandingPage.tsx (Entry page with repo input) +├─ DashboardPage.tsx (Main analytics dashboard) +│ ├─ CommitHeatmap.tsx (GitHub-style heatmap) +│ ├─ ActivityChart.tsx (Time-series activity) +│ ├─ CodeChurnChart.tsx (Code change visualization) +│ ├─ FileDistributionChart.tsx (File type distribution) +│ ├─ FileTypeList.tsx (Detailed file breakdown) +│ ├─ GraphViewTimeline.tsx (Git graph timeline) +│ ├─ GitDiffViewer.tsx (Commit diff display) +│ ├─ AIInsights.tsx (AI-powered repository insights) +│ └─ PremiumFeatures.tsx (Premium feature showcase) +├─ SettingsDrawer.tsx (Settings panel) +├─ NewsDrawer.tsx (News/changelog panel) +├─ InfoModal.tsx (Information modals) +└─ RiveLoader.tsx (Loading animation) ``` +### UI Component Library (shadcn/ui) + +Located in `components/ui/`, includes: + +- **Primitives**: button, input, textarea, label, select, checkbox, radio-group, switch, slider +- **Layout**: card, tabs, accordion, collapsible, separator, sheet, drawer, sidebar +- **Overlays**: dialog, alert-dialog, popover, hover-card, tooltip, dropdown-menu, context-menu +- **Data Display**: table, badge, avatar, progress, skeleton, alert +- **Navigation**: navigation-menu, menubar, breadcrumb, pagination +- **Charts**: chart component (recharts wrapper) +- **Forms**: form components with React Hook Form integration +- **Advanced**: carousel, command, scroll-area, resizable, sonner (toast) +- **Utilities**: utils.ts (cn function for className merging), use-mobile.ts hook + +All components built on: + +- **Radix UI primitives** for accessibility and headless UI +- **class-variance-authority (CVA)** for variant-based styling +- **tailwind-merge** for className conflict resolution +- **clsx** for conditional classNames + ### API Communication + - **Centralized API client**: `services/api.ts` - **Axios-based**: Configured with base URL and interceptors - **Type-safe**: All requests/responses use types from `@gitray/shared-types` ### State Management + - React hooks for local state - No global state management (Redux/Context) currently - Direct API calls from components +- Theme state managed via next-themes + +### UI/UX Features + +- **Dark/Light Mode**: System-aware theme switching with next-themes +- **Toast Notifications**: Sonner for user feedback +- **Loading States**: Rive animations for engaging loading experiences +- **Responsive Design**: Mobile-first with Tailwind responsive utilities +- **Animations**: Framer Motion for smooth transitions +- **Accessibility**: Full keyboard navigation and screen reader support via Radix UI ## Shared Types Package ### Purpose + - Single source of truth for TypeScript types - Prevents type duplication between frontend/backend - Exported as `@gitray/shared-types` workspace package ### Key Exports + - `Commit`, `Author`, `CommitFilterOptions` - `CommitHeatmapData`, `CommitAggregation`, `TimePeriod` - `FileTypeDistribution`, `FileInfo`, `FileCategory` @@ -331,6 +420,7 @@ MainPage.tsx (Main layout) - Constants: `HTTP_STATUS`, `TIME`, `ERROR_MESSAGES`, `GIT_SERVICE` ### Build Process + - Must be built before backend/frontend (`pnpm build:shared-types`) - Produces both CommonJS and ESM outputs - Consumed via TypeScript project references @@ -338,6 +428,7 @@ MainPage.tsx (Main layout) ## Performance Optimizations ### Backend + - Multi-tier caching reduces Git operations by ~90% - Repository coordination eliminates duplicate clones - Streaming mode for large repositories @@ -345,12 +436,14 @@ MainPage.tsx (Main layout) - LRU eviction maintains optimal cache size ### Frontend + - Vite for fast HMR and optimized builds - React 19 with automatic batching - Lazy loading of heavy components - Efficient re-rendering with proper key usage ### Network + - Compressed responses (gzip/brotli via helmet) - Cache headers for static assets - Minimal payload sizes via selective data fetching @@ -367,6 +460,7 @@ MainPage.tsx (Main layout) ## Monitoring & Observability ### Metrics (Prometheus) + - Request count, duration, status codes - Cache hit/miss rates per tier - Memory usage and pressure levels @@ -374,6 +468,7 @@ MainPage.tsx (Main layout) - Git operation durations ### Logging (Winston) + - Structured JSON logs - Log levels: error, warn, info, debug - Daily rotation with compression @@ -381,6 +476,7 @@ MainPage.tsx (Main layout) - Contextual metadata in all logs ### Health Checks + - Basic: Service up/down - Detailed: Redis status, memory usage, cache health - Memory: Current pressure level and thresholds @@ -388,11 +484,13 @@ MainPage.tsx (Main layout) ## Scalability Considerations ### Current Design Supports + - Multiple concurrent users on single server - Horizontal scaling limited by Redis as single point - Repository cache shared via filesystem ### Future Scaling Options + - Redis Cluster for distributed caching - Load balancer with sticky sessions - Shared filesystem (NFS/S3) for repository cache diff --git a/.serena/memories/codebase_structure.md b/.serena/memories/codebase_structure.md index a862016d..cab81897 100644 --- a/.serena/memories/codebase_structure.md +++ b/.serena/memories/codebase_structure.md @@ -46,28 +46,56 @@ gitray/ │ │ ├── package.json │ │ └── tsconfig.json │ │ -│ └── frontend/ # React UI +│ └── frontend/ # React UI (redesigned with shadcn/ui) │ ├── src/ │ │ ├── components/ # React components -│ │ │ ├── ActivityHeatmap.tsx -│ │ │ ├── CommitList.tsx -│ │ │ ├── RepoInput.tsx +│ │ │ ├── ui/ # shadcn/ui component library +│ │ │ │ ├── button.tsx +│ │ │ │ ├── card.tsx +│ │ │ │ ├── tabs.tsx +│ │ │ │ ├── dialog.tsx +│ │ │ │ ├── drawer.tsx +│ │ │ │ ├── alert.tsx +│ │ │ │ ├── badge.tsx +│ │ │ │ ├── avatar.tsx +│ │ │ │ ├── chart.tsx +│ │ │ │ ├── sonner.tsx (toast) +│ │ │ │ ├── form.tsx +│ │ │ │ ├── table.tsx +│ │ │ │ ├── select.tsx +│ │ │ │ ├── input.tsx +│ │ │ │ ├── textarea.tsx +│ │ │ │ ├── utils.ts +│ │ │ │ └── [47+ more shadcn components] +│ │ │ ├── figma/ # Figma design references +│ │ │ ├── Header.tsx +│ │ │ ├── Footer.tsx +│ │ │ ├── LandingPage.tsx +│ │ │ ├── DashboardPage.tsx (main analytics view) +│ │ │ ├── CommitHeatmap.tsx +│ │ │ ├── ActivityChart.tsx +│ │ │ ├── CodeChurnChart.tsx +│ │ │ ├── FileDistributionChart.tsx +│ │ │ ├── FileTypeList.tsx +│ │ │ ├── GraphViewTimeline.tsx +│ │ │ ├── GitDiffViewer.tsx +│ │ │ ├── AIInsights.tsx +│ │ │ ├── PremiumFeatures.tsx +│ │ │ ├── SettingsDrawer.tsx +│ │ │ ├── NewsDrawer.tsx +│ │ │ ├── InfoModal.tsx +│ │ │ ├── LoadingSpinner.tsx │ │ │ ├── RiveLogo.tsx │ │ │ └── RiveLoader.tsx -│ │ ├── pages/ # Page components -│ │ │ └── MainPage.tsx │ │ ├── services/ # API clients │ │ │ └── api.ts -│ │ ├── utils/ # Utility functions -│ │ │ └── dateUtils.ts -│ │ ├── styles/ # CSS files -│ │ │ └── heatmap.css -│ │ ├── types/ # TypeScript type definitions -│ │ │ └── react-calendar-heatmap.d.ts -│ │ ├── assets/ # Static assets +│ │ ├── styles/ # CSS files (minimal, mostly Tailwind) │ │ ├── App.tsx # Root component │ │ ├── main.tsx # Application entry -│ │ └── test-setup.ts # Vitest setup +│ │ ├── index.css # Global styles + Tailwind imports +│ │ ├── test-setup.ts # Vitest setup +│ │ └── vite-env.d.ts # Vite type declarations +│ ├── public/ # Static assets (Rive animations, etc.) │ ├── package.json │ ├── tsconfig.json │ └── vite.config.ts @@ -108,6 +136,7 @@ gitray/ ## Important File Locations ### Configuration Files + - **Root TypeScript**: `tsconfig.json` (composite project references) - **Backend Config**: `apps/backend/src/config.ts` - **Environment**: `.env` (not checked in) @@ -116,16 +145,19 @@ gitray/ - **Vitest**: `vitest.config.ts` (workspace-aware) ### Entry Points + - **Backend Server**: `apps/backend/src/index.ts` - **Frontend App**: `apps/frontend/src/main.tsx` - **Shared Types**: `packages/shared-types/src/index.ts` ### Testing + - **Backend Tests**: Co-located with source files as `*.test.ts` - **Frontend Tests**: Co-located with components as `*.test.tsx` - **Performance Tests**: `apps/backend/perf/` ## Build Artifacts (Gitignored) + - `dist/` - Compiled TypeScript output - `*.tsbuildinfo` - TypeScript incremental build cache - `coverage/` - Test coverage reports @@ -138,6 +170,7 @@ gitray/ ## Key Architectural Components ### Backend Services + - **gitService**: Git operations (clone, log, analysis) - **cache**: Multi-tier caching (Redis + Memory + Disk) - **repositoryCoordinator**: Shared repository management @@ -148,10 +181,53 @@ gitray/ - **logger**: Winston logging with daily rotation ### Backend Utilities + - **hybridLruCache**: LRU cache with hierarchical tiers - **lockManager**: Distributed locking for coordination - **memoryPressureManager**: Memory threshold monitoring - **urlSecurity**: Repository URL validation -### Frontend Services +### Frontend Components & Services + +#### Core Components + +- **App.tsx**: Root component managing routing, theme, and global state +- **LandingPage.tsx**: Repository input and onboarding +- **DashboardPage.tsx**: Main analytics dashboard with multiple visualization tabs +- **Header.tsx**: Navigation bar with theme toggle, settings, news +- **Footer.tsx**: Footer with links and information + +#### Visualization Components + +- **CommitHeatmap.tsx**: GitHub-style contribution calendar heatmap +- **ActivityChart.tsx**: Time-series activity visualization +- **CodeChurnChart.tsx**: Code change and stability metrics +- **FileDistributionChart.tsx**: Pie/donut chart for file type distribution +- **FileTypeList.tsx**: Detailed file type breakdown with icons +- **GraphViewTimeline.tsx**: Git graph visualization with branches +- **GitDiffViewer.tsx**: Commit diff viewer with syntax highlighting + +#### Feature Components + +- **AIInsights.tsx**: AI-powered repository analysis and recommendations +- **PremiumFeatures.tsx**: Premium feature showcase and upsell +- **SettingsDrawer.tsx**: User settings and preferences +- **NewsDrawer.tsx**: Product updates and changelog +- **InfoModal.tsx**: Contextual help and information modals +- **LoadingSpinner.tsx**: Loading state indicator +- **RiveLoader.tsx**: Rive-powered animated loader +- **RiveLogo.tsx**: Animated Rive-based logo + +#### shadcn/ui Component Library (`components/ui/`) + +Complete set of 47+ accessible, customizable UI primitives built on Radix UI: + +- **Form Controls**: button, input, textarea, select, checkbox, radio-group, switch, slider +- **Containers**: card, sheet, drawer, dialog, alert-dialog, popover, hover-card +- **Navigation**: tabs, accordion, navigation-menu, menubar, breadcrumb, pagination +- **Display**: table, badge, avatar, alert, skeleton, progress, chart +- **Advanced**: carousel, command, sonner (toast), scroll-area, resizable panels + +#### Services + - **api.ts**: Axios-based API client for backend communication diff --git a/.serena/memories/coding_standards.md b/.serena/memories/coding_standards.md index 9c78897e..4858e9fb 100644 --- a/.serena/memories/coding_standards.md +++ b/.serena/memories/coding_standards.md @@ -1,6 +1,7 @@ # GitRay - Coding Standards and Conventions ## Core Principles + - **TypeScript Strict Mode**: Enabled everywhere, avoid `any` and implicit `any` - **Functional React**: Use functional components with hooks only - **Professional Logging**: Use Winston logger, not `console.log` in runtime code @@ -12,6 +13,7 @@ ## Naming Conventions ### Components & Types (PascalCase) + ```typescript // React Components export const CommitHeatmap: React.FC = ({ ... }) => { ... }; @@ -26,12 +28,14 @@ export class RepositoryCoordinator { ... } ``` ### Hooks (use + camelCase) + ```typescript export const useCommitFilters = () => { ... }; export const useRepositoryData = (repoUrl: string) => { ... }; ``` ### Functions & Variables (camelCase) + ```typescript export const calculateCommitStats = (commits: Commit[]) => { ... }; const filteredCommits = filterByAuthor(commits, author); @@ -39,6 +43,7 @@ let isLoading = false; ``` ### Constants & Enums (SCREAMING_SNAKE_CASE) + ```typescript export const MAX_CACHE_ENTRIES = 10000; export const STREAMING_THRESHOLD = 50000; @@ -52,6 +57,7 @@ export enum CacheTier { ``` ### Environment Variables (UPPER_SNAKE_CASE) + ```bash PORT=3001 REDIS_HOST=localhost @@ -62,24 +68,29 @@ NODE_ENV=development ## File and Directory Naming ### Frontend -- **Components**: `apps/frontend/src/components//index.tsx` (PascalCase) -- **Pages**: `apps/frontend/src/pages/.tsx` (PascalCase) + +- **Components**: `apps/frontend/src/components/.tsx` (PascalCase, single file) +- **UI Components**: `apps/frontend/src/components/ui/.tsx` (camelCase for shadcn/ui components) - **Hooks**: `apps/frontend/src/hooks/use.ts` (camelCase with 'use' prefix) - **Utilities**: `apps/frontend/src/utils/.ts` (camelCase) - **Services**: `apps/frontend/src/services/.ts` (camelCase) +- **UI Utils**: `apps/frontend/src/components/ui/utils.ts` (contains `cn()` function) ### Backend + - **Routes**: `apps/backend/src/routes/Routes.ts` (camelCase + 'Routes') - **Services**: `apps/backend/src/services/Service.ts` (camelCase + 'Service') - **Utilities**: `apps/backend/src/utils/.ts` (camelCase) - **Middlewares**: `apps/backend/src/middlewares/.ts` (camelCase) ### Shared Types + - **Index file**: `packages/shared-types/src/index.ts` (all exports in single file) ## Import Organization Group and order imports: + 1. External packages (React, Express, etc.) 2. Internal modules (`@gitray/shared-types`, `@/...`) 3. Relative imports @@ -109,6 +120,7 @@ import { describe, it, expect, vi } from 'vitest'; ## Async & Error Handling ### Use async/await, not promise chains + ```typescript // ✅ GOOD async function getCommits(repoUrl: string): Promise { @@ -126,11 +138,14 @@ async function getCommits(repoUrl: string): Promise { function getCommits(repoUrl: string): Promise { return cloneRepository(repoUrl) .then(extractCommits) - .catch(error => { throw error; }); + .catch((error) => { + throw error; + }); } ``` ### Never swallow errors + ```typescript // ✅ GOOD try { @@ -149,8 +164,13 @@ try { ``` ### Use typed error classes + ```typescript -import { GitrayError, RepositoryError, ValidationError } from '@gitray/shared-types'; +import { + GitrayError, + RepositoryError, + ValidationError, +} from '@gitray/shared-types'; throw new ValidationError('Invalid input', errors); throw new RepositoryError('Clone failed', repoUrl); @@ -159,35 +179,83 @@ throw new GitrayError('Internal error', HTTP_STATUS.INTERNAL_SERVER_ERROR); ## React Component Style -### Functional components with proper typing +### Functional components with proper typing (shadcn/ui style) + ```typescript import { FC } from 'react'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/components/ui/utils'; interface CommitListProps { commits: Commit[]; onCommitClick?: (commit: Commit) => void; + className?: string; // Allow className override } -export const CommitList: FC = ({ commits, onCommitClick }) => { +export const CommitList: FC = ({ + commits, + onCommitClick, + className +}) => { return ( -
- {commits.map((commit) => ( -
onCommitClick?.(commit)}> - {commit.message} -
- ))} -
+ + + Recent Commits + + + {commits.map((commit) => ( + + ))} + + + ); +}; +``` + +### Component composition with shadcn/ui + +```typescript +// Build complex UIs by composing shadcn/ui primitives +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; + +export const DashboardTabs: FC = () => { + return ( + + + Overview + Activity + Files + + + + + + + + + + + ); }; ``` ### Follow Rules of Hooks + ```typescript // ✅ GOOD - hooks at top level const MyComponent: FC = () => { const [data, setData] = useState([]); const { loading, error } = useRepositoryData(repoUrl); - + useEffect(() => { fetchData(); }, []); @@ -206,17 +274,69 @@ const MyComponent: FC = () => { ## Styling -### Use Tailwind CSS classes +### Use Tailwind CSS classes with shadcn/ui patterns + +```tsx +// Use shadcn/ui components as building blocks +import { Button } from '@/components/ui/button'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; + + + + Repository Stats + + + Commits + + +; +``` + +### Use `cn()` utility for conditional classes + ```tsx -
- Title - +import { cn } from '@/components/ui/utils'; + +
+ Content +
; +``` + +### Theme colors via CSS variables + +```tsx +// ✅ GOOD - use semantic color variables +
+ Subtitle + +
+ +// ❌ BAD - hardcoded colors +
+ Subtitle + +
+``` + +### Dark mode support + +```tsx +// Tailwind dark mode classes +
+ Content adapts to theme
``` ### Avoid inline styles (except dynamic values) + ```tsx // ✅ GOOD - dynamic value
...
@@ -225,9 +345,26 @@ const MyComponent: FC = () => {
...
``` +### shadcn/ui component variants + +```tsx +// Use built-in variant systems + + + + + + +New +Info +Error +Outlined +``` + ## Backend Route Structure ### RESTful conventions + ```typescript import { Router } from 'express'; import { validateRequest } from '@/middlewares/validation'; @@ -236,14 +373,14 @@ import { handleValidationErrors } from '@/utils/routeHelpers'; const router = Router(); // GET: Retrieve data -router.get('/repositories/summary', +router.get('/repositories/summary', repoUrlValidation, handleValidationErrors, async (req, res) => { ... } ); // POST: Create or process data -router.post('/repositories', +router.post('/repositories', repoUrlValidation, handleValidationErrors, async (req, res) => { ... } @@ -253,12 +390,17 @@ export default router; ``` ### Consistent error handling in routes + ```typescript -import { setupRouteRequest, recordRouteSuccess, recordRouteError } from '@/utils/routeHelpers'; +import { + setupRouteRequest, + recordRouteSuccess, + recordRouteError, +} from '@/utils/routeHelpers'; router.get('/endpoint', async (req, res) => { const { logger, startTime } = setupRouteRequest(req, 'operation-name'); - + try { const result = await performOperation(); recordRouteSuccess(res, result, logger, startTime, 'operation-name'); @@ -271,12 +413,14 @@ router.get('/endpoint', async (req, res) => { ## Testing Standards ### Test file naming + - Place beside source: `myModule.ts` → `myModule.test.ts` - Use descriptive test names - Use HappyPath Concept - Use AAA Pattern ### Test structure (Vitest) + ```typescript import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { myFunction } from './myModule'; @@ -303,6 +447,7 @@ describe('myFunction', () => { ``` ### Maintain ≥80% coverage + - Focus on critical paths - Test error cases - Mock external dependencies (Redis, Git, filesystem) @@ -310,6 +455,7 @@ describe('myFunction', () => { ## Code Quality Rules ### No `any` without justification + ```typescript // ✅ GOOD function processData(data: Commit[]): CommitStats { ... } @@ -324,6 +470,7 @@ function legacyAPI(data: any): any { // External API with unknown shape ``` ### Prefer readonly where appropriate + ```typescript interface Config { readonly port: number; @@ -334,23 +481,25 @@ const config: Readonly = { ... }; ``` ### Use const assertions for constants + ```typescript export const HTTP_STATUS = { OK: 200, BAD_REQUEST: 400, - INTERNAL_SERVER_ERROR: 500 + INTERNAL_SERVER_ERROR: 500, } as const; -export type HttpStatus = typeof HTTP_STATUS[keyof typeof HTTP_STATUS]; +export type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS]; ``` ## Documentation ### JSDoc for public APIs + ```typescript /** * Aggregates commits by time period for heatmap visualization. - * + * * @param commits - Array of commits to aggregate * @param timePeriod - Aggregation period ('day' | 'week' | 'month' | 'year') * @param filterOptions - Optional filtering criteria @@ -365,6 +514,7 @@ export function aggregateCommits( ``` ### Complex logic comments + ```typescript // Use temporal locality: recently used entries are more likely to be used again. // This implements a 3-tier LRU cache with 60/25/15 memory allocation. @@ -374,6 +524,7 @@ const tierSizes = calculateTierSizes(maxEntries); ## Commit Message Convention Follow Conventional Commits: + ``` feat: add code churn analysis endpoint fix: resolve memory leak in cache manager diff --git a/.serena/memories/frontend_architecture_detailed.md b/.serena/memories/frontend_architecture_detailed.md new file mode 100644 index 00000000..20a3b714 --- /dev/null +++ b/.serena/memories/frontend_architecture_detailed.md @@ -0,0 +1,616 @@ +# GitRay - Frontend Architecture (shadcn/ui Migration) + +## Overview + +The GitRay frontend has been completely redesigned and migrated to use **shadcn/ui**, a modern component library built on Radix UI primitives. This represents a major architectural shift from the previous implementation. + +## Current Branch + +**Branch**: `87-featfrontend-ui-redesign-migration-to-shadcnui` +**Status**: Active development - UI redesign in progress + +## Technology Stack + +### Core Technologies + +- **React**: 18.3.1 (not 19 as initially documented) +- **TypeScript**: 5.7+ (strict mode) +- **Vite**: 6.3+ (build tool with HMR) +- **Tailwind CSS**: 4.1+ (utility-first styling) + +### UI Component Library - shadcn/ui + +shadcn/ui is NOT a traditional component library but a collection of re-usable components that you copy into your codebase: + +- Built on **Radix UI** primitives (headless, accessible components) +- Styled with **Tailwind CSS** +- Uses **class-variance-authority (CVA)** for variant management +- Customizable and owns the code (no npm package dependency for components) + +### Supporting Libraries + +- **@radix-ui/react-\***: 30+ Radix UI primitive packages for accessibility +- **class-variance-authority**: Type-safe variant-based styling +- **clsx** & **tailwind-merge**: Conditional and conflict-free className merging +- **next-themes**: Dark/light mode management with system detection +- **lucide-react**: Icon library (consistent, tree-shakeable) +- **sonner**: Toast notification system +- **motion** (Framer Motion): Animation library +- **react-hook-form**: Form state management and validation +- **recharts**: Charting library (replaces ApexCharts) +- **@rive-app/react-canvas**: Rive animation integration +- **embla-carousel-react**: Carousel functionality +- **react-day-picker**: Date picker component +- **react-resizable-panels**: Resizable panel layouts +- **vaul**: Drawer component (mobile-friendly) +- **cmdk**: Command palette component + +## Component Architecture + +### Directory Structure + +``` +apps/frontend/src/ +├── components/ +│ ├── ui/ # shadcn/ui components (47+) +│ │ ├── button.tsx +│ │ ├── card.tsx +│ │ ├── tabs.tsx +│ │ ├── dialog.tsx +│ │ ├── drawer.tsx +│ │ ├── ... +│ │ └── utils.ts # cn() utility +│ ├── figma/ # Figma design references +│ ├── Header.tsx +│ ├── Footer.tsx +│ ├── LandingPage.tsx +│ ├── DashboardPage.tsx # Main view +│ ├── [Visualization Components] +│ └── [Feature Components] +├── services/ +│ └── api.ts # Backend API client +├── App.tsx # Root component +├── main.tsx # Entry point +└── index.css # Global styles + Tailwind +``` + +### shadcn/ui Component Catalog (components/ui/) + +#### Form Controls + +- **button**: Primary interaction element with variants (default, destructive, outline, secondary, ghost, link) +- **input**: Text input with error states +- **textarea**: Multi-line text input +- **label**: Accessible form labels +- **select**: Dropdown select (Radix UI based) +- **checkbox**: Checkboxes with indeterminate state +- **radio-group**: Radio button groups +- **switch**: Toggle switches +- **slider**: Range sliders +- **form**: React Hook Form integration + +#### Layout & Containers + +- **card**: Content card with header, content, footer sections +- **sheet**: Side panel overlays +- **drawer**: Mobile-friendly bottom drawer (vaul) +- **dialog**: Modal dialogs +- **alert-dialog**: Confirmation dialogs +- **popover**: Floating popovers +- **hover-card**: Hover-triggered cards +- **tooltip**: Hover tooltips +- **separator**: Divider lines +- **scroll-area**: Custom scrollbars +- **resizable**: Resizable panels + +#### Navigation + +- **tabs**: Tab navigation with content panels +- **accordion**: Collapsible sections +- **collapsible**: Simple collapse/expand +- **navigation-menu**: Complex navigation menus +- **menubar**: Menu bar navigation +- **breadcrumb**: Breadcrumb navigation +- **pagination**: Page navigation +- **dropdown-menu**: Context menus and dropdowns +- **context-menu**: Right-click menus + +#### Data Display + +- **table**: Responsive tables +- **badge**: Status badges and labels +- **avatar**: User avatars with fallbacks +- **alert**: Alert messages with variants +- **skeleton**: Loading skeletons +- **progress**: Progress bars +- **chart**: Recharts wrapper with theming + +#### Advanced Components + +- **carousel**: Image/content carousels (Embla) +- **command**: Command palette (⌘K) +- **calendar**: Date picker calendar +- **input-otp**: OTP input fields +- **sonner**: Toast notifications +- **toggle**: Toggle buttons +- **toggle-group**: Toggle button groups +- **sidebar**: App sidebar layout +- **aspect-ratio**: Aspect ratio containers + +#### Utilities + +- **utils.ts**: `cn()` function for merging Tailwind classes +- **use-mobile.ts**: Hook for responsive mobile detection + +## Component Design Patterns + +### Variant-Based Styling (CVA) + +```typescript +const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-md text-sm font-medium', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground', + outline: 'border border-input hover:bg-accent', + // ... + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); +``` + +### Composition Pattern + +```typescript +// Card component composition + + + Title + Description + + + {/* Content */} + + + {/* Footer */} + + +``` + +### Accessibility First + +All components built on Radix UI primitives ensure: + +- Keyboard navigation +- Screen reader support +- Focus management +- ARIA attributes +- Semantic HTML + +## Application Components + +### Core Pages + +1. **App.tsx**: Root component + - Theme management (dark/light/system) + - Global state (loading, auth, modal states) + - Repository data state (commits, heatmap, repo URL) + - Page routing (landing vs dashboard) + +2. **LandingPage.tsx**: Entry page + - Repository URL input + - Quick start guide + - Feature highlights + - Call-to-action + +3. **DashboardPage.tsx**: Main analytics view + - Tab-based navigation + - Multiple visualization panels + - Responsive grid layout + - Data fetching and state management + +### Layout Components + +- **Header.tsx**: Top navigation + - Logo/branding + - Theme toggle + - Settings button + - News notifications + - Sign-in state + +- **Footer.tsx**: Bottom footer + - Links (privacy, terms, contact) + - Copyright + - Social links + +### Visualization Components + +1. **CommitHeatmap.tsx**: GitHub-style contribution calendar + - Daily commit counts + - Color-coded intensity + - Interactive tooltips + - Date range filtering + +2. **ActivityChart.tsx**: Time-series activity + - Line/area charts + - Commit frequency over time + - Author filtering + - Recharts-based + +3. **CodeChurnChart.tsx**: Code change metrics + - Lines added/deleted + - Churn rate visualization + - Risk level indicators + - Stability trends + +4. **FileDistributionChart.tsx**: File type distribution + - Pie/donut charts + - Language breakdown + - Interactive legends + - Percentage calculations + +5. **FileTypeList.tsx**: Detailed file breakdown + - Categorized lists (code, docs, config, assets) + - File counts and percentages + - Icon indicators + - Expandable sections + +6. **GraphViewTimeline.tsx**: Git graph visualization + - Branch visualization + - Commit timeline + - Merge tracking + - Interactive navigation + +7. **GitDiffViewer.tsx**: Commit diff display + - Syntax-highlighted diffs + - Side-by-side or unified view + - File tree navigation + - Line-by-line changes + +### Feature Components + +1. **AIInsights.tsx**: AI-powered analysis + - Repository health score + - Code quality recommendations + - Team insights + - Predictive analytics + - **Note**: Likely placeholder for future AI features + +2. **PremiumFeatures.tsx**: Premium upsell + - Feature showcase + - Pricing information + - Upgrade prompts + - Feature comparison + +3. **SettingsDrawer.tsx**: User preferences + - Theme selection + - Display options + - Notification preferences + - Account settings + +4. **NewsDrawer.tsx**: Product updates + - Changelog + - Feature announcements + - Unread indicators + - Dismissible items + +5. **InfoModal.tsx**: Contextual help + - "What is GitRay?" + - Privacy information + - Local/remote repo info + - Tutorial content + +### Utility Components + +- **LoadingSpinner.tsx**: Loading indicator +- **RiveLoader.tsx**: Animated Rive-based loader +- **RiveLogo.tsx**: Animated logo + +## Styling Approach + +### Tailwind CSS 4.1 + +- **Utility-first CSS**: All styling via Tailwind utilities +- **Custom theme**: Defined in `tailwind.config.js` +- **CSS variables**: Theme colors defined as CSS variables for easy theming +- **Dark mode**: Class-based dark mode (`dark:` prefix) +- **Responsive**: Mobile-first breakpoints (`sm:`, `md:`, `lg:`, `xl:`, `2xl:`) + +### Theme System + +- **next-themes**: System-aware theme switching +- **Theme options**: light, dark, system +- **CSS variables**: Colors defined as HSL values + - `--background`, `--foreground` + - `--primary`, `--primary-foreground` + - `--secondary`, `--secondary-foreground` + - `--muted`, `--muted-foreground` + - `--accent`, `--accent-foreground` + - `--destructive`, `--destructive-foreground` + - `--border`, `--input`, `--ring` + - `--chart-1` through `--chart-5` + +### Component Styling Pattern + +```typescript +import { cn } from './ui/utils'; + +export function MyComponent({ className, ...props }) { + return ( +
+ ); +} +``` + +## State Management + +### Local State (React Hooks) + +- `useState` for component-level state +- `useEffect` for side effects (data fetching, theme application) +- `useMemo` for expensive computations +- `useCallback` for memoized callbacks + +### No Global State Library + +- No Redux, Zustand, or Context API currently +- State lifted to nearest common ancestor +- Props drilling for shared state +- Direct API calls from components + +### Repository Data Flow + +``` +App.tsx (holds commits, heatmapData, repoUrl) + ↓ +DashboardPage.tsx (receives as props) + ↓ +Individual visualization components +``` + +## API Integration + +### Centralized API Client (`services/api.ts`) + +- Axios-based HTTP client +- Base URL configuration +- Request/response interceptors +- Error handling +- Type-safe with `@gitray/shared-types` + +### API Methods + +- `getRepositoryFullData(url, options)`: Fetch all repository data +- Additional methods for specific endpoints + +### Type Safety + +All API requests/responses typed with interfaces from `@gitray/shared-types`: + +- `Commit`, `CommitHeatmapData`, `RepositorySummary` +- `FileTypeDistribution`, `CodeChurnAnalysis` +- Error types: `GitrayError`, `RepositoryError`, `ValidationError` + +## Animation & Motion + +### Framer Motion (`motion`) + +- Component animations +- Page transitions +- Gesture animations +- Stagger effects + +### Rive Animations + +- Loading animations +- Logo animations +- Interactive illustrations +- Performance-optimized + +## User Experience Features + +### Toast Notifications (Sonner) + +```typescript +import { toast } from 'sonner'; + +toast.success('Repository loaded successfully!'); +toast.error('Failed to load repository'); +toast.loading('Loading commits...'); +``` + +### Dark/Light Mode + +- System detection by default +- Manual toggle in header +- Persisted preference +- Smooth transitions + +### Responsive Design + +- Mobile-first approach +- Breakpoints: 640px, 768px, 1024px, 1280px, 1536px +- Adaptive layouts +- Touch-friendly interactions + +### Accessibility + +- Keyboard navigation +- Screen reader support +- Focus indicators +- Semantic HTML +- ARIA attributes (via Radix UI) + +## Testing + +### Test Setup (`test-setup.ts`) + +- Vitest configuration +- Testing Library setup +- Mock setup for APIs + +### Test Locations + +- Co-located with components: `ComponentName.test.tsx` +- Located in `__tests__/` directory + +### Testing Tools + +- **@testing-library/react**: Component testing +- **@testing-library/jest-dom**: DOM matchers +- **@testing-library/user-event**: User interaction simulation +- **Vitest**: Test runner +- **@vitest/coverage-v8**: Coverage reporting + +## Build & Development + +### Development + +```bash +pnpm dev:frontend # Start Vite dev server (port 5173) +``` + +### Build + +```bash +pnpm build:frontend # Production build +``` + +### Build Output + +- Optimized bundles in `dist/` +- Code splitting by route/component +- Asset optimization +- Source maps in development + +## Migration Notes (Old → New) + +### Removed Dependencies + +- `react-calendar-heatmap` → Custom heatmap component +- `apexcharts` / `react-apexcharts` → Recharts +- `react-select` → Radix UI Select +- `date-fns` → (May still be in use, check) + +### Added Dependencies + +- All `@radix-ui/react-*` packages (30+) +- `class-variance-authority`, `clsx`, `tailwind-merge` +- `lucide-react` (icons) +- `sonner` (toasts) +- `next-themes` (theme management) +- `motion` (Framer Motion) +- `recharts` (charts) +- `react-hook-form` (forms) +- `vaul`, `cmdk`, `embla-carousel-react` (advanced components) + +### Component Refactors + +- Old `ActivityHeatmap.tsx` → New `CommitHeatmap.tsx` +- Old `CommitList.tsx` → Integrated into `DashboardPage.tsx` or removed +- Old `RepoInput.tsx` → Integrated into `LandingPage.tsx` +- New components: `Header`, `Footer`, `SettingsDrawer`, `NewsDrawer`, `InfoModal` +- New visualizations: `ActivityChart`, `CodeChurnChart`, `GraphViewTimeline`, `GitDiffViewer` +- New features: `AIInsights`, `PremiumFeatures` + +### Styling Migration + +- From custom CSS to Tailwind utilities +- Consistent design system via CSS variables +- Dark mode support added +- Responsive design improved + +## Best Practices + +### Component Creation + +1. Use shadcn/ui components as building blocks +2. Compose with semantic HTML +3. Apply Tailwind utilities for styling +4. Use `cn()` utility for conditional classes +5. Ensure accessibility (keyboard, ARIA, focus) + +### Styling Guidelines + +1. Prefer Tailwind utilities over custom CSS +2. Use CSS variables for theme colors +3. Follow mobile-first responsive design +4. Use dark mode variants (`dark:`) +5. Keep specificity low + +### Type Safety + +1. Import types from `@gitray/shared-types` +2. Define component prop interfaces +3. Use strict TypeScript mode +4. Avoid `any` types + +### Performance + +1. Lazy load heavy components +2. Memoize expensive computations +3. Use proper React keys +4. Optimize re-renders +5. Code split by route + +## Future Considerations + +### Potential Improvements + +- Global state management (Zustand/Context) if state becomes complex +- Route-based code splitting for better performance +- Progressive Web App (PWA) features +- Advanced animation system +- Real-time updates (WebSocket) +- Offline support +- Advanced filtering and search +- Export functionality (PDF, CSV) + +### Known Limitations + +- No global state management (may become limiting) +- Direct API calls from components (no data layer abstraction) +- Limited error boundary implementation +- No comprehensive loading state management +- AI features appear to be placeholders + +## Debugging Tips + +### Component Issues + +1. Check Radix UI documentation for primitive usage +2. Verify Tailwind class application with browser DevTools +3. Use React DevTools for component hierarchy +4. Check console for accessibility warnings + +### Styling Issues + +1. Verify CSS variable values in `:root` and `.dark` +2. Check Tailwind class conflicts with browser inspector +3. Use `cn()` utility correctly for class merging +4. Ensure Tailwind CSS is properly imported in `index.css` + +### Build Issues + +1. Ensure `@gitray/shared-types` is built first +2. Check TypeScript errors: `tsc --noEmit` +3. Verify all imports resolve correctly +4. Clear Vite cache: `rm -rf .vite` diff --git a/.serena/memories/memory_update_summary_2026-01-03.md b/.serena/memories/memory_update_summary_2026-01-03.md new file mode 100644 index 00000000..801588d2 --- /dev/null +++ b/.serena/memories/memory_update_summary_2026-01-03.md @@ -0,0 +1,168 @@ +# GitRay Memory Update Summary + +## Date: January 3, 2026 + +## Context + +Following the successful indexing of the GitRay project with Serena MCP server, all memory files have been updated to reflect the current state of the codebase, particularly the major frontend UI redesign migration to shadcn/ui. + +## Updated Memory Files + +### 1. project_overview.md + +**Changes:** + +- Updated frontend technology stack to reflect shadcn/ui components +- Changed React version from 19 to 18.3 (correct version) +- Added shadcn/ui, Radix UI, motion (Framer Motion), Recharts +- Added UI libraries: next-themes, lucide-react, sonner, React Hook Form +- Updated current development branch to `87-featfrontend-ui-redesign-migration-to-shadcnui` +- Noted recent major changes: Complete frontend UI redesign + +### 2. architecture_overview.md + +**Changes:** + +- Completely rewrote Frontend Architecture section +- Documented new component structure with Header, Footer, LandingPage, DashboardPage +- Added comprehensive shadcn/ui component library documentation (47+ components) +- Documented UI component categories: primitives, layout, overlays, data display, navigation, charts, forms, advanced +- Added UI/UX features section: dark/light mode, toast notifications, loading states, responsive design, animations, accessibility +- Updated component hierarchy diagram +- Added theme management details with next-themes + +### 3. codebase_structure.md + +**Changes:** + +- Updated frontend directory structure to show new components +- Added `components/ui/` directory with all shadcn/ui components +- Documented new visualization components: ActivityChart, CodeChurnChart, GraphViewTimeline, GitDiffViewer +- Added new feature components: AIInsights, PremiumFeatures, SettingsDrawer, NewsDrawer, InfoModal +- Created comprehensive "Frontend Components & Services" section listing all components by category +- Updated dependencies in package.json description + +### 4. coding_standards.md + +**Changes:** + +- Completely updated "Styling" section with shadcn/ui patterns +- Added `cn()` utility usage examples +- Added theme colors via CSS variables pattern +- Added dark mode support examples +- Added shadcn/ui component variant usage +- Updated React Component Style section with shadcn/ui composition examples +- Updated file naming conventions to reflect single-file components and UI components directory +- Added examples of building complex UIs by composing shadcn/ui primitives + +### 5. task_completion_checklist.md + +**Changes:** + +- Added shadcn/ui specific checks to React Components section +- Added requirement to use `cn()` utility for className merging +- Added theme colors via CSS variables check +- Added dark mode classes check +- Added accessibility checks +- Added new section "Adding a New Frontend Component" with 10-step checklist + +### 6. frontend_architecture_detailed.md (NEW) + +**Created comprehensive new memory file covering:** + +- Complete technology stack documentation +- Directory structure with all 47+ shadcn/ui components listed +- Detailed component catalog organized by category +- Component design patterns (variant-based styling, composition, accessibility) +- Application components breakdown (core pages, layout, visualizations, features, utilities) +- Styling approach with Tailwind CSS 4.1 and theme system +- State management patterns (local state with hooks) +- API integration details +- Animation & motion (Framer Motion, Rive) +- User experience features (toasts, dark mode, responsive design, accessibility) +- Testing setup and tools +- Build & development commands +- Migration notes (old → new dependencies and components) +- Best practices for component creation, styling, type safety, performance +- Future considerations and potential improvements +- Known limitations +- Debugging tips + +## Current Project State + +### Branch + +- **Current**: `87-featfrontend-ui-redesign-migration-to-shadcnui` +- **Status**: Active development - UI redesign in progress + +### Frontend Stack (Updated) + +- React 18.3.1 (not 19) +- Vite 6 +- Tailwind CSS 4.1 +- shadcn/ui (47+ components) +- Radix UI primitives (30+ packages) +- Recharts (replaces ApexCharts) +- Framer Motion (via `motion` package) +- next-themes for theme management +- lucide-react for icons +- sonner for toast notifications +- React Hook Form for forms + +### Key Frontend Components (New) + +- Header, Footer (layout) +- LandingPage, DashboardPage (pages) +- CommitHeatmap, ActivityChart, CodeChurnChart (visualizations) +- FileDistributionChart, FileTypeList (file analysis) +- GraphViewTimeline, GitDiffViewer (git visualizations) +- AIInsights, PremiumFeatures (features) +- SettingsDrawer, NewsDrawer, InfoModal (UI features) +- 47+ shadcn/ui components in `components/ui/` + +### Removed/Replaced + +- `react-calendar-heatmap` → Custom heatmap component +- `apexcharts` → Recharts +- `react-select` → Radix UI Select +- Old components: ActivityHeatmap, CommitList, RepoInput + +## Notes for Future Work + +### AI Features + +- AIInsights component appears to be a placeholder for future AI-powered features +- No actual AI integration observed yet + +### Premium Features + +- PremiumFeatures component suggests future monetization/tiering +- Currently appears to be UI mockups + +### Testing + +- Frontend tests need to be updated to reflect new component structure +- shadcn/ui components come with accessibility built-in via Radix UI + +### Documentation Gaps + +- No comprehensive user guide for the new UI +- Migration guide from old to new UI not documented +- Component storybook or style guide not present + +## Serena Integration Status + +- ✅ Project indexed successfully (167 files: 159 TypeScript, 8 Bash) +- ✅ All 6 memory files updated/created +- ✅ Project activated in Serena +- ✅ Ready for development tasks + +## Recommendations + +1. **Update README.md** to reflect new UI and features +2. **Create component documentation** or Storybook for shadcn/ui customizations +3. **Update tests** to cover new component structure +4. **Document AI integration plans** if AIInsights is intended to be functional +5. **Create migration guide** for developers familiar with old UI +6. **Add screenshots** to documentation showcasing new UI +7. **Document theming system** for customization by users/developers diff --git a/.serena/memories/memory_update_summary_2026-01-05.md b/.serena/memories/memory_update_summary_2026-01-05.md new file mode 100644 index 00000000..bc7c8484 --- /dev/null +++ b/.serena/memories/memory_update_summary_2026-01-05.md @@ -0,0 +1,412 @@ +# GitRay Memory Update Summary + +## Date: January 5, 2026 + +## Context + +Memory files have been reviewed and updated to ensure alignment with the AGENTS.md file, which serves as the comprehensive project documentation. This update confirms the current state of the GitRay project following the Serena MCP server indexing. + +## Current Project State + +### Repository Information + +- **Repository**: `gitray` +- **Owner**: `jonasyr` +- **Current Branch**: `87-featfrontend-ui-redesign-migration-to-shadcnui` +- **Status**: Active development - shadcn/ui migration in progress + +### Project Structure Verified + +``` +gitray/ (Monorepo) +├── apps/ +│ ├── backend/ # Express 5.1.0 API server (Node.js 18+) +│ └── frontend/ # React 18.3.1 + Vite 6.3.5 application +├── packages/ +│ └── shared-types/ # Shared TypeScript definitions +├── scripts/ # Development and maintenance scripts +├── locks/ # Lock files for coordination +└── logs/ # Application logs +``` + +### Technology Stack Summary + +**Backend:** +- Node.js 18+ with TypeScript 5.7 +- Express 5.1.0 +- simple-git for Git operations +- ioredis for Redis 7 caching +- winston for logging +- prom-client for Prometheus metrics +- express-validator + Zod for validation +- helmet, cors, express-rate-limit for security + +**Frontend:** +- React 18.3.1 (NOT 19 - corrected) +- Vite 6.3.5 +- Tailwind CSS 4.1.7 +- shadcn/ui component library (Radix UI primitives) +- Recharts for visualizations +- motion (Framer Motion) for animations +- @rive-app/react-canvas for Rive animations +- axios for HTTP client +- React Hook Form for forms +- Sonner for toasts +- next-themes for theme management +- Lucide React for icons + +**Tooling:** +- pnpm 10.16.1 (workspace package manager) +- Vitest 3.2.3 (86.4% test coverage) +- ESLint 9 (flat config) +- Prettier 3 +- Husky + lint-staged for pre-commit hooks +- k6 for backend performance testing +- markdownlint-cli2 for Markdown linting + +## Key Features Verified + +### Core Functionality + +1. **Activity Heatmaps**: GitHub-style contribution calendars with customizable time periods +2. **Commit Analysis**: Detailed statistics and author breakdowns +3. **Code Churn Analysis**: Track code changes with risk level indicators +4. **File Type Distribution**: Analyze codebase composition +5. **Interactive Filtering**: Filter by authors, date ranges, and patterns +6. **Multi-tier Caching**: Redis + Memory + Disk (60%/25%/15% allocation) +7. **Streaming Support**: Handles 50k+ commits efficiently via Server-Sent Events +8. **Repository Coordination**: Prevents duplicate clones with reference counting + +### Architecture Highlights + +**Backend Services:** +- `gitService`: Git operations and streaming +- `cache`: Multi-tier hierarchical caching +- `repositoryCoordinator`: Shared repository management +- `repositoryCache`: Physical repository caching +- `fileAnalysisService`: File type distribution +- `repositorySummaryService`: Repository metadata +- `memoryPressureManager`: Memory monitoring (75%/85%/95% thresholds) +- `lockManager`: Distributed locking for coordination +- `metrics`: Prometheus metrics collection +- `logger`: Winston with daily rotation + +**Frontend Components:** +- Core Pages: `LandingPage`, `DashboardPage`, `Header`, `Footer` +- Visualizations: `CommitHeatmap`, `ActivityChart`, `CodeChurnChart`, `FileDistributionChart`, `FileTypeList`, `GraphViewTimeline`, `GitDiffViewer` +- Features: `AIInsights`, `PremiumFeatures`, `SettingsDrawer`, `NewsDrawer`, `InfoModal` +- UI Library: 47+ shadcn/ui components in `components/ui/` + +### API Endpoints + +- `POST /api/repositories` - Fetch commit list +- `GET /api/commits/heatmap` - Aggregated heatmap data +- `GET /api/commits/info` - Repository statistics +- `GET /api/commits/stream` - Stream commit data (SSE) +- `GET /api/repositories/churn` - Code churn analysis +- `GET /api/repositories/summary` - Repository metadata +- `GET /api/cache/stats` - Cache metrics +- `GET /health`, `/health/detailed`, `/health/memory` - Health checks +- `GET /metrics` - Prometheus metrics + +## Configuration + +### Environment Variables (Required) + +**Backend:** +- `PORT` (default: 3001) +- `CORS_ORIGIN` (default: http://localhost:5173) +- `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD` +- `CACHE_MAX_ENTRIES`, `CACHE_MEMORY_LIMIT_GB` +- `MEMORY_WARNING_THRESHOLD`, `MEMORY_CRITICAL_THRESHOLD` +- `STREAMING_ENABLED`, `STREAMING_COMMIT_THRESHOLD` +- `LOG_LEVEL`, `DEBUG_CACHE_LOGGING` + +**Frontend:** +- Vite automatically proxies API calls to backend during development + +## Development Workflow + +### Prerequisites + +- Node.js 18+ +- pnpm 10.16.1 +- Docker (for Redis) +- Git +- 4 GB RAM minimum (8 GB+ recommended for large repositories) +- 2 GB free disk space + +### Setup Commands + +```bash +# Clone and install +git clone +pnpm install + +# Build shared types (MUST run first) +pnpm run build:shared-types + +# Environment setup +cp apps/backend/.env.example apps/backend/.env +cp apps/frontend/.env.example apps/frontend/.env + +# Start development +pnpm start # Full setup: build types, start Redis, backend, frontend +pnpm dev # Build types + start all services with hot reload +pnpm quick # Quick start (frontend only, assumes backend running) +pnpm app # Interactive menu + +# Individual services +pnpm dev:backend # Backend only +pnpm dev:frontend # Frontend only + +# Build +pnpm build # Full build: types → backend → frontend +pnpm build:shared-types # Build types only +pnpm build:apps # Build backend + frontend + +# Clean and rebuild +pnpm clean # Remove build artifacts +pnpm rebuild # Clean + install + build + +# Environment management +pnpm env:status # Check service status +pnpm env:stop # Stop services +pnpm env:clean # Clean environment + +# Testing +pnpm test # All tests +pnpm test:frontend # Frontend tests only +pnpm test:backend # Backend tests only +pnpm test:watch # Watch mode +pnpm test:coverage # Generate coverage reports + +# Code quality +pnpm lint # Lint all files +pnpm lint:fix # Auto-fix issues +pnpm lint:md # Markdown linting +pnpm format # Format with Prettier +``` + +### Access Points + +- **Frontend**: http://localhost:5173 (Vite dev server) +- **Backend**: http://localhost:3001 +- **Health Checks**: `/health`, `/health/detailed`, `/health/memory` +- **Metrics**: `/metrics` (Prometheus format) + +## Code Quality System + +### Multi-Layer Quality Enforcement + +1. **ESLint**: TypeScript, React, hooks, a11y, SonarJS, Prettier integration +2. **Prettier**: Consistent code formatting +3. **markdownlint-cli2**: Markdown file quality +4. **Husky + lint-staged**: Pre-commit hooks (ESLint, Prettier, Markdown lint) +5. **TypeScript**: Strict type checking across all packages + +### Quality Standards + +- Enforce import order and consistent quoting +- Follow React's Rules of Hooks +- Accessibility guidelines (via Radix UI + ESLint a11y) +- Incremental linting with ESLint cache +- Staged file linting for performance +- ≥80% test coverage on critical paths + +## Performance Characteristics + +- **Small repos** (< 1k commits): ~500 ms +- **Medium repos** (1k-10k commits): ~2 s +- **Large repos** (10k-50k): ~10 s +- **Streaming mode**: for 50k+ commits +- **Cache hit rate**: > 80% typical + +## Important Patterns + +### Multi-Tier Caching + +Three-tier hierarchical cache with 60%/25%/15% memory allocation: +- **Tier 1**: Raw commits (60%, TTL 1h) +- **Tier 2**: Filtered commits (25%, TTL 30min) +- **Tier 3**: Aggregated data (15%, TTL 15min) + +Falls back: Redis → Memory → Disk. Supports transactional operations with rollback and ordered locking. + +### Repository Coordination + +Prevents duplicate clones with: +- Shared map of repository handles +- Reference counting for cleanup +- Operation coalescing for identical requests +- Distributed locking to prevent race conditions + +### Memory Pressure Management + +Four-tier system: +- **Normal** (< 75%): Allow all operations +- **Warning** (75-85%): Log warnings +- **Critical** (85-95%): Throttle requests, evict cache +- **Emergency** (> 95%): Block operations, aggressive eviction + +### Streaming Support + +For large repositories (50k+ commits): +- `/api/commits/stream` endpoint (Server-Sent Events) +- Batch processing (default: 1000 commits per batch) +- Memory-efficient for massive histories + +## Common Issues & Solutions + +### Build Issues + +- **Missing types**: Run `pnpm run build:shared-types` first +- **Redis not running**: Start Docker with Redis container +- **Port conflicts**: Adjust `PORT` in `.env` or stop conflicting services +- **TypeScript errors**: Run `tsc --noEmit` to check types + +### Performance Issues + +- **Slow queries**: Check cache hit rates at `/api/cache/stats` +- **Memory issues**: Monitor `/health/memory` endpoint +- **Large repos**: Streaming mode activates automatically at 50k+ commits + +### Development Issues + +- **Skipping shared types build**: Always build types before apps +- **Console.log in production**: Use winston logger instead +- **Type duplication**: Import from `@gitray/shared-types` + +## Testing Strategy + +### Unit Tests + +- Co-located with source files (`*.test.ts`/`*.spec.ts`) +- Vitest test runner +- Mock external dependencies (Redis, Git, filesystem) +- Target ≥80% coverage on critical paths + +### Integration Tests + +- Located in `apps/backend/__tests__/integration/` +- Test complete request/response cycles +- Verify caching behavior and coordination + +### Performance Tests + +- k6 load tests in `apps/backend/perf/` +- Scenarios: smoke, standard, stress +- Measure request latency, throughput, cache performance + +### Coverage Reports + +```bash +pnpm test:coverage # Full coverage pipeline +pnpm test:coverage:frontend # Frontend only +pnpm test:coverage:backend # Backend only +``` + +Reports stored in `coverage/` and `.nyc_output/`. + +## Observability + +### Metrics (Prometheus) + +- HTTP request count, duration, status codes +- Cache hit/miss rates per tier +- Memory usage and pressure levels +- Repository coordination stats +- Git operation durations + +### Logging (Winston) + +- Structured JSON logs +- Log levels: error, warn, info, debug +- Daily rotation with compression +- Request correlation IDs +- Contextual metadata + +### Health Checks + +- **Basic** (`/health`): Service up/down +- **Detailed** (`/health/detailed`): Redis, memory, cache health +- **Memory** (`/health/memory`): Current pressure level and thresholds + +## Security Measures + +- **Helmet**: Security headers (CSP, HSTS, etc.) +- **CORS**: Restricted origins +- **Rate Limiting**: 100 requests per 15 minutes per IP +- **Input Validation**: express-validator + Zod schemas +- **URL Security**: Blocks malicious repository URLs (file://, javascript:, etc.) +- **Content-Type Enforcement**: Strict JSON for POST/PUT +- **Admin Auth**: Protected admin endpoints + +## Serena Integration Status + +- ✅ Project indexed: 167 files (159 TypeScript, 8 Bash) +- ✅ All memory files created/updated +- ✅ Project activated in Serena +- ✅ Ready for development tasks + +## Memory Files Current State + +1. **project_overview.md**: High-level project summary, features, tech stack +2. **architecture_overview.md**: Detailed architecture, services, caching, coordination +3. **codebase_structure.md**: Directory layout, file locations, component organization +4. **coding_standards.md**: Naming conventions, patterns, best practices +5. **frontend_architecture_detailed.md**: shadcn/ui migration, component catalog, styling +6. **task_completion_checklist.md**: Checklists for common development tasks +7. **suggested_commands.md**: Common command reference +8. **memory_update_summary_2026-01-03.md**: Previous update summary +9. **memory_update_summary_2026-01-05.md**: This file + +## Next Steps + +### Documentation + +- ✅ All memory files aligned with AGENTS.md +- ⏳ Consider updating README.md to match AGENTS.md details +- ⏳ Add screenshots/videos of new UI to documentation +- ⏳ Create component Storybook for shadcn/ui customizations + +### Testing + +- ⏳ Update frontend tests for new component structure +- ⏳ Add tests for shadcn/ui component customizations +- ⏳ Verify test coverage remains ≥80% + +### Features + +- ⏳ Complete shadcn/ui migration (branch 87) +- ⏳ Implement AI integration for AIInsights component +- ⏳ Define premium features for monetization + +### Infrastructure + +- ⏳ Consider Redis Cluster for horizontal scaling +- ⏳ Evaluate shared filesystem for multi-instance deployments +- ⏳ Plan database for persistent metadata (currently cache-only) + +## Notes for AI Assistants + +When working with this project: + +1. **Always build shared types first**: `pnpm run build:shared-types` +2. **Use winston logger**: Never use `console.log` in production code +3. **Import types from shared package**: `@gitray/shared-types` +4. **Follow shadcn/ui patterns**: Use `cn()` utility, component composition +5. **Respect memory pressure**: Check thresholds before heavy operations +6. **Use repository coordinator**: Don't clone repositories directly +7. **Multi-tier caching**: Understand cache hierarchy for optimization +8. **Streaming for large repos**: Auto-enabled at 50k+ commits +9. **Test coverage**: Maintain ≥80% on critical paths +10. **Code quality**: ESLint, Prettier, TypeScript strict mode enforced + +## Reference Documentation + +- **AGENTS.md**: Comprehensive project documentation (primary reference) +- **README.md**: User-facing documentation +- **CLAUDE.md**: Guidelines for Claude AI assistant +- **GEMINI.md**: Guidelines for Gemini AI assistant +- **Strategy.md**: Project strategy and roadmap diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md index 1fc6e902..a5bd6f0b 100644 --- a/.serena/memories/project_overview.md +++ b/.serena/memories/project_overview.md @@ -1,11 +1,13 @@ # GitRay - Project Overview ## Purpose + GitRay is a professional Git visualization tool that transforms repository commit history into beautiful, interactive heatmaps and activity calendars. It provides deep insights into development patterns and team collaboration. ## Key Features + - **Activity Heatmaps**: GitHub-style contribution calendars with customizable time periods -- **Commit Analysis**: Detailed commit statistics and author breakdowns +- **Commit Analysis**: Detailed commit statistics and author breakdowns - **Code Churn Analysis**: Track code changes and stability patterns with risk level indicators - **Interactive Filtering**: Filter by authors, date ranges, and commit patterns - **Multi-tier Caching**: Intelligent caching system with Redis, disk, and memory tiers @@ -15,40 +17,53 @@ GitRay is a professional Git visualization tool that transforms repository commi ## Technology Stack ### Backend + - **Runtime**: Node.js 18+ with TypeScript 5.7 -- **Framework**: Express 5 +- **Framework**: Express 5.1.0 - **Git Operations**: simple-git - **Caching**: Redis 7 (via ioredis) - **Logging**: Winston with daily rotate file - **Metrics**: Prometheus (prom-client) - **Validation**: Express-validator, Zod - **Security**: Helmet, CORS, express-rate-limit +- **Date Utilities**: date-fns for date manipulation -### Frontend -- **Framework**: React 19 with TypeScript 5.7 -- **Build Tool**: Vite 6 -- **Styling**: Tailwind CSS 4 -- **Visualizations**: react-calendar-heatmap, ApexCharts -- **Animations**: Rive (@rive-app/react-canvas) +### Frontend + +- **Framework**: React 18.3.1 with TypeScript 5.7 +- **Build Tool**: Vite 6.3.5 +- **Styling**: Tailwind CSS 4.1.7 +- **UI Components**: shadcn/ui (Radix UI primitives + CVA) +- **Visualizations**: Recharts for charts, custom heatmap components +- **Animations**: Rive (@rive-app/react-canvas), motion (Framer Motion) - **HTTP Client**: Axios -- **Date Handling**: date-fns +- **Forms**: React Hook Form +- **Toast Notifications**: Sonner +- **Themes**: next-themes for dark/light mode +- **Icons**: lucide-react +- **Carousel**: Embla Carousel for carousels +- **Command Menu**: cmdk for command palette ### Shared Infrastructure + - **Package Manager**: pnpm 10.16.1 (workspaces) - **Testing**: Vitest 3.2.3 (86.4% coverage) - **Performance Testing**: k6 for backend load testing - **Linting**: ESLint 9 (flat config) - **Formatting**: Prettier 3 +- **Markdown Linting**: markdownlint-cli2 - **Git Hooks**: Husky + lint-staged -- **CI/CD**: GitHub Actions (assumed from .github directory) +- **CI/CD**: GitHub Actions ## Monorepo Structure + - **apps/backend**: Express API server -- **apps/frontend**: React UI application +- **apps/frontend**: React UI application - **packages/shared-types**: Shared TypeScript types and error classes - **scripts/**: Development and maintenance scripts ## Architecture Principles + - **Strict TypeScript**: No `any` types, strict type checking enabled - **Monorepo with Project References**: TypeScript project references for incremental builds - **Shared Type Safety**: All types exported from @gitray/shared-types @@ -59,8 +74,16 @@ GitRay is a professional Git visualization tool that transforms repository commi - **Performance Optimized**: Multi-tier caching, streaming, memory pressure management ## Current Development Branch -Main development branch: `dev` -Current working branch: `120-enhancementscopebackend-refactor-old-routes-to-use-unified-cache-service-retain-redis-remove-manual-caching` + +**Main development branch**: `dev` +**Current working branch**: `87-featfrontend-ui-redesign-migration-to-shadcnui` +**Repository**: `gitray` (owner: `jonasyr`) +**Recent major changes**: Complete frontend UI redesign with shadcn/ui component library + +## Current Date + +As of January 5, 2026, the project is actively maintained and in production-ready state with ongoing feature development. ## License + ISC License diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md index ca7d1247..450ee581 100644 --- a/.serena/memories/suggested_commands.md +++ b/.serena/memories/suggested_commands.md @@ -1,236 +1,672 @@ -# GitRay - Suggested Development Commands +# GitRay - Suggested Commands Reference -## Essential Commands (Most Commonly Used) +## Quick Reference + +This file contains commonly used commands for GitRay development. All commands should be run from the project root unless otherwise specified. + +## Setup & Installation -### Development Environment ```bash -pnpm app # Interactive development environment manager -pnpm dev # Build shared types + start all services with hot reload -pnpm dev:frontend # Start frontend only (Vite on port 5173) -pnpm dev:backend # Start backend only (Express on port 3001) +# Clone repository +git clone +cd gitray + +# Install all dependencies (workspace-aware) +pnpm install + +# Build shared types (MUST run before apps) +pnpm run build:shared-types + +# Setup environment files +cp apps/backend/.env.example apps/backend/.env +cp apps/frontend/.env.example apps/frontend/.env +# Edit .env files with your configuration ``` -### Testing +## Development + +### Starting Services + ```bash -pnpm test # Run all tests across workspace -pnpm test:frontend # Frontend tests only -pnpm test:backend # Backend tests only -pnpm test:watch # Watch mode for development -pnpm test:watch:changed # Watch changed files only -pnpm test:coverage # Generate combined coverage report (86.4%+) -pnpm test:ui # Open Vitest UI +# Interactive menu to start services +pnpm app + +# Full development setup (build types, start Redis, backend, frontend) +pnpm start + +# Quick start (frontend only, assumes backend is running) +pnpm quick + +# Build types and start all services with hot reload +pnpm dev + +# Start individual services +pnpm dev:frontend # Frontend only (Vite dev server) +pnpm dev:backend # Backend only (Express with nodemon) ``` -### Code Quality +### Environment Management + ```bash -pnpm lint # Run ESLint on all code -pnpm lint:fix # Auto-fix linting issues -pnpm lint:md # Lint markdown files -pnpm format # Format all files with Prettier +# Check status of services +pnpm env:status + +# Stop all services +pnpm env:stop + +# Clean environment (remove containers, temp files) +pnpm env:clean ``` -### Building +## Building + ```bash -pnpm build # Build everything (shared-types → backend → frontend) -pnpm build:shared-types # Build shared types only (REQUIRED before apps) -pnpm build:apps # Build backend + frontend +# Full build pipeline: shared-types → backend → frontend +pnpm build + +# Build only shared types package +pnpm build:shared-types + +# Build backend and frontend (assumes types are built) +pnpm build:apps + +# Clean build artifacts and caches +pnpm clean + +# Clean install and build from scratch +pnpm rebuild ``` -### Environment Management +## Testing + +### Running Tests + +```bash +# Run all tests across all packages +pnpm test + +# Run tests for specific package +pnpm test:frontend +pnpm test:backend + +# Watch mode for all tests +pnpm test:watch + +# Watch mode for changed files only +pnpm test:watch:changed + +# Launch Vitest UI for interactive debugging +pnpm test:ui +``` + +### Coverage Reports + +```bash +# Full coverage pipeline (clean → test → merge → report) +pnpm test:coverage + +# Coverage for individual packages +pnpm test:coverage:frontend +pnpm test:coverage:backend +``` + +### Performance Testing + +```bash +# Run standard k6 load test (backend only) +pnpm --filter backend test:perf + +# Light load smoke test +pnpm --filter backend test:perf:smoke + +# Heavy load stress test +pnpm --filter backend test:perf:stress +``` + +## Code Quality + +### Linting + ```bash -pnpm env:status # Show service status (frontend, backend, Redis) -pnpm env:stop # Stop all services -pnpm env:clean # Clean environment (stop services + clean cache) +# Lint all files +pnpm lint + +# Auto-fix linting issues +pnpm lint:fix + +# Lint Markdown files only +pnpm lint:md + +# Format all files with Prettier +pnpm format +``` + +### Type Checking + +```bash +# Type check all packages +pnpm type-check + +# Type check backend only +cd apps/backend && pnpm tsc --noEmit + +# Type check frontend only +cd apps/frontend && pnpm tsc --noEmit +``` + +## Package Management + +```bash +# Add dependency to workspace root +pnpm add -w + +# Add dependency to specific workspace +pnpm --filter backend add +pnpm --filter frontend add +pnpm --filter @gitray/shared-types add + +# Add dev dependency +pnpm --filter backend add -D + +# Update all dependencies +pnpm update + +# Update specific package +pnpm update + +# List outdated packages +pnpm outdated + +# Remove dependency +pnpm --filter backend remove ``` -### Cleanup +## Git & Version Control + ```bash -pnpm clean # Clean dist + cache + node_modules -pnpm clean:dist # Remove build artifacts only -pnpm clean:cache # Remove Vite/ESLint/nyc caches -pnpm clean:node_modules # Remove all node_modules (deep clean) -pnpm clean:all # Deep clean including logs -pnpm rebuild # Full clean + install + build +# Check current branch +git branch --show-current + +# Create feature branch +git checkout -b feat/your-feature-name + +# Create bugfix branch +git checkout -b fix/issue-description + +# Commit with conventional commit message +git commit -m "feat: add new feature" +git commit -m "fix: resolve bug in cache manager" +git commit -m "refactor: extract route helpers" +git commit -m "test: add integration tests" +git commit -m "docs: update API documentation" +git commit -m "perf: optimize commit aggregation" +git commit -m "style: format code with prettier" +git commit -m "chore: update dependencies" + +# Push branch +git push origin + +# Pull latest changes +git pull origin + +# View recent commits +git log --oneline -n 10 + +# Check diff +git diff +git diff --staged ``` -## Installation & Setup +## Docker & Redis ```bash -# Initial setup -pnpm install # Install all workspace dependencies +# Start Redis container (development) +docker run -d --name gitray-redis -p 6379:6379 redis:7-alpine + +# Stop Redis container +docker stop gitray-redis + +# Remove Redis container +docker rm gitray-redis + +# View Redis logs +docker logs gitray-redis + +# Connect to Redis CLI +docker exec -it gitray-redis redis-cli + +# Check Redis memory usage +docker exec -it gitray-redis redis-cli INFO memory +``` + +## Backend Development + +### Running Backend Services -# Start Redis (via Docker) -docker run --name gitray-redis -d -p 6379:6379 redis:7-alpine +```bash +# Start backend in development mode +cd apps/backend +pnpm dev -# Check Redis status -docker ps | grep redis -docker restart gitray-redis # If needed +# Start backend in production mode +cd apps/backend +pnpm start -# Build before first run +# Build backend +cd apps/backend pnpm build ``` -## Application Management Scripts +### Backend Testing & Debugging ```bash -pnpm start # Full development setup (via scripts/start.sh) -pnpm quick # Frontend-only quick start +# Run backend unit tests +cd apps/backend +pnpm test + +# Run backend integration tests +cd apps/backend +pnpm test:integration + +# View backend logs +tail -f apps/backend/logs/combined.log +tail -f apps/backend/logs/error.log + +# Check backend health +curl http://localhost:3001/health +curl http://localhost:3001/health/detailed +curl http://localhost:3001/health/memory + +# Check cache statistics +curl http://localhost:3001/api/cache/stats + +# View Prometheus metrics +curl http://localhost:3001/metrics +``` + +### API Testing + +```bash +# Test repository endpoint +curl -X POST http://localhost:3001/api/repositories \ + -H "Content-Type: application/json" \ + -d '{"url": "https://github.com/user/repo"}' + +# Test heatmap endpoint +curl "http://localhost:3001/api/commits/heatmap?url=https://github.com/user/repo" + +# Test streaming endpoint +curl "http://localhost:3001/api/commits/stream?url=https://github.com/user/repo" + +# Test churn analysis +curl "http://localhost:3001/api/repositories/churn?url=https://github.com/user/repo" + +# Test repository summary +curl "http://localhost:3001/api/repositories/summary?url=https://github.com/user/repo" +``` + +### Backend Scripts + +```bash +# Run manual XSS verification +cd apps/backend/__tests__ +bash manual-xss-verification.sh + +# Run end-to-end cache test +cd scripts +bash end2end_cache_test.sh + +# Run complete API test +cd scripts +bash test_api_complete.sh + +# Verify health endpoints +cd scripts +bash verify-health.sh + +# Verify SSRF protection +cd scripts +bash verify-ssrf-protection.sh ``` -## Testing Variants +## Frontend Development + +### Running Frontend -### Backend-Specific ```bash -pnpm --filter backend test # Backend unit tests -pnpm --filter backend test:coverage # Backend coverage -pnpm --filter backend test:perf # k6 performance tests -pnpm --filter backend test:perf:smoke # Quick smoke test (30s) -pnpm --filter backend test:perf:stress # Stress test (2x load) +# Start frontend dev server +cd apps/frontend +pnpm dev + +# Build frontend for production +cd apps/frontend +pnpm build + +# Preview production build +cd apps/frontend +pnpm preview ``` -### Frontend-Specific +### Frontend Testing + ```bash -pnpm --filter frontend test # Frontend unit tests -pnpm --filter frontend test:coverage # Frontend coverage +# Run frontend tests +cd apps/frontend +pnpm test + +# Run frontend tests in watch mode +cd apps/frontend +pnpm test:watch + +# Run frontend tests with UI +cd apps/frontend +pnpm test:ui ``` -### Coverage Details +## Shared Types Development + ```bash -pnpm test:coverage:frontend # Frontend coverage (apps/frontend/coverage) -pnpm test:coverage:backend # Backend coverage (apps/backend/coverage) -pnpm test:coverage:merge # Merge coverage reports -pnpm test:coverage:report # Generate HTML/LCOV/text reports -pnpm clean:coverage-output # Clean coverage artifacts +# Build shared types +cd packages/shared-types +pnpm build + +# Watch mode for shared types (rebuild on change) +cd packages/shared-types +pnpm build --watch + +# Type check shared types +cd packages/shared-types +pnpm tsc --noEmit ``` -## Git Hooks (Automated via Husky) +## Troubleshooting -### Pre-commit (Automated) -- ESLint auto-fix on `*.{ts,tsx,js,jsx}` -- Prettier format on code files -- Markdownlint on `*.md` files -- Prettier format on `*.{json,yml,yaml}` +### Common Issues -### Manual Hook Setup ```bash -pnpm prepare # Install Husky hooks +# Shared types not found - rebuild types +pnpm run build:shared-types + +# Redis connection failed - start Redis +docker run -d --name gitray-redis -p 6379:6379 redis:7-alpine + +# Port already in use - check and kill process +lsof -ti:3001 | xargs kill -9 # Backend port +lsof -ti:5173 | xargs kill -9 # Frontend port + +# Clear all node_modules and reinstall +pnpm clean +rm -rf node_modules apps/*/node_modules packages/*/node_modules +pnpm install + +# Clear TypeScript build cache +find . -name "*.tsbuildinfo" -delete + +# Clear Vite cache +rm -rf apps/frontend/.vite + +# Clear ESLint cache +rm .eslintcache + +# Clear all caches +pnpm clean +rm -rf .vite .eslintcache .nyc_output coverage + +# Reset everything +pnpm clean +rm -rf node_modules apps/*/node_modules packages/*/node_modules +rm -rf apps/backend/dist apps/frontend/dist packages/shared-types/dist +rm -rf .vite .eslintcache .nyc_output coverage +pnpm install +pnpm build ``` -## Debugging & Troubleshooting +### Debugging Commands ```bash -# Check what's using a port -lsof -i :3001 # Backend port -lsof -i :5173 # Frontend port -lsof -i :6379 # Redis port +# Check Node.js version +node --version + +# Check pnpm version +pnpm --version + +# Check TypeScript version +pnpm tsc --version + +# List all workspace packages +pnpm list --depth 0 + +# Check for circular dependencies +pnpm why -# Kill process by PID -kill -9 +# Verify workspace configuration +pnpm list --workspace-root + +# Check environment variables (backend) +cd apps/backend && cat .env + +# Check running processes +ps aux | grep node # Check Redis connection -docker logs gitray-redis +redis-cli ping -# View application logs -tail -f logs/combined.log -tail -f logs/error.log +# Monitor memory usage +watch -n 1 free -h -# Memory and system status -pnpm env:status +# Monitor disk space +df -h ``` -## Performance Testing (k6) +## Maintenance + +### Cache Management ```bash -# Standard load test -pnpm --filter backend test:perf +# Clear Redis cache +docker exec -it gitray-redis redis-cli FLUSHALL -# Quick smoke test (5 VUs, 30 seconds) -pnpm --filter backend test:perf:smoke +# Clear disk cache +rm -rf apps/backend/cache/* -# Stress test (2x multiplier) -pnpm --filter backend test:perf:stress +# Clear repository locks +rm -rf apps/backend/locks/* -# Custom k6 test -cd apps/backend -k6 run --vus 10 --duration 60s perf/load-test.ts +# Clear logs +rm -rf apps/backend/logs/* +rm -rf logs/* ``` -## Useful System Commands (Linux) +### Log Management -### File Operations ```bash -ls -la # List files with details -find . -name "*.ts" # Find TypeScript files -grep -r "pattern" src/ # Search in files +# View combined logs +tail -f apps/backend/logs/combined.log + +# View error logs only +tail -f apps/backend/logs/error.log + +# Search logs for errors +grep -i error apps/backend/logs/combined.log + +# Count errors in logs +grep -c ERROR apps/backend/logs/combined.log + +# View last 100 log entries +tail -n 100 apps/backend/logs/combined.log + +# Follow logs with grep filter +tail -f apps/backend/logs/combined.log | grep "cache" ``` -### Git Operations +### Performance Monitoring + ```bash -git status # Current branch status -git log --oneline -10 # Recent commits -git diff # View changes -git checkout dev # Switch to dev branch +# Monitor backend metrics +watch -n 5 curl -s http://localhost:3001/metrics + +# Monitor health status +watch -n 5 curl -s http://localhost:3001/health/detailed + +# Monitor memory pressure +watch -n 5 curl -s http://localhost:3001/health/memory + +# Monitor cache stats +watch -n 5 curl -s http://localhost:3001/api/cache/stats + +# Node.js heap snapshot (requires --inspect) +node --inspect apps/backend/dist/index.js +# Then in Chrome: chrome://inspect ``` -## Build Order (IMPORTANT!) +## Git Workflow + +### Feature Development + +```bash +# Start new feature +git checkout dev +git pull origin dev +git checkout -b feat/feature-name + +# Make changes and commit +git add . +git commit -m "feat: add feature description" -**Always build in this order:** -1. `pnpm build:shared-types` (or `pnpm --filter @gitray/shared-types build`) -2. `pnpm build:apps` (or manually: backend → frontend) +# Push feature branch +git push origin feat/feature-name -**Why?** Backend and frontend depend on built types from `@gitray/shared-types`. +# Create pull request (via GitHub UI) +``` -## Environment Variables +### Bug Fixes -Create `.env` in project root: ```bash -# Server -PORT=3001 -CORS_ORIGIN=http://localhost:5173 +# Start bug fix +git checkout dev +git pull origin dev +git checkout -b fix/bug-description -# Redis -REDIS_HOST=localhost -REDIS_PORT=6379 +# Make changes and commit +git add . +git commit -m "fix: resolve bug description" -# Caching -CACHE_ENABLE_REDIS=true -CACHE_ENABLE_DISK=true +# Push fix branch +git push origin fix/bug-description -# Development -NODE_ENV=development -LOG_LEVEL=info -DEBUG_CACHE_LOGGING=false +# Create pull request (via GitHub UI) ``` -## Quick Reference: Common Workflows +### Updating Branch -### Starting Development ```bash -pnpm app # Interactive menu -# OR -pnpm dev # Direct start (recommended) +# Update feature branch with latest dev +git checkout dev +git pull origin dev +git checkout feat/feature-name +git merge dev + +# Or use rebase (cleaner history) +git checkout feat/feature-name +git rebase dev + +# Resolve conflicts if any +git add . +git rebase --continue + +# Force push if rebased +git push origin feat/feature-name --force-with-lease ``` -### Before Committing +## CI/CD + +### Pre-commit Checks (Run Locally) + ```bash -pnpm lint # Check for issues -pnpm test # Run tests -pnpm format # Format code +# Run all pre-commit checks manually +pnpm lint +pnpm format +pnpm lint:md +pnpm test +pnpm build + +# Simulate CI pipeline locally +pnpm lint && pnpm format && pnpm test && pnpm build ``` -### After Pulling Changes +### GitHub Actions (Automated) + ```bash -pnpm install # Update dependencies -pnpm build:shared-types # Rebuild shared types +# View workflow status +git push origin +# Check GitHub Actions tab in repository + +# Re-run failed workflows (via GitHub UI) +# Navigate to Actions → Select workflow → Re-run jobs ``` -### Adding New Dependencies +## Production Deployment + +### Build for Production + ```bash -# Root level -pnpm add -D +# Full production build +pnpm clean +pnpm install --frozen-lockfile +pnpm build -# Specific workspace -pnpm --filter backend add -pnpm --filter frontend add -pnpm --filter @gitray/shared-types add +# Verify build artifacts +ls -lh apps/backend/dist +ls -lh apps/frontend/dist +ls -lh packages/shared-types/dist ``` + +### Environment Setup + +```bash +# Set production environment variables +export NODE_ENV=production +export PORT=3001 +export REDIS_HOST=production-redis-host +export REDIS_PORT=6379 +# ... other production variables + +# Or use .env.production files +cp apps/backend/.env.example apps/backend/.env.production +# Edit with production values +``` + +### Start Production Services + +```bash +# Start backend in production mode +cd apps/backend +NODE_ENV=production node dist/index.js + +# Or use PM2 for process management +pm2 start apps/backend/dist/index.js --name gitray-backend + +# Serve frontend static files (via Nginx, Apache, or Node.js server) +# Frontend dist files in: apps/frontend/dist +``` + +## Useful Aliases (Add to ~/.bashrc or ~/.zshrc) + +```bash +# GitRay aliases +alias gitray-dev="cd ~/gitray && pnpm dev" +alias gitray-test="cd ~/gitray && pnpm test" +alias gitray-build="cd ~/gitray && pnpm build" +alias gitray-clean="cd ~/gitray && pnpm clean && pnpm install" +alias gitray-logs="tail -f ~/gitray/apps/backend/logs/combined.log" +alias gitray-redis="docker exec -it gitray-redis redis-cli" +alias gitray-health="curl -s http://localhost:3001/health/detailed | jq" +``` + +## References + +- **AGENTS.md**: Comprehensive project documentation +- **README.md**: User-facing documentation +- **package.json**: Scripts and dependencies +- **apps/backend/src/config.ts**: Backend configuration +- **scripts/**: Development scripts diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md index 5af2af20..db030ab7 100644 --- a/.serena/memories/task_completion_checklist.md +++ b/.serena/memories/task_completion_checklist.md @@ -3,6 +3,7 @@ ## Before Committing Code ### 1. Code Quality Checks + ```bash # Run linting pnpm lint @@ -15,6 +16,7 @@ pnpm lint:md ``` ### 2. Run Tests + ```bash # Run all tests pnpm test @@ -28,6 +30,7 @@ pnpm test:coverage ``` ### 3. Build Validation + ```bash # Ensure clean build pnpm build @@ -38,6 +41,7 @@ pnpm build:apps # If app code changed ``` ### 4. Manual Testing + - [ ] Test the feature/fix in the running application - [ ] Verify frontend behavior (`pnpm dev:frontend`) - [ ] Verify backend endpoints (`pnpm dev:backend`) @@ -45,6 +49,7 @@ pnpm build:apps # If app code changed - [ ] Check backend logs for errors ### 5. Type Safety + - [ ] No TypeScript errors (`pnpm build`) - [ ] No use of `any` without justification - [ ] Proper types imported from `@gitray/shared-types` @@ -53,6 +58,7 @@ pnpm build:apps # If app code changed ## Code Review Self-Checklist ### General + - [ ] Code follows project conventions (see `coding_standards.md`) - [ ] No debug code (`console.log`, commented code, etc.) - [ ] Descriptive variable and function names @@ -60,19 +66,27 @@ pnpm build:apps # If app code changed - [ ] No duplicate code (DRY principle) ### TypeScript + - [ ] Strict type checking passes - [ ] No `any` types without justification - [ ] Proper error handling with typed error classes - [ ] Async functions use `async/await`, not promise chains ### React Components (Frontend) + - [ ] Functional components with proper typing - [ ] Hooks follow Rules of Hooks - [ ] Proper key props for lists - [ ] No inline functions in render (performance) - [ ] Tailwind CSS for styling (avoid inline styles) +- [ ] Use shadcn/ui components from `components/ui/` when available +- [ ] Use `cn()` utility for conditional className merging +- [ ] Theme colors via CSS variables (not hardcoded colors) +- [ ] Dark mode classes where appropriate (`dark:` prefix) +- [ ] Accessibility maintained (keyboard navigation, ARIA labels) ### Backend Routes & Services + - [ ] Proper error handling with try/catch - [ ] Use Winston logger, not `console.log` - [ ] Input validation with express-validator or Zod @@ -80,6 +94,7 @@ pnpm build:apps # If app code changed - [ ] Route helpers used for consistency (`setupRouteRequest`, etc.) ### Testing + - [ ] New features have tests - [ ] Bug fixes have regression tests - [ ] Test coverage maintained (≥80%) @@ -87,6 +102,7 @@ pnpm build:apps # If app code changed - [ ] Mocks are used for external dependencies ### Documentation + - [ ] README updated if user-facing changes - [ ] CLAUDE.md updated if guidelines change - [ ] JSDoc comments for public APIs @@ -97,11 +113,13 @@ pnpm build:apps # If app code changed If you modified `packages/shared-types/src/index.ts`: 1. **Rebuild shared types** + ```bash pnpm build:shared-types ``` 2. **Update imports** in backend and frontend + ```typescript import { YourNewType } from '@gitray/shared-types'; ``` @@ -118,11 +136,13 @@ If you modified `packages/shared-types/src/index.ts`: ## When Adding Dependencies ### Root dependencies + ```bash pnpm add -D # Dev dependency at root ``` ### Workspace dependencies + ```bash pnpm --filter backend add pnpm --filter frontend add @@ -130,6 +150,7 @@ pnpm --filter @gitray/shared-types add ``` ### After adding dependencies + - [ ] Verify `pnpm-lock.yaml` is updated - [ ] Test that build still works - [ ] Update README if dependency is significant @@ -137,6 +158,7 @@ pnpm --filter @gitray/shared-types add ## When Creating a Pull Request ### 1. Ensure Clean Branch + ```bash # Sync with main development branch git checkout dev @@ -148,7 +170,9 @@ git rebase dev ``` ### 2. Commit Message + Follow Conventional Commits format: + ``` feat: add code churn risk indicators fix: resolve cache eviction race condition @@ -159,11 +183,14 @@ perf: optimize commit aggregation algorithm ``` ### 3. PR Description Template + ```markdown ## Description + Brief description of changes ## Type of Change + - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) @@ -171,11 +198,13 @@ Brief description of changes - [ ] Documentation update ## Testing + - [ ] Tests pass locally - [ ] Added tests for new features - [ ] Coverage maintained ≥80% ## Checklist + - [ ] Code follows project conventions - [ ] Self-reviewed the code - [ ] Commented complex logic @@ -217,6 +246,19 @@ When implementing features, consider: 9. **Test end-to-end** 10. **Update documentation** (README, API docs) +### Adding a New Frontend Component + +1. **Check shadcn/ui catalog** for existing components to reuse +2. **Create component** in `apps/frontend/src/components/.tsx` +3. **Import shadcn/ui primitives** from `@/components/ui/` +4. **Use Tailwind utilities** for styling +5. **Apply theme colors** via CSS variables +6. **Add dark mode support** with `dark:` classes +7. **Ensure accessibility** (keyboard nav, ARIA, focus management) +8. **Add proper TypeScript types** for props +9. **Test component** in isolation +10. **Integrate into parent component/page** + ### Fixing a Bug 1. **Write failing test** that reproduces the bug @@ -238,12 +280,14 @@ When implementing features, consider: ## Environment-Specific Checks ### Development + - [ ] Redis running (`docker ps | grep redis`) - [ ] Ports available (3001, 5173, 6379) - [ ] Environment variables set (`.env` file) - [ ] Logs accessible (`logs/` directory) ### Before Production Deploy (Future) + - [ ] All tests pass in CI - [ ] Coverage ≥80% - [ ] No TypeScript errors @@ -276,6 +320,7 @@ git push origin your-branch-name ## Automated Checks (Pre-commit Hook) The project uses Husky with lint-staged for automatic checks: + - **TypeScript/JavaScript**: ESLint auto-fix + Prettier - **Markdown**: Markdownlint - **JSON/YAML**: Prettier formatting @@ -285,21 +330,25 @@ These run automatically on `git commit`. If they fail, fix issues before committ ## Quick Reference ### Validation Pipeline + ``` Code → Lint → Format → Test → Build → Manual Test → Commit ``` ### Must-Run Before Commit + ```bash pnpm lint && pnpm test && pnpm build ``` ### If Shared Types Changed + ```bash pnpm build:shared-types && pnpm test ``` ### If Unsure, Run Everything + ```bash pnpm rebuild && pnpm lint && pnpm test ``` diff --git a/AGENTS.md b/AGENTS.md index fa28149f..bfeafccf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,5 @@ + # GitRay GitRay is a production-ready Git repository analysis and visualization platform that transforms commit history into interactive visualizations such as heatmaps, commit statistics, code churn analysis and time-series aggregations. @@ -149,14 +150,21 @@ scripts/ **Frontend:** -- React 19.1.0 +- React 18.3.1 - Vite 6.3.5 - Tailwind CSS 4.1.7 - axios for HTTP calls -- ApexCharts and react-apexcharts for charts -- react-calendar-heatmap for heatmaps +- Recharts for data visualizations and charts +- shadcn/ui component library (built on Radix UI primitives) +- Radix UI components (Accordion, Dialog, Select, Dropdown, Tabs, Tooltip, etc.) +- Lucide React for icons - @rive-app/react-canvas for animations -- react-select for dropdowns +- React Hook Form for form management +- Sonner for toast notifications +- next-themes for theme management +- Embla Carousel for carousels +- cmdk for command menu +- class-variance-authority (CVA) for component variants **Shared Types:** diff --git a/CLAUDE.md b/CLAUDE.md index 91e411ae..b7ef4f38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,11 @@ + # CLAUDE.md Guidance for Claude when contributing to the GitRay monorepo. Follow these rules before any other doc unless overridden by a nearer `AGENTS.md`. ## Project Snapshot + - **Monorepo**: pnpm workspaces with TypeScript project references - **Frontend**: React 19 + Vite 6 + Tailwind CSS 4 - **Backend**: Express 5 with simple-git, Redis caching, Prometheus metrics @@ -11,6 +13,7 @@ Guidance for Claude when contributing to the GitRay monorepo. Follow these rules - **Testing**: Vitest across apps; k6 for backend perf ## Repo Layout (high level) + ``` apps/ frontend/ # React UI, Vite, Tailwind, API clients @@ -19,9 +22,11 @@ packages/ shared-types/ # Reusable TypeScript types and schemas scripts/ # Dev/start/maintenance scripts ``` + Keep new files inside these roots; never add code under build artifacts (`dist/`, `.next/`, `coverage/`, `node_modules/`). ## Daily Commands + ```bash pnpm install # Install workspace deps pnpm dev # Start frontend+backend with hot reload (builds shared-types) @@ -33,9 +38,11 @@ pnpm lint # ESLint flat config pnpm lint:md # Markdown lint pnpm format # Prettier format ``` + Build order matters: run `pnpm build:shared-types` before isolated backend/frontend builds. ## Code Standards (enforceable) + - TypeScript **strict** everywhere; avoid `any` and implicit `any`. - React components must be functional with hooks; follow Rules of Hooks. - Use provided logger (winston) instead of `console.log` in runtime code. @@ -46,6 +53,7 @@ Build order matters: run `pnpm build:shared-types` before isolated backend/front - Prefer named exports; avoid default exports for components and utilities. ### Naming + - Components & types/interfaces: **PascalCase** (`CommitHeatmap`, `CommitHeatmapProps`) - Hooks: `use` + camelCase (`useCommitFilters`) - Utilities/functions: `camelCase` @@ -53,10 +61,12 @@ Build order matters: run `pnpm build:shared-types` before isolated backend/front - Environment vars: `UPPER_SNAKE_CASE` ### Async & Error Handling + - Use `async/await` with try/catch at call boundaries; wrap errors with context and rethrow typed errors. - Avoid promise chains; never swallow errors. Use abort signals for cancellable Git/HTTP operations. ## File Placement Rules + - Frontend components: `apps/frontend/src/components//index.tsx` - Pages/routes: `apps/frontend/src/pages` or `/src/routes` per existing pattern - Hooks: `apps/frontend/src/hooks/use.ts` @@ -69,6 +79,7 @@ Build order matters: run `pnpm build:shared-types` before isolated backend/front If unsure where to place code, search existing modules and mirror their location before creating new folders. ## Workflow Expectations + - For feature work: update types first, then backend services/routes, then frontend API clients/components, with tests at each layer. - For bug fixes: reproduce with a failing test, patch minimally, keep regression test. - For refactors: keep behavior identical, maintain coverage, and avoid mixing with feature changes. @@ -76,6 +87,7 @@ If unsure where to place code, search existing modules and mirror their location - Use conventional commits (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`). ## Common Mistakes to Avoid + - Skipping `pnpm run build:shared-types` before running/packaging apps → leads to missing types. - Adding new `node_modules` or build outputs to git. - Creating duplicate types instead of importing from shared types. @@ -84,16 +96,19 @@ If unsure where to place code, search existing modules and mirror their location - Forgetting to update both backend and frontend when API contracts change. ## Quality & Checks + - Run tests and lint for code changes; doc-only edits may skip tests (still ensure formatting is clean). - Keep ≥80% coverage on critical paths; prefer writing tests alongside new logic. - Use `pnpm lint:md` for Markdown changes if formatting issues are possible. ## Context Links + - Architecture: `docs/ARCHITECTURE.md` (overall design, caches, streaming) - API: `docs/API.md` (endpoints/contracts) - Testing: `docs/TESTING.md` (testing strategy, coverage) ## When in Doubt + - Mirror existing patterns within the same folder. - Prefer modifying existing modules over creating new abstractions. - Ask for guidance before adding new top-level packages or changing folder structure. diff --git a/FRONTEND_API_MIGRATION.md b/FRONTEND_API_MIGRATION.md deleted file mode 100644 index 34dfb195..00000000 --- a/FRONTEND_API_MIGRATION.md +++ /dev/null @@ -1,1107 +0,0 @@ -# Frontend API Migration Guide - -## Overview - -This guide documents the backend API changes from PR #122 (Issue #120) and provides -complete migration instructions for **any frontend implementation** consuming the GitRay -backend API. - -**Scope**: This document is frontend-agnostic and covers general API interaction -patterns, not specific to the current frontend implementation (which is being replaced). - -**Key Changes**: - -- All POST endpoints → GET endpoints with query parameters -- Enhanced pagination support -- Filter parameters flattened to query params -- Improved response structures with nested data -- Multi-tier caching for better performance - ---- - -## Table of Contents - -- [API Endpoint Changes](#api-endpoint-changes) -- [Detailed Endpoint Documentation](#detailed-endpoint-documentation) - - [1. GET /api/repositories/commits](#1-get-apirepositoriescommits) - - [2. GET /api/repositories/heatmap](#2-get-apirepositoriesheatmap) - - [3. GET /api/repositories/contributors](#3-get-apirepositoriescontributors) - - [4. GET /api/repositories/churn](#4-get-apirepositorieschurn) - - [5. GET /api/repositories/summary](#5-get-apirepositoriessummary) - - [6. GET /api/repositories/full-data](#6-get-apirepositories full-data) -- [Migration Patterns](#migration-patterns) -- [Query Parameter Guidelines](#query-parameter-guidelines) -- [Response Structure Changes](#response-structure-changes) -- [Error Handling](#error-handling) -- [Testing Recommendations](#testing-recommendations) -- [Common Pitfalls](#common-pitfalls) - ---- - -## API Endpoint Changes - -### Complete Endpoint Mapping - -| **Old Endpoint** | **New Endpoint** | **Method** | **Key Differences** | -|------------------|------------------|------------|---------------------| -| `POST /api/repositories` | `GET /api/repositories/commits` | POST→GET | Pagination added | -| `POST /api/repositories/heatmap` | `GET /api/repositories/heatmap` | POST→GET | Query params | -| `POST /api/repositories/contributors` | `GET /api/repositories/contributors` | POST→GET | Filters | -| `POST /api/repositories/churn` | `GET /api/repositories/churn` | POST→GET | Churn filters | -| `POST /api/repositories/full-data` | `GET /api/repositories/full-data` | POST→GET | Pagination | -| `GET /api/repositories/summary` | `GET /api/repositories/summary` | No change | Improved caching | - ---- - -## Detailed Endpoint Documentation - -### 1. GET /api/repositories/commits - -**Purpose**: Retrieve paginated commit history for a repository. - -**Query Parameters**: - -```typescript -{ - repoUrl: string; // Required - Git repository URL - page?: number; // Optional - Page number (default: 1) - limit?: number; // Optional - Items per page (default: 100) -} -``` - -**Example Request**: - -```bash -GET /api/repositories/commits?repoUrl=https://github.com/jonasyr/gitray.git&page=1&limit=50 -``` - -**Response Structure**: - -```typescript -{ - commits: Commit[]; // Array of commit objects - page: number; // Current page number - limit: number; // Items per page -} -``` - -**Sample Response**: - -```json -{ - "commits": [ - { - "sha": "abc123...", - "message": "feat: add new feature", - "author": { - "name": "Jonas", - "email": "jonas@example.com" - }, - "date": "2024-12-01T10:30:00Z", - "stats": { - "additions": 150, - "deletions": 30 - } - } - ], - "page": 1, - "limit": 50 -} -``` - -**Migration Example**: - -```typescript -// OLD (POST) -const response = await fetch('/api/repositories', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ repoUrl }) -}); - -// NEW (GET) -const params = new URLSearchParams({ - repoUrl, - page: '1', - limit: '50' -}); -const response = await fetch(`/api/repositories/commits?${params}`); -const { commits, page, limit } = await response.json(); -``` - ---- - -### 2. GET /api/repositories/heatmap - -**Purpose**: Retrieve commit activity heatmap data with optional filters. - -**Query Parameters**: - -```typescript -{ - repoUrl: string; // Required - Git repository URL - author?: string; // Optional - Filter by single author - authors?: string; // Optional - Comma-separated author list - fromDate?: string; // Optional - Start date (ISO 8601) - toDate?: string; // Optional - End date (ISO 8601) -} -``` - -**Example Request**: - -```bash -GET /api/repositories/heatmap?repoUrl=https://github.com/user/repo.git&fromDate=2024-01-01&toDate=2024-12-31 -``` - -**Response Structure**: - -```typescript -{ - heatmapData: { - timePeriod: 'day' | 'week' | 'month'; - data: Array<{ - date: string; // ISO 8601 date - count: number; // Commit count - authors: number; // Unique author count - }>; - metadata?: { - totalCommits: number; - dateRange: { start: string; end: string }; - }; - } -} -``` - -**Sample Response**: - -```json -{ - "heatmapData": { - "timePeriod": "day", - "data": [ - { "date": "2024-01-01", "count": 5, "authors": 2 }, - { "date": "2024-01-02", "count": 3, "authors": 1 } - ], - "metadata": { - "totalCommits": 480, - "dateRange": { - "start": "2024-01-01", - "end": "2024-12-31" - } - } - } -} -``` - -**Migration Example**: - -```typescript -// OLD (POST with nested filterOptions) -const response = await fetch('/api/repositories/heatmap', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - repoUrl, - filterOptions: { - author: 'john', - fromDate: '2024-01-01', - toDate: '2024-12-31' - } - }) -}); - -// NEW (GET with flat query params) -const params = new URLSearchParams({ repoUrl }); -if (author) params.append('author', author); -if (fromDate) params.append('fromDate', fromDate); -if (toDate) params.append('toDate', toDate); - -const response = await fetch(`/api/repositories/heatmap?${params}`); -const { heatmapData } = await response.json(); -``` - ---- - -### 3. GET /api/repositories/contributors - -**Purpose**: Retrieve all unique contributors without statistics or ranking (GDPR-compliant). - -**Query Parameters**: - -```typescript -{ - repoUrl: string; // Required - Git repository URL - author?: string; // Optional - Filter by single author - authors?: string; // Optional - Comma-separated author list - fromDate?: string; // Optional - Start date (ISO 8601) - toDate?: string; // Optional - End date (ISO 8601) -} -``` - -**Example Request**: - -```bash -GET /api/repositories/contributors?repoUrl=https://github.com/user/repo.git&fromDate=2024-01-01 -``` - -**Response Structure**: - -```typescript -{ - contributors: Array<{ - login: string; // Author name (GDPR-compliant pseudonymized identifier) - }> -} -``` - -**Sample Response**: - -```json -{ - "contributors": [ - { "login": "Alice" }, - { "login": "Bob" }, - { "login": "Charlie" } - ] -} -``` - -**Migration Example**: - -```typescript -// OLD (POST) -const response = await fetch('/api/repositories/contributors', { - method: 'POST', - body: JSON.stringify({ repoUrl, filterOptions }) -}); - -// NEW (GET) -const params = new URLSearchParams({ repoUrl }); -if (fromDate) params.append('fromDate', fromDate); -if (toDate) params.append('toDate', toDate); - -const response = await fetch(`/api/repositories/contributors?${params}`); -const { contributors } = await response.json(); -// Note: Contributors now contain only { login: string }, no statistics -``` - -**IMPORTANT CHANGES (Issue #121)**: - -- Returns **all unique contributors**, not just top 5 -- No commit counts, line statistics, or contribution percentages -- Alphabetically sorted for consistency -- Fully GDPR-compliant (only author names, no tracking metrics) - ---- - -### 4. GET /api/repositories/churn - -**Purpose**: Retrieve code churn analysis showing file change frequency. - -**Query Parameters**: - -```typescript -{ - repoUrl: string; // Required - Git repository URL - fromDate?: string; // Optional - Analysis start date (ISO 8601) - toDate?: string; // Optional - Analysis end date (ISO 8601) - minChanges?: string; // Optional - Minimum changes filter (numeric) - extensions?: string; // Optional - Comma-separated file extensions (e.g., 'ts,tsx,js') -} -``` - -**Example Request**: - -```bash -GET /api/repositories/churn?repoUrl=https://github.com/user/repo.git&minChanges=10&extensions=ts,tsx -``` - -**Response Structure**: - -```typescript -{ - churnData: { - files: Array<{ - path: string; - additions: number; - deletions: number; - changes: number; - riskLevel: 'low' | 'medium' | 'high' | 'critical'; - }>; - summary: { - totalFiles: number; - highRiskFiles: number; - averageChanges: number; - }; - metadata: { - dateRange: { start: string; end: string }; - filters: { - minChanges?: number; - extensions?: string[]; - }; - }; - } -} -``` - -**Sample Response**: - -```json -{ - "churnData": { - "files": [ - { - "path": "src/services/cache.ts", - "additions": 450, - "deletions": 120, - "changes": 570, - "riskLevel": "high" - } - ], - "summary": { - "totalFiles": 87, - "highRiskFiles": 12, - "averageChanges": 45.3 - } - } -} -``` - -**Migration Example**: - -```typescript -// OLD (POST) -const response = await fetch('/api/repositories/churn', { - method: 'POST', - body: JSON.stringify({ repoUrl, filterOptions }) -}); - -// NEW (GET with churn-specific params) -const params = new URLSearchParams({ repoUrl }); -if (minChanges) params.append('minChanges', minChanges.toString()); -if (extensions && extensions.length > 0) { - params.append('extensions', extensions.join(',')); -} -if (fromDate) params.append('fromDate', fromDate); - -const response = await fetch(`/api/repositories/churn?${params}`); -const { churnData } = await response.json(); -``` - ---- - -### 5. GET /api/repositories/summary - -**Purpose**: Retrieve repository metadata and statistics. - -**Query Parameters**: - -```typescript -{ - repoUrl: string; // Required - Git repository URL -} -``` - -**Example Request**: - -```bash -GET /api/repositories/summary?repoUrl=https://github.com/jonasyr/gitray.git -``` - -**Response Structure**: - -```typescript -{ - summary: { - repository: { - name: string; - owner: string; - url: string; - platform: 'github' | 'gitlab' | 'bitbucket' | 'other'; - defaultBranch?: string; - }; - created: { - date: string; // ISO 8601 - source: 'git-log' | 'github-api' | 'gitlab-api' | 'estimated'; - }; - age: { - years: number; - months: number; - formatted: string; // e.g., "2.5y" - }; - lastCommit: { - date: string; // ISO 8601 - relativeTime: string; // e.g., "2 days ago" - sha: string; - author: string; - }; - stats: { - totalCommits: number; // ⚠️ Important: nested under stats - contributors: number; // ⚠️ Important: nested under stats - status: 'active' | 'inactive' | 'archived'; - }; - metadata: { - cached: boolean; - dataSource: 'git-sparse-clone' | 'cache'; - createdDateAccuracy: 'exact' | 'approximate'; - bandwidthSaved?: string; - lastUpdated: string; // ISO 8601 - }; - } -} -``` - -**Sample Response**: - -```json -{ - "summary": { - "repository": { - "name": "gitray", - "owner": "jonasyr", - "url": "https://github.com/jonasyr/gitray.git", - "platform": "github" - }, - "stats": { - "totalCommits": 480, - "contributors": 6, - "status": "active" - }, - "lastCommit": { - "date": "2024-12-02T08:15:00Z", - "relativeTime": "2 hours ago", - "sha": "abc123def456", - "author": "Jonas" - }, - "metadata": { - "cached": true, - "dataSource": "cache" - } - } -} -``` - -**⚠️ Critical Migration Note**: - -```typescript -// ❌ WRONG - Old structure (will be undefined) -const totalCommits = response.totalCommits; -const contributors = response.totalContributors; - -// ✅ CORRECT - New nested structure -const totalCommits = response.summary.stats.totalCommits; -const contributors = response.summary.stats.contributors; // Note: field is 'contributors', not 'totalContributors' -``` - ---- - -### 6. GET /api/repositories/full-data - -**Purpose**: Retrieve both commits and heatmap data in a single request with pagination and filters. - -**Query Parameters**: - -```typescript -{ - repoUrl: string; // Required - Git repository URL - page?: number; // Optional - Page number (default: 1) - limit?: number; // Optional - Items per page (default: 100) - author?: string; // Optional - Filter by single author - authors?: string; // Optional - Comma-separated author list - fromDate?: string; // Optional - Start date (ISO 8601) - toDate?: string; // Optional - End date (ISO 8601) -} -``` - -**Example Request**: - -```bash -GET /api/repositories/full-data?repoUrl=https://github.com/user/repo.git&page=1&limit=20&fromDate=2024-01-01 -``` - -**Response Structure**: - -```typescript -{ - commits: Commit[]; // Paginated commits - heatmapData: CommitHeatmapData; // Filtered heatmap data - page: number; - limit: number; - isValidHeatmap: boolean; // Backend validation flag -} -``` - -**Sample Response**: - -```json -{ - "commits": [ - { - "sha": "abc123", - "message": "Initial commit", - "author": { "name": "Jonas", "email": "jonas@example.com" }, - "date": "2024-01-01T10:00:00Z" - } - ], - "heatmapData": { - "timePeriod": "day", - "data": [ - { "date": "2024-01-01", "count": 1, "authors": 1 } - ] - }, - "page": 1, - "limit": 20, - "isValidHeatmap": true -} -``` - -**Migration Example**: - -```typescript -// OLD (POST) -const response = await fetch('/api/repositories/full-data', { - method: 'POST', - body: JSON.stringify({ - repoUrl, - timePeriod: 'month', - filterOptions: { fromDate, toDate } - }) -}); - -// NEW (GET) -const params = new URLSearchParams({ - repoUrl, - page: '1', - limit: '100' -}); -if (fromDate) params.append('fromDate', fromDate); -if (toDate) params.append('toDate', toDate); - -const response = await fetch(`/api/repositories/full-data?${params}`); -const { commits, heatmapData, page, limit } = await response.json(); -``` - ---- - -## Migration Patterns - -### Pattern 1: Basic POST → GET Migration - -```typescript -// Before -async function fetchData(repoUrl: string) { - const response = await apiClient.post('/api/repositories', { repoUrl }); - return response.data; -} - -// After -async function fetchData(repoUrl: string) { - const params = new URLSearchParams({ repoUrl }); - const response = await apiClient.get('/api/repositories/commits', { params }); - return response.data; -} -``` - -### Pattern 2: Handling Optional Filters - -```typescript -function buildQueryParams( - repoUrl: string, - filters?: { - author?: string; - authors?: string[]; - fromDate?: string; - toDate?: string; - } -): URLSearchParams { - const params = new URLSearchParams({ repoUrl }); - - if (filters?.author) { - params.append('author', filters.author); - } - - if (filters?.authors && filters.authors.length > 0) { - params.append('authors', filters.authors.join(',')); - } - - if (filters?.fromDate) { - params.append('fromDate', filters.fromDate); - } - - if (filters?.toDate) { - params.append('toDate', filters.toDate); - } - - return params; -} - -// Usage -const params = buildQueryParams(repoUrl, { fromDate: '2024-01-01' }); -const response = await fetch(`/api/repositories/heatmap?${params}`); -``` - -### Pattern 3: Pagination Helper - -```typescript -interface PaginationParams { - page?: number; - limit?: number; -} - -function addPaginationParams( - params: URLSearchParams, - pagination?: PaginationParams -): void { - const page = pagination?.page ?? 1; - const limit = pagination?.limit ?? 100; - - params.append('page', page.toString()); - params.append('limit', limit.toString()); -} - -// Usage -const params = new URLSearchParams({ repoUrl }); -addPaginationParams(params, { page: 2, limit: 50 }); -const response = await fetch(`/api/repositories/commits?${params}`); -``` - -### Pattern 4: Error Handling - -```typescript -async function fetchWithErrorHandling( - endpoint: string, - params: URLSearchParams -): Promise { - try { - const response = await fetch(`${endpoint}?${params}`); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || `HTTP ${response.status}`); - } - - return await response.json(); - } catch (error) { - console.error(`Failed to fetch ${endpoint}:`, error); - throw error; - } -} - -// Usage -const params = new URLSearchParams({ repoUrl }); -const data = await fetchWithErrorHandling('/api/repositories/summary', params); -``` - ---- - -## Query Parameter Guidelines - -### Arrays (authors, extensions) - -**Convert arrays to comma-separated strings**: - -```typescript -// Array to comma-separated string -const authors = ['alice', 'bob', 'charlie']; -params.append('authors', authors.join(',')); // 'alice,bob,charlie' - -const extensions = ['ts', 'tsx', 'js']; -params.append('extensions', extensions.join(',')); // 'ts,tsx,js' -``` - -### Dates (fromDate, toDate) - -**Use ISO 8601 format**: - -```typescript -// Correct date formats -params.append('fromDate', '2024-01-01'); -params.append('toDate', '2024-12-31'); - -// Also accepts full ISO 8601 -params.append('fromDate', '2024-01-01T00:00:00Z'); -``` - -### Numbers (page, limit, minChanges) - -**Convert numbers to strings**: - -```typescript -params.append('page', page.toString()); -params.append('limit', limit.toString()); -params.append('minChanges', minChanges.toString()); -``` - -### Conditional Parameters - -**Only include defined values**: - -```typescript -// Good - only includes defined values -if (author) params.append('author', author); -if (fromDate) params.append('fromDate', fromDate); - -// Bad - includes undefined -params.append('author', author || ''); // ❌ Don't do this -``` - ---- - -## Response Structure Changes - -### Summary Endpoint - Nested Stats - -**Critical**: The `summary` endpoint now returns deeply nested data. - -```typescript -// ❌ WRONG - Old pattern (undefined) -interface OldResponse { - totalCommits: number; - totalContributors: number; - status: string; -} - -// ✅ CORRECT - New pattern -interface NewResponse { - summary: { - repository: { name: string; owner: string; url: string; platform: string }; - stats: { - totalCommits: number; // Access via response.summary.stats.totalCommits - contributors: number; // Note: 'contributors' not 'totalContributors' - status: string; - }; - lastCommit: { date: string; sha: string; author: string }; - metadata: { cached: boolean }; - }; -} - -// Migration example -function getTotalCommits(response: NewResponse): number { - return response.summary?.stats?.totalCommits ?? 0; -} -``` - -### Heatmap Data - Always an Object - -```typescript -// Backend returns this structure -interface HeatmapResponse { - heatmapData: { - timePeriod: string; - data: Array<{ date: string; count: number }>; - metadata?: { totalCommits: number }; - }; -} - -// Access pattern -const dataPoints = response.heatmapData.data.length; -const totalCommits = response.heatmapData.metadata?.totalCommits; -``` - -### Full-Data - Validation Flag - -```typescript -interface FullDataResponse { - commits: Commit[]; - heatmapData: CommitHeatmapData; - isValidHeatmap: boolean; // Backend validation result -} - -// Always check validation flag -if (response.isValidHeatmap) { - renderHeatmap(response.heatmapData); -} else { - console.warn('Invalid heatmap data structure'); -} -``` - ---- - -## Error Handling - -### HTTP Status Codes - -| Code | Meaning | Common Causes | -|------|---------|---------------| -| `400` | Bad Request | Missing `repoUrl`, invalid date format, invalid URL | -| `404` | Not Found | Wrong endpoint path, typo in URL | -| `422` | Validation Error | Invalid query parameter values | -| `500` | Server Error | Cache failure, Git operation error | -| `504` | Gateway Timeout | Large repository taking too long | - -### Validation Errors - -```typescript -// Example validation error response -{ - "error": "Validation failed", - "details": [ - { - "field": "repoUrl", - "message": "Invalid URL format" - }, - { - "field": "fromDate", - "message": "Invalid date format, use YYYY-MM-DD" - } - ] -} -``` - -### Error Handling Pattern - -```typescript -async function handleApiCall( - endpoint: string, - params: URLSearchParams -): Promise { - try { - const response = await fetch(`${endpoint}?${params}`); - - if (response.status === 400) { - const error = await response.json(); - console.error('Validation error:', error.details); - return null; - } - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return await response.json(); - } catch (error) { - console.error('API call failed:', error); - return null; - } -} -``` - ---- - -## Testing Recommendations - -### 1. Test with Real Repository - -Use the GitRay repository for testing: - -```bash -curl "http://localhost:3001/api/repositories/summary?repoUrl=https://github.com/jonasyr/gitray.git" -``` - -**Expected Results**: - -- `stats.totalCommits`: 480 -- `stats.contributors`: 6 -- `stats.status`: "active" - -### 2. Test Pagination - -```bash -# Page 1 -curl "http://localhost:3001/api/repositories/commits?repoUrl=https://github.com/jonasyr/gitray.git&page=1&limit=10" - -# Page 2 -curl "http://localhost:3001/api/repositories/commits?repoUrl=https://github.com/jonasyr/gitray.git&page=2&limit=10" -``` - -### 3. Test Filters - -```bash -# Date range filter -curl "http://localhost:3001/api/repositories/heatmap?repoUrl=https://github.com/jonasyr/gitray.git&fromDate=2024-01-01&toDate=2024-12-31" - -# Author filter -curl "http://localhost:3001/api/repositories/contributors?repoUrl=https://github.com/jonasyr/gitray.git&author=jonas" - -# Multiple authors -curl "http://localhost:3001/api/repositories/heatmap?repoUrl=https://github.com/jonasyr/gitray.git&authors=jonas,contributor2" -``` - -### 4. Test Error Cases - -```bash -# Missing repoUrl -curl "http://localhost:3001/api/repositories/summary" -# Expected: HTTP 400 - -# Invalid date -curl "http://localhost:3001/api/repositories/heatmap?repoUrl=https://github.com/jonasyr/gitray.git&fromDate=invalid" -# Expected: HTTP 400 -``` - -### 5. Automated Test Checklist - -- [ ] All endpoints return HTTP 200 with valid params -- [ ] Pagination works correctly (page 1, 2, 3) -- [ ] Date filters reduce result set appropriately -- [ ] Author filters return subset of commits -- [ ] Multiple authors filter works (comma-separated) -- [ ] Invalid parameters return HTTP 400 -- [ ] Missing `repoUrl` returns HTTP 400 -- [ ] Response structures match documented types -- [ ] `summary.stats.totalCommits` accessible and correct -- [ ] Heatmap data has `timePeriod` and `data` fields -- [ ] Full-data returns both `commits` and `heatmapData` - ---- - -## Common Pitfalls - -### 1. Using POST Instead of GET - -```typescript -// ❌ WRONG - Will get HTTP 404 -fetch('/api/repositories/commits', { - method: 'POST', - body: JSON.stringify({ repoUrl }) -}); - -// ✅ CORRECT -const params = new URLSearchParams({ repoUrl }); -fetch(`/api/repositories/commits?${params}`); -``` - -### 2. Accessing Top-Level Fields in Summary - -```typescript -// ❌ WRONG - Returns undefined -const commits = response.totalCommits; - -// ✅ CORRECT - Access nested field -const commits = response.summary.stats.totalCommits; -``` - -### 3. Incorrect Field Name - -```typescript -// ❌ WRONG - Field doesn't exist -const count = response.summary.stats.totalContributors; - -// ✅ CORRECT - Field is 'contributors' -const count = response.summary.stats.contributors; -``` - -### 4. Arrays as JSON in Query Params - -```typescript -// ❌ WRONG - Don't stringify arrays -params.append('authors', JSON.stringify(['alice', 'bob'])); - -// ✅ CORRECT - Comma-separated string -params.append('authors', ['alice', 'bob'].join(',')); -``` - -### 5. Not Handling Optional Parameters - -```typescript -// ❌ WRONG - Includes undefined -params.append('author', author); // If author is undefined - -// ✅ CORRECT - Conditional inclusion -if (author) params.append('author', author); -``` - -### 6. Incorrect Date Format - -```typescript -// ❌ WRONG - Invalid format -params.append('fromDate', '12/01/2024'); - -// ✅ CORRECT - ISO 8601 format -params.append('fromDate', '2024-12-01'); -``` - ---- - -## Performance Considerations - -### Cache Behavior - -The backend uses multi-tier caching: - -- **Memory tier**: ~1ms response time -- **Disk tier**: ~10-50ms response time -- **Redis tier**: ~50-100ms response time -- **Git clone**: 5-30 seconds (first request only) - -**Recommendations**: - -- First request will be slow (Git clone) -- Subsequent requests with same parameters are fast (cache hit) -- Different filter combinations create separate cache entries -- Don't make unnecessary duplicate requests - -### Pagination Best Practices - -```typescript -// Good - Use reasonable page sizes -const limit = 50; // ✅ Balanced - -// Avoid - Too small or too large -const limit = 1; // ❌ Too many requests -const limit = 10000; // ❌ Memory issues -``` - ---- - -## Summary Checklist - -Use this checklist when migrating your frontend: - -### Endpoints - -- [ ] Changed all POST requests to GET -- [ ] Updated endpoint paths (`/repositories` → `/repositories/commits`) -- [ ] Moved request body to query parameters - -### Parameters - -- [ ] Arrays converted to comma-separated strings -- [ ] Dates in ISO 8601 format (`YYYY-MM-DD`) -- [ ] Numbers converted to strings for query params -- [ ] Conditional parameters only included if defined - -### Response Handling - -- [ ] Updated to access `response.summary.stats.totalCommits` -- [ ] Using `contributors` instead of `totalContributors` -- [ ] Handling nested `summary` object structure -- [ ] Validating `isValidHeatmap` flag in full-data endpoint - -### Error Management - -- [ ] Handling HTTP 400 for validation errors -- [ ] Handling HTTP 404 for incorrect endpoints -- [ ] Graceful degradation on server errors -- [ ] Logging errors for debugging - -### Testing - -- [ ] Tested all endpoints with valid parameters -- [ ] Tested pagination (multiple pages) -- [ ] Tested filters (author, date range) -- [ ] Tested error cases (missing params, invalid format) -- [ ] Verified response structures match documented types - ---- - -## Additional Resources - -- **Backend Repository Routes**: `apps/backend/src/routes/repositoryRoutes.ts` -- **Shared Types Package**: `packages/shared-types/src/index.ts` -- **API Test Script**: `test-api-phase1.sh` -- **Test Scenarios Documentation**: `scripts/api_test_scenarios.md` - ---- - -## Questions or Issues? - -If you encounter problems during migration: - -1. **Check backend logs** - Detailed error messages are logged -2. **Verify query parameters** - Use browser DevTools Network tab -3. **Test with curl** - Isolate frontend vs backend issues -4. **Review response structure** - Compare against documented types -5. **Check SonarQube** - Code quality issues may surface - -For the most up-to-date backend implementation, always refer to the source code in `apps/backend/src/routes/repositoryRoutes.ts`. diff --git a/GEMINI.md b/GEMINI.md index 17ffb394..9960f2ff 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,4 +1,5 @@ + # GitRay GitRay is a production-ready Git repository analysis and visualization platform that transforms commit history into interactive visualizations such as heatmaps, commit statistics, code churn analysis and time-series aggregations. diff --git a/README.md b/README.md index 7a6287ac..22240b0f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE) [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)](https://nodejs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/) -[![React](https://img.shields.io/badge/React-19-blue.svg)](https://reactjs.org/) +[![React](https://img.shields.io/badge/React-18.3-blue.svg)](https://reactjs.org/) [![Test Coverage](https://img.shields.io/badge/Coverage-86.4%25-brightgreen.svg)](coverage/index.html) [![DeepWiki](https://img.shields.io/badge/DeepWiki-jonasyr%2Fgitray-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/jonasyr/gitray) @@ -100,11 +100,22 @@ gitray/ **Frontend:** -- React 19 with TypeScript -- Vite for fast development and building -- Tailwind CSS for styling +- React 18.3 with TypeScript +- Vite 6.3.5 for fast development and building +- Tailwind CSS 4.1.7 for styling +- shadcn/ui component library (built on Radix UI primitives) +- Radix UI components for accessible UI primitives (20+ components including Dialog, Select, + Dropdown, Tabs, etc.) +- Recharts for data visualizations and charts +- Lucide React for icons - Rive for interactive animations -- React Calendar Heatmap for visualizations +- React Hook Form for form management +- Sonner for toast notifications +- next-themes for theme management +- Embla Carousel for carousels +- cmdk for command menu +- class-variance-authority (CVA) for component variants +- tailwind-merge for className merging **Development:** @@ -438,7 +449,7 @@ pnpm test # Frontend tests only pnpm test:frontend -# Backend tests only +# Backend tests only pnpm test:backend # Watch mode for development @@ -451,7 +462,7 @@ pnpm test:coverage ### Test Structure - **Unit Tests**: Component and service-level testing -- **Integration Tests**: API endpoint and workflow testing +- **Integration Tests**: API endpoint and workflow testing - **Performance Tests**: Cache and memory management testing - **E2E Tests**: Full user workflow testing @@ -680,7 +691,7 @@ We welcome contributions! Please read our contributing guidelines. ```bash # Example commit messages feat: add repository coordination system -fix: resolve memory leak in cache manager +fix: resolve memory leak in cache manager docs: update API documentation test: add integration tests for heatmap ``` @@ -825,7 +836,10 @@ The backend API is documented through: ## Acknowledgements -- [React Calendar Heatmap](https://github.com/kevinsqi/react-calendar-heatmap) for visualization components +- [shadcn/ui](https://ui.shadcn.com/) for beautiful, accessible component library +- [Radix UI](https://www.radix-ui.com/) for robust UI primitives +- [Recharts](https://recharts.org/) for visualization components +- [Lucide React](https://lucide.dev/) for icons - [Rive](https://rive.app/) for interactive animations - [simple-git](https://github.com/steveukx/git-js) for Git operations - The open-source community for inspiration and tools diff --git a/Strategy.md b/Strategy.md index e4ad4dd0..96579a0a 100644 --- a/Strategy.md +++ b/Strategy.md @@ -85,7 +85,6 @@ Einfaches Aufgabenmanagement mit GitHub Issues & Kanban-Board: - Regel: **Keine Commits direkt auf `main` oder `dev`** - PRs sollten: - - Review durch den anderen bekommen - mit einer kleinen Beschreibung versehen sein @@ -111,7 +110,6 @@ Kurze Beschreibung der Änderung. ## ⚙️ **CI/CD** - Nutzt GitHub Actions: - - `npm run test` - `eslint .` @@ -155,7 +153,6 @@ Klarheit über Zusammenarbeit: - Verwendet `.env.example` für Umgebungsvariablen - Macht Commits sprechend, z. B.: - - `feat: add interactive commit graph` - `fix: resolve layout bug on zoom` diff --git a/apps/backend/__tests__/unit/services/repositorySummaryService.unit.test.ts b/apps/backend/__tests__/unit/services/repositorySummaryService.unit.test.ts index fb7772bb..1afe37c3 100644 --- a/apps/backend/__tests__/unit/services/repositorySummaryService.unit.test.ts +++ b/apps/backend/__tests__/unit/services/repositorySummaryService.unit.test.ts @@ -131,7 +131,7 @@ describe('RepositorySummaryService', () => { .mockResolvedValueOnce('100\n') // rev-list --count .mockResolvedValueOnce('2011-03-22T00:00:00.000Z\n') // log --reverse (first commit) .mockResolvedValueOnce( - '2025-11-15T10:30:00.000Z|abc123def|Test Author\n' + `${new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()}|abc123def|Test Author\n` ) // log -1 (last commit) .mockResolvedValueOnce(' 10 Author One\n 5 Author Two\n'); // shortlog diff --git a/apps/backend/perf/README.md b/apps/backend/perf/README.md index a6425c99..eeb85e5e 100644 --- a/apps/backend/perf/README.md +++ b/apps/backend/perf/README.md @@ -106,17 +106,14 @@ The test suite enforces these performance thresholds: The load test simulates realistic usage patterns with weighted scenarios: 1. **Health Check** (10% of traffic) - - Simple GET requests to `/health` - Validates service availability 2. **Get Repository Commits** (40% of traffic) - - POST requests to `/api/repositories` - Tests git cloning and commit retrieval 3. **Get Heatmap Data** (30% of traffic) - - GET requests to `/api/commits/heatmap` - Tests data aggregation and caching diff --git a/apps/backend/src/services/gitService.ts b/apps/backend/src/services/gitService.ts index 561991b1..fb19d867 100644 --- a/apps/backend/src/services/gitService.ts +++ b/apps/backend/src/services/gitService.ts @@ -1234,6 +1234,11 @@ class GitService { // Sort by changes (descending) - highest churn files first filtered.sort((a, b) => b.changes - a.changes); + // Apply limit if specified + if (options?.limit !== undefined && options.limit > 0) { + filtered = filtered.slice(0, options.limit); + } + return filtered; } diff --git a/apps/backend/test-config-dynamic.mjs b/apps/backend/test-config-dynamic.mjs index 39ef6d75..b47e55b8 100644 --- a/apps/backend/test-config-dynamic.mjs +++ b/apps/backend/test-config-dynamic.mjs @@ -4,13 +4,13 @@ /** * Comprehensive Environment Variable Test Script with Auto-Discovery - * + * * This script automatically discovers ALL environment variables in .env * and dynamically maps them to their config paths by: * 1. Auto-discovery phase - maps each env var to its config location * 2. Current values test - verifies env vars match current config * 3. Dynamic change test - verifies changing env vars affects config - * + * * NO HARDCODED MAPPINGS NEEDED! ✨ */ @@ -27,40 +27,45 @@ let ENV_TO_CONFIG_MAP = {}; // Deep object comparison to find changed paths function deepCompare(obj1, obj2, path = '') { const changes = []; - + if (obj1 === obj2) return changes; - - if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) { + + if ( + typeof obj1 !== 'object' || + typeof obj2 !== 'object' || + obj1 === null || + obj2 === null + ) { if (obj1 !== obj2) { changes.push({ path, oldValue: obj1, newValue: obj2 }); } return changes; } - + const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]); - + for (const key of allKeys) { const newPath = path ? `${path}.${key}` : key; const subChanges = deepCompare(obj1[key], obj2[key], newPath); changes.push(...subChanges); } - + return changes; } // Create a getter function for a config path function createConfigGetter(path) { - return function(config) { + return function (config) { const parts = path.split('.'); let current = config; - + for (const part of parts) { if (current === null || current === undefined) { return undefined; } current = current[part]; } - + return current; }; } @@ -70,79 +75,85 @@ async function discoverEnvVarMapping(envVar, originalValue) { try { // Load original config const originalConfig = await loadConfig(); - + // First, check if this variable is accessed directly via process.env // by testing if changing the env var affects process.env but not config await modifyEnvValue(envVar, DISCOVERY_TEST_VALUE); - + if (process.env[envVar] === DISCOVERY_TEST_VALUE) { // This variable is available in process.env, create a direct getter await modifyEnvValue(envVar, originalValue); - return function() { return process.env[envVar]; }; + return function () { + return process.env[envVar]; + }; } - + // Generate multiple test values to handle different types const testValues = [ DISCOVERY_TEST_VALUE, // string - '12345', // number - 'true', // boolean true - 'false', // boolean false - '99' // percentage/small number + '12345', // number + 'true', // boolean true + 'false', // boolean false + '99', // percentage/small number ]; - + for (const testValue of testValues) { // Set test value await modifyEnvValue(envVar, testValue); - + // Load modified config const modifiedConfig = await loadConfig(); - + // Find what changed const changes = deepCompare(originalConfig, modifiedConfig); - + // Look for changes that could match this test value - const relevantChanges = changes.filter(change => { + const relevantChanges = changes.filter((change) => { const newVal = change.newValue; - + // Direct string match if (newVal === testValue) return true; - + // Boolean conversion if (testValue === 'true' && newVal === true) return true; if (testValue === 'false' && newVal === false) return true; - + // Number conversion if (!isNaN(testValue) && newVal === parseInt(testValue)) return true; if (!isNaN(testValue) && newVal === parseFloat(testValue)) return true; - + // Percentage conversion (divide by 100) - if (!isNaN(testValue) && newVal === parseFloat(testValue) / 100) return true; - + if (!isNaN(testValue) && newVal === parseFloat(testValue) / 100) + return true; + // GB conversion (multiply by 1024^3) - if (!isNaN(testValue) && newVal === parseFloat(testValue) * 1024**3) return true; - + if (!isNaN(testValue) && newVal === parseFloat(testValue) * 1024 ** 3) + return true; + // Bytes conversion (round from bytes to GB) - return !isNaN(testValue) && Math.round(newVal / 1024**3) === parseInt(testValue); + return ( + !isNaN(testValue) && + Math.round(newVal / 1024 ** 3) === parseInt(testValue) + ); }); - + if (relevantChanges.length === 1) { // Restore original value before returning await modifyEnvValue(envVar, originalValue); return createConfigGetter(relevantChanges[0].path); } else if (relevantChanges.length > 1) { // Multiple matches - pick the most specific one - const shortestPath = relevantChanges.reduce((shortest, current) => + const shortestPath = relevantChanges.reduce((shortest, current) => current.path.length < shortest.path.length ? current : shortest ); await modifyEnvValue(envVar, originalValue); return createConfigGetter(shortestPath.path); } } - + // Restore original value await modifyEnvValue(envVar, originalValue); return null; - } catch (error) { console.warn(`Failed to discover mapping for ${envVar}: ${error.message}`); // Try to restore original value @@ -158,17 +169,17 @@ async function discoverEnvVarMapping(envVar, originalValue) { // Auto-discover all environment variable mappings async function discoverAllMappings() { console.log('🔍 AUTO-DISCOVERING environment variable mappings...'); - + const envVars = parseEnvFile(); const discoveries = []; let discovered = 0; let failed = 0; - + for (const [envVar, originalValue] of Object.entries(envVars)) { process.stdout.write(`⏳ Discovering ${envVar}... `); - + const getter = await discoverEnvVarMapping(envVar, originalValue); - + if (getter) { ENV_TO_CONFIG_MAP[envVar] = getter; console.log(`✅`); @@ -176,17 +187,23 @@ async function discoverAllMappings() { } else { // For unmapped variables, create a direct process.env accessor as fallback // This will catch variables that are used directly without going through config - ENV_TO_CONFIG_MAP[envVar] = function() { return process.env[envVar]; }; + ENV_TO_CONFIG_MAP[envVar] = function () { + return process.env[envVar]; + }; console.log(`✅ (direct process.env access)`); discovered++; } - + discoveries.push({ envVar, found: true }); // All variables are now "found" } - - console.log(`\n📊 DISCOVERY RESULTS: ${discovered}/${discovered + failed} mappings found`); - console.log(`✨ All environment variables mapped (config object + direct process.env access)`); - + + console.log( + `\n📊 DISCOVERY RESULTS: ${discovered}/${discovered + failed} mappings found` + ); + console.log( + `✨ All environment variables mapped (config object + direct process.env access)` + ); + return discoveries; } @@ -194,8 +211,8 @@ async function discoverAllMappings() { function parseEnvFile() { const envContent = fs.readFileSync(ENV_FILE, 'utf8'); const envVars = {}; - - envContent.split('\n').forEach(line => { + + envContent.split('\n').forEach((line) => { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) { const [key, ...valueParts] = trimmed.split('='); @@ -203,7 +220,7 @@ function parseEnvFile() { envVars[key] = value; } }); - + return envVars; } @@ -219,27 +236,27 @@ async function restoreEnvFile() { async function modifyEnvValue(envVar, newValue) { const envContent = await fs.promises.readFile(ENV_FILE, 'utf8'); const lines = envContent.split('\n'); - + let found = false; - const newLines = lines.map(line => { + const newLines = lines.map((line) => { if (line.startsWith(`${envVar}=`)) { found = true; return `${envVar}=${newValue}`; } return line; }); - + if (!found) { throw new Error(`Environment variable ${envVar} not found in .env file`); } - + await fs.promises.writeFile(ENV_FILE, newLines.join('\n')); } async function loadConfig() { // Reload dotenv with override dotenv.config({ override: true }); - + // Import fresh config with timestamp to avoid caching const configModule = await import(`./dist/src/config.js?${Date.now()}`); return configModule.config; @@ -247,19 +264,19 @@ async function loadConfig() { function normalizeValue(value, envValue) { if (value === undefined || value === null) return value; - + // Handle boolean values if (typeof value === 'boolean') { if (envValue === 'true') return true; if (envValue === 'false') return false; } - + // Handle numeric values if (typeof value === 'number') { const numValue = parseFloat(envValue); return isNaN(numValue) ? envValue : numValue; } - + return value; } @@ -270,15 +287,17 @@ function generateTestValue(originalValue) { if (originalValue === 'development') return 'production'; if (originalValue === 'localhost') return '127.0.0.1'; if (originalValue === 'info') return 'debug'; - if (originalValue.startsWith('./')) return originalValue.replace('./', './test-'); - if (originalValue.includes(':')) return originalValue.replace('gitray:', 'test:'); - + if (originalValue.startsWith('./')) + return originalValue.replace('./', './test-'); + if (originalValue.includes(':')) + return originalValue.replace('gitray:', 'test:'); + // For numbers, increase by 50% const num = parseFloat(originalValue); if (!isNaN(num)) { return Math.floor(num * 1.5).toString(); } - + // Default: append suffix return originalValue + '_test'; } @@ -287,11 +306,14 @@ function generateTestValue(originalValue) { async function testSingleCurrentValue(envVar, envValue, config) { const configValue = ENV_TO_CONFIG_MAP[envVar](config); const normalizedConfigValue = normalizeValue(configValue, envValue); - + let match = false; - + // Handle empty/undefined values - if ((envValue === '' || envValue === undefined) && (configValue === undefined || configValue === null)) { + if ( + (envValue === '' || envValue === undefined) && + (configValue === undefined || configValue === null) + ) { match = true; } else { // Convert envValue to appropriate type for comparison @@ -301,28 +323,32 @@ async function testSingleCurrentValue(envVar, envValue, config) { } else if (typeof normalizedConfigValue === 'number') { compareEnvValue = parseFloat(envValue); } - + match = compareEnvValue == normalizedConfigValue; } - + return { match, configValue, normalizedConfigValue }; } async function testCurrentValues() { console.log('🔍 PHASE 1: Testing current .env values match config...'); - + const envVars = parseEnvFile(); const config = await loadConfig(); const results = []; - + let passed = 0; let failed = 0; - + for (const [envVar, envValue] of Object.entries(envVars)) { if (ENV_TO_CONFIG_MAP[envVar]) { try { - const { match, configValue } = await testSingleCurrentValue(envVar, envValue, config); - + const { match, configValue } = await testSingleCurrentValue( + envVar, + envValue, + config + ); + if (match) { console.log(`✅ ${envVar}`); passed++; @@ -330,19 +356,27 @@ async function testCurrentValues() { console.log(`❌ ${envVar}: "${envValue}" → "${configValue}"`); failed++; } - + results.push({ envVar, envValue, configValue, match }); } catch (error) { console.log(`❌ ${envVar}: ERROR - ${error.message}`); failed++; - results.push({ envVar, envValue, configValue: null, match: false, error: error.message }); + results.push({ + envVar, + envValue, + configValue: null, + match: false, + error: error.message, + }); } } else { console.log(`⚠️ ${envVar}: No config mapping found`); } } - - console.log(`\n📊 PHASE 1 RESULTS: ${passed}/${passed + failed} current values match`); + + console.log( + `\n📊 PHASE 1 RESULTS: ${passed}/${passed + failed} current values match` + ); return { results, passed, failed }; } @@ -350,37 +384,40 @@ async function testCurrentValues() { async function testSingleDynamicChange(envVar, originalValue) { // Generate test value const testValue = generateTestValue(originalValue); - + // Load original config const originalConfig = await loadConfig(); const originalConfigValue = ENV_TO_CONFIG_MAP[envVar](originalConfig); - + // Modify env file await modifyEnvValue(envVar, testValue); - + // Reload environment variables to pick up changes dotenv.config({ path: ENV_FILE, override: true }); - + // Load modified config const modifiedConfig = await loadConfig(); const modifiedConfigValue = ENV_TO_CONFIG_MAP[envVar](modifiedConfig); - + // Smart expected value calculation let expectedValue = testValue; - + // For direct process.env access, the expected value is always the string value - if (typeof originalConfigValue === 'string' && originalConfigValue === originalValue) { + if ( + typeof originalConfigValue === 'string' && + originalConfigValue === originalValue + ) { expectedValue = testValue; } else if (typeof originalConfigValue === 'boolean') { expectedValue = testValue === 'true'; } else if (typeof originalConfigValue === 'number') { const testNum = parseFloat(testValue); const originalNum = parseFloat(originalValue); - + if (!isNaN(testNum) && !isNaN(originalNum)) { // Check if this looks like a GB conversion (bytes = GB * 1024^3) - if (originalConfigValue === originalNum * 1024**3) { - expectedValue = testNum * 1024**3; + if (originalConfigValue === originalNum * 1024 ** 3) { + expectedValue = testNum * 1024 ** 3; } // Check if this looks like a percentage conversion (config = env / 100) else if (Math.abs(originalConfigValue - originalNum / 100) < 0.001) { @@ -396,106 +433,122 @@ async function testSingleDynamicChange(envVar, originalValue) { } } } - - const success = Math.abs(modifiedConfigValue - expectedValue) < 0.001 || modifiedConfigValue == expectedValue; - + + const success = + Math.abs(modifiedConfigValue - expectedValue) < 0.001 || + modifiedConfigValue == expectedValue; + return { testValue, originalConfigValue, modifiedConfigValue, expectedValue, - success + success, }; } async function testDynamicChanges() { console.log('\n🔄 PHASE 2: Testing dynamic value changes...'); - + const envVars = parseEnvFile(); const results = []; - + let passed = 0; let failed = 0; - + for (const [envVar, originalValue] of Object.entries(envVars)) { if (ENV_TO_CONFIG_MAP[envVar]) { try { const testResult = await testSingleDynamicChange(envVar, originalValue); - + if (testResult.success) { console.log(`✅ ${envVar}`); passed++; } else { - console.log(`❌ ${envVar}: Expected ${testResult.expectedValue}, got ${testResult.modifiedConfigValue}`); + console.log( + `❌ ${envVar}: Expected ${testResult.expectedValue}, got ${testResult.modifiedConfigValue}` + ); failed++; } - + results.push({ envVar, originalValue, ...testResult }); - + // Restore original value await modifyEnvValue(envVar, originalValue); - } catch (error) { console.log(`❌ ${envVar}: ERROR - ${error.message}`); failed++; results.push({ envVar, success: false, error: error.message }); - + // Try to restore original value try { await modifyEnvValue(envVar, originalValue); } catch (restoreError) { - console.log(`⚠️ Failed to restore ${envVar}: ${restoreError.message}`); + console.log( + `⚠️ Failed to restore ${envVar}: ${restoreError.message}` + ); } } } } - - console.log(`\n📊 PHASE 2 RESULTS: ${passed}/${passed + failed} dynamic changes work`); + + console.log( + `\n📊 PHASE 2 RESULTS: ${passed}/${passed + failed} dynamic changes work` + ); return { results, passed, failed }; } async function main() { console.log('🧪 COMPREHENSIVE ENVIRONMENT VARIABLE TEST WITH AUTO-DISCOVERY'); console.log('============================================================='); - + const envVars = parseEnvFile(); const totalVars = Object.keys(envVars).length; - + console.log(`Found ${totalVars} environment variables\n`); - + try { // Backup original .env await backupEnvFile(); - + // Phase 0: Auto-discover mappings await discoverAllMappings(); - + const mappedVars = Object.keys(ENV_TO_CONFIG_MAP).length; - console.log(`\nTesting ${mappedVars} variables with discovered config mappings\n`); - + console.log( + `\nTesting ${mappedVars} variables with discovered config mappings\n` + ); + if (mappedVars === 0) { console.log('❌ No mappings discovered! Cannot proceed with testing.'); return; } - + // Phase 1: Test current values const phase1Results = await testCurrentValues(); - + // Phase 2: Test dynamic changes const phase2Results = await testDynamicChanges(); - + // Final summary console.log('\n🎯 FINAL SUMMARY:'); console.log('================='); console.log(`Auto-discovery: ${mappedVars}/${totalVars} mappings found`); - console.log(`Phase 1 - Current values: ${phase1Results.passed}/${phase1Results.passed + phase1Results.failed} ✅`); - console.log(`Phase 2 - Dynamic changes: ${phase2Results.passed}/${phase2Results.passed + phase2Results.failed} ✅`); - + console.log( + `Phase 1 - Current values: ${phase1Results.passed}/${phase1Results.passed + phase1Results.failed} ✅` + ); + console.log( + `Phase 2 - Dynamic changes: ${phase2Results.passed}/${phase2Results.passed + phase2Results.failed} ✅` + ); + const totalPassed = phase1Results.passed + phase2Results.passed; - const totalTests = (phase1Results.passed + phase1Results.failed) + (phase2Results.passed + phase2Results.failed); - + const totalTests = + phase1Results.passed + + phase1Results.failed + + (phase2Results.passed + phase2Results.failed); + console.log(`\nOverall: ${totalPassed}/${totalTests} tests passed`); - + if (phase1Results.failed === 0 && phase2Results.failed === 0) { console.log('\n🎉 ALL DISCOVERED ENVIRONMENT VARIABLES WORK PERFECTLY!'); console.log('✅ Current values match config'); @@ -504,12 +557,13 @@ async function main() { } else { console.log('\n⚠️ Some tests failed. Check the configuration system.'); } - + if (mappedVars < totalVars) { const unmapped = totalVars - mappedVars; - console.log(`\nℹ️ ${unmapped} environment variables have no config mapping (may be unused)`); + console.log( + `\nℹ️ ${unmapped} environment variables have no config mapping (may be unused)` + ); } - } catch (error) { console.error('❌ Test execution failed:', error.message); } finally { diff --git a/apps/frontend/.env.example b/apps/frontend/.env.example new file mode 100644 index 00000000..e6b4f797 --- /dev/null +++ b/apps/frontend/.env.example @@ -0,0 +1,22 @@ +# ============================================================================= +# GITRAY FRONTEND CONFIGURATION - EXAMPLE +# ============================================================================= +# +# 🚀 SETUP INSTRUCTIONS: +# 1. Copy this file to .env: cp .env.example .env +# 2. In development: Leave VITE_API_URL empty (uses Vite proxy) +# 3. In production: Set VITE_API_URL to your backend URL +# +# 📝 NOTES: +# - This file shows the environment variables needed for the frontend +# - Modify .env (not this file) for your actual configuration +# - .env is gitignored to prevent accidental commits of local settings +# ============================================================================= + +# ----------------------------------------------------------------------------- +# API CONFIGURATION +# ----------------------------------------------------------------------------- +# Backend API URL +# - Development: Leave empty or comment out (Vite proxy forwards /api to localhost:3001) +# - Production: Set to your backend URL (e.g., https://api.yourdomain.com) +VITE_API_URL= diff --git a/apps/frontend/.gitignore b/apps/frontend/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/apps/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/apps/frontend/README.md b/apps/frontend/README.md index b7a941b9..9398866c 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -1,61 +1,178 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite -with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) - uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) - uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the -configuration to enable type-aware lint rules: - -```js -export default tseslint.config({ - extends: [ - // Remove ...tseslint.configs.recommended and replace with this - ...tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - ...tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - ...tseslint.configs.stylisticTypeChecked, - ], - languageOptions: { - // other options... - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - }, -}) - -You can also install -eslint-plugin-react-x -and -eslint-plugin-react-dom -for React‑specific lint rules: - -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default tseslint.config({ - plugins: { - // Add the react-x and react-dom plugins - 'react-x': reactX, - 'react-dom': reactDom, - }, - rules: { - // other rules... - // Enable its recommended typescript rules - ...reactX.configs['recommended-typescript'].rules, - ...reactDom.configs.recommended.rules, - }, -}) +# GitRay Frontend +The modern, redesigned frontend for GitRay - a Git repository analysis and +visualization platform built with React, TypeScript, and shadcn/ui. + +## 🎨 Design + +This frontend is based on the **GitRay Web App Design** and implements a +complete UI redesign with shadcn/ui components. + +## 🚀 Tech Stack + +- **React 18.3** - UI library +- **TypeScript 5.7** - Type safety +- **Vite 6.3** - Build tool with HMR +- **Tailwind CSS 4.1** - Utility-first styling +- **shadcn/ui** - High-quality component library built on Radix UI +- **Radix UI** - 20+ accessible UI primitives (Dialog, Select, Dropdown, Tabs, Tooltip, etc.) +- **Recharts** - Charting library for data visualization +- **Axios** - HTTP client +- **Lucide React** - Icon library +- **@rive-app/react-canvas** - Interactive animations +- **React Hook Form** - Form state management +- **Sonner** - Toast notifications +- **next-themes** - Theme management (dark/light mode) +- **cmdk** - Command menu component +- **class-variance-authority** - Component variant management + +## 📦 Installation + +From the project root: + +```bash +# Install all workspace dependencies +pnpm install + +# Or install only frontend dependencies +pnpm --filter frontend install +``` + +## 🛠️ Development + +```bash +# Start development server (from project root) +pnpm dev:frontend + +# Or from this directory +pnpm dev + +# Start with full stack (Redis + Backend + Frontend) +pnpm start +``` + +The development server runs on `http://localhost:5173` with hot module replacement (HMR) enabled. + +## 🏗️ Build + +```bash +# Build for production +pnpm build + +# Preview production build +pnpm preview +``` + +Output directory: `build/` + +## 🧪 Testing + +```bash +# Run tests once +pnpm test + +# Run tests in watch mode +pnpm test:watch + +# Generate coverage report +pnpm test:coverage +``` + +### Test Structure + +Tests are organized in the `__tests__/` directory: + +```text +__tests__/ +├── components/ # Component unit tests +├── services/ # API service tests +├── utils/ # Utility function tests +└── example.test.tsx # Example test template ``` + +### Writing Tests + +Use the `example.test.tsx` file as a template for creating new tests: + +```typescript +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import YourComponent from '../components/YourComponent'; + +describe('YourComponent', () => { + it('should render successfully', () => { + render(); + expect(screen.getByText('Expected Text')).toBeInTheDocument(); + }); +}); +``` + +### Test Utilities + +The `src/test-setup.ts` file provides: + +- **Jest-DOM matchers** for better assertions +- **Automatic cleanup** after each test +- **window.matchMedia mock** for components using media queries +- **React 18 compatibility** setup + +## 📁 Project Structure + +```text +apps/frontend/ +├── src/ +│ ├── components/ # React components +│ │ ├── ui/ # shadcn/ui base components +│ │ └── figma/ # Figma-specific components +│ ├── services/ # API services +│ ├── styles/ # Global styles +│ └── main.tsx # Application entry point +├── __tests__/ # Test files +├── public/ # Static assets +├── build/ # Production build output +└── vite.config.ts # Vite configuration +``` + +## 🔧 Configuration Files + +- `vite.config.ts` - Vite configuration with SWC plugin and proxy setup +- `vitest.config.ts` - Vitest test configuration +- `eslint.config.js` - ESLint rules for React and TypeScript +- `postcss.config.cjs` - PostCSS with Tailwind CSS +- `tsconfig.json` - TypeScript compiler options + +## 🌐 API Integration + +The frontend communicates with the backend API through: + +- **Development**: Vite proxy forwards `/api` requests to `http://localhost:3001` +- **Production**: Configure `VITE_API_URL` environment variable + +## 🎨 UI Components + +This project uses [shadcn/ui](https://ui.shadcn.com/), a collection of +re-usable components built with Radix UI and Tailwind CSS. Components are +located in `src/components/ui/`. + +## 🔌 Vite Plugins + +- **@vitejs/plugin-react-swc** - Fast Refresh using SWC instead of Babel for improved performance + +## 📝 ESLint Configuration + +The project uses TypeScript ESLint with: + +- Recommended TypeScript rules +- React Hooks rules +- React Refresh rules +- Special rules for test files + +## 🔗 Related + +- [Backend README](../backend/README.md) +- [Project Root Documentation](../../README.md) +- [Shared Types Package](../../packages/shared-types/) + +## 📄 License + +Part of the GitRay project. diff --git a/apps/frontend/__tests__/App.test.tsx b/apps/frontend/__tests__/App.test.tsx index 27f14612..40d6a8e5 100644 --- a/apps/frontend/__tests__/App.test.tsx +++ b/apps/frontend/__tests__/App.test.tsx @@ -1,50 +1,114 @@ -import { describe, expect, test } from 'vitest'; -// apps/frontend/src/__tests__/App.test.tsx -import { render, screen } from '@testing-library/react'; +import { render, screen, act } from '@testing-library/react'; import App from '../src/App'; +import { describe, test, expect, beforeEach, vi } from 'vitest'; -// NOTE: These tests are temporarily disabled due to React hooks issues in test environment -// Frontend will be fully replaced in the near future, so these failures are acceptable -// Related to backend file analysis implementation - frontend tests unrelated to PR -describe.skip('App Component', () => { - test('renders main page with login and signup buttons', () => { - // Arrange +// Mock dependencies +vi.mock('../src/components/Header', () => ({ + Header: () =>
, +})); +vi.mock('../src/components/Footer', () => ({ + Footer: () =>
, +})); +vi.mock('../src/components/LandingPage', () => ({ + LandingPage: ({ onAnalyze }: any) => ( +
+ +
+ ), +})); +vi.mock('../src/components/DashboardPage', () => ({ + default: () =>
, +})); +vi.mock('../src/components/RiveLoader', () => ({ + RiveLoader: () =>
, +})); +vi.mock('sonner', () => ({ + toast: { error: vi.fn(), success: vi.fn(), info: vi.fn(), dismiss: vi.fn() }, + Toaster: () =>
, +})); +vi.mock('../src/services/api', () => ({ + getRepositoryFullData: vi.fn(), +})); + +describe('App Component', () => { + beforeEach(() => { + // Arrange: Reset mocks and DOM + vi.clearAllMocks(); + }); - // Act: render the component under test - render(); + test('should render LandingPage by default with Header and Footer', async () => { + // Act + await act(async () => { + render(); + }); - // Assert: both buttons should be present - const loginBtn = screen.getByRole('button', { name: /login/i }); - const signupBtn = screen.getByRole('button', { name: /signup/i }); - expect(loginBtn).toBeDefined(); - expect(signupBtn).toBeDefined(); + // Assert + expect(screen.getByTestId('mock-header')).toBeInTheDocument(); + expect(screen.getByTestId('mock-landing')).toBeInTheDocument(); + expect(screen.getByTestId('mock-footer')).toBeInTheDocument(); }); - test('renders exactly three explanation buttons', () => { + test('should transition to DashboardPage on successful analysis', async () => { // Arrange + const { getRepositoryFullData } = await import('../src/services/api'); + (getRepositoryFullData as any).mockResolvedValue({ + commits: [], + heatmapData: null, + isValidHeatmap: true, + }); + + await act(async () => { + render(); + }); // Act - render(); + const analyzeBtn = screen.getByTestId('analyze-btn'); + await act(async () => { + analyzeBtn.click(); + }); // Assert - const explainBtns = screen.getAllByRole('button', { - name: /explanation \d+/i, - }); - expect(explainBtns.length).toBe(3); + // It should first show loader, then dashboard + // Check API was called + expect(getRepositoryFullData).toHaveBeenCalledWith( + 'https://github.com/test', + 'day' + ); + + // Check that dashboard is rendered (since the mock returns immediately in `act`) + expect(screen.getByTestId('mock-dashboard')).toBeInTheDocument(); + // Landing shouldn't be rendered anymore + expect(screen.queryByTestId('mock-landing')).not.toBeInTheDocument(); }); - test('renders footer with the correct text', () => { + test('should handle API errors gracefully via toast', async () => { // Arrange + const { getRepositoryFullData } = await import('../src/services/api'); + const { toast } = await import('sonner'); + (getRepositoryFullData as any).mockRejectedValue( + new Error('Network error') + ); + + await act(async () => { + render(); + }); // Act - render(); + const analyzeBtn = screen.getByTestId('analyze-btn'); + await act(async () => { + analyzeBtn.click(); + }); // Assert - const preFooter = screen.getByText(/Nach dem runterscrollen:/i); - const linkText = screen.getByText( - /Links: Impressum; Datenschutzerklärung; "Über Uns"-Seite; created by "Namen von uns"/i - ); - expect(preFooter).toBeDefined(); - expect(linkText).toBeDefined(); + expect(toast.error).toHaveBeenCalledWith('Analysis failed', { + description: 'Network error', + }); + // Should still be on landing page + expect(screen.getByTestId('mock-landing')).toBeInTheDocument(); }); }); diff --git a/apps/frontend/__tests__/components/AIInsights.test.tsx b/apps/frontend/__tests__/components/AIInsights.test.tsx new file mode 100644 index 00000000..97e2e036 --- /dev/null +++ b/apps/frontend/__tests__/components/AIInsights.test.tsx @@ -0,0 +1,70 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, test, expect } from 'vitest'; +import { AIInsights } from '../../src/components/AIInsights'; + +describe('AIInsights Component', () => { + test('should render the component and default Overview tab', () => { + // Act + render(); + + // Assert + expect( + screen.getByText( + /AI-powered insights are generated based on your project's structure/i + ) + ).toBeInTheDocument(); + + // Check if tabs are present + expect(screen.getByRole('tab', { name: /Overview/i })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: /Weekly/i })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: /Trends/i })).toBeInTheDocument(); + + // Check Overview content + expect(screen.getByText('Project Health Score')).toBeInTheDocument(); + expect(screen.getByText('78')).toBeInTheDocument(); + expect(screen.getByText('Recommendations')).toBeInTheDocument(); + expect( + screen.getByText('Implement lazy loading for feature modules') + ).toBeInTheDocument(); + }); + + test('should switch to the Weekly tab when clicked', async () => { + // Arrange + const user = userEvent.setup(); + render(); + const weeklyTab = screen.getByRole('tab', { name: /Weekly/i }); + + // Act + await user.click(weeklyTab); + + // Assert + await waitFor(() => { + expect( + screen.getByText('Weekly Development Summary') + ).toBeInTheDocument(); + }); + expect(screen.getByText('Week 47 (Nov 1-7)')).toBeInTheDocument(); + expect( + screen.getByText('Implemented new dashboard analytics feature') + ).toBeInTheDocument(); + }); + + test('should switch to the Trends tab when clicked', async () => { + // Arrange + const user = userEvent.setup(); + render(); + const trendsTab = screen.getByRole('tab', { name: /Trends/i }); + + // Act + await user.click(trendsTab); + + // Assert + await waitFor(() => { + expect(screen.getByText('Monthly Trends')).toBeInTheDocument(); + }); + expect(screen.getByText('Team Productivity')).toBeInTheDocument(); + expect(screen.getByText('Code Quality')).toBeInTheDocument(); + expect(screen.getByText('Team Collaboration')).toBeInTheDocument(); + }); +}); diff --git a/apps/frontend/__tests__/components/ActivityHeatmap.test.tsx b/apps/frontend/__tests__/components/ActivityHeatmap.test.tsx deleted file mode 100644 index 18b92ed2..00000000 --- a/apps/frontend/__tests__/components/ActivityHeatmap.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { render, waitFor, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import ActivityHeatmap from '../../src/components/ActivityHeatmap'; -import { getHeatmapData } from '../../src/services/api'; - -vi.mock('../../src/services/api', () => ({ - getHeatmapData: vi.fn(), -})); - -const mockedGetHeatmapData = vi.mocked(getHeatmapData); - -// NOTE: These tests are temporarily disabled due to React hooks issues in test environment -// Frontend will be fully replaced in the near future, so these failures are acceptable -// Related to backend file analysis implementation - frontend tests unrelated to PR -describe.skip('ActivityHeatmap (happy path, AAA)', () => { - test('renders tooltip title with correct commit count', async () => { - // Arrange - const date = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000) - .toISOString() - .slice(0, 10); - mockedGetHeatmapData.mockResolvedValue({ - timePeriod: 'day', - data: [{ periodStart: date, commitCount: 3 }], - metadata: { maxCommitCount: 3, totalCommits: 3 }, - }); - - // Act - render(); - await waitFor(() => expect(mockedGetHeatmapData).toHaveBeenCalled()); - - // Assert - const tooltipElements = screen.getAllByText(/3 commits/); - expect(tooltipElements.length).toBeGreaterThan(0); - }); - - test('displays author filters correctly sorted by commit count', async () => { - // Arrange - const commits = [ - { - sha: '1', - message: 'msg1', - date: new Date().toISOString(), - authorName: 'Alice', - authorEmail: 'alice@example.com', - }, - { - sha: '2', - message: 'msg2', - date: new Date().toISOString(), - authorName: 'Bob', - authorEmail: 'bob@example.com', - }, - { - sha: '3', - message: 'msg3', - date: new Date().toISOString(), - authorName: 'Bob', - authorEmail: 'bob@example.com', - }, - ]; - - // Act - render(); - await waitFor(() => expect(mockedGetHeatmapData).toHaveBeenCalled()); - const combo = screen.getByRole('combobox'); - await userEvent.click(combo); - const options = screen.getAllByRole('option'); - - // Assert - expect(options[0].textContent).toContain('Bob (2)'); - expect(options[1].textContent).toContain('Alice (1)'); - }); -}); diff --git a/apps/frontend/__tests__/components/CommitHeatmap.test.tsx b/apps/frontend/__tests__/components/CommitHeatmap.test.tsx new file mode 100644 index 00000000..1880eece --- /dev/null +++ b/apps/frontend/__tests__/components/CommitHeatmap.test.tsx @@ -0,0 +1,137 @@ +import { render, screen } from '@testing-library/react'; +import { CommitHeatmap } from '../../src/components/CommitHeatmap'; +import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; +import { CommitHeatmapData } from '@gitray/shared-types'; + +describe('CommitHeatmap Component', () => { + const mockDate = new Date('2023-12-31T12:00:00Z'); + + beforeEach(() => { + // Arrange: Fix current date for deterministic tests + vi.useFakeTimers(); + vi.setSystemTime(mockDate); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + test('should render properly with no data', () => { + // Act + render(); + + // Assert + expect(screen.getByText('Commit Activity')).toBeInTheDocument(); + // It should render the heatmap grid but empty (all bg-muted/30) + const items = document.querySelectorAll('.rounded-sm'); + expect(items.length).toBeGreaterThan(300); // 365 days + some legends + }); + + test('should log a warning if isValidHeatmap is false', () => { + // Arrange + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const heatmapData: CommitHeatmapData = { data: [], timePeriod: 'day' }; + + // Act + render(); + + // Assert + expect(consoleSpy).toHaveBeenCalledWith( + '[CommitHeatmap] Heatmap data may be incomplete or invalid' + ); + consoleSpy.mockRestore(); + }); + + test('should render heatmap with data from heatmapData prop', () => { + // Arrange + const heatmapData: CommitHeatmapData = { + data: [ + { periodStart: '2023-12-30T00:00:00Z', commitCount: 15 }, // High intensity + { periodStart: '2023-12-31T00:00:00Z', commitCount: 2 }, // Low intensity + ], + timePeriod: 'day', + }; + + // Act + render(); + + // Assert + // Check that we have elements with the specific intensity classes + const highIntensityNodes = document.querySelectorAll('.bg-primary'); + const lowIntensityNodes = document.querySelectorAll('.bg-primary\\/20'); + + // There are legendary items colored as well, so we expect at least 1 + legend items + expect(highIntensityNodes.length).toBeGreaterThanOrEqual(1); + expect(lowIntensityNodes.length).toBeGreaterThanOrEqual(1); + }); + + test('should render heatmap with data fallback from commits prop', () => { + // Arrange + const commits = [ + { + hash: 'abc', + message: 'test', + author: 'tester', + date: '2023-12-25T10:00:00Z', + filesChanged: 1, + insertions: 10, + deletions: 5, + }, + { + hash: 'def', + message: 'test2', + author: 'tester', + date: '2023-12-25T11:00:00Z', + filesChanged: 1, + insertions: 10, + deletions: 5, + }, + ]; + + // Act + // Omitting heatmapData to trigger fallback + render(); + + // Assert + // 2 commits on same day -> expect bg-primary/20 + const intensityNodes = document.querySelectorAll('.bg-primary\\/20'); + expect(intensityNodes.length).toBeGreaterThanOrEqual(1); + }); + + test('should restrict data visibility based on monthsToShow prop', () => { + // Arrange + const twelveMonthsAgo = new Date(mockDate); + twelveMonthsAgo.setMonth(twelveMonthsAgo.getMonth() - 11); + + const commits = [ + { + hash: 'old', + message: 'old commit', + author: 'tester', + date: twelveMonthsAgo.toISOString(), + filesChanged: 1, + insertions: 1, + deletions: 1, + }, + { + hash: 'new', + message: 'new commit', + author: 'tester', + date: mockDate.toISOString(), + filesChanged: 1, + insertions: 1, + deletions: 1, + }, + ]; + + // Act: only show 3 months + render(); + + // Assert + // The old commit should not be colored because it's outside the 3 months window. + // The new commit should be colored (count 1 -> bg-primary/20). + const paintedNodes = document.querySelectorAll('.bg-primary\\/20'); + // Legends uses 1 bg-primary/20, plus the new commit = 2 + expect(paintedNodes.length).toBe(2); + }); +}); diff --git a/apps/frontend/__tests__/components/CommitList.test.tsx b/apps/frontend/__tests__/components/CommitList.test.tsx deleted file mode 100644 index b88ce4d4..00000000 --- a/apps/frontend/__tests__/components/CommitList.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import CommitList from '../../src/components/CommitList'; -import { Commit } from '@gitray/shared-types'; - -describe('CommitList Component', () => { - test.skip('should render commit list with data', () => { - // Arrange - const mockCommits: Commit[] = [ - { - sha: '123abc456def', - message: 'Test commit message', - date: '2023-05-01T12:00:00Z', - authorName: 'Test User', - authorEmail: 'test@example.com', - }, - ]; - - // Act - render(); - - // Assert - expect(screen.getByText('Repository Commits')).toBeDefined(); - expect(screen.getByText('123abc4')).toBeDefined(); // First 7 chars of SHA - expect(screen.getByText('Test commit message')).toBeDefined(); - expect(screen.getByText('Test User')).toBeDefined(); - }); - - test.skip('should render nothing when commits array is empty', () => { - // Arrange - const emptyCommits: Commit[] = []; - - // Act - const { container } = render(); - - // Assert - expect(container.firstChild).toBeNull(); - }); -}); diff --git a/apps/frontend/__tests__/components/DashboardPage.test.tsx b/apps/frontend/__tests__/components/DashboardPage.test.tsx new file mode 100644 index 00000000..a252125f --- /dev/null +++ b/apps/frontend/__tests__/components/DashboardPage.test.tsx @@ -0,0 +1,131 @@ +import { render, screen, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import DashboardPage from '../../src/components/DashboardPage'; +import { describe, test, expect, beforeEach, vi } from 'vitest'; +import * as api from '../../src/services/api'; + +// Mock dependencies BEFORE imports +vi.mock('../../src/services/api', () => ({ + getFileAnalysis: vi.fn(), + getCodeChurn: vi.fn(), + getRepositorySummary: vi.fn(), +})); + +// Mock sub-components that might do complex rendering +vi.mock('../../src/components/CommitHeatmap', () => ({ + CommitHeatmap: () =>
, +})); +vi.mock('../../src/components/ActivityChart', () => ({ + ActivityChart: () =>
, +})); +vi.mock('../../src/components/FileDistributionChart', () => ({ + FileDistributionChart: () =>
, +})); +vi.mock('../../src/components/CodeChurnChart', () => ({ + CodeChurnChart: () =>
, +})); +vi.mock('../../src/components/GraphViewTimeline', () => ({ + GraphViewTimeline: () =>
, +})); + +describe('DashboardPage Component', () => { + const mockRepoUrl = 'https://github.com/test/repo'; + const mockCommits = [ + { + hash: '123', + message: 'test', + date: '2023-01-01', + author: 'tester', + filesChanged: 1, + insertions: 1, + deletions: 1, + }, + ]; + + beforeEach(() => { + // Arrange: reset API mocks + vi.clearAllMocks(); + + // Default resolve values + (api.getRepositorySummary as any).mockResolvedValue({ + stats: { totalCommits: 100, branches: 2, contributors: 3 }, + lastCommit: { relativeTime: '2 days ago' }, + created: { date: '2023-01-01T00:00:00Z' }, + repository: { name: 'test-repo', url: 'https://github.com/test/repo' }, + }); + (api.getFileAnalysis as any).mockResolvedValue({ ts: 10 }); + (api.getCodeChurn as any).mockResolvedValue([]); + }); + + test('should render dashboard tabs and overview by default', async () => { + // Act + await act(async () => { + render( + + ); + }); + + // Assert + expect(screen.getAllByText('Overview')[0]).toBeInTheDocument(); + expect(screen.getAllByText('Analytics')[0]).toBeInTheDocument(); + expect(screen.getByTestId('mock-activity-chart')).toBeInTheDocument(); + }); + + test('should trigger API calls on mount with repoUrl', async () => { + // Act + await act(async () => { + render( + + ); + }); + + // Assert + expect(api.getRepositorySummary).toHaveBeenCalledWith( + 'https://github.com/test/repo2' + ); + expect(api.getFileAnalysis).toHaveBeenCalledWith( + 'https://github.com/test/repo2' + ); + expect(api.getCodeChurn).toHaveBeenCalledWith( + 'https://github.com/test/repo2' + ); + }); + + test('should handle missing heatmap data gracefully', async () => { + // Act + await act(async () => { + render( + + ); + }); + + // Assert + // It should still render the main dashboard structure + expect(screen.getByText(/Repository Analytics/i)).toBeInTheDocument(); + + // Switch to Heatmap tab and ensure it handles it gracefully + await act(async () => { + const user = userEvent.setup(); + const tab = screen.getByRole('tab', { name: /Heatmap/i }); + await user.click(tab); + }); + + expect(await screen.findByTestId('mock-heatmap')).toBeInTheDocument(); + }); +}); diff --git a/apps/frontend/__tests__/components/FileTypeList.test.tsx b/apps/frontend/__tests__/components/FileTypeList.test.tsx new file mode 100644 index 00000000..d8c75ed5 --- /dev/null +++ b/apps/frontend/__tests__/components/FileTypeList.test.tsx @@ -0,0 +1,180 @@ +import { render, screen } from '@testing-library/react'; +import { FileTypeList } from '../../src/components/FileTypeList'; +import { describe, test, expect } from 'vitest'; + +describe('FileTypeList Component', () => { + test('should render properly with no data', () => { + // Arrange + render(); + + // Assert + expect(screen.getByText('File Type Distribution')).toBeInTheDocument(); + expect(screen.getByText('No file data available')).toBeInTheDocument(); + }); + + test('should render file types and calculate correct percentages', () => { + // Arrange + const allExtensions = [ + '.ts', + '.tsx', + '.js', + '.jsx', + '.mjs', + '.cjs', + '.py', + '.go', + '.rs', + '.java', + '.c', + '.cpp', + '.cs', + '.rb', + '.php', + '.swift', + '.kt', + '.scala', + '.clj', + '.ex', + '.erl', + '.hs', + '.lua', + '.r', + '.m', + '.dart', + '.sh', + '.bash', + '.ps1', + '.psd1', + '.psm1', + '.pl', + '.vb', + '.fs', + '.html', + '.css', + '.scss', + '.sass', + '.less', + '.xml', + '.svg', + '.vue', + '.json', + '.yaml', + '.yml', + '.toml', + '.csv', + '.sql', + '.md', + '.txt', + '.rst', + '.tex', + '.pdf', + '.doc', + '.png', + '.jpg', + '.jpeg', + '.gif', + '.webp', + '.bmp', + '.ico', + '.tif', + '.riv', + '.env', + '.gitignore', + '.dockerignore', + '.editorconfig', + '.eslintrc', + '.prettierrc', + '.lock', + '.log', + '.zip', + '.tar', + '.gz', + '', // No extension + '.unknown', // Fallback + // specific icon branches + '.mp4', + '.mp3', + '.db', + '.sqlite', + '.xlsx', + '.pem', + ]; + + const mockDistribution = { + extensions: {} as Record< + string, + { count: number; percentage: number; size: number; averageSize: number } + >, + categories: { + code: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + documentation: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + configuration: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + assets: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + other: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + }, + directories: [], + metadata: { + totalFiles: 100, + totalSize: 1024000, // 1MB, so ~10KB/file + analyzedAt: new Date().toISOString(), + repositorySize: '1 MB', + }, + }; + + allExtensions.forEach((ext) => { + mockDistribution.extensions[ext] = { + count: 1, + percentage: 1, + size: 10, + averageSize: 10, + }; + }); + + // Act + render(); + + // Assert + expect(screen.getByText('File Type Distribution')).toBeInTheDocument(); + + // Random checks + expect(screen.getByText('TypeScript')).toBeInTheDocument(); + expect(screen.getByText('Python')).toBeInTheDocument(); + expect(screen.getByText('JSON')).toBeInTheDocument(); + expect(screen.getByText('files without extension')).toBeInTheDocument(); + + // Check Average File Size + expect(screen.getByText('≈ 10 KB')).toBeInTheDocument(); + }); + + test('should format multiple extensions of the same display type together', () => { + // Arrange + const mockDistribution = { + extensions: { + '.ts': { count: 10, percentage: 40.05, size: 100, averageSize: 10 }, + '.tsx': { count: 10, percentage: 40.05, size: 100, averageSize: 10 }, + }, + categories: { + code: { count: 20, percentage: 80.1, size: 0, averageSize: 0 }, + documentation: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + configuration: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + assets: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + other: { count: 0, percentage: 0, size: 0, averageSize: 0 }, + }, + directories: [], + metadata: { + totalFiles: 20, + totalSize: 0, + analyzedAt: new Date().toISOString(), + repositorySize: '0 B', + }, + }; + + // Act + render(); + + // Assert + // They both map to "TypeScript" + expect(screen.getByText('TypeScript')).toBeInTheDocument(); + expect(screen.getByText('.ts 40.05%, .tsx 40.05%')).toBeInTheDocument(); + }); +}); diff --git a/apps/frontend/__tests__/components/GitDiffViewer.test.tsx b/apps/frontend/__tests__/components/GitDiffViewer.test.tsx new file mode 100644 index 00000000..a663ef87 --- /dev/null +++ b/apps/frontend/__tests__/components/GitDiffViewer.test.tsx @@ -0,0 +1,59 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, test, expect } from 'vitest'; +import { GitDiffViewer } from '../../src/components/GitDiffViewer'; + +describe('GitDiffViewer Component', () => { + test('should render the diff viewer with correct totals', () => { + // Act + render(); + + // Assert + expect(screen.getByText('Git Diff Viewer')).toBeInTheDocument(); + + // totalAdditions: 45 + 23 + 8 = 76 + // totalDeletions: 12 + 5 + 2 = 19 + expect(screen.getByText(/76 additions/i)).toBeInTheDocument(); + expect(screen.getByText(/19 deletions/i)).toBeInTheDocument(); + expect(screen.getByText(/3 files changed/i)).toBeInTheDocument(); + + // verify file names are displayed + expect( + screen.getByText('src/components/Dashboard.tsx') + ).toBeInTheDocument(); + expect(screen.getByText('src/lib/api.ts')).toBeInTheDocument(); + expect(screen.getByText('src/styles/globals.css')).toBeInTheDocument(); + }); + + test('should toggle file diff visibility when clicked', () => { + // Arrange + render(); + const firstFileRow = screen.getByRole('button', { + name: /src\/components\/Dashboard.tsx/i, + }); + const secondFileRow = screen.getByRole('button', { + name: /src\/lib\/api.ts/i, + }); + + // Act & Assert initially (Dashboard is open by default, api.ts is closed) + expect( + screen.getByText("import { Card } from './ui/card';") + ).toBeInTheDocument(); + expect( + screen.queryByText('async function fetchData() {') + ).not.toBeInTheDocument(); + + // Act - close Dashboard + fireEvent.click(firstFileRow); + // Assert + expect( + screen.queryByText("import { Card } from './ui/card';") + ).not.toBeInTheDocument(); + + // Act - open api.ts + fireEvent.click(secondFileRow); + // Assert + expect( + screen.getByText('async function fetchData() {') + ).toBeInTheDocument(); + }); +}); diff --git a/apps/frontend/__tests__/components/PremiumFeatures.test.tsx b/apps/frontend/__tests__/components/PremiumFeatures.test.tsx new file mode 100644 index 00000000..ff998bc5 --- /dev/null +++ b/apps/frontend/__tests__/components/PremiumFeatures.test.tsx @@ -0,0 +1,72 @@ +import { render, screen } from '@testing-library/react'; +import { describe, test, expect } from 'vitest'; +import { PremiumFeatures } from '../../src/components/PremiumFeatures'; + +describe('PremiumFeatures Component', () => { + test('should render the premium features list by default', () => { + // Act + render(); + + // Assert + expect( + screen.getByRole('tab', { name: /Premium Features/i }) + ).toBeInTheDocument(); + + // Check for categories + expect(screen.getByText('Visualizations')).toBeInTheDocument(); + expect(screen.getByText('Analysis Tools')).toBeInTheDocument(); + expect(screen.getByText('Multi-Project Management')).toBeInTheDocument(); + expect(screen.getByText('Desktop & Mobile')).toBeInTheDocument(); + + // Check for specific features + expect( + screen.getByText('Time-lapse Animation (Gource)') + ).toBeInTheDocument(); + expect(screen.getByText('UML Diagram Generation')).toBeInTheDocument(); + expect( + screen.getByText('Manage Multiple Repositories') + ).toBeInTheDocument(); + + // Check for CTA + expect(screen.getByText('Unlock All Premium Features')).toBeInTheDocument(); + }); + + test('should render only the pricing compare view when showPricingOnly is true', () => { + // Act + render(); + + // Assert + // Check for pricing plans + expect(screen.getByRole('heading', { name: 'Free' })).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: 'Premium' }) + ).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Team' })).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: 'Enterprise' }) + ).toBeInTheDocument(); + + // Check for prices + expect(screen.getByText('$0')).toBeInTheDocument(); + expect(screen.getByText('$15')).toBeInTheDocument(); + expect(screen.getByText('$49')).toBeInTheDocument(); + expect(screen.getByText('Custom')).toBeInTheDocument(); + + // Check for the comparison table + expect(screen.getByText('Compare All Plans')).toBeInTheDocument(); + expect(screen.getByText('7 days')).toBeInTheDocument(); + expect(screen.getByText('30 days')).toBeInTheDocument(); + expect(screen.getByText('90 days')).toBeInTheDocument(); + + // Ensure features list is NOT rendered + expect( + screen.queryByRole('tab', { name: /Premium Features/i }) + ).not.toBeInTheDocument(); + expect( + screen.queryByText('Time-lapse Animation (Gource)') + ).not.toBeInTheDocument(); + expect( + screen.queryByText('Unlock All Premium Features') + ).not.toBeInTheDocument(); + }); +}); diff --git a/apps/frontend/__tests__/components/RiveLoader.test.tsx b/apps/frontend/__tests__/components/RiveLoader.test.tsx deleted file mode 100644 index 4760c2bb..00000000 --- a/apps/frontend/__tests__/components/RiveLoader.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { render, screen, act } from '@testing-library/react'; -import { vi } from 'vitest'; -import RiveLoader from '../../src/components/RiveLoader'; -import { useRive } from '@rive-app/react-canvas'; - -vi.mock('@rive-app/react-canvas', () => ({ useRive: vi.fn() })); - -const mockedUseRive = useRive as ReturnType; - -const MockRiveComponent = () =>
; - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe('RiveLoader Component (happy path, AAA)', () => { - test.skip('renders default loader and triggers callbacks', () => { - // Arrange - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - let options: Parameters[0] | undefined; - mockedUseRive.mockImplementation((opts) => { - options = opts; - return { RiveComponent: MockRiveComponent } as unknown as ReturnType< - typeof useRive - >; - }); - - // Act - render(); - - // Assert - expect(mockedUseRive).toHaveBeenCalled(); - expect(options).toBeDefined(); - expect(options?.src).toBe('/Logo_Animation_StateMachine_DarkMode.riv'); - expect(screen.getByTestId('rive')).toBeInTheDocument(); - expect(screen.getByText('Loading...')).toBeInTheDocument(); - - // Act - act(() => { - // Mock the callback parameters as needed by the implementation - options?.onLoad?.({} as any); - options?.onLoadError?.('test error' as any); - }); - - // Assert - expect(logSpy).toHaveBeenCalledWith('Rive animation loaded successfully'); - expect(errSpy).toHaveBeenCalledWith( - 'Failed to load Rive animation:', - 'test error' - ); - }); - - test.skip('accepts custom props', () => { - // Arrange - mockedUseRive.mockReturnValue({ - RiveComponent: MockRiveComponent, - } as unknown as ReturnType); - - // Act - const { container } = render( - - ); - - // Assert - const wrapper = container.firstChild as HTMLElement; - expect(wrapper).toHaveClass('flex', 'extra'); - const inner = screen.getByTestId('rive').parentElement as HTMLElement; - expect(inner.style.width).toBe('50px'); - expect(screen.getByText('wait')).toBeInTheDocument(); - }); -}); diff --git a/apps/frontend/__tests__/components/RiveLogo.test.tsx b/apps/frontend/__tests__/components/RiveLogo.test.tsx deleted file mode 100644 index 0e423ed4..00000000 --- a/apps/frontend/__tests__/components/RiveLogo.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { render, screen, act } from '@testing-library/react'; -import { vi } from 'vitest'; -import RiveLogo from '../../src/components/RiveLogo'; -import { useRive } from '@rive-app/react-canvas'; - -vi.mock('@rive-app/react-canvas', () => ({ useRive: vi.fn() })); - -const mockedUseRive = useRive as ReturnType; - -const MockRiveComponent = () =>
; - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe('RiveLogo Component (happy path, AAA)', () => { - test.skip('renders default logo and triggers callbacks', () => { - // Arrange - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - let options: Parameters[0] | undefined; - mockedUseRive.mockImplementation((opts) => { - options = opts; - return { RiveComponent: MockRiveComponent } as unknown as ReturnType< - typeof useRive - >; - }); - - // Act - const { container } = render(); - - // Assert - expect(mockedUseRive).toHaveBeenCalled(); - expect(options).toBeDefined(); - expect(options?.src).toBe('/Logo_Animation_StateMachine_DarkMode.riv'); - expect(screen.getByTestId('rive')).toBeInTheDocument(); - const wrapper = container.firstChild as HTMLElement; - expect(wrapper).toHaveClass('cursor-pointer'); - expect(wrapper).toHaveStyle({ width: '60px', height: '60px' }); - - // Act - act(() => { - options?.onLoad?.({} as any); - options?.onLoadError?.('err' as any); - }); - - // Assert - expect(logSpy).toHaveBeenCalledWith('Rive logo loaded successfully'); - expect(errSpy).toHaveBeenCalledWith('Failed to load Rive logo:', 'err'); - }); - - test.skip('accepts custom props', () => { - // Arrange - mockedUseRive.mockReturnValue({ - RiveComponent: MockRiveComponent, - } as unknown as ReturnType); - - // Act - const { container } = render( - - ); - - // Assert - const wrapper = container.firstChild as HTMLElement; - expect(wrapper).toHaveClass('flex', 'extra'); - expect(wrapper.className).not.toContain('cursor-pointer'); - expect(wrapper).toHaveStyle({ width: '40px', height: '40px' }); - }); -}); diff --git a/apps/frontend/__tests__/components/ui/use-mobile.test.ts b/apps/frontend/__tests__/components/ui/use-mobile.test.ts new file mode 100644 index 00000000..7ad77b0f --- /dev/null +++ b/apps/frontend/__tests__/components/ui/use-mobile.test.ts @@ -0,0 +1,90 @@ +import { renderHook, act } from '@testing-library/react'; +import { useIsMobile } from '../../../src/components/ui/use-mobile'; +import { describe, test, expect, beforeEach, vi } from 'vitest'; + +describe('useIsMobile Hook', () => { + let mockMatchMedia: ReturnType; + let resizeListeners: Array<() => void> = []; + + beforeEach(() => { + // Arrange + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: 1024, // Desktop by default + }); + + resizeListeners = []; + mockMatchMedia = vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addEventListener: (event: string, listener: () => void) => { + if (event === 'change') resizeListeners.push(listener); + }, + removeEventListener: (event: string, listener: () => void) => { + if (event === 'change') { + resizeListeners = resizeListeners.filter((l) => l !== listener); + } + }, + dispatchEvent: vi.fn(), + })); + + Object.defineProperty(window, 'matchMedia', { + writable: true, + configurable: true, + value: mockMatchMedia, + }); + }); + + test('should return false for desktop displays', () => { + // Arrange + window.innerWidth = 1024; + + // Act + const { result } = renderHook(() => useIsMobile()); + + // Assert + expect(result.current).toBe(false); + }); + + test('should return true for mobile displays', () => { + // Arrange + window.innerWidth = 500; + + // Act + const { result } = renderHook(() => useIsMobile()); + + // Assert + expect(result.current).toBe(true); + }); + + test('should update value on resize event', () => { + // Arrange + window.innerWidth = 1024; + const { result } = renderHook(() => useIsMobile()); + + expect(result.current).toBe(false); + + // Act + act(() => { + window.innerWidth = 500; + resizeListeners.forEach((listener) => listener()); + }); + + // Assert + expect(result.current).toBe(true); + }); + + test('should setup and cleanup event listeners', () => { + // Arrange + const { unmount } = renderHook(() => useIsMobile()); + expect(resizeListeners.length).toBe(1); + + // Act + unmount(); + + // Assert + expect(resizeListeners.length).toBe(0); + }); +}); diff --git a/apps/frontend/__tests__/components/ui/utils.test.ts b/apps/frontend/__tests__/components/ui/utils.test.ts new file mode 100644 index 00000000..adc6877c --- /dev/null +++ b/apps/frontend/__tests__/components/ui/utils.test.ts @@ -0,0 +1,68 @@ +import { describe, test, expect } from 'vitest'; +import { cn } from '../../../src/components/ui/utils'; + +describe('UI Utilities', () => { + describe('cn()', () => { + test('should merge standard classes', () => { + // Arrange + const class1 = 'text-red-500'; + const class2 = 'bg-blue-500'; + + // Act + const result = cn(class1, class2); + + // Assert + expect(result).toBe('text-red-500 bg-blue-500'); + }); + + test('should resolve tailwind conflicts', () => { + // Arrange + const baseClass = 'p-4 m-2 text-black'; + const overrideClass = 'p-2 text-white'; + + // Act + const result = cn(baseClass, overrideClass); + + // Assert + // Expected tailwind-merge to override previous padding/text classes + expect(result).toBe('m-2 p-2 text-white'); + }); + + test('should handle conditional classes properly', () => { + // Arrange + const isRed = true; + const isBlue = false; + + // Act + const result = cn('base-class', { + 'text-red-500': isRed, + 'text-blue-500': isBlue, + }); + + // Assert + expect(result).toBe('base-class text-red-500'); + }); + + test('should handle array inputs', () => { + // Arrange + const classArray = ['flex', 'flex-col', 'items-center']; + + // Act + const result = cn(classArray, 'justify-center'); + + // Assert + expect(result).toBe('flex flex-col items-center justify-center'); + }); + + test('should handle null and undefined', () => { + // Arrange + const inputs = ['btn', null, undefined, 'active']; + + // Act + const result = cn(inputs); + + // Assert + expect(result).toBe('btn active'); + }); + }); +}); diff --git a/apps/frontend/__tests__/main.test.tsx b/apps/frontend/__tests__/main.test.tsx deleted file mode 100644 index e15544bd..00000000 --- a/apps/frontend/__tests__/main.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { createRoot } from 'react-dom/client'; - -// any the modules -vi.mock('react-dom/client', () => ({ - createRoot: vi.fn(() => ({ - render: vi.fn(), - })), -})); - -vi.mock('react', async () => { - const actualReact = await vi.importActual('react'); - return { - ...actualReact, - StrictMode: ({ children }: { children: React.ReactNode }) => children, - }; -}); - -vi.mock('../src/App', () => ({ - default: () => null, -})); - -vi.mock('../src/index.css', () => ({})); - -describe('Main Entry Point', () => { - let mockRootElement: HTMLDivElement; - const originalGetElementById = document.getElementById; - - beforeEach(() => { - vi.clearAllMocks(); - mockRootElement = document.createElement('div'); - document.getElementById = vi.fn().mockReturnValue(mockRootElement); - }); - - afterEach(() => { - document.getElementById = originalGetElementById; - vi.resetModules(); - }); - - test('should render App component into root element', async () => { - // Act - Import main.tsx to trigger execution - await import('../src/main'); - - // Assert - expect(document.getElementById).toHaveBeenCalledWith('root'); - expect(createRoot).toHaveBeenCalledWith(mockRootElement); - const mockCreateRoot = vi.mocked(createRoot); - expect(mockCreateRoot.mock.results[0].value.render).toHaveBeenCalled(); - }); -}); diff --git a/apps/frontend/__tests__/pages/MainPage.test.tsx b/apps/frontend/__tests__/pages/MainPage.test.tsx deleted file mode 100644 index 883d4896..00000000 --- a/apps/frontend/__tests__/pages/MainPage.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import MainPage from '../../src/pages/MainPage'; -import { getRepositoryFullData } from '../../src/services/api'; -import { Commit } from '@gitray/shared-types'; - -// any the API module -vi.mock('../../src/services/api', () => ({ - getRepositoryFullData: vi.fn(), - getHeatmapData: vi.fn(), -})); - -const mockedGetRepositoryFullData = vi.mocked(getRepositoryFullData); - -// NOTE: These tests are temporarily disabled due to React hooks issues in test environment -// Frontend will be fully replaced in the near future, so these failures are acceptable -// Related to backend file analysis implementation - frontend tests unrelated to PR -describe.skip('MainPage Component', () => { - test('should fetch and display commits when repository URL is submitted', async () => { - // Arrange - const mockCommits: Commit[] = [ - { - sha: '123abc456def', - message: 'Test commit message', - date: '2023-05-01T12:00:00Z', - authorName: 'Test User', - authorEmail: 'test@example.com', - }, - ]; - - mockedGetRepositoryFullData.mockResolvedValue({ - commits: mockCommits, - heatmapData: { - timePeriod: 'day', - data: [], - metadata: { maxCommitCount: 0, totalCommits: 0 }, - }, - }); - render(); - - const input = screen.getByRole('textbox'); - const button = screen.getByRole('button', { name: /visualize/i }); - - // Act - fireEvent.change(input, { - target: { value: 'https://github.com/test/repo.git' }, - }); - fireEvent.click(button); - - // Assert - await waitFor(() => { - expect(mockedGetRepositoryFullData).toHaveBeenCalledWith( - 'https://github.com/test/repo.git', - 'day' - ); - }); - - // The heatmap should be displayed by default - expect(screen.getByText('Repository Activity')).toBeInTheDocument(); - }); -}); diff --git a/apps/frontend/__tests__/services/api.test.ts b/apps/frontend/__tests__/services/api.test.ts new file mode 100644 index 00000000..5f5c0943 --- /dev/null +++ b/apps/frontend/__tests__/services/api.test.ts @@ -0,0 +1,369 @@ +// Mock external dependencies BEFORE any imports +import { describe, test, expect, beforeEach, vi } from 'vitest'; +vi.mock('axios', async () => { + const actual = await vi.importActual('axios'); + const mockAxiosInstance = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + interceptors: { + request: { use: vi.fn(), eject: vi.fn() }, + response: { use: vi.fn(), eject: vi.fn() }, + }, + }; + return { + default: { + ...actual.default, + create: vi.fn(() => mockAxiosInstance), + isAxiosError: vi.fn((_payload) => true), // Mock generic isAxiosError + }, + }; +}); + +import axios from 'axios'; +import { + getRepositoryHeatmap, + getRepositoryCommits, + getFileAnalysis, + getCodeChurn, + getRepositorySummary, + getRepositoryFullData, + getRepositoryContributors, +} from '../../src/services/api'; + +describe('API Service Unit Tests', () => { + let mockGet: ReturnType; + + beforeEach(() => { + // Arrange: Reset all mocks before each test + vi.clearAllMocks(); + // Get the mocked instance's get method + const mockAxiosInstance = axios.create(); + mockGet = mockAxiosInstance.get as ReturnType; + + // Explicitly mock isAxiosError to behave properly for these tests + (axios.isAxiosError as any).mockImplementation( + (payload: any) => payload?.isAxiosError === true + ); + }); + + describe('getRepositoryCommits', () => { + const mockRepoUrl = 'https://github.com/user/repo'; + const mockNormalizedUrl = 'https://github.com/user/repo.git'; + + test('should fetch commits successfully', async () => { + // Arrange + const mockResponse = { + data: { + commits: [{ hash: '12345', message: 'Test commit' }], + page: 1, + limit: 100, + }, + }; + mockGet.mockResolvedValueOnce(mockResponse); + + // Act + const result = await getRepositoryCommits(mockRepoUrl, 1, 100); + + // Assert + expect(mockGet).toHaveBeenCalledWith( + `/api/repositories/commits?repoUrl=${encodeURIComponent(mockNormalizedUrl)}&page=1&limit=100` + ); + expect(result).toEqual({ + commits: mockResponse.data.commits, + page: 1, + limit: 100, + }); + }); + + test('should append .git to repoUrl if missing', async () => { + // Arrange + const urlWithoutGit = 'https://github.com/test/repo'; + mockGet.mockResolvedValueOnce({ + data: { commits: [], page: 1, limit: 100 }, + }); + + // Act + await getRepositoryCommits(urlWithoutGit, 1, 50); + + // Assert + expect(mockGet).toHaveBeenCalledWith( + `/api/repositories/commits?repoUrl=${encodeURIComponent(urlWithoutGit + '.git')}&page=1&limit=50` + ); + }); + + test('should throw meaningful error on API failure', async () => { + // Arrange + const mockError = { + isAxiosError: true, + response: { data: { error: 'Repository not found' } }, + }; + mockGet.mockRejectedValueOnce(mockError); + + // Act & Assert + await expect(getRepositoryCommits(mockRepoUrl)).rejects.toThrow( + 'Server error: Repository not found' + ); + }); + }); + + describe('getRepositoryHeatmap', () => { + const mockRepoUrl = 'https://github.com/user/repo'; + + test('should fetch heatmap without filters', async () => { + // Arrange + const mockResponse = { data: { heatmapData: { 12345: 5 } } }; + mockGet.mockResolvedValueOnce(mockResponse); + + // Act + const result = await getRepositoryHeatmap(mockRepoUrl); + + // Assert + expect(mockGet).toHaveBeenCalledWith( + expect.stringContaining( + `/api/repositories/heatmap?repoUrl=${encodeURIComponent(mockRepoUrl + '.git')}` + ) + ); + expect(result).toEqual(mockResponse.data.heatmapData); + }); + + test('should append filter query parameters', async () => { + // Arrange + mockGet.mockResolvedValueOnce({ data: { heatmapData: {} } }); + const filters = { + author: 'Alice', + fromDate: '2023-01-01', + toDate: '2023-12-31', + }; + + // Act + await getRepositoryHeatmap(mockRepoUrl, filters); + + // Assert + expect(mockGet).toHaveBeenCalledWith( + expect.stringContaining('author=Alice') + ); + expect(mockGet).toHaveBeenCalledWith( + expect.stringContaining('fromDate=2023-01-01') + ); + expect(mockGet).toHaveBeenCalledWith( + expect.stringContaining('toDate=2023-12-31') + ); + }); + }); + + describe('getFileAnalysis', () => { + test('should fetch file analysis successfully', async () => { + // Arrange + const mockResponse = { data: { ts: 10, md: 5 } }; + mockGet.mockResolvedValueOnce(mockResponse); + + // Act + const result = await getFileAnalysis('test-repo'); + + // Assert + expect(mockGet).toHaveBeenCalledWith( + expect.stringContaining( + `/api/commits/file-analysis?repoUrl=test-repo.git` + ) + ); + expect(result).toEqual(mockResponse.data); + }); + }); + + describe('getCodeChurn', () => { + test('should fetch code churn with all parameters', async () => { + // Arrange + const mockResponse = { data: { churnData: [] } }; + mockGet.mockResolvedValueOnce(mockResponse); + + // Act + await getCodeChurn('test-repo', '2023-01-01', '2023-12-31', 5, [ + '.ts', + '.tsx', + ]); + + // Assert + const url = mockGet.mock.calls[0][0]; + expect(url).toContain('repoUrl=test-repo.git'); + expect(url).toContain('fromDate=2023-01-01'); + expect(url).toContain('toDate=2023-12-31'); + expect(url).toContain('minChanges=5'); + expect(url).toContain('extensions=.ts%2C.tsx'); + }); + }); + + describe('getRepositorySummary', () => { + test('should fetch summary successfully', async () => { + // Arrange + const mockResponse = { data: { summary: { commitCount: 100 } } }; + mockGet.mockResolvedValueOnce(mockResponse); + + // Act + const result = await getRepositorySummary('repo'); + + // Assert + expect(mockGet).toHaveBeenCalledWith( + expect.stringContaining(`/api/repositories/summary?repoUrl=repo.git`) + ); + expect(result).toEqual(mockResponse.data.summary); + }); + }); + + describe('getRepositoryFullData', () => { + test('should fetch full data successfully', async () => { + // Arrange + const mockResponse = { + data: { + commits: [], + heatmapData: {}, + page: 1, + limit: 100, + isValidHeatmap: true, + }, + }; + mockGet.mockResolvedValueOnce(mockResponse); + + // Act + const result = await getRepositoryFullData('repo', 'week', 1, 100, { + author: 'Alice', + }); + + // Assert + const url = mockGet.mock.calls[0][0]; + expect(url).toContain('/api/repositories/full-data'); + expect(url).toContain('author=Alice'); + expect(result).toEqual(mockResponse.data); + }); + + test('should handle network error (no response)', async () => { + // Arrange + const mockError = { + isAxiosError: true, + request: {}, // Indicates no response received + }; + mockGet.mockRejectedValueOnce(mockError); + + // Act & Assert + await expect(getRepositoryFullData('repo')).rejects.toThrow( + 'No response from server. Please check your network connection.' + ); + }); + + test('should handle server error response (e.g. 500)', async () => { + // Arrange + const mockError = { + isAxiosError: true, + response: { status: 500, data: { msg: 'Internal Server Error' } }, + config: { url: '/api/repositories/full-data' }, + }; + mockGet.mockRejectedValueOnce(mockError); + + // Act & Assert + await expect(getRepositoryFullData('repo')).rejects.toThrow( + 'Full-data error (500): {"msg":"Internal Server Error"}' + ); + }); + + test('should handle non-axios error', async () => { + // Arrange + (axios.isAxiosError as any).mockImplementationOnce(() => false); + const mockError = new Error('Generic error'); + mockGet.mockRejectedValueOnce(mockError); + + // Act & Assert + await expect(getRepositoryFullData('repo')).rejects.toThrow( + 'An unexpected error occurred' + ); + }); + + test('should handle axios error without response or request', async () => { + // Arrange + const mockError = { + isAxiosError: true, + message: 'Network Error', + }; + mockGet.mockRejectedValueOnce(mockError); + + // Act & Assert + await expect(getRepositoryFullData('repo')).rejects.toThrow( + 'Error: Network Error' + ); + }); + }); + + describe('getRepositoryContributors', () => { + test('should fetch contributors successfully', async () => { + // Arrange + const mockResponse = { data: { contributors: [{ login: 'alice' }] } }; + mockGet.mockResolvedValueOnce(mockResponse); + + // Act + const result = await getRepositoryContributors('repo'); + + // Assert + expect(mockGet).toHaveBeenCalledWith( + expect.stringContaining( + `/api/repositories/contributors?repoUrl=repo.git` + ) + ); + expect(result).toEqual(mockResponse.data.contributors); + }); + + test('should handle network error (no response)', async () => { + // Arrange + const mockError = { + isAxiosError: true, + request: {}, + }; + mockGet.mockRejectedValueOnce(mockError); + + // Act & Assert + await expect(getRepositoryContributors('repo')).rejects.toThrow( + 'No response from server. Please check your network connection.' + ); + }); + + test('should handle server error response (e.g. 500)', async () => { + // Arrange + const mockError = { + isAxiosError: true, + response: { status: 500, data: { msg: 'Internal Server Error' } }, + config: { url: '/api/repositories/contributors' }, + }; + mockGet.mockRejectedValueOnce(mockError); + + // Act & Assert + await expect(getRepositoryContributors('repo')).rejects.toThrow( + 'Contributors error (500): {"msg":"Internal Server Error"}' + ); + }); + + test('should handle non-axios error', async () => { + // Arrange + (axios.isAxiosError as any).mockImplementationOnce(() => false); + const mockError = new Error('Generic error'); + mockGet.mockRejectedValueOnce(mockError); + + // Act & Assert + await expect(getRepositoryContributors('repo')).rejects.toThrow( + 'An unexpected error occurred' + ); + }); + + test('should handle axios error without response or request', async () => { + // Arrange + const mockError = { + isAxiosError: true, + message: 'Network Error', + }; + mockGet.mockRejectedValueOnce(mockError); + + // Act & Assert + await expect(getRepositoryContributors('repo')).rejects.toThrow( + 'Error: Network Error' + ); + }); + }); +}); diff --git a/apps/frontend/__tests__/services/api.test.tsx b/apps/frontend/__tests__/services/api.test.tsx deleted file mode 100644 index 7261cec8..00000000 --- a/apps/frontend/__tests__/services/api.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { getWorkspaceCommits } from '../../src/services/api'; -import { Commit } from '@gitray/shared-types'; - -// any axios -vi.mock('axios', () => { - const mockPost = vi.fn(); - const mockCreate = vi.fn(() => ({ post: mockPost })); - const mockIsAxiosError = vi.fn(); - - return { - default: { - create: mockCreate, - isAxiosError: mockIsAxiosError, - }, - }; -}); - -describe('API Service', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - test('should fetch commits successfully', async () => { - // Arrange - const repoUrl = 'https://github.com/user/repo.git'; - const expectedCommits: Commit[] = [ - { - sha: 'abc123', - message: 'Test commit', - date: '2023-05-01T12:00:00Z', - authorName: 'Test User', - authorEmail: 'test@example.com', - }, - ]; - - const mockResponse = { - data: { commits: expectedCommits }, - status: 200, - statusText: 'OK', - headers: {}, - config: {}, - }; - - // Access the mocked functions - const axios = await import('axios'); - const mockAxiosInstance = (axios.default.create as any)(); - mockAxiosInstance.post.mockResolvedValueOnce(mockResponse); - - // Act - const result = await getWorkspaceCommits(repoUrl); - - // Assert - expect(result).toEqual(expectedCommits); - expect(mockAxiosInstance.post).toHaveBeenCalledWith('/api/repositories', { - repoUrl, - }); - }); - - test('should handle API errors correctly', async () => { - // Arrange - const repoUrl = 'https://github.com/user/repo.git'; - const error = new Error('Network error'); - - // Access the mocked functions - const axios = await import('axios'); - const mockAxiosInstance = (axios.default.create as any)(); - mockAxiosInstance.post.mockRejectedValueOnce(error); - (axios.default.isAxiosError as unknown as any).mockReturnValueOnce(true); - - // Act & Assert - await expect(getWorkspaceCommits(repoUrl)).rejects.toThrow('Network error'); - expect(mockAxiosInstance.post).toHaveBeenCalledWith('/api/repositories', { - repoUrl, - }); - }); -}); diff --git a/apps/frontend/__tests__/services/apiExtended.test.tsx b/apps/frontend/__tests__/services/apiExtended.test.tsx deleted file mode 100644 index 17d7d26e..00000000 --- a/apps/frontend/__tests__/services/apiExtended.test.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { getHeatmapData, getRepositoryFullData } from '../../src/services/api'; -import { CommitHeatmapData } from '@gitray/shared-types'; - -// any axios -vi.mock('axios', () => { - const mockGet = vi.fn(); - const mockPost = vi.fn(); - const mockCreate = vi.fn(() => ({ get: mockGet, post: mockPost })); - const mockIsAxiosError = vi.fn(); - - return { - default: { - create: mockCreate, - isAxiosError: mockIsAxiosError, - }, - }; -}); - -describe('API Service extended', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - test('getHeatmapData constructs query params and returns data', async () => { - // Arrange - const heatmap: CommitHeatmapData = { - timePeriod: 'day', - data: [], - metadata: { maxCommitCount: 0, totalCommits: 0 }, - }; - - // Access the mocked functions - const axios = await import('axios'); - const mockAxiosInstance = (axios.default.create as any)(); - mockAxiosInstance.get.mockResolvedValueOnce({ data: heatmap }); - - // Act - const result = await getHeatmapData('url', 'day', { author: 'Me' }); - - // Assert - expect(mockAxiosInstance.get).toHaveBeenCalled(); - expect(result).toEqual(heatmap); - }); - - test('getHeatmapData handles network error', async () => { - // Arrange - const error = { - request: {}, - message: 'Network', - }; - - // Access the mocked functions - const axios = await import('axios'); - const mockAxiosInstance = (axios.default.create as any)(); - mockAxiosInstance.get.mockRejectedValueOnce(error); - (axios.default.isAxiosError as unknown as any).mockReturnValueOnce(true); - - // Act & Assert - await expect(getHeatmapData('url', 'day')).rejects.toThrow( - 'No response from server' - ); - }); - - test('getRepositoryFullData posts payload and returns commits and heatmap', async () => { - // Arrange - const response = { - data: { - commits: [], - heatmapData: { - timePeriod: 'day', - data: [], - metadata: { maxCommitCount: 0, totalCommits: 0 }, - }, - }, - }; - - // Access the mocked functions - const axios = await import('axios'); - const mockAxiosInstance = (axios.default.create as any)(); - mockAxiosInstance.post.mockResolvedValueOnce(response); - - // Act - const result = await getRepositoryFullData('url', 'day'); - - // Assert - expect(mockAxiosInstance.post).toHaveBeenCalledWith( - '/api/repositories/full-data', - { repoUrl: 'url', timePeriod: 'day', filterOptions: undefined } - ); - expect(result).toEqual({ - commits: [], - heatmapData: response.data.heatmapData, - }); - }); - - test('getRepositoryFullData handles server error', async () => { - // Arrange - const error = { - response: { data: { error: 'fail' } }, - }; - - // Access the mocked functions - const axios = await import('axios'); - const mockAxiosInstance = (axios.default.create as any)(); - mockAxiosInstance.post.mockRejectedValueOnce(error); - (axios.default.isAxiosError as unknown as any).mockReturnValueOnce(true); - - // Act & Assert - await expect(getRepositoryFullData('url')).rejects.toThrow( - 'Server error: fail' - ); - }); -}); diff --git a/apps/frontend/__tests__/utils/dateUtils.test.tsx b/apps/frontend/__tests__/utils/dateUtils.test.tsx deleted file mode 100644 index 3d5a8df9..00000000 --- a/apps/frontend/__tests__/utils/dateUtils.test.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { - formatDateByPeriod, - generateDateRange, - getColorShade, - createTooltipText, -} from '../../src/utils/dateUtils'; - -describe('dateUtils', () => { - describe('formatDateByPeriod', () => { - test('formats dates for each period', () => { - // Arrange - const date = new Date('2023-04-15T12:00:00Z'); - expect(formatDateByPeriod(date, 'day')).toMatch('Apr'); - expect(formatDateByPeriod(date, 'month')).toBe('April 2023'); - expect(formatDateByPeriod(date, 'year')).toBe('2023'); - - const week = formatDateByPeriod(date, 'week'); - expect(week).toMatch('Apr'); - expect(week).toMatch('2023'); - }); - }); - - describe('generateDateRange', () => { - test('creates a range for days', () => { - // Arrange - const start = new Date('2023-01-01'); - const end = new Date('2023-01-03'); - - // Act - const range = generateDateRange(start, end, 'day'); - - // Assert - expect(range).toHaveLength(3); - expect(range[0]).toContain('Jan'); - }); - - test('creates a range for months', () => { - // Arrange - const start = new Date('2023-01-01'); - const end = new Date('2023-03-01'); - - // Act - const range = generateDateRange(start, end, 'month'); - - // Assert - expect(range).toEqual(['January 2023', 'February 2023', 'March 2023']); - }); - }); - - describe('getColorShade', () => { - test('returns correct shades depending on intensity', () => { - // Act & Assert - expect(getColorShade(0, 10)).toBe('#ebedf0'); - expect(getColorShade(1, 5)).toBe('#40c463'); - expect(getColorShade(2, 5)).toBe('#30a14e'); - expect(getColorShade(3, 5)).toBe('#216e39'); - expect(getColorShade(4, 5)).toBe('#0d4620'); - expect(getColorShade(5, 5)).toBe('#0d4620'); - }); - }); - - describe('createTooltipText', () => { - test('formats tooltip text with authors', () => { - // Act - const text = createTooltipText(2, 'Apr 10', ['Alice', 'Bob', 'Alice']); - - // Assert - expect(text).toBe('2 commits on Apr 10 by 2 authors'); - }); - - test('formats tooltip text without authors', () => { - // Act - const text = createTooltipText(1, 'Apr 10'); - - // Assert - expect(text).toBe('1 commit on Apr 10'); - }); - }); -}); -test('generate weekly and yearly ranges', () => { - // Arrange - const startWeek = new Date('2023-04-02'); - const endWeek = new Date('2023-04-16'); - - // Act - const weekRange = generateDateRange(startWeek, endWeek, 'week'); - - // Assert - expect(weekRange.length).toBe(3); - - // Arrange - const startYear = new Date('2021-01-01'); - const endYear = new Date('2023-12-31'); - - // Act - const yearRange = generateDateRange(startYear, endYear, 'year'); - - // Assert - expect(yearRange).toEqual(['2021', '2022', '2023']); -}); - -test('createTooltipText single author', () => { - // Act - const text = createTooltipText(3, 'Apr', ['Alice']); - - // Assert - expect(text).toBe('3 commits on Apr by Alice'); -}); diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.mjs similarity index 52% rename from apps/frontend/eslint.config.js rename to apps/frontend/eslint.config.mjs index 6253ce6e..155e8446 100644 --- a/apps/frontend/eslint.config.js +++ b/apps/frontend/eslint.config.mjs @@ -5,8 +5,8 @@ import reactRefresh from 'eslint-plugin-react-refresh'; import tseslint from 'typescript-eslint'; export default tseslint.config( - { ignores: ['dist'] }, - // Haupt-TypeScript-Konfiguration (unverändert) + { ignores: ['dist', 'build', 'node_modules', 'coverage'] }, + // Main TypeScript configuration { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], @@ -28,14 +28,37 @@ export default tseslint.config( }, // Configuration for test setup files { - files: ['**/test-setup*.ts'], + files: [ + '**/test-setup*.ts', + '**/__tests__/**/*.{ts,tsx}', + '**/*.test.{ts,tsx}', + '**/*.spec.{ts,tsx}', + ], rules: { - '@typescript-eslint/no-explicit-any': 'off', // Disable any rule for test setup files + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + // Configuration for shadcn/ui components - allow helper exports and any types + { + files: ['**/components/ui/**/*.{ts,tsx}'], + rules: { + 'react-refresh/only-export-components': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, + }, + // Configuration for chart components using third-party libraries (Recharts) + { + files: [ + '**/components/*Chart*.{ts,tsx}', + '**/components/*Heatmap*.{ts,tsx}', + ], + rules: { + '@typescript-eslint/no-explicit-any': 'off', }, }, // Configuration for CommonJS files (config files, etc.) { - files: ['**/*.cjs', '**/vitest.config.ts'], + files: ['**/*.cjs', '**/vitest.config.ts', '**/vite.config.ts'], languageOptions: { globals: { ...globals.node, diff --git a/apps/frontend/index.html b/apps/frontend/index.html index e4b78eae..d498e307 100644 --- a/apps/frontend/index.html +++ b/apps/frontend/index.html @@ -2,10 +2,10 @@ - - Vite + React + TS + GitRay - Git analytics +
diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 5f6f288a..92f4f360 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -1,46 +1,82 @@ { "name": "frontend", + "version": "0.1.0", "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "npx vite", - "build": "tsc -b", - "preview": "npx vite preview", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage" - }, "dependencies": { - "@gitray/shared-types": "workspace:*", - "@rive-app/react-canvas": "^4.21.3", - "apexcharts": "^4.7.0", - "axios": "^1.9.0", - "date-fns": "^4.1.0", - "react": "^19.1.0", - "react-apexcharts": "^1.7.0", - "react-calendar-heatmap": "^1.10.0", - "react-dom": "^19.1.0", - "react-select": "^5.10.1", - "typescript": "~5.7.3" + "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-alert-dialog": "^1.1.6", + "@radix-ui/react-aspect-ratio": "^1.1.2", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-context-menu": "^2.2.6", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-hover-card": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-menubar": "^1.1.6", + "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-radio-group": "^1.2.3", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slider": "^1.2.3", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-toggle": "^1.1.2", + "@radix-ui/react-toggle-group": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.8", + "@rive-app/react-canvas": "^4.24.0", + "axios": "^1.13.2", + "class-variance-authority": "^0.7.1", + "clsx": "*", + "cmdk": "^1.1.1", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.487.0", + "motion": "*", + "next-themes": "^0.4.6", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.55.0", + "react-resizable-panels": "^2.1.7", + "recharts": "^2.15.2", + "sonner": "^2.0.3", + "tailwind-merge": "*", + "vaul": "^1.1.2" }, "devDependencies": { - "@tailwindcss/cli": "^4.1.7", "@tailwindcss/postcss": "^4.1.7", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^14.3.1", "@testing-library/user-event": "^14.6.1", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.4.1", + "@types/node": "^20.10.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react-swc": "^3.10.2", "@vitest/coverage-v8": "^3.2.3", "@vitest/ui": "^3.2.3", "autoprefixer": "^10.4.21", - "eslint-plugin-react-refresh": "^0.4.20", "jsdom": "^26.1.0", "postcss": "^8.5.3", "tailwindcss": "^4.1.7", - "vite": "^6.3.5", + "typescript": "~5.7.3", + "vite": "6.3.5", "vitest": "^3.2.3" + }, + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "type-check": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" } } diff --git a/apps/frontend/public/vite.svg b/apps/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/apps/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/frontend/src/App.css b/apps/frontend/src/App.css deleted file mode 100644 index b9d355df..00000000 --- a/apps/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index 07501230..104ee46e 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -1,16 +1,188 @@ -import MainPage from './pages/MainPage'; -import './App.css'; - -/** - * Root component rendered by `main.tsx`. Acts as the entry point for the - * single-page application. - */ - -/** - * Simple wrapper component that renders the `MainPage` component. - */ -function App() { - return ; -} +import { useState, useEffect } from 'react'; +import { Header } from './components/Header'; +import { Footer } from './components/Footer'; +import { SettingsDrawer } from './components/SettingsDrawer'; +import { NewsDrawer } from './components/NewsDrawer'; +import { InfoModal } from './components/InfoModal'; +import { LandingPage } from './components/LandingPage'; +import DashboardPage from './components/DashboardPage'; +import { RiveLoader } from './components/RiveLoader'; +import { Toaster } from './components/ui/sonner'; +import { toast } from 'sonner'; +import { getRepositoryFullData } from './services/api'; +import { Commit, CommitHeatmapData } from '@gitray/shared-types'; + +type Page = 'landing' | 'dashboard'; +type InfoType = 'what' | 'private' | 'local' | null; + +export default function App() { + const [currentPage, setCurrentPage] = useState('landing'); + const [isSignedIn, setIsSignedIn] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('dark'); + const [settingsOpen, setSettingsOpen] = useState(false); + const [newsOpen, setNewsOpen] = useState(false); + const [infoModalType, setInfoModalType] = useState(null); + const [hasUnreadNews, setHasUnreadNews] = useState(true); + + // State for repository data + const [commits, setCommits] = useState([]); + const [heatmapData, setHeatmapData] = useState( + null + ); + const [isValidHeatmap, setIsValidHeatmap] = useState(true); + const [repoUrl, setRepoUrl] = useState(''); + + // Apply theme + useEffect(() => { + const root = window.document.documentElement; + root.classList.remove('light', 'dark'); + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') + .matches + ? 'dark' + : 'light'; + root.classList.add(systemTheme); + } else { + root.classList.add(theme); + } + }, [theme]); + + const handleAnalyze = async (url: string, mode: string) => { + setIsLoading(true); + toast.info('Analysis started!', { + description: `Analyzing repository in ${mode} mode...`, + }); + + try { + // Fetch real data from backend + console.log('Calling API with URL:', url); + const data = await getRepositoryFullData(url, 'day'); + console.log('API response:', data); + + // Store the fetched data in state + setCommits(data.commits); + setHeatmapData(data.heatmapData); + setIsValidHeatmap(data.isValidHeatmap); + setRepoUrl(url); + + // Navigate to dashboard + setCurrentPage('dashboard'); + setIsSignedIn(true); + setIsLoading(false); + + // Show warning if heatmap data is incomplete + if (!data.isValidHeatmap) { + toast.warning('Heatmap data may be incomplete', { + description: 'Some heatmap visualizations may not display correctly.', + }); + } + + toast.success('Analysis complete!', { + description: `Successfully analyzed ${data.commits.length} commits.`, + }); + } catch (error) { + setIsLoading(false); + console.error('Analysis error details:', error); + toast.error('Analysis failed', { + description: + error instanceof Error + ? error.message + : 'Failed to analyze repository', + }); + } + }; -export default App; + const handleSignOut = () => { + setIsSignedIn(false); + setCurrentPage('landing'); + toast.info('Signed out successfully'); + }; + + const handleNavigateHome = () => { + setCurrentPage('landing'); + }; + + const handleInfoClick = (type: 'what' | 'private' | 'local') => { + setInfoModalType(type); + }; + + const handleNewsClick = () => { + setNewsOpen(true); + setHasUnreadNews(false); + }; + + return ( +
+
setSettingsOpen(true)} + onNewsClick={handleNewsClick} + onSignOut={handleSignOut} + onNavigateHome={handleNavigateHome} + showNews={currentPage === 'dashboard'} + hasUnreadNews={hasUnreadNews} + title={currentPage === 'dashboard' ? 'GitRay' : undefined} + theme={theme} + /> + +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {currentPage === 'landing' && ( + + )} + {currentPage === 'dashboard' && ( + + )} + + )} +
+ +
+ + setSettingsOpen(false)} + theme={theme} + onThemeChange={setTheme} + /> + + setNewsOpen(false)} /> + + setInfoModalType(null)} + type={infoModalType} + /> + + +
+ ); +} diff --git a/apps/frontend/src/assets/react.svg b/apps/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/apps/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/frontend/src/components/AIInsights.tsx b/apps/frontend/src/components/AIInsights.tsx new file mode 100644 index 00000000..fba3e169 --- /dev/null +++ b/apps/frontend/src/components/AIInsights.tsx @@ -0,0 +1,367 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from './ui/card'; +import { Badge } from './ui/badge'; +import { Button } from './ui/button'; +import { + Sparkles, + TrendingUp, + AlertTriangle, + CheckCircle, + Lightbulb, + Clock, + BookOpen, +} from 'lucide-react'; +import { Alert, AlertDescription } from './ui/alert'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs'; +import { Progress } from './ui/progress'; + +const projectInsights = { + overallScore: 78, + summary: + 'This Angular project shows good architectural patterns with some areas for improvement in code organization and testing coverage.', + recommendations: [ + { + category: 'Architecture', + priority: 'high', + title: 'Implement lazy loading for feature modules', + description: + 'Several large modules are eagerly loaded, impacting initial bundle size. Consider implementing lazy loading for non-critical features.', + impact: 'Could reduce initial load time by ~35%', + }, + { + category: 'Code Quality', + priority: 'medium', + title: 'Increase test coverage', + description: + 'Current test coverage is at 62%. Focus on testing critical business logic in the authentication and payment modules.', + impact: 'Improve code reliability and maintainability', + }, + { + category: 'Best Practices', + priority: 'medium', + title: 'Standardize component structure', + description: + 'Some components use different organizational patterns. Follow Angular style guide consistently across all modules.', + impact: 'Better code maintainability and team collaboration', + }, + { + category: 'Performance', + priority: 'low', + title: 'Optimize change detection', + description: + "Consider using OnPush strategy for components that don't require frequent updates.", + impact: 'Reduce unnecessary re-renders by ~20%', + }, + ], +}; + +const weeklyInsights = [ + { + week: 'Week 47 (Nov 1-7)', + commits: 34, + highlights: [ + 'Implemented new dashboard analytics feature', + 'Fixed 12 reported bugs in the authentication flow', + 'Refactored API service layer for better maintainability', + ], + keyMetrics: { + linesAdded: 2340, + linesRemoved: 876, + filesChanged: 45, + }, + }, + { + week: 'Week 46 (Oct 25-31)', + commits: 28, + highlights: [ + 'Added user profile customization options', + 'Upgraded dependencies to latest versions', + 'Improved error handling across the application', + ], + keyMetrics: { + linesAdded: 1823, + linesRemoved: 654, + filesChanged: 38, + }, + }, +]; + +const monthlyTrends = { + productivity: 82, + codeQuality: 75, + collaboration: 88, +}; + +export function AIInsights() { + return ( +
+ + + + AI-powered insights are generated based on your project's structure, + commit history, and industry best practices. + + + + + + + + Overview + + + + Weekly + + + + Trends + + + + + + + + + Project Health Score + + + Overall assessment of your project + + + +
+
+ {projectInsights.overallScore} +
+
+ +

+ {projectInsights.summary} +

+
+
+
+
+ + + + Recommendations + + AI-generated suggestions to improve your project + + + + {projectInsights.recommendations.map((rec, index) => ( +
+
+
+ {rec.priority === 'high' ? ( + + ) : ( + + )} +
+
+
+ + {rec.category} + + + {rec.priority} priority + +
+
+

{rec.title}

+

+ {rec.description} +

+

+ + Impact: {rec.impact} +

+
+
+
+
+ ))} +
+
+
+ + + + + + + Weekly Development Summary + + + Iterative summary of changes and progress + + + + {weeklyInsights.map((week, index) => ( +
+
+

{week.week}

+ {week.commits} commits +
+ +
+
+

+ Lines Added +

+

+ +{week.keyMetrics.linesAdded.toLocaleString()} +

+
+
+

+ Lines Removed +

+

+ -{week.keyMetrics.linesRemoved.toLocaleString()} +

+
+
+

+ Files Changed +

+

+ {week.keyMetrics.filesChanged} +

+
+
+ +
+

Highlights:

+
    + {week.highlights.map((highlight, hIndex) => ( +
  • + + {highlight} +
  • + ))} +
+
+
+ ))} + +
+
+
+ + + + + Monthly Trends + + Key performance indicators for the last 30 days + + + +
+
+
+ + Team Productivity + + + {monthlyTrends.productivity}% + +
+ +

+ Based on commit frequency, code reviews, and task completion +

+
+ +
+
+ Code Quality + + {monthlyTrends.codeQuality}% + +
+ +

+ Measured by test coverage, code review feedback, and + refactoring efforts +

+
+ +
+
+ + Team Collaboration + + + {monthlyTrends.collaboration}% + +
+ +

+ Based on PR reviews, pair programming sessions, and code + contributions +

+
+
+ + + + + Your team's collaboration score has increased by 12% this + month. Keep up the great work! + + +
+
+
+
+
+ ); +} diff --git a/apps/frontend/src/components/ActivityChart.tsx b/apps/frontend/src/components/ActivityChart.tsx new file mode 100644 index 00000000..3d3c00c1 --- /dev/null +++ b/apps/frontend/src/components/ActivityChart.tsx @@ -0,0 +1,113 @@ +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { Commit } from '@gitray/shared-types'; + +interface ActivityChartProps { + commits?: Commit[]; +} + +// Generate activity data from commits for the last 30 days +function generateActivityData(commits: Commit[]) { + const data = []; + const today = new Date(); + const thirtyDaysAgo = new Date(today); + thirtyDaysAgo.setDate(today.getDate() - 29); + + // Create a map of date -> commit count + const commitsByDate = new Map(); + + // Filter commits from last 30 days and count by date + commits.forEach((commit) => { + const commitDate = new Date(commit.date); + if (commitDate >= thirtyDaysAgo && commitDate <= today) { + const dateKey = commitDate.toISOString().split('T')[0]; + commitsByDate.set(dateKey, (commitsByDate.get(dateKey) || 0) + 1); + } + }); + + // Generate data for each day in the last 30 days + for (let i = 29; i >= 0; i--) { + const date = new Date(today); + date.setDate(date.getDate() - i); + const dateKey = date.toISOString().split('T')[0]; + + data.push({ + date: date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }), + commits: commitsByDate.get(dateKey) || 0, + }); + } + + return data; +} + +export function ActivityChart({ commits = [] }: ActivityChartProps) { + const data = generateActivityData(commits); + return ( +
+ + + + + + + + + + + + { + if (active && payload && payload.length) { + return ( +
+

{payload[0].payload.date}

+

+ {payload[0].value} commits +

+
+ ); + } + return null; + }} + /> + +
+
+
+ ); +} diff --git a/apps/frontend/src/components/ActivityHeatmap.tsx b/apps/frontend/src/components/ActivityHeatmap.tsx deleted file mode 100644 index ad655f16..00000000 --- a/apps/frontend/src/components/ActivityHeatmap.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import React, { - useEffect, - useState, - useMemo, - useRef, - useCallback, -} from 'react'; -import CalendarHeatmap from 'react-calendar-heatmap'; -import 'react-calendar-heatmap/dist/styles.css'; -import '../styles/heatmap.css'; - -// Displays a calendar-style heatmap of commit activity with optional filters -import Select, { - StylesConfig, - GroupBase, - ControlProps, - CSSObjectWithLabel, - OptionProps, -} from 'react-select'; -import { - CommitFilterOptions, - CommitHeatmapData, - Commit, - TIME, -} from '@gitray/shared-types'; -import { getHeatmapData } from '../services/api'; -import RiveLoader from './RiveLoader'; - -interface ActivityHeatmapProps { - repoUrl: string; - commits: Commit[]; -} - -interface HeatmapValue { - date: string; - count: number; - authors?: string[]; -} - -// add this below HeatmapValue -interface AuthorOption { - value: string; - label: string; - count?: number; -} - -const customStyles: StylesConfig< - AuthorOption, - true, - GroupBase -> = { - control: ( - base: CSSObjectWithLabel, - - _props: ControlProps> - ) => ({ - ...base, - backgroundColor: '#1f2937', // Maintains dark background for the control - }), - placeholder: (base: CSSObjectWithLabel) => ({ - ...base, - color: 'white', // Only placeholder text in white - }), - input: (base: CSSObjectWithLabel) => ({ - ...base, - color: 'white', // Input text remains white - }), - menu: (base: CSSObjectWithLabel) => ({ - ...base, - backgroundColor: '#1f2937', // Set dropdown menu background to dark - }), - option: ( - base: CSSObjectWithLabel, - state: OptionProps> - ) => ({ - ...base, - backgroundColor: state.isFocused ? '#374151' : '#1f2937', - color: 'white', - cursor: 'pointer', - }), - multiValue: (base: CSSObjectWithLabel) => ({ - ...base, - backgroundColor: '#065f46', - color: 'white', - }), - multiValueLabel: (base: CSSObjectWithLabel) => ({ - ...base, - color: 'white', - }), - multiValueRemove: (base: CSSObjectWithLabel) => ({ - ...base, - color: 'white', - ':hover': { - backgroundColor: '#064e3b', - color: 'white', - }, - }), -}; - -/** - * Component that renders a calendar-style heatmap of commit activity. - * Users can filter by author and the heatmap dynamically adjusts to the - * container size. - */ -const ActivityHeatmap: React.FC = ({ - repoUrl, - commits, -}) => { - const [filterOptions, setFilterOptions] = useState({}); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [containerWidth, setContainerWidth] = useState(0); - const containerRef = useRef(null); - - // memoize start/end dates to avoid unstable deps - const startDate = useMemo(() => new Date(Date.now() - TIME.DAY * 364), []); - const endDate = useMemo(() => new Date(), []); - - // Count commits per author in that range - const authorCommitCounts = useMemo(() => { - const counts = new Map(); - for (const commit of commits) { - const commitDate = new Date(commit.date); - if (commitDate >= startDate && commitDate <= endDate) { - const previousCount = counts.get(commit.authorName) ?? 0; - counts.set(commit.authorName, previousCount + 1); - } - } - return counts; - }, [commits, startDate, endDate]); - - // Build dropdown options including the count and sort by commit count - const authorOptions = useMemo( - () => - Array.from(new Set(commits.map((c) => c.authorName))) - .map((a) => ({ - value: a, - label: `${a} (${authorCommitCounts.get(a) ?? 0})`, - count: authorCommitCounts.get(a) ?? 0, - })) - .sort((a, b) => b.count - a.count), - [commits, authorCommitCounts] - ); - - // Calculate dynamic cell size based on container width - useEffect(() => { - const updateWidth = () => { - if (containerRef.current) { - const width = containerRef.current.offsetWidth; - setContainerWidth(width); - } - }; - - updateWidth(); - window.addEventListener('resize', updateWidth); - return () => window.removeEventListener('resize', updateWidth); - }, []); - - // Calculate cell size dynamically - // We have 53 weeks (columns) + some padding - // Formula: (containerWidth - padding) / (weeks + gaps) - const cellSize = useMemo(() => { - if (containerWidth === 0) return 12; // Default size - const padding = 40; // Space for labels - const weeks = 53; - const gutterSize = 2; - const availableWidth = containerWidth - padding; - const calculatedSize = Math.floor( - (availableWidth - weeks * gutterSize) / weeks - ); - // Limit the size to reasonable bounds - return Math.min(Math.max(calculatedSize, 8), 20); - }, [containerWidth]); - - // Helper to compare author arrays - const compareAuthorArrays = (a: string[], b: string[]): boolean => { - if (a.length !== b.length) return false; - const sortedA = [...a].sort((x, y) => x.localeCompare(y)); - const sortedB = [...b].sort((x, y) => x.localeCompare(y)); - return sortedA.every((v, i) => v === sortedB[i]); - }; - - const isFilterOptionsEqual = ( - a: CommitFilterOptions, - b: CommitFilterOptions - ) => { - const A = a.authors ?? []; - const B = b.authors ?? []; - return compareAuthorArrays(A, B); - }; - - // Ref to hold last‐used filters - const prevFilters = useRef({ authors: [] }); - - const fetchData = useCallback(async () => { - if (!repoUrl) return; - setLoading(true); - try { - const d = await getHeatmapData(repoUrl, 'day', filterOptions); - setData(d); - } finally { - setLoading(false); - } - }, [repoUrl, filterOptions]); - - // Only call fetchData when filters changed - const handleMenuClose = () => { - if (!isFilterOptionsEqual(filterOptions, prevFilters.current)) { - fetchData(); - prevFilters.current = filterOptions; - } - }; - - // initial fetch on repoUrl change - useEffect(() => { - fetchData().catch(console.error); - }, [fetchData]); - - const values: HeatmapValue[] = data - ? data.data.map((b) => ({ - date: b.periodStart, - count: b.commitCount, - authors: b.authors, - })) - : []; - const max = data?.metadata?.maxCommitCount ?? 0; - - const classForValue = (v?: HeatmapValue) => { - if (!v) return 'color-empty'; - const step = max / 4 || 1; - const level = Math.min(4, Math.ceil(v.count / step)); - return `color-scale-${level}`; - }; - - const titleForValue = (v?: HeatmapValue) => { - if (!v) return null; - const plural = v.count === 1 ? '' : 's'; - return `${v.count} commit${plural} on ${v.date}`; - }; - - return ( -
-
-

- Repository Activity -

- - {/* Author selector with proper spacing */} -
- - setUrl(e.target.value)} + className="h-12 md:h-14 text-base md:text-lg border-2 focus:border-primary transition-colors" + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleAnalyze(); + } + }} + /> +
+ +
+ + +
+ + +
+ + {/* Helper Chips */} +
+ + + +
+
+
+ ); +} diff --git a/apps/frontend/src/components/LoadingSpinner.tsx b/apps/frontend/src/components/LoadingSpinner.tsx new file mode 100644 index 00000000..38e6df0e --- /dev/null +++ b/apps/frontend/src/components/LoadingSpinner.tsx @@ -0,0 +1,19 @@ +import { Loader2 } from 'lucide-react'; + +interface LoadingSpinnerProps { + message?: string; +} + +export function LoadingSpinner({ + message = 'Loading...', +}: LoadingSpinnerProps) { + return ( +
+
+
+ +
+

{message}

+
+ ); +} diff --git a/apps/frontend/src/components/NewsDrawer.tsx b/apps/frontend/src/components/NewsDrawer.tsx new file mode 100644 index 00000000..46406009 --- /dev/null +++ b/apps/frontend/src/components/NewsDrawer.tsx @@ -0,0 +1,94 @@ +import { ExternalLink } from 'lucide-react'; +import { Button } from './ui/button'; +import { Sheet, SheetContent, SheetHeader, SheetTitle } from './ui/sheet'; + +interface NewsDrawerProps { + open: boolean; + onClose: () => void; +} + +const newsItems = [ + { + version: 'v2.0', + title: 'Major Update: AI Insights & Premium Features', + description: + 'Introducing AI-powered project analysis, weekly summaries, and comprehensive premium features including multi-language support (8 languages available).', + date: 'Just now', + }, + { + version: 'v2.0', + title: 'New: Advanced Analytics Tab', + description: + 'Access Code Churn Analysis, File Type Distribution, Graph View Timeline with playback, and Git Diff viewer all in one place.', + date: 'Just now', + }, + { + version: 'v2.0', + title: 'Enhanced: Contribution Ranking', + description: + 'See detailed contributor leaderboards with commit counts, code changes, and achievement badges on the Overview tab.', + date: 'Just now', + }, + { + version: 'v1.9', + title: 'Private repo tokens', + description: + 'You can now add tokens to Connections to analyze private repositories securely.', + date: '2 days ago', + }, + { + version: 'v1.8', + title: 'New: File Distribution chart', + description: + "Visualize your repository's file types and language distribution with our new chart.", + date: '1 week ago', + }, +]; + +export function NewsDrawer({ open, onClose }: NewsDrawerProps) { + return ( + + + + What's New + + +
+ {newsItems.map((item, index) => ( +
+
+
+
+ + {item.version} + +
+

{item.title}

+
+
+

+ {item.description} +

+

{item.date}

+
+ ))} + + +
+
+
+ ); +} diff --git a/apps/frontend/src/components/PremiumFeatures.tsx b/apps/frontend/src/components/PremiumFeatures.tsx new file mode 100644 index 00000000..56f1f799 --- /dev/null +++ b/apps/frontend/src/components/PremiumFeatures.tsx @@ -0,0 +1,421 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from './ui/card'; +import { Badge } from './ui/badge'; +import { Button } from './ui/button'; +import { + Lock, + Play, + Download, + ZoomIn, + Layers, + Users, + MessageSquare, + Trophy, + FileType, + Smartphone, + Monitor, + Shield, + Rocket, +} from 'lucide-react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs'; + +const premiumFeatures = [ + { + category: 'Visualizations', + icon: Play, + features: [ + { + name: 'Time-lapse Animation (Gource)', + description: + 'Watch your project evolution with animated commit history visualization', + icon: Play, + badge: 'Premium', + }, + { + name: 'Export Visualizations', + description: + 'Export charts, graphs, and reports in PNG, SVG, or PDF formats', + icon: Download, + badge: 'Premium', + }, + { + name: 'Advanced Zoom & Pan', + description: + 'Interactive navigation with smooth zooming and panning controls', + icon: ZoomIn, + badge: 'Premium', + }, + ], + }, + { + category: 'Analysis Tools', + icon: Layers, + features: [ + { + name: 'UML Diagram Generation', + description: + 'Automatically generate UML diagrams from your codebase using PlantUML', + icon: FileType, + badge: 'Premium', + }, + { + name: 'Project Efficiency Analysis', + description: + 'Get insights on how to complete projects faster and more efficiently', + icon: Rocket, + badge: 'Premium', + }, + { + name: 'Security Insights', + description: + 'Advanced vulnerability detection and security recommendations', + icon: Shield, + badge: 'Premium', + }, + ], + }, + { + category: 'Multi-Project Management', + icon: Layers, + features: [ + { + name: 'Manage Multiple Repositories', + description: + 'Analyze and track multiple projects simultaneously with unified dashboard', + icon: Layers, + badge: 'Team', + }, + { + name: 'Team Collaboration', + description: + 'Share insights, add team members, and collaborate with built-in chat', + icon: Users, + badge: 'Team', + }, + { + name: 'Team Chat Rooms', + description: + 'Real-time communication with push notifications and email alerts', + icon: MessageSquare, + badge: 'Team', + }, + { + name: 'Gamification System', + description: + 'Track levels, ranks, and achievements to motivate your team', + icon: Trophy, + badge: 'Team', + }, + ], + }, + { + category: 'Desktop & Mobile', + icon: Monitor, + features: [ + { + name: 'Desktop Application', + description: + 'Download GitRay as a standalone executable with offline AI capabilities', + icon: Monitor, + badge: 'Enterprise', + }, + { + name: 'Progressive Web App (PWA)', + description: + 'Install GitRay on Android devices with native app experience', + icon: Smartphone, + badge: 'Premium', + }, + ], + }, +]; + +const pricingPlans = [ + { + name: 'Free', + price: '$0', + period: 'forever', + description: 'Perfect for individuals and small projects', + features: [ + 'Up to 3 repositories', + 'Basic analytics & visualizations', + '7-day data retention', + 'Community support', + ], + cta: 'Current Plan', + highlighted: false, + }, + { + name: 'Premium', + price: '$15', + period: 'per month', + description: 'Advanced features for serious developers', + features: [ + 'Unlimited repositories', + 'All visualizations & exports', + '30-day data retention', + 'AI-powered insights', + 'Priority support', + 'Zoom & Pan controls', + ], + cta: 'Upgrade to Premium', + highlighted: true, + }, + { + name: 'Team', + price: '$49', + period: 'per month', + description: 'Collaborate with your entire team', + features: [ + 'Everything in Premium', + 'Multi-project management', + 'Team collaboration tools', + 'Real-time team chat', + 'Gamification system', + '90-day data retention', + 'Up to 10 team members', + ], + cta: 'Start Team Trial', + highlighted: false, + }, + { + name: 'Enterprise', + price: 'Custom', + period: 'contact us', + description: 'For large organizations with specific needs', + features: [ + 'Everything in Team', + 'Unlimited team members', + 'Desktop application', + 'Offline AI capabilities', + 'Custom integrations', + 'Dedicated support', + 'SLA guarantees', + 'On-premise deployment', + ], + cta: 'Contact Sales', + highlighted: false, + }, +]; + +interface PremiumFeaturesProps { + showPricingOnly?: boolean; +} + +export function PremiumFeatures({ + showPricingOnly = false, +}: PremiumFeaturesProps) { + if (showPricingOnly) { + return ( +
+
+ {pricingPlans.map((plan, index) => ( + + {plan.highlighted && ( +
+ Most Popular +
+ )} + + {plan.name} +
+ {plan.price} + + {plan.price !== 'Custom' && `/${plan.period}`} + +
+ {plan.description} +
+ +
    + {plan.features.map((feature, featureIndex) => ( +
  • +
    +
    +
    + {feature} +
  • + ))} +
+ +
+
+ ))} +
+ + + + Compare All Plans + + Choose the plan that best fits your needs + + + +
+ + + + + {pricingPlans.map((plan, index) => ( + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Feature + {plan.name} +
RepositoriesUp to 3UnlimitedUnlimitedUnlimited
Data Retention7 days30 days90 daysUnlimited
AI Insights-✓ + Offline
Team Collaboration--
Desktop App---
+
+
+
+
+ ); + } + + return ( +
+ + + Premium Features + + + + {premiumFeatures.map((category, categoryIndex) => { + const CategoryIcon = category.icon; + return ( + + + + + {category.category} + + + Unlock powerful tools to supercharge your workflow + + + +
+ {category.features.map((feature, featureIndex) => { + const FeatureIcon = feature.icon; + return ( +
+
+
+
+
+ +
+ + {feature.badge} + +
+
+

+ {feature.name} +

+

+ {feature.description} +

+
+ +
+
+ ); + })} +
+ + + ); + })} + + + + +
+

Unlock All Premium Features

+

+ Get access to advanced visualizations, AI insights, team + collaboration tools, and much more with a Premium + subscription. +

+
+
+ + +
+

+ 14-day free trial · No credit card required +

+
+
+ + +
+ ); +} diff --git a/apps/frontend/src/components/RepoInput.tsx b/apps/frontend/src/components/RepoInput.tsx deleted file mode 100644 index c34f5ed4..00000000 --- a/apps/frontend/src/components/RepoInput.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useState } from 'react'; - -/** - * Input box allowing the user to enter a repository URL and trigger the - * visualization of that repository. - */ - -interface RepoInputProps { - onVisualize: (_repoUrl: string) => void; -} - -/** - * Small form used on the main page for repository submission. It manages its - * own input state and notifies the parent when the user clicks the - * "Visualize" button. - */ -const RepoInput: React.FC = ({ onVisualize }) => { - const [repoUrl, setRepoUrl] = useState( - // Placeholder text shown on first render - 'https://github.com/username/Repository' - ); - - const handleInputChange = (event: React.ChangeEvent) => { - setRepoUrl(event.target.value); - }; - - const handleVisualizeClick = () => { - // Bubble the entered repo URL up to the parent component - onVisualize(repoUrl); - }; - - return ( -
- { - if (repoUrl === 'https://github.com/username/Repository') { - setRepoUrl(''); - } - }} - onBlur={() => { - if (repoUrl === '') { - setRepoUrl('https://github.com/username/Repository'); - } - }} - /> - -
- ); -}; - -export default RepoInput; diff --git a/apps/frontend/src/components/RiveLoader.tsx b/apps/frontend/src/components/RiveLoader.tsx index f1f56b45..b3b5043e 100644 --- a/apps/frontend/src/components/RiveLoader.tsx +++ b/apps/frontend/src/components/RiveLoader.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { useEffect, useState } from 'react'; import { useRive } from '@rive-app/react-canvas'; interface RiveLoaderProps { @@ -8,26 +8,42 @@ interface RiveLoaderProps { className?: string; /** Loading message to display */ message?: string; + /** Current theme (light/dark/system) */ + theme?: 'light' | 'dark' | 'system'; } /** - * Rive animation component that displays a loading animation from the dark mode logo file. - * The animation uses the "Loading" state machine and timeline. + * Rive animation component that displays a loading animation. + * Automatically switches between light and dark mode animations based on the theme. + * Uses the "Loading" state machine for the loading animation. */ -const RiveLoader: React.FC = ({ +function RiveLoaderInner({ size = 120, className = '', message = 'Loading...', -}) => { + resolvedTheme, +}: { + size: number; + className: string; + message: string; + resolvedTheme: 'light' | 'dark'; +}) { + const animationSrc = + resolvedTheme === 'dark' + ? '/Logo_Animation_StateMachine_DarkMode.riv' + : '/Logo_Animation_StateMachine_LightMode.riv'; + const { RiveComponent } = useRive({ - src: '/Logo_Animation_StateMachine_DarkMode.riv', + src: animationSrc, stateMachines: 'Loading', autoplay: true, onLoad: () => { - console.log('Rive animation loaded successfully'); + console.log( + `Rive loading animation loaded successfully (${resolvedTheme} mode)` + ); }, onLoadError: (error) => { - console.error('Failed to load Rive animation:', error); + console.error('Failed to load Rive loading animation:', error); }, }); @@ -39,9 +55,45 @@ const RiveLoader: React.FC = ({ >
- {message &&

{message}

} + {message && ( +

{message}

+ )}
); -}; +} + +export function RiveLoader({ + size = 120, + className = '', + message = 'Loading...', + theme = 'dark', +}: RiveLoaderProps) { + const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('dark'); -export default RiveLoader; + // Resolve system theme preference + useEffect(() => { + if (theme === 'system') { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const updateTheme = () => { + const newTheme = mediaQuery.matches ? 'dark' : 'light'; + setResolvedTheme(newTheme); + }; + + updateTheme(); + mediaQuery.addEventListener('change', updateTheme); + return () => mediaQuery.removeEventListener('change', updateTheme); + } else { + setResolvedTheme(theme); + } + }, [theme]); + + return ( + + ); +} diff --git a/apps/frontend/src/components/RiveLogo.tsx b/apps/frontend/src/components/RiveLogo.tsx index 10050ab6..be6d1e19 100644 --- a/apps/frontend/src/components/RiveLogo.tsx +++ b/apps/frontend/src/components/RiveLogo.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import { useEffect, useState } from 'react'; import { useRive } from '@rive-app/react-canvas'; interface RiveLogoProps { @@ -8,23 +8,58 @@ interface RiveLogoProps { className?: string; /** Whether to show hover effects */ interactive?: boolean; + /** Current theme (light/dark/system) */ + theme?: 'light' | 'dark' | 'system'; } /** * Rive logo component that displays the brand logo with interactive elements. + * Automatically switches between light and dark mode logos based on the theme. * Uses the "State Machine 1" state machine for hover effects and interactions. */ -const RiveLogo: React.FC = ({ +export function RiveLogo({ size = 60, className = '', interactive = true, -}) => { + theme = 'dark', +}: RiveLogoProps) { + const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('dark'); + const [logoKey, setLogoKey] = useState(0); + + // Resolve system theme preference + useEffect(() => { + if (theme === 'system') { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const updateTheme = () => { + const newTheme = mediaQuery.matches ? 'dark' : 'light'; + if (newTheme !== resolvedTheme) { + setResolvedTheme(newTheme); + setLogoKey((prev) => prev + 1); + } + }; + + updateTheme(); + mediaQuery.addEventListener('change', updateTheme); + return () => mediaQuery.removeEventListener('change', updateTheme); + } else { + if (theme !== resolvedTheme) { + setResolvedTheme(theme); + setLogoKey((prev) => prev + 1); + } + } + }, [theme, resolvedTheme]); + + const logoSrc = + resolvedTheme === 'dark' + ? '/Logo_Animation_StateMachine_DarkMode.riv' + : '/Logo_Animation_StateMachine_LightMode.riv'; + const { RiveComponent } = useRive({ - src: '/Logo_Animation_StateMachine_DarkMode.riv', + src: logoSrc, stateMachines: 'State Machine 1', autoplay: true, onLoad: () => { - console.log('Rive logo loaded successfully'); + console.log(`Rive logo loaded successfully (${resolvedTheme} mode)`); }, onLoadError: (error) => { console.error('Failed to load Rive logo:', error); @@ -33,12 +68,11 @@ const RiveLogo: React.FC = ({ return (
); -}; - -export default RiveLogo; +} diff --git a/apps/frontend/src/components/SettingsDrawer.tsx b/apps/frontend/src/components/SettingsDrawer.tsx new file mode 100644 index 00000000..25a90d1f --- /dev/null +++ b/apps/frontend/src/components/SettingsDrawer.tsx @@ -0,0 +1,183 @@ +import { Button } from './ui/button'; +import { Label } from './ui/label'; +import { Input } from './ui/input'; +import { RadioGroup, RadioGroupItem } from './ui/radio-group'; +import { Switch } from './ui/switch'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs'; +import { Sheet, SheetContent, SheetHeader, SheetTitle } from './ui/sheet'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './ui/select'; +import { useState } from 'react'; + +interface SettingsDrawerProps { + open: boolean; + onClose: () => void; + theme: 'light' | 'dark' | 'system'; + onThemeChange: (theme: 'light' | 'dark' | 'system') => void; +} + +export function SettingsDrawer({ + open, + onClose, + theme, + onThemeChange, +}: SettingsDrawerProps) { + const [language, setLanguage] = useState('english'); + + return ( + + + + Settings + + + + + General + Appearance + Account + Connections + + + +
+

General Settings

+
+
+ + +

+ Choose your preferred display language +

+
+
+
+ +

+ Start analysis automatically when pasting a URL +

+
+ +
+
+
+ +

+ Get notified when analysis completes +

+
+ +
+
+
+ +

+ Allow exporting visualizations and reports +

+
+ +
+
+
+
+ + +
+

Theme

+ +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+

Account Information

+
+
+ + +
+
+ + +
+ +
+
+
+ + +
+

API Connections

+
+
+ + +

+ Required for private repositories +

+
+
+ + +

+ For enhanced AI-powered insights +

+
+ +
+
+
+
+
+
+ ); +} diff --git a/apps/frontend/src/components/figma/ImageWithFallback.tsx b/apps/frontend/src/components/figma/ImageWithFallback.tsx new file mode 100644 index 00000000..0b119580 --- /dev/null +++ b/apps/frontend/src/components/figma/ImageWithFallback.tsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; + +const ERROR_IMG_SRC = + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='; + +export function ImageWithFallback( + props: React.ImgHTMLAttributes +) { + const [didError, setDidError] = useState(false); + + const handleError = () => { + setDidError(true); + }; + + const { src, alt, style, className, ...rest } = props; + + return didError ? ( +
+
+ Error loading image +
+
+ ) : ( + {alt} + ); +} diff --git a/apps/frontend/src/components/ui/accordion.tsx b/apps/frontend/src/components/ui/accordion.tsx new file mode 100644 index 00000000..1f038ccd --- /dev/null +++ b/apps/frontend/src/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +'use client'; + +import * as React from 'react'; +import * as AccordionPrimitive from '@radix-ui/react-accordion'; +import { ChevronDownIcon } from 'lucide-react'; + +import { cn } from './utils'; + +function Accordion({ + ...props +}: React.ComponentProps) { + return ; +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180', + className + )} + {...props} + > + {children} + + + + ); +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ); +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/apps/frontend/src/components/ui/alert-dialog.tsx b/apps/frontend/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..ea119720 --- /dev/null +++ b/apps/frontend/src/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +'use client'; + +import * as React from 'react'; +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; + +import { cn } from './utils'; +import { buttonVariants } from './button'; + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/apps/frontend/src/components/ui/alert.tsx b/apps/frontend/src/components/ui/alert.tsx new file mode 100644 index 00000000..06dfe5c2 --- /dev/null +++ b/apps/frontend/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from './utils'; + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ); +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { Alert, AlertTitle, AlertDescription }; diff --git a/apps/frontend/src/components/ui/aspect-ratio.tsx b/apps/frontend/src/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..afb7845c --- /dev/null +++ b/apps/frontend/src/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +'use client'; + +import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return ; +} + +export { AspectRatio }; diff --git a/apps/frontend/src/components/ui/avatar.tsx b/apps/frontend/src/components/ui/avatar.tsx new file mode 100644 index 00000000..f8cba895 --- /dev/null +++ b/apps/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client'; + +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from './utils'; + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/frontend/src/components/ui/badge.tsx b/apps/frontend/src/components/ui/badge.tsx new file mode 100644 index 00000000..c342cb9a --- /dev/null +++ b/apps/frontend/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from './utils'; + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span'; + + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/apps/frontend/src/components/ui/breadcrumb.tsx b/apps/frontend/src/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..263388c1 --- /dev/null +++ b/apps/frontend/src/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { ChevronRight, MoreHorizontal } from 'lucide-react'; + +import { cn } from './utils'; + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return