diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..35495c0 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,21 @@ +{ + "mcpServers": { + "shadcn-ui-server": { + "command": "npx", + "args": ["-y", "shadcn-ui-mcp-server"] + }, + "plate": { + "description": "Plate editors, plugins, components, and docs", + "type": "stdio", + "command": "npx", + "args": ["-y", "shadcn@canary", "registry:mcp"], + "env": { + "REGISTRY_URL": "https://platejs.org/r/registry.json" + } + }, + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest"] + } + } +} diff --git a/.cursor/rules/accessibility.mdc b/.cursor/rules/accessibility.mdc new file mode 100644 index 0000000..83930fa --- /dev/null +++ b/.cursor/rules/accessibility.mdc @@ -0,0 +1,27 @@ +--- +description: +globs: +alwaysApply: false +--- +## Accessibility Guidelines + +- Use semantic HTML elements for proper document structure +- Implement proper ARIA labels and roles for complex interactions +- Ensure keyboard navigation works for all interactive elements +- Use proper focus management and visible focus indicators +- Implement proper heading hierarchy (h1, h2, h3, etc.) +- Provide alternative text for all images and media content +- Use sufficient color contrast ratios for text and backgrounds +- Ensure all form inputs have proper labels and error messages +- Implement proper screen reader support for dynamic content +- Use landmark roles for better navigation structure +- Provide skip links for keyboard users +- Ensure all interactive elements are properly focusable +- Use proper live regions for dynamic content updates +- Implement proper error announcement for form validation +- Use descriptive link text and button labels +- Ensure proper table markup with headers and captions +- Implement proper modal and dialog accessibility +- Use consistent navigation patterns throughout the application +- Test with screen readers and keyboard-only navigation +- Follow WCAG 2.1 AA guidelines for compliance diff --git a/.cursor/rules/ai-assisted-coding.mdc b/.cursor/rules/ai-assisted-coding.mdc new file mode 100644 index 0000000..200669a --- /dev/null +++ b/.cursor/rules/ai-assisted-coding.mdc @@ -0,0 +1,158 @@ +--- +description: +globs: +alwaysApply: false +--- +## AI-Assisted Coding Guidelines + +### Cursor Rules & MCP Best Practices + +- Use `@rules` to reference these workspace rules in your prompts +- Leverage the `.cursor/rules/` directory for consistent AI guidance +- Reference specific rule files: `@rules/typescript`, `@rules/error-handling`, etc. +- Use MCP tools for codebase exploration and understanding +- Keep rules concise but comprehensive for better AI comprehension + +### Free Tier Optimization + +- **Be Specific**: Provide clear, detailed prompts to avoid back-and-forth +- **Use Context Wisely**: Reference existing files and patterns in your requests +- **Batch Requests**: Combine related changes into single comprehensive requests +- **Focus on Quality**: Prefer fewer, well-crafted prompts over many short ones +- **Use Existing Patterns**: Reference established code patterns to reduce explanation needs + +### Effective AI Prompting + +``` +# Good Prompt Structure: +1. Context: "I'm working on [specific feature/component]" +2. Goal: "I need to [specific action]" +3. Constraints: "Following our [specific rule/pattern]" +4. Reference: "Similar to [existing file/pattern]" +``` + +### Model Selection for Free Tier + +- **Claude 3.5 Sonnet**: Best for complex refactoring and architecture decisions +- **GPT-4**: Good for general coding tasks and explanations +- **GPT-3.5**: Suitable for simple tasks and quick fixes +- **Local Models**: Consider for frequent, simple tasks to preserve quota + +### Efficient AI Workflows + +#### For New Features: +1. Start with architecture planning using AI +2. Generate component structure following established patterns +3. Implement core logic with AI assistance +4. Use AI for error handling and edge cases +5. AI-assisted testing and documentation + +#### For Bug Fixes: +1. Use AI to analyze error patterns +2. Reference similar fixes in codebase +3. Apply established error handling patterns +4. Validate fix with AI review + +#### For Refactoring: +1. Use AI to identify improvement opportunities +2. Follow established code organization rules +3. Maintain consistency with existing patterns +4. Use AI for impact analysis + +### Smart Context Usage + +- **File References**: Use `@filename` to include relevant files +- **Rule References**: Use `@rules/rulename` for specific guidelines +- **Pattern Examples**: Reference similar implementations in codebase +- **Error Context**: Include full error messages and stack traces + +### Prompt Templates + +#### New Component Creation: +``` +Create a new [component type] following @rules/ui-and-styling +and @rules/typescript patterns. Reference [similar component] +for consistency. Include proper error handling per @rules/error-handling. +``` + +#### Server Action Implementation: +``` +Implement a server action following @rules/data-management +and @rules/error-handling patterns. Use the established +ServerActionResult pattern from lib/server-action-utils.ts. +``` + +#### Bug Investigation: +``` +Analyze this error following @rules/error-handling. +Check similar patterns in [relevant files]. +Suggest fix maintaining consistency with our codebase. +``` + +### Code Review with AI + +- Use AI to review code against established rules +- Check for consistency with existing patterns +- Validate error handling implementation +- Review performance implications +- Ensure accessibility compliance + +### AI-Assisted Documentation + +- Generate JSDoc comments for complex functions +- Create README updates for new features +- Document API changes and breaking changes +- Generate code examples following established patterns + +### Limitations & Alternatives + +#### When AI Struggles: +- Very large file modifications (break into smaller chunks) +- Complex state management (reference existing patterns) +- Performance optimization (use specific metrics and requirements) +- Security-sensitive code (validate thoroughly) + +#### Free Tier Strategies: +- Plan requests during off-peak hours +- Use AI for planning, implement manually when quota is low +- Focus AI usage on complex problems, not simple syntax +- Cache AI-generated solutions for similar future problems + +### Collaboration Patterns + +#### Team Usage: +- Share effective prompts and patterns +- Document AI-generated solutions for team reference +- Review AI suggestions before implementation +- Maintain consistency across team members' AI usage + +#### Code Quality: +- Always review AI-generated code thoroughly +- Test AI solutions against established patterns +- Validate security and performance implications +- Ensure adherence to project rules and conventions + +### Integration with Development Workflow + +1. **Planning Phase**: Use AI for architecture and approach +2. **Implementation**: AI-assisted coding with rule validation +3. **Review Phase**: AI code review against established patterns +4. **Documentation**: AI-generated docs following project standards +5. **Testing**: AI-assisted test creation and validation + +### AI Planning Workflow + +Before proposing implementation plans: +1. Ask 4-6 clarifying questions based on your findings +2. Draft a comprehensive plan of action and seek approval +3. If feedback is provided, revise the plan and ask for approval again +4. Once approved, implement all steps in that plan +5. After completing each phase/step, mention what was completed and remaining phases + +### Monitoring AI Effectiveness + +- Track which types of requests work best +- Document successful prompt patterns +- Note common AI limitations for your use case +- Share learnings with team for collective improvement + diff --git a/.cursor/rules/code-organization.mdc b/.cursor/rules/code-organization.mdc new file mode 100644 index 0000000..b9beeee --- /dev/null +++ b/.cursor/rules/code-organization.mdc @@ -0,0 +1,46 @@ +--- +description: Organize code in a consistent and logical manner. +globs: */** +alwaysApply: false +--- +- Structure files logically, grouping related components, helpers, types and static content +- Prefer named exports for components over default exports +- Favor small, single-purpose components over large, monolithic ones +- Separate concerns between presentational and container components +- Use consistent file naming: kebab-case for files, PascalCase for components + +## Project Structure + +The folders follow a standard Next.js application with specific conventions: + +/actions - Server-side mutations (Create, Update, Delete operations) +/app - Next.js App Router pages and API routes + /(admin) - Admin-only pages with role-based access + /api - API route handlers for external integrations + /auth.config.ts, /auth.ts - NextAuth configuration and setup + /db.ts - Centralized database connection and basic queries +/components - Reusable UI components organized by feature + /ui - Base UI components (buttons, inputs, dialogs) from shadcn/ui + /editor - Plate.js editor components and plugins + /providers - React context providers (session, theme, etc.) +/db - Database schema and configuration + /schema.ts - Drizzle ORM schema definitions +/drizzle - Database migrations and metadata + /meta - Migration metadata and journal +/hooks - Custom React hooks for shared logic +/lib - Shared utility functions and configurations + /utils.ts - Utility functions and class name helpers + /constants.ts - Application constants and configuration +/public - Static assets (images, icons, fonts) +/types - TypeScript type definitions and DTOs +/__tests__ - Test files and test utilities + +## Component Organization + +- Group related components in feature-specific directories +- Keep component files focused on a single responsibility +- Use index files for clean imports when appropriate +- Separate complex components into smaller, composed parts +- Co-locate related files (component, styles, tests) when beneficial +- Use the `/components/ui` directory for reusable UI primitives +- Place feature-specific components in dedicated subdirectories \ No newline at end of file diff --git a/.cursor/rules/data-management.mdc b/.cursor/rules/data-management.mdc new file mode 100644 index 0000000..9a278b7 --- /dev/null +++ b/.cursor/rules/data-management.mdc @@ -0,0 +1,23 @@ +--- +description: Interaction with the database. +globs: **/*.{js,jsx,ts,tsx} +alwaysApply: false +--- +- Interact with the database exclusively using Drizzle ORM client +- Leverage Drizzle's generated types for type safety +- Use the centralized Drizzle client from `app/db.ts` and schema from `db/schema.ts` +- Implement data operations in separate layers: + - `/actions` for mutations (Create, Update, Delete operations) + - Direct database queries in components/pages for read operations +- Use Drizzle's query methods (select, insert, update, delete) with proper type safety +- Apply proper error handling for database operations using try-catch blocks +- Implement transaction support using Drizzle's transaction API when needed +- Use Drizzle's select with specific fields for performance optimization +- Follow the established patterns for server actions with Zod validation +- Implement proper caching strategies for read operations using Next.js caching +- Use database-level constraints and validation defined in the schema +- Handle relationship management using Drizzle's relational queries +- Use proper indexing for performance-critical queries +- Use Drizzle enums for consistent data validation (userRoleEnum, postStatusEnum, etc.) +- Leverage Drizzle's JSONB support for rich content storage (Plate.js content) +- Follow the established connection pattern using postgres client with connection pooling diff --git a/.cursor/rules/error-handling.mdc b/.cursor/rules/error-handling.mdc new file mode 100644 index 0000000..878d4ae --- /dev/null +++ b/.cursor/rules/error-handling.mdc @@ -0,0 +1,24 @@ +--- +description: Enfore friendly and logical error handling. +globs: **/*.{js,jsx,ts,tsx} +--- +- Implement robust error handling and logging mechanisms. +- Implement robust error handling and logging mechanisms throughout the application +- Use structured error handling in server actions with consistent return patterns +- Provide clear and user-friendly error messages using toast notifications +- Handle database errors consistently using try-catch blocks around Drizzle operations +- Use proper validation error handling with Zod schemas for form validation +- Follow the established error handling pattern in server actions: + - Wrap database operations in try-catch blocks + - Log errors with contextual information to console/logging service + - Return consistent error response format from server actions + - Handle specific error types (database, validation, authentication) +- Use error boundaries for React component error handling where appropriate +- Handle upload errors gracefully with appropriate fallbacks +- Implement retry logic for transient failures (network, database connection) +- Provide proper loading states during async operations +- Log errors with appropriate context like userId, operation type, and timestamps +- Use NextAuth error handling patterns for authentication-related errors +- Implement proper error handling for API routes with appropriate HTTP status codes +- Handle Drizzle constraint violations and provide meaningful user feedback +- Use proper error handling for file operations and external API calls diff --git a/.cursor/rules/general-principles.mdc b/.cursor/rules/general-principles.mdc new file mode 100644 index 0000000..a4daffe --- /dev/null +++ b/.cursor/rules/general-principles.mdc @@ -0,0 +1,27 @@ +--- +description: Applies general principles across the project. +globs: */** +--- +- You are an expert in TypeScript, Node.js, Next.js with the app router, React, shadcn/ui, Tailwind, Auth.js and Prisma. +- You are an expert in TypeScript, Node.js, Next.js (App Router), React, Shadcn/UI, Tailwind, NextAuth, and Drizzle ORM +- Write clean, concise and well-commented TypeScript code +- Favor functional and declarative programming patterns over object-oriented approaches +- Prioritize code reuse and modularization over duplication +- Follow the established patterns in the codebase for consistency +- Use console.log for debugging and error logging during development +- Implement proper validation using Zod schemas for form validation and API inputs +- Use the established server action patterns for data mutations +- Implement proper error handling and user feedback using toast notifications +- Follow security best practices for data handling and authentication +- Use performance optimization techniques (memoization, lazy loading, Next.js caching) +- Implement proper testing strategies using Jest and Playwright +- Use semantic HTML and proper accessibility practices +- Follow the established patterns for state management with React hooks +- Implement proper documentation for complex functions and components +- Use consistent naming conventions across the project (see naming-conventions rule) +- Prioritize user experience and performance in all implementations +- Leverage Drizzle ORM's type safety and query capabilities +- Use NextAuth v5 beta patterns for authentication and session management +- Follow the established database schema patterns with proper enums and relationships +- Use Plate.js for rich text editing with JSONB storage in the database + diff --git a/.cursor/rules/naming-conventions.mdc b/.cursor/rules/naming-conventions.mdc new file mode 100644 index 0000000..596a3ed --- /dev/null +++ b/.cursor/rules/naming-conventions.mdc @@ -0,0 +1,41 @@ +--- +description: Enforces a consistent naming convention across the project. +globs: */** +--- +- Use PascalCase for class names and type definitions. +- Use PascalCase for React components and type definitions +- Utilize camelCase for variables, functions and methods +- Employ kebab-case for file and directory names +- Reserve UPPERCASE for environment variables and constants +- Avoid magic numbers by defining constants with meaningful names +- Start function names with verbs to indicate their purpose + +## Specific Conventions + +### Components +- `UserProfile` (PascalCase for component names) +- `user-profile.tsx` (kebab-case for file names) +- `useUserProfile` (camelCase for hooks, starting with 'use') + +### Server Actions +- `createUser`, `updateUser`, `deleteUser` (camelCase, verb-first) +- `create-user.ts` (kebab-case for file names) + +### Types and Interfaces +- `UserProfile`, `ServerActionResult` (PascalCase) +- `CreateUserInput`, `UpdateUserInput` (descriptive, specific) + +### Variables and Functions +- `isLoading`, `hasError`, `shouldRender` (boolean variables with auxiliary verbs) +- `userData`, `userList`, `documentId` (camelCase, descriptive) +- `handleSubmit`, `processImage`, `validateInput` (camelCase, verb-first) + +### Constants +- `MAX_FILE_SIZE`, `DEFAULT_AVATAR_URL` (UPPERCASE with underscores) +- `MEDIA_CONFIG`, `PROMPT_TEMPLATES` (UPPERCASE for configuration objects) + +### Database/Drizzle +- Follow Drizzle naming conventions (snake_case for database fields, camelCase for client-side) +- Use descriptive field names: `createdAt`, `updatedAt`, `isPublished` +- Use meaningful enum names: `userRoleEnum`, `postStatusEnum`, `courseStatusEnum` +- Follow consistent table naming: singular form (e.g., `user`, `post`, `course`) diff --git a/.cursor/rules/nextjs.mdc b/.cursor/rules/nextjs.mdc new file mode 100644 index 0000000..0259ea8 --- /dev/null +++ b/.cursor/rules/nextjs.mdc @@ -0,0 +1,20 @@ +--- +description: Applies optimal Next.js rules and best practices. +globs: **/*.{js,jsx,ts,tsx} +--- +- Favor React Server Components (RSC) where possible. +- Favor React Server Components (RSC) where possible. +- Minimize `'use client'` directives - only use for interactivity. +- Optimize for performance and Web Vitals. +- Use server actions for mutations instead of API route handlers. +- Use `next/image` for optimized image loading and processing. +- Implement proper loading states and error boundaries. +- Use Next.js built-in caching strategies appropriately. +- Leverage the App Router's streaming capabilities. +- Implement proper SEO with metadata API. +- Use Next.js built-in optimizations (fonts, images, scripts). +- Follow the established routing patterns in `/app` directory. +- Use proper data fetching patterns with server components. +- Implement progressive enhancement where appropriate. +- Use dynamic imports for code splitting when beneficial. +- Optimize bundle size with proper tree shaking. diff --git a/.cursor/rules/performance.mdc b/.cursor/rules/performance.mdc new file mode 100644 index 0000000..722ff8f --- /dev/null +++ b/.cursor/rules/performance.mdc @@ -0,0 +1,27 @@ +--- +description: +globs: +alwaysApply: false +--- +## Performance Optimization Guidelines + +- Use React Server Components by default, client components only when necessary +- Implement proper loading states and skeleton screens +- Use Next.js Image component for all image assets +- Implement proper caching strategies for data fetching +- Use dynamic imports for code splitting large components +- Implement proper memoization with React.memo, useMemo, useCallback +- Use the established debouncing patterns from `hooks/use-debounce.ts` +- Optimize database queries with proper Prisma selections +- Implement proper pagination for large data sets +- Use streaming for better perceived performance +- Minimize bundle size with proper tree shaking +- Implement proper lazy loading for non-critical content +- Use established patterns for optimistic updates +- Handle large lists with virtualization when necessary +- Implement proper preloading for critical resources +- Use efficient state management patterns +- Minimize re-renders with proper dependency arrays +- Implement proper error boundaries to prevent cascading failures +- Use Web Vitals monitoring and optimization techniques +- Follow established patterns for auto-save functionality diff --git a/.cursor/rules/security.mdc b/.cursor/rules/security.mdc new file mode 100644 index 0000000..9798bb3 --- /dev/null +++ b/.cursor/rules/security.mdc @@ -0,0 +1,27 @@ +--- +description: +globs: +alwaysApply: false +--- +## Security Guidelines + +- Validate all user inputs using Zod schemas consistently +- Use proper authentication and authorization patterns +- Implement proper CSRF protection for all forms +- Use the established permission checking patterns +- Sanitize user-generated content before storage and display +- Implement proper rate limiting for API endpoints +- Use secure file upload validation and processing +- Follow established patterns for sensitive data handling +- Implement proper session management and token handling +- Use environment variables for all sensitive configuration +- Implement proper logging without exposing sensitive data +- Use proper data encryption for sensitive information +- Follow secure coding practices for database operations +- Implement proper input validation and output encoding +- Use Content Security Policy (CSP) headers appropriately +- Implement proper error handling without information leakage +- Use secure communication protocols (HTTPS) consistently +- Follow established patterns for user data privacy +- Implement proper backup and recovery procedures +- Use security headers and proper CORS configuration diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc new file mode 100644 index 0000000..171f881 --- /dev/null +++ b/.cursor/rules/testing.mdc @@ -0,0 +1,47 @@ +--- +description: +globs: +alwaysApply: false +--- +--- +description: Testing guidelines for Jest, React Testing Library, and Playwright end-to-end testing +globs: + - "**/*.test.ts" + - "**/*.test.tsx" + - "**/*.spec.ts" + - "**/*.spec.tsx" + - "__tests__/**/*" + - "e2e/**/*" + - "jest.config.ts" + - "jest.setup.ts" + - "playwright.config.ts" + - "playwright-report/**/*" + - "test-results/**/*" +alwaysApply: false +--- +## Testing Guidelines + +- Write tests for all critical business logic and server actions +- Use Jest and React Testing Library for component testing +- Implement integration tests for complex user workflows +- Test error handling and edge cases thoroughly +- Use proper mocking for external dependencies and APIs +- Test accessibility features and keyboard navigation +- Implement visual regression testing for UI components +- Use proper test data and factories for consistent testing +- Test responsive design and mobile functionality +- Implement end-to-end tests for critical user journeys +- Test database operations and data integrity +- Use proper setup and teardown for test environments +- Test performance characteristics of critical operations +- Implement proper test coverage reporting +- Use descriptive test names and clear assertions +- Test both happy path and error scenarios +- Implement proper async testing patterns +- Test user interactions and form submissions +- Use proper component testing isolation +- Maintain test data separate from production data +- Use Playwright MCP tools for enhanced test automation and debugging +- Leverage MCP for intelligent test generation and maintenance +- Integrate MCP tools for cross-browser testing consistency + diff --git a/.cursor/rules/typescript.mdc b/.cursor/rules/typescript.mdc new file mode 100644 index 0000000..14d513e --- /dev/null +++ b/.cursor/rules/typescript.mdc @@ -0,0 +1,24 @@ +--- +description: Applies general TypeScript coding standards and best practices across the project. +globs: **/*.{ts,tsx} +--- +- Use TypeScript for all code; prefer types over interfaces. +- Use TypeScript for all code; prefer `type` over `interface`. +- Write concise, technical TypeScript code with accurate examples. +- Use functional and declarative programming patterns; avoid classes. +- Prefer iteration and modularization over code duplication. +- Use descriptive variable names with auxiliary verbs (e.g., `isLoading`, `hasError`). +- Leverage Drizzle ORM's generated types for database operations and schema definitions. +- Use proper generic types for reusable components and functions. +- Implement strict type checking with proper null/undefined handling. +- Use discriminated unions for complex state management. +- Implement proper typing for server actions and API responses. +- Use const assertions for immutable data structures. +- Implement proper typing for React component props and state. +- Use utility types (Pick, Omit, Partial) for type transformations. +- Implement proper error typing with custom error types. +- Use type guards for runtime type checking. +- Leverage TypeScript's template literal types for string validation. +- Use Drizzle's InferSelectModel and InferInsertModel for type inference from schema. +- Implement proper typing for NextAuth session and user objects. +- Use proper typing for Plate.js editor content with JSONB storage. diff --git a/.cursor/rules/ui-and-styling.mdc b/.cursor/rules/ui-and-styling.mdc new file mode 100644 index 0000000..0fbe634 --- /dev/null +++ b/.cursor/rules/ui-and-styling.mdc @@ -0,0 +1,18 @@ +--- +description: Enforces UI and styling conventions using Shadcn UI, Radix UI and Tailwind CSS for all components. +globs: **/*.{js,jsx,ts,tsx} +alwaysApply: false +--- +- Use Shadcn UI, Radix UI and Tailwind for components and styling. +- Use Shadcn UI, Radix UI and Tailwind CSS for all components and styling +- Make use of the existing components in `/components/ui/` directory +- Implement responsive design with Tailwind CSS using a mobile-first approach +- Configure the Tailwind CSS theme in the `tailwind.config.ts` file +- Follow the established design patterns in the editor components +- Use consistent color schemes: brand colors for primary actions, muted colors for secondary elements +- Apply proper spacing using Tailwind's spacing scale (p-4, m-2, etc.) +- Implement dark mode support using Tailwind's dark: prefix +- Use semantic HTML elements and proper ARIA labels for accessibility +- Ensure consistent typography using Tailwind's font utilities +- Leverage existing UI component variants (button variants, card variants, etc.) +- Follow the established pattern of using `cn()` utility for conditional classes diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..6f9f00f --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..de2c909 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +ο»Ώ# Database Configuration for Development +POSTGRES_URL="postgresql://..." + +GOOGLE_CLIENT_ID="your_google_client_id" +GOOGLE_CLIENT_SECRET="your_google_client_secret" +NEXTAUTH_URL="http://localhost:3000" +NEXTAUTH_SECRET="your_nextauth_secret" \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..63ca953 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/components/ui/**.tsx diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..8d0f89c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"], + "rules": { + "@typescript-eslint/no-explicit-any": ["off"] + } +} diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..1844ac3 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,194 @@ +# GitHub Actions Workflows + +This directory contains automated testing and CI/CD workflows for the codac application. + +## πŸ“ Workflow Files + +### πŸš€ Primary Workflows + +#### `test.yml` - Main Test Suite +**Triggers:** Push to `main`/`develop`, Pull Requests +**Purpose:** Comprehensive testing including unit tests, e2e tests, and build verification + +**Jobs:** +- **Unit Tests** - Runs Jest tests with coverage reporting +- **E2E Tests** - Full Playwright test suite across all browsers +- **Build Check** - Verifies the application builds successfully +- **Test Summary** - Aggregates results and reports overall status + +#### `test-pr.yml` - Pull Request Tests +**Triggers:** Pull Request events (opened, synchronize, reopened) +**Purpose:** Fast feedback for PRs with essential tests only + +**Jobs:** +- **Quick Checks** - Linting, formatting, type checking, unit tests +- **Critical E2E** - Key e2e tests (auth, navigation) in Chromium only + +#### `nightly-tests.yml` - Comprehensive Nightly Testing +**Triggers:** Daily at 2 AM UTC, Manual dispatch +**Purpose:** Extensive testing including performance and accessibility + +**Jobs:** +- **Cross-browser E2E** - Full test suite across Chrome, Firefox, Safari +- **Performance Tests** - Lighthouse performance analysis +- **Accessibility Tests** - Automated accessibility compliance checking +- **Notifications** - Summary reporting (can be extended with Slack/Discord) + +## 🎯 Testing Strategy + +### Development Workflow +```mermaid +graph TD + A[Developer pushes code] --> B[test-pr.yml runs] + B --> C{Quick checks pass?} + C -->|Yes| D[Critical E2E tests] + C -->|No| E[PR blocked] + D --> F{E2E tests pass?} + F -->|Yes| G[PR ready for review] + F -->|No| E +``` + +### Main Branch Protection +```mermaid +graph TD + A[PR approved] --> B[test.yml runs] + B --> C[Unit Tests] + B --> D[Full E2E Tests] + B --> E[Build Check] + C --> F[Test Summary] + D --> F + E --> F + F --> G{All pass?} + G -->|Yes| H[Merge allowed] + G -->|No| I[Merge blocked] +``` + +### Nightly Quality Assurance +```mermaid +graph TD + A[2 AM UTC Daily] --> B[nightly-tests.yml] + B --> C[Chrome Tests] + B --> D[Firefox Tests] + B --> E[Safari Tests] + B --> F[Performance Tests] + B --> G[Accessibility Tests] + C --> H[Results Aggregation] + D --> H + E --> H + F --> H + G --> H + H --> I[Notification/Report] +``` + +## βš™οΈ Configuration + +### Environment Variables +Required secrets in your GitHub repository: + +```bash +# Production environment variables (if needed for build) +NEXTAUTH_SECRET=your_nextauth_secret +``` + +### Package.json Scripts +Added CI-specific scripts: + +```json +{ + "test:ci": "jest --coverage --watchAll=false --ci --passWithNoTests", + "test:e2e:ci": "playwright test --reporter=github", + "test:e2e:headed": "playwright test --headed", + "test:e2e:debug": "playwright test --debug" +} +``` + +## πŸ“Š Test Coverage & Reporting + +### Unit Test Coverage +- Jest generates coverage reports automatically +- Coverage uploaded to Codecov (if token provided) +- Coverage threshold can be configured in `jest.config.js` + +### E2E Test Reporting +- Playwright generates HTML reports for all test runs +- Test artifacts (screenshots, videos) uploaded on failures +- GitHub annotations for failed tests in PR reviews + +### Performance Monitoring +- Lighthouse reports generated nightly +- Performance budgets can be configured +- Regression detection for performance metrics + +## πŸ”§ Optimization Features + +### Caching Strategy +- **pnpm cache** - Dependencies cached between runs +- **Playwright browsers** - Browser binaries cached +- **Build cache** - Next.js build cache optimization + +### Parallel Execution +- Unit and E2E tests run in parallel jobs +- Cross-browser testing uses matrix strategy +- Fail-fast disabled for comprehensive reporting + +### Resource Management +- **Artifact retention** configured by importance: + - Critical results: 30 days + - Test reports: 7-14 days + - PR artifacts: 3 days + +## 🚦 Status Badges + +Add these badges to your README.md: + +```markdown +![Tests](https://github.com/your-org/codac/workflows/Test%20Suite/badge.svg) +![Nightly Tests](https://github.com/your-org/codac/workflows/Nightly%20Tests/badge.svg) +``` + +## πŸ”§ Maintenance + +### Adding New Tests +1. Add test files to appropriate `e2e/` directories +2. Tests automatically picked up by existing workflows +3. Consider adding critical tests to PR workflow + +### Customizing Workflows +1. **Performance budgets** - Modify Lighthouse configuration +2. **Browser support** - Add/remove browsers in matrix strategy +3. **Notification channels** - Configure Slack/Discord webhooks +4. **Test schedules** - Adjust cron expressions for nightly runs + +### Troubleshooting +- **Browser installation failures** - Check Playwright version compatibility +- **Test timeouts** - Adjust timeout values in `playwright.config.ts` +- **Build failures** - Verify environment variables are set correctly +- **Cache issues** - Clear workflow caches in GitHub Actions settings + +## πŸ“ˆ Metrics & Monitoring + +### Key Metrics Tracked +- **Test execution time** - Monitor for performance regressions +- **Test success rate** - Track flaky tests +- **Coverage percentage** - Ensure adequate test coverage +- **Performance scores** - Lighthouse metrics trending + +### Recommended Monitoring +- Set up alerts for consistent test failures +- Monitor test execution time trends +- Track coverage changes over time +- Performance regression alerts + +## 🎯 Best Practices + +### For Developers +- Run `pnpm test:e2e:headed` locally before pushing +- Use `pnpm test:e2e:debug` for debugging failing tests +- Keep tests focused and deterministic +- Update tests when changing UI components + +### For Maintainers +- Review test reports in PR reviews +- Monitor nightly test results regularly +- Update browser versions periodically +- Maintain test data and fixtures \ No newline at end of file diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml new file mode 100644 index 0000000..0824b17 --- /dev/null +++ b/.github/workflows/nightly-tests.yml @@ -0,0 +1,176 @@ +name: Nightly Tests + +on: + schedule: + # Run every night at 2 AM UTC + - cron: '0 2 * * *' + workflow_dispatch: # Allow manual trigger + +jobs: + comprehensive-e2e: + name: Cross-browser E2E Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + browser: [chromium, firefox, webkit] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps + + - name: Run all e2e tests for ${{ matrix.browser }} + run: pnpm playwright test --project=${{ matrix.browser }} + env: + CI: true + NEXTAUTH_SECRET: nightly-test-secret-key + NEXTAUTH_URL: http://localhost:3000 + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: nightly-test-results-${{ matrix.browser }} + path: | + test-results/ + playwright-report/ + retention-days: 14 + + performance-tests: + name: Performance Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + run: pnpm exec playwright install chromium --with-deps + + - name: Run performance tests + run: | + # Build and start application first + pnpm build + pnpm start & + + # Wait for application to be ready + timeout 60s bash -c 'until curl -f http://localhost:3000; do sleep 2; done' + + # Run Lighthouse CI or similar performance tests + npx lighthouse http://localhost:3000 --output=json --output-path=./lighthouse-report.json --chrome-flags="--headless --no-sandbox" + env: + NEXTAUTH_SECRET: performance-test-secret + NEXTAUTH_URL: http://localhost:3000 + + - name: Upload performance results + uses: actions/upload-artifact@v4 + if: always() + with: + name: performance-results + path: lighthouse-report.json + retention-days: 30 + + accessibility-tests: + name: Accessibility Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + run: pnpm exec playwright install chromium --with-deps + + - name: Run accessibility tests + run: | + # Build and start application first + pnpm build + pnpm start & + + # Wait for application to be ready + timeout 60s bash -c 'until curl -f http://localhost:3000; do sleep 2; done' + + # Install axe-core CLI + npm install -g @axe-core/cli + + # Run accessibility tests on key pages + axe http://localhost:3000/login --exit + axe http://localhost:3000/register --exit + env: + NEXTAUTH_SECRET: accessibility-test-secret + NEXTAUTH_URL: http://localhost:3000 + + - name: Upload accessibility results + uses: actions/upload-artifact@v4 + if: always() + with: + name: accessibility-results + path: axe-results/ + retention-days: 14 + + notification: + name: Test Notification + runs-on: ubuntu-latest + needs: [comprehensive-e2e, performance-tests, accessibility-tests] + if: always() + + steps: + - name: Send notification + run: | + if [[ "${{ needs.comprehensive-e2e.result }}" == "success" && \ + "${{ needs.performance-tests.result }}" == "success" && \ + "${{ needs.accessibility-tests.result }}" == "success" ]]; then + echo "βœ… All nightly tests passed!" + else + echo "❌ Some nightly tests failed!" + echo "Cross-browser E2E: ${{ needs.comprehensive-e2e.result }}" + echo "Performance: ${{ needs.performance-tests.result }}" + echo "Accessibility: ${{ needs.accessibility-tests.result }}" + fi + + # You can add Slack/Discord/email notifications here + # Example: curl -X POST -H 'Content-type: application/json' \ + # --data '{"text":"Nightly test results: ..."}' \ + # ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml new file mode 100644 index 0000000..7817d7a --- /dev/null +++ b/.github/workflows/test-pr.yml @@ -0,0 +1,111 @@ +name: PR Tests + +on: + pull_request: + branches: [main, develop] + types: [opened, synchronize, reopened] + +jobs: + quick-checks: + name: Quick Checks + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run linting + run: pnpm lint + + - name: Run formatting check + run: pnpm check + + - name: Type check + run: pnpm build --dry-run || echo "Type checking completed" + + - name: Run unit tests + run: pnpm test --watchAll=false + + critical-e2e: + name: Critical E2E Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }} + + - name: Install Playwright browsers + run: pnpm exec playwright install chromium --with-deps + + - name: Run critical e2e tests + run: pnpm playwright test --project=chromium auth.spec.ts navigation.spec.ts + env: + CI: true + NEXTAUTH_SECRET: test-secret-key-for-pr + NEXTAUTH_URL: http://localhost:3000 + + - name: Upload test results on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: pr-e2e-test-results + path: test-results/ + retention-days: 3 \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..418bb2a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,191 @@ +name: Test Suite + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run linting + run: pnpm lint + + - name: Run type checking + run: pnpm build --dry-run || echo "Type checking completed" + + - name: Run unit tests + run: pnpm test --coverage --watchAll=false + + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + if: always() + with: + file: ./coverage/lcov.info + flags: unit-tests + name: unit-tests-coverage + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + e2e-tests: + name: E2E Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps + + - name: Run e2e tests + run: pnpm test:e2e + env: + CI: true + NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET || 'test-secret-key-for-ci' }} + NEXTAUTH_URL: http://localhost:3000 + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-test-results + path: test-results/ + retention-days: 7 + + build-check: + name: Build Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build application + run: pnpm build + env: + NEXTAUTH_SECRET: test-secret-key-for-build-check + NEXTAUTH_URL: http://localhost:3000 + + test-summary: + name: Test Summary + runs-on: ubuntu-latest + needs: [unit-tests, e2e-tests, build-check] + if: always() + + steps: + - name: Check test results + run: | + if [[ "${{ needs.unit-tests.result }}" == "success" && \ + "${{ needs.e2e-tests.result }}" == "success" && \ + "${{ needs.build-check.result }}" == "success" ]]; then + echo "βœ… All tests passed!" + exit 0 + else + echo "❌ Some tests failed:" + echo "Unit tests: ${{ needs.unit-tests.result }}" + echo "E2E tests: ${{ needs.e2e-tests.result }}" + echo "Build check: ${{ needs.build-check.result }}" + exit 1 + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index 98a70e4..b9ac885 100644 --- a/.gitignore +++ b/.gitignore @@ -1,86 +1,45 @@ -# Dependencies -node_modules/ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* +.pnpm-debug.log* -# Environment variables +# local env files .env .env.local .env.development.local .env.test.local .env.production.local -# Next.js -.next/ -out/ -build/ -dist/ - -# Production -/build - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Coverage directory used by tools like istanbul -coverage/ -*.lcov - -# nyc test coverage -.nyc_output - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript -*.tsbuildinfo -next-env.d.ts - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Storybook build outputs -.out -.storybook-out -storybook-static - -# Temporary folders -tmp/ -temp/ - -# Logs -logs -*.log - -# Database -*.db -*.sqlite - -# macOS -.DS_Store - -# Windows -Thumbs.db -ehthumbs.db -Desktop.ini +# vercel +.vercel +.env*.local -# VS Code -.vscode/ +.codegpt -# IDEs -.idea/ -*.swp -*.swo -*~ -# Vercel -.vercel \ No newline at end of file +# test artifacts +/test-results/ +/playwright-report/ +/tsconfig.tsbuildinfo \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..4008bdd --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "yoavbls.pretty-ts-errors", + "bradlc.vscode-tailwindcss", + "github.vscode-github-actions" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ee3bdd7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug full stack", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "serverReadyAction": { + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "action": "debugWithChrome" + } + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index ea6da48..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,151 +0,0 @@ -# Contributing to codac - -Thank you for your interest in contributing to **codac**! πŸŽ‰ We welcome contributions from developers of all skill levels, especially beginners. - -## πŸš€ Getting Started - -### Prerequisites - -Before you begin, make sure you have: - -- **Node.js** (v20 or higher) - [Download here](https://nodejs.org/) -- **pnpm** (recommended) - Install with `npm install -g pnpm` -- **Git** - [Download here](https://git-scm.com/) -- A **GitHub account** - -### Setting Up Your Development Environment - -1. **Fork the repository** - - - Click the "Fork" button at the top right of the GitHub page - - This creates your own copy of the project - -2. **Clone your fork** - - ```bash - git clone https://github.com/YOUR-USERNAME/codac.git - cd codac - ``` - -3. **Install dependencies** - - ```bash - pnpm install - ``` - -4. **Start the development server** - - ```bash - pnpm dev - ``` - -5. **Open your browser** - - Navigate to `http://localhost:3000` - - You should see the codac application running! - -## 🀝 How to Contribute - -### For Beginners - -If you're new to open source, here are some great ways to start: - -1. **Documentation improvements** - Fix typos, improve explanations, add examples -2. **Bug reports** - Found something that doesn't work? Let us know! -3. **Feature requests** - Have an idea? Open an issue to discuss it -4. **Good first issues** - Look for issues labeled `good first issue` - -### Making Changes - -1. **Create a new branch** - - ```bash - git checkout -b feature/your-feature-name - # or - git checkout -b fix/your-bug-fix - ``` - -2. **Make your changes** - - - Write clear, readable code - - Follow the existing code style - - Add comments where necessary - -3. **Test your changes** - - ```bash - pnpm lint # Check code style - pnpm build # Ensure it builds - ``` - -4. **Commit your changes** - - ```bash - git add . - git commit -m "Add: brief description of your changes" - ``` - -5. **Push to your fork** - - ```bash - git push origin feature/your-feature-name - ``` - -6. **Create a Pull Request** - - Go to your fork on GitHub - - Click "New Pull Request" - - Fill out the PR template - -## πŸ“ Commit Message Guidelines - -Use clear, descriptive commit messages: - -- `Add: new feature description` -- `Fix: bug description` -- `Update: what you updated` -- `Remove: what you removed` - -Examples: - -- `Add: user profile component` -- `Fix: login form validation` -- `Update: README installation instructions` - -## πŸ› Reporting Bugs - -Found a bug? Help us fix it! - -1. **Check existing issues** - Someone might have already reported it -2. **Create a new issue** using the bug report template -3. **Include details**: - - What you expected to happen - - What actually happened - - Steps to reproduce the bug - - Your operating system and browser - -## πŸ’‘ Suggesting Features - -Have an idea for codac? - -1. **Check existing issues** - Maybe someone already suggested it -2. **Create a new issue** using the feature request template -3. **Describe your idea** clearly and explain why it would be useful - -## ❓ Need Help? - -Don't hesitate to ask for help! You can: - -- **Open an issue** with the `question` label -- **Join our discussions** in the GitHub Discussions tab -- **Comment on existing issues** if you need clarification - -## πŸŽ‰ Recognition - -All contributors will be recognized in our README! Your contributions, no matter how small, are valuable and appreciated. - -## πŸ“œ Code of Conduct - -Please be respectful and inclusive. We want everyone to feel welcome in our community. See our [Code of Conduct](CODE_OF_CONDUCT.md) for details. - ---- - -**Happy coding!** πŸš€ diff --git a/README.md b/README.md index 76f9d58..dbab70c 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,143 @@ # codac -**codac** is the comprehensive learning management system and community platform designed specifically for students and alumni of Code Academy Berlin. codac facilitates learning, collaboration, and community building among current students and graduates. +**codac** is the comprehensive learning management system and community platform designed specifically for students and alumni of Code Academy Berlin. Beyond facilitating learning and community building, codac serves as a practical training ground for beginner developers to learn open source development practices and contribute to real-world projects. ## 🎯 Mission -codac empowers Code Academy Berlin students and alumni to learn, collaborate, and grow together through a modern, integrated platform that combines educational content delivery with vibrant community features. +codac empowers Code Academy Berlin students and alumni to learn, collaborate, and grow together through a modern, integrated platform that combines educational content delivery with vibrant community features. **Additionally, codac provides hands-on experience in open source development**, teaching students how to contribute to collaborative projects, use version control effectively, and participate in the global developer community. -## ✨ Key Features to be implemented +## πŸ“‹ Table of Contents + +- [πŸŽ“ Learning Open Source Development](#-learning-open-source-development) +- [βœ… Implemented Features](#-implemented-features) +- [🚧 Work in Progress](#-work-in-progress) +- [πŸ—ΊοΈ Roadmap](#️-roadmap) +- [πŸ›  Tech Stack](#-tech-stack) +- [πŸš€ Quick Start](#-quick-start) +- [🀝 Contributing](#-contributing) +- [πŸ”„ How to Fork & Contribute](#-how-to-fork--contribute) + +## πŸŽ“ Learning Open Source Development + +codac is designed to be a beginner-friendly project where students can learn essential open source development skills: + +### What You'll Learn + +- **Git & GitHub Workflow** - Master version control, branching, and collaboration +- **Code Review Process** - Learn how to give and receive constructive feedback +- **Issue Management** - Understand how open source projects are organized and tracked +- **Documentation** - Practice writing clear, helpful documentation +- **Testing** - Learn to write and run tests for your contributions +- **Community Collaboration** - Experience working with a diverse group of developers + +### Why codac is Perfect for Learning + +- **Real-world codebase** with modern technologies and best practices +- **Welcoming community** that supports beginners and celebrates learning +- **Well-documented issues** with clear requirements and guidance +- **Mentorship opportunities** from experienced contributors +- **Gradual complexity** - start small and work up to larger features +- **Educational mission** - everyone understands you're here to learn + +### Getting Started with Open Source + +Never contributed to open source before? That's exactly why we're here! Check out our [detailed contribution guide](#-how-to-fork--contribute) below to learn the complete workflow step by step. + +## βœ… Implemented Features + +### πŸ—οΈ Foundation & Infrastructure + +- **Project Structure** - Next.js 15 with App Router, TypeScript, and modern tooling +- **Authentication System** - NextAuth.js integration with email/password and database sessions +- **Database Schema** - Complete Drizzle ORM schema with PostgreSQL for all domain entities +- **Admin Dashboard** - Basic admin interface with metrics, activity feed, and quick actions +- **Rich Content Editor** - Plate.js integration for creating and editing educational content +- **UI Components** - Shadcn/UI component library with Tailwind CSS styling +- **Navigation Structure** - Sidebar navigation with role-based menu items +- **TypeScript Types** - Comprehensive type definitions for all domain objects +- **Testing Setup** - Jest for unit testing and Playwright for end-to-end testing +- **Code Quality** - Biome for formatting and linting with modern standards + +### πŸ‘€ User Management (Basic) + +- **User Roles** - Database support for Students, Alumni, Mentors, Instructors, and Admins +- **User Profiles** - Database schema for comprehensive user profiles with social links +- **Authentication** - Secure login/logout with session management + +### πŸ“Š Database Foundation + +- **Cohort Management** - Database tables for organizing student groups +- **Course Structure** - Database schema for courses, lessons, and educational content +- **Assignment System** - Database tables for assignments, submissions, and grading +- **Community Posts** - Database schema for posts, comments, and discussions +- **Achievement System** - Database tables for badges, points, and gamification +- **Mentorship Framework** - Database schema for mentor-mentee relationships + +## 🚧 Work in Progress ### πŸ“š Learning Management System -- **Rich Content Editor** - for creating engaging educational content with advanced formatting, media embedding, and collaborative editing capabilities -- **Document Management** - Comprehensive document creation, editing, and sharing system with version control and real-time collaboration -- **Course Structure** - Organized learning paths with projects, lessons, and assignments -- **Progress Tracking** - Detailed analytics on learning progress, completion rates, and time spent on various activities -- **Assignment System** - Create, submit, and grade assignments with integrated feedback mechanisms -- **Resource Library** - Centralized repository of learning materials, code examples, and references +- **Course Creation** - Interface for instructors to create and manage courses +- **Assignment Management** - Tools for creating, distributing, and grading assignments +- **Progress Tracking** - Student dashboard showing learning progress and completion rates +- **Resource Library** - File upload and management for learning materials ### πŸ‘₯ Community Platform -- **Cohort Management** - Organized student groups with dedicated spaces for each cohort -- **Student Directory** - Browse and connect with current students, alumni, and mentors -- **User Profiles** - Comprehensive profiles with professional information, social links, and achievements -- **Role-Based Access** - Different permissions and features for Students, Alumni, Mentors, Instructors, and Admins -- **Community Posts** - Share updates, ask questions, and engage with the community -- **Discussion System** - Threaded discussions with comments and likes +- **User Directory** - Browse and search functionality for students and alumni +- **Discussion System** - Threaded discussions with real-time updates +- **Community Posts** - Create, edit, and interact with community content +- **Profile Management** - Complete user profile editing and customization + +### πŸ” Role-Based Features + +- **Instructor Tools** - Course management and student progress monitoring +- **Student Dashboard** - Personalized learning dashboard with assignments and progress +- **Admin Panel** - Complete administration tools for managing users, courses, and content + +## πŸ—ΊοΈ Roadmap ### 🀝 Mentorship & Career Support -- **Mentor Matching** - Connect current students with successful alumni -- **Job Board** - Alumni and partner companies share job opportunities (planned) -- **Career Resources** - Interview preparation, resume building, and career guidance (planned) -- **Portfolio Management** - Showcase your work and track career progress +- **Mentor Matching Algorithm** - AI-powered matching between students and alumni mentors +- **Job Board Integration** - Partner company job postings and application tracking +- **Career Resources** - Interview preparation materials and resume building tools +- **Portfolio Management** - Showcase student work and track career progression +- **Networking Events** - Event management and RSVP system for community meetups + +### πŸ† Advanced Features -### πŸ† Gamification & Engagement +- **Gamification Engine** - Points, leaderboards, and achievement notifications +- **Learning Analytics** - Advanced insights into learning patterns and performance +- **Mobile Application** - React Native app for iOS and Android +- **Video Integration** - Live streaming and recorded video lessons +- **Certificate System** - Digital certificates and blockchain verification +- **API Integration** - Third-party integrations (GitHub, LinkedIn, Slack) -- **Achievement System** - Earn badges and points for various accomplishments -- **Progress Tracking** - Visual progress indicators for courses and projects -- **Community Points** - Reward helpful community participation -- **Learning Analytics** - Detailed insights into learning patterns and performance +### 🌐 Platform Enhancements -...more features +- **Multi-language Support** - Internationalization for German and English +- **Advanced Search** - Full-text search across all content types +- **Notification System** - Real-time notifications via email, push, and in-app +- **Advanced Permissions** - Fine-grained access control and content permissions +- **Export Tools** - Data export for portfolios and academic records +- **Advanced Analytics** - Comprehensive reporting and data visualization ## πŸ›  Tech Stack - **[Next.js 15](https://nextjs.org/)** for performance and scalability. - **[Auth.js](https://auth.js.org/)** for authentication. - **[Drizzle ORM](https://orm.drizzle.team/)** for type-safe database management. -- **[SQLite](https://www.sqlite.org/)** for lightweight, serverless database management. +- **[PostgreSQL](https://www.postgresql.org/)** for reliable and scalable database solutions. - **[Shadcn/UI](https://ui.shadcn.com/)** for beautiful, customizable components. - **[Biome](https://biomejs.dev/)** for **Formatting and Linting** - **[Jest](https://jestjs.io/)** A testing framework for ensuring your app works as expected. +- **Playwright** End to end A testing framework for ensuring your app works as expected. - **React Hook Form**: A library for managing form state and validation in React applications - **Zod**: A TypeScript-first schema declaration and validation library -- **Zustand**: Simple state management - **Nuqs**: Type-safe URL state management +- **[Platejs](https://platejs.org) Rich Content Editor** - for creating engaging educational content with advanced formatting, media embedding, and collaborative editing capabilities +- **Document Management** - Comprehensive document creation, editing, and sharing system with version control and real-time collaboration ## πŸš€ Quick Start @@ -63,15 +146,16 @@ codac empowers Code Academy Berlin students and alumni to learn, collaborate, an Make sure you have the following installed on your system: - **Node.js** (v20 or higher) - [Download here](https://nodejs.org/) -- **pnpm** (recommended) or npm - Install with `npm install -g pnpm` +- **pnpm** - Install with `npm install -g pnpm` - **Git** - [Download here](https://git-scm.com/) +- **PostgreSQL** - [Download here](https://www.postgresql.org/download/) ### Installation 1. **Clone the repository** ```bash - git clone https://github.com/your-username/codac.git + git clone https://github.com/CodeAcademyBerlin/codac.git cd codac ``` @@ -87,48 +171,258 @@ Make sure you have the following installed on your system: cp .env.example .env.local ``` - Then edit `.env.local` with your configuration. + Then edit `.env.local` with your configuration: + ``` + # Database + POSTGRES_URL="postgresql://username:password@localhost:5432/codac" + + # NextAuth + NEXTAUTH_SECRET="your-nextauth-secret" + NEXTAUTH_URL="http://localhost:3000" + ``` -4. **Start the development server** +4. **Set up the database** + + ```bash + pnpm db:push + ``` + +5. **Start the development server** ```bash pnpm dev ``` -5. **Open your browser** +6. **Open your browser** Navigate to `http://localhost:3000` to see the application running! ## 🀝 Contributing -We welcome contributions from developers of all skill levels! Whether you're a beginner or experienced developer, there are many ways to help improve codac. +We welcome contributions from developers of all skill levels! Whether you're a complete beginner or experienced developer, there are many ways to help improve codac while learning valuable skills. ### For Beginners πŸ‘‹ -New to open source? No problem! Here are some great ways to get started: +**First time contributing to open source?** You're in the right place! codac is specifically designed to help you learn: + +- **πŸ“ Start with documentation** - Fix typos, improve explanations, or add examples +- **πŸ› Report bugs** - Found something that doesn't work? Create an issue! +- **πŸ’‘ Suggest features** - Have ideas for improvements? We'd love to hear them! +- **πŸ” Review code** - Read through pull requests and ask questions +- **❓ Ask questions** - Use GitHub Discussions or issues - no question is too basic! + +### Learning Path for New Contributors + +1. **Week 1-2**: Read the codebase, set it up locally, report any issues you encounter +2. **Week 3-4**: Fix documentation, typos, or small UI improvements +3. **Week 5-8**: Take on "good first issue" labeled tasks +4. **Month 2+**: Contribute features, review others' code, help newer contributors + +## πŸ”„ How to Fork & Contribute + +This section provides a complete, step-by-step guide for contributing to codac. Perfect for beginners learning open source development! + +### Step 1: Fork the Repository + +**Forking creates your own copy of the project where you can make changes safely.** + +1. **Go to the codac repository**: Navigate to `https://github.com/CodeAcademyBerlin/codac` +2. **Click the "Fork" button**: Located in the top-right corner of the page +3. **Choose your account**: Select where you want to fork the repository (usually your personal GitHub account) +4. **Wait for the fork**: GitHub will create a copy of the repository in your account + +**What just happened?** You now have your own copy of codac at `https://github.com/YOUR-USERNAME/codac` + +### Step 2: Clone Your Fork + +**Cloning downloads your fork to your computer so you can work on it.** + +```bash +# Replace YOUR-USERNAME with your actual GitHub username +git clone https://github.com/YOUR-USERNAME/codac.git +cd codac +``` + +### Step 3: Add the Original Repository as "Upstream" + +**This lets you get updates from the main project.** + +```bash +git remote add upstream https://github.com/CodeAcademyBerlin/codac.git +git remote -v # Verify you have both 'origin' (your fork) and 'upstream' (original) +``` -- **πŸ“ Fix typos or improve documentation** - Every fix helps! -- **πŸ› Report bugs** - Found something that doesn't work? Let us know! -- **πŸ’‘ Suggest features** - Have ideas? We'd love to hear them! -- **❓ Ask questions** - Don't hesitate to ask for help or clarification +### Step 4: Create a Branch for Your Changes -### Quick Contributing Guide +**Never work directly on the main branch! Always create a feature branch.** -1. **Fork the repository** - Click the "Fork" button on GitHub -2. **Create a branch** - `git checkout -b feature/your-feature-name` -3. **Make your changes** - Write your code and add comments -4. **Test your changes** - Make sure everything works -5. **Submit a pull request** - We'll review it together! +```bash +# Get the latest changes from the main project +git fetch upstream +git checkout main +git merge upstream/main -πŸ“– **Read our full [Contributing Guide](CONTRIBUTING.md)** for detailed instructions. +# Create and switch to a new branch +git checkout -b feature/your-feature-name +``` + +**Branch naming conventions:** +- `feature/add-user-profile` - for new features +- `fix/login-bug` - for bug fixes +- `docs/update-readme` - for documentation +- `refactor/cleanup-components` - for code improvements + +### Step 5: Make Your Changes + +1. **Set up the development environment** (follow the [Quick Start](#-quick-start) guide) +2. **Make your changes** - edit files, add features, fix bugs +3. **Test your changes** - make sure everything still works +4. **Follow the code style** - we use Biome for formatting + +```bash +# Format your code +pnpm format + +# Lint your code +pnpm lint + +# Run tests +pnpm test +``` + +### Step 6: Commit Your Changes + +**Write clear commit messages that explain what you did.** + +```bash +# Add your changes to staging +git add . + +# Commit with a descriptive message +git commit -m "Add user profile editing functionality + +- Add ProfileForm component with validation +- Update user API endpoint to handle PATCH requests +- Add tests for profile update functionality +- Update documentation for new feature" +``` + +**Good commit message format:** +- **First line**: Brief summary (50 characters or less) +- **Blank line** +- **Description**: Explain what and why, not how (if needed) + +### Step 7: Push Your Branch + +**Upload your changes to your fork on GitHub.** + +```bash +git push origin feature/your-feature-name +``` + +### Step 8: Create a Pull Request + +**A pull request asks the maintainers to review and merge your changes.** + +1. **Go to your fork on GitHub**: `https://github.com/YOUR-USERNAME/codac` +2. **Click "Compare & pull request"**: GitHub usually shows this button automatically +3. **Fill out the PR template**: + - **Title**: Clear, descriptive summary of your changes + - **Description**: Explain what you did, why, and how to test it + - **Link any related issues**: Use "Closes #123" if your PR fixes an issue +4. **Click "Create pull request"** + +### Step 9: Respond to Feedback + +**Code review is a collaborative process - don't take feedback personally!** + +- **Address comments promptly**: Make requested changes or ask for clarification +- **Update your branch**: Push new commits to address feedback +- **Engage in discussion**: Ask questions, explain your approach, learn from others + +```bash +# Make changes based on feedback +git add . +git commit -m "Address code review feedback + +- Extract validation logic into separate function +- Add error handling for edge cases +- Update tests to cover new scenarios" +git push origin feature/your-feature-name +``` + +### Step 10: Keep Your Fork Updated + +**Regularly sync with the main project to avoid conflicts.** + +```bash +# Switch to main branch +git checkout main + +# Pull latest changes from upstream +git fetch upstream +git merge upstream/main + +# Push updates to your fork +git push origin main +``` + +### πŸŽ‰ Congratulations! + +You've just learned the complete open source contribution workflow! This process is used by millions of developers worldwide for collaborating on software projects. + +### Common Git Commands Cheat Sheet + +```bash +# Check status of your changes +git status + +# See what files have changed +git diff + +# View commit history +git log --oneline + +# Switch between branches +git checkout branch-name + +# Create and switch to new branch +git checkout -b new-branch-name + +# Undo unstaged changes +git checkout -- filename + +# Undo last commit (keep changes) +git reset --soft HEAD~1 +``` + +### Getting Help + +- **GitHub Discussions**: Ask questions about contributing +- **Issues**: Report bugs or request features +- **Discord/Slack**: Real-time chat with the community +- **Documentation**: Check our [Contributing Guide](./docs/dev/contributing.md) + +Remember: **Everyone was a beginner once!** The codac community is here to help you learn and grow as a developer. ### Good First Issues Look for issues labeled `good first issue` - these are perfect for newcomers! They're typically: -- Well-documented with clear requirements -- Small in scope and easier to tackle -- Great learning opportunities -- Mentored by experienced contributors +- **Well-documented** with clear requirements and context +- **Small in scope** and easier to tackle (usually 1-3 hours of work) +- **Great learning opportunities** that teach important concepts +- **Mentored** by experienced contributors who will guide you + +### Types of Contributions We Need + +- **πŸ› Bug fixes**: Solve problems and improve user experience +- **✨ New features**: Add functionality that users have requested +- **πŸ“š Documentation**: Help others understand the codebase +- **🎨 UI/UX improvements**: Make the interface more beautiful and usable +- **πŸ§ͺ Tests**: Improve code reliability and prevent regressions +- **♻️ Refactoring**: Clean up code while maintaining functionality +- **🌐 Accessibility**: Make the app usable for everyone +- **πŸ“± Responsive design**: Ensure the app works on all devices ## 🌟 Community @@ -144,7 +438,7 @@ All contributors are recognized in our project! No matter how small your contrib ## πŸ“œ Code of Conduct -We are committed to providing a welcoming and inclusive environment for all contributors. Please read our [Code of Conduct](CODE_OF_CONDUCT.md) to understand our community standards. +We are committed to providing a welcoming and inclusive environment for all contributors. Please read our [Code of Conduct](./docs/dev/CODE_OF_CONDUCT.md) to understand our community standards. ## πŸ“„ License diff --git a/__tests__/login.test.tsx b/__tests__/login.test.tsx new file mode 100644 index 0000000..a391705 --- /dev/null +++ b/__tests__/login.test.tsx @@ -0,0 +1,100 @@ +import { toast } from '@/hooks/use-toast' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { signIn } from 'next-auth/react' +import React from 'react' // Ensures React is in scope when JSX is used +import LoginForm from '../components/login-form' + +// Mock the signIn function from next-auth +jest.mock('next-auth/react', () => ({ + signIn: jest.fn(), +})) + +// Mock the toast function +jest.mock('@/hooks/use-toast', () => ({ + toast: jest.fn(), +})) + +// Test case +test('renders login form and handles input', async () => { + render() + + // Simulate user entering email and password + const emailInput = screen.getByLabelText(/email/i) as HTMLInputElement + const passwordInput = screen.getByLabelText(/password/i) as HTMLInputElement + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(passwordInput, { target: { value: 'password123' } }) + + // Verify that the values are correctly entered + expect(emailInput.value).toBe('test@example.com') + expect(passwordInput.value).toBe('password123') + + // Simulate form submission by clicking the submit button + const buttons = screen.getAllByRole('button') + const loginButton = buttons.find((button) => button.textContent === 'Login') + + if (!loginButton) { + throw new Error('Login button not found') + } + + fireEvent.click(loginButton) + + // Check if signIn function is called with the correct parameters + await waitFor(() => { + expect(signIn).toHaveBeenCalledWith('credentials', { + redirect: false, + email: 'test@example.com', + password: 'password123', + }) + }) +}) + +test('displays error message on failed login', async () => { + // Mock signIn to simulate a failure + ;(signIn as jest.Mock).mockResolvedValueOnce({ error: 'Invalid credentials' }) + + render() + + // Simulate user entering email and password + const emailInput = screen.getByLabelText(/email/i) as HTMLInputElement + const passwordInput = screen.getByLabelText(/password/i) as HTMLInputElement + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }) + fireEvent.change(passwordInput, { target: { value: 'password123' } }) + + // Simulate form submission by clicking the submit button + const buttons = screen.getAllByRole('button') + const loginButton = buttons.find((button) => button.textContent === 'Login') + + if (!loginButton) { + throw new Error('Login button not found') + } + + fireEvent.click(loginButton) + + // Wait for the toast to be called + await waitFor(() => { + expect(toast).toHaveBeenCalledWith({ + variant: 'destructive', + title: 'Uh oh! Something went wrong.', + // description: "Invalid credentials", + }) + }) +}) + +test('triggers Google login on button click', async () => { + render() + + // Find the "Login with Google" button + const googleLoginButton = screen.getByRole('button', { name: /login with google/i }) + + // Simulate a click on the Google login button + fireEvent.click(googleLoginButton) + + // Verify that the signIn function is called with "google" as the provider + await waitFor(() => { + expect(signIn).toHaveBeenCalledWith('google', { + redirectTo: '/', + }) + }) +}) diff --git a/actions/auth.ts b/actions/auth.ts new file mode 100644 index 0000000..0dd2ba1 --- /dev/null +++ b/actions/auth.ts @@ -0,0 +1,20 @@ +'use server' + +import { createUser, getUser } from '@/app/db' +import { redirect } from 'next/navigation' + +interface RegisterData { + email: string + password: string +} + +export async function register(data: RegisterData) { + const user = await getUser(data.email) + + if (user.length > 0) { + return { message: 'A user with this identifier already exists' } + } + + await createUser(data.email, data.password) + redirect('/login') +} diff --git a/actions/user.ts b/actions/user.ts new file mode 100644 index 0000000..c6fbe37 --- /dev/null +++ b/actions/user.ts @@ -0,0 +1,19 @@ +'use server' + +import { db } from '@/app/db' +import { users } from '@/db/schema' +import { eq } from 'drizzle-orm' +import { revalidatePath } from 'next/cache' + +export async function updateUserProfile( + userId: string, + data: { + name?: string + email?: string + image?: string + } +) { + await db.update(users).set(data).where(eq(users.id, userId)) + + revalidatePath('/profile') +} diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx new file mode 100644 index 0000000..268d45c --- /dev/null +++ b/app/(admin)/layout.tsx @@ -0,0 +1,31 @@ +import { SidebarProvider } from '@/components/ui/sidebar' +import '@/app/globals.css' + +import { AppSidebar } from '@/components/app-sidebar' +import Header from '@/components/header' + +const title = 'Admin Page' +const description = '' + +export const metadata = { + title, + description, + twitter: { + card: 'summary_large_image', + title, + description, + }, + metadataBase: new URL('https://nextjs-postgres-auth.vercel.app'), +} + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+
+ {children} +
+
+ ) +} diff --git a/app/(admin)/page.tsx b/app/(admin)/page.tsx new file mode 100644 index 0000000..20866bb --- /dev/null +++ b/app/(admin)/page.tsx @@ -0,0 +1,132 @@ +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { BookOpen, GraduationCap, TrendingUp, Users } from 'lucide-react' +import * as React from 'react' + +export default function AdminDashboard() { + return ( +
+
+
+
+
+ +

codac Admin Dashboard

+
+
+
+
+ +
+
+ + + Total Students + + + +
1,234
+

+12% from last month

+
+
+ + + + Active Courses + + + +
24
+

+3 new this month

+
+
+ + + + Alumni + + + +
542
+

+8% graduation rate

+
+
+ + + + Completion Rate + + + +
87%
+

+5% from last cohort

+
+
+
+ +
+ + + Recent Activity + Latest updates from the codac platform + + +
+
+
+
+

New student enrolled

+

2 minutes ago

+
+
+
+
+
+

+ Course "React Fundamentals" updated +

+

1 hour ago

+
+
+
+
+
+

Assignment submission pending review

+

3 hours ago

+
+
+
+ + + + + + Quick Actions + Common administrative tasks + + +
+ + + + +
+
+
+
+
+
+ ) +} diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..1b2a620 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1 @@ +export { GET, POST } from 'app/auth' diff --git a/app/api/user/route.ts b/app/api/user/route.ts new file mode 100644 index 0000000..9a9263d --- /dev/null +++ b/app/api/user/route.ts @@ -0,0 +1,4 @@ +export async function POST(request: Request) { + const res = await request.json() + return Response.json({ res }) +} diff --git a/app/auth.config.ts b/app/auth.config.ts new file mode 100644 index 0000000..785b525 --- /dev/null +++ b/app/auth.config.ts @@ -0,0 +1,48 @@ +import { DrizzleAdapter } from '@auth/drizzle-adapter' +import type { NextAuthConfig } from 'next-auth' +import { db } from './db' + +export const authConfig = { + pages: { + signIn: '/login', + }, + adapter: DrizzleAdapter(db), + providers: [ + // added later in auth.ts since it requires bcrypt which is only compatible with Node.js + // while this file is also used in non-Node.js environments + ], + session: { strategy: 'jwt' }, + trustHost: true, // Trust all hosts in development + callbacks: { + authorized({ auth, request: { nextUrl } }) { + const isLoggedIn = !!auth?.user + const isLoginPage = nextUrl.pathname.startsWith('/login') + const isRegisterPage = nextUrl.pathname.startsWith('/register') + + if (!isLoggedIn && !(isLoginPage || isRegisterPage)) { + return false + } + return true + }, + async signIn({ account, profile }) { + // Optional: Restrict Google sign-ins to verified emails from specific domains + if (account?.provider === 'google') { + // Uncomment and modify the domain restriction as needed + // return profile?.email_verified && profile?.email?.endsWith("@yourcompany.com") + return profile?.email_verified ?? true + } + return true // Allow other providers + }, + async session({ session, token }) { + session.user.image = token.picture + session.user.id = token.sub ?? '' + return session + }, + }, +} satisfies NextAuthConfig + +declare module 'next-auth' { + interface Session { + accessToken?: string + } +} diff --git a/app/auth.ts b/app/auth.ts new file mode 100644 index 0000000..a800db0 --- /dev/null +++ b/app/auth.ts @@ -0,0 +1,54 @@ +import NextAuth, { CredentialsSignin, type User } from 'next-auth' +import Credentials from 'next-auth/providers/credentials' + +import { compare } from 'bcrypt-ts' +import Google from 'next-auth/providers/google' + +import { authConfig } from 'app/auth.config' +import { getUser } from 'app/db' + +class InvalidLoginError extends CredentialsSignin { + code = 'Invalid identifier or password' +} + +export const { + handlers: { GET, POST }, + auth, + signIn, + signOut, +} = NextAuth({ + ...authConfig, + providers: [ + Google({ + clientId: process.env.GOOGLE_CLIENT_ID ?? '', + clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '', + allowDangerousEmailAccountLinking: true, + authorization: { + params: { + prompt: 'consent', + access_type: 'offline', + response_type: 'code', + }, + }, + }), + Credentials({ + credentials: { + email: {}, + password: {}, + }, + + async authorize(credentials: Partial>) { + const email = credentials.email as string + const password = credentials.password as string + + if (!email || !password) throw new InvalidLoginError() + + const user = await getUser(email) + if (user.length === 0 || !user[0].password) throw new InvalidLoginError() + const passwordsMatch = await compare(password, user[0].password) + if (passwordsMatch) return user[0] as User + throw new InvalidLoginError() + }, + }), + ], +}) diff --git a/app/db.ts b/app/db.ts new file mode 100644 index 0000000..4495a15 --- /dev/null +++ b/app/db.ts @@ -0,0 +1,18 @@ +import { genSaltSync, hashSync } from 'bcrypt-ts' +import { eq } from 'drizzle-orm' + +// Import the database connection and schema from db/schema.ts +import { db, users } from '../db/schema' + +export { db } + +export async function getUser(email: string) { + return await db.select().from(users).where(eq(users.email, email)) +} + +export async function createUser(email: string, password: string) { + const salt = genSaltSync(10) + const hash = hashSync(password, salt) + + return await db.insert(users).values({ email: email, password: hash }) +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..65ae045 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,132 @@ +@import "tailwindcss"; + +/* Configure class-based dark mode */ +@variant dark (&:where(.dark, .dark *)); + +@theme { + --radius: 0.5rem; + + /* Border radius scale using --radius variable */ + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 4px); + + /* Color theme using HSL values from CSS variables */ + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + --color-chart-1: hsl(var(--chart-1)); + --color-chart-2: hsl(var(--chart-2)); + --color-chart-3: hsl(var(--chart-3)); + --color-chart-4: hsl(var(--chart-4)); + --color-chart-5: hsl(var(--chart-5)); + --color-sidebar: hsl(var(--sidebar-background)); + --color-sidebar-foreground: hsl(var(--sidebar-foreground)); + --color-sidebar-primary: hsl(var(--sidebar-primary)); + --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground)); + --color-sidebar-accent: hsl(var(--sidebar-accent)); + --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground)); + --color-sidebar-border: hsl(var(--sidebar-border)); + --color-sidebar-ring: hsl(var(--sidebar-ring)); +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..788e820 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,32 @@ +import './globals.css' + +import { GeistSans } from 'geist/font/sans' + +import { Toaster } from '@/components/ui/toaster' +import { SessionProvider } from 'next-auth/react' + +const title = 'codac - Learning Management System' +const description = + 'Comprehensive learning management system and community platform designed specifically for students and alumni of Code Academy Berlin. codac facilitates learning, collaboration, and community building.' + +export const metadata = { + title, + description, + twitter: { + card: 'summary_large_image', + title, + description, + }, + metadataBase: new URL('https://codac.vercel.app'), +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + + ) +} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..692bf7a --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,9 @@ +import LoginForm from '@/components/login-form' + +export default function Login() { + return ( +
+ +
+ ) +} diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..917f851 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,32 @@ +import { auth } from '@/app/auth' +import ProfileForm from '@/components/profile-form' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +export default async function ProfilePage() { + const session = await auth() + + if (!session?.user) { + return null + } + + return ( +
+ + + My Profile + + +
+ + + {session.user.name?.[0] ?? session.user.email?.[0]} + +
+ + +
+
+
+ ) +} diff --git a/app/register/page.tsx b/app/register/page.tsx new file mode 100644 index 0000000..9015733 --- /dev/null +++ b/app/register/page.tsx @@ -0,0 +1,9 @@ +import RegisterForm from '@/components/register-form' + +export default function register() { + return ( +
+ +
+ ) +} diff --git a/app/submit-button.tsx b/app/submit-button.tsx new file mode 100644 index 0000000..b668eec --- /dev/null +++ b/app/submit-button.tsx @@ -0,0 +1,44 @@ +'use client' + +import { useFormStatus } from 'react-dom' + +export function SubmitButton({ children }: { children: React.ReactNode }) { + const { pending } = useFormStatus() + + return ( + + ) +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c0245fb --- /dev/null +++ b/biome.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": ["node_modules/**", ".next/**", "dist/**", "build/**", "coverage/**", "drizzle/**"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "useImportType": "error" + }, + "correctness": { + "useExhaustiveDependencies": "warn" + }, + "suspicious": { + "noExplicitAny": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "es5", + "semicolons": "asNeeded" + } + }, + "json": { + "formatter": { + "enabled": true + } + } +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..8c3acf5 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx new file mode 100644 index 0000000..2173c63 --- /dev/null +++ b/components/app-sidebar.tsx @@ -0,0 +1,194 @@ +'use client' + +import { BookOpen, GraduationCap, Home, Settings, TrendingUp, UserCheck, Users } from 'lucide-react' +import type * as React from 'react' + +import { NavMain } from './nav-main' +import { TeamSwitcher } from './team-switcher' +import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from './ui/sidebar' + +// Codac-specific navigation data +const data = { + user: { + name: 'Student', + email: 'student@codeacademy.berlin', + avatar: '/images/user.png', + }, + teams: [ + { + name: 'Code Academy Berlin', + logo: GraduationCap, + plan: 'Premium', + }, + ], + navMain: [ + { + title: 'Dashboard', + url: '/dashboard', + icon: Home, + isActive: true, + items: [ + { + title: 'Overview', + url: '/dashboard', + }, + { + title: 'Progress', + url: '/dashboard/progress', + }, + { + title: 'Achievements', + url: '/dashboard/achievements', + }, + ], + }, + { + title: 'Learning', + url: '/learning', + icon: BookOpen, + items: [ + { + title: 'Courses', + url: '/learning/courses', + }, + { + title: 'Assignments', + url: '/learning/assignments', + }, + { + title: 'Resources', + url: '/learning/resources', + }, + { + title: 'Schedule', + url: '/learning/schedule', + }, + ], + }, + { + title: 'Community', + url: '/community', + icon: Users, + items: [ + { + title: 'Posts', + url: '/community/posts', + }, + { + title: 'Discussions', + url: '/community/discussions', + }, + { + title: 'Student Directory', + url: '/community/directory', + }, + { + title: 'Alumni Network', + url: '/community/alumni', + }, + ], + }, + { + title: 'Mentorship', + url: '/mentorship', + icon: UserCheck, + items: [ + { + title: 'Find a Mentor', + url: '/mentorship/find', + }, + { + title: 'My Mentors', + url: '/mentorship/mentors', + }, + { + title: 'Mentoring', + url: '/mentorship/mentoring', + }, + { + title: 'Sessions', + url: '/mentorship/sessions', + }, + ], + }, + { + title: 'Career', + url: '/career', + icon: TrendingUp, + items: [ + { + title: 'Job Board', + url: '/career/jobs', + }, + { + title: 'Portfolio', + url: '/career/portfolio', + }, + { + title: 'Interview Prep', + url: '/career/interview-prep', + }, + { + title: 'Resources', + url: '/career/resources', + }, + ], + }, + { + title: 'Settings', + url: '/settings', + icon: Settings, + items: [ + { + title: 'Profile', + url: '/settings/profile', + }, + { + title: 'Notifications', + url: '/settings/notifications', + }, + { + title: 'Privacy', + url: '/settings/privacy', + }, + { + title: 'Account', + url: '/settings/account', + }, + ], + }, + ], + projects: [ + { + name: 'Full Stack Development', + url: '/projects/full-stack', + icon: BookOpen, + }, + { + name: 'Data Science', + url: '/projects/data-science', + icon: TrendingUp, + }, + { + name: 'Web Development', + url: '/projects/web-dev', + icon: GraduationCap, + }, + ], +} + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + + + + + + {/* */} + + {/* */} + + + ) +} diff --git a/components/custom-link.tsx b/components/custom-link.tsx new file mode 100644 index 0000000..524de6a --- /dev/null +++ b/components/custom-link.tsx @@ -0,0 +1,38 @@ +import { cn } from '@/lib/utils' +import { ExternalLink } from 'lucide-react' +import Link from 'next/link' + +interface CustomLinkProps extends React.LinkHTMLAttributes { + href: string +} + +const CustomLink = ({ href, children, className, ...rest }: CustomLinkProps) => { + const isInternalLink = href.startsWith('/') + const isAnchorLink = href.startsWith('#') + + if (isInternalLink || isAnchorLink) { + return ( + + {children} + + ) + } + + return ( + + {children} + + + ) +} + +export default CustomLink diff --git a/components/editor/plate-editor.tsx b/components/editor/plate-editor.tsx new file mode 100644 index 0000000..c059da5 --- /dev/null +++ b/components/editor/plate-editor.tsx @@ -0,0 +1,146 @@ +'use client' + +import { + BlockquotePlugin, + BoldPlugin, + H1Plugin, + H2Plugin, + H3Plugin, + ItalicPlugin, + UnderlinePlugin, +} from '@platejs/basic-nodes/react' +import type { Value } from 'platejs' +import { Plate, PlateContent, usePlateEditor } from 'platejs/react' +import React from 'react' + +// Initial value for the editor +const initialValue: Value = [ + { + type: 'p', + children: [{ text: 'Start typing your content here...' }], + }, +] + +interface PlateEditorProps { + value?: Value + onChange?: (value: Value) => void + placeholder?: string + readOnly?: boolean + className?: string +} + +export function PlateEditor({ + value = initialValue, + onChange, + placeholder = 'Start typing...', + readOnly = false, + className = '', +}: PlateEditorProps) { + const editor = usePlateEditor({ + plugins: [ + BoldPlugin, + ItalicPlugin, + UnderlinePlugin, + H1Plugin, + H2Plugin, + H3Plugin, + BlockquotePlugin, + ], + value, + }) + + return ( +
+ onChange(value) : undefined} + readOnly={readOnly} + > + + +
+ ) +} + +// Example usage for different codac contexts +export function CourseContentEditor({ + value, + onChange, +}: { + value?: Value + onChange?: (value: Value) => void +}) { + return ( + + ) +} + +export function AssignmentEditor({ + value, + onChange, +}: { + value?: Value + onChange?: (value: Value) => void +}) { + return ( + + ) +} + +export function PostEditor({ + value, + onChange, +}: { + value?: Value + onChange?: (value: Value) => void +}) { + return ( + + ) +} + +export function CommentEditor({ + value, + onChange, +}: { + value?: Value + onChange?: (value: Value) => void +}) { + const simpleInitialValue: Value = [ + { + type: 'p', + children: [{ text: '' }], + }, + ] + + const editor = usePlateEditor({ + plugins: [BoldPlugin, ItalicPlugin], // Simplified for comments + value: value || simpleInitialValue, + }) + + return ( +
+ onChange(value) : undefined}> + + +
+ ) +} diff --git a/components/header.tsx b/components/header.tsx new file mode 100644 index 0000000..cfed9db --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,21 @@ +import { SessionProvider } from 'next-auth/react' +// import { auth } from '@/app/auth'; +import { Search } from './search' +import { SidebarTrigger } from './ui/sidebar' +import { UserNav } from './user-nav' + +export default function Header() { + return ( +
+
+ +
+ + + + +
+
+
+ ) +} diff --git a/components/login-form.tsx b/components/login-form.tsx new file mode 100644 index 0000000..ea7a678 --- /dev/null +++ b/components/login-form.tsx @@ -0,0 +1,130 @@ +'use client' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { zodResolver } from '@hookform/resolvers/zod' +import { signIn } from 'next-auth/react' +import Link from 'next/link' +import { useForm } from 'react-hook-form' +import { z } from 'zod' + +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { toast } from '@/hooks/use-toast' + +const FormSchema = z.object({ + email: z.string().email(), + password: z.string().min(6), +}) + +interface AuthError { + code: string + message?: string +} + +export default function LoginForm() { + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + email: '', + password: '', + }, + }) + + async function onSubmit(data: z.infer) { + const res = await signIn('credentials', { + redirect: false, + email: data.email, + password: data.password, + }) + if (res?.error) { + toast({ + variant: 'destructive', + title: 'Uh oh! Something went wrong.', + description: (res as AuthError).code, + }) + form.setError('password', { type: 'manual', message: (res as AuthError).code }) + } else { + window.location.href = '/' + } + } + + return ( + + + Login + Enter your email below to login to your account + + +
+ +
+ ( + + Email + + + + + + )} + /> + ( + +
+ Password + {/* + Forgot your password? + */} +
+ + + + + +
+ )} + /> + + + +
+
+ + +
+ Don't have an account?{' '} + + Sign up + +
+
+
+ ) +} diff --git a/components/nav-main.tsx b/components/nav-main.tsx new file mode 100644 index 0000000..cf6453a --- /dev/null +++ b/components/nav-main.tsx @@ -0,0 +1,69 @@ +'use client' + +import { ChevronRight, type LucideIcon } from 'lucide-react' + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible' +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from './ui/sidebar' + +export function NavMain({ + items, +}: { + items: { + title: string + url: string + icon?: LucideIcon + isActive?: boolean + items?: { + title: string + url: string + }[] + }[] +}) { + return ( + + Platform + + {items.map((item) => ( + + + + + {item.icon && } + {item.title} + + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + + ))} + + + ) +} diff --git a/components/profile-form.tsx b/components/profile-form.tsx new file mode 100644 index 0000000..a011702 --- /dev/null +++ b/components/profile-form.tsx @@ -0,0 +1,99 @@ +'use client' + +import { updateUserProfile } from '@/actions/user' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { useToast } from '@/hooks/use-toast' +import { zodResolver } from '@hookform/resolvers/zod' +import type { User } from 'next-auth' +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import * as z from 'zod' + +const profileFormSchema = z.object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + email: z.string().email('Invalid email address'), + image: z.string().optional(), +}) + +type ProfileFormValues = z.infer + +export default function ProfileForm({ user }: { user: User }) { + const { toast } = useToast() + const [isLoading, setIsLoading] = useState(false) + + const form = useForm({ + resolver: zodResolver(profileFormSchema), + defaultValues: { + name: user.name || '', + email: user.email || '', + }, + }) + + async function onSubmit(data: ProfileFormValues) { + setIsLoading(true) + try { + if (!user.id) throw new Error('User ID is required') + await updateUserProfile(user.id, data) + + toast({ + title: 'Success', + description: 'Profile updated successfully', + }) + } catch (error) { + console.log(error) + toast({ + title: 'Error', + description: 'Failed to update profile', + variant: 'destructive', + }) + } + setIsLoading(false) + } + + return ( +
+ + ( + + Name + + + + + + )} + /> + + ( + + Email + + + + + + )} + /> + + + + + ) +} diff --git a/components/providers/session-provider.tsx b/components/providers/session-provider.tsx new file mode 100644 index 0000000..5f01d21 --- /dev/null +++ b/components/providers/session-provider.tsx @@ -0,0 +1,14 @@ +'use client' + +import type { Session } from 'next-auth' +import { SessionProvider } from 'next-auth/react' + +export default function AuthProvider({ + children, + session, +}: { + children: React.ReactNode + session: Session | null +}) { + return {children} +} diff --git a/components/register-form.tsx b/components/register-form.tsx new file mode 100644 index 0000000..4de446f --- /dev/null +++ b/components/register-form.tsx @@ -0,0 +1,133 @@ +'use client' + +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card' +// import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input' +import Link from 'next/link' + +import { register } from '@/actions/auth' +import { zodResolver } from '@hookform/resolvers/zod' +import { useRouter } from 'next/navigation' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form' + +const formSchema = z + .object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + email: z.string().email('Invalid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), + confirmPassword: z.string().min(6, 'Password must be at least 6 characters'), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords must match', + path: ['confirmPassword'], + }) + +export default function RegisterForm() { + const router = useRouter() + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + email: '', + password: '', + confirmPassword: '', + }, + }) + + async function onSubmit(values: z.infer) { + const res = await register(values) + if (res.message !== 'success') { + form.setError('email', { type: 'manual', message: res.message }) + } else { + router.push('/login') + } + } + + return ( + + + Create Account + Create your account to get started + + +
+ + ( + + Name + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + ( + + Confirm Password + + + + + + )} + /> + + + +
+ +

