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.

refactor: remove old UI components and reorganize file structure

+125 -2825
-57
ARCHITECTURE.md
··· 1 - # Architecture Overview 2 - 3 - This project follows a three-layer architecture with clear separation of concerns and strong typing across boundaries. 4 - 5 - ## Layers 6 - 7 - 1. GraphQL (API Layer) 8 - - Location: `apps/server/src/modules/**` 9 - - Implements resolvers and GraphQL object types (classes) 10 - - GraphQL classes expose `fromDomain()` static factories to convert domain entities to API types 11 - - No database access; no business logic 12 - 13 - 2. Domain (Core Model Layer) 14 - - Location: `apps/server/src/modules/**/*.entity.ts` 15 - - Plain classes representing core business entities 16 - - Encapsulate business rules where appropriate 17 - - No framework-specific decorators except GraphQL field metadata on API classes 18 - 19 - 3. Persistence (Prisma Layer) 20 - - Location: Prisma client via `PrismaService` 21 - - No domain logic; pure data access 22 - 23 - ## Mapping Pattern 24 - 25 - - Each entity has a dedicated Mapper service implementing `BaseMapper<PrismaModel, DomainEntity>` 26 - - Mappers are `@Injectable()` Nest providers 27 - - Services inject mappers and use them to convert from Prisma models to domain entities 28 - - GraphQL classes convert domain entities via `fromDomain()` when returning data 29 - 30 - Example flow: Resolver -> Service -> Prisma -> Mapper.toDomain -> Domain -> GraphQLType.fromDomain 31 - 32 - ## Conventions 33 - 34 - - Services accept and return domain entities (never Prisma models) 35 - - Mappers live alongside their entity modules, e.g. `organization.mapper.ts`, `vacancy.mapper.ts` 36 - - Modules must register mapper providers and export them if shared 37 - - Prefer constructor-based instantiation; avoid non-null assertions 38 - - Use bracket notation for `process.env["VAR"]` and `prisma["model"]` 39 - - Linting and formatting are enforced via Biome with `$schema` validation 40 - 41 - ## Current Mapper Implementations 42 - 43 - - Auth: `user.mapper.ts` (already injectable) 44 - - Organization: `organization.mapper.ts` (injectable and used by `organization.service.ts`) 45 - - Organization Roles: `organization-role.mapper.ts` (injectable and used by `organization-role.service.ts`) 46 - - Vacancies: `vacancy.mapper.ts` (injectable and used by `vacancy.service.ts`) 47 - - Job Experience submodules (company/level/role/skill) include injectable mappers 48 - 49 - ## Docker & Tooling 50 - 51 - - Node 22 for all images (client, server, compose client service) 52 - - Biome 2.2.6 with `$schema` and `files.includes` exclusions for `node_modules`, `dist`, `build` 53 - 54 - ## Testing 55 - 56 - - E2E tests under `apps/server/test` exercise GraphQL endpoints against a test app 57 - - Tests expect services to return proper domain entities; GraphQL types handle API shaping
-68
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 - ## [Unreleased] 9 - 10 - ### Added 11 - - Initial project setup with NestJS backend and React frontend 12 - - Docker containerization with docker-compose 13 - - GraphQL API with Apollo Server 14 - - Authentication system with JWT tokens 15 - - Database schema with Prisma ORM 16 - - User management and organization features 17 - - Job experience tracking system 18 - - Company, role, level, and skill management 19 - - Toast notification system 20 - - Error boundary components 21 - - TypeScript configuration with strict settings 22 - - Biome linting and formatting configuration 23 - 24 - ### Changed 25 - - Improved configuration management using `getOrThrow()` instead of manual error throwing 26 - - Enhanced type safety by prohibiting non-null assertion operators (`!`) 27 - - Updated entity instantiation patterns to use constructor-based initialization 28 - - Improved error handling patterns throughout the codebase 29 - 30 - ### Fixed 31 - - Configuration validation issues 32 - - Type safety improvements 33 - - Build process optimization 34 - 35 - ### Security 36 - - Implemented secure JWT authentication 37 - - Added proper environment variable validation 38 - - Enhanced input validation with Zod schemas 39 - 40 - ## [0.1.0] - 2025-01-19 41 - 42 - ### Added 43 - - Initial project structure 44 - - Basic authentication flow 45 - - Core database entities 46 - - GraphQL schema definition 47 - - Docker development environment 48 - 49 - --- 50 - 51 - ## Development Guidelines 52 - 53 - ### Commit Convention 54 - This project uses [Conventional Commits](https://www.conventionalcommits.org/) format: 55 - 56 - - `feat:` A new feature 57 - - `fix:` A bug fix 58 - - `docs:` Documentation only changes 59 - - `style:` Changes that do not affect the meaning of the code 60 - - `refactor:` A code change that neither fixes a bug nor adds a feature 61 - - `perf:` A code change that improves performance 62 - - `test:` Adding missing tests or correcting existing tests 63 - - `chore:` Changes to the build process or auxiliary tools 64 - 65 - ### Versioning 66 - - **MAJOR** version when you make incompatible API changes 67 - - **MINOR** version when you add functionality in a backwards compatible manner 68 - - **PATCH** version when you make backwards compatible bug fixes
-209
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 - - [ ] **CV Templates & Design System** 9 - - [ ] Create responsive CV templates 10 - - [ ] Implement design customization options 11 - - [ ] Add theme selection (professional, creative, minimal) 12 - - [ ] Support for multiple CV formats (chronological, functional, hybrid) 13 - 14 - - [ ] **Content Management** 15 - - [ ] Rich text editor for CV sections 16 - - [ ] Drag-and-drop section reordering 17 - - [ ] Real-time preview functionality 18 - - [ ] Auto-save functionality 19 - - [ ] Version history and rollback 20 - 21 - - [ ] **Export & Sharing** 22 - - [ ] PDF export with high-quality rendering 23 - - [ ] Multiple file format support (PDF, DOCX, HTML) 24 - - [ ] Shareable CV links 25 - - [ ] QR code generation for CV sharing 26 - - [ ] Social media integration 27 - 28 - ### Advanced Features 29 - - [ ] **AI-Powered Enhancements** 30 - - [ ] AI content suggestions and improvements 31 - - [ ] Keyword optimization for ATS systems 32 - - [ ] Skills gap analysis 33 - - [ ] Industry-specific recommendations 34 - 35 - - [ ] **Analytics & Insights** 36 - - [ ] CV view tracking and analytics 37 - - [ ] Performance metrics dashboard 38 - - [ ] ATS compatibility scoring 39 - - [ ] Improvement suggestions 40 - 41 - ### User Experience 42 - - [ ] **Onboarding & Tutorials** 43 - - [ ] Interactive CV creation wizard 44 - - [ ] Step-by-step guidance system 45 - - [ ] Sample CV library 46 - - [ ] Video tutorials and help center 47 - 48 - - [ ] **Mobile Experience** 49 - - [ ] Mobile-responsive design 50 - - [ ] Progressive Web App (PWA) features 51 - - [ ] Offline editing capabilities 52 - - [ ] Mobile-optimized export options 53 - 54 - --- 55 - 56 - ## 🏢 B2B Solution for Managing Groups of Employees 57 - 58 - ### Organization Management 59 - - [ ] **Multi-tenant Architecture** 60 - - [ ] Organization hierarchy management 61 - - [ ] Role-based access control (RBAC) 62 - - [ ] Department and team structures 63 - - [ ] Custom organization branding 64 - 65 - - [ ] **Employee Management** 66 - - [ ] Bulk employee import/export 67 - - [ ] Employee profile management 68 - - [ ] Skills inventory and tracking 69 - - [ ] Performance review integration 70 - - [ ] Employee directory with search and filtering 71 - 72 - ### HR Analytics & Reporting 73 - - [ ] **Workforce Analytics** 74 - - [ ] Skills gap analysis across organization 75 - - [ ] Employee development tracking 76 - - [ ] Career progression insights 77 - - [ ] Diversity and inclusion metrics 78 - - [ ] Retention and turnover analysis 79 - 80 - - [ ] **Reporting Dashboard** 81 - - [ ] Custom report builder 82 - - [ ] Scheduled report delivery 83 - - [ ] Executive summary dashboards 84 - - [ ] Compliance reporting 85 - - [ ] Export capabilities (PDF, Excel, CSV) 86 - 87 - ### Integration & Automation 88 - - [ ] **HR System Integrations** 89 - - [ ] ATS (Applicant Tracking System) integration 90 - - [ ] HRIS (Human Resource Information System) sync 91 - - [ ] Learning Management System (LMS) integration 92 - - [ ] Performance management system integration 93 - - [ ] Single Sign-On (SSO) support 94 - 95 - - [ ] **Workflow Automation** 96 - - [ ] Automated CV updates and reminders 97 - - [ ] Approval workflows for CV changes 98 - - [ ] Notification system for managers 99 - - [ ] Bulk operations and batch processing 100 - - [ ] API for custom integrations 101 - 102 - ### Compliance & Security 103 - - [ ] **Data Privacy & Security** 104 - - [ ] GDPR compliance features 105 - - [ ] Data encryption at rest and in transit 106 - - [ ] Audit logging and trail 107 - - [ ] Data retention policies 108 - - [ ] Backup and disaster recovery 109 - 110 - - [ ] **Access Control** 111 - - [ ] Granular permission system 112 - - [ ] Multi-factor authentication (MFA) 113 - - [ ] Session management and timeout 114 - - [ ] IP whitelisting and restrictions 115 - - [ ] Admin console for system management 116 - 117 - --- 118 - 119 - ## 📈 Advertising & Monetization Options 120 - 121 - ### Freemium Model 122 - - [ ] **Free Tier Features** 123 - - [ ] Basic CV templates (3-5 options) 124 - - [ ] Standard export formats (PDF only) 125 - - [ ] Limited customization options 126 - - [ ] Basic analytics and insights 127 - - [ ] Community support 128 - 129 - - [ ] **Premium Tier Features** 130 - - [ ] Unlimited template access 131 - - [ ] Advanced customization tools 132 - - [ ] Multiple export formats (PDF, DOCX, HTML) 133 - - [ ] AI-powered content suggestions 134 - - [ ] Advanced analytics and ATS optimization 135 - - [ ] Priority customer support 136 - 137 - ### Enterprise Solutions 138 - - [ ] **B2B Subscription Tiers** 139 - - [ ] Small business package (up to 50 employees) 140 - - [ ] Medium enterprise package (up to 500 employees) 141 - - [ ] Large enterprise package (unlimited employees) 142 - - [ ] Custom enterprise solutions with dedicated support 143 - 144 - ### Revenue Streams 145 - - [ ] **Direct Monetization** 146 - - [ ] Subscription-based pricing models 147 - - [ ] Pay-per-use CV generation 148 - - [ ] Premium template marketplace 149 - - [ ] White-label solutions for partners 150 - 151 - - [ ] **Partnership & Integration Revenue** 152 - - [ ] Job board partnerships and referrals 153 - - [ ] Recruitment agency integrations 154 - - [ ] HR software marketplace listings 155 - - [ ] Affiliate marketing programs 156 - 157 - ### Marketing & Growth 158 - - [ ] **User Acquisition** 159 - - [ ] SEO optimization for CV-related keywords 160 - - [ ] Content marketing strategy (CV tips, career advice) 161 - - [ ] Social media presence and engagement 162 - - [ ] Influencer partnerships and collaborations 163 - - [ ] Referral program implementation 164 - 165 - - [ ] **Retention & Engagement** 166 - - [ ] Email marketing campaigns 167 - - [ ] In-app notifications and tips 168 - - [ ] Gamification elements (achievements, progress tracking) 169 - - [ ] Community features and forums 170 - - [ ] Regular feature updates and improvements 171 - 172 - --- 173 - 174 - ## 🛠 Technical Infrastructure 175 - 176 - ### Platform & Scalability 177 - - [ ] **Performance Optimization** 178 - - [ ] Database query optimization 179 - - [ ] Caching strategies (Redis implementation) 180 - - [ ] CDN integration for static assets 181 - - [ ] Load balancing and auto-scaling 182 - - [ ] Performance monitoring and alerting 183 - 184 - - [ ] **DevOps & Deployment** 185 - - [ ] CI/CD pipeline optimization 186 - - [ ] Automated testing (unit, integration, e2e) 187 - - [ ] Infrastructure as Code (IaC) 188 - - [ ] Blue-green deployment strategy 189 - - [ ] Monitoring and logging solutions 190 - 191 - ### Security & Compliance 192 - - [ ] **Security Enhancements** 193 - - [ ] Penetration testing and security audits 194 - - [ ] Vulnerability scanning and management 195 - - [ ] Security headers and CSP implementation 196 - - [ ] Rate limiting and DDoS protection 197 - - [ ] Security incident response plan 198 - 199 - ### Data & Analytics 200 - - [ ] **Data Infrastructure** 201 - - [ ] Data warehouse implementation 202 - - [ ] ETL processes for analytics 203 - - [ ] Real-time analytics pipeline 204 - - [ ] Data visualization tools 205 - - [ ] Machine learning model deployment 206 - 207 - --- 208 - 209 - *This roadmap is a living document and will be updated regularly based on user feedback, market research, and business priorities.*
-25
apps/client/src/components/icons/CloseIcon.tsx
··· 1 - interface CloseIconProps { 2 - className?: string; 3 - } 4 - 5 - /** 6 - * Close icon component 7 - */ 8 - export default function CloseIcon({ className = "w-4 h-4" }: CloseIconProps) { 9 - return ( 10 - <svg 11 - className={className} 12 - fill="none" 13 - stroke="currentColor" 14 - viewBox="0 0 24 24" 15 - > 16 - <title>Close icon</title> 17 - <path 18 - strokeLinecap="round" 19 - strokeLinejoin="round" 20 - strokeWidth={2} 21 - d="M6 18L18 6M6 6l12 12" 22 - /> 23 - </svg> 24 - ); 25 - }
-25
apps/client/src/components/icons/DeleteIcon.tsx
··· 1 - interface DeleteIconProps { 2 - className?: string; 3 - } 4 - 5 - /** 6 - * Delete icon component 7 - */ 8 - export default function DeleteIcon({ className = "w-4 h-4" }: DeleteIconProps) { 9 - return ( 10 - <svg 11 - className={className} 12 - fill="none" 13 - stroke="currentColor" 14 - viewBox="0 0 24 24" 15 - > 16 - <title>Delete icon</title> 17 - <path 18 - strokeLinecap="round" 19 - strokeLinejoin="round" 20 - strokeWidth={2} 21 - d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" 22 - /> 23 - </svg> 24 - ); 25 - }
-19
apps/client/src/components/icons/DocumentIcon.tsx
··· 1 - export default function DocumentIcon() { 2 - return ( 3 - <svg 4 - className="w-5 h-5" 5 - fill="none" 6 - stroke="currentColor" 7 - viewBox="0 0 24 24" 8 - aria-label="Document icon" 9 - > 10 - <title>Document</title> 11 - <path 12 - strokeLinecap="round" 13 - strokeLinejoin="round" 14 - strokeWidth={2} 15 - d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" 16 - /> 17 - </svg> 18 - ); 19 - }
-25
apps/client/src/components/icons/EditIcon.tsx
··· 1 - interface EditIconProps { 2 - className?: string; 3 - } 4 - 5 - /** 6 - * Edit icon component 7 - */ 8 - export default function EditIcon({ className = "w-4 h-4" }: EditIconProps) { 9 - return ( 10 - <svg 11 - className={className} 12 - fill="none" 13 - stroke="currentColor" 14 - viewBox="0 0 24 24" 15 - > 16 - <title>Edit icon</title> 17 - <path 18 - strokeLinecap="round" 19 - strokeLinejoin="round" 20 - strokeWidth={2} 21 - d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" 22 - /> 23 - </svg> 24 - ); 25 - }
-19
apps/client/src/components/icons/ErrorIcon.tsx
··· 1 - interface ErrorIconProps { 2 - className?: string; 3 - } 4 - 5 - /** 6 - * Error icon component 7 - */ 8 - export default function ErrorIcon({ className = "w-5 h-5" }: ErrorIconProps) { 9 - return ( 10 - <svg className={className} fill="currentColor" viewBox="0 0 20 20"> 11 - <title>Error icon</title> 12 - <path 13 - fillRule="evenodd" 14 - d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" 15 - clipRule="evenodd" 16 - /> 17 - </svg> 18 - ); 19 - }
-19
apps/client/src/components/icons/LinkIcon.tsx
··· 1 - export default function LinkIcon() { 2 - return ( 3 - <svg 4 - className="w-5 h-5" 5 - fill="none" 6 - stroke="currentColor" 7 - viewBox="0 0 24 24" 8 - aria-label="Link icon" 9 - > 10 - <title>Link</title> 11 - <path 12 - strokeLinecap="round" 13 - strokeLinejoin="round" 14 - strokeWidth={2} 15 - d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" 16 - /> 17 - </svg> 18 - ); 19 - }
-33
apps/client/src/components/icons/LoadingIcon.tsx
··· 1 - interface LoadingIconProps { 2 - className?: string; 3 - } 4 - 5 - /** 6 - * Loading spinner icon component 7 - */ 8 - export default function LoadingIcon({ 9 - className = "w-4 h-4", 10 - }: LoadingIconProps) { 11 - return ( 12 - <svg 13 - className={`${className} animate-spin`} 14 - fill="none" 15 - viewBox="0 0 24 24" 16 - > 17 - <title>Loading icon</title> 18 - <circle 19 - className="opacity-25" 20 - cx="12" 21 - cy="12" 22 - r="10" 23 - stroke="currentColor" 24 - strokeWidth="4" 25 - /> 26 - <path 27 - className="opacity-75" 28 - fill="currentColor" 29 - d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" 30 - /> 31 - </svg> 32 - ); 33 - }
-58
apps/client/src/components/icons/ToastIcon.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority"; 2 - 3 - const iconVariants = cva("w-5 h-5", { 4 - variants: { 5 - level: { 6 - success: "text-green-600", 7 - warning: "text-yellow-600", 8 - info: "text-blue-600", 9 - error: "text-red-600", 10 - }, 11 - }, 12 - defaultVariants: { 13 - level: "info", 14 - }, 15 - }); 16 - 17 - interface ToastIconProps extends VariantProps<typeof iconVariants> { 18 - level: "success" | "warning" | "info" | "error"; 19 - } 20 - 21 - /** 22 - * Toast icon component with CVA styling 23 - */ 24 - export default function ToastIcon({ level }: ToastIconProps) { 25 - const iconProps = { 26 - className: iconVariants({ level }), 27 - fill: "none", 28 - stroke: "currentColor", 29 - viewBox: "0 0 24 24", 30 - }; 31 - 32 - return ( 33 - <svg {...iconProps}> 34 - <title>{level.charAt(0).toUpperCase() + level.slice(1)} icon</title> 35 - <path 36 - strokeLinecap="round" 37 - strokeLinejoin="round" 38 - strokeWidth={2} 39 - d={getIconPath(level)} 40 - /> 41 - </svg> 42 - ); 43 - } 44 - 45 - /** 46 - * Get the SVG path for the given toast level 47 - */ 48 - function getIconPath(level: "success" | "warning" | "info" | "error"): string { 49 - const iconPaths = { 50 - success: "M5 13l4 4L19 7", 51 - warning: 52 - "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z", 53 - info: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z", 54 - error: "M6 18L18 6M6 6l12 12", 55 - }; 56 - 57 - return iconPaths[level]; 58 - }
-19
apps/client/src/components/icons/UploadIcon.tsx
··· 1 - export default function UploadIcon() { 2 - return ( 3 - <svg 4 - className="w-5 h-5" 5 - fill="none" 6 - stroke="currentColor" 7 - viewBox="0 0 24 24" 8 - aria-label="Upload icon" 9 - > 10 - <title>Upload</title> 11 - <path 12 - strokeLinecap="round" 13 - strokeLinejoin="round" 14 - strokeWidth={2} 15 - d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" 16 - /> 17 - </svg> 18 - ); 19 - }
-9
apps/client/src/components/icons/index.ts
··· 1 - export { default as CloseIcon } from "./CloseIcon"; 2 - export { default as DeleteIcon } from "./DeleteIcon"; 3 - export { default as DocumentIcon } from "./DocumentIcon"; 4 - export { default as EditIcon } from "./EditIcon"; 5 - export { default as ErrorIcon } from "./ErrorIcon"; 6 - export { default as LinkIcon } from "./LinkIcon"; 7 - export { default as LoadingIcon } from "./LoadingIcon"; 8 - export { default as ToastIcon } from "./ToastIcon"; 9 - export { default as UploadIcon } from "./UploadIcon";
+2
apps/client/src/constants/auth.ts
··· 1 1 export const AUTH_TOKEN_KEY = "auth_token" as const; 2 + export const AUTH_REFRESH_TOKEN_KEY = "auth_refresh_token" as const; 3 + export const AUTH_TOKEN_EXPIRY_KEY = "auth_token_expiry" as const; 2 4 export const AUTH_USER_QUERY_KEY = ["auth", "user"] as const;
-4
apps/client/src/features/app/queries/app.graphql
··· 1 - query Hello { 2 - hello 3 - } 4 - 5 1 query Health { 6 2 health { 7 3 status
-55
apps/client/src/features/auth/LoginForm.tsx
··· 1 - import { useState } from "react"; 2 - import { Link } from "react-router-dom"; 3 - import { useLogin } from "@/hooks/useAuth"; 4 - import Button from "@/ui/Button"; 5 - import TextInput from "@/ui/TextInput"; 6 - import AuthForm from "./AuthForm"; 7 - 8 - export default function LoginForm() { 9 - const [email, setEmail] = useState(""); 10 - const [password, setPassword] = useState(""); 11 - const [loginMutation, { loading, error }] = useLogin(); 12 - 13 - const handleSubmit = (e: React.FormEvent) => { 14 - e.preventDefault(); 15 - loginMutation({ 16 - variables: { 17 - email, 18 - password, 19 - }, 20 - }); 21 - }; 22 - 23 - return ( 24 - <AuthForm title="Welcome back"> 25 - <form className="space-y-4" onSubmit={handleSubmit}> 26 - <TextInput 27 - label="Email" 28 - type="email" 29 - placeholder="you@example.com" 30 - value={email} 31 - onChange={setEmail} 32 - error={error?.message || undefined} 33 - /> 34 - <TextInput 35 - label="Password" 36 - type="password" 37 - placeholder="••••••••" 38 - value={password} 39 - onChange={setPassword} 40 - /> 41 - <Button className="w-full" type="submit" disabled={loading}> 42 - {loading ? "Signing in..." : "Sign in"} 43 - </Button> 44 - <p className="mt-4 text-center text-sm text-ctp-subtext0"> 45 - No account yet?{" "} 46 - <Link to="/auth/register"> 47 - <Button variant="ghost" size="sm"> 48 - Register 49 - </Button> 50 - </Link> 51 - </p> 52 - </form> 53 - </AuthForm> 54 - ); 55 - }
-63
apps/client/src/features/auth/RegisterForm.tsx
··· 1 - import { useState } from "react"; 2 - import { Link } from "react-router-dom"; 3 - import { useRegister } from "@/hooks/useAuth"; 4 - import Button from "@/ui/Button"; 5 - import TextInput from "@/ui/TextInput"; 6 - import AuthForm from "./AuthForm"; 7 - 8 - export default function RegisterForm() { 9 - const [name, setName] = useState(""); 10 - const [email, setEmail] = useState(""); 11 - const [password, setPassword] = useState(""); 12 - const [register, { loading, error }] = useRegister(); 13 - 14 - const handleSubmit = (e: React.FormEvent) => { 15 - e.preventDefault(); 16 - register({ 17 - variables: { 18 - name, 19 - email, 20 - password, 21 - }, 22 - }); 23 - }; 24 - 25 - return ( 26 - <AuthForm title="Create account"> 27 - <form className="space-y-4" onSubmit={handleSubmit}> 28 - <TextInput 29 - label="Name" 30 - placeholder="Jane Doe" 31 - value={name} 32 - onChange={setName} 33 - /> 34 - <TextInput 35 - label="Email" 36 - type="email" 37 - placeholder="you@example.com" 38 - value={email} 39 - onChange={setEmail} 40 - error={error?.message || undefined} 41 - /> 42 - <TextInput 43 - label="Password" 44 - type="password" 45 - placeholder="••••••••" 46 - value={password} 47 - onChange={setPassword} 48 - /> 49 - <Button className="w-full" type="submit" disabled={loading}> 50 - {loading ? "Creating account..." : "Create account"} 51 - </Button> 52 - <p className="mt-4 text-center text-sm text-ctp-subtext0"> 53 - Already have an account?{" "} 54 - <Link to="/auth/login"> 55 - <Button variant="ghost" size="sm"> 56 - Login 57 - </Button> 58 - </Link> 59 - </p> 60 - </form> 61 - </AuthForm> 62 - ); 63 - }
+5 -5
apps/client/src/features/job-experience/components/index.ts
··· 1 1 /** 2 2 * Job Experience feature components 3 3 */ 4 - export { default as JobExperienceCard } from "./JobExperienceCard"; 4 + export { JobExperienceCard } from "./JobExperienceCard"; 5 5 export { JobExperienceCreationSelector } from "./JobExperienceCreationSelector"; 6 - export { default as JobExperienceEmpty } from "./JobExperienceEmpty"; 6 + export { JobExperienceEmpty } from "./JobExperienceEmpty"; 7 7 export { JobExperienceForm } from "./JobExperienceForm"; 8 - export { default as JobExperienceHeader } from "./JobExperienceHeader"; 9 - export { default as JobExperienceList } from "./JobExperienceList"; 10 - export { default as JobExperienceLoading } from "./JobExperienceLoading"; 8 + export { JobExperienceHeader } from "./JobExperienceHeader"; 9 + export { JobExperienceList } from "./JobExperienceList"; 10 + export { JobExperienceLoading } from "./JobExperienceLoading"; 11 11 export { JobExperienceTable } from "./JobExperienceTable";
+9 -5
apps/client/src/features/job-experience/queries/companies-query.graphql
··· 1 1 query Companies { 2 2 companies { 3 - id 4 - name 5 - description 6 - createdAt 7 - updatedAt 3 + edges { 4 + node { 5 + id 6 + name 7 + description 8 + createdAt 9 + updatedAt 10 + } 11 + } 8 12 } 9 13 }
+36 -20
apps/client/src/features/job-experience/queries/job-experience-form-data.graphql
··· 1 1 query JobExperienceFormData { 2 2 companies { 3 - id 4 - name 5 - description 6 - createdAt 7 - updatedAt 3 + edges { 4 + node { 5 + id 6 + name 7 + description 8 + createdAt 9 + updatedAt 10 + } 11 + } 8 12 } 9 13 roles { 10 - id 11 - name 12 - description 13 - createdAt 14 - updatedAt 14 + edges { 15 + node { 16 + id 17 + name 18 + description 19 + createdAt 20 + updatedAt 21 + } 22 + } 15 23 } 16 24 levels { 17 - id 18 - name 19 - description 20 - createdAt 21 - updatedAt 25 + edges { 26 + node { 27 + id 28 + name 29 + description 30 + createdAt 31 + updatedAt 32 + } 33 + } 22 34 } 23 35 skills { 24 - id 25 - name 26 - description 27 - createdAt 28 - updatedAt 36 + edges { 37 + node { 38 + id 39 + name 40 + description 41 + createdAt 42 + updatedAt 43 + } 44 + } 29 45 } 30 46 }
+9 -5
apps/client/src/features/job-experience/queries/levels-query.graphql
··· 1 1 query Levels { 2 2 levels { 3 - id 4 - name 5 - description 6 - createdAt 7 - updatedAt 3 + edges { 4 + node { 5 + id 6 + name 7 + description 8 + createdAt 9 + updatedAt 10 + } 11 + } 8 12 } 9 13 }
+34 -21
apps/client/src/features/job-experience/queries/me-job-experience.graphql
··· 1 1 query MeJobExperience { 2 - myEmploymentHistory { 3 - id 4 - startDate 5 - endDate 6 - description 7 - company { 8 - id 9 - name 10 - website 11 - } 12 - role { 13 - id 14 - name 15 - } 16 - level { 17 - id 18 - name 19 - } 20 - skills { 21 - id 22 - name 2 + me { 3 + experience { 4 + edges { 5 + node { 6 + id 7 + startDate 8 + endDate 9 + description 10 + company { 11 + id 12 + name 13 + website 14 + } 15 + role { 16 + id 17 + name 18 + } 19 + level { 20 + id 21 + name 22 + } 23 + skills { 24 + id 25 + name 26 + } 27 + } 28 + } 29 + pageInfo { 30 + hasNextPage 31 + hasPreviousPage 32 + startCursor 33 + endCursor 34 + } 35 + totalCount 23 36 } 24 37 } 25 38 }
+9 -5
apps/client/src/features/job-experience/queries/roles-query.graphql
··· 1 1 query Roles { 2 2 roles { 3 - id 4 - name 5 - description 6 - createdAt 7 - updatedAt 3 + edges { 4 + node { 5 + id 6 + name 7 + description 8 + createdAt 9 + updatedAt 10 + } 11 + } 8 12 } 9 13 }
+19 -7
apps/client/src/features/job-experience/queries/skills-query.graphql
··· 1 - query Skills { 2 - skills { 3 - id 4 - name 5 - description 6 - createdAt 7 - updatedAt 1 + query Skills($first: Int, $after: String, $searchTerm: String) { 2 + skills(first: $first, after: $after, searchTerm: $searchTerm) { 3 + edges { 4 + cursor 5 + node { 6 + id 7 + name 8 + description 9 + createdAt 10 + updatedAt 11 + } 12 + } 13 + pageInfo { 14 + hasNextPage 15 + hasPreviousPage 16 + startCursor 17 + endCursor 18 + } 19 + totalCount 8 20 } 9 21 }
+1
apps/client/src/features/organizations/components/index.ts
··· 1 + export { CollapsibleOrganizationTable } from "./CollapsibleOrganizationTable"; 1 2 export { MembersTableBody } from "./MembersTableBody"; 2 3 export { MembersTableHeader } from "./MembersTableHeader"; 3 4 export { OrganizationMemberRow } from "./OrganizationMemberRow";
-54
apps/client/src/features/user/queries/me-with-organizations.graphql
··· 1 - query MeWithOrganizations { 2 - me { 3 - id 4 - email 5 - name 6 - createdAt 7 - organizations { 8 - id 9 - name 10 - description 11 - createdAt 12 - updatedAt 13 - users { 14 - id 15 - joinedAt 16 - role { 17 - id 18 - name 19 - description 20 - color 21 - } 22 - user { 23 - id 24 - name 25 - email 26 - createdAt 27 - experience { 28 - id 29 - startDate 30 - endDate 31 - description 32 - company { 33 - id 34 - name 35 - website 36 - } 37 - role { 38 - id 39 - name 40 - } 41 - level { 42 - id 43 - name 44 - } 45 - skills { 46 - id 47 - name 48 - } 49 - } 50 - } 51 - } 52 - } 53 - } 54 - }
-23
apps/client/src/lib/apollo-client.ts
··· 1 - import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; 2 - import { setContext } from "@apollo/client/link/context"; 3 - import { AUTH_TOKEN_KEY } from "@/constants/auth"; 4 - import { getGraphQLEndpoint } from "./config"; 5 - 6 - const httpLink = createHttpLink({ 7 - uri: getGraphQLEndpoint(), 8 - }); 9 - 10 - const authLink = setContext((_, { headers }) => { 11 - const token = localStorage.getItem(AUTH_TOKEN_KEY); 12 - return { 13 - headers: { 14 - ...headers, 15 - authorization: token ? `Bearer ${token}` : "", 16 - }, 17 - }; 18 - }); 19 - 20 - export const apolloClient = new ApolloClient({ 21 - link: authLink.concat(httpLink), 22 - cache: new InMemoryCache(), 23 - });
-47
apps/client/src/lib/config.ts
··· 1 - /** 2 - * Shared configuration utilities for the client application 3 - */ 4 - 5 - /** 6 - * Get the server URL from environment variables with fallback 7 - */ 8 - export const getServerUrl = (): string => { 9 - const serverUrl = import.meta.env["VITE_SERVER_URL"]; 10 - 11 - if (!serverUrl) { 12 - return "http://localhost:3000"; 13 - } 14 - 15 - // Remove trailing slash if present 16 - return serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl; 17 - }; 18 - 19 - /** 20 - * Get the GraphQL endpoint URL 21 - */ 22 - export const getGraphQLEndpoint = (): string => { 23 - return `${getServerUrl()}/graphql`; 24 - }; 25 - 26 - /** 27 - * Parse server URL to extract components 28 - */ 29 - export const parseServerUrl = (url: string) => { 30 - try { 31 - const urlObj = new URL(url); 32 - return { 33 - protocol: urlObj.protocol.slice(0, -1), // Remove trailing ':' 34 - hostname: urlObj.hostname, 35 - port: urlObj.port || (url.startsWith("https") ? "443" : "80"), 36 - url: url, 37 - }; 38 - } catch { 39 - // Fallback for invalid URLs 40 - return { 41 - protocol: "http", 42 - hostname: "localhost", 43 - port: "3000", 44 - url: "http://localhost:3000", 45 - }; 46 - } 47 - };
-62
apps/client/src/ui/Badge.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority"; 2 - import type { ReactNode } from "react"; 3 - 4 - export type CatppuccinColor = 5 - | "ctp-red" 6 - | "ctp-orange" 7 - | "ctp-yellow" 8 - | "ctp-green" 9 - | "ctp-teal" 10 - | "ctp-sky" 11 - | "ctp-sapphire" 12 - | "ctp-blue" 13 - | "ctp-lavender" 14 - | "ctp-mauve" 15 - | "ctp-pink" 16 - | "ctp-maroon" 17 - | "ctp-peach" 18 - | "ctp-rosewater" 19 - | "ctp-gray"; 20 - 21 - const badgeVariants = cva( 22 - "inline-flex items-center rounded-full px-2 py-1 text-xs font-medium text-white whitespace-nowrap", 23 - { 24 - variants: { 25 - color: { 26 - "ctp-red": "bg-ctp-red", 27 - "ctp-orange": "bg-ctp-orange", 28 - "ctp-yellow": "bg-ctp-yellow", 29 - "ctp-green": "bg-ctp-green", 30 - "ctp-teal": "bg-ctp-teal", 31 - "ctp-sky": "bg-ctp-sky", 32 - "ctp-sapphire": "bg-ctp-sapphire", 33 - "ctp-blue": "bg-ctp-blue", 34 - "ctp-lavender": "bg-ctp-lavender", 35 - "ctp-mauve": "bg-ctp-mauve", 36 - "ctp-pink": "bg-ctp-pink", 37 - "ctp-maroon": "bg-ctp-maroon", 38 - "ctp-peach": "bg-ctp-peach", 39 - "ctp-rosewater": "bg-ctp-rosewater", 40 - "ctp-gray": "bg-ctp-gray", 41 - }, 42 - }, 43 - defaultVariants: { 44 - color: "ctp-blue", 45 - }, 46 - }, 47 - ); 48 - 49 - type BadgeProps = { 50 - children: ReactNode; 51 - color?: CatppuccinColor; 52 - } & Omit<VariantProps<typeof badgeVariants>, "color">; 53 - 54 - export default function Badge({ 55 - children, 56 - color, 57 - className, 58 - }: BadgeProps & { className?: string }) { 59 - return ( 60 - <span className={badgeVariants({ color, className })}>{children}</span> 61 - ); 62 - }
-56
apps/client/src/ui/Button.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority"; 2 - import type { ButtonHTMLAttributes, PropsWithChildren, ReactNode } from "react"; 3 - 4 - const buttonVariants = cva( 5 - "inline-flex items-center justify-center rounded px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2", 6 - { 7 - variants: { 8 - variant: { 9 - primary: 10 - "bg-ctp-mauve text-ctp-base hover:bg-ctp-mauve/90 focus:ring-ctp-mauve", 11 - secondary: 12 - "bg-ctp-surface1 text-ctp-text hover:bg-ctp-surface2 focus:ring-ctp-surface1", 13 - ghost: "text-ctp-text hover:bg-ctp-surface1 focus:ring-ctp-surface1", 14 - outline: 15 - "border border-ctp-surface0 text-ctp-text hover:bg-ctp-surface0 focus:ring-ctp-surface0", 16 - destructive: 17 - "bg-ctp-red text-ctp-base hover:bg-ctp-red/90 focus:ring-ctp-red", 18 - }, 19 - size: { 20 - sm: "px-3 py-1.5 text-xs", 21 - md: "px-4 py-2 text-sm", 22 - lg: "px-6 py-3 text-base", 23 - }, 24 - }, 25 - defaultVariants: { 26 - variant: "primary", 27 - size: "md", 28 - }, 29 - }, 30 - ); 31 - 32 - type ButtonProps = PropsWithChildren< 33 - ButtonHTMLAttributes<HTMLButtonElement> & 34 - VariantProps<typeof buttonVariants> & { 35 - leftIcon?: ReactNode; 36 - rightIcon?: ReactNode; 37 - } 38 - >; 39 - 40 - export default function Button({ 41 - children, 42 - className = "", 43 - variant, 44 - size, 45 - leftIcon, 46 - rightIcon, 47 - ...props 48 - }: ButtonProps) { 49 - return ( 50 - <button className={buttonVariants({ variant, size, className })} {...props}> 51 - {leftIcon && <span className="mr-2">{leftIcon}</span>} 52 - {children} 53 - {rightIcon && <span className="ml-2">{rightIcon}</span>} 54 - </button> 55 - ); 56 - }
-64
apps/client/src/ui/Checkbox.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority"; 2 - import type { InputHTMLAttributes } from "react"; 3 - 4 - const checkboxVariants = cva( 5 - "h-4 w-4 rounded border border-ctp-surface1 bg-ctp-surface0 text-ctp-mauve focus:ring-2 focus:ring-ctp-mauve focus:ring-offset-1 transition-colors", 6 - { 7 - variants: { 8 - state: { 9 - default: "", 10 - error: "border-ctp-red focus:ring-ctp-red", 11 - success: "border-ctp-green focus:ring-ctp-green", 12 - }, 13 - }, 14 - defaultVariants: { 15 - state: "default", 16 - }, 17 - }, 18 - ); 19 - 20 - type CheckboxProps = { 21 - label: string; 22 - error?: string; 23 - onChange?: (checked: boolean) => void; 24 - } & Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "type"> & 25 - VariantProps<typeof checkboxVariants>; 26 - 27 - export default function Checkbox({ 28 - label, 29 - className = "", 30 - error, 31 - onChange, 32 - id, 33 - ...props 34 - }: CheckboxProps) { 35 - const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { 36 - onChange?.(e.target.checked); 37 - }; 38 - 39 - const inputId = id || `checkbox-${Math.random().toString(36).substr(2, 9)}`; 40 - 41 - return ( 42 - <div className="space-y-1"> 43 - <div className="flex items-center"> 44 - <input 45 - type="checkbox" 46 - id={inputId} 47 - className={checkboxVariants({ 48 - state: error ? "error" : "default", 49 - className, 50 - })} 51 - onChange={handleChange} 52 - {...props} 53 - /> 54 - <label 55 - htmlFor={inputId} 56 - className="ml-2 block text-sm font-medium text-ctp-text" 57 - > 58 - {label} 59 - </label> 60 - </div> 61 - {error && <p className="text-xs text-ctp-red">{error}</p>} 62 - </div> 63 - ); 64 - }
-59
apps/client/src/ui/IconButton.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority"; 2 - import type { ButtonHTMLAttributes, ReactNode } from "react"; 3 - 4 - const iconButtonVariants = cva( 5 - "inline-flex items-center justify-center rounded transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2", 6 - { 7 - variants: { 8 - variant: { 9 - primary: 10 - "bg-ctp-mauve text-ctp-base hover:bg-ctp-mauve/90 focus:ring-ctp-mauve", 11 - secondary: 12 - "bg-ctp-surface1 text-ctp-text hover:bg-ctp-surface2 focus:ring-ctp-surface1", 13 - ghost: "text-ctp-text hover:bg-ctp-surface1 focus:ring-ctp-surface1", 14 - outline: 15 - "border border-ctp-surface0 text-ctp-text hover:bg-ctp-surface0 focus:ring-ctp-surface0", 16 - destructive: 17 - "bg-ctp-red text-ctp-base hover:bg-ctp-red/90 focus:ring-ctp-red", 18 - }, 19 - size: { 20 - sm: "p-1.5", 21 - md: "p-2", 22 - lg: "p-3", 23 - }, 24 - }, 25 - defaultVariants: { 26 - variant: "ghost", 27 - size: "md", 28 - }, 29 - }, 30 - ); 31 - 32 - type IconButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & 33 - VariantProps<typeof iconButtonVariants> & { 34 - icon: ReactNode; 35 - label: string; 36 - }; 37 - 38 - /** 39 - * Icon-only button component for actions like edit, delete, etc. 40 - */ 41 - export default function IconButton({ 42 - icon, 43 - label, 44 - className = "", 45 - variant, 46 - size, 47 - ...props 48 - }: IconButtonProps) { 49 - return ( 50 - <button 51 - className={iconButtonVariants({ variant, size, className })} 52 - title={label} 53 - aria-label={label} 54 - {...props} 55 - > 56 - {icon} 57 - </button> 58 - ); 59 - }
-86
apps/client/src/ui/Select.tsx
··· 1 - import { forwardRef } from "react"; 2 - 3 - interface SelectOption { 4 - value: string; 5 - label: string; 6 - } 7 - 8 - interface SelectProps { 9 - id?: string; 10 - label?: string; 11 - options: SelectOption[]; 12 - value: string; 13 - onChange: (value: string) => void; 14 - placeholder?: string; 15 - required?: boolean; 16 - disabled?: boolean; 17 - className?: string; 18 - error?: string | undefined; 19 - } 20 - 21 - export const Select = forwardRef<HTMLSelectElement, SelectProps>( 22 - ( 23 - { 24 - id, 25 - label, 26 - options, 27 - value, 28 - onChange, 29 - placeholder, 30 - required = false, 31 - disabled = false, 32 - className = "", 33 - error, 34 - }, 35 - ref, 36 - ) => { 37 - const inputId = id || `select-${Math.random().toString(36).substr(2, 9)}`; 38 - 39 - return ( 40 - <div className={`space-y-2 ${className}`}> 41 - {label && ( 42 - <label 43 - htmlFor={inputId} 44 - className="block text-sm font-medium text-ctp-text" 45 - > 46 - {label} 47 - {required && <span className="text-ctp-red ml-1">*</span>} 48 - </label> 49 - )} 50 - 51 - <select 52 - ref={ref} 53 - id={inputId} 54 - value={value} 55 - onChange={(e) => onChange(e.target.value)} 56 - disabled={disabled} 57 - className={` 58 - w-full px-3 py-2 border rounded-md shadow-sm bg-ctp-base text-ctp-text 59 - focus:outline-none focus:ring-2 focus:ring-ctp-blue focus:border-transparent 60 - disabled:bg-ctp-surface1 disabled:text-ctp-subtext1 disabled:cursor-not-allowed 61 - ${ 62 - error 63 - ? "border-ctp-red focus:ring-ctp-red" 64 - : "border-ctp-surface1 focus:border-ctp-blue" 65 - } 66 - `} 67 - > 68 - {placeholder && ( 69 - <option value="" disabled> 70 - {placeholder} 71 - </option> 72 - )} 73 - {options.map((option) => ( 74 - <option key={option.value} value={option.value}> 75 - {option.label} 76 - </option> 77 - ))} 78 - </select> 79 - 80 - {error && <p className="text-sm text-ctp-red">{error}</p>} 81 - </div> 82 - ); 83 - }, 84 - ); 85 - 86 - Select.displayName = "Select";
-35
apps/client/src/ui/StatusBadge.tsx
··· 1 - import { cva } from "class-variance-authority"; 2 - 3 - const statusBadgeVariants = cva("px-2 py-1 text-xs rounded-full font-medium", { 4 - variants: { 5 - status: { 6 - active: "bg-ctp-green text-white", 7 - inactive: "bg-ctp-red text-white", 8 - }, 9 - }, 10 - defaultVariants: { 11 - status: "inactive", 12 - }, 13 - }); 14 - 15 - interface StatusBadgeProps { 16 - isActive: boolean; 17 - activeLabel?: string; 18 - inactiveLabel?: string; 19 - className?: string; 20 - } 21 - 22 - export const StatusBadge = ({ 23 - isActive, 24 - activeLabel = "Active", 25 - inactiveLabel = "Inactive", 26 - className = "", 27 - }: StatusBadgeProps) => { 28 - return ( 29 - <span 30 - className={`${statusBadgeVariants({ status: isActive ? "active" : "inactive" })} ${className}`} 31 - > 32 - {isActive ? activeLabel : inactiveLabel} 33 - </span> 34 - ); 35 - };
-103
apps/client/src/ui/Table.tsx
··· 1 - import type { ReactNode } from "react"; 2 - 3 - interface TableProps { 4 - children: ReactNode; 5 - className?: string; 6 - } 7 - 8 - interface TableHeaderProps { 9 - children: ReactNode; 10 - className?: string; 11 - } 12 - 13 - interface TableBodyProps { 14 - children: ReactNode; 15 - className?: string; 16 - } 17 - 18 - interface TableRowProps { 19 - children: ReactNode; 20 - className?: string; 21 - onClick?: () => void; 22 - } 23 - 24 - interface TableCellProps { 25 - children: ReactNode; 26 - className?: string; 27 - colSpan?: number; 28 - } 29 - 30 - interface TableHeaderCellProps { 31 - children: ReactNode; 32 - className?: string; 33 - colSpan?: number; 34 - } 35 - 36 - export const Table = ({ children, className = "" }: TableProps) => { 37 - return ( 38 - <div 39 - className={`overflow-hidden rounded-lg border border-ctp-surface1 ${className}`} 40 - > 41 - <table className="min-w-full divide-y divide-ctp-surface1"> 42 - {children} 43 - </table> 44 - </div> 45 - ); 46 - }; 47 - 48 - export const TableHeader = ({ children, className = "" }: TableHeaderProps) => { 49 - return <thead className={`bg-ctp-surface1 ${className}`}>{children}</thead>; 50 - }; 51 - 52 - export const TableBody = ({ children, className = "" }: TableBodyProps) => { 53 - return ( 54 - <tbody className={`divide-y divide-ctp-surface1 ${className}`}> 55 - {children} 56 - </tbody> 57 - ); 58 - }; 59 - 60 - export const TableRow = ({ 61 - children, 62 - className = "", 63 - onClick, 64 - }: TableRowProps) => { 65 - return ( 66 - <tr 67 - className={`${onClick ? "cursor-pointer hover:bg-ctp-surface1/50" : ""} ${className}`} 68 - onClick={onClick} 69 - > 70 - {children} 71 - </tr> 72 - ); 73 - }; 74 - 75 - export const TableHeaderCell = ({ 76 - children, 77 - className = "", 78 - colSpan, 79 - }: TableHeaderCellProps) => { 80 - return ( 81 - <th 82 - className={`px-6 py-4 text-left text-xs font-medium text-ctp-subtext0 uppercase tracking-wider ${className}`} 83 - colSpan={colSpan} 84 - > 85 - {children} 86 - </th> 87 - ); 88 - }; 89 - 90 - export const TableCell = ({ 91 - children, 92 - className = "", 93 - colSpan, 94 - }: TableCellProps) => { 95 - return ( 96 - <td 97 - className={`px-6 py-4 text-sm text-ctp-text ${className}`} 98 - colSpan={colSpan} 99 - > 100 - {children} 101 - </td> 102 - ); 103 - };
-70
apps/client/src/ui/TextInput.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority"; 2 - import type { InputHTMLAttributes } from "react"; 3 - 4 - const textInputVariants = cva( 5 - "w-full rounded bg-ctp-surface0 p-2 text-ctp-text placeholder-ctp-subtext0 outline-none transition-colors focus:ring-2 focus:ring-ctp-mauve focus:ring-offset-1", 6 - { 7 - variants: { 8 - size: { 9 - sm: "px-2 py-1 text-xs", 10 - md: "px-3 py-2 text-sm", 11 - lg: "px-4 py-3 text-base", 12 - }, 13 - state: { 14 - default: "", 15 - error: "border border-ctp-red focus:ring-ctp-red", 16 - success: "border border-ctp-green focus:ring-ctp-green", 17 - }, 18 - }, 19 - defaultVariants: { 20 - size: "md", 21 - state: "default", 22 - }, 23 - }, 24 - ); 25 - 26 - type TextInputProps = { 27 - label: string; 28 - error?: string | undefined; 29 - onChange?: (value: string) => void; 30 - } & Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> & 31 - VariantProps<typeof textInputVariants>; 32 - 33 - export default function TextInput({ 34 - label, 35 - className = "", 36 - size, 37 - state, 38 - error, 39 - onChange, 40 - id, 41 - ...props 42 - }: TextInputProps) { 43 - const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { 44 - onChange?.(e.target.value); 45 - }; 46 - 47 - const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`; 48 - 49 - return ( 50 - <div className="space-y-1"> 51 - <label 52 - htmlFor={inputId} 53 - className="block text-sm font-medium text-ctp-text" 54 - > 55 - {label} 56 - </label> 57 - <input 58 - id={inputId} 59 - className={textInputVariants({ 60 - size, 61 - state: error ? "error" : state, 62 - className, 63 - })} 64 - onChange={handleChange} 65 - {...props} 66 - /> 67 - {error && <p className="text-xs text-ctp-red">{error}</p>} 68 - </div> 69 - ); 70 - }
-70
apps/client/src/ui/Textarea.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority"; 2 - import type { TextareaHTMLAttributes } from "react"; 3 - 4 - const textareaVariants = cva( 5 - "w-full rounded bg-ctp-surface0 border border-ctp-surface1 p-2 text-ctp-text placeholder-ctp-subtext0 outline-none transition-colors focus:ring-2 focus:ring-ctp-mauve focus:ring-offset-1 focus:border-transparent", 6 - { 7 - variants: { 8 - size: { 9 - sm: "px-2 py-1 text-xs", 10 - md: "px-3 py-2 text-sm", 11 - lg: "px-4 py-3 text-base", 12 - }, 13 - state: { 14 - default: "", 15 - error: "border-ctp-red focus:ring-ctp-red", 16 - success: "border-ctp-green focus:ring-ctp-green", 17 - }, 18 - }, 19 - defaultVariants: { 20 - size: "md", 21 - state: "default", 22 - }, 23 - }, 24 - ); 25 - 26 - type TextareaProps = { 27 - label: string; 28 - error?: string; 29 - onChange?: (value: string) => void; 30 - } & Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange"> & 31 - VariantProps<typeof textareaVariants>; 32 - 33 - export default function Textarea({ 34 - label, 35 - className = "", 36 - size, 37 - state, 38 - error, 39 - onChange, 40 - id, 41 - ...props 42 - }: TextareaProps) { 43 - const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { 44 - onChange?.(e.target.value); 45 - }; 46 - 47 - const inputId = id || `textarea-${Math.random().toString(36).substr(2, 9)}`; 48 - 49 - return ( 50 - <div className="space-y-1"> 51 - <label 52 - htmlFor={inputId} 53 - className="block text-sm font-medium text-ctp-text" 54 - > 55 - {label} 56 - </label> 57 - <textarea 58 - id={inputId} 59 - className={textareaVariants({ 60 - size, 61 - state: error ? "error" : state, 62 - className, 63 - })} 64 - onChange={handleChange} 65 - {...props} 66 - /> 67 - {error && <p className="text-xs text-ctp-red">{error}</p>} 68 - </div> 69 - ); 70 - }
-9
apps/client/src/ui/index.ts
··· 1 - // UI Components 2 - export { default as Button } from "./Button"; 3 - export { default as Checkbox } from "./Checkbox"; 4 - export { default as IconButton } from "./IconButton"; 5 - export { Select } from "./Select"; 6 - export { StatusBadge } from "./StatusBadge"; 7 - export * from "./Table"; 8 - export { default as Textarea } from "./Textarea"; 9 - export { default as TextInput } from "./TextInput";
-7
apps/client/src/utils/auth.ts
··· 1 - import type { AuthResponse } from "@/types/auth"; 2 - 3 - export function handleAuthSuccess(user: AuthResponse["user"]) { 4 - // Token storage is now handled by TokenProvider 5 - // Apollo Client will handle caching automatically 6 - console.log("User authenticated:", user); 7 - }
+1 -1
apps/client/tsconfig.tsbuildinfo
··· 1 - {"root":["./src/App.tsx","./src/main.tsx","./src/components/ConfirmationModal.tsx","./src/components/ErrorBoundary.tsx","./src/components/Navbar.tsx","./src/components/ServerStatusIndicator.tsx","./src/components/Toast.tsx","./src/components/ToastContainer.tsx","./src/components/navLinks.ts","./src/components/ServerStatusIndicator/ServerTooltip.tsx","./src/components/ServerStatusIndicator/StatusDot.tsx","./src/components/ServerStatusIndicator/constants.ts","./src/components/ServerStatusIndicator/index.tsx","./src/components/ServerStatusIndicator/types.ts","./src/components/ServerStatusIndicator/utils.ts","./src/components/icons/CloseIcon.tsx","./src/components/icons/DeleteIcon.tsx","./src/components/icons/DocumentIcon.tsx","./src/components/icons/EditIcon.tsx","./src/components/icons/ErrorIcon.tsx","./src/components/icons/LinkIcon.tsx","./src/components/icons/LoadingIcon.tsx","./src/components/icons/ToastIcon.tsx","./src/components/icons/UploadIcon.tsx","./src/components/icons/index.ts","./src/constants/auth.ts","./src/contexts/ConfirmationModalContext.tsx","./src/contexts/ToastContext.tsx","./src/features/auth/components/AuthForm.tsx","./src/features/auth/components/LoginForm.tsx","./src/features/auth/components/RegisterForm.tsx","./src/features/auth/components/index.ts","./src/features/job-experience/components/JobExperienceCard.tsx","./src/features/job-experience/components/JobExperienceCreationSelector.tsx","./src/features/job-experience/components/JobExperienceEmpty.tsx","./src/features/job-experience/components/JobExperienceForm.tsx","./src/features/job-experience/components/JobExperienceHeader.tsx","./src/features/job-experience/components/JobExperienceList.tsx","./src/features/job-experience/components/JobExperienceLoading.tsx","./src/features/job-experience/components/JobExperienceTable.tsx","./src/features/job-experience/components/index.ts","./src/features/organizations/components/MembersTableBody.tsx","./src/features/organizations/components/MembersTableHeader.tsx","./src/features/organizations/components/OrganizationMemberRow.tsx","./src/features/organizations/components/OrganizationMembersTable.tsx","./src/features/organizations/components/index.ts","./src/features/vacancies/components/VacancyCard.tsx","./src/features/vacancies/components/VacancyForm.tsx","./src/features/vacancies/components/VacancyList.tsx","./src/features/vacancies/components/index.ts","./src/features/vacancies/components/VacancyCreationSelector/CreationMethodCard.tsx","./src/features/vacancies/components/VacancyCreationSelector/PlaceholderForm.tsx","./src/features/vacancies/components/VacancyCreationSelector/VacancyCreationSelector.tsx","./src/features/vacancies/components/VacancyCreationSelector/constants.ts","./src/features/vacancies/components/VacancyCreationSelector/index.ts","./src/features/vacancies/components/VacancyCreationSelector/types.ts","./src/features/vacancies/components/VacancyCreationSelector/variants.ts","./src/generated/graphql.ts","./src/hooks/useAuth.ts","./src/hooks/useServerHealth.ts","./src/layouts/AuthenticatedLayout.tsx","./src/lib/apollo-client.ts","./src/lib/config.ts","./src/pages/CreateJobExperiencePage.tsx","./src/pages/CreateVacancyPage.tsx","./src/pages/DashboardPage.tsx","./src/pages/JobExperiencePage.tsx","./src/pages/OrganizationsPage.tsx","./src/pages/ProfilePage.tsx","./src/pages/VacanciesPage.tsx","./src/providers/TokenProvider.tsx","./src/router/AppRouter.tsx","./src/types/auth.ts","./src/types/graphql.d.ts","./src/ui/Badge.tsx","./src/ui/Button.tsx","./src/ui/Checkbox.tsx","./src/ui/IconButton.tsx","./src/ui/Select.tsx","./src/ui/StatusBadge.tsx","./src/ui/Table.tsx","./src/ui/TextInput.tsx","./src/ui/Textarea.tsx","./src/ui/index.ts","./src/utils/auth.ts","./src/utils/dateUtils.ts","./src/validation/schemas.ts"],"version":"5.9.3"} 1 + {"root":["./src/app.tsx","./src/main.tsx","./src/components/confirmationmodal.tsx","./src/components/errorboundary.tsx","./src/components/navbar.tsx","./src/components/profileimage.tsx","./src/components/serverstatusindicator.tsx","./src/components/toast.tsx","./src/components/toastcontainer.tsx","./src/components/tooltip.tsx","./src/components/userprofiledrawer.tsx","./src/components/navlinks.ts","./src/components/serverstatusindicator/servertooltip.tsx","./src/components/serverstatusindicator/statusdot.tsx","./src/components/serverstatusindicator/constants.ts","./src/components/serverstatusindicator/index.tsx","./src/components/serverstatusindicator/types.ts","./src/components/serverstatusindicator/utils.ts","./src/constants/auth.ts","./src/contexts/confirmationmodalcontext.tsx","./src/contexts/toastcontext.tsx","./src/contexts/tokenprovider.tsx","./src/features/applications/components/applicationflow.tsx","./src/features/applications/components/applicationsoverview.tsx","./src/features/applications/components/cvtemplateselector.tsx","./src/features/applications/components/matchindicator.tsx","./src/features/applications/components/matchtooltip.tsx","./src/features/applications/components/vacancycard.tsx","./src/features/applications/components/vacancyfilterpanel.tsx","./src/features/applications/components/vacancyselector.tsx","./src/features/applications/components/usevacancyfilters.ts","./src/features/auth/components/authform.tsx","./src/features/auth/components/loginform.tsx","./src/features/auth/components/registerform.tsx","./src/features/auth/components/index.ts","./src/features/job-experience/components/companyselect.tsx","./src/features/job-experience/components/jobexperiencecard.tsx","./src/features/job-experience/components/jobexperiencecreationselector.tsx","./src/features/job-experience/components/jobexperienceempty.tsx","./src/features/job-experience/components/jobexperienceform.tsx","./src/features/job-experience/components/jobexperienceheader.tsx","./src/features/job-experience/components/jobexperiencelist.tsx","./src/features/job-experience/components/jobexperienceloading.tsx","./src/features/job-experience/components/jobexperiencetable.tsx","./src/features/job-experience/components/levelselect.tsx","./src/features/job-experience/components/roleselect.tsx","./src/features/job-experience/components/selectedskillsdisplay.tsx","./src/features/job-experience/components/skillsselect.tsx","./src/features/job-experience/components/index.ts","./src/features/job-experience/components/jobexperience.schema.ts","./src/features/job-experience/components/inputs/baseinfiniteselect.tsx","./src/features/job-experience/components/inputs/companyselect.tsx","./src/features/job-experience/components/inputs/useinfiniteconnectionquery.ts","./src/features/organizations/components/collapsibleorganizationtable.tsx","./src/features/organizations/components/memberstablebody.tsx","./src/features/organizations/components/memberstableheader.tsx","./src/features/organizations/components/organizationmemberrow.tsx","./src/features/organizations/components/organizationmemberstable.tsx","./src/features/organizations/components/index.ts","./src/features/vacancies/components/vacancycard.tsx","./src/features/vacancies/components/vacancyform.tsx","./src/features/vacancies/components/vacancylist.tsx","./src/features/vacancies/components/index.ts","./src/features/vacancies/components/vacancy.schema.ts","./src/features/vacancies/components/vacancycreationselector/creationmethodcard.tsx","./src/features/vacancies/components/vacancycreationselector/placeholderform.tsx","./src/features/vacancies/components/vacancycreationselector/vacancycreationselector.tsx","./src/features/vacancies/components/vacancycreationselector/constants.ts","./src/features/vacancies/components/vacancycreationselector/index.ts","./src/features/vacancies/components/vacancycreationselector/types.ts","./src/features/vacancies/components/vacancycreationselector/variants.ts","./src/generated/graphql.ts","./src/hooks/useauth.ts","./src/hooks/useserverhealth.ts","./src/layouts/authenticatedlayout.tsx","./src/pages/applicationflowpage.tsx","./src/pages/applicationspage.tsx","./src/pages/cvviewpage.tsx","./src/pages/cvspage.tsx","./src/pages/createcvpage.tsx","./src/pages/createjobexperiencepage.tsx","./src/pages/createvacancypage.tsx","./src/pages/dashboardpage.tsx","./src/pages/educationpage.tsx","./src/pages/jobexperiencepage.tsx","./src/pages/organizationspage.tsx","./src/pages/profilepage.tsx","./src/pages/vacanciespage.tsx","./src/router/approuter.tsx","./src/types/auth.ts","./src/types/graphql.d.ts","./src/utils/auth.ts","./src/utils/cn.ts","./src/utils/config.ts","./src/utils/dateutils.ts","./src/utils/graphql-fetcher.ts","./src/utils/skillsmatcher.ts","./src/utils/userutils.ts"],"errors":true,"version":"5.9.3"}
-69
apps/server/src/modules/job-experience/company/company.resolver.ts
··· 1 - import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; 2 - import { CompanyService } from "./company.service"; 3 - import { Company } from "./company.type"; 4 - 5 - @Resolver(() => Company) 6 - export class CompanyResolver { 7 - constructor(private readonly companyService: CompanyService) {} 8 - 9 - @Query(() => [Company]) 10 - async companies(): Promise<Company[]> { 11 - const domainCompanies = await this.companyService.findAll(); 12 - return domainCompanies.map((company) => Company.fromDomain(company)); 13 - } 14 - 15 - @Query(() => Company) 16 - async company(@Args("id") id: string): Promise<Company> { 17 - const domainCompany = await this.companyService.findByIdOrFail(id); 18 - return Company.fromDomain(domainCompany); 19 - } 20 - 21 - @Mutation(() => Company) 22 - async createCompany( 23 - @Args("name") name: string, 24 - @Args("description", { nullable: true }) description?: string, 25 - @Args("website", { nullable: true }) website?: string, 26 - ): Promise<Company> { 27 - const createData: { name: string; description?: string; website?: string } = 28 - { name }; 29 - if (description !== undefined) { 30 - createData.description = description; 31 - } 32 - if (website !== undefined) { 33 - createData.website = website; 34 - } 35 - const domainCompany = await this.companyService.create(createData); 36 - return Company.fromDomain(domainCompany); 37 - } 38 - 39 - @Mutation(() => Company) 40 - async updateCompany( 41 - @Args("id") id: string, 42 - @Args("name", { nullable: true }) name?: string, 43 - @Args("description", { nullable: true }) description?: string, 44 - @Args("website", { nullable: true }) website?: string, 45 - ): Promise<Company> { 46 - const updateData: { 47 - name?: string; 48 - description?: string; 49 - website?: string; 50 - } = {}; 51 - if (name !== undefined) { 52 - updateData.name = name; 53 - } 54 - if (description !== undefined) { 55 - updateData.description = description; 56 - } 57 - if (website !== undefined) { 58 - updateData.website = website; 59 - } 60 - const domainCompany = await this.companyService.update(id, updateData); 61 - return Company.fromDomain(domainCompany); 62 - } 63 - 64 - @Mutation(() => Boolean) 65 - async deleteCompany(@Args("id") id: string): Promise<boolean> { 66 - await this.companyService.delete(id); 67 - return true; 68 - } 69 - }
-54
apps/server/src/modules/job-experience/company/company.type.ts
··· 1 - import { Field, ID, ObjectType } from "@nestjs/graphql"; 2 - import type { Company as DomainCompany } from "./company.entity"; 3 - 4 - @ObjectType() 5 - export class Company { 6 - @Field(() => ID) 7 - id: string; 8 - 9 - @Field() 10 - name: string; 11 - 12 - @Field({ nullable: true }) 13 - description?: string; 14 - 15 - @Field({ nullable: true }) 16 - website?: string; 17 - 18 - @Field() 19 - createdAt: Date; 20 - 21 - @Field() 22 - updatedAt: Date; 23 - 24 - constructor( 25 - id: string, 26 - name: string, 27 - createdAt: Date, 28 - updatedAt: Date, 29 - description?: string, 30 - website?: string, 31 - ) { 32 - this.id = id; 33 - this.name = name; 34 - this.createdAt = createdAt; 35 - this.updatedAt = updatedAt; 36 - if (description !== undefined) { 37 - this.description = description; 38 - } 39 - if (website !== undefined) { 40 - this.website = website; 41 - } 42 - } 43 - 44 - static fromDomain(domainCompany: DomainCompany): Company { 45 - return new Company( 46 - domainCompany.id, 47 - domainCompany.name, 48 - domainCompany.createdAt, 49 - domainCompany.updatedAt, 50 - domainCompany.description, 51 - domainCompany.website, 52 - ); 53 - } 54 - }
-158
apps/server/src/modules/job-experience/employment/employment.resolver.ts
··· 1 - import { UseGuards } from "@nestjs/common"; 2 - import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; 3 - import { CurrentUser } from "../../auth/current-user.decorator"; 4 - import { JwtAuthGuard } from "../../auth/jwt-auth.guard"; 5 - import type { User } from "../../auth/user.type"; 6 - import { CompanyService } from "../company/company.service"; 7 - import { LevelService } from "../level/level.service"; 8 - import { RoleService } from "../role/role.service"; 9 - import { SkillService } from "../skill/skill.service"; 10 - import type { 11 - CreateUserJobExperienceDto, 12 - UpdateUserJobExperienceDto, 13 - } from "./user-job-experience.dto"; 14 - import { UserJobExperienceService } from "./user-job-experience.service"; 15 - import { UserJobExperience } from "./user-job-experience.type"; 16 - 17 - @Resolver(() => UserJobExperience) 18 - @UseGuards(JwtAuthGuard) 19 - export class EmploymentResolver { 20 - constructor( 21 - private readonly userJobExperienceService: UserJobExperienceService, 22 - private readonly companyService: CompanyService, 23 - private readonly roleService: RoleService, 24 - private readonly levelService: LevelService, 25 - private readonly skillService: SkillService, 26 - ) {} 27 - 28 - @Query(() => [UserJobExperience]) 29 - async myEmploymentHistory( 30 - @CurrentUser() user: User, 31 - ): Promise<UserJobExperience[]> { 32 - const domainExperiences = 33 - await this.userJobExperienceService.findForUser(user); 34 - return domainExperiences.map((exp) => UserJobExperience.fromDomain(exp)); 35 - } 36 - 37 - @Mutation(() => UserJobExperience) 38 - async createJobExperience( 39 - @CurrentUser() user: User, 40 - @Args("companyId") companyId: string, 41 - @Args("roleId") roleId: string, 42 - @Args("levelId") levelId: string, 43 - @Args("startDate") startDate: Date, 44 - @Args("endDate", { nullable: true }) endDate?: Date, 45 - @Args("description", { nullable: true }) description?: string, 46 - @Args("skillIds", { type: () => [String], nullable: true }) 47 - skillIds?: string[], 48 - ): Promise<UserJobExperience> { 49 - // Fetch full entities 50 - const company = await this.companyService.findByIdOrFail(companyId); 51 - const role = await this.roleService.findByIdOrFail(roleId); 52 - const level = await this.levelService.findByIdOrFail(levelId); 53 - const skills = skillIds 54 - ? await Promise.all( 55 - skillIds.map((id) => this.skillService.findByIdOrFail(id)), 56 - ) 57 - : undefined; 58 - 59 - const createData: CreateUserJobExperienceDto = { 60 - user, 61 - company, 62 - role, 63 - level, 64 - startDate, 65 - }; 66 - 67 - if (endDate !== undefined) { 68 - createData.endDate = endDate; 69 - } 70 - if (description !== undefined) { 71 - createData.description = description; 72 - } 73 - if (skills !== undefined) { 74 - createData.skills = skills; 75 - } 76 - 77 - const domainExperience = 78 - await this.userJobExperienceService.create(createData); 79 - 80 - return UserJobExperience.fromDomain(domainExperience); 81 - } 82 - 83 - @Mutation(() => UserJobExperience) 84 - async updateJobExperience( 85 - @Args("id") id: string, 86 - @Args("companyId", { nullable: true }) companyId?: string, 87 - @Args("roleId", { nullable: true }) roleId?: string, 88 - @Args("levelId", { nullable: true }) levelId?: string, 89 - @Args("startDate", { nullable: true }) startDate?: Date, 90 - @Args("endDate", { nullable: true }) endDate?: Date, 91 - @Args("description", { nullable: true }) description?: string, 92 - @Args("skillIds", { type: () => [String], nullable: true }) 93 - skillIds?: string[], 94 - ): Promise<UserJobExperience> { 95 - const updateData: UpdateUserJobExperienceDto = {}; 96 - 97 - if (companyId !== undefined) { 98 - updateData.company = await this.companyService.findByIdOrFail(companyId); 99 - } 100 - if (roleId !== undefined) { 101 - updateData.role = await this.roleService.findByIdOrFail(roleId); 102 - } 103 - if (levelId !== undefined) { 104 - updateData.level = await this.levelService.findByIdOrFail(levelId); 105 - } 106 - if (startDate !== undefined) { 107 - updateData.startDate = startDate; 108 - } 109 - if (endDate !== undefined) { 110 - updateData.endDate = endDate; 111 - } 112 - if (description !== undefined) { 113 - updateData.description = description; 114 - } 115 - if (skillIds !== undefined) { 116 - updateData.skills = await Promise.all( 117 - skillIds.map((id) => this.skillService.findByIdOrFail(id)), 118 - ); 119 - } 120 - 121 - const domainExperience = await this.userJobExperienceService.update( 122 - id, 123 - updateData, 124 - ); 125 - 126 - return UserJobExperience.fromDomain(domainExperience); 127 - } 128 - 129 - @Mutation(() => Boolean) 130 - async deleteJobExperience(@Args("id") id: string): Promise<boolean> { 131 - await this.userJobExperienceService.delete(id); 132 - return true; 133 - } 134 - 135 - @Mutation(() => UserJobExperience) 136 - async addSkillsToJobExperience( 137 - @Args("experienceId") experienceId: string, 138 - @Args("skillIds", { type: () => [String] }) skillIds: string[], 139 - ): Promise<UserJobExperience> { 140 - const domainExperience = await this.userJobExperienceService.addSkills( 141 - experienceId, 142 - skillIds, 143 - ); 144 - return UserJobExperience.fromDomain(domainExperience); 145 - } 146 - 147 - @Mutation(() => UserJobExperience) 148 - async removeSkillsFromJobExperience( 149 - @Args("experienceId") experienceId: string, 150 - @Args("skillIds", { type: () => [String] }) skillIds: string[], 151 - ): Promise<UserJobExperience> { 152 - const domainExperience = await this.userJobExperienceService.removeSkills( 153 - experienceId, 154 - skillIds, 155 - ); 156 - return UserJobExperience.fromDomain(domainExperience); 157 - } 158 - }
-18
apps/server/src/modules/job-experience/employment/user-field.resolver.ts
··· 1 - import { Parent, ResolveField, Resolver } from "@nestjs/graphql"; 2 - import { User } from "../../auth/user.type"; 3 - import { UserJobExperienceService } from "./user-job-experience.service"; 4 - import { UserJobExperience } from "./user-job-experience.type"; 5 - 6 - @Resolver(() => User) 7 - export class UserFieldResolver { 8 - constructor( 9 - private readonly userJobExperienceService: UserJobExperienceService, 10 - ) {} 11 - 12 - @ResolveField(() => [UserJobExperience], { nullable: true }) 13 - async experience(@Parent() user: User): Promise<UserJobExperience[]> { 14 - const domainExperiences = 15 - await this.userJobExperienceService.findForUser(user); 16 - return domainExperiences.map((exp) => UserJobExperience.fromDomain(exp)); 17 - } 18 - }
-86
apps/server/src/modules/job-experience/employment/user-job-experience.type.ts
··· 1 - import { Field, ID, ObjectType } from "@nestjs/graphql"; 2 - import { Company } from "../company/company.type"; 3 - import { Level } from "../level/level.type"; 4 - import { Role } from "../role/role.type"; 5 - import { Skill } from "../skill/skill.type"; 6 - import type { UserJobExperience as DomainUserJobExperience } from "./user-job-experience.entity"; 7 - 8 - @ObjectType() 9 - export class UserJobExperience { 10 - @Field(() => ID) 11 - id: string; 12 - 13 - @Field(() => Company) 14 - company: Company; 15 - 16 - @Field(() => Role) 17 - role: Role; 18 - 19 - @Field(() => Level) 20 - level: Level; 21 - 22 - @Field(() => [Skill]) 23 - skills: Skill[]; 24 - 25 - @Field() 26 - startDate: Date; 27 - 28 - @Field({ nullable: true }) 29 - endDate?: Date; 30 - 31 - @Field({ nullable: true }) 32 - description?: string; 33 - 34 - @Field() 35 - createdAt: Date; 36 - 37 - @Field() 38 - updatedAt: Date; 39 - 40 - constructor( 41 - id: string, 42 - company: Company, 43 - role: Role, 44 - level: Level, 45 - skills: Skill[], 46 - startDate: Date, 47 - createdAt: Date, 48 - updatedAt: Date, 49 - endDate?: Date, 50 - description?: string, 51 - ) { 52 - this.id = id; 53 - this.company = company; 54 - this.role = role; 55 - this.level = level; 56 - this.skills = skills; 57 - this.startDate = startDate; 58 - this.createdAt = createdAt; 59 - this.updatedAt = updatedAt; 60 - if (endDate !== undefined) { 61 - this.endDate = endDate; 62 - } 63 - if (description !== undefined) { 64 - this.description = description; 65 - } 66 - } 67 - 68 - static fromDomain( 69 - domainExperience: DomainUserJobExperience, 70 - ): UserJobExperience { 71 - return new UserJobExperience( 72 - domainExperience.id, 73 - Company.fromDomain(domainExperience.company), 74 - Role.fromDomain(domainExperience.role), 75 - Level.fromDomain(domainExperience.level), 76 - domainExperience.skills 77 - ? domainExperience.skills.map((skill) => Skill.fromDomain(skill)) 78 - : [], 79 - domainExperience.startDate, 80 - domainExperience.createdAt, 81 - domainExperience.updatedAt, 82 - domainExperience.endDate, 83 - domainExperience.description, 84 - ); 85 - } 86 - }
-56
apps/server/src/modules/job-experience/level/level.resolver.ts
··· 1 - import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; 2 - import { LevelService } from "./level.service"; 3 - import { Level } from "./level.type"; 4 - 5 - @Resolver(() => Level) 6 - export class LevelResolver { 7 - constructor(private readonly levelService: LevelService) {} 8 - 9 - @Query(() => [Level]) 10 - async levels(): Promise<Level[]> { 11 - const domainLevels = await this.levelService.findAll(); 12 - return domainLevels.map((level) => Level.fromDomain(level)); 13 - } 14 - 15 - @Query(() => Level) 16 - async level(@Args("id") id: string): Promise<Level> { 17 - const domainLevel = await this.levelService.findByIdOrFail(id); 18 - return Level.fromDomain(domainLevel); 19 - } 20 - 21 - @Mutation(() => Level) 22 - async createLevel( 23 - @Args("name") name: string, 24 - @Args("description", { nullable: true }) description?: string, 25 - ): Promise<Level> { 26 - const createData: { name: string; description?: string } = { name }; 27 - if (description !== undefined) { 28 - createData.description = description; 29 - } 30 - const domainLevel = await this.levelService.create(createData); 31 - return Level.fromDomain(domainLevel); 32 - } 33 - 34 - @Mutation(() => Level) 35 - async updateLevel( 36 - @Args("id") id: string, 37 - @Args("name", { nullable: true }) name?: string, 38 - @Args("description", { nullable: true }) description?: string, 39 - ): Promise<Level> { 40 - const updateData: { name?: string; description?: string } = {}; 41 - if (name !== undefined) { 42 - updateData.name = name; 43 - } 44 - if (description !== undefined) { 45 - updateData.description = description; 46 - } 47 - const domainLevel = await this.levelService.update(id, updateData); 48 - return Level.fromDomain(domainLevel); 49 - } 50 - 51 - @Mutation(() => Boolean) 52 - async deleteLevel(@Args("id") id: string): Promise<boolean> { 53 - await this.levelService.delete(id); 54 - return true; 55 - } 56 - }
-46
apps/server/src/modules/job-experience/level/level.type.ts
··· 1 - import { Field, ID, ObjectType } from "@nestjs/graphql"; 2 - import type { Level as DomainLevel } from "./level.entity"; 3 - 4 - @ObjectType() 5 - export class Level { 6 - @Field(() => ID) 7 - id: string; 8 - 9 - @Field() 10 - name: string; 11 - 12 - @Field({ nullable: true }) 13 - description?: string; 14 - 15 - @Field() 16 - createdAt: Date; 17 - 18 - @Field() 19 - updatedAt: Date; 20 - 21 - constructor( 22 - id: string, 23 - name: string, 24 - createdAt: Date, 25 - updatedAt: Date, 26 - description?: string, 27 - ) { 28 - this.id = id; 29 - this.name = name; 30 - this.createdAt = createdAt; 31 - this.updatedAt = updatedAt; 32 - if (description !== undefined) { 33 - this.description = description; 34 - } 35 - } 36 - 37 - static fromDomain(domainLevel: DomainLevel): Level { 38 - return new Level( 39 - domainLevel.id, 40 - domainLevel.name, 41 - domainLevel.createdAt, 42 - domainLevel.updatedAt, 43 - domainLevel.description, 44 - ); 45 - } 46 - }
-56
apps/server/src/modules/job-experience/role/role.resolver.ts
··· 1 - import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; 2 - import { RoleService } from "./role.service"; 3 - import { Role } from "./role.type"; 4 - 5 - @Resolver(() => Role) 6 - export class RoleResolver { 7 - constructor(private readonly roleService: RoleService) {} 8 - 9 - @Query(() => [Role]) 10 - async roles(): Promise<Role[]> { 11 - const domainRoles = await this.roleService.findAll(); 12 - return domainRoles.map((role) => Role.fromDomain(role)); 13 - } 14 - 15 - @Query(() => Role) 16 - async role(@Args("id") id: string): Promise<Role> { 17 - const domainRole = await this.roleService.findByIdOrFail(id); 18 - return Role.fromDomain(domainRole); 19 - } 20 - 21 - @Mutation(() => Role) 22 - async createRole( 23 - @Args("name") name: string, 24 - @Args("description", { nullable: true }) description?: string, 25 - ): Promise<Role> { 26 - const createData: { name: string; description?: string } = { name }; 27 - if (description !== undefined) { 28 - createData.description = description; 29 - } 30 - const domainRole = await this.roleService.create(createData); 31 - return Role.fromDomain(domainRole); 32 - } 33 - 34 - @Mutation(() => Role) 35 - async updateRole( 36 - @Args("id") id: string, 37 - @Args("name", { nullable: true }) name?: string, 38 - @Args("description", { nullable: true }) description?: string, 39 - ): Promise<Role> { 40 - const updateData: { name?: string; description?: string } = {}; 41 - if (name !== undefined) { 42 - updateData.name = name; 43 - } 44 - if (description !== undefined) { 45 - updateData.description = description; 46 - } 47 - const domainRole = await this.roleService.update(id, updateData); 48 - return Role.fromDomain(domainRole); 49 - } 50 - 51 - @Mutation(() => Boolean) 52 - async deleteRole(@Args("id") id: string): Promise<boolean> { 53 - await this.roleService.delete(id); 54 - return true; 55 - } 56 - }
-46
apps/server/src/modules/job-experience/role/role.type.ts
··· 1 - import { Field, ID, ObjectType } from "@nestjs/graphql"; 2 - import type { Role as DomainRole } from "./role.entity"; 3 - 4 - @ObjectType() 5 - export class Role { 6 - @Field(() => ID) 7 - id: string; 8 - 9 - @Field() 10 - name: string; 11 - 12 - @Field({ nullable: true }) 13 - description?: string; 14 - 15 - @Field() 16 - createdAt: Date; 17 - 18 - @Field() 19 - updatedAt: Date; 20 - 21 - constructor( 22 - id: string, 23 - name: string, 24 - createdAt: Date, 25 - updatedAt: Date, 26 - description?: string, 27 - ) { 28 - this.id = id; 29 - this.name = name; 30 - this.createdAt = createdAt; 31 - this.updatedAt = updatedAt; 32 - if (description !== undefined) { 33 - this.description = description; 34 - } 35 - } 36 - 37 - static fromDomain(domainRole: DomainRole): Role { 38 - return new Role( 39 - domainRole.id, 40 - domainRole.name, 41 - domainRole.createdAt, 42 - domainRole.updatedAt, 43 - domainRole.description, 44 - ); 45 - } 46 - }
-56
apps/server/src/modules/job-experience/skill/skill.resolver.ts
··· 1 - import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; 2 - import { SkillService } from "./skill.service"; 3 - import { Skill } from "./skill.type"; 4 - 5 - @Resolver(() => Skill) 6 - export class SkillResolver { 7 - constructor(private readonly skillService: SkillService) {} 8 - 9 - @Query(() => [Skill]) 10 - async skills(): Promise<Skill[]> { 11 - const domainSkills = await this.skillService.findAll(); 12 - return domainSkills.map((skill) => Skill.fromDomain(skill)); 13 - } 14 - 15 - @Query(() => Skill) 16 - async skill(@Args("id") id: string): Promise<Skill> { 17 - const domainSkill = await this.skillService.findByIdOrFail(id); 18 - return Skill.fromDomain(domainSkill); 19 - } 20 - 21 - @Mutation(() => Skill) 22 - async createSkill( 23 - @Args("name") name: string, 24 - @Args("description", { nullable: true }) description?: string, 25 - ): Promise<Skill> { 26 - const createData: { name: string; description?: string } = { name }; 27 - if (description !== undefined) { 28 - createData.description = description; 29 - } 30 - const domainSkill = await this.skillService.create(createData); 31 - return Skill.fromDomain(domainSkill); 32 - } 33 - 34 - @Mutation(() => Skill) 35 - async updateSkill( 36 - @Args("id") id: string, 37 - @Args("name", { nullable: true }) name?: string, 38 - @Args("description", { nullable: true }) description?: string, 39 - ): Promise<Skill> { 40 - const updateData: { name?: string; description?: string } = {}; 41 - if (name !== undefined) { 42 - updateData.name = name; 43 - } 44 - if (description !== undefined) { 45 - updateData.description = description; 46 - } 47 - const domainSkill = await this.skillService.update(id, updateData); 48 - return Skill.fromDomain(domainSkill); 49 - } 50 - 51 - @Mutation(() => Boolean) 52 - async deleteSkill(@Args("id") id: string): Promise<boolean> { 53 - await this.skillService.delete(id); 54 - return true; 55 - } 56 - }
-46
apps/server/src/modules/job-experience/skill/skill.type.ts
··· 1 - import { Field, ID, ObjectType } from "@nestjs/graphql"; 2 - import type { Skill as DomainSkill } from "./skill.entity"; 3 - 4 - @ObjectType() 5 - export class Skill { 6 - @Field(() => ID) 7 - id: string; 8 - 9 - @Field() 10 - name: string; 11 - 12 - @Field({ nullable: true }) 13 - description?: string; 14 - 15 - @Field() 16 - createdAt: Date; 17 - 18 - @Field() 19 - updatedAt: Date; 20 - 21 - constructor( 22 - id: string, 23 - name: string, 24 - createdAt: Date, 25 - updatedAt: Date, 26 - description?: string, 27 - ) { 28 - this.id = id; 29 - this.name = name; 30 - this.createdAt = createdAt; 31 - this.updatedAt = updatedAt; 32 - if (description !== undefined) { 33 - this.description = description; 34 - } 35 - } 36 - 37 - static fromDomain(domainSkill: DomainSkill): Skill { 38 - return new Skill( 39 - domainSkill.id, 40 - domainSkill.name, 41 - domainSkill.createdAt, 42 - domainSkill.updatedAt, 43 - domainSkill.description, 44 - ); 45 - } 46 - }
-97
apps/server/src/modules/organization/organization.resolver.ts
··· 1 - import { UseGuards } from "@nestjs/common"; 2 - import { 3 - Args, 4 - Mutation, 5 - Parent, 6 - Query, 7 - ResolveField, 8 - Resolver, 9 - } from "@nestjs/graphql"; 10 - import { JwtAuthGuard } from "../auth/jwt-auth.guard"; 11 - import { Organization } from "./organization.entity"; 12 - import { OrganizationService } from "./organization.service"; 13 - import { OrganizationRole } from "./organization-role.entity"; 14 - import { UserOrganization } from "./user-organization.type"; 15 - 16 - @Resolver(() => Organization) 17 - @UseGuards(JwtAuthGuard) 18 - export class OrganizationResolver { 19 - constructor(private readonly organizationService: OrganizationService) {} 20 - 21 - @Query(() => Organization, { name: "organization", nullable: true }) 22 - async getOrganization(@Args("id") id: string): Promise<Organization | null> { 23 - return this.organizationService.findById(id); 24 - } 25 - 26 - @ResolveField(() => [UserOrganization]) 27 - async users( 28 - @Parent() organization: Organization, 29 - ): Promise<UserOrganization[]> { 30 - const userOrgs = await this.organizationService.findUsersByOrganizationId( 31 - organization.id, 32 - ); 33 - return userOrgs.map((uo) => 34 - UserOrganization.fromDomain({ 35 - id: uo.id, 36 - joinedAt: uo.joinedAt, 37 - user: uo.user, 38 - role: OrganizationRole.fromDomain(uo.role), 39 - }), 40 - ); 41 - } 42 - 43 - @Mutation(() => Organization) 44 - async createOrganization( 45 - @Args("name") name: string, 46 - @Args("description", { nullable: true }) description?: string, 47 - ): Promise<Organization> { 48 - const data: { name: string; description?: string } = { name }; 49 - if (description !== undefined) { 50 - data.description = description; 51 - } 52 - return this.organizationService.create(data); 53 - } 54 - 55 - @Mutation(() => Organization) 56 - async updateOrganization( 57 - @Args("id") id: string, 58 - @Args("name", { nullable: true }) name?: string, 59 - @Args("description", { nullable: true }) description?: string, 60 - ): Promise<Organization> { 61 - const data: { name?: string; description?: string } = {}; 62 - if (name !== undefined) { 63 - data.name = name; 64 - } 65 - if (description !== undefined) { 66 - data.description = description; 67 - } 68 - return this.organizationService.update(id, data); 69 - } 70 - 71 - @Mutation(() => Boolean) 72 - async deleteOrganization(@Args("id") id: string): Promise<boolean> { 73 - return this.organizationService.delete(id); 74 - } 75 - 76 - @Mutation(() => Boolean) 77 - async addUserToOrganization( 78 - @Args("organizationId") organizationId: string, 79 - @Args("userId") userId: string, 80 - ): Promise<boolean> { 81 - return this.organizationService.addUserToOrganization( 82 - organizationId, 83 - userId, 84 - ); 85 - } 86 - 87 - @Mutation(() => Boolean) 88 - async removeUserFromOrganization( 89 - @Args("organizationId") organizationId: string, 90 - @Args("userId") userId: string, 91 - ): Promise<boolean> { 92 - return this.organizationService.removeUserFromOrganization( 93 - organizationId, 94 - userId, 95 - ); 96 - } 97 - }
-14
apps/server/src/modules/organization/user-field.resolver.ts
··· 1 - import { Parent, ResolveField, Resolver } from "@nestjs/graphql"; 2 - import { User } from "../auth/user.type"; 3 - import { Organization } from "./organization.entity"; 4 - import { OrganizationService } from "./organization.service"; 5 - 6 - @Resolver(() => User) 7 - export class UserFieldResolver { 8 - constructor(private readonly organizationService: OrganizationService) {} 9 - 10 - @ResolveField(() => [Organization]) 11 - async organizations(@Parent() user: User): Promise<Organization[]> { 12 - return this.organizationService.findForUser(user.id); 13 - } 14 - }
-44
apps/server/src/modules/organization/user-organization.type.ts
··· 1 - import { Field, ObjectType } from "@nestjs/graphql"; 2 - import { User } from "../auth/user.type"; 3 - import { OrganizationRole } from "./organization-role.entity"; 4 - 5 - @ObjectType() 6 - export class UserOrganization { 7 - @Field(() => String) 8 - id: string; 9 - 10 - @Field(() => Date) 11 - joinedAt: Date; 12 - 13 - @Field(() => User) 14 - user: User; 15 - 16 - @Field(() => OrganizationRole) 17 - role: OrganizationRole; 18 - 19 - constructor(data: { 20 - id: string; 21 - joinedAt: Date; 22 - user: User; 23 - role: OrganizationRole; 24 - }) { 25 - this.id = data.id; 26 - this.joinedAt = data.joinedAt; 27 - this.user = data.user; 28 - this.role = data.role; 29 - } 30 - 31 - static fromDomain(domainUserOrg: { 32 - id: string; 33 - joinedAt: Date; 34 - user: User; 35 - role: OrganizationRole; 36 - }): UserOrganization { 37 - return new UserOrganization({ 38 - id: domainUserOrg.id, 39 - joinedAt: domainUserOrg.joinedAt, 40 - user: domainUserOrg.user, 41 - role: domainUserOrg.role, 42 - }); 43 - } 44 - }
-34
apps/server/src/modules/seed/organization-seed.service.ts
··· 1 - import { faker } from "@faker-js/faker"; 2 - import { Injectable } from "@nestjs/common"; 3 - import { PrismaService } from "../database/prisma.service"; 4 - 5 - @Injectable() 6 - export class OrganizationSeedService { 7 - // logger reserved for future diagnostics 8 - 9 - constructor(private readonly prisma: PrismaService) {} 10 - 11 - async ensureOrganizations(): Promise<Array<{ id: string }>> { 12 - const names = [ 13 - "TechCorp Solutions", 14 - "Innovation Labs", 15 - "Digital Dynamics", 16 - "Future Systems", 17 - ]; 18 - const created: Array<{ id: string }> = []; 19 - for (const name of names) { 20 - const existing = await this.prisma["organization"].findFirst({ 21 - where: { name }, 22 - }); 23 - if (existing) { 24 - created.push({ id: existing.id }); 25 - continue; 26 - } 27 - const org = await this.prisma["organization"].create({ 28 - data: { name, description: faker.company.catchPhrase() }, 29 - }); 30 - created.push({ id: org.id }); 31 - } 32 - return created; 33 - } 34 - }
-92
apps/server/src/modules/seed/reference-data-seed.service.ts
··· 1 - import { faker } from "@faker-js/faker"; 2 - import { Injectable } from "@nestjs/common"; 3 - import { PrismaService } from "../database/prisma.service"; 4 - 5 - @Injectable() 6 - export class ReferenceDataSeedService { 7 - // logger reserved for future diagnostics 8 - 9 - constructor(private readonly prisma: PrismaService) {} 10 - 11 - async ensureSkills(): Promise<Array<{ id: string }>> { 12 - const skills = ["React", "TypeScript", "Node.js", "GraphQL", "PostgreSQL"]; 13 - const created: Array<{ id: string }> = []; 14 - for (const name of skills) { 15 - const existing = await this.prisma["skill"].findFirst({ 16 - where: { name }, 17 - }); 18 - if (existing) { 19 - created.push({ id: existing.id }); 20 - continue; 21 - } 22 - const s = await this.prisma["skill"].create({ 23 - data: { name, description: faker.lorem.sentence() }, 24 - }); 25 - created.push({ id: s.id }); 26 - } 27 - return created; 28 - } 29 - 30 - async ensureCompanies(): Promise<Array<{ id: string }>> { 31 - const names = ["Google", "Microsoft", "Amazon", "Netflix"]; 32 - const created: Array<{ id: string }> = []; 33 - for (const name of names) { 34 - const existing = await this.prisma["company"].findFirst({ 35 - where: { name }, 36 - }); 37 - if (existing) { 38 - created.push({ id: existing.id }); 39 - continue; 40 - } 41 - const c = await this.prisma["company"].create({ 42 - data: { 43 - name, 44 - description: faker.company.catchPhrase(), 45 - website: faker.internet.url(), 46 - }, 47 - }); 48 - created.push({ id: c.id }); 49 - } 50 - return created; 51 - } 52 - 53 - async ensureRoles(): Promise<Array<{ id: string }>> { 54 - const names = [ 55 - "Software Engineer", 56 - "Senior Software Engineer", 57 - "Engineering Manager", 58 - ]; 59 - const created: Array<{ id: string }> = []; 60 - for (const name of names) { 61 - const existing = await this.prisma["role"].findFirst({ where: { name } }); 62 - if (existing) { 63 - created.push({ id: existing.id }); 64 - continue; 65 - } 66 - const r = await this.prisma["role"].create({ 67 - data: { name, description: faker.lorem.sentence() }, 68 - }); 69 - created.push({ id: r.id }); 70 - } 71 - return created; 72 - } 73 - 74 - async ensureLevels(): Promise<Array<{ id: string }>> { 75 - const names = ["Junior", "Mid-level", "Senior"]; 76 - const created: Array<{ id: string }> = []; 77 - for (const name of names) { 78 - const existing = await this.prisma["level"].findFirst({ 79 - where: { name }, 80 - }); 81 - if (existing) { 82 - created.push({ id: existing.id }); 83 - continue; 84 - } 85 - const l = await this.prisma["level"].create({ 86 - data: { name, description: faker.lorem.sentence() }, 87 - }); 88 - created.push({ id: l.id }); 89 - } 90 - return created; 91 - } 92 - }
-19
apps/server/src/modules/seed/seed.module.ts
··· 1 - import { Module } from "@nestjs/common"; 2 - import { ConfigModule } from "@nestjs/config"; 3 - import { DatabaseModule } from "../database/database.module"; 4 - import { OrganizationSeedService } from "./organization-seed.service"; 5 - import { ReferenceDataSeedService } from "./reference-data-seed.service"; 6 - import { SeedService } from "./seed.service"; 7 - import { UserSeedService } from "./user-seed.service"; 8 - 9 - @Module({ 10 - imports: [ConfigModule.forRoot({ isGlobal: true }), DatabaseModule], 11 - providers: [ 12 - SeedService, 13 - ReferenceDataSeedService, 14 - UserSeedService, 15 - OrganizationSeedService, 16 - ], 17 - exports: [SeedService], 18 - }) 19 - export class SeedModule {}
-127
apps/server/src/modules/seed/seed.service.ts
··· 1 - import { faker } from "@faker-js/faker"; 2 - import { Injectable, Logger } from "@nestjs/common"; 3 - import { PrismaService } from "../database/prisma.service"; 4 - import { OrganizationSeedService } from "./organization-seed.service"; 5 - import { ReferenceDataSeedService } from "./reference-data-seed.service"; 6 - import { UserSeedService } from "./user-seed.service"; 7 - 8 - @Injectable() 9 - export class SeedService { 10 - private readonly logger = new Logger(SeedService.name); 11 - 12 - constructor( 13 - private readonly prisma: PrismaService, 14 - private readonly users: UserSeedService, 15 - private readonly refs: ReferenceDataSeedService, 16 - private readonly orgs: OrganizationSeedService, 17 - ) {} 18 - 19 - async seed(): Promise<void> { 20 - this.logger.log("🌱 Starting database seeding..."); 21 - 22 - // Find or create the test user 23 - const testUser = await this.users.ensureTestUser(); 24 - 25 - // Clear existing job experiences for this user only 26 - this.logger.log("🧹 Clearing existing job experiences for test user..."); 27 - await this.prisma.userJobExperience.deleteMany({ 28 - where: { userId: testUser.id }, 29 - }); 30 - 31 - const createdSkills = await this.refs.ensureSkills(); 32 - 33 - const createdCompanies = await this.refs.ensureCompanies(); 34 - 35 - const createdRoles = await this.refs.ensureRoles(); 36 - 37 - const createdLevels = await this.refs.ensureLevels(); 38 - 39 - // Create job experiences for the test user 40 - this.logger.log("💼 Creating job experiences for test user..."); 41 - const numExperiences = faker.number.int({ min: 2, max: 5 }); 42 - 43 - for (let i = 0; i < numExperiences; i++) { 44 - const startDate = faker.date.past({ years: 8 }); 45 - const endDate = faker.datatype.boolean() 46 - ? faker.date.between({ from: startDate, to: new Date() }) 47 - : null; 48 - 49 - const company = faker.helpers.arrayElement(createdCompanies); 50 - const role = faker.helpers.arrayElement(createdRoles); 51 - const level = faker.helpers.arrayElement(createdLevels); 52 - 53 - // Select 3-8 random skills for this job experience 54 - const selectedSkills = faker.helpers.arrayElements(createdSkills, { 55 - min: 3, 56 - max: 8, 57 - }); 58 - 59 - await this.prisma.userJobExperience.create({ 60 - data: { 61 - userId: testUser.id, 62 - companyId: company.id, 63 - roleId: role.id, 64 - levelId: level.id, 65 - startDate, 66 - endDate, 67 - description: faker.lorem.paragraph(), 68 - skills: { 69 - connect: selectedSkills.map((skill) => ({ id: skill.id })), 70 - }, 71 - }, 72 - }); 73 - } 74 - 75 - const createdOrganizations = await this.orgs.ensureOrganizations(); 76 - 77 - // Get all users to assign to organizations 78 - this.logger.log("👥 Assigning users to organizations..."); 79 - const allUsers = await this.prisma.user.findMany(); 80 - 81 - for (const user of allUsers) { 82 - // Each user should have 1-2 organizations, usually at least one 83 - const numOrganizations = faker.number.int({ min: 1, max: 2 }); 84 - const selectedOrganizations = faker.helpers.arrayElements( 85 - createdOrganizations, 86 - { 87 - min: 1, 88 - max: numOrganizations, 89 - }, 90 - ); 91 - 92 - for (const org of selectedOrganizations) { 93 - // Check if user is already in this organization 94 - const existingMembership = await this.prisma.userOrganization.findFirst( 95 - { 96 - where: { 97 - userId: user.id, 98 - organizationId: org.id, 99 - }, 100 - }, 101 - ); 102 - 103 - if (!existingMembership) { 104 - await this.prisma.userOrganization.create({ 105 - data: { 106 - userId: user.id, 107 - organizationId: org.id, 108 - organizationRoleId: "member_role_id", 109 - }, 110 - }); 111 - } 112 - } 113 - } 114 - 115 - this.logger.log("✅ Database seeding completed!"); 116 - this.logger.log(`📊 Created job experiences for: ${testUser.name}`); 117 - this.logger.log(` - ${createdSkills.length} skills available`); 118 - this.logger.log(` - ${createdCompanies.length} companies available`); 119 - this.logger.log(` - ${createdRoles.length} roles available`); 120 - this.logger.log(` - ${createdLevels.length} levels available`); 121 - this.logger.log(` - ${numExperiences} job experiences created`); 122 - this.logger.log( 123 - ` - ${createdOrganizations.length} organizations created`, 124 - ); 125 - this.logger.log(` - ${allUsers.length} users assigned to organizations`); 126 - } 127 - }
-30
apps/server/src/modules/seed/user-seed.service.ts
··· 1 - import { Injectable, Logger } from "@nestjs/common"; 2 - import { PrismaService } from "../database/prisma.service"; 3 - 4 - @Injectable() 5 - export class UserSeedService { 6 - private readonly logger = new Logger(UserSeedService.name); 7 - 8 - constructor(private readonly prisma: PrismaService) {} 9 - 10 - async ensureTestUser(): Promise<{ id: string; email: string; name: string }> { 11 - let testUser = await this.prisma["user"].findUnique({ 12 - where: { email: "test@test.test" }, 13 - }); 14 - 15 - if (!testUser) { 16 - this.logger.log("Creating test user..."); 17 - testUser = await this.prisma["user"].create({ 18 - data: { 19 - email: "test@test.test", 20 - name: "Test User", 21 - // bcrypt hash for "password" 22 - password: 23 - "$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", 24 - }, 25 - }); 26 - } 27 - 28 - return { id: testUser.id, email: testUser.email, name: testUser.name }; 29 - } 30 - }
-58
apps/server/src/modules/vacancies/vacancy.resolver.ts
··· 1 - import { UseGuards } from "@nestjs/common"; 2 - import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; 3 - import { CurrentUser } from "../auth/current-user.decorator"; 4 - import { JwtAuthGuard } from "../auth/jwt-auth.guard"; 5 - import { User } from "../auth/user.type"; 6 - import { VacancyService } from "./vacancy.service"; 7 - import { Vacancy } from "./vacancy.type"; 8 - 9 - @Resolver(() => Vacancy) 10 - @UseGuards(JwtAuthGuard) 11 - export class VacancyResolver { 12 - constructor(private readonly vacancyService: VacancyService) {} 13 - 14 - @Query(() => [Vacancy]) 15 - async myVacancies(@CurrentUser() user: User): Promise<Vacancy[]> { 16 - const domainVacancies = await this.vacancyService.findForUser(user.id); 17 - return domainVacancies.map((vacancy) => Vacancy.fromDomain(vacancy)); 18 - } 19 - 20 - @Mutation(() => Vacancy) 21 - async createVacancy( 22 - @CurrentUser() user: User, 23 - @Args("title") title: string, 24 - @Args("company") company: string, 25 - @Args("description", { nullable: true }) description?: string, 26 - @Args("requirements", { nullable: true }) requirements?: string, 27 - @Args("location", { nullable: true }) location?: string, 28 - @Args("salary", { nullable: true }) salary?: string, 29 - @Args("jobType", { nullable: true }) jobType?: string, 30 - @Args("applicationUrl", { nullable: true }) applicationUrl?: string, 31 - @Args("deadline", { nullable: true }) deadline?: Date, 32 - @Args("isActive", { nullable: true }) isActive?: boolean, 33 - ): Promise<Vacancy> { 34 - const createData = { 35 - title, 36 - company, 37 - ...(description !== undefined && { description }), 38 - ...(requirements !== undefined && { requirements }), 39 - ...(location !== undefined && { location }), 40 - ...(salary !== undefined && { salary }), 41 - ...(jobType !== undefined && { jobType }), 42 - ...(applicationUrl !== undefined && { applicationUrl }), 43 - ...(deadline !== undefined && { deadline }), 44 - ...(isActive !== undefined && { isActive }), 45 - }; 46 - 47 - const domainVacancy = await this.vacancyService.create(user.id, createData); 48 - return Vacancy.fromDomain(domainVacancy); 49 - } 50 - 51 - @Mutation(() => Boolean) 52 - async deleteVacancy( 53 - @CurrentUser() user: User, 54 - @Args("id") id: string, 55 - ): Promise<boolean> { 56 - return this.vacancyService.delete(id, user.id); 57 - } 58 - }
-98
apps/server/src/modules/vacancies/vacancy.type.ts
··· 1 - import { Field, ID, ObjectType } from "@nestjs/graphql"; 2 - import type { Vacancy as DomainVacancy } from "./vacancy.entity"; 3 - 4 - @ObjectType() 5 - export class Vacancy { 6 - @Field(() => ID) 7 - id: string; 8 - 9 - @Field(() => ID) 10 - userId: string; 11 - 12 - @Field(() => String) 13 - title: string; 14 - 15 - @Field(() => String) 16 - company: string; 17 - 18 - @Field(() => String, { nullable: true }) 19 - description: string | null; 20 - 21 - @Field(() => String, { nullable: true }) 22 - requirements: string | null; 23 - 24 - @Field(() => String, { nullable: true }) 25 - location: string | null; 26 - 27 - @Field(() => String, { nullable: true }) 28 - salary: string | null; 29 - 30 - @Field(() => String, { nullable: true }) 31 - jobType: string | null; 32 - 33 - @Field(() => String, { nullable: true }) 34 - applicationUrl: string | null; 35 - 36 - @Field(() => Date, { nullable: true }) 37 - deadline: Date | null; 38 - 39 - @Field(() => Boolean) 40 - isActive: boolean; 41 - 42 - @Field(() => Date) 43 - createdAt: Date; 44 - 45 - @Field(() => Date) 46 - updatedAt: Date; 47 - 48 - constructor(data: { 49 - id: string; 50 - userId: string; 51 - title: string; 52 - company: string; 53 - description?: string | null; 54 - requirements?: string | null; 55 - location?: string | null; 56 - salary?: string | null; 57 - jobType?: string | null; 58 - applicationUrl?: string | null; 59 - deadline?: Date | null; 60 - isActive: boolean; 61 - createdAt: Date; 62 - updatedAt: Date; 63 - }) { 64 - this.id = data.id; 65 - this.userId = data.userId; 66 - this.title = data.title; 67 - this.company = data.company; 68 - this.description = data.description ?? null; 69 - this.requirements = data.requirements ?? null; 70 - this.location = data.location ?? null; 71 - this.salary = data.salary ?? null; 72 - this.jobType = data.jobType ?? null; 73 - this.applicationUrl = data.applicationUrl ?? null; 74 - this.deadline = data.deadline ?? null; 75 - this.isActive = data.isActive; 76 - this.createdAt = data.createdAt; 77 - this.updatedAt = data.updatedAt; 78 - } 79 - 80 - static fromDomain(domainVacancy: DomainVacancy): Vacancy { 81 - return new Vacancy({ 82 - id: domainVacancy.id, 83 - userId: domainVacancy.userId, 84 - title: domainVacancy.title, 85 - company: domainVacancy.company, 86 - description: domainVacancy.description ?? null, 87 - requirements: domainVacancy.requirements ?? null, 88 - location: domainVacancy.location ?? null, 89 - salary: domainVacancy.salary ?? null, 90 - jobType: domainVacancy.jobType ?? null, 91 - applicationUrl: domainVacancy.applicationUrl ?? null, 92 - deadline: domainVacancy.deadline ?? null, 93 - isActive: domainVacancy.isActive, 94 - createdAt: domainVacancy.createdAt, 95 - updatedAt: domainVacancy.updatedAt, 96 - }); 97 - } 98 - }