diff --git a/.claude/agents/kfc/spec-design.md b/.claude/agents/kfc/spec-design.md index aecf2078..0afab2c0 100644 --- a/.claude/agents/kfc/spec-design.md +++ b/.claude/agents/kfc/spec-design.md @@ -14,7 +14,7 @@ You are a professional spec design document expert. Your sole responsibility is - task_type: "create" - feature_name: Feature name - spec_base_path: Document path -- output_suffix: Output file suffix (optional, such as "_v1") +- output_suffix: Output file suffix (optional, such as "\_v1") ### Refine/Update Existing Design Input @@ -31,33 +31,43 @@ You are a professional spec design document expert. Your sole responsibility is # Design Document ## Overview + [Design goal and scope] ## Architecture Design + ### System Architecture Diagram + [Overall architecture, using Mermaid graph to show component relationships] ### Data Flow Diagram + [Show data flow between components, using Mermaid diagrams] ## Component Design + ### Component A + - Responsibilities: - Interfaces: - Dependencies: ## Data Model + [Core data structure definitions, using TypeScript interfaces or class diagrams] ## Business Process ### Process 1: [Process name] + [Use Mermaid flowchart or sequenceDiagram to show, call the component interfaces and methods defined earlier] ### Process 2: [Process name] + [Use Mermaid flowchart or sequenceDiagram to show, call the component interfaces and methods defined earlier] ## Error Handling Strategy + [Error handling and recovery mechanisms] ``` @@ -93,7 +103,7 @@ flowchart TD E --> F{Has Permission?} F -->|Yes| G[permissionManager.startMonitoring] F -->|No| H[permissionManager.showPermissionSetup] - + %% Note: Directly reference the interface methods defined earlier %% This ensures design consistency and traceability ``` diff --git a/.claude/agents/kfc/spec-judge.md b/.claude/agents/kfc/spec-judge.md index 13176e3a..5f5bc79c 100644 --- a/.claude/agents/kfc/spec-judge.md +++ b/.claude/agents/kfc/spec-judge.md @@ -37,14 +37,17 @@ eg: #### General Evaluation Criteria 1. **Completeness** (25 points) + - Whether all necessary content is covered - Whether there are any important aspects missing 2. **Clarity** (25 points) + - Whether the expression is clear and explicit - Whether the structure is logical and easy to understand 3. **Feasibility** (25 points) + - Whether the solution is practical and feasible - Whether implementation difficulty has been considered @@ -92,7 +95,7 @@ def evaluate_documents(documents): 'weaknesses': identify_weaknesses(doc) } scores.append(score) - + return select_best_or_combine(scores) ``` @@ -102,7 +105,7 @@ def evaluate_documents(documents): - Requirements: Refer to user's original requirement description (feature_name, feature_description) - Design: Refer to approved requirements.md - Tasks: Refer to approved requirements.md and design.md -2. Read candidate documents (requirements:requirements_v*.md, design:design_v*.md, tasks:tasks_v*.md) +2. Read candidate documents (requirements:requirements_v*.md, design:design_v*.md, tasks:tasks_v\*.md) 3. Score based on reference documents and Specific Type Criteria 4. Select the best solution or combine strengths from x solutions 5. Copy the final solution to a new path with a random 4-digit suffix (e.g., requirements_v1234.md) diff --git a/.claude/agents/kfc/spec-requirements.md b/.claude/agents/kfc/spec-requirements.md index 0a151882..df178cc8 100644 --- a/.claude/agents/kfc/spec-requirements.md +++ b/.claude/agents/kfc/spec-requirements.md @@ -15,7 +15,7 @@ You are an EARS (Easy Approach to Requirements Syntax) requirements document exp - feature_name: Feature name (kebab-case) - feature_description: Feature description - spec_base_path: Spec document path -- output_suffix: Output file suffix (optional, such as "_v1", "_v2", "_v3", required for parallel execution) +- output_suffix: Output file suffix (optional, such as "\_v1", "\_v2", "\_v3", required for parallel execution) ### Refine/Update Requirements Input @@ -70,7 +70,7 @@ If the requirements clarification process seems to be going in circles or not ma ## **Important Constraints** - The directory '.claude/specs/{feature_name}' is already created by the main thread, DO NOT attempt to create this directory -- The model MUST create a '.claude/specs/{feature_name}/requirements_{output_suffix}.md' file if it doesn't already exist +- The model MUST create a '.claude/specs/{feature*name}/requirements*{output_suffix}.md' file if it doesn't already exist - The model MUST generate an initial version of the requirements document based on the user's rough idea WITHOUT asking sequential questions first - The model MUST format the initial requirements.md document with: - A clear introduction section that summarizes the feature @@ -93,11 +93,12 @@ If the requirements clarification process seems to be going in circles or not ma **User Story:** As a [role], I want [feature], so that [benefit] #### Acceptance Criteria + This section should have EARS requirements 1. WHEN [event] THEN [system] SHALL [response] 2. IF [precondition] THEN [system] SHALL [response] - + ### Requirement 2 **User Story:** As a [role], I want [feature], so that [benefit] diff --git a/.claude/agents/kfc/spec-tasks.md b/.claude/agents/kfc/spec-tasks.md index dc2d740e..ba71e7e9 100644 --- a/.claude/agents/kfc/spec-tasks.md +++ b/.claude/agents/kfc/spec-tasks.md @@ -14,7 +14,7 @@ You are a spec tasks document expert. Your sole responsibility is to create and - task_type: "create" - feature_name: Feature name (kebab-case) - spec_base_path: Spec document path -- output_suffix: Output file suffix (optional, such as "_v1", "_v2", "_v3", required for parallel execution) +- output_suffix: Output file suffix (optional, such as "\_v1", "\_v2", "\_v3", required for parallel execution) ### Refine/Update Tasks Input @@ -66,12 +66,12 @@ flowchart TD T3[Task 3: Implement AgentRegistry] T4[Task 4: Implement TaskDispatcher] T5[Task 5: Implement MCPIntegration] - + T1 --> T2_1 T2_1 --> T2_2 T2_1 --> T3 T2_1 --> T4 - + style T3 fill:#e1f5fe style T4 fill:#e1f5fe style T5 fill:#c8e6c9 @@ -147,31 +147,35 @@ Convert the feature design into a series of prompts for a code-generation LLM th # Implementation Plan - [ ] 1. Set up project structure and core interfaces - - Create directory structure for models, services, repositories, and API components - - Define interfaces that establish system boundaries - - _Requirements: 1.1_ +- Create directory structure for models, services, repositories, and API components +- Define interfaces that establish system boundaries +- _Requirements: 1.1_ - [ ] 2. Implement data models and validation - [ ] 2.1 Create core data model interfaces and types + - Write TypeScript interfaces for all data models - Implement validation functions for data integrity - _Requirements: 2.1, 3.3, 1.2_ - [ ] 2.2 Implement User model with validation + - Write User class with validation methods - Create unit tests for User model validation - _Requirements: 1.2_ - [ ] 2.3 Implement Document model with relationships - - Code Document class with relationship handling - - Write unit tests for relationship management - - _Requirements: 2.1, 3.3, 1.2_ + + - Code Document class with relationship handling + - Write unit tests for relationship management + - _Requirements: 2.1, 3.3, 1.2_ - [ ] 3. Create storage mechanism - [ ] 3.1 Implement database connection utilities - - Write connection management code - - Create error handling utilities for database operations - - _Requirements: 2.1, 3.3, 1.2_ + + - Write connection management code + - Create error handling utilities for database operations + - _Requirements: 2.1, 3.3, 1.2_ - [ ] 3.2 Implement repository pattern for data access - Code base repository interface diff --git a/.claude/agents/kfc/spec-test.md b/.claude/agents/kfc/spec-test.md index b7e60be9..ad266ab6 100644 --- a/.claude/agents/kfc/spec-test.md +++ b/.claude/agents/kfc/spec-test.md @@ -40,6 +40,7 @@ You will receive: | ------- | ------------------- | ------------- | | XX-01 | [Description] | Positive Test | | XX-02 | [Description] | Error Test | + [More cases...] ## Detailed Test Steps @@ -49,15 +50,18 @@ You will receive: **Test Purpose**: [Specific purpose] **Test Data Preparation**: + - [Mock data preparation] - [Environment setup] **Test Steps**: + 1. [Step 1] 2. [Step 2] 3. [Verification point] **Expected Results**: + - [Expected result 1] - [Expected result 2] @@ -66,12 +70,15 @@ You will receive: ## Test Considerations ### Mock Strategy + [Explain how to mock dependencies] ### Boundary Conditions + [List boundary cases that need testing] ### Asynchronous Operations + [Considerations for async testing] ``` diff --git a/.claude/settings/kfc-settings.json b/.claude/settings/kfc-settings.json index 8a5c1614..7cd48696 100644 --- a/.claude/settings/kfc-settings.json +++ b/.claude/settings/kfc-settings.json @@ -21,4 +21,4 @@ "visible": false } } -} \ No newline at end of file +} diff --git a/.claude/system-prompts/spec-workflow-starter.md b/.claude/system-prompts/spec-workflow-starter.md index b36a705d..2d7b2c9c 100644 --- a/.claude/system-prompts/spec-workflow-starter.md +++ b/.claude/system-prompts/spec-workflow-starter.md @@ -23,9 +23,9 @@ Here is the workflow you need to follow: You are helping guide the user through the process of transforming a rough idea for a feature into a detailed design document with an implementation plan and todo list. It follows the spec driven development methodology to systematically refine your feature idea, conduct necessary research, create a comprehensive design, and develop an actionable implementation plan. The process is designed to be iterative, allowing movement between requirements clarification and research as needed. A core principal of this workflow is that we rely on the user establishing ground-truths as we progress through. We always want to ensure the user is happy with changes to any document before moving on. - + Before you get started, think of a short feature name based on the user's rough idea. This will be used for the feature directory. Use kebab-case format for the feature_name (e.g. "user-authentication") - + Rules: - Do not tell the user about this workflow. We do not need to tell them which step we are on or that you are following a workflow @@ -108,24 +108,24 @@ stateDiagram-v2 Requirements --> ReviewReq : Complete Requirements ReviewReq --> Requirements : Feedback/Changes Requested ReviewReq --> Design : Explicit Approval - + Design --> ReviewDesign : Complete Design ReviewDesign --> Design : Feedback/Changes Requested ReviewDesign --> Tasks : Explicit Approval - + Tasks --> ReviewTasks : Complete Tasks ReviewTasks --> Tasks : Feedback/Changes Requested ReviewTasks --> [*] : Explicit Approval - + Execute : Execute Task - + state "Entry Points" as EP { [*] --> Requirements : Update [*] --> Design : Update [*] --> Tasks : Update [*] --> Execute : Execute task } - + Execute --> [*] : Complete ``` @@ -144,7 +144,7 @@ stateDiagram-v2 Note: -- output_suffix is only provided when multiple sub-agents are running in parallel, e.g., when 4 sub-agents are running, the output_suffix is "_v1", "_v2", "_v3", "_v4" +- output_suffix is only provided when multiple sub-agents are running in parallel, e.g., when 4 sub-agents are running, the output_suffix is "\_v1", "\_v2", "\_v3", "\_v4" - spec-tasks and spec-impl are completely different sub agents, spec-tasks is for task planning, spec-impl is for task implementation #### Create Requirements - spec-requirements @@ -154,7 +154,7 @@ Note: - feature_name: Feature name (kebab-case) - feature_description: Feature description - spec_base_path: Spec document base path -- output_suffix: Output file suffix (optional, such as "_v1", "_v2", "_v3", required for parallel execution) +- output_suffix: Output file suffix (optional, such as "\_v1", "\_v2", "\_v3", required for parallel execution) #### Refine/Update Requirements - spec-requirements @@ -169,7 +169,7 @@ Note: - task_type: "create" - feature_name: Feature name - spec_base_path: Spec document base path -- output_suffix: Output file suffix (optional, such as "_v1") +- output_suffix: Output file suffix (optional, such as "\_v1") #### Refine/Update Existing Design - spec-design @@ -184,7 +184,7 @@ Note: - task_type: "create" - feature_name: Feature name (kebab-case) - spec_base_path: Spec document base path -- output_suffix: Output file suffix (optional, such as "_v1", "_v2", "_v3", required for parallel execution) +- output_suffix: Output file suffix (optional, such as "\_v1", "\_v2", "\_v3", required for parallel execution) #### Refine/Update Tasks - spec-tasks @@ -221,10 +221,12 @@ Note: When parallel agents generate multiple outputs (n >= 2), use tree-based evaluation: 1. **First round**: Each judge evaluates 3-4 documents maximum + - Number of judges = ceil(n / 4) - Each judge selects 1 best from their group 2. **Subsequent rounds**: If previous round output > 3 documents + - Continue with new round using same rules - Until <= 3 documents remain @@ -260,10 +262,11 @@ Example with 10 documents: - You MUST maintain a clear record of which step you are currently on. - You MUST NOT combine multiple steps into a single interaction. - When executing implementation tasks from tasks.md: + - **Default mode**: Main thread executes tasks directly for better user interaction - **Parallel mode**: Use spec-impl agents when user explicitly requests parallel execution of specific tasks (e.g., "execute task2.1 and task2.2 in parallel") - **Auto mode**: When user requests automatic/fast execution of all tasks (e.g., "execute all tasks automatically", "run everything quickly"), analyze task dependencies in tasks.md and orchestrate spec-impl agents to execute independent tasks in parallel while respecting dependencies - + Example dependency patterns: ```mermaid @@ -276,6 +279,7 @@ Example with 10 documents: ``` Orchestration steps: + 1. Start: Launch spec-impl1 (task1) and spec-impl2 (task3) in parallel 2. After task1 completes: Launch spec-impl3 (task2.1) and spec-impl4 (task2.2) in parallel 3. After task2.1, task2.2, and task3 all complete: Launch spec-impl5 (task4) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 937bf1b5..8e2d220f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,19 +1,23 @@ ## Description + Brief description of changes ## Related Issue + Closes # ## Type of Change + - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Checklist + - [ ] Code follows project style guidelines - [ ] Self-review completed - [ ] No console errors - [ ] Uses Lucide icons consistently - [ ] Responsive design implemented -- [ ] Starknet best practices followed \ No newline at end of file +- [ ] Starknet best practices followed diff --git a/DOCKER_DEPLOYMENT.md b/DOCKER_DEPLOYMENT.md index 83443779..fd01e59e 100644 --- a/DOCKER_DEPLOYMENT.md +++ b/DOCKER_DEPLOYMENT.md @@ -5,10 +5,12 @@ This document provides comprehensive instructions for building, running, and dep ## Architecture The Docker setup uses **multi-stage builds** to optimize: + - **Build Stage**: Compiles Next.js and validates i18n configuration - **Runtime Stage**: Lean production image with minimal dependencies ### Image Optimization + - Base image: `node:20-alpine` (~150MB) - Production image size: ~250-300MB (after build) - Development image: Includes dev dependencies for fast iteration @@ -161,7 +163,7 @@ services: nginx: image: nginx:alpine ports: - - "80:80" + - '80:80' volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro depends_on: @@ -231,16 +233,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - + - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - + - name: Build and push uses: docker/build-push-action@v4 with: diff --git a/GRAPHQL_SUBSCRIPTIONS_GUIDE.md b/GRAPHQL_SUBSCRIPTIONS_GUIDE.md index f4ebfe34..ac3adefe 100644 --- a/GRAPHQL_SUBSCRIPTIONS_GUIDE.md +++ b/GRAPHQL_SUBSCRIPTIONS_GUIDE.md @@ -53,11 +53,13 @@ This implementation provides production-ready GraphQL subscriptions for TeachLin ## Installation Dependencies are already added to `package.json`: + - `@apollo/client` - GraphQL client - `graphql` - GraphQL core - `graphql-ws` - WebSocket protocol for GraphQL Run installation: + ```bash npm install ``` @@ -76,11 +78,7 @@ npm install import { SubscriptionProvider } from '@/components/SubscriptionProvider'; import type { JSX } from 'react'; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}): JSX.Element { +export default function RootLayout({ children }: { children: React.ReactNode }): JSX.Element { const subscriptionConfig = { subscriptionUrl: process.env.NEXT_PUBLIC_GRAPHQL_WS_URL || 'wss://api.teachlink.com/graphql', httpUrl: process.env.NEXT_PUBLIC_GRAPHQL_HTTP_URL || 'https://api.teachlink.com/graphql', @@ -92,9 +90,7 @@ export default function RootLayout({ return ( - - {children} - + {children} ); @@ -127,12 +123,9 @@ import { useSubscription } from '@/hooks/useSubscription'; import { NEW_POSTS_SUBSCRIPTION } from '@/lib/graphql/subscriptionQueries'; export function PostFeed() { - const { data, loading, error, connectionState } = useSubscription( - NEW_POSTS_SUBSCRIPTION, - { - variables: { topicId: 'web3' }, - }, - ); + const { data, loading, error, connectionState } = useSubscription(NEW_POSTS_SUBSCRIPTION, { + variables: { topicId: 'web3' }, + }); if (loading) return
Loading posts...
; if (error) return
Error: {error.message}
; @@ -154,33 +147,24 @@ export function PostFeed() { ```tsx export function NotificationCenter() { - const { data, errorMessage, resubscribe } = useSubscription( - USER_NOTIFICATIONS_SUBSCRIPTION, - { - variables: { userId: 'user-123' }, - onConnect: () => { - console.log('Connected to notifications'); - }, - onData: (newNotification) => { - console.log('New notification:', newNotification); - // Play sound, show toast, etc. - }, - onError: (error) => { - console.error('Subscription error:', error); - }, - onDisconnect: () => { - console.log('Disconnected from notifications'); - }, + const { data, errorMessage, resubscribe } = useSubscription(USER_NOTIFICATIONS_SUBSCRIPTION, { + variables: { userId: 'user-123' }, + onConnect: () => { + console.log('Connected to notifications'); }, - ); + onData: (newNotification) => { + console.log('New notification:', newNotification); + // Play sound, show toast, etc. + }, + onError: (error) => { + console.error('Subscription error:', error); + }, + onDisconnect: () => { + console.log('Disconnected from notifications'); + }, + }); - return ( -
- {errorMessage && ( - - )} -
- ); + return
{errorMessage && }
; } ``` @@ -217,24 +201,29 @@ export function LiveQuizResults() { Pre-built subscription queries available in `src/lib/graphql/subscriptionQueries.ts`: ### Posts & Comments + - `NEW_POSTS_SUBSCRIPTION` - New posts in topic - `POST_COMMENTS_SUBSCRIPTION` - Comments on post ### Notifications + - `USER_NOTIFICATIONS_SUBSCRIPTION` - User notifications - `FEED_UPDATES_SUBSCRIPTION` - Feed updates ### Tipping & Reputation + - `TIPPING_UPDATES_SUBSCRIPTION` - Received tips - `REPUTATION_UPDATES_SUBSCRIPTION` - Reputation changes ### Real-Time Features + - `USER_ACTIVITY_SUBSCRIPTION` - User activity status - `TYPING_INDICATOR_SUBSCRIPTION` - Typing indicators - `MESSAGE_STATUS_SUBSCRIPTION` - Message delivery status - `PRESENCE_SUBSCRIPTION` - Who's online ### Advanced + - `STUDY_GROUP_UPDATES_SUBSCRIPTION` - Study group messages - `LIVE_QUIZ_RESPONSES_SUBSCRIPTION` - Quiz responses - `BLOCKCHAIN_TRANSACTION_SUBSCRIPTION` - Transaction updates @@ -252,10 +241,7 @@ export function Header() { return (

TeachLink

- +
); } @@ -292,11 +278,7 @@ export function PostFeed() { const { data, loading, error } = useSubscription(POST_SUBSCRIPTION); return ( - } - > + }> ); @@ -374,13 +356,7 @@ import { formatSubscriptionError } from '@/lib/graphql/subscriptions'; export function SubscriptionWithErrorDisplay() { const { error } = useSubscription(SUBSCRIPTION); - return ( -
- {error && ( - - )} -
- ); + return
{error && }
; } ``` @@ -423,13 +399,10 @@ interface Props { } export function PostComments({ postId, isOpen }: Props) { - const { data } = useSubscription( - POST_COMMENTS_SUBSCRIPTION, - { - variables: { postId: postId || '' }, - skip: !isOpen || !postId, // Skip if post not open or no postId - }, - ); + const { data } = useSubscription(POST_COMMENTS_SUBSCRIPTION, { + variables: { postId: postId || '' }, + skip: !isOpen || !postId, // Skip if post not open or no postId + }); if (!isOpen) return null; return ; @@ -441,16 +414,13 @@ export function PostComments({ postId, isOpen }: Props) { ```tsx // Create a custom hook for specific feature export function usePostComments(postId: string) { - return useSubscription( - POST_COMMENTS_SUBSCRIPTION, - { - variables: { postId }, - onData: (comment) => { - // Custom logic here - playNotificationSound(); - }, + return useSubscription(POST_COMMENTS_SUBSCRIPTION, { + variables: { postId }, + onData: (comment) => { + // Custom logic here + playNotificationSound(); }, - ); + }); } // Use in component @@ -487,6 +457,7 @@ export function OptimizedFeed() { ### Subscription Cleanup The `useSubscription` hook automatically cleans up resources: + - Unsubscribes when component unmounts - Clears timers and listeners - Closes WebSocket connections @@ -515,16 +486,14 @@ const config = { subscriptionUrl: 'wss://api.teachlink.com/graphql', httpUrl: 'https://api.teachlink.com/graphql', reconnect: { - maxRetries: 10, // Retry up to 10 times - initialDelayMs: 500, // Start with 500ms delay - maxDelayMs: 60000, // Cap at 60 seconds + maxRetries: 10, // Retry up to 10 times + initialDelayMs: 500, // Start with 500ms delay + maxDelayMs: 60000, // Cap at 60 seconds }, connectionTimeoutMs: 10000, // 10 second timeout }; - - {children} - +{children}; ``` ### Custom Apollo Client @@ -538,12 +507,9 @@ const customClient = new ApolloClient({ // ... your configuration }); - + {children} - +; ``` --- @@ -558,9 +524,7 @@ import { useSubscription } from '@/hooks/useSubscription'; describe('useSubscription', () => { it('should subscribe and receive data', async () => { - const { result } = renderHook(() => - useSubscription(SUBSCRIPTION) - ); + const { result } = renderHook(() => useSubscription(SUBSCRIPTION)); await waitFor(() => { expect(result.current.data).toBeDefined(); @@ -590,7 +554,7 @@ const mocks = [ - +; ``` --- @@ -642,6 +606,7 @@ Solution: ## Best Practices ✅ **Do:** + - Wrap app with `SubscriptionProvider` once at root - Use `useSubscription` hook for subscriptions - Handle errors gracefully with fallback UI @@ -650,6 +615,7 @@ Solution: - Clean up with proper dependency arrays ❌ **Don't:** + - Create multiple SubscriptionProviders - Subscribe in server components - Ignore connection errors diff --git a/GRAPHQL_SUBSCRIPTIONS_IMPLEMENTATION.md b/GRAPHQL_SUBSCRIPTIONS_IMPLEMENTATION.md index ae964fa8..434b072e 100644 --- a/GRAPHQL_SUBSCRIPTIONS_IMPLEMENTATION.md +++ b/GRAPHQL_SUBSCRIPTIONS_IMPLEMENTATION.md @@ -1,6 +1,7 @@ # GraphQL Subscriptions Implementation - Complete Guide ## Issue Reference + **#266 GraphQL Subscriptions** - Real-time data updates via WebSocket --- @@ -97,6 +98,7 @@ src/ **Location**: `src/lib/graphql/subscriptions.ts` (347 lines) **Exports**: + - `SubscriptionConfig` - Configuration interface - `ConnectionState` - Enum for connection states - `ConnectionEvent` - Connection lifecycle events @@ -109,6 +111,7 @@ src/ - `formatSubscriptionError()` - User-friendly error messages **Key Features**: + - ✅ Exponential backoff for reconnection - ✅ Connection lifecycle management - ✅ Event-driven state changes @@ -119,11 +122,13 @@ src/ **Location**: `src/hooks/useSubscription.ts` (360 lines) **Exports**: + - `useSubscription()` - Main hook - `useSubscriptionConnection()` - Connection state listener - `usePollableSubscription()` - With polling fallback **Features**: + - ✅ TypeScript generics for type safety - ✅ Connection state tracking - ✅ Error handling with retry logic @@ -133,6 +138,7 @@ src/ - ✅ Polling fallback mechanism **Result Object**: + ```typescript interface UseSubscriptionResult { data: TData | undefined; @@ -150,6 +156,7 @@ interface UseSubscriptionResult { **Location**: `src/lib/graphql/subscriptionQueries.ts` (190 lines) **Includes 15+ subscription definitions**: + - NEW_POSTS_SUBSCRIPTION - POST_COMMENTS_SUBSCRIPTION - USER_NOTIFICATIONS_SUBSCRIPTION @@ -170,11 +177,13 @@ interface UseSubscriptionResult { **Location**: `src/components/SubscriptionProvider.tsx` (92 lines) **Exports**: + - `SubscriptionProvider` - Wrapper component - `useSubscriptionClient()` - Access Apollo client - `useHasSubscriptionClient()` - Check availability **Features**: + - ✅ React Context for Apollo client - ✅ Configuration merging - ✅ Custom client support @@ -185,6 +194,7 @@ interface UseSubscriptionResult { **Location**: `src/components/subscription/SubscriptionUI.tsx` (270 lines) **Components**: + - `ConnectionStatusIndicator` - Status dot with label - `ConnectionStatusBanner` - Prominent status banner - `SubscriptionLoadingState` - Loading wrapper @@ -192,6 +202,7 @@ interface UseSubscriptionResult { - `SubscriptionSkeleton` - Loading skeleton **Styling**: + - ✅ Tailwind CSS - ✅ Dark mode support - ✅ Responsive design @@ -202,6 +213,7 @@ interface UseSubscriptionResult { **Location**: `src/app/subscriptions-demo/page.tsx` (340 lines) **Features**: + - Live connection status display - Example subscriptions showcase - Code examples @@ -215,6 +227,7 @@ interface UseSubscriptionResult { ### Step 1: Dependencies Already Added Check `package.json` - these are already included: + ```json { "@apollo/client": "^3.8.0", @@ -225,6 +238,7 @@ Check `package.json` - these are already included: ``` Install if needed: + ```bash npm install ``` @@ -232,6 +246,7 @@ npm install ### Step 2: Environment Variables Create `.env.local`: + ```bash # GraphQL Subscription Endpoints NEXT_PUBLIC_GRAPHQL_WS_URL=wss://api.teachlink.com/graphql @@ -253,11 +268,7 @@ NEXT_PUBLIC_SUBSCRIPTION_TIMEOUT=5000 import { SubscriptionProvider } from '@/components/SubscriptionProvider'; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( @@ -291,12 +302,9 @@ import { useSubscription } from '@/hooks/useSubscription'; import { NEW_POSTS_SUBSCRIPTION } from '@/lib/graphql/subscriptionQueries'; export function PostFeed() { - const { data, loading, error, connectionState } = useSubscription( - NEW_POSTS_SUBSCRIPTION, - { - variables: { topicId: 'web3' }, - }, - ); + const { data, loading, error, connectionState } = useSubscription(NEW_POSTS_SUBSCRIPTION, { + variables: { topicId: 'web3' }, + }); if (loading) return
Loading...
; if (error) return
Error: {error.message}
; @@ -318,20 +326,17 @@ export function PostFeed() { ```tsx export function NotificationCenter() { - const { data, connectionState, resubscribe } = useSubscription( - USER_NOTIFICATIONS_SUBSCRIPTION, - { - variables: { userId: 'user-123' }, - onData: (notification) => { - showToast(notification.message); - }, + const { data, connectionState, resubscribe } = useSubscription(USER_NOTIFICATIONS_SUBSCRIPTION, { + variables: { userId: 'user-123' }, + onData: (notification) => { + showToast(notification.message); }, - ); + }); return (
- + {connectionState === ConnectionState.ERROR && ( )} @@ -346,17 +351,14 @@ export function NotificationCenter() { ```tsx export function LiveResults() { - const { data, loading } = usePollableSubscription( - LIVE_QUIZ_RESPONSES_SUBSCRIPTION, - { - variables: { quizId: 'quiz-123' }, - pollFn: async () => { - const res = await fetch(`/api/quiz/quiz-123/responses`); - return res.json(); - }, - pollIntervalMs: 5000, + const { data, loading } = usePollableSubscription(LIVE_QUIZ_RESPONSES_SUBSCRIPTION, { + variables: { quizId: 'quiz-123' }, + pollFn: async () => { + const res = await fetch(`/api/quiz/quiz-123/responses`); + return res.json(); }, - ); + pollIntervalMs: 5000, + }); return (
@@ -375,10 +377,10 @@ export function LiveResults() { ```typescript enum ConnectionState { - CONNECTING = 'CONNECTING', // Initial connection - CONNECTED = 'CONNECTED', // Ready for data - DISCONNECTED = 'DISCONNECTED', // Offline - ERROR = 'ERROR', // Connection error + CONNECTING = 'CONNECTING', // Initial connection + CONNECTED = 'CONNECTED', // Ready for data + DISCONNECTED = 'DISCONNECTED', // Offline + ERROR = 'ERROR', // Connection error RECONNECTING = 'RECONNECTING', // Retry attempt } ``` @@ -438,18 +440,15 @@ SubscriptionError { ### Handle Errors ```tsx -const { error, errorMessage, resubscribe } = useSubscription( - SUBSCRIPTION, - { - onError: (error) => { - if (isConnectionError(error)) { - console.log('Network error, will retry...'); - } else { - console.log('Subscription error:', error.message); - } - }, +const { error, errorMessage, resubscribe } = useSubscription(SUBSCRIPTION, { + onError: (error) => { + if (isConnectionError(error)) { + console.log('Network error, will retry...'); + } else { + console.log('Subscription error:', error.message); + } }, -); +}); if (error) { return ( @@ -464,11 +463,13 @@ if (error) { ### Error Recovery Automatic: + - ✅ Exponential backoff reconnection - ✅ Max 5 retry attempts (configurable) - ✅ Delay: 1000ms → 2000ms → 4000ms... Manual: + - ✅ `resubscribe()` function - ✅ User-triggered refresh button @@ -479,11 +480,13 @@ Manual: ### Optimizations 1. **Memoization**: Use `useMemo` for variables + ```tsx const variables = useMemo(() => ({ topicId }), [topicId]); ``` 2. **Conditional Subscriptions**: Skip when not needed + ```tsx const { data } = useSubscription(SUBSCRIPTION, { skip: !isOpen, @@ -491,10 +494,11 @@ Manual: ``` 3. **Multiple Subscriptions**: Subscribe to what you need + ```tsx // ✓ Good: Subscribe only to needed data const posts = useSubscription(NEW_POSTS, { variables: {...} }); - + // ✗ Bad: Subscribe to everything const all = useSubscription(ALL_DATA, {}); ``` @@ -524,9 +528,7 @@ import { useSubscription } from '@/hooks/useSubscription'; describe('useSubscription', () => { it('should handle subscription data', async () => { - const { result } = renderHook(() => - useSubscription(SUBSCRIPTION) - ); + const { result } = renderHook(() => useSubscription(SUBSCRIPTION)); await waitFor(() => { expect(result.current.data).toBeDefined(); @@ -537,16 +539,14 @@ describe('useSubscription', () => { it('should retry on error', async () => { const onError = vi.fn(); - const { result, rerender } = renderHook( - () => useSubscription(SUBSCRIPTION, { onError }) - ); + const { result, rerender } = renderHook(() => useSubscription(SUBSCRIPTION, { onError })); await waitFor(() => { expect(onError).toHaveBeenCalled(); }); result.current.resubscribe(); - + expect(result.current.loading).toBe(true); }); }); @@ -557,22 +557,24 @@ describe('useSubscription', () => { ```tsx import { MockedProvider } from '@apollo/client/testing'; -const mocks = [{ - request: { - query: SUBSCRIPTION, - variables: { id: '1' }, - }, - result: { - data: { - onUpdate: { id: '1', data: 'test' }, +const mocks = [ + { + request: { + query: SUBSCRIPTION, + variables: { id: '1' }, + }, + result: { + data: { + onUpdate: { id: '1', data: 'test' }, + }, }, }, -}]; +]; render( - + , ); ``` @@ -586,6 +588,7 @@ render( ✅ Mobile browsers (iOS Safari 15+, Chrome Android) **Requirements**: + - WebSocket support - ES2020 JavaScript features - Secure context (HTTPS, except localhost) @@ -610,18 +613,16 @@ const config: SubscriptionConfig = { // Reconnection strategy reconnect: { - maxRetries: 10, // Max retry attempts - initialDelayMs: 500, // Starting delay - maxDelayMs: 60000, // Max delay cap + maxRetries: 10, // Max retry attempts + initialDelayMs: 500, // Starting delay + maxDelayMs: 60000, // Max delay cap }, // Connection timeout connectionTimeoutMs: 10000, }; - - {children} - +{children}; ``` --- @@ -629,22 +630,26 @@ const config: SubscriptionConfig = { ## Acceptance Criteria - ✅ All Met - ✅ **Real-time data updates without polling** + - WebSocket subscriptions working - Instant data delivery - Demo page at `/subscriptions-demo` - ✅ **WebSocket link setup** + - Apollo Client configured - GraphQL-ws integration - Automatic connection management - ✅ **useSubscription hook** + - Full lifecycle management - Error handling & recovery - Connection state tracking - Callbacks for events - ✅ **Connection lifecycle handling** + - Connection state enum - State change notifications - Proper cleanup @@ -660,6 +665,7 @@ const config: SubscriptionConfig = { ## Files Changed ### New Files + ``` src/lib/graphql/subscriptions.ts (347 lines) src/lib/graphql/subscriptionQueries.ts (190 lines) @@ -672,6 +678,7 @@ GRAPHQL_SUBSCRIPTIONS_GUIDE.md (500+ lines) ``` ### Modified Files + ``` package.json (+3 dependencies) ``` @@ -718,6 +725,7 @@ package.json (+3 dependencies) ## Troubleshooting ### WebSocket not connecting + ``` Check: 1. WSS URL is correct and HTTPS @@ -727,6 +735,7 @@ Check: ``` ### Data not updating + ``` Check: 1. Subscription is not skipped @@ -736,6 +745,7 @@ Check: ``` ### Memory leaks + ``` Check: 1. Components unmounting properly diff --git a/PR_DATA_TABLE_VIRTUALIZATION.md b/PR_DATA_TABLE_VIRTUALIZATION.md index 4892a46a..2395e8ca 100644 --- a/PR_DATA_TABLE_VIRTUALIZATION.md +++ b/PR_DATA_TABLE_VIRTUALIZATION.md @@ -1,6 +1,7 @@ # Pull Request: Data Table Virtualization (Close #258) ## PR Title + ✨ feat: Add Data Table Virtualization with Sticky Headers and Resizable Columns (Close #258) ## Description @@ -8,9 +9,11 @@ This PR implements virtualization for large TeachLink data tables, enabling smooth rendering for datasets with 10k+ rows. The feature integrates a lightweight virtualization library, supports variable row heights, sticky headers, and column resizing to improve performance and usability. ### Problem Statement + Tables with 1000+ rows become slow and unresponsive. The current table rendering strategy does not scale for large datasets, causing poor UX on dashboards and analytics views. ### Solution Overview + - Integrate `react-window` for list virtualization - Add support for variable row heights - Keep table headers sticky during scroll @@ -23,7 +26,9 @@ Tables with 1000+ rows become slow and unresponsive. The current table rendering ## Changes Made ### New/Updated Components + - `src/components/DataTable.tsx` + - Refactored table rendering to use virtualization - Added sticky header behavior - Added responsive column sizing and resize handles @@ -35,16 +40,19 @@ Tables with 1000+ rows become slow and unresponsive. The current table rendering - Integrates with `react-window` and custom measurement logic ### Dependencies + - Added `react-window` or similar virtualization library - (Optional) Added utility package for resize handling if needed ### Performance Improvements + - Smooth scrolling with 10k+ rows - Reduced DOM node count dramatically - Lower memory usage and rendering overhead - Responsive sticky headers with stable layout behavior ### Accessibility and UX + - Sticky header remains visible on vertical scroll - Column resize controls keyboard accessible - Table cells maintain focus and row highlight behavior @@ -68,10 +76,12 @@ Tables with 1000+ rows become slow and unresponsive. The current table rendering ## Files Changed ### Primary + - `src/components/DataTable.tsx` - `src/components/VirtualList.tsx` ### Possible supporting changes + - `src/components/TableHeader.tsx` (if header logic is extracted) - `src/components/TableRow.tsx` (if row rendering is modularized) - `src/lib/tableUtils.ts` or `src/utils/tableUtils.ts` (for resize/virtualization helpers) @@ -82,6 +92,7 @@ Tables with 1000+ rows become slow and unresponsive. The current table rendering ## Usage Example ### Basic Virtualized Table + ```tsx import { DataTable } from '@/components/DataTable'; @@ -100,6 +111,7 @@ export function ReportsPage() { ``` ### Column Resizing + ```tsx ; if (error) return ; - return ( -
- {data?.onNewPost && ( - - )} -
- ); + return
{data?.onNewPost && }
; } ``` @@ -222,25 +241,20 @@ export function PostFeed() { ```tsx export function NotificationCenter() { - const { data, connectionState, resubscribe } = useSubscription( - USER_NOTIFICATIONS_SUBSCRIPTION, - { - variables: { userId: 'user-123' }, - onData: (notification) => { - playSound(); - showToast(notification.message); - }, + const { data, connectionState, resubscribe } = useSubscription(USER_NOTIFICATIONS_SUBSCRIPTION, { + variables: { userId: 'user-123' }, + onData: (notification) => { + playSound(); + showToast(notification.message); }, - ); + }); return ( <> - - {connectionState === ConnectionState.ERROR && ( - - )} - + + {connectionState === ConnectionState.ERROR && } + ); @@ -251,17 +265,14 @@ export function NotificationCenter() { ```tsx export function LiveQuizResults() { - const { data, loading } = usePollableSubscription( - LIVE_QUIZ_RESPONSES_SUBSCRIPTION, - { - variables: { quizId: 'quiz-123' }, - pollFn: async () => { - const res = await fetch(`/api/quiz/quiz-123/responses`); - return res.json(); - }, - pollIntervalMs: 5000, + const { data, loading } = usePollableSubscription(LIVE_QUIZ_RESPONSES_SUBSCRIPTION, { + variables: { quizId: 'quiz-123' }, + pollFn: async () => { + const res = await fetch(`/api/quiz/quiz-123/responses`); + return res.json(); }, - ); + pollIntervalMs: 5000, + }); return (
@@ -279,6 +290,7 @@ export function LiveQuizResults() { ### 1. Environment Variables Add to `.env.local`: + ```bash NEXT_PUBLIC_GRAPHQL_WS_URL=wss://api.teachlink.com/graphql NEXT_PUBLIC_GRAPHQL_HTTP_URL=https://api.teachlink.com/graphql @@ -288,6 +300,7 @@ NEXT_PUBLIC_AUTH_TOKEN=your-jwt-token ### 2. Wrap App with Provider In `src/app/layout.tsx`: + ```tsx {avatar && {username}}

{username}

- +
); } @@ -66,6 +64,7 @@ export function TopicCard({ topicSlug }) { ## 📚 Common Patterns ### Pattern 1: Profile Sharing + ```tsx export function ProfileCard({ username }) { const [showShare, setShowShare] = useState(false); @@ -86,15 +85,13 @@ export function ProfileCard({ username }) { ``` ### Pattern 2: Resource Card with QR + ```tsx export function ResourceCard({ resourceId, title }) { return (

{title}

- +

Scan to access

); @@ -102,6 +99,7 @@ export function ResourceCard({ resourceId, title }) { ``` ### Pattern 3: Full Page Share + ```tsx export function SharePage({ item }) { const [showShare, setShowShare] = useState(false); @@ -129,16 +127,18 @@ export function SharePage({ item }) { ## 🎨 Customization ### Custom Colors + ```tsx ``` ### Different Sizes + ```tsx // Mobile @@ -148,6 +148,7 @@ export function SharePage({ item }) { ``` ### Themed QR Code + ```tsx // Dark mode setShowShare(false)} - shareUrl={shareUrl} -/> + setShowShare(false)} shareUrl={shareUrl} />; ``` ### Conditional Rendering + ```tsx export function ShareButton({ isLoggedIn, itemId }) { const [showShare, setShowShare] = useState(false); @@ -219,6 +220,7 @@ export function ShareButton({ isLoggedIn, itemId }) { ``` ### With Error Boundaries + ```tsx import { ErrorBoundarySystem } from '@/components'; @@ -238,17 +240,20 @@ export function SafeShare({ itemId }) { ## 🧪 Testing in Development ### View the Demo Page + ``` http://localhost:3000/qr-code-demo ``` ### Test Different URLs + 1. Open `/qr-code-demo` 2. Modify the URL input 3. Use the QR preview 4. Test download, print, and copy ### Test on Mobile + 1. Use browser DevTools mobile view 2. Or access demo on actual mobile device 3. Scan QR with phone camera @@ -269,6 +274,7 @@ http://localhost:3000/qr-code-demo ## 🐛 Troubleshooting ### QR Code not showing? + ```tsx // ❌ Wrong @@ -278,16 +284,19 @@ http://localhost:3000/qr-code-demo ``` ### Copy not working? + - Check browser is HTTPS (or localhost) - Test in a different browser - Check browser permissions ### Share Modal styling issues? + - Verify Tailwind CSS is loaded - Check dark mode context is available - Inspect Modal parent styling ### Download not working? + - Check browser popup blocking - Try a different file format - Browser might not support canvas download diff --git a/apply-hooks-fixes.js b/apply-hooks-fixes.js index f3a9cc19..2123d3a5 100644 --- a/apply-hooks-fixes.js +++ b/apply-hooks-fixes.js @@ -1,5 +1,4 @@ -const fs = require('fs'); -const path = require('path'); +import fs from 'node:fs'; const fixes = [ { diff --git a/package-lock.json b/package-lock.json index 850654de..4b97c300 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "my-app", "version": "0.1.0", "dependencies": { + "@apollo/client": "^3.8.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -25,10 +26,14 @@ "date-fns": "^3.6.0", "dompurify": "^3.2.4", "framer-motion": "^12.23.0", + "graphql": "^16.8.0", + "graphql-ws": "^5.14.0", + "i18next": "^24.0.0", "idb": "^8.0.0", "lucide-react": "^0.462.0", "next": "15.3.1", "next-themes": "^0.4.6", + "qrcode.react": "^3.2.0", "react": "^18.3.1", "react-countdown": "^2.3.6", "react-dnd": "^16.0.1", @@ -36,6 +41,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.60.0", "react-hot-toast": "^2.6.0", + "react-i18next": "^15.0.0", "react-icons": "^5.5.0", "react-intersection-observer": "^10.0.3", "react-virtualized-auto-sizer": "^1.0.7", @@ -99,6 +105,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@apollo/client": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.14.1.tgz", + "integrity": "sha512-SgGX6E23JsZhUdG2anxiyHvEvvN6CUaI4ZfMsndZFeuHPXL3H0IsaiNAhLITSISbeyeYd+CBd9oERXQDdjXWZw==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", + "@wry/equality": "^0.5.6", + "@wry/trie": "^0.5.0", + "graphql-tag": "^2.12.6", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.18.0", + "prop-types": "^15.7.2", + "rehackt": "^0.1.0", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.10.3", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.5" + }, + "peerDependencies": { + "graphql": "^15.0.0 || ^16.0.0", + "graphql-ws": "^5.5.5 || ^6.0.3", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -2377,6 +2425,15 @@ "license": "MIT", "optional": true }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@hello-pangea/dnd": { "version": "18.0.1", "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", @@ -6374,6 +6431,54 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@wry/caches": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", + "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/context": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", + "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", + "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", + "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -9062,6 +9167,42 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/graphql": { + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/graphql-ws": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.2.tgz", + "integrity": "sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -9177,6 +9318,15 @@ "node": ">=18" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -9221,6 +9371,37 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/i18next": { + "version": "24.2.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", + "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.10" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -11169,6 +11350,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optimism": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz", + "integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==", + "license": "MIT", + "dependencies": { + "@wry/caches": "^1.0.0", + "@wry/context": "^0.7.0", + "@wry/trie": "^0.5.0", + "tslib": "^2.3.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -11725,6 +11918,15 @@ ], "license": "MIT" }, + "node_modules/qrcode.react": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.2.0.tgz", + "integrity": "sha512-YietHHltOHA4+l5na1srdaMx4sVSOjV9tamHs+mwiLWAMr6QVACRUw1Neax5CptFILcNoITctJY0Ipyn5enQ8g==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11871,6 +12073,32 @@ "react-dom": ">=16" } }, + "node_modules/react-i18next": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", + "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.4.0", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", @@ -12140,6 +12368,24 @@ "regjsparser": "bin/parser" } }, + "node_modules/rehackt": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz", + "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -13174,6 +13420,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -13460,6 +13715,18 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -13582,7 +13849,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13948,6 +14215,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -14811,6 +15087,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", + "license": "MIT" + }, + "node_modules/zen-observable-ts": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", + "license": "MIT", + "dependencies": { + "zen-observable": "0.8.15" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index f942d1ea..b94baccb 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "lucide-react": "^0.462.0", "next": "15.3.1", "next-themes": "^0.4.6", - "qrcode.react": "^1.0.1", + "qrcode.react": "^3.2.0", "react": "^18.3.1", "react-countdown": "^2.3.6", "react-dnd": "^16.0.1", diff --git a/scripts/generate-sitemap.ts b/scripts/generate-sitemap.ts index c93a7069..a88e5db4 100644 --- a/scripts/generate-sitemap.ts +++ b/scripts/generate-sitemap.ts @@ -34,7 +34,7 @@ async function fetchAllCourseIds(): Promise { if (!res.ok) break; const json = await res.json(); - const page: { id: string }[] = Array.isArray(json) ? json : (json.data ?? []); + const page: { id: string }[] = Array.isArray(json) ? json : json.data ?? []; ids.push(...page.map((c) => c.id)); cursor = json.nextCursor; } while (cursor); diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index f85b6c45..f7aaadb2 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -131,10 +131,7 @@ export default function LoginPage() {
- + {successMessage && (
- + {successMessage && ( }, -) { - const { addHeaders, rateLimitResponse } = withRateLimit(req, 'API'); +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { addHeaders, rateLimitResponse } = withRateLimit(req, 'READ'); if (rateLimitResponse) return rateLimitResponse; const { id } = await params; @@ -25,10 +19,7 @@ export async function GET( // ─── PUT /api/admin/feature-flags/[id] ─────────────────────────────────────── // Full or partial update. Also handles toggle via { enabled: boolean }. -export async function PUT( - req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { +export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { const { addHeaders, rateLimitResponse } = withRateLimit(req, 'AUTH'); if (rateLimitResponse) return rateLimitResponse; @@ -46,8 +37,12 @@ export async function PUT( ...(typeof body.name === 'string' && body.name.trim() ? { name: body.name.trim() } : {}), ...(typeof body.description === 'string' ? { description: body.description.trim() } : {}), ...(typeof body.enabled === 'boolean' ? { enabled: body.enabled } : {}), - ...(['all', 'percentage', 'targeting'].includes(body.strategy) ? { strategy: body.strategy } : {}), - ...(typeof body.percentage === 'number' ? { percentage: Math.max(0, Math.min(100, body.percentage)) } : {}), + ...(['all', 'percentage', 'targeting'].includes(body.strategy) + ? { strategy: body.strategy } + : {}), + ...(typeof body.percentage === 'number' + ? { percentage: Math.max(0, Math.min(100, body.percentage)) } + : {}), ...(Array.isArray(body.rules) ? { rules: body.rules as TargetingRule[] } : {}), ...(Array.isArray(body.tags) ? { tags: body.tags.map(String) } : {}), updatedAt: new Date().toISOString(), @@ -55,9 +50,8 @@ export async function PUT( flagStore.set(id, updated); - const action = typeof body.enabled === 'boolean' && body.enabled !== existing.enabled - ? 'toggled' - : 'updated'; + const action = + typeof body.enabled === 'boolean' && body.enabled !== existing.enabled ? 'toggled' : 'updated'; createAuditEntry(action, actor, existing, updated); return addHeaders(NextResponse.json({ flag: updated })); @@ -65,10 +59,7 @@ export async function PUT( // ─── DELETE /api/admin/feature-flags/[id] ──────────────────────────────────── -export async function DELETE( - req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { const { addHeaders, rateLimitResponse } = withRateLimit(req, 'AUTH'); if (rateLimitResponse) return rateLimitResponse; diff --git a/src/app/api/admin/feature-flags/audit/route.ts b/src/app/api/admin/feature-flags/audit/route.ts index 1614bdf6..f52a5106 100644 --- a/src/app/api/admin/feature-flags/audit/route.ts +++ b/src/app/api/admin/feature-flags/audit/route.ts @@ -6,13 +6,13 @@ import { withRateLimit } from '@/lib/ratelimit'; * GET /api/admin/feature-flags/audit?flagId=&limit=50&offset=0 */ export async function GET(req: NextRequest) { - const { addHeaders, rateLimitResponse } = withRateLimit(req, 'API'); + const { addHeaders, rateLimitResponse } = withRateLimit(req, 'READ'); if (rateLimitResponse) return rateLimitResponse; const { searchParams } = new URL(req.url); - const flagId = searchParams.get('flagId'); - const limit = Math.min(200, Math.max(1, parseInt(searchParams.get('limit') ?? '50', 10))); - const offset = Math.max(0, parseInt(searchParams.get('offset') ?? '0', 10)); + const flagId = searchParams.get('flagId'); + const limit = Math.min(200, Math.max(1, parseInt(searchParams.get('limit') ?? '50', 10))); + const offset = Math.max(0, parseInt(searchParams.get('offset') ?? '0', 10)); const filtered = flagId ? auditLog.filter((e) => e.flagId === flagId) : auditLog; const page = filtered.slice(offset, offset + limit); diff --git a/src/app/api/admin/feature-flags/evaluate/route.ts b/src/app/api/admin/feature-flags/evaluate/route.ts index 830c789e..7085f64c 100644 --- a/src/app/api/admin/feature-flags/evaluate/route.ts +++ b/src/app/api/admin/feature-flags/evaluate/route.ts @@ -8,7 +8,7 @@ import { withRateLimit } from '@/lib/ratelimit'; * All query params beyond `id` are passed as the evaluation context. */ export async function GET(req: NextRequest) { - const { addHeaders, rateLimitResponse } = withRateLimit(req, 'API'); + const { addHeaders, rateLimitResponse } = withRateLimit(req, 'READ'); if (rateLimitResponse) return rateLimitResponse; const { searchParams } = new URL(req.url); diff --git a/src/app/api/admin/feature-flags/route.ts b/src/app/api/admin/feature-flags/route.ts index f1ee6491..455ae1c3 100644 --- a/src/app/api/admin/feature-flags/route.ts +++ b/src/app/api/admin/feature-flags/route.ts @@ -13,7 +13,7 @@ import { withRateLimit } from '@/lib/ratelimit'; // Returns the full flag list sorted by updatedAt desc. export async function GET(req: NextRequest) { - const { addHeaders, rateLimitResponse } = withRateLimit(req, 'API'); + const { addHeaders, rateLimitResponse } = withRateLimit(req, 'READ'); if (rateLimitResponse) return rateLimitResponse; const flags = Array.from(flagStore.values()).sort( @@ -43,10 +43,9 @@ export async function POST(req: NextRequest) { name: body.name.trim(), description: typeof body.description === 'string' ? body.description.trim() : '', enabled: false, - strategy: ['all', 'percentage', 'targeting'].includes(body.strategy) - ? body.strategy - : 'all', - percentage: typeof body.percentage === 'number' ? Math.max(0, Math.min(100, body.percentage)) : 0, + strategy: ['all', 'percentage', 'targeting'].includes(body.strategy) ? body.strategy : 'all', + percentage: + typeof body.percentage === 'number' ? Math.max(0, Math.min(100, body.percentage)) : 0, rules: Array.isArray(body.rules) ? (body.rules as TargetingRule[]) : [], tags: Array.isArray(body.tags) ? body.tags.map(String) : [], createdAt: now, diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 32c25e78..9f349462 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { withRateLimit } from '@/lib/ratelimit'; +import type { AuthResponse } from '@/types/api'; export async function POST(request: NextRequest) { const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH'); diff --git a/src/app/api/auth/signup/route.ts b/src/app/api/auth/signup/route.ts index 3ffff1dc..823c980f 100644 --- a/src/app/api/auth/signup/route.ts +++ b/src/app/api/auth/signup/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { withRateLimit } from '@/lib/ratelimit'; +import type { AuthResponse } from '@/types/api'; export async function POST(request: NextRequest) { const { addHeaders, rateLimitResponse } = withRateLimit(request, 'AUTH'); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1b9a9701..ba8ce717 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,10 +4,23 @@ import { Geist, Geist_Mono } from 'next/font/google'; import Script from 'next/script'; import './globals.css'; import { RootProviders } from '@/providers/RootProviders'; -import { getHtmlDir } from '@/lib/i18n/config'; // Languages supported at startup — extend as new locale files are added. -const VALID_LOCALES = new Set(['en', 'es', 'ar', 'fr', 'de', 'he', 'ja', 'zh', 'pt', 'ru', 'it', 'ko']); +const VALID_LOCALES = new Set([ + 'en', + 'es', + 'ar', + 'fr', + 'de', + 'he', + 'ja', + 'zh', + 'pt', + 'ru', + 'it', + 'ko', +]); +const RTL_LOCALES = new Set(['ar', 'he']); const geistSans = Geist({ variable: '--font-geist-sans', @@ -38,7 +51,7 @@ export default async function RootLayout({ // avoids a hydration flash for RTL users. const rawLocale = cookieStore.get('i18n:language')?.value ?? 'en'; const locale = VALID_LOCALES.has(rawLocale) ? rawLocale : 'en'; - const dir = getHtmlDir(locale); + const dir = RTL_LOCALES.has(locale) ? 'rtl' : 'ltr'; const themeScript = ` (function() { diff --git a/src/app/pages/admin/feature-flags/page.tsx b/src/app/pages/admin/feature-flags/page.tsx index e0ffee82..37e00fbb 100644 --- a/src/app/pages/admin/feature-flags/page.tsx +++ b/src/app/pages/admin/feature-flags/page.tsx @@ -26,16 +26,24 @@ import { useAllFeatureFlags } from '@/hooks/useFeatureFlag'; // ─── Small shared UI ────────────────────────────────────────────────────────── -function Badge({ children, variant = 'default' }: { children: React.ReactNode; variant?: 'default' | 'green' | 'red' | 'yellow' | 'blue' }) { +function Badge({ + children, + variant = 'default', +}: { + children: React.ReactNode; + variant?: 'default' | 'green' | 'red' | 'yellow' | 'blue'; +}) { const colors = { default: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', - green: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300', - red: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', - yellow: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300', - blue: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300', + green: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300', + red: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', + yellow: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300', + blue: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300', }; return ( - + {children} ); @@ -43,11 +51,19 @@ function Badge({ children, variant = 'default' }: { children: React.ReactNode; v function StrategyIcon({ strategy }: { strategy: RolloutStrategy }) { if (strategy === 'percentage') return ; - if (strategy === 'targeting') return ; + if (strategy === 'targeting') return ; return ; } -function Toggle({ checked, onChange, disabled = false }: { checked: boolean; onChange: (v: boolean) => void; disabled?: boolean }) { +function Toggle({ + checked, + onChange, + disabled = false, +}: { + checked: boolean; + onChange: (v: boolean) => void; + disabled?: boolean; +}) { return ( @@ -150,7 +178,9 @@ function FlagForm({ initial, onSave, onClose }: FlagFormProps) { {/* Description */}
- +