+ Already have an account?{' '} + + Sign in + +

+
+
+ ) +} diff --git a/components/search.tsx b/components/search.tsx new file mode 100644 index 0000000..9c15d42 --- /dev/null +++ b/components/search.tsx @@ -0,0 +1,9 @@ +import { Input } from './ui/input' + +export function Search() { + return ( +
+ +
+ ) +} diff --git a/components/team-switcher.tsx b/components/team-switcher.tsx new file mode 100644 index 0000000..485191a --- /dev/null +++ b/components/team-switcher.tsx @@ -0,0 +1,80 @@ +'use client' + +import { ChevronsUpDown, Plus } from 'lucide-react' +import * as React from 'react' + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from './ui/dropdown-menu' +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './ui/sidebar' + +export function TeamSwitcher({ + teams, +}: { + teams: { + name: string + logo: React.ElementType + plan: string + }[] +}) { + const { isMobile } = useSidebar() + const [activeTeam, setActiveTeam] = React.useState(teams[0]) + + return ( + + + + + +
+ +
+
+ {activeTeam.name} + {activeTeam.plan} +
+ +
+
+ + Teams + {teams.map((team, index) => ( + setActiveTeam(team)} + className="gap-2 p-2" + > +
+ +
+ {team.name} + ⌘{index + 1} +
+ ))} + + +
+ +
+
Add team
+
+
+
+
+
+ ) +} diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..f009937 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,47 @@ +'use client' + +import * as AvatarPrimitive from '@radix-ui/react-avatar' +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..994ff7b --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,50 @@ +import { Slot } from '@radix-ui/react-slot' +import { type VariantProps, cva } from 'class-variance-authority' +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + outline: + 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..5581f36 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx new file mode 100644 index 0000000..7cee61e --- /dev/null +++ b/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0dd189a --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,184 @@ +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { Check, ChevronRight, Circle } from 'lucide-react' +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0', + inset && 'pl-8', + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..46a4608 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,169 @@ +'use client' + +import type * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import * as React from 'react' +import { + Controller, + type ControllerProps, + type FieldPath, + type FieldValues, + FormProvider, + useFormContext, +} from 'react-hook-form' + +import { Label } from '@/components/ui/label' +import { cn } from '@/lib/utils' + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext({} as FormFieldContextValue) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error('useFormField should be used within ') + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext({} as FormItemContextValue) + +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) + } +) +FormItem.displayName = 'FormItem' + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +