because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs(roadmap): update roadmap with new features and completed items

+1835 -124
+162
.cursorrules
··· 12 12 - Use guard clauses at the beginning of functions to handle edge cases 13 13 - **Prefer ternary operations** over if/else for simple conditional assignments 14 14 - Use ternary for inline conditionals: `condition ? valueIfTrue : valueIfFalse` 15 + - **If statements should have proper formatting**: curly brackets and whitespace 16 + - **Always use curly braces for if/else statements** - never omit braces even for single-line statements 17 + - **Always add a newline after if/else keywords** - format as: `if (condition) {\n // code\n}` not `if (condition) { // code }` 18 + - **Prefer ternary expressions** over if statements when possible for cleaner code 15 19 16 20 ### Destructuring & Assignment 17 21 - **Prefer destructuring** for object and array access 18 22 - Use destructuring in function parameters: `({ id, name }) => ...` 19 23 - Use destructuring for imports: `import { Component } from 'library'` 20 24 - Prefer object spread over Object.assign: `{ ...obj, newProp: value }` 25 + - **Use object property shorthand** when variable names match object keys 26 + - Example: Use `{ where }` instead of `{ where: whereClause }` 27 + - Name variables appropriately to enable shorthand syntax (e.g., `where` not `whereClause`) 28 + 29 + ### Exports 30 + - **Prefer named exports** over default exports for consistency and better refactoring support 31 + - Use `export const Component = () => {}` instead of `export default function Component() {}` 32 + - Named exports make imports explicit and enable better IDE support 33 + - Exception: Only use default exports when required by frameworks (e.g., Next.js pages) 21 34 22 35 ### Variable Declarations 23 36 - **Prefer `const`** over `let` where possible ··· 43 56 - Extract inline SVGs to separate icon components 44 57 - **Prefer CVA (Class Variance Authority) for dynamic styling configurations** 45 58 - Use CVA for conditional styling patterns instead of manual className concatenation 59 + - **Prefer single state object over multiple useState calls** 60 + - Use `useState` with object state when managing multiple related values 61 + - Example: `const [state, setState] = useState({ items: [], loading: false, error: null })` 46 62 47 63 ### NestJS/Backend 48 64 - Use dependency injection properly with regular imports (not `import type`) ··· 52 68 - **Use `getOrThrow()` for configuration values instead of manual error throwing** 53 69 - Inject ConfigService where needed for environment variable access 54 70 - Use proper type annotations with `getOrThrow<Type>("VARIABLE_NAME")` 71 + - **One injectable service/class per file** - keep services focused and maintainable 72 + - **Extract domain mapping logic to separate mapper files** - services should not contain `toDomain()` methods 73 + - **Services should contain pure domain logic** - no GraphQL concerns (connections, edges, pageInfo) 74 + - **GraphQL resolvers handle connection composition** - services only provide `findMany()` and `count()` methods 55 75 56 76 ### GraphQL 57 77 - **Prefer `.graphql` files for queries and mutations** instead of inline strings ··· 64 84 - Prefer specific error types over generic Error 65 85 - Use optional chaining (`?.`) for safe property access 66 86 - Handle errors at the appropriate level of abstraction 87 + - **Use `notFound()` utility for consistent 404 errors** 88 + - **Include property and value in error messages**: `notFound("Entity", "property", value)` 89 + - **Prefer ternary expressions with `notFound()` for early returns**: `return entity ?? notFound("Entity", "id", id)` 67 90 68 91 ### Code Organization 69 92 - Group related functionality together 70 93 - Use barrel exports (`index.ts`) for clean imports 71 94 - Prefer composition over inheritance 72 95 - Keep functions small and focused on single responsibility 96 + 97 + ### Comments 98 + - **Avoid inline comments** - code should be self-documenting through clear naming and structure 99 + - Use descriptive variable and function names instead of comments 100 + - Only add comments when absolutely necessary to explain complex business logic or non-obvious behavior 101 + - Prefer JSDoc comments for public APIs and complex functions when documentation is needed 102 + 103 + ### Import Management 104 + - **Prefer TypeScript path aliases over relative directory traversal** 105 + - Use `@/` alias for all imports within the project 106 + - Avoid relative imports like `../`, `../../`, `../../../` 107 + - Use absolute paths with aliases: `@/modules/entity/entity.service` 108 + - This improves maintainability and reduces import path complexity 73 109 74 110 ### Performance 75 111 - Use `useMemo` and `useCallback` judiciously in React ··· 147 183 animated: false, 148 184 }, 149 185 }); 186 + 187 + // TypeScript path aliases for imports 188 + import { CompanyService } from "@/modules/job-experience/company/company.service"; 189 + import { BasePaginationArgs } from "@/modules/base/pagination.types"; 190 + import { User } from "@/modules/auth/user.entity"; 191 + 192 + // Error handling with notFound utility 193 + async findByIdOrFail(id: string): Promise<Company> { 194 + const company = await this.findById(id); 195 + return company ?? notFound("Company", "id", id); 196 + } 197 + 198 + // Proper if statement formatting (when ternary isn't suitable) 199 + if (user.isActive) { 200 + return processUser(user); 201 + } 202 + 203 + // If/else with proper formatting 204 + if (user.isActive) { 205 + return processUser(user); 206 + } else { 207 + return null; 208 + } 209 + 210 + // Ternary expressions for simple conditionals 211 + const statusColor = isActive ? 'green' : 'red'; 212 + const message = user ? `Hello ${user.name}` : 'Hello Guest'; 150 213 ``` 151 214 152 215 ### Avoid โŒ ··· 205 268 206 269 // Direct process.env access without proper validation 207 270 const port = process.env["PORT"] || "3000"; 271 + 272 + // Relative directory traversal imports 273 + import { CompanyService } from "../company/company.service"; 274 + import { BasePaginationArgs } from "../../../base/pagination.types"; 275 + import { User } from "../../auth/user.entity"; 276 + 277 + // Verbose error handling 278 + async findByIdOrFail(id: string): Promise<Company> { 279 + const company = await this.findById(id); 280 + if (!company) { 281 + throw new NotFoundException(`Company with id ${id} not found`); 282 + } 283 + return company; 284 + } 285 + 286 + // Poor if statement formatting 287 + if(user.isActive)return processUser(user); 288 + 289 + // Missing curly braces 290 + if (user.isActive) return processUser(user); 291 + 292 + // Missing newline after if 293 + if (user.isActive) { return processUser(user); } 294 + 295 + // Verbose conditionals that could be ternary 296 + let statusColor; 297 + if (isActive) { 298 + statusColor = 'green'; 299 + } else { 300 + statusColor = 'red'; 301 + } 208 302 ``` 209 303 210 304 ## Project-Specific Rules ··· 228 322 - Use arrow functions for resolvers 229 323 - Prefer early returns for validation 230 324 - Use destructuring for resolver parameters 325 + - **Prefer GraphQL Relay-style connections over arrays** for list queries 326 + - Use `Connection` types with `edges`, `pageInfo`, and `totalCount` for paginated data 327 + - Only return arrays for small, non-paginated lists (e.g., enum values, small reference data) 328 + - Resolvers should compose connections from service `findMany()` and `count()` methods using `PaginationService` 231 329 232 330 ### Three-Layer Architecture 233 331 **Prefer GraphQL -> Domain Entity -> Prisma Entity separation** ··· 269 367 - **Only use IDs for Prisma operations** - extract IDs from entities at the service boundary 270 368 - **OrFail delegation**: Implement `findByXOrFail` methods by delegating to the corresponding non-throwing `findByX` method and only handling the error-throwing responsibility (prefer early return style). This avoids duplication and ensures consistent behavior. 271 369 - **Maximise mapper usage**: Services must use their injected mappers (`toDomain`, `mapToDomain`, and any specialized helpers) for all conversions from Prisma to domain, including joined/`include` cases. Avoid manual `new Entity(...)` in services. 370 + - **Use PaginationService.buildQueryOptions()** for cursor-based pagination instead of manual cursor logic. This method handles all after/before/first/last logic generically. 371 + - **Connection classes must handle their own edge management**: All GraphQL connection types must include a static `fromPaginationResult()` factory method that handles edge creation and domain mapping. This keeps edge management logic within the connection class and follows the established pattern used by other connections in the codebase. 272 372 273 373 #### Mapping Functions 274 374 - **`fromDomain()`** - Domain entity to GraphQL type 275 375 - **`toDomain()`** - Prisma model to domain entity 276 376 - **`mapToDomain()`** - Array of Prisma models to domain entities 377 + - **`fromDomain()` MUST always accept domain entities, never Prisma entities** 378 + - GraphQL types should only work with domain entities, not Prisma models 379 + - If relations are needed, domain entities should include them as optional properties 277 380 - Keep resolvers focused on GraphQL concerns 278 381 - Keep domain logic in domain entities 279 382 - Keep database operations in Prisma services ··· 403 506 - **When making commits, check if any roadmap items were completed and check them off** 404 507 - Review ROADMAP.md for relevant completed features and update checkboxes accordingly 405 508 509 + ### Scripts and Automation 510 + - **Avoid creating one-off scripts in a scripts directory unless explicitly requested** 511 + - **Prefer running commands directly or using existing package.json scripts** 512 + - **Only create package.json scripts for operations that will be used repeatedly** 513 + - **For one-time operations, run commands directly rather than creating script files** 514 + 515 + ### Monorepo Dependencies 516 + - **Unless it's specifically monorepo tooling (like Lerna, Nx, Rush), install npm libraries in the sub-projects where they're used** 517 + - **Avoid relying on root-level node_modules for sub-project dependencies** 518 + - **Each package should have its own dependencies installed locally for better isolation and reliability** 519 + 406 520 ### Conventional Commits 407 521 - **Always use conventional commit format**: `type(scope): description` 408 522 - **Types**: `feat:`, `fix:`, `docs:`, `style:`, `refactor:`, `test:`, `chore:`, `perf:`, `ci:`, `build:` ··· 415 529 - `docs(api): update GraphQL schema documentation` 416 530 - `refactor(ui): extract toast icons to separate components` 417 531 - `chore(deps): update biome to latest version` 532 + 533 + ### Docker & Service Health 534 + - **NEVER use `sleep` commands to wait for Docker services to start** 535 + - **Always rely on healthchecks and `depends_on` conditions** defined in `docker-compose.yml` 536 + - Services are configured with `depends_on` and `condition: service_healthy` to ensure proper startup order 537 + - If a service needs to be restarted, use `docker-compose restart <service>` and trust the healthchecks 538 + - Check service status with `docker-compose ps` to see health status 539 + - Only run commands on healthy services using `docker-compose exec` 540 + - Example: All services have healthchecks (db, server, client) with proper intervals and timeouts 541 + 542 + ### Post-Refactor Validation 543 + - **After major refactors** (schema changes, service refactoring, entity changes), always: 544 + 1. **Run codegen in Docker**: `docker-compose exec client sh -c "cd /app/apps/client && GRAPHQL_ENDPOINT=http://localhost:3000/graphql npm run codegen"` 545 + 2. **Run TypeScript typecheck**: `docker-compose exec client sh -c "cd /app/apps/client && npx tsc --noEmit"` 546 + 3. **Fix any type errors** before considering the refactor complete 547 + 4. **Clean up generated JavaScript files**: `find apps/client -name "*.js" -type f | grep -v node_modules | xargs rm -f` 548 + - This ensures the frontend remains in sync with backend schema changes and catches type issues early 549 + 550 + ## Bug Tracking 551 + 552 + ### Bug Report Format 553 + When reporting bugs, use this standardized format in `apps/docs/content/KNOWN_BUGS.md`: 554 + 555 + ```markdown 556 + ### [Bug Title] 557 + - **File**: `path/to/file.ts` 558 + - **Issue**: Brief description of the problem 559 + - **Steps to Reproduce**: 560 + 1. Step 1 561 + 2. Step 2 562 + 3. Step 3 563 + - **Expected Behavior**: What should happen 564 + - **Actual Behavior**: What actually happens 565 + - **Priority**: High/Medium/Low 566 + - **Status**: Open/In Progress/Fixed 567 + - **Commit**: `abc1234` (if applicable) 568 + - **Ticket**: `#123` or `JIRA-456` (if applicable) 569 + - **PR**: `#789` or `https://github.com/owner/repo/pull/789` (if applicable) 570 + ``` 571 + 572 + ### Bug Management 573 + - **Track all bugs** in `apps/docs/content/KNOWN_BUGS.md` 574 + - **Organize by category** (Authentication, UI, Backend, etc.) 575 + - **Update status** when bugs are fixed 576 + - **Include file paths** for easy navigation 577 + - **Use clear, descriptive titles** 578 + - **Provide reproduction steps** for complex bugs 579 + - **Mark fixed bugs** with โœ… and fix date
+14 -107
.dockerignore
··· 1 1 # Dependencies 2 2 node_modules/ 3 - npm-debug.log* 4 - yarn-debug.log* 5 - yarn-error.log* 6 - pnpm-debug.log* 7 3 8 4 # Keep packages directory for workspace dependencies 9 5 !packages/ ··· 11 7 # Build outputs 12 8 dist/ 13 9 build/ 14 - .next/ 15 - out/ 10 + .turbo/ 11 + .vite/ 16 12 17 13 # Environment files 18 14 .env 19 15 .env.local 20 - .env.development.local 21 - .env.test.local 22 - .env.production.local 16 + .env.*.local 23 17 24 18 # IDE and editor files 25 19 .vscode/ 26 20 .idea/ 27 - *.swp 28 - *.swo 29 - *~ 30 21 31 22 # OS generated files 32 23 .DS_Store 33 - .DS_Store? 34 24 ._* 35 - .Spotlight-V100 36 - .Trashes 37 - ehthumbs.db 38 25 Thumbs.db 39 26 40 27 # Git ··· 53 40 # Runtime data 54 41 pids/ 55 42 *.pid 56 - *.seed 57 - *.pid.lock 43 + db-data 58 44 59 - # Coverage directory used by tools like istanbul 45 + # Coverage 60 46 coverage/ 61 47 *.lcov 62 48 63 - # nyc test coverage 64 - .nyc_output 65 - 66 - # Dependency directories 67 - jspm_packages/ 68 - 69 - # Optional npm cache directory 70 - .npm 71 - 72 - # Optional eslint cache 73 - .eslintcache 74 - 75 - # Microbundle cache 76 - .rpt2_cache/ 77 - .rts2_cache_cjs/ 78 - .rts2_cache_es/ 79 - .rts2_cache_umd/ 80 - 81 - # Optional REPL history 82 - .node_repl_history 83 - 84 - # Output of 'npm pack' 85 - *.tgz 86 - 87 - # Yarn Integrity file 88 - .yarn-integrity 89 - 90 - # parcel-bundler cache (https://parceljs.org/) 91 - .cache 92 - .parcel-cache 93 - 94 - # Next.js build output 95 - .next 96 - 97 - # Nuxt.js build / generate output 98 - .nuxt 99 - 100 - # Gatsby files 49 + # Cache directories 101 50 .cache/ 102 - public 103 - 104 - # Storybook build outputs 105 - .out 106 - .storybook-out 107 - 108 - # Temporary folders 109 - tmp/ 110 - temp/ 111 - 112 - # Editor directories and files 113 - .vscode/* 114 - !.vscode/extensions.json 115 - .idea 116 - *.suo 117 - *.ntvs* 118 - *.njsproj 119 - *.sln 120 - *.sw? 121 - 122 - # Test files 123 - test/ 124 - tests/ 125 - __tests__/ 126 - *.test.js 127 - *.test.ts 128 - *.spec.js 129 - *.spec.ts 130 - 131 - # Documentation 132 - docs/ 133 - *.md 134 - README* 135 - 136 - # CI/CD 137 - .github/ 138 - .gitlab-ci.yml 139 - .travis.yml 140 - .circleci/ 51 + .npm/ 141 52 142 53 # Package manager lock files (keep package.json but ignore locks for faster builds) 143 54 package-lock.json 144 55 yarn.lock 145 56 pnpm-lock.yaml 146 57 147 - # Generated files 148 - *.generated.* 149 - generated/ 58 + # Generated files (will be regenerated in container) 59 + **/generated/ 60 + apps/client/src/generated/ 61 + apps/server/node_modules/@prisma/client/ 62 + apps/server/prisma/generated/ 150 63 151 - # Prisma generated client (will be regenerated in container) 152 - node_modules/@prisma/client/ 153 - 154 - # Client build artifacts 64 + # Build artifacts 155 65 apps/client/dist/ 156 - apps/client/build/ 157 - 158 - # Server build artifacts 159 66 apps/server/dist/ 160 - apps/server/build/ 67 + packages/**/dist/ 161 68 162 69 # Lerna 163 70 lerna-debug.log
+37 -10
.gitignore
··· 1 + # Dependencies 1 2 node_modules/ 3 + 4 + # Build outputs 2 5 dist/ 6 + build/ 3 7 .turbo/ 4 8 .vite/ 9 + 10 + # Environment files 11 + .env 12 + .env.local 13 + .env.*.local 14 + 15 + # OS generated files 5 16 .DS_Store 17 + ._* 18 + Thumbs.db 19 + 20 + # Logs 6 21 *.log 22 + logs/ 23 + 24 + # Coverage 7 25 coverage/ 8 - .env 9 - .env.* 10 - apps/client/dist 11 - apps/server/dist 12 - packages/**/dist 26 + *.lcov 27 + 28 + # Runtime data 29 + pids/ 30 + *.pid 13 31 docker/*.pid 14 32 db-data 15 33 16 - # Environment files 17 - .env 18 - .env.local 19 - .env.*.local 34 + # Generated files (exclude from git but keep local copies for syntax highlighting) 35 + **/generated/ 36 + **/generated-local/ 37 + apps/client/src/generated/ 38 + apps/client/src/generated-local/ 39 + apps/server/node_modules/@prisma/client/ 40 + apps/server/prisma/generated/ 41 + 42 + # Build artifacts 43 + apps/client/dist/ 44 + apps/server/dist/ 45 + packages/**/dist/ 20 46 21 - **/generated/ 47 + # Lerna 48 + lerna-debug.log
+36
apps/docs/content/KNOWN_BUGS.md
··· 1 + # Known Bugs 2 + 3 + This document tracks known bugs and issues in the CV Generator application that need to be addressed. 4 + 5 + ## Bug Report Template 6 + 7 + When reporting new bugs, please use this template: 8 + 9 + ```markdown 10 + ### [Bug Title] 11 + 12 + - **File**: `path/to/file.ts` 13 + - **Issue**: Brief description of the problem 14 + - **Steps to Reproduce**: 15 + 1. Step 1 16 + 2. Step 2 17 + 3. Step 3 18 + - **Expected Behavior**: What should happen 19 + - **Actual Behavior**: What actually happens 20 + - **Priority**: High/Medium/Low 21 + - **Status**: Open/In Progress/Fixed 22 + - **Commit**: `abc1234` (if applicable) 23 + - **Ticket**: `#123` or `JIRA-456` (if applicable) 24 + - **PR**: `#789` or `https://github.com/owner/repo/pull/789` (if applicable) 25 + ``` 26 + 27 + ## Notes 28 + 29 + - All bugs marked with โœ… have been resolved 30 + - Bugs are organized by category for easier navigation 31 + - When fixing bugs, update the status and add commit/PR information 32 + - For new bugs, add them to the "Current Open Issues" section 33 + 34 + ## Current Open Issues 35 + 36 + _No open issues at this time._
+85
apps/docs/content/docs/changelog.md
··· 1 + # Changelog 2 + 3 + All notable changes to this project will be documented in this file. 4 + 5 + The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 + and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 + 8 + ## Development Guidelines 9 + 10 + ### Commit Convention 11 + 12 + This project uses [Conventional Commits](https://www.conventionalcommits.org/) format: 13 + 14 + - `feat:` A new feature 15 + - `fix:` A bug fix 16 + - `docs:` Documentation only changes 17 + - `style:` Changes that do not affect the meaning of the code 18 + - `refactor:` A code change that neither fixes a bug nor adds a feature 19 + - `perf:` A code change that improves performance 20 + - `test:` Adding missing tests or correcting existing tests 21 + - `chore:` Changes to the build process or auxiliary tools 22 + 23 + ### Versioning 24 + 25 + - **MAJOR** version when you make incompatible API changes 26 + - **MINOR** version when you add functionality in a backwards compatible manner 27 + - **PATCH** version when you make backwards compatible bug fixes 28 + 29 + ## [Unreleased] 30 + 31 + ### Added 32 + 33 + - Initial project setup with NestJS backend and React frontend 34 + - Docker containerization with docker-compose 35 + - GraphQL API with Apollo Server 36 + - Authentication system with JWT tokens 37 + - Database schema with Prisma ORM 38 + - User management and organization features 39 + - Job experience tracking system 40 + - Company, role, level, and skill management 41 + - Toast notification system 42 + - Error boundary components 43 + - TypeScript configuration with strict settings 44 + - Biome linting and formatting configuration 45 + 46 + ### Changed 47 + 48 + - Improved configuration management using `getOrThrow()` instead of manual error throwing 49 + - Enhanced type safety by prohibiting non-null assertion operators (`!`) 50 + - Updated entity instantiation patterns to use constructor-based initialization 51 + - Improved error handling patterns throughout the codebase 52 + - Application domain entity now requires status and vacancy relations 53 + - Updated Application.fromDomain to accept domain entities instead of Prisma entities 54 + - Server status indicator now always visible on login page 55 + - Navigation section renamed from "Project Management" to "Project" 56 + - Roadmap restructured: removed "Recently Completed Features" section, all items marked as pending 57 + 58 + ### Fixed 59 + 60 + - Configuration validation issues 61 + - Type safety improvements 62 + - Build process optimization 63 + - Application domain entity type safety with required relations 64 + - Card component accessibility (replaced div with semantic button) 65 + - Icon components accessibility (added title and aria-label attributes) 66 + - TypeScript any types in useAuth hook replaced with proper types 67 + - Env interpolation plugin now correctly mutates AST nodes 68 + - Missing env.config.ts in tsconfig.node.json include list 69 + - Unused variables and parameters in docs components 70 + 71 + ### Security 72 + 73 + - Implemented secure JWT authentication 74 + - Added proper environment variable validation 75 + - Enhanced input validation with Zod schemas 76 + 77 + ## [0.1.0] - 2025-01-19 78 + 79 + ### Added 80 + 81 + - Initial project structure 82 + - Basic authentication flow 83 + - Core database entities 84 + - GraphQL schema definition 85 + - Docker development environment
+304
apps/docs/content/docs/roadmap.md
··· 1 + # Roadmap 2 + 3 + This document outlines the planned features and improvements for the CV Generator project. Items are organized by topic and prioritized for development. 4 + 5 + ## ๐Ÿข B2C CV Builder Functionality 6 + 7 + ### Core Features 8 + 9 + - [ ] **CV Templates & Design System** 10 + 11 + - [ ] Create responsive CV templates 12 + - [ ] Implement design customization options 13 + - [x] Add theme selection (professional, creative, minimal) 14 + - [x] Support for multiple CV formats (chronological, functional, hybrid) 15 + - [x] Navigation active state 16 + - [ ] Multiple template rendering formats (Liquid, LaTeX, Handlebars) 17 + - [ ] Template engine abstraction layer 18 + - [ ] Custom template syntax support 19 + 20 + - [ ] **Content Management** 21 + 22 + - [ ] Rich text editor for CV sections 23 + - [ ] Drag-and-drop section reordering 24 + - [ ] Real-time preview functionality 25 + - [ ] Auto-save functionality 26 + - [ ] Version history and rollback 27 + 28 + - [ ] **Export & Sharing** 29 + - [ ] PDF export with high-quality rendering 30 + - [ ] Multiple file format support (PDF, DOCX, HTML) 31 + - [ ] Shareable CV links 32 + - [ ] QR code generation for CV sharing 33 + - [ ] Social media integration 34 + 35 + ### Advanced Features 36 + 37 + - [ ] **AI-Powered Enhancements** 38 + 39 + - [ ] AI content suggestions and improvements 40 + - [ ] Keyword optimization for ATS systems 41 + - [ ] Skills gap analysis 42 + - [ ] Industry-specific recommendations 43 + 44 + - [ ] **Analytics & Insights** 45 + - [ ] CV view tracking and analytics 46 + - [ ] Performance metrics dashboard 47 + - [ ] ATS compatibility scoring 48 + - [ ] Improvement suggestions 49 + 50 + ### User Experience 51 + 52 + - [ ] **Onboarding & Tutorials** 53 + 54 + - [x] Interactive CV creation wizard 55 + - [ ] Step-by-step guidance system 56 + - [ ] Sample CV library 57 + - [ ] Video tutorials and help center 58 + 59 + - [ ] **Mobile Experience** 60 + - [ ] Mobile-responsive design 61 + - [ ] Progressive Web App (PWA) features 62 + - [ ] Offline editing capabilities 63 + - [ ] Mobile-optimized export options 64 + 65 + --- 66 + 67 + ## ๐Ÿข B2B Solution for Managing Groups of Employees 68 + 69 + ### Organization Management 70 + 71 + - [ ] **Organization Management** 72 + - [ ] Multi-tenant architecture 73 + - [ ] Organization hierarchy management 74 + - [ ] Role-based access control (RBAC) 75 + - [ ] Granular permission system 76 + - [ ] Role assignment and management 77 + - [ ] Permission inheritance and delegation 78 + 79 + - [ ] **Multi-tenant Architecture** 80 + 81 + - [ ] Department and team structures 82 + - [ ] Custom organization branding 83 + 84 + - [ ] **Job Experience Management** 85 + - [ ] Full CRUD for companies 86 + - [ ] Full CRUD for roles 87 + - [ ] Full CRUD for levels 88 + - [ ] Full CRUD for skills 89 + 90 + - [ ] **Vacancy Management** 91 + - [x] Job vacancy creation 92 + - [ ] Vacancy management interface 93 + - [ ] Vacancy search and filtering 94 + 95 + - [ ] **Employee Management** 96 + - [ ] Bulk employee import/export 97 + - [x] Employee profile management 98 + - [x] Skills inventory and tracking 99 + - [ ] Performance review integration 100 + - [x] Employee directory with search and filtering 101 + 102 + ### HR Analytics & Reporting 103 + 104 + - [ ] **Workforce Analytics** 105 + 106 + - [ ] Skills gap analysis across organization 107 + - [ ] Employee development tracking 108 + - [ ] Career progression insights 109 + - [ ] Diversity and inclusion metrics 110 + - [ ] Retention and turnover analysis 111 + 112 + - [ ] **Reporting Dashboard** 113 + - [ ] Custom report builder 114 + - [ ] Scheduled report delivery 115 + - [ ] Executive summary dashboards 116 + - [ ] Compliance reporting 117 + - [ ] Export capabilities (PDF, Excel, CSV) 118 + 119 + ### Integration & Automation 120 + 121 + - [ ] **HR System Integrations** 122 + 123 + - [ ] ATS (Applicant Tracking System) integration 124 + - [ ] HRIS (Human Resource Information System) sync 125 + - [ ] Learning Management System (LMS) integration 126 + - [ ] Performance management system integration 127 + - [ ] Single Sign-On (SSO) support 128 + 129 + - [ ] **Workflow Automation** 130 + - [ ] Automated CV updates and reminders 131 + - [ ] Approval workflows for CV changes 132 + - [ ] Notification system for managers 133 + - [ ] Bulk operations and batch processing 134 + - [ ] API for custom integrations 135 + - [ ] Message queue integration (RabbitMQ, Redis Queue, Bull) 136 + - [ ] Asynchronous job processing 137 + - [ ] Background task scheduling 138 + 139 + ### Compliance & Security 140 + 141 + - [ ] **Data Privacy & Security** 142 + 143 + - [ ] GDPR compliance features 144 + - [ ] Data encryption at rest and in transit 145 + - [ ] Audit logging and trail 146 + - [ ] Data retention policies 147 + - [ ] Backup and disaster recovery 148 + 149 + - [ ] **Access Control** 150 + - [ ] Granular permission system 151 + - [ ] Multi-factor authentication (MFA) 152 + - [ ] Session management and timeout 153 + - [ ] IP whitelisting and restrictions 154 + - [ ] Admin console for system management 155 + 156 + --- 157 + 158 + ## ๐Ÿ“ˆ Advertising & Monetization Options 159 + 160 + ### Freemium Model 161 + 162 + - [ ] **Free Tier Features** 163 + 164 + - [ ] Basic CV templates (3-5 options) 165 + - [ ] Standard export formats (PDF only) 166 + - [ ] Limited customization options 167 + - [ ] Basic analytics and insights 168 + - [ ] Community support 169 + 170 + - [ ] **Premium Tier Features** 171 + - [ ] Unlimited template access 172 + - [ ] Advanced customization tools 173 + - [ ] Multiple export formats (PDF, DOCX, HTML) 174 + - [ ] AI-powered content suggestions 175 + - [ ] Advanced analytics and ATS optimization 176 + - [ ] Priority customer support 177 + 178 + ### Enterprise Solutions 179 + 180 + - [ ] **B2B Subscription Tiers** 181 + - [ ] Small business package (up to 50 employees) 182 + - [ ] Medium enterprise package (up to 500 employees) 183 + - [ ] Large enterprise package (unlimited employees) 184 + - [ ] Custom enterprise solutions with dedicated support 185 + 186 + ### Revenue Streams 187 + 188 + - [ ] **Direct Monetization** 189 + 190 + - [ ] Subscription-based pricing models 191 + - [ ] Pay-per-use CV generation 192 + - [ ] Premium template marketplace 193 + - [ ] White-label solutions for partners 194 + 195 + - [ ] **Partnership & Integration Revenue** 196 + - [ ] Job board partnerships and referrals 197 + - [ ] Recruitment agency integrations 198 + - [ ] HR software marketplace listings 199 + - [ ] Affiliate marketing programs 200 + 201 + ### Marketing & Growth 202 + 203 + - [ ] **User Acquisition** 204 + 205 + - [ ] SEO optimization for CV-related keywords 206 + - [ ] Content marketing strategy (CV tips, career advice) 207 + - [ ] Social media presence and engagement 208 + - [ ] Influencer partnerships and collaborations 209 + - [ ] Referral program implementation 210 + 211 + - [ ] **Retention & Engagement** 212 + - [ ] Email marketing campaigns 213 + - [ ] In-app notifications and tips 214 + - [ ] Gamification elements (achievements, progress tracking) 215 + - [ ] Community features and forums 216 + - [ ] Regular feature updates and improvements 217 + 218 + --- 219 + 220 + ## ๐Ÿ›  Technical Infrastructure 221 + 222 + ### Core Systems 223 + 224 + - [ ] **Authentication System** 225 + - [x] JWT-based authentication 226 + - [x] Refresh token mechanism 227 + - [x] User session management 228 + 229 + - [ ] **Database System** 230 + - [x] PostgreSQL database setup 231 + - [x] Prisma ORM integration 232 + - [x] Database migrations 233 + 234 + - [ ] **GraphQL API** 235 + - [x] Apollo Server implementation 236 + - [x] GraphQL schema design 237 + - [x] Query and mutation resolvers 238 + 239 + - [ ] **Frontend Application** 240 + - [x] React application setup 241 + - [x] TypeScript configuration 242 + - [x] Modern UI components 243 + 244 + - [ ] **Docker Infrastructure** 245 + - [x] Containerization setup 246 + - [x] Health checks configuration 247 + - [x] Multi-service orchestration 248 + 249 + - [ ] **Testing Infrastructure** 250 + - [ ] E2E testing setup 251 + - [ ] Integration testing 252 + - [ ] Test coverage 253 + 254 + - [ ] **Code Quality** 255 + - [x] Biome linting configuration 256 + - [x] TypeScript strict mode 257 + - [x] Code formatting standards 258 + 259 + - [ ] **Email System** 260 + - [ ] Email service integration (SendGrid, AWS SES, SMTP) 261 + - [ ] Transactional email templates 262 + - [ ] Email notification system 263 + - [ ] Email verification and password reset 264 + - [ ] Bulk email capabilities 265 + - [ ] Email delivery tracking and analytics 266 + 267 + ### Platform & Scalability 268 + 269 + - [ ] **Performance Optimization** 270 + 271 + - [ ] Database query optimization 272 + - [ ] Caching strategies (Redis implementation) 273 + - [ ] CDN integration for static assets 274 + - [ ] Load balancing and auto-scaling 275 + - [ ] Performance monitoring and alerting 276 + 277 + - [ ] **DevOps & Deployment** 278 + - [ ] CI/CD pipeline optimization 279 + - [ ] Automated testing (unit, integration, e2e) 280 + - [ ] Infrastructure as Code (IaC) 281 + - [ ] Blue-green deployment strategy 282 + - [ ] Monitoring and logging solutions 283 + 284 + ### Security & Compliance 285 + 286 + - [ ] **Security Enhancements** 287 + - [ ] Penetration testing and security audits 288 + - [ ] Vulnerability scanning and management 289 + - [ ] Security headers and CSP implementation 290 + - [ ] Rate limiting and DDoS protection 291 + - [ ] Security incident response plan 292 + 293 + ### Data & Analytics 294 + 295 + - [ ] **Data Infrastructure** 296 + - [ ] Data warehouse implementation 297 + - [ ] ETL processes for analytics 298 + - [ ] Real-time analytics pipeline 299 + - [ ] Data visualization tools 300 + - [ ] Machine learning model deployment 301 + 302 + --- 303 + 304 + _This roadmap is a living document and will be updated regularly based on user feedback, market research, and business priorities._
+31
apps/docs/src/components/ComponentExample.tsx
··· 1 + import type React from "react"; 2 + import { useState } from "react"; 3 + 4 + /** 5 + * Wrapper component for rendering stateful component examples in MDX 6 + * Provides useState functionality for components that need state management 7 + */ 8 + export const ComponentExample = ({ 9 + children, 10 + className, 11 + }: { 12 + children: React.ReactNode; 13 + className?: string; 14 + }) => { 15 + return <div className={className}>{children}</div>; 16 + }; 17 + 18 + /** 19 + * Hook provider for component examples that need shared state 20 + * Components should be available directly from MDX components scope 21 + */ 22 + export const ExampleStateProvider = <T,>({ 23 + children, 24 + initialValue, 25 + }: { 26 + children: (value: T, setValue: (value: T) => void) => React.ReactNode; 27 + initialValue?: T; 28 + }) => { 29 + const [value, setValue] = useState<T>(initialValue as T); 30 + return <>{children(value, setValue)}</>; 31 + };
+126
apps/docs/src/components/Sidebar.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { Link, useLocation } from "react-router-dom"; 3 + import { APP_META, ENV_VARS, NAVIGATION, type NavigationItem } from "../config"; 4 + 5 + const isDocSection = ( 6 + item: NavigationItem, 7 + ): item is { 8 + title: string; 9 + icon?: string; 10 + items: Array<{ title: string; slug: string; icon?: string }>; 11 + } => { 12 + return "items" in item && Array.isArray(item.items); 13 + }; 14 + 15 + export const Sidebar = () => { 16 + const location = useLocation(); 17 + const currentPath = location.pathname.slice(1); // Remove leading slash 18 + 19 + const [expandedSections, setExpandedSections] = useState<Set<string>>( 20 + new Set(), 21 + ); 22 + 23 + useEffect(() => { 24 + const initiallyExpanded = new Set<string>(); 25 + NAVIGATION.forEach((item) => { 26 + if (isDocSection(item)) { 27 + const isSectionActive = item.items.some( 28 + (doc) => currentPath === doc.slug, 29 + ); 30 + if (isSectionActive) { 31 + initiallyExpanded.add(item.title); 32 + } 33 + } 34 + }); 35 + setExpandedSections(initiallyExpanded); 36 + }, [currentPath]); 37 + 38 + const toggleSection = (sectionTitle: string) => { 39 + setExpandedSections((prev) => { 40 + const next = new Set(prev); 41 + if (next.has(sectionTitle)) { 42 + next.delete(sectionTitle); 43 + } else { 44 + next.add(sectionTitle); 45 + } 46 + return next; 47 + }); 48 + }; 49 + 50 + return ( 51 + <aside className="w-64 bg-ctp-mantle border-r border-ctp-surface0 p-6"> 52 + <div className="mb-8"> 53 + <h1 className="text-xl font-bold text-ctp-text">{APP_META.title}</h1> 54 + <p className="text-sm text-ctp-subtext0">{APP_META.subtitle}</p> 55 + </div> 56 + 57 + <nav className="space-y-4"> 58 + {NAVIGATION.map((item) => { 59 + if (isDocSection(item)) { 60 + const isExpanded = expandedSections.has(item.title); 61 + 62 + return ( 63 + <div key={item.title} className="space-y-1"> 64 + <button 65 + type="button" 66 + onClick={() => toggleSection(item.title)} 67 + className="flex items-center w-full px-3 py-2 text-xs font-semibold text-ctp-subtext0 uppercase tracking-wider hover:text-ctp-text transition-colors" 68 + > 69 + {item.icon && <span className="mr-2">{item.icon}</span>} 70 + {item.title} 71 + </button> 72 + {isExpanded && ( 73 + <div className="ml-2 space-y-1"> 74 + {item.items.map((doc) => { 75 + const isActive = currentPath === doc.slug; 76 + return ( 77 + <Link 78 + key={doc.slug} 79 + to={`/${doc.slug}`} 80 + className={`block px-3 py-2 rounded-md text-sm transition-colors ${ 81 + isActive 82 + ? "bg-ctp-surface0 text-ctp-text font-medium" 83 + : "text-ctp-subtext0 hover:bg-ctp-surface0/50 hover:text-ctp-text" 84 + }`} 85 + > 86 + {doc.icon && <span className="mr-2">{doc.icon}</span>} 87 + {doc.title} 88 + </Link> 89 + ); 90 + })} 91 + </div> 92 + )} 93 + </div> 94 + ); 95 + } 96 + 97 + // Handle flat navigation items (for backward compatibility) 98 + const isActive = currentPath === item.slug; 99 + return ( 100 + <Link 101 + key={item.slug} 102 + to={`/${item.slug}`} 103 + className={`block px-3 py-2 rounded-md text-sm transition-colors ${ 104 + isActive 105 + ? "bg-ctp-surface0 text-ctp-text font-medium" 106 + : "text-ctp-subtext0 hover:bg-ctp-surface0/50 hover:text-ctp-text" 107 + }`} 108 + > 109 + {item.icon && <span className="mr-2">{item.icon}</span>} 110 + {item.title} 111 + </Link> 112 + ); 113 + })} 114 + </nav> 115 + 116 + <div className="mt-8 pt-8 border-t border-ctp-surface0"> 117 + <a 118 + href={ENV_VARS.CLIENT_URL} 119 + className="text-sm text-ctp-blue hover:text-ctp-sapphire transition-colors" 120 + > 121 + {APP_META.backToAppLabel} 122 + </a> 123 + </div> 124 + </aside> 125 + ); 126 + };
+25
apps/docs/src/config/env-interpolation-remark-plugin.ts
··· 1 + import type { Root, Text } from "mdast"; 2 + import type { Plugin } from "unified"; 3 + import { visit } from "unist-util-visit"; 4 + import { ENV_VARS } from "./env.config"; 5 + 6 + /** 7 + * Remark plugin to interpolate environment variables in markdown 8 + * Replaces {{VARIABLE_NAME}} with actual values from ENV_VARS 9 + */ 10 + export const envInterpolationPlugin: Plugin<[], Root> = () => (tree: Root) => { 11 + visit(tree, "text", (node: Text) => { 12 + if (!node.value) { 13 + return; 14 + } 15 + 16 + const interpolated = node.value.replace( 17 + /\{\{(\w+)\}\}/g, 18 + (match, varName) => ENV_VARS[varName] ?? match, 19 + ); 20 + 21 + if (interpolated !== node.value) { 22 + node.value = interpolated; 23 + } 24 + }); 25 + };
+132
apps/docs/src/config/navigation.config.ts
··· 1 + /** 2 + * Documentation navigation configuration 3 + * 4 + * This file defines the structure and links for the documentation sidebar 5 + */ 6 + 7 + export interface DocLink { 8 + title: string; 9 + slug: string; 10 + icon?: string; 11 + } 12 + 13 + export interface DocSection { 14 + title: string; 15 + icon?: string; 16 + items: DocLink[]; 17 + } 18 + 19 + export type NavigationItem = DocSection | DocLink; 20 + 21 + /** 22 + * Helper function to format component name for display 23 + * Converts "searchableselect" to "SearchableSelect", etc. 24 + */ 25 + const formatComponentName = (slug: string): string => { 26 + const filename = slug.split("/").pop(); 27 + if (!filename) { 28 + return slug; 29 + } 30 + return filename 31 + .split("-") 32 + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 33 + .join(""); 34 + }; 35 + 36 + /** 37 + * Component slugs - manually maintained list of component documentation files 38 + * This avoids circular dependencies while ensuring all components are listed 39 + */ 40 + const COMPONENT_SLUGS = [ 41 + "components/badge", 42 + "components/button", 43 + "components/calendar", 44 + "components/card", 45 + "components/checkbox", 46 + "components/formatteddate", 47 + "components/formatteddaterange", 48 + "components/iconbutton", 49 + "components/pageheader", 50 + "components/placeholder", 51 + "components/rangeslider", 52 + "components/searchableselect", 53 + "components/select", 54 + "components/statusbadge", 55 + "components/table", 56 + "components/textarea", 57 + "components/textinput", 58 + ]; 59 + 60 + /** 61 + * Get component navigation items 62 + */ 63 + const getComponentItems = (): DocLink[] => { 64 + const componentDocs = COMPONENT_SLUGS.map((slug) => ({ 65 + title: formatComponentName(slug), 66 + slug, 67 + })); 68 + 69 + // Add overview at the top 70 + return [ 71 + { title: "Components Overview", slug: "components" }, 72 + ...componentDocs, 73 + ]; 74 + }; 75 + 76 + /** 77 + * Documentation navigation structure 78 + * Supports both sections (groups) and individual links 79 + * Order in this array determines the order in the sidebar 80 + */ 81 + export const NAVIGATION: NavigationItem[] = [ 82 + { 83 + title: "Architecture", 84 + icon: "๐Ÿ—๏ธ", 85 + items: [ 86 + { title: "Architecture Overview", slug: "docs/architecture" }, 87 + { title: "Frontend Structure", slug: "docs/frontend-structure" }, 88 + { title: "Docker Strategy", slug: "docs/docker-strategy" }, 89 + ], 90 + }, 91 + { 92 + title: "Project", 93 + icon: "๐Ÿ“‹", 94 + items: [ 95 + { title: "Roadmap", slug: "docs/roadmap" }, 96 + { title: "Changelog", slug: "docs/changelog" }, 97 + ], 98 + }, 99 + { 100 + title: "API", 101 + icon: "๐Ÿ”Œ", 102 + items: [ 103 + { title: "API Overview", slug: "api" }, 104 + { title: "GraphQL Architecture", slug: "docs/graphql-architecture" }, 105 + ], 106 + }, 107 + { 108 + title: "Components", 109 + icon: "๐Ÿงฉ", 110 + items: getComponentItems(), 111 + }, 112 + ]; 113 + 114 + /** 115 + * Flattened list of all navigation links (for backward compatibility) 116 + * @deprecated Use NAVIGATION instead 117 + */ 118 + export const DOC_LINKS: DocLink[] = NAVIGATION.flatMap((item) => { 119 + if ("items" in item) { 120 + return item.items; 121 + } 122 + return [item]; 123 + }); 124 + 125 + /** 126 + * App metadata 127 + */ 128 + export const APP_META = { 129 + title: "CV Generator", 130 + subtitle: "Documentation", 131 + backToAppLabel: "โ† Back to App", 132 + };
+16
apps/docs/tsconfig.node.json
··· 1 + { 2 + "extends": "../../packages/tsconfig/tsconfig.node.json", 3 + "compilerOptions": { 4 + "composite": true, 5 + "skipLibCheck": true, 6 + "module": "ESNext", 7 + "moduleResolution": "bundler", 8 + "allowSyntheticDefaultImports": true, 9 + "exactOptionalPropertyTypes": false 10 + }, 11 + "include": [ 12 + "vite.config.ts", 13 + "src/config/env-interpolation-remark-plugin.ts", 14 + "src/config/env.config.ts" 15 + ] 16 + }
+78
apps/server/src/modules/application/application.entity.ts
··· 1 + import { BaseEntity } from "@/modules/base/base.entity"; 2 + 3 + export interface ApplicationStatusRelation { 4 + id: string; 5 + name: string; 6 + description: string | null; 7 + createdAt: Date; 8 + updatedAt: Date; 9 + } 10 + 11 + export interface VacancyRelation { 12 + id: string; 13 + title: string; 14 + description: string | null; 15 + location: string | null; 16 + minSalary: number | null; 17 + maxSalary: number | null; 18 + company: { 19 + name: string; 20 + }; 21 + role: { 22 + name: string; 23 + }; 24 + level: { 25 + name: string; 26 + } | null; 27 + jobType: { 28 + name: string; 29 + } | null; 30 + } 31 + 32 + export interface CVRelation { 33 + id: string; 34 + title: string; 35 + } 36 + 37 + export class Application extends BaseEntity { 38 + userId: string; 39 + vacancyId: string; 40 + cvId?: string; 41 + coverLetter?: string; 42 + statusId: string; 43 + appliedAt: Date; 44 + status: ApplicationStatusRelation; 45 + vacancy: VacancyRelation; 46 + cv: CVRelation | null; 47 + 48 + constructor( 49 + id: string, 50 + userId: string, 51 + vacancyId: string, 52 + statusId: string, 53 + appliedAt: Date, 54 + createdAt: Date, 55 + updatedAt: Date, 56 + status: ApplicationStatusRelation, 57 + vacancy: VacancyRelation, 58 + cv: CVRelation | null = null, 59 + cvId?: string, 60 + coverLetter?: string, 61 + ) { 62 + super(id, createdAt, updatedAt); 63 + this.userId = userId; 64 + this.vacancyId = vacancyId; 65 + this.statusId = statusId; 66 + this.appliedAt = appliedAt; 67 + this.status = status; 68 + this.vacancy = vacancy; 69 + this.cv = cv; 70 + 71 + if (cvId !== undefined) { 72 + this.cvId = cvId; 73 + } 74 + if (coverLetter !== undefined) { 75 + this.coverLetter = coverLetter; 76 + } 77 + } 78 + }
+133
apps/server/src/modules/application/application.mapper.ts
··· 1 + import { Injectable } from "@nestjs/common"; 2 + import type { Prisma } from "@prisma/client"; 3 + import type { BaseMapper } from "@/modules/base/mapper.interface"; 4 + import { 5 + Application, 6 + type ApplicationStatusRelation, 7 + type CVRelation, 8 + type VacancyRelation, 9 + } from "./application.entity"; 10 + 11 + type PrismaApplication = Prisma.ApplicationGetPayload<Record<string, never>>; 12 + 13 + type PrismaApplicationWithRelations = Prisma.ApplicationGetPayload<{ 14 + include: { 15 + status: true; 16 + vacancy: { 17 + include: { 18 + company: true; 19 + role: true; 20 + level: true; 21 + jobType: true; 22 + }; 23 + }; 24 + cv: true; 25 + }; 26 + }>; 27 + 28 + @Injectable() 29 + export class ApplicationMapper 30 + implements BaseMapper<PrismaApplication, Application> 31 + { 32 + toDomain(prismaApplication: null): null; 33 + toDomain(prismaApplication: PrismaApplication): Application; 34 + toDomain(prismaApplication: PrismaApplicationWithRelations): Application; 35 + toDomain( 36 + prismaApplication: 37 + | PrismaApplication 38 + | PrismaApplicationWithRelations 39 + | null, 40 + ): Application | null; 41 + toDomain( 42 + prismaApplication: 43 + | PrismaApplication 44 + | PrismaApplicationWithRelations 45 + | null, 46 + ): Application | null { 47 + if (!prismaApplication) { 48 + return null; 49 + } 50 + 51 + const hasRelations = 52 + "status" in prismaApplication && 53 + prismaApplication.status !== null && 54 + "vacancy" in prismaApplication && 55 + prismaApplication.vacancy !== null; 56 + 57 + if (!hasRelations) { 58 + throw new Error( 59 + "Application domain entity requires status and vacancy relations", 60 + ); 61 + } 62 + 63 + const withRelations = prismaApplication as PrismaApplicationWithRelations; 64 + const status: ApplicationStatusRelation = { 65 + id: withRelations.status.id, 66 + name: withRelations.status.name, 67 + description: withRelations.status.description, 68 + createdAt: withRelations.status.createdAt, 69 + updatedAt: withRelations.status.updatedAt, 70 + }; 71 + 72 + const vacancy: VacancyRelation = { 73 + id: withRelations.vacancy.id, 74 + title: withRelations.vacancy.title, 75 + description: withRelations.vacancy.description, 76 + location: withRelations.vacancy.location, 77 + minSalary: withRelations.vacancy.minSalary, 78 + maxSalary: withRelations.vacancy.maxSalary, 79 + company: { 80 + name: withRelations.vacancy.company.name, 81 + }, 82 + role: { 83 + name: withRelations.vacancy.role.name, 84 + }, 85 + level: withRelations.vacancy.level 86 + ? { name: withRelations.vacancy.level.name } 87 + : null, 88 + jobType: withRelations.vacancy.jobType 89 + ? { name: withRelations.vacancy.jobType.name } 90 + : null, 91 + }; 92 + 93 + const cv: CVRelation | null = withRelations.cv 94 + ? { 95 + id: withRelations.cv.id, 96 + title: withRelations.cv.title, 97 + } 98 + : null; 99 + 100 + return new Application( 101 + withRelations.id, 102 + withRelations.userId, 103 + withRelations.vacancyId, 104 + withRelations.statusId, 105 + withRelations.appliedAt, 106 + withRelations.createdAt, 107 + withRelations.updatedAt, 108 + status, 109 + vacancy, 110 + cv, 111 + withRelations.cvId ?? undefined, 112 + withRelations.coverLetter ?? undefined, 113 + ); 114 + } 115 + 116 + mapToDomain(prismaApplications: PrismaApplication[]): Application[] { 117 + return prismaApplications 118 + .map((application) => this.toDomain(application)) 119 + .filter( 120 + (application): application is Application => application !== null, 121 + ); 122 + } 123 + 124 + mapToDomainWithRelations( 125 + prismaApplications: PrismaApplicationWithRelations[], 126 + ): Application[] { 127 + return prismaApplications 128 + .map((application) => this.toDomain(application)) 129 + .filter( 130 + (application): application is Application => application !== null, 131 + ); 132 + } 133 + }
+19
apps/server/src/modules/application/application.module.ts
··· 1 + import { Module } from "@nestjs/common"; 2 + import { AuthModule } from "@/modules/auth/auth.module"; 3 + import { DatabaseModule } from "@/modules/database/database.module"; 4 + import { ApplicationMapper } from "./application.mapper"; 5 + import { ApplicationService } from "./application.service"; 6 + import { ApplicationResolver } from "./graphql/application.resolver"; 7 + import { ApplicationUserFieldResolver } from "./graphql/user-field.resolver"; 8 + 9 + @Module({ 10 + imports: [DatabaseModule, AuthModule], 11 + providers: [ 12 + ApplicationService, 13 + ApplicationMapper, 14 + ApplicationResolver, 15 + ApplicationUserFieldResolver, 16 + ], 17 + exports: [ApplicationService, ApplicationMapper], 18 + }) 19 + export class ApplicationModule {}
+170
apps/server/src/modules/application/application.service.ts
··· 1 + import { Injectable, Logger } from "@nestjs/common"; 2 + import { notFound } from "@/modules/base/not-found.util"; 3 + import { PrismaService } from "@/modules/database/prisma.service"; 4 + import { Application } from "./application.entity"; 5 + import { ApplicationMapper } from "./application.mapper"; 6 + 7 + @Injectable() 8 + export class ApplicationService { 9 + private readonly logger = new Logger(ApplicationService.name); 10 + 11 + constructor( 12 + private readonly prisma: PrismaService, 13 + private readonly applicationMapper: ApplicationMapper, 14 + ) {} 15 + 16 + async createApplication( 17 + userId: string, 18 + vacancyId: string, 19 + statusId: string, 20 + cvId?: string, 21 + coverLetter?: string, 22 + ): Promise<Application> { 23 + this.logger.log( 24 + `Creating application for user ${userId} to vacancy ${vacancyId}`, 25 + ); 26 + 27 + // Check if user already applied to this vacancy 28 + const existingApplication = await this.prisma["application"].findFirst({ 29 + where: { 30 + userId, 31 + vacancyId, 32 + }, 33 + }); 34 + 35 + if (existingApplication) { 36 + throw new Error("User has already applied to this vacancy"); 37 + } 38 + 39 + const application = await this.prisma["application"].create({ 40 + data: { 41 + userId, 42 + vacancyId, 43 + statusId, 44 + cvId: cvId ?? null, 45 + coverLetter: coverLetter ?? null, 46 + }, 47 + }); 48 + 49 + return this.applicationMapper.toDomain(application); 50 + } 51 + 52 + async findApplicationsByUser(userId: string): Promise<Application[]> { 53 + this.logger.log(`Finding applications for user: ${userId}`); 54 + 55 + const applications = await this.prisma["application"].findMany({ 56 + where: { userId }, 57 + include: { 58 + vacancy: { 59 + include: { 60 + company: true, 61 + role: true, 62 + level: true, 63 + jobType: true, 64 + }, 65 + }, 66 + status: true, 67 + cv: true, 68 + }, 69 + orderBy: { appliedAt: "desc" }, 70 + }); 71 + 72 + return this.applicationMapper.mapToDomainWithRelations(applications); 73 + } 74 + 75 + async findById(id: string): Promise<Application | null> { 76 + this.logger.log(`Finding application by id: ${id}`); 77 + 78 + const application = await this.prisma["application"].findUnique({ 79 + where: { id }, 80 + include: { 81 + vacancy: { 82 + include: { 83 + company: true, 84 + role: true, 85 + level: true, 86 + jobType: true, 87 + }, 88 + }, 89 + status: true, 90 + cv: true, 91 + }, 92 + }); 93 + 94 + return application ? this.applicationMapper.toDomain(application) : null; 95 + } 96 + 97 + async findByIdOrFail(id: string): Promise<Application> { 98 + const application = await this.findById(id); 99 + return application ?? notFound("Application", "id", id); 100 + } 101 + 102 + async findByIdWithRelations(id: string): Promise<Application | null> { 103 + this.logger.log(`Finding application by id with relations: ${id}`); 104 + 105 + const application = await this.prisma["application"].findUnique({ 106 + where: { id }, 107 + include: { 108 + vacancy: { 109 + include: { 110 + company: true, 111 + role: true, 112 + level: true, 113 + jobType: true, 114 + }, 115 + }, 116 + status: true, 117 + cv: true, 118 + }, 119 + }); 120 + 121 + return application ? this.applicationMapper.toDomain(application) : null; 122 + } 123 + 124 + async findApplicationsByUserWithRelations( 125 + userId: string, 126 + ): Promise<Application[]> { 127 + this.logger.log(`Finding applications for user with relations: ${userId}`); 128 + 129 + const applications = await this.prisma["application"].findMany({ 130 + where: { userId }, 131 + include: { 132 + vacancy: { 133 + include: { 134 + company: true, 135 + role: true, 136 + level: true, 137 + jobType: true, 138 + }, 139 + }, 140 + status: true, 141 + cv: true, 142 + }, 143 + orderBy: { appliedAt: "desc" }, 144 + }); 145 + 146 + return this.applicationMapper.mapToDomainWithRelations(applications); 147 + } 148 + 149 + async updateApplicationStatus( 150 + id: string, 151 + statusId: string, 152 + ): Promise<Application> { 153 + this.logger.log(`Updating application ${id} status to ${statusId}`); 154 + 155 + const application = await this.prisma["application"].update({ 156 + where: { id }, 157 + data: { statusId }, 158 + }); 159 + 160 + return this.applicationMapper.toDomain(application); 161 + } 162 + 163 + async deleteApplication(id: string): Promise<void> { 164 + this.logger.log(`Deleting application: ${id}`); 165 + 166 + await this.prisma["application"].delete({ 167 + where: { id }, 168 + }); 169 + } 170 + }
+22
apps/server/src/modules/application/graphql/application.input.ts
··· 1 + import { Field, ID, InputType } from "@nestjs/graphql"; 2 + 3 + @InputType() 4 + export class CreateApplicationInput { 5 + @Field(() => ID) 6 + vacancyId!: string; 7 + 8 + @Field(() => ID, { nullable: true }) 9 + cvId?: string | null; 10 + 11 + @Field(() => String, { nullable: true }) 12 + coverLetter?: string | null; 13 + } 14 + 15 + @InputType() 16 + export class UpdateApplicationStatusInput { 17 + @Field(() => ID) 18 + applicationId!: string; 19 + 20 + @Field(() => ID) 21 + statusId!: string; 22 + }
+123
apps/server/src/modules/application/graphql/application.resolver.ts
··· 1 + import { UseGuards } from "@nestjs/common"; 2 + import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; 3 + import { CurrentUser } from "@/modules/auth/current-user.decorator"; 4 + import { JwtAuthGuard } from "@/modules/auth/jwt-auth.guard"; 5 + import { User } from "@/modules/user/user.entity"; 6 + import { ApplicationService } from "../application.service"; 7 + import { 8 + CreateApplicationInput, 9 + UpdateApplicationStatusInput, 10 + } from "./application.input"; 11 + import { Application, ApplicationConnection } from "./application.type"; 12 + 13 + @Resolver(() => Application) 14 + @UseGuards(JwtAuthGuard) 15 + export class ApplicationResolver { 16 + constructor(private readonly applicationService: ApplicationService) {} 17 + 18 + @Query(() => ApplicationConnection) 19 + async myApplications( 20 + @CurrentUser() user: User, 21 + ): Promise<ApplicationConnection> { 22 + const applications = 23 + await this.applicationService.findApplicationsByUserWithRelations( 24 + user.id, 25 + ); 26 + 27 + return new ApplicationConnection({ 28 + edges: applications.map((app) => Application.fromDomain(app)), 29 + totalCount: applications.length, 30 + }); 31 + } 32 + 33 + @Query(() => Application, { nullable: true }) 34 + async application( 35 + @Args("id", { type: () => String }) id: string, 36 + @CurrentUser() user: User, 37 + ): Promise<Application | null> { 38 + const application = await this.applicationService.findByIdWithRelations(id); 39 + 40 + if (!application || application.userId !== user.id) { 41 + return null; 42 + } 43 + 44 + return Application.fromDomain(application); 45 + } 46 + 47 + @Mutation(() => Application) 48 + async createApplication( 49 + @Args("input") input: CreateApplicationInput, 50 + @CurrentUser() user: User, 51 + ): Promise<Application> { 52 + // Get the default "Applied" status 53 + const appliedStatus = await this.applicationService["prisma"][ 54 + "applicationStatus" 55 + ].findFirst({ 56 + where: { name: "Applied" }, 57 + }); 58 + 59 + if (!appliedStatus) { 60 + throw new Error("Application status 'Applied' not found"); 61 + } 62 + 63 + const application = await this.applicationService.createApplication( 64 + user.id, 65 + input.vacancyId, 66 + appliedStatus.id, 67 + input.cvId ?? undefined, 68 + input.coverLetter ?? undefined, 69 + ); 70 + 71 + const fullApplication = await this.applicationService.findByIdWithRelations( 72 + application.id, 73 + ); 74 + if (!fullApplication) { 75 + throw new Error("Failed to fetch created application"); 76 + } 77 + 78 + return Application.fromDomain(fullApplication); 79 + } 80 + 81 + @Mutation(() => Application) 82 + async updateApplicationStatus( 83 + @Args("input") input: UpdateApplicationStatusInput, 84 + @CurrentUser() user: User, 85 + ): Promise<Application> { 86 + // Verify the application belongs to the user 87 + const existingApplication = await this.applicationService.findById( 88 + input.applicationId, 89 + ); 90 + if (!existingApplication || existingApplication.userId !== user.id) { 91 + throw new Error("Application not found or access denied"); 92 + } 93 + 94 + const application = await this.applicationService.updateApplicationStatus( 95 + input.applicationId, 96 + input.statusId, 97 + ); 98 + 99 + const fullApplication = await this.applicationService.findByIdWithRelations( 100 + application.id, 101 + ); 102 + if (!fullApplication) { 103 + throw new Error("Failed to fetch updated application"); 104 + } 105 + 106 + return Application.fromDomain(fullApplication); 107 + } 108 + 109 + @Mutation(() => Boolean) 110 + async deleteApplication( 111 + @Args("id", { type: () => String }) id: string, 112 + @CurrentUser() user: User, 113 + ): Promise<boolean> { 114 + // Verify the application belongs to the user 115 + const existingApplication = await this.applicationService.findById(id); 116 + if (!existingApplication || existingApplication.userId !== user.id) { 117 + throw new Error("Application not found or access denied"); 118 + } 119 + 120 + await this.applicationService.deleteApplication(id); 121 + return true; 122 + } 123 + }
+226
apps/server/src/modules/application/graphql/application.type.ts
··· 1 + import { Field, ID, Int, ObjectType } from "@nestjs/graphql"; 2 + import { GraphQLDate } from "graphql-scalars"; 3 + import { Application as ApplicationEntity } from "../application.entity"; 4 + 5 + @ObjectType() 6 + export class ApplicationStatus { 7 + @Field(() => ID) 8 + id: string; 9 + 10 + @Field(() => String) 11 + name: string; 12 + 13 + @Field(() => String, { nullable: true }) 14 + description: string | null; 15 + 16 + @Field(() => GraphQLDate) 17 + createdAt: Date; 18 + 19 + @Field(() => GraphQLDate) 20 + updatedAt: Date; 21 + 22 + constructor(data: { 23 + id: string; 24 + name: string; 25 + description?: string | null; 26 + createdAt: Date; 27 + updatedAt: Date; 28 + }) { 29 + this.id = data.id; 30 + this.name = data.name; 31 + this.description = data.description ?? null; 32 + this.createdAt = data.createdAt; 33 + this.updatedAt = data.updatedAt; 34 + } 35 + } 36 + 37 + @ObjectType() 38 + export class VacancyInfo { 39 + @Field(() => ID) 40 + id: string; 41 + 42 + @Field(() => String) 43 + title: string; 44 + 45 + @Field(() => String, { nullable: true }) 46 + description: string | null; 47 + 48 + @Field(() => String, { nullable: true }) 49 + location: string | null; 50 + 51 + @Field(() => Number, { nullable: true }) 52 + minSalary: number | null; 53 + 54 + @Field(() => Number, { nullable: true }) 55 + maxSalary: number | null; 56 + 57 + @Field(() => String) 58 + companyName: string; 59 + 60 + @Field(() => String) 61 + roleName: string; 62 + 63 + @Field(() => String, { nullable: true }) 64 + levelName: string | null; 65 + 66 + @Field(() => String, { nullable: true }) 67 + jobTypeName: string | null; 68 + 69 + constructor(data: { 70 + id: string; 71 + title: string; 72 + description?: string | null; 73 + location?: string | null; 74 + minSalary?: number | null; 75 + maxSalary?: number | null; 76 + companyName: string; 77 + roleName: string; 78 + levelName?: string | null; 79 + jobTypeName?: string | null; 80 + }) { 81 + this.id = data.id; 82 + this.title = data.title; 83 + this.description = data.description ?? null; 84 + this.location = data.location ?? null; 85 + this.minSalary = data.minSalary ?? null; 86 + this.maxSalary = data.maxSalary ?? null; 87 + this.companyName = data.companyName; 88 + this.roleName = data.roleName; 89 + this.levelName = data.levelName ?? null; 90 + this.jobTypeName = data.jobTypeName ?? null; 91 + } 92 + } 93 + 94 + @ObjectType() 95 + export class CVInfo { 96 + @Field(() => ID) 97 + id: string; 98 + 99 + @Field(() => String) 100 + title: string; 101 + 102 + constructor(data: { id: string; title: string }) { 103 + this.id = data.id; 104 + this.title = data.title; 105 + } 106 + } 107 + 108 + @ObjectType() 109 + export class Application { 110 + @Field(() => ID) 111 + id: string; 112 + 113 + @Field(() => ID) 114 + userId: string; 115 + 116 + @Field(() => ID) 117 + vacancyId: string; 118 + 119 + @Field(() => ID, { nullable: true }) 120 + cvId: string | null; 121 + 122 + @Field(() => String, { nullable: true }) 123 + coverLetter: string | null; 124 + 125 + @Field(() => ID) 126 + statusId: string; 127 + 128 + @Field(() => GraphQLDate) 129 + appliedAt: Date; 130 + 131 + @Field(() => GraphQLDate) 132 + createdAt: Date; 133 + 134 + @Field(() => GraphQLDate) 135 + updatedAt: Date; 136 + 137 + @Field(() => ApplicationStatus) 138 + status: ApplicationStatus; 139 + 140 + @Field(() => VacancyInfo) 141 + vacancy: VacancyInfo; 142 + 143 + @Field(() => CVInfo, { nullable: true }) 144 + cv: CVInfo | null; 145 + 146 + constructor(data: { 147 + id: string; 148 + userId: string; 149 + vacancyId: string; 150 + cvId?: string | null; 151 + coverLetter?: string | null; 152 + statusId: string; 153 + appliedAt: Date; 154 + createdAt: Date; 155 + updatedAt: Date; 156 + status: ApplicationStatus; 157 + vacancy: VacancyInfo; 158 + cv?: CVInfo | null; 159 + }) { 160 + this.id = data.id; 161 + this.userId = data.userId; 162 + this.vacancyId = data.vacancyId; 163 + this.cvId = data.cvId ?? null; 164 + this.coverLetter = data.coverLetter ?? null; 165 + this.statusId = data.statusId; 166 + this.appliedAt = data.appliedAt; 167 + this.createdAt = data.createdAt; 168 + this.updatedAt = data.updatedAt; 169 + this.status = data.status; 170 + this.vacancy = data.vacancy; 171 + this.cv = data.cv ?? null; 172 + } 173 + 174 + static fromDomain(domainApplication: ApplicationEntity): Application { 175 + return new Application({ 176 + id: domainApplication.id, 177 + userId: domainApplication.userId, 178 + vacancyId: domainApplication.vacancyId, 179 + cvId: domainApplication.cvId ?? null, 180 + coverLetter: domainApplication.coverLetter ?? null, 181 + statusId: domainApplication.statusId, 182 + appliedAt: domainApplication.appliedAt, 183 + createdAt: domainApplication.createdAt, 184 + updatedAt: domainApplication.updatedAt, 185 + status: new ApplicationStatus({ 186 + id: domainApplication.status.id, 187 + name: domainApplication.status.name, 188 + description: domainApplication.status.description, 189 + createdAt: domainApplication.status.createdAt, 190 + updatedAt: domainApplication.status.updatedAt, 191 + }), 192 + vacancy: new VacancyInfo({ 193 + id: domainApplication.vacancy.id, 194 + title: domainApplication.vacancy.title, 195 + description: domainApplication.vacancy.description, 196 + location: domainApplication.vacancy.location, 197 + minSalary: domainApplication.vacancy.minSalary, 198 + maxSalary: domainApplication.vacancy.maxSalary, 199 + companyName: domainApplication.vacancy.company.name, 200 + roleName: domainApplication.vacancy.role.name, 201 + levelName: domainApplication.vacancy.level?.name ?? null, 202 + jobTypeName: domainApplication.vacancy.jobType?.name ?? null, 203 + }), 204 + cv: domainApplication.cv 205 + ? new CVInfo({ 206 + id: domainApplication.cv.id, 207 + title: domainApplication.cv.title, 208 + }) 209 + : null, 210 + }); 211 + } 212 + } 213 + 214 + @ObjectType() 215 + export class ApplicationConnection { 216 + @Field(() => [Application]) 217 + edges: Application[]; 218 + 219 + @Field(() => Int) 220 + totalCount: number; 221 + 222 + constructor(data: { edges: Application[]; totalCount: number }) { 223 + this.edges = data.edges; 224 + this.totalCount = data.totalCount; 225 + } 226 + }
+23
apps/server/src/modules/application/graphql/user-field.resolver.ts
··· 1 + import { UseGuards } from "@nestjs/common"; 2 + import { Parent, ResolveField, Resolver } from "@nestjs/graphql"; 3 + import { JwtAuthGuard } from "@/modules/auth/jwt-auth.guard"; 4 + import { User } from "@/modules/user/user.type"; 5 + import { ApplicationService } from "../application.service"; 6 + import { Application, ApplicationConnection } from "./application.type"; 7 + 8 + @Resolver(() => User) 9 + @UseGuards(JwtAuthGuard) 10 + export class ApplicationUserFieldResolver { 11 + constructor(private readonly applicationService: ApplicationService) {} 12 + 13 + @ResolveField(() => ApplicationConnection, { nullable: true }) 14 + async applications(@Parent() user: User): Promise<ApplicationConnection> { 15 + const applications = await this.applicationService.findApplicationsByUser( 16 + user.id, 17 + ); 18 + return new ApplicationConnection({ 19 + edges: applications.map((app) => Application.fromDomain(app)), 20 + totalCount: applications.length, 21 + }); 22 + } 23 + }
+5 -5
biome.json
··· 4 4 "includes": [ 5 5 "apps/**/*", 6 6 "packages/**/*", 7 - "!**/node_modules", 8 - "!**/dist", 9 - "!**/build", 10 - "!**/coverage", 11 - "!**/coverage-unit" 7 + "!**/node_modules/**", 8 + "!**/dist/**", 9 + "!**/build/**", 10 + "!**/coverage/**", 11 + "!**/coverage-unit/**" 12 12 ], 13 13 "ignoreUnknown": false 14 14 },
+3 -2
package.json
··· 15 15 "dev": "lerna run dev --parallel --stream", 16 16 "start": "lerna run start --stream", 17 17 "lint": "lerna run lint --stream", 18 - "codegen": "lerna run prisma:generate --scope=@cv/server && lerna run codegen --scope=@cv/client", 18 + "codegen": "lerna run prisma:generate --scope=@cv/server && lerna run codegen --scope=@cv/client && node scripts/generate-local-copies.js", 19 19 "prisma:generate": "lerna run prisma:generate --scope=@cv/server", 20 20 "prisma:migrate": "lerna run prisma:migrate --scope=@cv/server", 21 21 "prisma:deploy": "lerna run prisma:deploy --scope=@cv/server", 22 22 "prisma:studio": "lerna run prisma:studio --scope=@cv/server", 23 - "seed": "lerna run seed --scope=@cv/server" 23 + "seed": "lerna run seed --scope=@cv/server", 24 + "generate:local": "node scripts/generate-local-copies.js" 24 25 }, 25 26 "dependencies": { 26 27 "@prisma/client": "^6.17.1"
+65
scripts/generate-local-copies.js
··· 1 + #!/usr/bin/env node 2 + 3 + /** 4 + * Script to generate local copies of generated files for syntax highlighting 5 + * These files are excluded from git but needed for IDE support 6 + */ 7 + 8 + const fs = require('fs'); 9 + const path = require('path'); 10 + 11 + const projectRoot = path.join(__dirname, '..'); 12 + 13 + // Files to copy for local development 14 + const filesToCopy = [ 15 + { 16 + source: 'apps/client/src/generated/graphql.ts', 17 + destination: 'apps/client/src/generated-local/graphql.ts', 18 + description: 'GraphQL generated types and hooks' 19 + } 20 + ]; 21 + 22 + // Prisma client files to copy (if they exist) 23 + const prismaClientPath = path.join(projectRoot, 'apps/server/node_modules/@prisma/client'); 24 + if (fs.existsSync(prismaClientPath)) { 25 + const prismaFiles = fs.readdirSync(prismaClientPath, { recursive: true }) 26 + .filter(file => typeof file === 'string' && file.endsWith('.d.ts')) 27 + .slice(0, 5); // Limit to first 5 files to avoid too many copies 28 + 29 + prismaFiles.forEach(file => { 30 + filesToCopy.push({ 31 + source: `apps/server/node_modules/@prisma/client/${file}`, 32 + destination: `apps/server/src/generated-local/prisma/${file}`, 33 + description: 'Prisma client types' 34 + }); 35 + }); 36 + } 37 + 38 + console.log('๐Ÿ”„ Generating local copies of generated files for syntax highlighting...\n'); 39 + 40 + filesToCopy.forEach(({ source, destination, description }) => { 41 + const sourcePath = path.join(projectRoot, source); 42 + const destPath = path.join(projectRoot, destination); 43 + const destDir = path.dirname(destPath); 44 + 45 + try { 46 + // Create destination directory if it doesn't exist 47 + if (!fs.existsSync(destDir)) { 48 + fs.mkdirSync(destDir, { recursive: true }); 49 + } 50 + 51 + // Copy file if source exists 52 + if (fs.existsSync(sourcePath)) { 53 + fs.copyFileSync(sourcePath, destPath); 54 + console.log(`โœ… Copied: ${description}`); 55 + console.log(` ${source} โ†’ ${destination}`); 56 + } else { 57 + console.log(`โš ๏ธ Source not found: ${source}`); 58 + } 59 + } catch (error) { 60 + console.error(`โŒ Error copying ${source}:`, error.message); 61 + } 62 + }); 63 + 64 + console.log('\n๐ŸŽ‰ Local copies generated successfully!'); 65 + console.log('๐Ÿ“ Note: These files are excluded from git but provide IDE support.');