From 5be1a92cb3bf43ddabeb1e808dd7ebda2669689e Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Thu, 7 Aug 2025 15:05:27 +0800 Subject: [PATCH 01/50] fix: update API endpoint paths and environment variable for devlog integration --- .vscode/mcp.json | 6 ++--- packages/mcp/src/api/devlog-api-client.ts | 31 +++++++++-------------- packages/mcp/src/index.ts | 2 +- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index a736a56f..1a2825cb 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -9,14 +9,14 @@ "type": "stdio" }, "devlog": { - "type": "stdio", "command": "npx", "args": [ "@codervisor/devlog-mcp@dev" ], + "type": "stdio", "env": { - // "DEVLOG_BASE_URL": "http://localhost:3200" - } + // "DEVLOG_API_URL": "http://localhost:3200/api" + }, }, "sequential-thinking": { "command": "npx", diff --git a/packages/mcp/src/api/devlog-api-client.ts b/packages/mcp/src/api/devlog-api-client.ts index f0ba5142..f64fcc97 100644 --- a/packages/mcp/src/api/devlog-api-client.ts +++ b/packages/mcp/src/api/devlog-api-client.ts @@ -195,23 +195,23 @@ export class DevlogApiClient { */ private getProjectEndpoint(): string { const projectId = this.currentProjectId || 'default'; - return `/api/projects/${projectId}`; + return `/projects/${projectId}`; } // Project Management async listProjects(): Promise { - const response = await this.get('/api/projects'); + const response = await this.get('/projects'); return this.unwrapApiResponse(response); } async getProject(projectId?: number): Promise { const id = projectId || this.currentProjectId || 0; - const response = await this.get(`/api/projects/${id}`); + const response = await this.get(`/projects/${id}`); return this.unwrapApiResponse(response); } async createProject(data: any): Promise { - const response = await this.post('/api/projects', data); + const response = await this.post('/projects', data); return this.unwrapApiResponse(response); } @@ -308,7 +308,9 @@ export class DevlogApiClient { // Health check async healthCheck(): Promise<{ status: string; timestamp: string }> { try { - const response = await this.get('/api/health'); + console.log('Performing health check...'); + console.log('API Base URL:', this.baseUrl); + const response = await this.get('/health'); const result = this.unwrapApiResponse<{ status: string; timestamp: string }>(response); // Validate the health check response @@ -319,20 +321,11 @@ export class DevlogApiClient { return result; } catch (error) { // If health endpoint doesn't exist, try a basic endpoint - console.warn('Health endpoint failed, trying projects endpoint as backup...'); - try { - await this.get('/api/projects'); - return { - status: 'ok', - timestamp: new Date().toISOString(), - }; - } catch (backupError) { - throw new DevlogApiClientError( - `Health check failed: ${error instanceof Error ? error.message : String(error)}`, - 0, - error, - ); - } + throw new DevlogApiClientError( + `Health check failed: ${error instanceof Error ? error.message : String(error)}`, + 0, + error, + ); } } diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 8f00619c..bd873788 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -101,7 +101,7 @@ async function main() { // Create adapter configuration const config: MCPAdapterConfig = { apiClient: { - baseUrl: process.env.DEVLOG_BASE_URL || 'https://devlog.codervisor.dev', + baseUrl: process.env.DEVLOG_API_URL || 'https://devlog.codervisor.dev/api', timeout: 30000, retries: 3, }, From 370b6c111ebc511ea421638ba802311143cd1e0b Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Thu, 7 Aug 2025 15:18:22 +0800 Subject: [PATCH 02/50] feat: add axios dependency and integrate axios for API requests in DevlogApiClient --- packages/mcp/package.json | 1 + packages/mcp/src/api/devlog-api-client.ts | 129 +++++++++++++--------- pnpm-lock.yaml | 3 + 3 files changed, 78 insertions(+), 55 deletions(-) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 33099f31..d79252b4 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -54,6 +54,7 @@ "dependencies": { "@codervisor/devlog-core": "workspace:*", "@modelcontextprotocol/sdk": "^1.0.0", + "axios": "^1.11.0", "better-sqlite3": "^11.10.0", "dotenv": "16.5.0", "zod": "^3.22.4" diff --git a/packages/mcp/src/api/devlog-api-client.ts b/packages/mcp/src/api/devlog-api-client.ts index f64fcc97..d2589e95 100644 --- a/packages/mcp/src/api/devlog-api-client.ts +++ b/packages/mcp/src/api/devlog-api-client.ts @@ -3,6 +3,7 @@ * Provides project-aware interface to @codervisor/devlog-web API endpoints */ +import axios, { type AxiosInstance, type AxiosError, type AxiosRequestConfig } from 'axios'; import type { CreateDevlogRequest, DevlogEntry, @@ -42,15 +43,28 @@ export class DevlogApiClientError extends Error { * HTTP API client for devlog operations */ export class DevlogApiClient { - private baseUrl: string; - private timeout: number; + private axiosInstance: AxiosInstance; private retries: number; private currentProjectId: number | null = null; constructor(config: DevlogApiClientConfig) { - this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash - this.timeout = config.timeout || 30000; this.retries = config.retries || 3; + + this.axiosInstance = axios.create({ + baseURL: config.baseUrl.replace(/\/$/, ''), // Remove trailing slash + timeout: config.timeout || 30000, + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Setup response interceptor for error handling + this.axiosInstance.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + throw this.handleAxiosError(error); + } + ); } /** @@ -67,59 +81,68 @@ export class DevlogApiClient { return this.currentProjectId; } + /** + * Handle axios errors and convert to DevlogApiClientError + */ + private handleAxiosError(error: AxiosError): DevlogApiClientError { + let errorMessage = error.message; + let statusCode: number | undefined; + let responseData: any; + + if (error.response) { + // Server responded with error status + statusCode = error.response.status; + responseData = error.response.data; + + // Try to extract error message from response + if (responseData) { + if (typeof responseData === 'string') { + errorMessage = responseData; + } else if (typeof responseData === 'object') { + errorMessage = responseData.error?.message || responseData.message || error.message; + } + } + + errorMessage = `HTTP ${statusCode}: ${errorMessage}`; + } else if (error.request) { + // Request was made but no response received + errorMessage = `Network error: ${error.message}`; + } + + return new DevlogApiClientError(errorMessage, statusCode, responseData); + } + /** * Make HTTP request with retry logic */ private async makeRequest( endpoint: string, - options: RequestInit = {}, + options: AxiosRequestConfig = {}, attempt = 1, - ): Promise { - const url = `${this.baseUrl}${endpoint}`; - - const requestOptions: RequestInit = { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers, - }, - signal: AbortSignal.timeout(this.timeout), - }; - + ): Promise { try { - const response = await fetch(url, requestOptions); - - if (!response.ok) { - let errorText: string; - try { - errorText = await response.text(); - } catch { - errorText = response.statusText; - } - - // Try to parse JSON error response - let errorData = errorText; - try { - const parsed = JSON.parse(errorText); - errorData = parsed.error?.message || parsed.message || errorText; - } catch { - // Keep original text if not JSON - } + const response = await this.axiosInstance.request({ + url: endpoint, + ...options, + }); - throw new DevlogApiClientError( - `HTTP ${response.status}: ${errorData}`, - response.status, - errorText, - ); - } - - return response; + return response.data; } catch (error) { - if (attempt < this.retries && !(error instanceof DevlogApiClientError)) { + const axiosError = error as AxiosError; + + // Only retry on network errors or 5xx server errors, not on client errors + const shouldRetry = attempt < this.retries && ( + !axiosError.response || + (axiosError.response.status >= 500) + ); + + if (shouldRetry) { console.warn(`Request failed (attempt ${attempt}/${this.retries}), retrying...`); await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); return this.makeRequest(endpoint, options, attempt + 1); } + + // Re-throw the error (will be handled by the response interceptor) throw error; } } @@ -156,38 +179,34 @@ export class DevlogApiClient { * GET request helper */ private async get(endpoint: string): Promise { - const response = await this.makeRequest(endpoint, { method: 'GET' }); - return response.json(); + return this.makeRequest(endpoint, { method: 'GET' }); } /** * POST request helper */ private async post(endpoint: string, data?: any): Promise { - const response = await this.makeRequest(endpoint, { + return this.makeRequest(endpoint, { method: 'POST', - body: data ? JSON.stringify(data) : undefined, + data: data, }); - return response.json(); } /** * PUT request helper */ private async put(endpoint: string, data?: any): Promise { - const response = await this.makeRequest(endpoint, { + return this.makeRequest(endpoint, { method: 'PUT', - body: data ? JSON.stringify(data) : undefined, + data: data, }); - return response.json(); } /** * DELETE request helper */ private async delete(endpoint: string): Promise { - const response = await this.makeRequest(endpoint, { method: 'DELETE' }); - return response.json(); + return this.makeRequest(endpoint, { method: 'DELETE' }); } /** @@ -309,7 +328,7 @@ export class DevlogApiClient { async healthCheck(): Promise<{ status: string; timestamp: string }> { try { console.log('Performing health check...'); - console.log('API Base URL:', this.baseUrl); + console.log('API Base URL:', this.axiosInstance.defaults.baseURL); const response = await this.get('/health'); const result = this.unwrapApiResponse<{ status: string; timestamp: string }>(response); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87755e59..ca8388aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.0.0 version: 1.13.0 + axios: + specifier: ^1.11.0 + version: 1.11.0 better-sqlite3: specifier: ^11.10.0 version: 11.10.0 From 086dc2d829cead0ee33917fb8f7b03ae0c16a107 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Thu, 7 Aug 2025 17:10:56 +0800 Subject: [PATCH 03/50] feat: implement centralized logging and proxy support in MCP server --- packages/core/src/utils/env-loader.ts | 8 +- packages/mcp/package.json | 2 + packages/mcp/src/adapters/mcp-adapter.ts | 7 +- packages/mcp/src/api/devlog-api-client.ts | 39 +++++++- packages/mcp/src/config/mcp-config.ts | 16 +-- packages/mcp/src/index.ts | 46 ++++++--- packages/mcp/src/server/index.ts | 5 + packages/mcp/src/server/server-manager.ts | 110 +++++++++++++++++++++ packages/mcp/src/utils/schema-converter.ts | 3 +- pnpm-lock.yaml | 19 ++++ 10 files changed, 220 insertions(+), 35 deletions(-) create mode 100644 packages/mcp/src/server/index.ts create mode 100644 packages/mcp/src/server/server-manager.ts diff --git a/packages/core/src/utils/env-loader.ts b/packages/core/src/utils/env-loader.ts index f9c6faa0..aa8fb40b 100644 --- a/packages/core/src/utils/env-loader.ts +++ b/packages/core/src/utils/env-loader.ts @@ -6,7 +6,6 @@ import * as dotenv from 'dotenv'; import * as path from 'path'; import * as fs from 'fs'; -import { fileURLToPath } from 'url'; /** * Find the monorepo root directory by looking for package.json with workspaces or pnpm-workspace.yaml @@ -84,19 +83,14 @@ export function loadRootEnv(): void { const result = dotenv.config({ path: envPath }); if (!result.error) { - console.log(`✅ Loaded environment variables from ${envPath}`); loaded = true; break; } } - if (!loaded) { - console.log(`ℹ️ No .env files found in ${rootDir}, using system environment variables only`); - } - isLoaded = true; } catch (error) { - console.warn(`⚠️ Failed to load environment variables:`, error); + console.error(`⚠️ Failed to load environment variables:`, error); isLoaded = true; // Mark as loaded to prevent retry loops } } diff --git a/packages/mcp/package.json b/packages/mcp/package.json index d79252b4..8fe70681 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -54,9 +54,11 @@ "dependencies": { "@codervisor/devlog-core": "workspace:*", "@modelcontextprotocol/sdk": "^1.0.0", + "@types/tunnel": "0.0.7", "axios": "^1.11.0", "better-sqlite3": "^11.10.0", "dotenv": "16.5.0", + "tunnel": "0.0.6", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/mcp/src/adapters/mcp-adapter.ts b/packages/mcp/src/adapters/mcp-adapter.ts index e4a3098e..d7b92bf0 100644 --- a/packages/mcp/src/adapters/mcp-adapter.ts +++ b/packages/mcp/src/adapters/mcp-adapter.ts @@ -10,6 +10,7 @@ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { DevlogApiClient, type DevlogApiClientConfig } from '../api/devlog-api-client.js'; +import { logger } from '../server/index.js'; import type { AddDevlogNoteArgs, CreateDevlogArgs, @@ -66,10 +67,10 @@ export class MCPAdapter { try { await this.apiClient.healthCheck(); - console.log('✅ MCP adapter initialized successfully'); + logger.info('✅ MCP adapter initialized successfully'); this.initialized = true; } catch (error) { - console.error('❌ Failed to initialize MCP adapter:', error); + logger.error('❌ Failed to initialize MCP adapter:', error); throw new Error( `Failed to connect to devlog API: ${error instanceof Error ? error.message : String(error)}`, ); @@ -127,7 +128,7 @@ export class MCPAdapter { private handleError(operation: string, error: unknown): CallToolResult { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`${operation}:`, errorMessage); + logger.error(`${operation}:`, { error: errorMessage }); return this.toStandardResponse(false, undefined, undefined, `${operation}: ${errorMessage}`); } diff --git a/packages/mcp/src/api/devlog-api-client.ts b/packages/mcp/src/api/devlog-api-client.ts index d2589e95..f4a99515 100644 --- a/packages/mcp/src/api/devlog-api-client.ts +++ b/packages/mcp/src/api/devlog-api-client.ts @@ -4,6 +4,7 @@ */ import axios, { type AxiosInstance, type AxiosError, type AxiosRequestConfig } from 'axios'; +import tunnel from 'tunnel'; import type { CreateDevlogRequest, DevlogEntry, @@ -15,6 +16,7 @@ import type { SortOptions, UpdateDevlogRequest, } from '@codervisor/devlog-core'; +import { logger } from '../server/index.js'; export interface DevlogApiClientConfig { /** Base URL for the web API */ @@ -50,12 +52,34 @@ export class DevlogApiClient { constructor(config: DevlogApiClientConfig) { this.retries = config.retries || 3; + // Create HTTPS agent for proxy tunneling to fix redirect loops + let httpsAgent; + const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY; + + if (proxyUrl && config.baseUrl.includes('devlog.codervisor.dev')) { + // Use tunnel agent for devlog.codervisor.dev to avoid redirect loops + const proxyMatch = proxyUrl.match(/https?:\/\/([^:]+):(\d+)/); + if (proxyMatch) { + httpsAgent = tunnel.httpsOverHttp({ + proxy: { + host: proxyMatch[1], + port: parseInt(proxyMatch[2]), + }, + }); + } + } + this.axiosInstance = axios.create({ baseURL: config.baseUrl.replace(/\/$/, ''), // Remove trailing slash timeout: config.timeout || 30000, headers: { 'Content-Type': 'application/json', }, + // Use custom agent if we created one, otherwise let axios handle proxy normally + ...(httpsAgent && { + httpsAgent, + proxy: false, // Disable built-in proxy when using custom agent + }), }); // Setup response interceptor for error handling @@ -89,6 +113,7 @@ export class DevlogApiClient { let statusCode: number | undefined; let responseData: any; + logger.error('API request failed', { error: error.message, code: error.code }); if (error.response) { // Server responded with error status statusCode = error.response.status; @@ -107,6 +132,13 @@ export class DevlogApiClient { } else if (error.request) { // Request was made but no response received errorMessage = `Network error: ${error.message}`; + + // Handle specific proxy/network related errors with helpful context + if (error.code === 'ERR_FR_TOO_MANY_REDIRECTS') { + errorMessage += '. Fixed: Using tunnel agent to avoid proxy redirect loops.'; + } else if (error.code === 'ECONNRESET') { + errorMessage += '. The server may be unreachable from this network.'; + } } return new DevlogApiClientError(errorMessage, statusCode, responseData); @@ -121,6 +153,7 @@ export class DevlogApiClient { attempt = 1, ): Promise { try { + logger.debug(`Making request to ${endpoint}`, options); const response = await this.axiosInstance.request({ url: endpoint, ...options, @@ -137,7 +170,7 @@ export class DevlogApiClient { ); if (shouldRetry) { - console.warn(`Request failed (attempt ${attempt}/${this.retries}), retrying...`); + logger.warn(`Request failed (attempt ${attempt}/${this.retries}), retrying...`); await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); return this.makeRequest(endpoint, options, attempt + 1); } @@ -327,8 +360,8 @@ export class DevlogApiClient { // Health check async healthCheck(): Promise<{ status: string; timestamp: string }> { try { - console.log('Performing health check...'); - console.log('API Base URL:', this.axiosInstance.defaults.baseURL); + logger.info('Performing health check...'); + logger.debug('API Base URL', { baseURL: this.axiosInstance.defaults.baseURL }); const response = await this.get('/health'); const result = this.unwrapApiResponse<{ status: string; timestamp: string }>(response); diff --git a/packages/mcp/src/config/mcp-config.ts b/packages/mcp/src/config/mcp-config.ts index 28b095fc..cefb5b4c 100644 --- a/packages/mcp/src/config/mcp-config.ts +++ b/packages/mcp/src/config/mcp-config.ts @@ -3,6 +3,8 @@ * Uses HTTP API client for secure and isolated access to devlog operations */ +import { logger } from '../server/index.js'; + export interface MCPServerConfig { /** Default project ID */ defaultProjectId?: string; @@ -71,11 +73,11 @@ export function validateMCPConfig(config: MCPServerConfig): void { * Print configuration summary for debugging */ export function printConfigSummary(config: MCPServerConfig): void { - console.log('\n=== MCP Server Configuration ==='); - console.log(`Default Project: ${config.defaultProjectId}`); - console.log(`Web API URL: ${config.webApi.baseUrl}`); - console.log(`Timeout: ${config.webApi.timeout}ms`); - console.log(`Retries: ${config.webApi.retries}`); - console.log(`Auto-discover: ${config.webApi.autoDiscover}`); - console.log('================================\n'); + logger.info('\n=== MCP Server Configuration ==='); + logger.info(`Default Project: ${config.defaultProjectId}`); + logger.info(`Web API URL: ${config.webApi.baseUrl}`); + logger.info(`Timeout: ${config.webApi.timeout}ms`); + logger.info(`Retries: ${config.webApi.retries}`); + logger.info(`Auto-discover: ${config.webApi.autoDiscover}`); + logger.info('================================\n'); } diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index bd873788..546c410b 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -11,10 +11,11 @@ loadRootEnv(); import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolRequestSchema, ListToolsRequestSchema, SetLevelRequestSchema, type LoggingLevel } from '@modelcontextprotocol/sdk/types.js'; import { MCPAdapter, type MCPAdapterConfig } from './adapters/index.js'; import { allTools } from './tools/index.js'; import { toolHandlers } from './handlers/tool-handlers.js'; +import { ServerManager, logger } from './server/index.js'; const server = new Server( { @@ -35,12 +36,12 @@ FEATURES: • AI-friendly progress tracking and status workflows • Project-based organization with multi-project support • Duplicate detection and relationship management - -This server provides 10 tools: 7 devlog operations + 3 project management tools.`, +`, }, { capabilities: { tools: {}, + logging: {}, }, }, ); @@ -48,12 +49,21 @@ This server provides 10 tools: 7 devlog operations + 3 project management tools. // Initialize the adapter let adapter: MCPAdapter; +server.setRequestHandler(SetLevelRequestSchema, async (request) => { + logger.setLoggingLevel(request.params.level); + return { success: true, level: request.params.level }; +}); + server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: allTools }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; + console.error(request.params); + + if (name === 'logging/setLevel') { + } try { // Get handler for the tool @@ -85,7 +95,7 @@ async function main() { const defaultProjectStr = projectArgIndex !== -1 && args[projectArgIndex + 1] ? args[projectArgIndex + 1] - : process.env.MCP_DEFAULT_PROJECT || '1'; + : process.env.DEVLOG_DEFAULT_PROJECT || '1'; // Convert to number, defaulting to 1 if invalid let defaultProjectId = 1; @@ -95,9 +105,16 @@ async function main() { defaultProjectId = parsed; } } catch { - console.error(`Invalid project ID '${defaultProjectStr}', using default project 1`); + logger.error(`Invalid project ID '${defaultProjectStr}', using default project 1`); } + const transport = new StdioServerTransport(); + await server.connect(transport); + + // Register server with ServerManager for centralized logging + const serverManager = ServerManager.getInstance(); + serverManager.setServer(server); + // Create adapter configuration const config: MCPAdapterConfig = { apiClient: { @@ -112,25 +129,26 @@ async function main() { adapter = new MCPAdapter(config); await adapter.initialize(); - console.error(`Set current project to: ${defaultProjectId}`); - - const transport = new StdioServerTransport(); - await server.connect(transport); - - console.error(`Devlog MCP Server started with project: ${defaultProjectId}`); + logger.info(`Set current project to: ${defaultProjectId}`); + logger.info(`Devlog MCP Server started with project: ${defaultProjectId}`); } // Cleanup on process exit process.on('SIGINT', async () => { - console.error('Shutting down server...'); + logger.info('Shutting down server...'); await adapter.dispose(); + ServerManager.getInstance().dispose(); process.exit(0); }); process.on('SIGTERM', async () => { - console.error('Shutting down server...'); + logger.info('Shutting down server...'); await adapter.dispose(); + ServerManager.getInstance().dispose(); process.exit(0); }); -main().catch(console.error); +main().catch((error) => { + logger.error('Failed to start MCP server:', error); + process.exit(1); +}); diff --git a/packages/mcp/src/server/index.ts b/packages/mcp/src/server/index.ts new file mode 100644 index 00000000..ff0341eb --- /dev/null +++ b/packages/mcp/src/server/index.ts @@ -0,0 +1,5 @@ +/** + * Server utilities and exports + */ + +export { ServerManager, logger } from './server-manager.js'; diff --git a/packages/mcp/src/server/server-manager.ts b/packages/mcp/src/server/server-manager.ts new file mode 100644 index 00000000..b442fc14 --- /dev/null +++ b/packages/mcp/src/server/server-manager.ts @@ -0,0 +1,110 @@ +/** + * Centralized MCP Server Manager + * Provides singleton access to server instance and unified logging + */ + +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js'; + +/** + * Singleton manager for MCP Server instance and logging + */ +export class ServerManager { + private static instance: ServerManager | null = null; + private server: Server | null = null; + private loggingLevel: LoggingLevel = 'info'; + + private constructor() {} + + static getInstance(): ServerManager { + if (!ServerManager.instance) { + ServerManager.instance = new ServerManager(); + } + return ServerManager.instance; + } + + /** + * Set the MCP Server instance + */ + setServer(server: Server): void { + this.server = server; + } + + /** + * Get the MCP Server instance + */ + getServer(): Server | null { + return this.server; + } + + /** + * Set logging level + */ + setLoggingLevel(level: LoggingLevel): void { + this.loggingLevel = level; + } + + /** + * Send a logging message to the MCP client + */ + private sendLog(level: LoggingLevel, message: string, data?: any): void { + if (this.server) { + try { + this.server.sendLoggingMessage({ + level, + data: data ? `${message} ${JSON.stringify(data)}` : message + }); + } catch (error) { + console.error(`Failed to send log to MCP client: ${error}`); + console[level === 'error' ? 'error' : level === 'warning' ? 'warn' : 'log'](message, data); + } + } else { + // Fallback to console if server not available + console[level === 'error' ? 'error' : level === 'warning' ? 'warn' : 'log'](message, data); + } + } + + /** + * Log info message + */ + info(message: string, data?: any): void { + this.sendLog('info', message, data); + } + + /** + * Log debug message + */ + debug(message: string, data?: any): void { + if (this.loggingLevel === 'debug') { + this.sendLog('debug', message, data); + } + } + + /** + * Log warning message + */ + warn(message: string, data?: any): void { + if (this.loggingLevel !== 'error') { + this.sendLog('warning', message, data); + } + } + + /** + * Log error message + */ + error(message: string, data?: any): void { + this.sendLog('error', message, data); + } + + /** + * Cleanup + */ + dispose(): void { + this.server = null; + } +} + +/** + * Global logger instance - use this throughout the MCP server + */ +export const logger = ServerManager.getInstance(); diff --git a/packages/mcp/src/utils/schema-converter.ts b/packages/mcp/src/utils/schema-converter.ts index 9024bbb8..bc9ca963 100644 --- a/packages/mcp/src/utils/schema-converter.ts +++ b/packages/mcp/src/utils/schema-converter.ts @@ -6,6 +6,7 @@ */ import { z } from 'zod'; +import { logger } from '../server/index.js'; /** * Convert a Zod schema to JSON Schema format for MCP tool inputSchema @@ -128,7 +129,7 @@ function zodToJsonSchemaRecursive(def: any): any { default: // Fallback for unsupported types - console.warn(`Unsupported Zod type: ${def.typeName}, falling back to any`); + logger.warn(`Unsupported Zod type: ${def.typeName}, falling back to any`); return {}; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca8388aa..7abea1ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.0.0 version: 1.13.0 + '@types/tunnel': + specifier: 0.0.7 + version: 0.0.7 axios: specifier: ^1.11.0 version: 1.11.0 @@ -213,6 +216,9 @@ importers: dotenv: specifier: 16.5.0 version: 16.5.0 + tunnel: + specifier: 0.0.6 + version: 0.0.6 zod: specifier: ^3.22.4 version: 3.25.67 @@ -1548,6 +1554,9 @@ packages: '@types/semver@7.7.0': resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/tunnel@0.0.7': + resolution: {integrity: sha512-VYKjZSmb2PvUwXoux4Gy4LAk7kzOB1ktkjyL4lxvpkqL2adgR+Qrh/yFyWluvJgIXWFicqs7XuzPI2NbTO/r3Q==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -3762,6 +3771,10 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + turbo-darwin-64@2.5.5: resolution: {integrity: sha512-RYnTz49u4F5tDD2SUwwtlynABNBAfbyT2uU/brJcyh5k6lDLyNfYKdKmqd3K2ls4AaiALWrFKVSBsiVwhdFNzQ==} cpu: [x64] @@ -5098,6 +5111,10 @@ snapshots: '@types/semver@7.7.0': {} + '@types/tunnel@0.0.7': + dependencies: + '@types/node': 20.19.1 + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -7718,6 +7735,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + tunnel@0.0.6: {} + turbo-darwin-64@2.5.5: optional: true From 4cf9a99aca21a4de4145446fc058f6ae5250ec48 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Thu, 7 Aug 2025 23:15:58 +0800 Subject: [PATCH 04/50] fix: remove unnecessary console error logging in CallToolRequestHandler --- packages/mcp/src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 546c410b..2f093b86 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -60,10 +60,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; - console.error(request.params); - - if (name === 'logging/setLevel') { - } try { // Get handler for the tool From 6d3861d1fd7a4df124da72453633352615d46bb5 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 8 Aug 2025 00:05:49 +0800 Subject: [PATCH 05/50] fix: improve layout and styling in DevlogList component --- .../app/components/features/devlogs/DevlogList.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/web/app/components/features/devlogs/DevlogList.tsx b/packages/web/app/components/features/devlogs/DevlogList.tsx index d70f7811..bc68d485 100644 --- a/packages/web/app/components/features/devlogs/DevlogList.tsx +++ b/packages/web/app/components/features/devlogs/DevlogList.tsx @@ -335,8 +335,8 @@ export function DevlogList({

No devlogs found

) : ( -
- +
+
@@ -362,7 +362,7 @@ export function DevlogList({ Actions - + {loading ? Array.from({ length: 20 }).map((_, i) => ( @@ -401,7 +401,7 @@ export function DevlogList({ : devlogs.map((devlog) => ( onViewDevlog(devlog)} > e.stopPropagation()}> @@ -460,10 +460,13 @@ export function DevlogList({
+ {/* Gutter */} +
+ {/* Pagination */} {pagination && ( {})} From fbf1a9a80c7d9308c07d73f67659ae915935c62c Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 8 Aug 2025 12:01:14 +0800 Subject: [PATCH 06/50] feat: Implement name-based routing for projects and devlogs - Refactored project ID handling to support GitHub-style project names. - Introduced RouteParams and ServiceHelper methods for parsing and validating project identifiers. - Updated API routes to utilize the new name-based routing logic. - Enhanced project and devlog components to resolve project IDs based on names. - Added ProjectResolver component for handling project resolution and redirects. - Created project URL utilities for generating URLs based on project names. - Implemented migration script to update existing project names to follow GitHub naming conventions. - Added validation utilities for project names, ensuring they meet specified criteria. --- packages/core/src/entities/project.entity.ts | 2 +- packages/core/src/services/project-service.ts | 20 +++ packages/core/src/types/project.ts | 2 +- packages/core/src/utils/index.ts | 1 + packages/core/src/utils/project-name.ts | 114 ++++++++++++++++ packages/core/src/utils/project-slug.ts | 114 ++++++++++++++++ .../core/src/validation/project-schemas.ts | 11 +- .../app/api/projects/[id]/devlogs/route.ts | 58 ++++---- .../api/projects/[id]/devlogs/search/route.ts | 26 ++-- packages/web/app/api/projects/[id]/route.ts | 18 +-- .../web/app/components/ProjectResolver.tsx | 101 ++++++++++++++ packages/web/app/components/index.ts | 1 + packages/web/app/lib/api/api-utils.ts | 54 ++++++-- packages/web/app/lib/index.ts | 3 + packages/web/app/lib/project-urls.ts | 64 +++++++++ packages/web/app/lib/routing/route-params.ts | 34 ++++- .../projects/[id]/devlogs/[devlogId]/page.tsx | 15 +- .../web/app/projects/[id]/devlogs/page.tsx | 13 +- packages/web/app/projects/[id]/page.tsx | 18 ++- packages/web/app/schemas/project.ts | 15 +- packages/web/tests/api.test.ts | 81 +++++++---- scripts/migrate-project-names.js | 128 ++++++++++++++++++ 22 files changed, 785 insertions(+), 108 deletions(-) create mode 100644 packages/core/src/utils/project-name.ts create mode 100644 packages/core/src/utils/project-slug.ts create mode 100644 packages/web/app/components/ProjectResolver.tsx create mode 100644 packages/web/app/lib/project-urls.ts create mode 100644 scripts/migrate-project-names.js diff --git a/packages/core/src/entities/project.entity.ts b/packages/core/src/entities/project.entity.ts index 60a6b842..c8c31000 100644 --- a/packages/core/src/entities/project.entity.ts +++ b/packages/core/src/entities/project.entity.ts @@ -15,7 +15,7 @@ export class ProjectEntity { @PrimaryGeneratedColumn() id!: number; - @Column({ type: 'varchar', length: 255 }) + @Column({ type: 'varchar', length: 255, unique: true }) name!: string; @Column({ type: 'text', nullable: true }) diff --git a/packages/core/src/services/project-service.ts b/packages/core/src/services/project-service.ts index 4279203e..64efba41 100644 --- a/packages/core/src/services/project-service.ts +++ b/packages/core/src/services/project-service.ts @@ -75,6 +75,26 @@ export class ProjectService { return entity.toProjectMetadata(); } + async getByName(name: string): Promise { + await this.ensureInitialized(); // Ensure initialization + + // Case-insensitive lookup using TypeORM's ILike operator + const entity = await this.repository + .createQueryBuilder('project') + .where('LOWER(project.name) = LOWER(:name)', { name }) + .getOne(); + + if (!entity) { + return null; + } + + // Update last accessed time + entity.lastAccessedAt = new Date(); + await this.repository.save(entity); + + return entity.toProjectMetadata(); + } + async create(project: Omit): Promise { await this.ensureInitialized(); // Ensure initialization diff --git a/packages/core/src/types/project.ts b/packages/core/src/types/project.ts index b0882883..73121559 100644 --- a/packages/core/src/types/project.ts +++ b/packages/core/src/types/project.ts @@ -13,7 +13,7 @@ export interface Project { /** Unique project identifier */ id: number; - /** Human-readable project name */ + /** Human-readable project name (also used as URL slug) */ name: string; /** Optional project description */ diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index edab13c3..5700c291 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -9,3 +9,4 @@ export * from './env-loader.js'; export * from './field-change-tracking.js'; export * from './change-history.js'; export * from './key-generator.js'; +export * from './project-name.js'; diff --git a/packages/core/src/utils/project-name.ts b/packages/core/src/utils/project-name.ts new file mode 100644 index 00000000..946940ec --- /dev/null +++ b/packages/core/src/utils/project-name.ts @@ -0,0 +1,114 @@ +/** + * Project name utilities following GitHub repository naming conventions + */ + +/** + * Generate a URL-safe slug from a project name for routing purposes. + * GitHub allows mixed case but we'll use lowercase for URL consistency. + */ +export function generateSlugFromName(name: string): string { + if (!name || typeof name !== 'string') { + throw new Error('Project name is required to generate slug'); + } + + return name.toLowerCase(); +} + +/** + * Validate a project name follows GitHub repository naming conventions: + * - Can contain letters (a-z, A-Z), numbers (0-9), hyphens (-), underscores (_) + * - Cannot start or end with hyphens + * - Must not be empty + * - Length between 1-100 characters + */ +export function validateProjectName(name: string): boolean { + if (!name || typeof name !== 'string') { + return false; + } + + // Must contain only letters, numbers, hyphens, underscores + if (!/^[a-zA-Z0-9_-]+$/.test(name)) { + return false; + } + + // Cannot start or end with hyphens + if (name.startsWith('-') || name.endsWith('-')) { + return false; + } + + // Must have at least one character + if (name.length === 0) { + return false; + } + + // Reasonable length limit (GitHub uses 100) + if (name.length > 100) { + return false; + } + + return true; +} + +/** + * Generate a unique project name by appending numbers if needed + * This handles case-insensitive collisions + */ +export function generateUniqueProjectName( + baseName: string, + existingNames: string[] +): string { + if (!validateProjectName(baseName)) { + throw new Error(`Base name "${baseName}" does not follow GitHub naming conventions`); + } + + // Convert existing names to lowercase for case-insensitive comparison + const existingLowercase = existingNames.map(name => name.toLowerCase()); + const baseLowercase = baseName.toLowerCase(); + + if (!existingLowercase.includes(baseLowercase)) { + return baseName; + } + + // Try appending numbers until we find a unique name + let counter = 1; + let uniqueName: string; + + do { + uniqueName = `${baseName}-${counter}`; + counter++; + } while (existingLowercase.includes(uniqueName.toLowerCase()) && counter < 1000); + + if (counter >= 1000) { + throw new Error('Unable to generate unique project name after 1000 attempts'); + } + + return uniqueName; +} + +/** + * Check if a string is a valid project name identifier (name only, no numeric IDs) + */ +export function isValidProjectIdentifier(identifier: string): { + type: 'name'; + valid: boolean; +} { + // Explicitly reject purely numeric strings (former IDs) + const numericId = parseInt(identifier, 10); + if (!isNaN(numericId) && numericId.toString() === identifier) { + return { type: 'name', valid: false }; + } + + // Only accept valid project names + if (validateProjectName(identifier)) { + return { type: 'name', valid: true }; + } + + return { type: 'name', valid: false }; +} + +/** + * Check if two project names are the same (case-insensitive) + */ +export function areProjectNamesEqual(name1: string, name2: string): boolean { + return name1.toLowerCase() === name2.toLowerCase(); +} diff --git a/packages/core/src/utils/project-slug.ts b/packages/core/src/utils/project-slug.ts new file mode 100644 index 00000000..2c48013f --- /dev/null +++ b/packages/core/src/utils/project-slug.ts @@ -0,0 +1,114 @@ +/** + * Project name utilities following GitHub repository naming conventions + * Names serve as both display names and URL slugs + */ + +/** + * Generate a valid project name following GitHub naming rules: + * - Lowercase only (for URL compatibility) + * - Replace spaces with hyphens + * - Remove invalid characters (keep only a-z, 0-9, hyphens, underscores) + * - Remove leading/trailing hyphens + * - Collapse consecutive hyphens + * - Must not be empty + */ +export function normalizeProjectName(name: string): string { + if (!name || typeof name !== 'string') { + throw new Error('Project name is required'); + } + + const normalized = name + .toLowerCase() + .trim() + .replace(/\s+/g, '-') // spaces to hyphens + .replace(/[^a-z0-9\-_]/g, '') // remove invalid chars (keep only a-z0-9-_) + .replace(/^-+|-+$/g, '') // trim leading/trailing hyphens + .replace(/-+/g, '-'); // collapse consecutive hyphens + + if (!normalized) { + throw new Error('Project name must contain at least one valid character (a-z, 0-9, -, _)'); + } + + return normalized; +} + +/** + * Validate a project name follows GitHub repository naming conventions + */ +export function validateProjectName(name: string): boolean { + if (!name || typeof name !== 'string') { + return false; + } + + // Must contain only lowercase letters, numbers, hyphens, underscores + if (!/^[a-z0-9_-]+$/.test(name)) { + return false; + } + + // Cannot start or end with hyphens + if (name.startsWith('-') || name.endsWith('-')) { + return false; + } + + // Must have at least one character + if (name.length === 0) { + return false; + } + + // Reasonable length limit (GitHub uses 100, we'll match that) + if (name.length > 100) { + return false; + } + + return true; +} + +/** + * Generate a unique project name by appending numbers if needed + */ +export function generateUniqueProjectName( + baseName: string, + existingNames: string[] +): string { + const normalizedName = normalizeProjectName(baseName); + + if (!existingNames.includes(normalizedName)) { + return normalizedName; + } + + // Try appending numbers until we find a unique name + let counter = 1; + let uniqueName: string; + + do { + uniqueName = `${normalizedName}-${counter}`; + counter++; + } while (existingNames.includes(uniqueName) && counter < 1000); + + if (counter >= 1000) { + throw new Error('Unable to generate unique project name after 1000 attempts'); + } + + return uniqueName; +} + +/** + * Check if a string could be a project identifier (either numeric ID or valid name) + */ +export function isValidProjectIdentifier(identifier: string): { + type: 'id' | 'name'; + valid: boolean; +} { + // Check if it's a valid numeric ID + const numericId = parseInt(identifier, 10); + if (!isNaN(numericId) && numericId > 0 && numericId.toString() === identifier) { + return { type: 'id', valid: true }; + } + + // Check if it's a valid project name + if (validateProjectName(identifier)) { + return { type: 'name', valid: true }; + } + + return { type: 'name', valid: false }; +} diff --git a/packages/core/src/validation/project-schemas.ts b/packages/core/src/validation/project-schemas.ts index 8983ea78..35f2562a 100644 --- a/packages/core/src/validation/project-schemas.ts +++ b/packages/core/src/validation/project-schemas.ts @@ -7,6 +7,7 @@ import { z } from 'zod'; import type { Project } from '../types/project.js'; +import { validateProjectName } from '../utils/project-name.js'; /** * Project creation request schema (excludes auto-generated fields) @@ -16,10 +17,7 @@ export const CreateProjectRequestSchema = z.object({ .string() .min(1, 'Project name is required') .max(100, 'Project name must be less than 100 characters') - .regex( - /^[a-zA-Z0-9\s\-_]+$/, - 'Project name can only contain letters, numbers, spaces, hyphens, and underscores', - ), + .refine(validateProjectName, 'Project name must follow GitHub naming conventions: letters, numbers, hyphens, underscores only; cannot start/end with hyphens'), description: z.string().max(500, 'Description must be less than 500 characters').optional(), }) satisfies z.ZodType>; @@ -31,10 +29,7 @@ export const UpdateProjectRequestSchema = z.object({ .string() .min(1, 'Project name is required') .max(100, 'Project name must be less than 100 characters') - .regex( - /^[a-zA-Z0-9\s\-_]+$/, - 'Project name can only contain letters, numbers, spaces, hyphens, and underscores', - ) + .refine(validateProjectName, 'Project name must follow GitHub naming conventions: letters, numbers, hyphens, underscores only; cannot start/end with hyphens') .optional(), description: z .string() diff --git a/packages/web/app/api/projects/[id]/devlogs/route.ts b/packages/web/app/api/projects/[id]/devlogs/route.ts index adbe29d0..522cacaa 100644 --- a/packages/web/app/api/projects/[id]/devlogs/route.ts +++ b/packages/web/app/api/projects/[id]/devlogs/route.ts @@ -9,13 +9,14 @@ import { ApiValidator, CreateDevlogBodySchema, DevlogListQuerySchema, - ProjectIdParamSchema, } from '@/schemas'; import { ApiErrors, createCollectionResponse, createSimpleCollectionResponse, createSuccessResponse, + RouteParams, + ServiceHelper, } from '@/lib'; import { RealtimeEventType } from '@/lib/realtime'; @@ -25,12 +26,14 @@ export const dynamic = 'force-dynamic'; // GET /api/projects/[id]/devlogs - List devlogs for a project export async function GET(request: NextRequest, { params }: { params: { id: string } }) { try { - // Validate project ID parameter - const paramValidation = ApiValidator.validateParams(params, ProjectIdParamSchema); - if (!paramValidation.success) { - return paramValidation.response; + // Parse and validate project identifier + const paramResult = RouteParams.parseProjectId(params); + if (!paramResult.success) { + return paramResult.response; } + const { identifier, identifierType } = paramResult.data; + // Validate query parameters const url = new URL(request.url); const queryValidation = ApiValidator.validateQuery(url.searchParams, DevlogListQuerySchema); @@ -38,14 +41,16 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri return queryValidation.response; } - const projectService = ProjectService.getInstance(); - const project = await projectService.get(paramValidation.data.id); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + // Create project-aware devlog service - const devlogService = DevlogService.getInstance(paramValidation.data.id); + const devlogService = DevlogService.getInstance(project.id); const queryData = queryValidation.data; const filter: any = {}; @@ -99,40 +104,45 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri // POST /api/projects/[id]/devlogs - Create new devlog entry export async function POST(request: NextRequest, { params }: { params: { id: string } }) { try { - // Validate project ID parameter - const paramValidation = ApiValidator.validateParams(params, ProjectIdParamSchema); - if (!paramValidation.success) { - return paramValidation.response; + // Parse and validate project identifier + const paramResult = RouteParams.parseProjectId(params); + if (!paramResult.success) { + return paramResult.response; } + const { identifier, identifierType } = paramResult.data; + // Validate request body const bodyValidation = await ApiValidator.validateJsonBody(request, CreateDevlogBodySchema); if (!bodyValidation.success) { return bodyValidation.response; } - const projectService = ProjectService.getInstance(); - const project = await projectService.get(paramValidation.data.id); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + // Create project-aware devlog service - const devlogService = DevlogService.getInstance(paramValidation.data.id); + const devlogService = DevlogService.getInstance(project.id); // Add required fields and get next ID const now = new Date().toISOString(); const nextId = await devlogService.getNextId(); - const devlogEntry = { - id: nextId, + const entry = { ...bodyValidation.data, + id: nextId, createdAt: now, updatedAt: now, - projectId: paramValidation.data.id, // Ensure project context + projectId: project.id, // Ensure project context }; - - await devlogService.save(devlogEntry); + + // Save the entry + await devlogService.save(entry); // Retrieve the actual saved entry to ensure we have the correct ID const savedEntry = await devlogService.get(nextId, false); // Don't include notes for performance diff --git a/packages/web/app/api/projects/[id]/devlogs/search/route.ts b/packages/web/app/api/projects/[id]/devlogs/search/route.ts index 54b30b0f..30a671fb 100644 --- a/packages/web/app/api/projects/[id]/devlogs/search/route.ts +++ b/packages/web/app/api/projects/[id]/devlogs/search/route.ts @@ -5,8 +5,8 @@ import { PaginationMeta, ProjectService, } from '@codervisor/devlog-core'; -import { ApiValidator, DevlogSearchQuerySchema, ProjectIdParamSchema } from '@/schemas'; -import { ApiErrors, createSuccessResponse } from '@/lib'; +import { ApiValidator, DevlogSearchQuerySchema } from '@/schemas'; +import { ApiErrors, createSuccessResponse, RouteParams, ServiceHelper } from '@/lib'; // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; @@ -35,12 +35,14 @@ interface SearchResponse { // GET /api/projects/[id]/devlogs/search - Enhanced search for devlogs export async function GET(request: NextRequest, { params }: { params: { id: string } }) { try { - // Validate project ID parameter - const paramValidation = ApiValidator.validateParams(params, ProjectIdParamSchema); - if (!paramValidation.success) { - return paramValidation.response; + // Parse and validate project name parameter + const paramResult = RouteParams.parseProjectId(params); + if (!paramResult.success) { + return paramResult.response; } + const { identifier, identifierType } = paramResult.data; + // Validate query parameters const url = new URL(request.url); const queryValidation = ApiValidator.validateQuery(url.searchParams, DevlogSearchQuerySchema); @@ -48,14 +50,16 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri return queryValidation.response; } - const projectService = ProjectService.getInstance(); - const project = await projectService.get(paramValidation.data.id); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + // Create project-aware devlog service - const devlogService = DevlogService.getInstance(paramValidation.data.id); + const devlogService = DevlogService.getInstance(project.id); const queryData = queryValidation.data; const searchQuery = queryData.q; diff --git a/packages/web/app/api/projects/[id]/route.ts b/packages/web/app/api/projects/[id]/route.ts index db42c770..c04e5e62 100644 --- a/packages/web/app/api/projects/[id]/route.ts +++ b/packages/web/app/api/projects/[id]/route.ts @@ -20,10 +20,10 @@ export const GET = withErrorHandling( return paramResult.response; } - const { projectId } = paramResult.data; + const { identifier, identifierType } = paramResult.data; // Get project using helper - const projectResult = await ServiceHelper.getProjectOrFail(projectId); + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); if (!projectResult.success) { return projectResult.response; } @@ -42,7 +42,7 @@ export const PUT = withErrorHandling( return paramResult.response; } - const { projectId } = paramResult.data; + const { identifier, identifierType } = paramResult.data; // Validate request body (HTTP layer validation) const bodyValidation = await ApiValidator.validateJsonBody(request, UpdateProjectBodySchema); @@ -51,13 +51,13 @@ export const PUT = withErrorHandling( } // Get project and service - const projectResult = await ServiceHelper.getProjectOrFail(projectId); + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); if (!projectResult.success) { return projectResult.response; } - // Update project - const updatedProject = await projectResult.data.projectService.update(projectId, bodyValidation.data); + // Update project using the resolved project ID + const updatedProject = await projectResult.data.projectService.update(projectResult.data.project.id, bodyValidation.data); return createSuccessResponse(updatedProject, { sseEventType: RealtimeEventType.PROJECT_UPDATED }); }, @@ -72,14 +72,16 @@ export const DELETE = withErrorHandling( return paramResult.response; } - const { projectId } = paramResult.data; + const { identifier, identifierType } = paramResult.data; // Get project service - const projectResult = await ServiceHelper.getProjectOrFail(projectId); + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); if (!projectResult.success) { return projectResult.response; } + const projectId = projectResult.data.project.id; + // Delete project await projectResult.data.projectService.delete(projectId); diff --git a/packages/web/app/components/ProjectResolver.tsx b/packages/web/app/components/ProjectResolver.tsx new file mode 100644 index 00000000..4e898a62 --- /dev/null +++ b/packages/web/app/components/ProjectResolver.tsx @@ -0,0 +1,101 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Project } from '@codervisor/devlog-core'; +import { generateSlugFromName } from '@codervisor/devlog-core'; +import { apiClient, ApiError } from '@/lib'; + +interface ProjectResolverProps { + identifier: string; + identifierType: 'id' | 'name'; + children: (projectId: number, project?: Project) => React.ReactNode; + onNotFound?: () => void; +} + +/** + * Resolves a project identifier (ID or name) to a project ID and project data + * Handles URL redirects when using name-based routing + */ +export function ProjectResolver({ + identifier, + identifierType, + children, + onNotFound +}: ProjectResolverProps) { + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const router = useRouter(); + + useEffect(() => { + async function resolveProject() { + try { + setLoading(true); + setError(null); + + // For numeric IDs, we can direct fetch, but for names we need to resolve + const projectData = await apiClient.get(`/api/projects/${identifier}`); + setProject(projectData); + + // If we're using name-based routing but the URL doesn't match the canonical slug, + // redirect to the canonical URL + if (identifierType === 'name') { + const canonicalSlug = generateSlugFromName(projectData.name); + if (identifier !== canonicalSlug) { + // Redirect to canonical URL + const currentPath = window.location.pathname; + const newPath = currentPath.replace(`/projects/${identifier}`, `/projects/${canonicalSlug}`); + router.replace(newPath); + return; + } + } + + } catch (error) { + console.error('Error resolving project:', error); + + // Handle specific API errors + if (error instanceof ApiError && error.status === 404) { + onNotFound?.(); + setError('Project not found'); + } else { + setError('Failed to load project'); + } + } finally { + setLoading(false); + } + } + + resolveProject(); + }, [identifier, identifierType, router, onNotFound]); + + if (loading) { + return ( +
+
+
+

Loading project...

+
+
+ ); + } + + if (error || !project) { + return ( +
+
+

Project Not Found

+

{error || 'The requested project could not be found.'}

+ +
+
+ ); + } + + return <>{children(project.id, project)}; +} diff --git a/packages/web/app/components/index.ts b/packages/web/app/components/index.ts index f501b905..48d44e4c 100644 --- a/packages/web/app/components/index.ts +++ b/packages/web/app/components/index.ts @@ -18,4 +18,5 @@ export * from './features/dashboard'; export * from './features/devlogs'; // Project Components +export { ProjectResolver } from './ProjectResolver'; export * from './project'; diff --git a/packages/web/app/lib/api/api-utils.ts b/packages/web/app/lib/api/api-utils.ts index 2b1a263a..f43d9c42 100644 --- a/packages/web/app/lib/api/api-utils.ts +++ b/packages/web/app/lib/api/api-utils.ts @@ -11,6 +11,7 @@ import type { ResponseMeta, } from '@/schemas/responses'; import { PaginationMeta } from '@codervisor/devlog-core'; +import { isValidProjectIdentifier } from '@codervisor/devlog-core'; import { broadcastUpdate } from '@/lib'; /** @@ -18,48 +19,54 @@ import { broadcastUpdate } from '@/lib'; */ export const RouteParams = { /** - * Parse project ID parameter + * Parse project name parameter (name-only routing) * Usage: /api/projects/[id] */ parseProjectId(params: { id: string }) { try { - const projectId = parseInt(params.id, 10); - if (isNaN(projectId) || projectId <= 0) { + const validation = isValidProjectIdentifier(params.id); + + if (!validation.valid) { return { success: false as const, response: NextResponse.json( - { error: 'Invalid project ID: must be a positive integer' }, + { error: 'Invalid project name: must follow GitHub naming conventions' }, { status: 400 }, ), }; } + // Always name-based routing now return { success: true as const, - data: { projectId }, + data: { + projectId: -1, // Will be resolved by service helper + identifier: params.id, + identifierType: 'name' as const + }, }; } catch (error) { return { success: false as const, - response: NextResponse.json({ error: 'Invalid project ID format' }, { status: 400 }), + response: NextResponse.json({ error: 'Invalid project name format' }, { status: 400 }), }; } }, /** - * Parse project ID and devlog ID parameters + * Parse project name and devlog ID parameters (name-only routing for projects) * Usage: /api/projects/[id]/devlogs/[devlogId] */ parseProjectAndDevlogId(params: { id: string; devlogId: string }) { try { - const projectId = parseInt(params.id, 10); + const projectValidation = isValidProjectIdentifier(params.id); const devlogId = parseInt(params.devlogId, 10); - if (isNaN(projectId) || projectId <= 0) { + if (!projectValidation.valid) { return { success: false as const, response: NextResponse.json( - { error: 'Invalid project ID: must be a positive integer' }, + { error: 'Invalid project name: must follow GitHub naming conventions' }, { status: 400 }, ), }; @@ -75,9 +82,15 @@ export const RouteParams = { }; } + // Always name-based routing for projects now return { success: true as const, - data: { projectId, devlogId }, + data: { + projectId: -1, // Will be resolved by service helper + devlogId, + identifier: params.id, + identifierType: 'name' as const + }, }; } catch (error) { return { @@ -94,7 +107,7 @@ export const RouteParams = { */ export class ServiceHelper { /** - * Get project and ensure it exists + * Get project by ID and ensure it exists */ static async getProjectOrFail(projectId: number) { const { ProjectService } = await import('@codervisor/devlog-core'); @@ -107,6 +120,23 @@ export class ServiceHelper { return { success: true as const, data: { project, projectService } }; } + /** + * Get project by name and ensure it exists (case-insensitive lookup) + */ + static async getProjectByIdentifierOrFail(identifier: string, identifierType: 'name') { + const { ProjectService } = await import('@codervisor/devlog-core'); + const projectService = ProjectService.getInstance(); + + // Only name-based routing supported now + const project = await projectService.getByName(identifier); + + if (!project) { + return { success: false as const, response: ApiErrors.projectNotFound() }; + } + + return { success: true as const, data: { project, projectService } }; + } + /** * Get devlog service for a project */ diff --git a/packages/web/app/lib/index.ts b/packages/web/app/lib/index.ts index 0b8edcc3..84a64bc4 100644 --- a/packages/web/app/lib/index.ts +++ b/packages/web/app/lib/index.ts @@ -18,6 +18,9 @@ export * from './devlog'; // Routing utilities export * from './routing'; +// Project URL utilities +export * from './project-urls'; + // Realtime utilities export * from './realtime'; diff --git a/packages/web/app/lib/project-urls.ts b/packages/web/app/lib/project-urls.ts new file mode 100644 index 00000000..508f9a65 --- /dev/null +++ b/packages/web/app/lib/project-urls.ts @@ -0,0 +1,64 @@ +/** + * URL generation utilities for name-based project routing + */ + +import { generateSlugFromName } from '@codervisor/devlog-core'; +import { apiClient } from '@/lib'; + +/** + * Generate project URLs using name-based routing + * Falls back to ID-based routing if name is not available + */ +export class ProjectUrls { + /** + * Generate URL for project main page + */ + static project(projectId: number, projectName?: string): string { + if (projectName) { + return `/projects/${generateSlugFromName(projectName)}`; + } + return `/projects/${projectId}`; + } + + /** + * Generate URL for project devlogs list + */ + static devlogs(projectId: number, projectName?: string): string { + return `${this.project(projectId, projectName)}/devlogs`; + } + + /** + * Generate URL for specific devlog + */ + static devlog(projectId: number, devlogId: number, projectName?: string): string { + return `${this.devlogs(projectId, projectName)}/${devlogId}`; + } + + /** + * Generate URL for project settings + */ + static settings(projectId: number, projectName?: string): string { + return `${this.project(projectId, projectName)}/settings`; + } + + /** + * Generate URL for creating a new devlog in project + */ + static createDevlog(projectId: number, projectName?: string): string { + return `${this.devlogs(projectId, projectName)}/create`; + } +} + +/** + * Legacy support - helper to get project name from current context + * This can be used when we have projectId but need to fetch the name + */ +export async function getProjectName(projectId: number): Promise { + try { + const project = await apiClient.get<{ name: string }>(`/api/projects/${projectId}`); + return project.name; + } catch (error) { + console.error('Failed to fetch project name:', error); + return null; + } +} diff --git a/packages/web/app/lib/routing/route-params.ts b/packages/web/app/lib/routing/route-params.ts index 686f8bca..5fc6c2ec 100644 --- a/packages/web/app/lib/routing/route-params.ts +++ b/packages/web/app/lib/routing/route-params.ts @@ -4,9 +4,12 @@ */ import { DevlogId } from '@codervisor/devlog-core'; +import { isValidProjectIdentifier, generateSlugFromName } from '@codervisor/devlog-core'; export interface ParsedProjectParams { projectId: number; + projectIdentifier: string; // The project name + identifierType: 'name'; } export interface ParsedDevlogParams extends ParsedProjectParams { @@ -14,7 +17,29 @@ export interface ParsedDevlogParams extends ParsedProjectParams { } /** - * Parse and validate a numeric ID parameter + * Parse and validate a project name (name-only routing) + */ +function parseProjectIdentifier(value: string, paramName: string): { + projectId: number; + projectIdentifier: string; + identifierType: 'name'; +} { + const validation = isValidProjectIdentifier(value); + + if (!validation.valid) { + throw new Error(`Invalid ${paramName}: must be a valid project name following GitHub naming conventions`); + } + + // Always name-based identifiers now + return { + projectId: -1, // Will be resolved later by service helper + projectIdentifier: value, + identifierType: 'name', + }; +} + +/** + * Parse and validate a numeric ID parameter (for devlog IDs) */ function parseId(value: string, paramName: string): number { const parsed = parseInt(value, 10); @@ -35,9 +60,7 @@ export const RouteParamParsers = { * For routes like: /projects/[id]/... */ parseProjectParams(params: { id: string }): ParsedProjectParams { - return { - projectId: parseId(params.id, 'project ID'), - }; + return parseProjectIdentifier(params.id, 'project identifier'); }, /** @@ -45,8 +68,9 @@ export const RouteParamParsers = { * For routes like: /projects/[id]/devlogs/[devlogId]/... */ parseDevlogParams(params: { id: string; devlogId: string }): ParsedDevlogParams { + const projectInfo = parseProjectIdentifier(params.id, 'project identifier'); return { - projectId: parseId(params.id, 'project ID'), + ...projectInfo, devlogId: parseId(params.devlogId, 'devlog ID') as DevlogId, }; }, diff --git a/packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx b/packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx index 19579f7b..a3baea4b 100644 --- a/packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx +++ b/packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx @@ -1,4 +1,5 @@ import { ProjectDevlogDetailsPage } from './ProjectDevlogDetailsPage'; +import { ProjectResolver } from '@/components/ProjectResolver'; import { RouteParamParsers } from '@/lib'; // Disable static generation for this page since it uses client-side features @@ -12,6 +13,16 @@ interface ProjectDevlogPageProps { } export default function ProjectDevlogPage({ params }: ProjectDevlogPageProps) { - const { projectId, devlogId } = RouteParamParsers.parseDevlogParams(params); - return ; + const { projectIdentifier, identifierType, devlogId } = RouteParamParsers.parseDevlogParams(params); + + return ( + + {(projectId) => ( + + )} + + ); } diff --git a/packages/web/app/projects/[id]/devlogs/page.tsx b/packages/web/app/projects/[id]/devlogs/page.tsx index eee0ead4..0f89740c 100644 --- a/packages/web/app/projects/[id]/devlogs/page.tsx +++ b/packages/web/app/projects/[id]/devlogs/page.tsx @@ -1,4 +1,5 @@ import { ProjectDevlogListPage } from './ProjectDevlogListPage'; +import { ProjectResolver } from '@/components/ProjectResolver'; import { RouteParamParsers } from '@/lib'; // Disable static generation for this page since it uses client-side features @@ -11,6 +12,14 @@ interface ProjectDevlogsPageProps { } export default function ProjectDevlogsPage({ params }: ProjectDevlogsPageProps) { - const { projectId } = RouteParamParsers.parseProjectParams(params); - return ; + const { projectIdentifier, identifierType } = RouteParamParsers.parseProjectParams(params); + + return ( + + {(projectId) => } + + ); } diff --git a/packages/web/app/projects/[id]/page.tsx b/packages/web/app/projects/[id]/page.tsx index 75066419..f3031edc 100644 --- a/packages/web/app/projects/[id]/page.tsx +++ b/packages/web/app/projects/[id]/page.tsx @@ -1,4 +1,5 @@ import { ProjectDetailsPage } from './ProjectDetailsPage'; +import { ProjectResolver } from '@/components/ProjectResolver'; import { RouteParamParsers } from '@/lib'; // Disable static generation for this page since it uses client-side features @@ -11,6 +12,19 @@ interface ProjectPageProps { } export default function ProjectPage({ params }: ProjectPageProps) { - const { projectId } = RouteParamParsers.parseProjectParams(params); - return ; + const { projectIdentifier, identifierType } = RouteParamParsers.parseProjectParams(params); + + return ( + { + // This will be handled in the ProjectResolver component + }} + > + {(projectId, project) => ( + + )} + + ); } diff --git a/packages/web/app/schemas/project.ts b/packages/web/app/schemas/project.ts index 44b7479e..70e970ef 100644 --- a/packages/web/app/schemas/project.ts +++ b/packages/web/app/schemas/project.ts @@ -6,21 +6,28 @@ */ import { z } from 'zod'; +import { validateProjectName, isValidProjectIdentifier } from '@codervisor/devlog-core'; /** - * Project ID parameter validation (from URL params) + * Project name parameter validation (from URL params) - name-only routing */ export const ProjectIdParamSchema = z .object({ - id: z.string().regex(/^\d+$/, 'Project ID must be a valid number'), + id: z.string().min(1, 'Project name is required'), }) - .transform((data) => ({ id: Number(data.id) })); + .refine( + (data) => isValidProjectIdentifier(data.id).valid, + 'Project name must follow GitHub naming conventions' + ); /** * Project creation request body schema */ export const CreateProjectBodySchema = z.object({ - name: z.string().min(1, 'Project name is required'), + name: z + .string() + .min(1, 'Project name is required') + .refine(validateProjectName, 'Project name must follow GitHub naming conventions: letters, numbers, hyphens, underscores only; cannot start/end with hyphens'), description: z.string().optional(), repositoryUrl: z.string().optional(), settings: z diff --git a/packages/web/tests/api.test.ts b/packages/web/tests/api.test.ts index 1c8e916e..2dc7fa5b 100644 --- a/packages/web/tests/api.test.ts +++ b/packages/web/tests/api.test.ts @@ -5,7 +5,7 @@ * Tests parameter validation, error handling, and service integration patterns. */ -import { describe, it, expect, beforeEach, vi, type MockedFunction } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { NextRequest, NextResponse } from 'next/server'; import { RouteParams, ServiceHelper, ApiErrors, ApiResponses, withErrorHandling } from '@/lib'; @@ -42,85 +42,110 @@ describe('API Utilities Test Suite', () => { describe('RouteParams', () => { describe('parseProjectId', () => { - it('should parse valid numeric project ID', () => { - const params = { id: '123' }; + it('should parse valid project name', () => { + const params = { id: 'my-project' }; const result = RouteParams.parseProjectId(params); expect(result.success).toBe(true); if (result.success) { - expect(result.data.projectId).toBe(123); + expect(result.data.identifier).toBe('my-project'); + expect(result.data.identifierType).toBe('name'); } }); - it('should reject invalid project ID', () => { - const params = { id: 'invalid' }; + it('should accept project names with underscores', () => { + const params = { id: 'my_project_name' }; const result = RouteParams.parseProjectId(params); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.response).toBeInstanceOf(NextResponse); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.identifier).toBe('my_project_name'); + expect(result.data.identifierType).toBe('name'); } }); - it('should reject negative project ID', () => { - const params = { id: '-1' }; + it('should accept mixed case project names', () => { + const params = { id: 'MyProject' }; + const result = RouteParams.parseProjectId(params); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.identifier).toBe('MyProject'); + expect(result.data.identifierType).toBe('name'); + } + }); + + it('should reject project names starting with hyphen', () => { + const params = { id: '-invalid' }; const result = RouteParams.parseProjectId(params); expect(result.success).toBe(false); }); - it('should reject zero as project ID', () => { - const params = { id: '0' }; + it('should reject project names ending with hyphen', () => { + const params = { id: 'invalid-' }; const result = RouteParams.parseProjectId(params); expect(result.success).toBe(false); }); - it('should accept floating point numbers (parsed as integers)', () => { - const params = { id: '123.45' }; + it('should reject project names with special characters', () => { + const params = { id: 'invalid@name' }; const result = RouteParams.parseProjectId(params); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.projectId).toBe(123); // parseInt truncates to 123 - } + expect(result.success).toBe(false); + }); + + it('should reject numeric IDs (name-only routing)', () => { + const params = { id: '123' }; + const result = RouteParams.parseProjectId(params); + + expect(result.success).toBe(false); }); }); describe('parseProjectAndDevlogId', () => { - it('should parse valid numeric IDs', () => { - const params = { id: '123', devlogId: '456' }; + it('should parse valid project name and devlog ID', () => { + const params = { id: 'my-project', devlogId: '456' }; const result = RouteParams.parseProjectAndDevlogId(params); expect(result.success).toBe(true); if (result.success) { - expect(result.data.projectId).toBe(123); + expect(result.data.identifier).toBe('my-project'); + expect(result.data.identifierType).toBe('name'); expect(result.data.devlogId).toBe(456); } }); - it('should reject invalid project ID', () => { - const params = { id: 'invalid', devlogId: '456' }; + it('should reject invalid project name', () => { + const params = { id: '-invalid', devlogId: '456' }; const result = RouteParams.parseProjectAndDevlogId(params); expect(result.success).toBe(false); }); it('should reject invalid devlog ID', () => { - const params = { id: '123', devlogId: 'invalid' }; + const params = { id: 'my-project', devlogId: 'invalid' }; + const result = RouteParams.parseProjectAndDevlogId(params); + + expect(result.success).toBe(false); + }); + + it('should reject numeric project identifiers (name-only routing)', () => { + const params = { id: '123', devlogId: '456' }; const result = RouteParams.parseProjectAndDevlogId(params); expect(result.success).toBe(false); }); it('should provide descriptive error messages', async () => { - const params = { id: 'invalid', devlogId: '456' }; + const params = { id: '-invalid', devlogId: '456' }; const result = RouteParams.parseProjectAndDevlogId(params); expect(result.success).toBe(false); if (!result.success) { const responseJson = await result.response.json(); - expect(responseJson.error).toContain('Invalid project ID: must be a positive integer'); + expect(responseJson.error).toContain('Invalid project name'); } }); }); @@ -377,7 +402,7 @@ describe('Route Handler Integration Tests', () => { expect(result.status).toBe(400); const json = await result.json(); - expect(json.error).toContain('Invalid project ID: must be a positive integer'); + expect(json.error).toContain('Invalid project name'); }); it('should handle nonexistent project', async () => { diff --git a/scripts/migrate-project-names.js b/scripts/migrate-project-names.js new file mode 100644 index 00000000..1c1636dc --- /dev/null +++ b/scripts/migrate-project-names.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node +/** + * Migration script to update existing projects for GitHub-style naming + * This script: + * 1. Updates project names to follow GitHub naming conventions + * 2. Ensures name uniqueness (case-insensitive) + * 3. Handles any invalid characters by replacing them + */ + +async function migrateProjectNames() { + console.log('🚀 Starting project name migration...'); + + try { + // Dynamic imports for ES modules + const { ProjectService } = await import('@codervisor/devlog-core'); + const { validateProjectName, generateUniqueProjectName } = await import('@codervisor/devlog-core'); + + const projectService = ProjectService.getInstance(); + const projects = await projectService.list(); + + console.log(`📋 Found ${projects.length} projects to check`); + + const existingNames = []; + const projectUpdates = []; + + // First pass: identify projects that need updates + for (const project of projects) { + const currentName = project.name; + + if (validateProjectName(currentName)) { + // Name is already valid, just track for uniqueness + existingNames.push(currentName); + console.log(`✅ "${currentName}" - already valid`); + } else { + // Name needs to be fixed + let fixedName = currentName + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/[^a-zA-Z0-9_-]/g, '') // Remove invalid characters + .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens + .replace(/-+/g, '-'); // Collapse multiple hyphens + + if (!fixedName) { + fixedName = `project-${project.id}`; // Fallback for empty names + } + + // Ensure uniqueness + const uniqueName = generateUniqueProjectName(fixedName, existingNames); + existingNames.push(uniqueName); + + projectUpdates.push({ + id: project.id, + currentName, + newName: uniqueName + }); + + console.log(`🔄 "${currentName}" -> "${uniqueName}"`); + } + } + + if (projectUpdates.length === 0) { + console.log('🎉 All project names are already valid! No migration needed.'); + return; + } + + console.log(`\n📝 Planning to update ${projectUpdates.length} projects:`); + projectUpdates.forEach(update => { + console.log(` - Project ${update.id}: "${update.currentName}" -> "${update.newName}"`); + }); + + // Confirm before proceeding + const readline = await import('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + const proceed = await new Promise((resolve) => { + rl.question('\nDo you want to proceed with these updates? (y/N): ', (answer) => { + resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); + rl.close(); + }); + }); + + if (!proceed) { + console.log('❌ Migration cancelled by user'); + return; + } + + // Second pass: apply updates + console.log('\n🔄 Applying updates...'); + let successCount = 0; + let errorCount = 0; + + for (const update of projectUpdates) { + try { + await projectService.update(update.id, { name: update.newName }); + console.log(`✅ Updated project ${update.id}: "${update.currentName}" -> "${update.newName}"`); + successCount++; + } catch (error) { + console.error(`❌ Failed to update project ${update.id}: ${error}`); + errorCount++; + } + } + + console.log(`\n🎉 Migration completed!`); + console.log(` - ✅ Successfully updated: ${successCount} projects`); + if (errorCount > 0) { + console.log(` - ❌ Failed updates: ${errorCount} projects`); + } + + } catch (error) { + console.error('💥 Migration failed:', error); + process.exit(1); + } +} + +// Run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + migrateProjectNames() + .then(() => { + console.log('✨ Migration script completed'); + process.exit(0); + }) + .catch((error) => { + console.error('💥 Migration script failed:', error); + process.exit(1); + }); +} From cf8e265eccccbbc39404b3c15ce50aa5a28b8b22 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 8 Aug 2025 13:57:16 +0800 Subject: [PATCH 07/50] feat: update icon imports and clean up project configuration files --- .../components/layout/NavigationBreadcrumb.tsx | 4 ++-- packages/web/app/components/layout/TopNavbar.tsx | 2 +- packages/web/app/not-found.tsx | 2 ++ packages/web/app/projects/ProjectListPage.tsx | 16 ++++++++-------- packages/web/app/projects/page.tsx | 2 ++ packages/web/package.json | 3 +-- packages/web/tsconfig.json | 1 - 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/web/app/components/layout/NavigationBreadcrumb.tsx b/packages/web/app/components/layout/NavigationBreadcrumb.tsx index 52fdfb4d..79d29b61 100644 --- a/packages/web/app/components/layout/NavigationBreadcrumb.tsx +++ b/packages/web/app/components/layout/NavigationBreadcrumb.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { usePathname, useRouter } from 'next/navigation'; import { useDevlogStore, useProjectStore } from '@/stores'; -import { CheckIcon, ChevronsUpDown, NotepadText, Package } from 'lucide-react'; +import { Check, ChevronsUpDown, NotepadText, Package } from 'lucide-react'; import { Breadcrumb, BreadcrumbItem, @@ -103,7 +103,7 @@ export function NavigationBreadcrumb() {
{project.id}
{isCurrentProject && ( - + )} ); diff --git a/packages/web/app/components/layout/TopNavbar.tsx b/packages/web/app/components/layout/TopNavbar.tsx index c62ac69c..908ca9b4 100644 --- a/packages/web/app/components/layout/TopNavbar.tsx +++ b/packages/web/app/components/layout/TopNavbar.tsx @@ -1,4 +1,4 @@ -import React from 'next'; +import React from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { NavigationBreadcrumb } from './NavigationBreadcrumb'; diff --git a/packages/web/app/not-found.tsx b/packages/web/app/not-found.tsx index cdd049c8..512c1b10 100644 --- a/packages/web/app/not-found.tsx +++ b/packages/web/app/not-found.tsx @@ -1,3 +1,5 @@ +export const dynamic = 'force-dynamic'; + export default function NotFound() { return (
diff --git a/packages/web/app/projects/ProjectListPage.tsx b/packages/web/app/projects/ProjectListPage.tsx index 5809c0b7..ae1eeebe 100644 --- a/packages/web/app/projects/ProjectListPage.tsx +++ b/packages/web/app/projects/ProjectListPage.tsx @@ -18,11 +18,11 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { - AlertTriangleIcon, + AlertTriangle, ChevronRight, - FolderIcon, - LoaderIcon, - PlusIcon, + Folder, + Loader2, + Plus, Search, Settings, } from 'lucide-react'; @@ -107,7 +107,7 @@ export function ProjectListPage() { if (projectsContext.error) { return ( - +
Error Loading Projects
{projectsContext.error} @@ -180,7 +180,7 @@ export function ProjectListPage() { {projects?.length === 0 && (
- +

No Projects Found

@@ -193,7 +193,7 @@ export function ProjectListPage() { onClick={() => setIsModalVisible(true)} className="flex items-center gap-2 px-8 py-3" > - + Create First Project
@@ -244,7 +244,7 @@ export function ProjectListPage() { -
-
- ); + return <>{children(project.name, project)}; + } catch (error) { + console.error('Error resolving project:', error); + return ; } - - return <>{children(project.name, project)}; } diff --git a/packages/web/app/components/index.ts b/packages/web/app/components/index.ts index 48d44e4c..e8f1f492 100644 --- a/packages/web/app/components/index.ts +++ b/packages/web/app/components/index.ts @@ -18,5 +18,6 @@ export * from './features/dashboard'; export * from './features/devlogs'; // Project Components -export { ProjectResolver } from './ProjectResolver'; +// Note: ProjectResolver is not exported as it's only used server-side in layout.tsx +export { ProjectNotFound } from './ProjectNotFound'; export * from './project'; diff --git a/packages/web/app/projects/[id]/ProjectDetailsPage.tsx b/packages/web/app/projects/[id]/ProjectDetailsPage.tsx index cddb1202..2358794a 100644 --- a/packages/web/app/projects/[id]/ProjectDetailsPage.tsx +++ b/packages/web/app/projects/[id]/ProjectDetailsPage.tsx @@ -6,12 +6,10 @@ import { useDevlogStore, useProjectStore } from '@/stores'; import { useDevlogEvents } from '@/hooks/use-realtime'; import { DevlogEntry, Project } from '@codervisor/devlog-core'; import { useRouter } from 'next/navigation'; +import { useProjectName } from './ProjectProvider'; -interface ProjectDetailsPageProps { - projectName: string; -} - -export function ProjectDetailsPage({ projectName }: ProjectDetailsPageProps) { +export function ProjectDetailsPage() { + const projectName = useProjectName(); const router = useRouter(); const { currentProjectName, setCurrentProjectName } = useProjectStore(); diff --git a/packages/web/app/projects/[id]/ProjectProvider.tsx b/packages/web/app/projects/[id]/ProjectProvider.tsx new file mode 100644 index 00000000..bae43d28 --- /dev/null +++ b/packages/web/app/projects/[id]/ProjectProvider.tsx @@ -0,0 +1,43 @@ +'use client'; + +import React, { createContext, useContext } from 'react'; +import type { Project } from '@codervisor/devlog-core'; + +interface ProjectContextValue { + project: Project; + projectName: string; +} + +const ProjectContext = createContext(null); + +export function ProjectProvider({ + children, + project +}: { + children: React.ReactNode; + project: Project; +}) { + const value: ProjectContextValue = { + project, + projectName: project.name, + }; + + return ( + + {children} + + ); +} + +export function useProject(): ProjectContextValue { + const context = useContext(ProjectContext); + if (!context) { + throw new Error('useProject must be used within a ProjectProvider'); + } + return context; +} + +export function useProjectName(): string { + const { projectName } = useProject(); + return projectName; +} \ No newline at end of file diff --git a/packages/web/app/projects/[id]/devlogs/ProjectDevlogListPage.tsx b/packages/web/app/projects/[id]/devlogs/ProjectDevlogListPage.tsx index 6a055e7e..c9076a68 100644 --- a/packages/web/app/projects/[id]/devlogs/ProjectDevlogListPage.tsx +++ b/packages/web/app/projects/[id]/devlogs/ProjectDevlogListPage.tsx @@ -6,12 +6,10 @@ import { useDevlogStore, useProjectStore } from '@/stores'; import { useDevlogEvents } from '@/hooks/use-realtime'; import { DevlogEntry, DevlogId } from '@codervisor/devlog-core'; import { useRouter } from 'next/navigation'; +import { useProjectName } from '../ProjectProvider'; -interface ProjectDevlogListPageProps { - projectName: string; -} - -export function ProjectDevlogListPage({ projectName }: ProjectDevlogListPageProps) { +export function ProjectDevlogListPage() { + const projectName = useProjectName(); const router = useRouter(); const { currentProjectName, setCurrentProjectName } = useProjectStore(); diff --git a/packages/web/app/projects/[id]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx b/packages/web/app/projects/[id]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx index e8f7ea2d..0f5b1dc1 100644 --- a/packages/web/app/projects/[id]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx +++ b/packages/web/app/projects/[id]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx @@ -9,13 +9,14 @@ import { ArrowLeftIcon, SaveIcon, TrashIcon, UndoIcon } from 'lucide-react'; import { toast } from 'sonner'; import { DevlogEntry } from '@codervisor/devlog-core'; import { RealtimeEventType } from '@/lib/realtime'; +import { useProjectName } from '../../ProjectProvider'; interface ProjectDevlogDetailsPageProps { - projectName: string; devlogId: number; } -export function ProjectDevlogDetailsPage({ projectName, devlogId }: ProjectDevlogDetailsPageProps) { +export function ProjectDevlogDetailsPage({ devlogId }: ProjectDevlogDetailsPageProps) { + const projectName = useProjectName(); const router = useRouter(); const { setCurrentProjectName } = useProjectStore(); diff --git a/packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx b/packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx index f1f7483f..d9f1fbb4 100644 --- a/packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx +++ b/packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx @@ -1,7 +1,4 @@ -'use client'; - import { ProjectDevlogDetailsPage } from './ProjectDevlogDetailsPage'; -import { ProjectResolver } from '@/components/ProjectResolver'; import { RouteParamParsers } from '@/lib'; interface ProjectDevlogPageProps { @@ -12,16 +9,7 @@ interface ProjectDevlogPageProps { } export default function ProjectDevlogPage({ params }: ProjectDevlogPageProps) { - const { projectIdentifier, identifierType, devlogId } = RouteParamParsers.parseDevlogParams(params); + const { devlogId } = RouteParamParsers.parseDevlogParams(params); - return ( - - {(projectName) => ( - - )} - - ); + return ; } diff --git a/packages/web/app/projects/[id]/devlogs/page.tsx b/packages/web/app/projects/[id]/devlogs/page.tsx index 723c587b..deabdc57 100644 --- a/packages/web/app/projects/[id]/devlogs/page.tsx +++ b/packages/web/app/projects/[id]/devlogs/page.tsx @@ -1,24 +1,5 @@ -'use client'; - import { ProjectDevlogListPage } from './ProjectDevlogListPage'; -import { ProjectResolver } from '@/components/ProjectResolver'; -import { RouteParamParsers } from '@/lib'; - -interface ProjectDevlogsPageProps { - params: { - id: string; - }; -} -export default function ProjectDevlogsPage({ params }: ProjectDevlogsPageProps) { - const { projectIdentifier, identifierType } = RouteParamParsers.parseProjectParams(params); - - return ( - - {(projectName) => } - - ); +export default function ProjectDevlogsPage() { + return ; } diff --git a/packages/web/app/projects/[id]/layout.tsx b/packages/web/app/projects/[id]/layout.tsx new file mode 100644 index 00000000..7b1bc3a7 --- /dev/null +++ b/packages/web/app/projects/[id]/layout.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { ProjectService } from '@codervisor/devlog-core/server'; +import { generateSlugFromName } from '@codervisor/devlog-core'; +import type { Project } from '@codervisor/devlog-core'; +import { RouteParamParsers } from '@/lib'; +import { ProjectNotFound } from '@/components/ProjectNotFound'; +import { redirect } from 'next/navigation'; +import { ProjectProvider } from './ProjectProvider'; + +interface ProjectLayoutProps { + children: React.ReactNode; + params: { + id: string; + }; +} + +/** + * Server layout that resolves project data and provides it to all child pages + */ +export default async function ProjectLayout({ children, params }: ProjectLayoutProps) { + const { projectIdentifier, identifierType } = RouteParamParsers.parseProjectParams(params); + + try { + const projectService = ProjectService.getInstance(); + + let project: Project | null = null; + + if (identifierType === 'name') { + project = await projectService.getByName(projectIdentifier); + + // If project exists but identifier doesn't match canonical slug, redirect + if (project) { + const canonicalSlug = generateSlugFromName(project.name); + if (projectIdentifier !== canonicalSlug) { + // Redirect to canonical URL + const currentPath = `/projects/${projectIdentifier}`; + const newPath = `/projects/${canonicalSlug}`; + redirect(newPath); + } + } + } else { + // For ID-based routing (fallback/legacy support) + const projectId = parseInt(projectIdentifier, 10); + if (!isNaN(projectId)) { + project = await projectService.get(projectId); + } + } + + if (!project) { + return ; + } + + return ( + + {children} + + ); + } catch (error) { + console.error('Error resolving project:', error); + return ; + } +} \ No newline at end of file diff --git a/packages/web/app/projects/[id]/page.tsx b/packages/web/app/projects/[id]/page.tsx index 4f9437b2..4bbb13db 100644 --- a/packages/web/app/projects/[id]/page.tsx +++ b/packages/web/app/projects/[id]/page.tsx @@ -1,26 +1,5 @@ -'use client'; - import { ProjectDetailsPage } from './ProjectDetailsPage'; -import { ProjectResolver } from '@/components/ProjectResolver'; -import { RouteParamParsers } from '@/lib'; - -interface ProjectPageProps { - params: { - id: string; - }; -} -export default function ProjectPage({ params }: ProjectPageProps) { - const { projectIdentifier, identifierType } = RouteParamParsers.parseProjectParams(params); - - return ( - - {(projectName, project) => ( - - )} - - ); +export default function ProjectPage() { + return ; } diff --git a/packages/web/app/projects/[id]/settings/ProjectSettingsPage.tsx b/packages/web/app/projects/[id]/settings/ProjectSettingsPage.tsx index f9317a33..6a57f3f8 100644 --- a/packages/web/app/projects/[id]/settings/ProjectSettingsPage.tsx +++ b/packages/web/app/projects/[id]/settings/ProjectSettingsPage.tsx @@ -25,17 +25,15 @@ import { Skeleton } from '@/components/ui/skeleton'; import { LoaderIcon, SaveIcon, TrashIcon, AlertTriangleIcon } from 'lucide-react'; import { toast } from 'sonner'; import { Project } from '@codervisor/devlog-core'; - -interface ProjectSettingsPageProps { - projectName: string; -} +import { useProjectName } from '../ProjectProvider'; interface ProjectFormData { name: string; description?: string; } -export function ProjectSettingsPage({ projectName }: ProjectSettingsPageProps) { +export function ProjectSettingsPage() { + const projectName = useProjectName(); const router = useRouter(); const { currentProjectContext, diff --git a/packages/web/app/projects/[id]/settings/page.tsx b/packages/web/app/projects/[id]/settings/page.tsx index 7510390e..5f0f0d94 100644 --- a/packages/web/app/projects/[id]/settings/page.tsx +++ b/packages/web/app/projects/[id]/settings/page.tsx @@ -1,16 +1,8 @@ import { ProjectSettingsPage } from './ProjectSettingsPage'; -import { RouteParamParsers } from '@/lib'; // Disable static generation for this page since it uses client-side features export const dynamic = 'force-dynamic'; -interface ProjectSettingsPageProps { - params: { - id: string; - }; -} - -export default function ProjectSettings({ params }: ProjectSettingsPageProps) { - const { projectIdentifier } = RouteParamParsers.parseProjectParams(params); - return ; +export default function ProjectSettings() { + return ; } From d573873c1f2188fc751c93de22896f2ef1acc038 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Thu, 21 Aug 2025 00:01:54 +0800 Subject: [PATCH 13/50] feat(api): add dynamic routes for project devlogs statistics and management - Implemented GET endpoint for project devlogs overview statistics. - Implemented GET endpoint for project devlogs time series statistics. - Created dynamic route for fetching project details, including GET, PUT, and DELETE methods. - Updated RouteParams utility to support name-based routing for projects. - Added ProjectProvider and ProjectDetailsPage components for managing project state. - Created ProjectDevlogListPage and ProjectDevlogDetailsPage for displaying and managing devlogs. - Implemented ProjectSettingsPage for updating and deleting project settings. - Added dynamic routing for project settings and devlogs pages. --- .../[devlogId]/notes/[noteId]/route.ts | 6 ++-- .../devlogs/[devlogId]/notes/route.ts | 6 ++-- .../devlogs/[devlogId]/route.ts | 6 ++-- .../{[id] => [name]}/devlogs/route.ts | 32 ++++++++----------- .../{[id] => [name]}/devlogs/search/route.ts | 17 ++++------ .../devlogs/stats/overview/route.ts | 2 +- .../devlogs/stats/timeseries/route.ts | 2 +- .../api/projects/{[id] => [name]}/route.ts | 30 ++++++++++++----- packages/web/app/lib/api/api-utils.ts | 24 +++++++------- packages/web/app/lib/routing/route-params.ts | 17 ++++++---- packages/web/app/projects/ProjectListPage.tsx | 18 +++++------ .../{[id] => [name]}/ProjectDetailsPage.tsx | 0 .../{[id] => [name]}/ProjectProvider.tsx | 0 .../devlogs/ProjectDevlogListPage.tsx | 0 .../[devlogId]/ProjectDevlogDetailsPage.tsx | 0 .../devlogs/[devlogId]/page.tsx | 0 .../{[id] => [name]}/devlogs/page.tsx | 0 .../app/projects/{[id] => [name]}/layout.tsx | 0 .../app/projects/{[id] => [name]}/page.tsx | 0 .../settings/ProjectSettingsPage.tsx | 0 .../{[id] => [name]}/settings/page.tsx | 0 21 files changed, 85 insertions(+), 75 deletions(-) rename packages/web/app/api/projects/{[id] => [name]}/devlogs/[devlogId]/notes/[noteId]/route.ts (94%) rename packages/web/app/api/projects/{[id] => [name]}/devlogs/[devlogId]/notes/route.ts (95%) rename packages/web/app/api/projects/{[id] => [name]}/devlogs/[devlogId]/route.ts (95%) rename packages/web/app/api/projects/{[id] => [name]}/devlogs/route.ts (90%) rename packages/web/app/api/projects/{[id] => [name]}/devlogs/search/route.ts (92%) rename packages/web/app/api/projects/{[id] => [name]}/devlogs/stats/overview/route.ts (92%) rename packages/web/app/api/projects/{[id] => [name]}/devlogs/stats/timeseries/route.ts (95%) rename packages/web/app/api/projects/{[id] => [name]}/route.ts (83%) rename packages/web/app/projects/{[id] => [name]}/ProjectDetailsPage.tsx (100%) rename packages/web/app/projects/{[id] => [name]}/ProjectProvider.tsx (100%) rename packages/web/app/projects/{[id] => [name]}/devlogs/ProjectDevlogListPage.tsx (100%) rename packages/web/app/projects/{[id] => [name]}/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx (100%) rename packages/web/app/projects/{[id] => [name]}/devlogs/[devlogId]/page.tsx (100%) rename packages/web/app/projects/{[id] => [name]}/devlogs/page.tsx (100%) rename packages/web/app/projects/{[id] => [name]}/layout.tsx (100%) rename packages/web/app/projects/{[id] => [name]}/page.tsx (100%) rename packages/web/app/projects/{[id] => [name]}/settings/ProjectSettingsPage.tsx (100%) rename packages/web/app/projects/{[id] => [name]}/settings/page.tsx (100%) diff --git a/packages/web/app/api/projects/[id]/devlogs/[devlogId]/notes/[noteId]/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts similarity index 94% rename from packages/web/app/api/projects/[id]/devlogs/[devlogId]/notes/[noteId]/route.ts rename to packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts index d4716f3d..1c7b891e 100644 --- a/packages/web/app/api/projects/[id]/devlogs/[devlogId]/notes/[noteId]/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts @@ -14,7 +14,7 @@ const UpdateNoteBodySchema = z.object({ category: z.string().optional(), }); -// GET /api/projects/[id]/devlogs/[devlogId]/notes/[noteId] - Get specific note +// GET /api/projects/[name]/devlogs/[devlogId]/notes/[noteId] - Get specific note export async function GET( request: NextRequest, { params }: { params: { id: string; devlogId: string; noteId: string } }, @@ -52,7 +52,7 @@ export async function GET( } } -// PUT /api/projects/[id]/devlogs/[devlogId]/notes/[noteId] - Update specific note +// PUT /api/projects/[name]/devlogs/[devlogId]/notes/[noteId] - Update specific note export async function PUT( request: NextRequest, { params }: { params: { id: string; devlogId: string; noteId: string } }, @@ -104,7 +104,7 @@ export async function PUT( } } -// DELETE /api/projects/[id]/devlogs/[devlogId]/notes/[noteId] - Delete specific note +// DELETE /api/projects/[name]/devlogs/[devlogId]/notes/[noteId] - Delete specific note export async function DELETE( request: NextRequest, { params }: { params: { id: string; devlogId: string; noteId: string } }, diff --git a/packages/web/app/api/projects/[id]/devlogs/[devlogId]/notes/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts similarity index 95% rename from packages/web/app/api/projects/[id]/devlogs/[devlogId]/notes/route.ts rename to packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts index 8e8fa7da..f82cde79 100644 --- a/packages/web/app/api/projects/[id]/devlogs/[devlogId]/notes/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts @@ -8,7 +8,7 @@ import { DevlogAddNoteBodySchema, DevlogUpdateWithNoteBodySchema } from '@/schem // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[id]/devlogs/[devlogId]/notes - List notes for a devlog entry +// GET /api/projects/[name]/devlogs/[devlogId]/notes - List notes for a devlog entry export async function GET( request: NextRequest, { params }: { params: { id: string; devlogId: string } }, @@ -67,7 +67,7 @@ export async function GET( } } -// POST /api/projects/[id]/devlogs/[devlogId]/notes - Add note to devlog entry +// POST /api/projects/[name]/devlogs/[devlogId]/notes - Add note to devlog entry export async function POST( request: NextRequest, { params }: { params: { id: string; devlogId: string } }, @@ -116,7 +116,7 @@ export async function POST( } } -// PUT /api/projects/[id]/devlogs/[devlogId]/notes - Update devlog and add note in one operation +// PUT /api/projects/[name]/devlogs/[devlogId]/notes - Update devlog and add note in one operation export async function PUT( request: NextRequest, { params }: { params: { id: string; devlogId: string } }, diff --git a/packages/web/app/api/projects/[id]/devlogs/[devlogId]/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts similarity index 95% rename from packages/web/app/api/projects/[id]/devlogs/[devlogId]/route.ts rename to packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts index f0591629..16dadad6 100644 --- a/packages/web/app/api/projects/[id]/devlogs/[devlogId]/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts @@ -6,7 +6,7 @@ import { RealtimeEventType } from '@/lib/realtime'; // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[id]/devlogs/[devlogId] - Get specific devlog entry +// GET /api/projects/[name]/devlogs/[devlogId] - Get specific devlog entry export async function GET( request: NextRequest, { params }: { params: { id: string; devlogId: string } }, @@ -53,7 +53,7 @@ export async function GET( } } -// PUT /api/projects/[id]/devlogs/[devlogId] - Update devlog entry +// PUT /api/projects/[name]/devlogs/[devlogId] - Update devlog entry export async function PUT( request: NextRequest, { params }: { params: { id: string; devlogId: string } }, @@ -103,7 +103,7 @@ export async function PUT( } } -// DELETE /api/projects/[id]/devlogs/[devlogId] - Delete devlog entry +// DELETE /api/projects/[name]/devlogs/[devlogId] - Delete devlog entry export async function DELETE( request: NextRequest, { params }: { params: { id: string; devlogId: string } }, diff --git a/packages/web/app/api/projects/[id]/devlogs/route.ts b/packages/web/app/api/projects/[name]/devlogs/route.ts similarity index 90% rename from packages/web/app/api/projects/[id]/devlogs/route.ts rename to packages/web/app/api/projects/[name]/devlogs/route.ts index c40ec499..c5350bc0 100644 --- a/packages/web/app/api/projects/[id]/devlogs/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/route.ts @@ -1,17 +1,7 @@ import { NextRequest } from 'next/server'; -import { - PaginationMeta, - SortOptions, -} from '@codervisor/devlog-core'; -import { - DevlogService, - ProjectService, -} from '@codervisor/devlog-core/server'; -import { - ApiValidator, - CreateDevlogBodySchema, - DevlogListQuerySchema, -} from '@/schemas'; +import { PaginationMeta, SortOptions } from '@codervisor/devlog-core'; +import { DevlogService, ProjectService } from '@codervisor/devlog-core/server'; +import { ApiValidator, CreateDevlogBodySchema, DevlogListQuerySchema } from '@/schemas'; import { ApiErrors, createCollectionResponse, @@ -25,7 +15,7 @@ import { RealtimeEventType } from '@/lib/realtime'; // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[id]/devlogs - List devlogs for a project +// GET /api/projects/[name]/devlogs - List devlogs for a project export async function GET(request: NextRequest, { params }: { params: { id: string } }) { try { // Parse and validate project identifier @@ -44,7 +34,10 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri } // Get project using helper - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( + identifier, + identifierType, + ); if (!projectResult.success) { return projectResult.response; } @@ -103,7 +96,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri } } -// POST /api/projects/[id]/devlogs - Create new devlog entry +// POST /api/projects/[name]/devlogs - Create new devlog entry export async function POST(request: NextRequest, { params }: { params: { id: string } }) { try { // Parse and validate project identifier @@ -121,7 +114,10 @@ export async function POST(request: NextRequest, { params }: { params: { id: str } // Get project using helper - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( + identifier, + identifierType, + ); if (!projectResult.success) { return projectResult.response; } @@ -142,7 +138,7 @@ export async function POST(request: NextRequest, { params }: { params: { id: str updatedAt: now, projectId: project.id, // Ensure project context }; - + // Save the entry await devlogService.save(entry); diff --git a/packages/web/app/api/projects/[id]/devlogs/search/route.ts b/packages/web/app/api/projects/[name]/devlogs/search/route.ts similarity index 92% rename from packages/web/app/api/projects/[id]/devlogs/search/route.ts rename to packages/web/app/api/projects/[name]/devlogs/search/route.ts index a518e7e4..7fccad3b 100644 --- a/packages/web/app/api/projects/[id]/devlogs/search/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/search/route.ts @@ -1,12 +1,6 @@ import { NextRequest } from 'next/server'; -import { - DevlogFilter, - PaginationMeta, -} from '@codervisor/devlog-core'; -import { - DevlogService, - ProjectService, -} from '@codervisor/devlog-core/server'; +import { DevlogFilter, PaginationMeta } from '@codervisor/devlog-core'; +import { DevlogService, ProjectService } from '@codervisor/devlog-core/server'; import { ApiValidator, DevlogSearchQuerySchema } from '@/schemas'; import { ApiErrors, createSuccessResponse, RouteParams, ServiceHelper } from '@/lib/api/api-utils'; @@ -34,7 +28,7 @@ interface SearchResponse { }; } -// GET /api/projects/[id]/devlogs/search - Enhanced search for devlogs +// GET /api/projects/[name]/devlogs/search - Enhanced search for devlogs export async function GET(request: NextRequest, { params }: { params: { id: string } }) { try { // Parse and validate project name parameter @@ -53,7 +47,10 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri } // Get project using helper - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( + identifier, + identifierType, + ); if (!projectResult.success) { return projectResult.response; } diff --git a/packages/web/app/api/projects/[id]/devlogs/stats/overview/route.ts b/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts similarity index 92% rename from packages/web/app/api/projects/[id]/devlogs/stats/overview/route.ts rename to packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts index 89581346..bd2118a9 100644 --- a/packages/web/app/api/projects/[id]/devlogs/stats/overview/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts @@ -10,7 +10,7 @@ import { // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[id]/devlogs/stats/overview - Get overview statistics +// GET /api/projects/[name]/devlogs/stats/overview - Get overview statistics export const GET = withErrorHandling( async (request: NextRequest, { params }: { params: { id: string } }) => { // Parse and validate parameters diff --git a/packages/web/app/api/projects/[id]/devlogs/stats/timeseries/route.ts b/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts similarity index 95% rename from packages/web/app/api/projects/[id]/devlogs/stats/timeseries/route.ts rename to packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts index fc7ea07f..aa7f28bf 100644 --- a/packages/web/app/api/projects/[id]/devlogs/stats/timeseries/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts @@ -10,7 +10,7 @@ import { // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[id]/devlogs/stats/timeseries - Get time series statistics +// GET /api/projects/[name]/devlogs/stats/timeseries - Get time series statistics export const GET = withErrorHandling( async (request: NextRequest, { params }: { params: { id: string } }) => { // Parse and validate parameters diff --git a/packages/web/app/api/projects/[id]/route.ts b/packages/web/app/api/projects/[name]/route.ts similarity index 83% rename from packages/web/app/api/projects/[id]/route.ts rename to packages/web/app/api/projects/[name]/route.ts index 3c54f7cd..1d5b87c8 100644 --- a/packages/web/app/api/projects/[id]/route.ts +++ b/packages/web/app/api/projects/[name]/route.ts @@ -11,7 +11,7 @@ import { ApiValidator, UpdateProjectBodySchema } from '@/schemas'; // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[id] - Get specific project +// GET /api/projects/[name] - Get specific project export const GET = withErrorHandling( async (request: NextRequest, { params }: { params: { id: string } }) => { // Parse and validate parameters @@ -23,7 +23,10 @@ export const GET = withErrorHandling( const { identifier, identifierType } = paramResult.data; // Get project using helper - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( + identifier, + identifierType, + ); if (!projectResult.success) { return projectResult.response; } @@ -33,7 +36,7 @@ export const GET = withErrorHandling( }, ); -// PUT /api/projects/[id] - Update project +// PUT /api/projects/[name] - Update project export const PUT = withErrorHandling( async (request: NextRequest, { params }: { params: { id: string } }) => { // Parse and validate parameters @@ -51,19 +54,27 @@ export const PUT = withErrorHandling( } // Get project and service - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( + identifier, + identifierType, + ); if (!projectResult.success) { return projectResult.response; } // Update project using the resolved project ID - const updatedProject = await projectResult.data.projectService.update(projectResult.data.project.id, bodyValidation.data); + const updatedProject = await projectResult.data.projectService.update( + projectResult.data.project.id, + bodyValidation.data, + ); - return createSuccessResponse(updatedProject, { sseEventType: RealtimeEventType.PROJECT_UPDATED }); + return createSuccessResponse(updatedProject, { + sseEventType: RealtimeEventType.PROJECT_UPDATED, + }); }, ); -// DELETE /api/projects/[id] - Delete project +// DELETE /api/projects/[name] - Delete project export const DELETE = withErrorHandling( async (request: NextRequest, { params }: { params: { id: string } }) => { // Parse and validate parameters @@ -75,7 +86,10 @@ export const DELETE = withErrorHandling( const { identifier, identifierType } = paramResult.data; // Get project service - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail(identifier, identifierType); + const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( + identifier, + identifierType, + ); if (!projectResult.success) { return projectResult.response; } diff --git a/packages/web/app/lib/api/api-utils.ts b/packages/web/app/lib/api/api-utils.ts index cb7c188b..fa22f27e 100644 --- a/packages/web/app/lib/api/api-utils.ts +++ b/packages/web/app/lib/api/api-utils.ts @@ -20,12 +20,12 @@ import { broadcastUpdate } from '@/lib/api/server-realtime'; export const RouteParams = { /** * Parse project name parameter (name-only routing) - * Usage: /api/projects/[id] + * Usage: /api/projects/[name] */ parseProjectId(params: { id: string }) { try { const validation = isValidProjectIdentifier(params.id); - + if (!validation.valid) { return { success: false as const, @@ -39,10 +39,10 @@ export const RouteParams = { // Always name-based routing now return { success: true as const, - data: { + data: { projectId: -1, // Will be resolved by service helper - identifier: params.id, - identifierType: 'name' as const + identifier: params.id, + identifierType: 'name' as const, }, }; } catch (error) { @@ -55,7 +55,7 @@ export const RouteParams = { /** * Parse project name and devlog ID parameters (name-only routing for projects) - * Usage: /api/projects/[id]/devlogs/[devlogId] + * Usage: /api/projects/[name]/devlogs/[devlogId] */ parseProjectAndDevlogId(params: { id: string; devlogId: string }) { try { @@ -85,11 +85,11 @@ export const RouteParams = { // Always name-based routing for projects now return { success: true as const, - data: { + data: { projectId: -1, // Will be resolved by service helper - devlogId, - identifier: params.id, - identifierType: 'name' as const + devlogId, + identifier: params.id, + identifierType: 'name' as const, }, }; } catch (error) { @@ -126,10 +126,10 @@ export class ServiceHelper { static async getProjectByIdentifierOrFail(identifier: string, identifierType: 'name') { const { ProjectService } = await import('@codervisor/devlog-core/server'); const projectService = ProjectService.getInstance(); - + // Only name-based routing supported now const project = await projectService.getByName(identifier); - + if (!project) { return { success: false as const, response: ApiErrors.projectNotFound() }; } diff --git a/packages/web/app/lib/routing/route-params.ts b/packages/web/app/lib/routing/route-params.ts index 5fc6c2ec..76204f22 100644 --- a/packages/web/app/lib/routing/route-params.ts +++ b/packages/web/app/lib/routing/route-params.ts @@ -19,15 +19,20 @@ export interface ParsedDevlogParams extends ParsedProjectParams { /** * Parse and validate a project name (name-only routing) */ -function parseProjectIdentifier(value: string, paramName: string): { +function parseProjectIdentifier( + value: string, + paramName: string, +): { projectId: number; projectIdentifier: string; identifierType: 'name'; } { const validation = isValidProjectIdentifier(value); - + if (!validation.valid) { - throw new Error(`Invalid ${paramName}: must be a valid project name following GitHub naming conventions`); + throw new Error( + `Invalid ${paramName}: must be a valid project name following GitHub naming conventions`, + ); } // Always name-based identifiers now @@ -57,7 +62,7 @@ function parseId(value: string, paramName: string): number { export const RouteParamParsers = { /** * Parse project route parameters - * For routes like: /projects/[id]/... + * For routes like: /projects/[name]/... */ parseProjectParams(params: { id: string }): ParsedProjectParams { return parseProjectIdentifier(params.id, 'project identifier'); @@ -65,7 +70,7 @@ export const RouteParamParsers = { /** * Parse project + devlog route parameters - * For routes like: /projects/[id]/devlogs/[devlogId]/... + * For routes like: /projects/[name]/devlogs/[devlogId]/... */ parseDevlogParams(params: { id: string; devlogId: string }): ParsedDevlogParams { const projectInfo = parseProjectIdentifier(params.id, 'project identifier'); @@ -77,7 +82,7 @@ export const RouteParamParsers = { /** * Parse single devlog ID parameter - * For routes like: /devlogs/[id]/... + * For routes like: /devlogs/[name]/... */ parseDevlogId(params: { id: string }): { devlogId: DevlogId } { return { diff --git a/packages/web/app/projects/ProjectListPage.tsx b/packages/web/app/projects/ProjectListPage.tsx index ae1eeebe..f5fdb4a3 100644 --- a/packages/web/app/projects/ProjectListPage.tsx +++ b/packages/web/app/projects/ProjectListPage.tsx @@ -43,6 +43,8 @@ export function ProjectListPage() { const { subscribe, unsubscribe } = useRealtimeStore(); useEffect(() => { + fetchProjects(); + subscribe(RealtimeEventType.PROJECT_CREATED, async () => { await fetchProjects(); toast.success('Project created successfully'); @@ -52,10 +54,6 @@ export function ProjectListPage() { }; }, [fetchProjects]); - useEffect(() => { - fetchProjects(); - }, []); - const { data: projects, loading: isLoadingProjects } = projectsContext; const handleCreateProject = async (e: React.FormEvent) => { @@ -95,13 +93,13 @@ export function ProjectListPage() { } }; - const handleViewProject = (projectId: number) => { - router.push(`/projects/${projectId}`); + const handleViewProject = (projectName: string) => { + router.push(`/projects/${projectName}`); }; - const handleProjectSettings = (e: React.MouseEvent, projectId: number) => { + const handleProjectSettings = (e: React.MouseEvent, projectName: string) => { e.stopPropagation(); // Prevent card click from triggering - router.push(`/projects/${projectId}/settings`); + router.push(`/projects/${projectName}/settings`); }; if (projectsContext.error) { @@ -147,7 +145,7 @@ export function ProjectListPage() { handleViewProject(project.id)} + onClick={() => handleViewProject(project.name)} >
@@ -157,7 +155,7 @@ export function ProjectListPage() { variant="ghost" size="sm" className="h-7 w-7 p-0 hover:bg-muted" - onClick={(e) => handleProjectSettings(e, project.id)} + onClick={(e) => handleProjectSettings(e, project.name)} title="Project Settings" > diff --git a/packages/web/app/projects/[id]/ProjectDetailsPage.tsx b/packages/web/app/projects/[name]/ProjectDetailsPage.tsx similarity index 100% rename from packages/web/app/projects/[id]/ProjectDetailsPage.tsx rename to packages/web/app/projects/[name]/ProjectDetailsPage.tsx diff --git a/packages/web/app/projects/[id]/ProjectProvider.tsx b/packages/web/app/projects/[name]/ProjectProvider.tsx similarity index 100% rename from packages/web/app/projects/[id]/ProjectProvider.tsx rename to packages/web/app/projects/[name]/ProjectProvider.tsx diff --git a/packages/web/app/projects/[id]/devlogs/ProjectDevlogListPage.tsx b/packages/web/app/projects/[name]/devlogs/ProjectDevlogListPage.tsx similarity index 100% rename from packages/web/app/projects/[id]/devlogs/ProjectDevlogListPage.tsx rename to packages/web/app/projects/[name]/devlogs/ProjectDevlogListPage.tsx diff --git a/packages/web/app/projects/[id]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx b/packages/web/app/projects/[name]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx similarity index 100% rename from packages/web/app/projects/[id]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx rename to packages/web/app/projects/[name]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx diff --git a/packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx b/packages/web/app/projects/[name]/devlogs/[devlogId]/page.tsx similarity index 100% rename from packages/web/app/projects/[id]/devlogs/[devlogId]/page.tsx rename to packages/web/app/projects/[name]/devlogs/[devlogId]/page.tsx diff --git a/packages/web/app/projects/[id]/devlogs/page.tsx b/packages/web/app/projects/[name]/devlogs/page.tsx similarity index 100% rename from packages/web/app/projects/[id]/devlogs/page.tsx rename to packages/web/app/projects/[name]/devlogs/page.tsx diff --git a/packages/web/app/projects/[id]/layout.tsx b/packages/web/app/projects/[name]/layout.tsx similarity index 100% rename from packages/web/app/projects/[id]/layout.tsx rename to packages/web/app/projects/[name]/layout.tsx diff --git a/packages/web/app/projects/[id]/page.tsx b/packages/web/app/projects/[name]/page.tsx similarity index 100% rename from packages/web/app/projects/[id]/page.tsx rename to packages/web/app/projects/[name]/page.tsx diff --git a/packages/web/app/projects/[id]/settings/ProjectSettingsPage.tsx b/packages/web/app/projects/[name]/settings/ProjectSettingsPage.tsx similarity index 100% rename from packages/web/app/projects/[id]/settings/ProjectSettingsPage.tsx rename to packages/web/app/projects/[name]/settings/ProjectSettingsPage.tsx diff --git a/packages/web/app/projects/[id]/settings/page.tsx b/packages/web/app/projects/[name]/settings/page.tsx similarity index 100% rename from packages/web/app/projects/[id]/settings/page.tsx rename to packages/web/app/projects/[name]/settings/page.tsx From ceedf5c9a5575406ae60e48e7cec8ebb9e908113 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Thu, 21 Aug 2025 00:23:46 +0800 Subject: [PATCH 14/50] feat: refactor project routing to use name-based identifiers and enhance service helper methods --- .../[devlogId]/notes/[noteId]/route.ts | 63 ++++++++++--------- .../[name]/devlogs/[devlogId]/notes/route.ts | 59 ++++++++--------- .../[name]/devlogs/[devlogId]/route.ts | 57 +++++++++-------- .../app/api/projects/[name]/devlogs/route.ts | 22 +++---- .../projects/[name]/devlogs/search/route.ts | 11 ++-- .../[name]/devlogs/stats/overview/route.ts | 14 +++-- .../[name]/devlogs/stats/timeseries/route.ts | 18 +++--- packages/web/app/api/projects/[name]/route.ts | 39 ++++-------- packages/web/app/lib/api/api-utils.ts | 38 +++-------- packages/web/app/lib/project-urls.ts | 47 ++++---------- 10 files changed, 157 insertions(+), 211 deletions(-) diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts index 1c7b891e..29d06d15 100644 --- a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from 'next/server'; import type { DevlogNoteCategory } from '@codervisor/devlog-core'; import { DevlogService, ProjectService } from '@codervisor/devlog-core/server'; -import { ApiErrors, createSuccessResponse, RouteParams } from '@/lib/api/api-utils'; +import { ApiErrors, createSuccessResponse, RouteParams, ServiceHelper } from '@/lib/api/api-utils'; import { RealtimeEventType } from '@/lib/realtime'; import { z } from 'zod'; @@ -17,27 +17,28 @@ const UpdateNoteBodySchema = z.object({ // GET /api/projects/[name]/devlogs/[devlogId]/notes/[noteId] - Get specific note export async function GET( request: NextRequest, - { params }: { params: { id: string; devlogId: string; noteId: string } }, + { params }: { params: { name: string; devlogId: string; noteId: string } }, ) { try { - // Parse and validate parameters - const paramResult = RouteParams.parseProjectAndDevlogId(params); + // Parse and validate parameters - only parse name and devlogId, handle noteId separately + const paramResult = RouteParams.parseProjectNameAndDevlogId(params); if (!paramResult.success) { return paramResult.response; } - const { projectId } = paramResult.data; + const { projectName, devlogId } = paramResult.data; const { noteId } = params; - // Ensure project exists - const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + // Create project-aware devlog service - const devlogService = DevlogService.getInstance(projectId); + const devlogService = DevlogService.getInstance(project.id); // Get the note const note = await devlogService.getNote(noteId); @@ -55,16 +56,16 @@ export async function GET( // PUT /api/projects/[name]/devlogs/[devlogId]/notes/[noteId] - Update specific note export async function PUT( request: NextRequest, - { params }: { params: { id: string; devlogId: string; noteId: string } }, + { params }: { params: { name: string; devlogId: string; noteId: string } }, ) { try { // Parse and validate parameters - const paramResult = RouteParams.parseProjectAndDevlogId(params); + const paramResult = RouteParams.parseProjectNameAndDevlogId(params); if (!paramResult.success) { return paramResult.response; } - const { projectId } = paramResult.data; + const { projectName, devlogId } = paramResult.data; const { noteId } = params; // Validate request body @@ -76,15 +77,16 @@ export async function PUT( const updates = validationResult.data; - // Ensure project exists - const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + // Create project-aware devlog service - const devlogService = DevlogService.getInstance(projectId); + const devlogService = DevlogService.getInstance(project.id); // Update the note const updatedNote = await devlogService.updateNote(noteId, { @@ -107,27 +109,28 @@ export async function PUT( // DELETE /api/projects/[name]/devlogs/[devlogId]/notes/[noteId] - Delete specific note export async function DELETE( request: NextRequest, - { params }: { params: { id: string; devlogId: string; noteId: string } }, + { params }: { params: { name: string; devlogId: string; noteId: string } }, ) { try { // Parse and validate parameters - const paramResult = RouteParams.parseProjectAndDevlogId(params); + const paramResult = RouteParams.parseProjectNameAndDevlogId(params); if (!paramResult.success) { return paramResult.response; } - const { projectId } = paramResult.data; - const { noteId, devlogId } = params; + const { projectName, devlogId } = paramResult.data; + const { noteId } = params; - // Ensure project exists - const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + // Create project-aware devlog service - const devlogService = DevlogService.getInstance(projectId); + const devlogService = DevlogService.getInstance(project.id); // Delete the note await devlogService.deleteNote(noteId); diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts index f82cde79..be2d960c 100644 --- a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from 'next/server'; import type { DevlogNoteCategory } from '@codervisor/devlog-core'; import { DevlogService, ProjectService } from '@codervisor/devlog-core/server'; -import { ApiErrors, createSuccessResponse, RouteParams } from '@/lib/api/api-utils'; +import { ApiErrors, createSuccessResponse, RouteParams, ServiceHelper } from '@/lib/api/api-utils'; import { RealtimeEventType } from '@/lib/realtime'; import { DevlogAddNoteBodySchema, DevlogUpdateWithNoteBodySchema } from '@/schemas'; @@ -11,16 +11,16 @@ export const dynamic = 'force-dynamic'; // GET /api/projects/[name]/devlogs/[devlogId]/notes - List notes for a devlog entry export async function GET( request: NextRequest, - { params }: { params: { id: string; devlogId: string } }, + { params }: { params: { name: string; devlogId: string } }, ) { try { // Parse and validate parameters - const paramResult = RouteParams.parseProjectAndDevlogId(params); + const paramResult = RouteParams.parseProjectNameAndDevlogId(params); if (!paramResult.success) { return paramResult.response; } - const { projectId, devlogId } = paramResult.data; + const { projectName, devlogId } = paramResult.data; // Parse query parameters const { searchParams } = new URL(request.url); @@ -32,15 +32,16 @@ export async function GET( return ApiErrors.invalidRequest('Limit must be a number between 1 and 1000'); } - // Ensure project exists - const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + // Create project-aware devlog service - const devlogService = DevlogService.getInstance(projectId); + const devlogService = DevlogService.getInstance(project.id); // Verify devlog exists const devlogEntry = await devlogService.get(devlogId, false); // Don't load notes yet @@ -70,16 +71,16 @@ export async function GET( // POST /api/projects/[name]/devlogs/[devlogId]/notes - Add note to devlog entry export async function POST( request: NextRequest, - { params }: { params: { id: string; devlogId: string } }, + { params }: { params: { name: string; devlogId: string } }, ) { try { // Parse and validate parameters - const paramResult = RouteParams.parseProjectAndDevlogId(params); + const paramResult = RouteParams.parseProjectNameAndDevlogId(params); if (!paramResult.success) { return paramResult.response; } - const { projectId, devlogId } = paramResult.data; + const { projectName, devlogId } = paramResult.data; // Validate request body const data = await request.json(); @@ -90,15 +91,16 @@ export async function POST( const { note, category } = validationResult.data; - // Ensure project exists - const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + // Create project-aware devlog service - const devlogService = DevlogService.getInstance(projectId); + const devlogService = DevlogService.getInstance(project.id); // Add the note directly using the new addNote method const newNote = await devlogService.addNote(devlogId, { @@ -119,16 +121,16 @@ export async function POST( // PUT /api/projects/[name]/devlogs/[devlogId]/notes - Update devlog and add note in one operation export async function PUT( request: NextRequest, - { params }: { params: { id: string; devlogId: string } }, + { params }: { params: { name: string; devlogId: string } }, ) { try { // Parse and validate parameters - const paramResult = RouteParams.parseProjectAndDevlogId(params); + const paramResult = RouteParams.parseProjectNameAndDevlogId(params); if (!paramResult.success) { return paramResult.response; } - const { projectId, devlogId } = paramResult.data; + const { projectName, devlogId } = paramResult.data; // Validate request body const data = await request.json(); @@ -139,15 +141,16 @@ export async function PUT( const { note, category, ...updateFields } = validationResult.data; - // Ensure project exists - const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + // Create project-aware devlog service - const devlogService = DevlogService.getInstance(projectId); + const devlogService = DevlogService.getInstance(project.id); // Get the existing devlog entry const existingEntry = await devlogService.get(devlogId, false); // Don't load notes diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts index 16dadad6..6343cc3c 100644 --- a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts @@ -1,6 +1,6 @@ import { NextRequest } from 'next/server'; import { DevlogService, ProjectService } from '@codervisor/devlog-core/server'; -import { ApiErrors, createSuccessResponse, RouteParams } from '@/lib/api/api-utils'; +import { ApiErrors, createSuccessResponse, RouteParams, ServiceHelper } from '@/lib/api/api-utils'; import { RealtimeEventType } from '@/lib/realtime'; // Mark this route as dynamic to prevent static generation @@ -9,16 +9,16 @@ export const dynamic = 'force-dynamic'; // GET /api/projects/[name]/devlogs/[devlogId] - Get specific devlog entry export async function GET( request: NextRequest, - { params }: { params: { id: string; devlogId: string } }, + { params }: { params: { name: string; devlogId: string } }, ) { try { // Parse and validate parameters - const paramResult = RouteParams.parseProjectAndDevlogId(params); + const paramResult = RouteParams.parseProjectNameAndDevlogId(params); if (!paramResult.success) { return paramResult.response; } - const { projectId, devlogId } = paramResult.data; + const { projectName, devlogId } = paramResult.data; // Parse query parameters for notes const { searchParams } = new URL(request.url); @@ -27,13 +27,15 @@ export async function GET( ? parseInt(searchParams.get('notesLimit')!) : undefined; - const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - if (!project) { - return ApiErrors.projectNotFound(); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; } - const devlogService = DevlogService.getInstance(projectId); + const project = projectResult.data.project; + + const devlogService = DevlogService.getInstance(project.id); const entry = await devlogService.get(devlogId, includeNotes); if (!entry) { @@ -56,26 +58,27 @@ export async function GET( // PUT /api/projects/[name]/devlogs/[devlogId] - Update devlog entry export async function PUT( request: NextRequest, - { params }: { params: { id: string; devlogId: string } }, + { params }: { params: { name: string; devlogId: string } }, ) { try { // Parse and validate parameters - const paramResult = RouteParams.parseProjectAndDevlogId(params); + const paramResult = RouteParams.parseProjectNameAndDevlogId(params); if (!paramResult.success) { return paramResult.response; } - const { projectId, devlogId } = paramResult.data; + const { projectName, devlogId } = paramResult.data; - const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - if (!project) { - return ApiErrors.projectNotFound(); + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; } + const project = projectResult.data.project; + const data = await request.json(); - const devlogService = DevlogService.getInstance(projectId); + const devlogService = DevlogService.getInstance(project.id); // Verify entry exists and belongs to project const existingEntry = await devlogService.get(devlogId); @@ -88,7 +91,7 @@ export async function PUT( ...existingEntry, ...data, id: devlogId, - projectId: projectId, // Ensure project context is maintained + projectId: project.id, // Ensure project context is maintained updatedAt: new Date().toISOString(), }; @@ -106,25 +109,25 @@ export async function PUT( // DELETE /api/projects/[name]/devlogs/[devlogId] - Delete devlog entry export async function DELETE( request: NextRequest, - { params }: { params: { id: string; devlogId: string } }, + { params }: { params: { name: string; devlogId: string } }, ) { try { // Parse and validate parameters - const paramResult = RouteParams.parseProjectAndDevlogId(params); + const paramResult = RouteParams.parseProjectNameAndDevlogId(params); if (!paramResult.success) { return paramResult.response; } - const { projectId, devlogId } = paramResult.data; + const { projectName, devlogId } = paramResult.data; - const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - - if (!project) { - return ApiErrors.projectNotFound(); + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; } - const devlogService = DevlogService.getInstance(projectId); + const project = projectResult.data.project; + + const devlogService = DevlogService.getInstance(project.id); // Verify entry exists and belongs to project const existingEntry = await devlogService.get(devlogId); diff --git a/packages/web/app/api/projects/[name]/devlogs/route.ts b/packages/web/app/api/projects/[name]/devlogs/route.ts index c5350bc0..ac205be8 100644 --- a/packages/web/app/api/projects/[name]/devlogs/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/route.ts @@ -16,15 +16,15 @@ import { RealtimeEventType } from '@/lib/realtime'; export const dynamic = 'force-dynamic'; // GET /api/projects/[name]/devlogs - List devlogs for a project -export async function GET(request: NextRequest, { params }: { params: { id: string } }) { +export async function GET(request: NextRequest, { params }: { params: { name: string } }) { try { // Parse and validate project identifier - const paramResult = RouteParams.parseProjectId(params); + const paramResult = RouteParams.parseProjectName(params); if (!paramResult.success) { return paramResult.response; } - const { identifier, identifierType } = paramResult.data; + const { projectName } = paramResult.data; // Validate query parameters const url = new URL(request.url); @@ -34,10 +34,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri } // Get project using helper - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( - identifier, - identifierType, - ); + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); if (!projectResult.success) { return projectResult.response; } @@ -97,15 +94,15 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri } // POST /api/projects/[name]/devlogs - Create new devlog entry -export async function POST(request: NextRequest, { params }: { params: { id: string } }) { +export async function POST(request: NextRequest, { params }: { params: { name: string } }) { try { // Parse and validate project identifier - const paramResult = RouteParams.parseProjectId(params); + const paramResult = RouteParams.parseProjectName(params); if (!paramResult.success) { return paramResult.response; } - const { identifier, identifierType } = paramResult.data; + const { projectName } = paramResult.data; // Validate request body const bodyValidation = await ApiValidator.validateJsonBody(request, CreateDevlogBodySchema); @@ -114,10 +111,7 @@ export async function POST(request: NextRequest, { params }: { params: { id: str } // Get project using helper - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( - identifier, - identifierType, - ); + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); if (!projectResult.success) { return projectResult.response; } diff --git a/packages/web/app/api/projects/[name]/devlogs/search/route.ts b/packages/web/app/api/projects/[name]/devlogs/search/route.ts index 7fccad3b..992ad01d 100644 --- a/packages/web/app/api/projects/[name]/devlogs/search/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/search/route.ts @@ -29,15 +29,15 @@ interface SearchResponse { } // GET /api/projects/[name]/devlogs/search - Enhanced search for devlogs -export async function GET(request: NextRequest, { params }: { params: { id: string } }) { +export async function GET(request: NextRequest, { params }: { params: { name: string } }) { try { // Parse and validate project name parameter - const paramResult = RouteParams.parseProjectId(params); + const paramResult = RouteParams.parseProjectName(params); if (!paramResult.success) { return paramResult.response; } - const { identifier, identifierType } = paramResult.data; + const { projectName } = paramResult.data; // Validate query parameters const url = new URL(request.url); @@ -47,10 +47,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri } // Get project using helper - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( - identifier, - identifierType, - ); + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); if (!projectResult.success) { return projectResult.response; } diff --git a/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts b/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts index bd2118a9..e5f541e5 100644 --- a/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts @@ -12,23 +12,25 @@ export const dynamic = 'force-dynamic'; // GET /api/projects/[name]/devlogs/stats/overview - Get overview statistics export const GET = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { + async (request: NextRequest, { params }: { params: { name: string } }) => { // Parse and validate parameters - const paramResult = RouteParams.parseProjectId(params); + const paramResult = RouteParams.parseProjectName(params); if (!paramResult.success) { return paramResult.response; } - const { projectId } = paramResult.data; + const { projectName } = paramResult.data; - // Ensure project exists - const projectResult = await ServiceHelper.getProjectOrFail(projectId); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); if (!projectResult.success) { return projectResult.response; } + const project = projectResult.data.project; + // Get devlog service and stats - const devlogService = await ServiceHelper.getDevlogService(projectId); + const devlogService = await ServiceHelper.getDevlogService(project.id); const stats = await devlogService.getStats(); return createSuccessResponse(stats); diff --git a/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts b/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts index aa7f28bf..995c9ecc 100644 --- a/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts @@ -12,21 +12,23 @@ export const dynamic = 'force-dynamic'; // GET /api/projects/[name]/devlogs/stats/timeseries - Get time series statistics export const GET = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { + async (request: NextRequest, { params }: { params: { name: string } }) => { // Parse and validate parameters - const paramResult = RouteParams.parseProjectId(params); + const paramResult = RouteParams.parseProjectName(params); if (!paramResult.success) { return paramResult.response; } - const { projectId } = paramResult.data; + const { projectName } = paramResult.data; - // Ensure project exists - const projectResult = await ServiceHelper.getProjectOrFail(projectId); + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); if (!projectResult.success) { return projectResult.response; } + const project = projectResult.data.project; + // Parse query parameters const url = new URL(request.url); const searchParams = url.searchParams; @@ -44,12 +46,12 @@ export const GET = withErrorHandling( days, ...(from && { from }), ...(to && { to }), - projectId, + projectId: project.id, }; // Get devlog service and time series stats - const devlogService = await ServiceHelper.getDevlogService(projectId); - const stats = await devlogService.getTimeSeriesStats(projectId, timeSeriesRequest); + const devlogService = await ServiceHelper.getDevlogService(project.id); + const stats = await devlogService.getTimeSeriesStats(project.id, timeSeriesRequest); return createSuccessResponse(stats); }, diff --git a/packages/web/app/api/projects/[name]/route.ts b/packages/web/app/api/projects/[name]/route.ts index 1d5b87c8..f9465426 100644 --- a/packages/web/app/api/projects/[name]/route.ts +++ b/packages/web/app/api/projects/[name]/route.ts @@ -13,39 +13,32 @@ export const dynamic = 'force-dynamic'; // GET /api/projects/[name] - Get specific project export const GET = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { - // Parse and validate parameters - const paramResult = RouteParams.parseProjectId(params); + async (req: NextRequest, { params }: { params: { name: string } }) => { + const paramResult = RouteParams.parseProjectName(params); if (!paramResult.success) { return paramResult.response; } - const { identifier, identifierType } = paramResult.data; - - // Get project using helper - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( - identifier, - identifierType, - ); + const { projectName } = paramResult.data; + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); if (!projectResult.success) { return projectResult.response; } - // Transform and return project data - return createSuccessResponse(projectResult.data!.project); + return createSuccessResponse(projectResult.data.project); }, ); // PUT /api/projects/[name] - Update project export const PUT = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { + async (request: NextRequest, { params }: { params: { name: string } }) => { // Parse and validate parameters - const paramResult = RouteParams.parseProjectId(params); + const paramResult = RouteParams.parseProjectName(params); if (!paramResult.success) { return paramResult.response; } - const { identifier, identifierType } = paramResult.data; + const { projectName } = paramResult.data; // Validate request body (HTTP layer validation) const bodyValidation = await ApiValidator.validateJsonBody(request, UpdateProjectBodySchema); @@ -54,10 +47,7 @@ export const PUT = withErrorHandling( } // Get project and service - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( - identifier, - identifierType, - ); + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); if (!projectResult.success) { return projectResult.response; } @@ -76,20 +66,17 @@ export const PUT = withErrorHandling( // DELETE /api/projects/[name] - Delete project export const DELETE = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { + async (request: NextRequest, { params }: { params: { name: string } }) => { // Parse and validate parameters - const paramResult = RouteParams.parseProjectId(params); + const paramResult = RouteParams.parseProjectName(params); if (!paramResult.success) { return paramResult.response; } - const { identifier, identifierType } = paramResult.data; + const { projectName } = paramResult.data; // Get project service - const projectResult = await ServiceHelper.getProjectByIdentifierOrFail( - identifier, - identifierType, - ); + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); if (!projectResult.success) { return projectResult.response; } diff --git a/packages/web/app/lib/api/api-utils.ts b/packages/web/app/lib/api/api-utils.ts index fa22f27e..1353bb8b 100644 --- a/packages/web/app/lib/api/api-utils.ts +++ b/packages/web/app/lib/api/api-utils.ts @@ -22,9 +22,9 @@ export const RouteParams = { * Parse project name parameter (name-only routing) * Usage: /api/projects/[name] */ - parseProjectId(params: { id: string }) { + parseProjectName(params: { name: string }) { try { - const validation = isValidProjectIdentifier(params.id); + const validation = isValidProjectIdentifier(params.name); if (!validation.valid) { return { @@ -40,9 +40,7 @@ export const RouteParams = { return { success: true as const, data: { - projectId: -1, // Will be resolved by service helper - identifier: params.id, - identifierType: 'name' as const, + projectName: params.name, }, }; } catch (error) { @@ -57,9 +55,9 @@ export const RouteParams = { * Parse project name and devlog ID parameters (name-only routing for projects) * Usage: /api/projects/[name]/devlogs/[devlogId] */ - parseProjectAndDevlogId(params: { id: string; devlogId: string }) { + parseProjectNameAndDevlogId(params: { name: string; devlogId: string }) { try { - const projectValidation = isValidProjectIdentifier(params.id); + const projectValidation = isValidProjectIdentifier(params.name); const devlogId = parseInt(params.devlogId, 10); if (!projectValidation.valid) { @@ -86,10 +84,8 @@ export const RouteParams = { return { success: true as const, data: { - projectId: -1, // Will be resolved by service helper + projectName: params.name, devlogId, - identifier: params.id, - identifierType: 'name' as const, }, }; } catch (error) { @@ -107,28 +103,12 @@ export const RouteParams = { */ export class ServiceHelper { /** - * Get project by ID and ensure it exists + * Get project by name and ensure it exists */ - static async getProjectOrFail(projectId: number) { + static async getProjectByNameOrFail(projectName: string) { const { ProjectService } = await import('@codervisor/devlog-core/server'); const projectService = ProjectService.getInstance(); - const project = await projectService.get(projectId); - if (!project) { - return { success: false as const, response: ApiErrors.projectNotFound() }; - } - - return { success: true as const, data: { project, projectService } }; - } - - /** - * Get project by name and ensure it exists (case-insensitive lookup) - */ - static async getProjectByIdentifierOrFail(identifier: string, identifierType: 'name') { - const { ProjectService } = await import('@codervisor/devlog-core/server'); - const projectService = ProjectService.getInstance(); - - // Only name-based routing supported now - const project = await projectService.getByName(identifier); + const project = await projectService.getByName(projectName); if (!project) { return { success: false as const, response: ApiErrors.projectNotFound() }; diff --git a/packages/web/app/lib/project-urls.ts b/packages/web/app/lib/project-urls.ts index e08b8232..e3b5c019 100644 --- a/packages/web/app/lib/project-urls.ts +++ b/packages/web/app/lib/project-urls.ts @@ -3,68 +3,43 @@ */ import { generateSlugFromName } from '@codervisor/devlog-core'; -import { apiClient } from '@/lib'; /** - * Generate project URLs using name-based routing - * Falls back to ID-based routing if name is not available + * Generate project URLs using name-based routing only */ export class ProjectUrls { /** * Generate URL for project main page */ - static project(projectId: number, projectName?: string): string { - if (projectName) { - return `/projects/${generateSlugFromName(projectName)}`; - } - return `/projects/${projectId}`; + static project(projectName: string): string { + return `/projects/${generateSlugFromName(projectName)}`; } /** * Generate URL for project devlogs list */ - static devlogs(projectId: number, projectName?: string): string { - return `${this.project(projectId, projectName)}/devlogs`; + static devlogs(projectName: string): string { + return `${this.project(projectName)}/devlogs`; } /** * Generate URL for specific devlog */ - static devlog(projectId: number, devlogId: number, projectName?: string): string { - return `${this.devlogs(projectId, projectName)}/${devlogId}`; + static devlog(projectName: string, devlogId: number): string { + return `${this.devlogs(projectName)}/${devlogId}`; } /** * Generate URL for project settings */ - static settings(projectId: number, projectName?: string): string { - return `${this.project(projectId, projectName)}/settings`; + static settings(projectName: string): string { + return `${this.project(projectName)}/settings`; } /** * Generate URL for creating a new devlog in project */ - static createDevlog(projectId: number, projectName?: string): string { - return `${this.devlogs(projectId, projectName)}/create`; - } -} - -/** - * Legacy support - helper to get project name from current context - * This can be used when we have projectId but need to fetch the name - */ -export async function getProjectName(projectNameOrId: string | number): Promise { - try { - // If it's already a string (project name), return it - if (typeof projectNameOrId === 'string') { - return projectNameOrId; - } - - // If it's a number (legacy project ID), fetch the name - const project = await apiClient.get<{ name: string }>(`/api/projects/${projectNameOrId}`); - return project.name; - } catch (error) { - console.error('Failed to fetch project name:', error); - return null; + static createDevlog(projectName: string): string { + return `${this.devlogs(projectName)}/create`; } } From 7f6558dca83d6d557e9deda38fc7fb035f36e58e Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 22 Aug 2025 11:08:55 +0800 Subject: [PATCH 15/50] chore: update pnpm version to 10.15.0 across configuration files --- .devcontainer/devcontainer.json | 2 +- .github/scripts/README.md | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/pr-validation.yml | 2 +- .github/workflows/vscode-automation.yml | 2 +- README.md | 2 +- package.json | 2 +- packages/mcp/src/config/mcp-config.ts | 83 ------------------------- 8 files changed, 7 insertions(+), 90 deletions(-) delete mode 100644 packages/mcp/src/config/mcp-config.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4d5970f9..d29686d6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,7 +15,7 @@ "forwardPorts": [3000, 3001, 5000, 5173, 8080], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "npm install -g pnpm@10.13.1 && pnpm install && pnpm build:types", + "postCreateCommand": "npm install -g pnpm@10.15.0 && pnpm install && pnpm build:types", // Configure tool-specific properties. "customizations": { diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 4e55b3a2..d73c9f98 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -19,7 +19,7 @@ Sets up Node.js environment and installs dependencies with pnpm. ```bash ./.github/scripts/setup-node.sh [node_version] [pnpm_version] ``` -- **Default**: Node.js 20, pnpm 10.13.1 +- **Default**: Node.js 20, pnpm 10.15.0 - **Used in**: All workflows that need Node.js #### `build-packages.sh` diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cb0ebfec..c5c972d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} NODE_VERSION: 22 - PNPM_VERSION: 10.13.1 + PNPM_VERSION: 10.15.0 jobs: # Phase 1: Build and Test diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 452a69e1..a18ffc06 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -21,7 +21,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 10.13.1 + version: 10.15.0 run_install: false - name: Install dependencies diff --git a/.github/workflows/vscode-automation.yml b/.github/workflows/vscode-automation.yml index 1b561b92..cf65955b 100644 --- a/.github/workflows/vscode-automation.yml +++ b/.github/workflows/vscode-automation.yml @@ -45,7 +45,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 10.13.1 + version: 10.15.0 run_install: false - name: Generate cache key diff --git a/README.md b/README.md index 7854707d..67a3fd20 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Next.js web interface for visual devlog management: ### Prerequisites - Node.js 18+ -- pnpm 10.13.1+ +- pnpm 10.15.0+ ### Installation diff --git a/package.json b/package.json index b82479d6..c8eb5bfd 100644 --- a/package.json +++ b/package.json @@ -70,5 +70,5 @@ "dotenv": "16.5.0", "tsx": "^4.0.0" }, - "packageManager": "pnpm@10.13.1" + "packageManager": "pnpm@10.15.0" } diff --git a/packages/mcp/src/config/mcp-config.ts b/packages/mcp/src/config/mcp-config.ts deleted file mode 100644 index cefb5b4c..00000000 --- a/packages/mcp/src/config/mcp-config.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Configuration for MCP server - * Uses HTTP API client for secure and isolated access to devlog operations - */ - -import { logger } from '../server/index.js'; - -export interface MCPServerConfig { - /** Default project ID */ - defaultProjectId?: string; - /** Web API configuration */ - webApi: { - /** Base URL for the web API server */ - baseUrl: string; - /** Request timeout in milliseconds */ - timeout?: number; - /** Number of retry attempts */ - retries?: number; - /** Auto-discovery of web service */ - autoDiscover?: boolean; - }; -} - -/** - * Load MCP server configuration from environment variables - */ -export function loadMCPConfig(): MCPServerConfig { - const defaultProjectId = process.env.MCP_DEFAULT_PROJECT || 'default'; - const baseUrl = process.env.MCP_WEB_API_URL || 'http://localhost:3200'; - const timeout = process.env.MCP_WEB_API_TIMEOUT - ? parseInt(process.env.MCP_WEB_API_TIMEOUT, 10) - : 30000; - const retries = process.env.MCP_WEB_API_RETRIES - ? parseInt(process.env.MCP_WEB_API_RETRIES, 10) - : 3; - const autoDiscover = process.env.MCP_WEB_API_AUTO_DISCOVER !== 'false'; // Default to true - - return { - defaultProjectId, - webApi: { - baseUrl, - timeout, - retries, - autoDiscover, - }, - }; -} - -/** - * Validate MCP configuration - */ -export function validateMCPConfig(config: MCPServerConfig): void { - if (!config.webApi?.baseUrl) { - throw new Error('Web API base URL is required'); - } - - try { - new URL(config.webApi.baseUrl); - } catch { - throw new Error(`Invalid web API base URL: ${config.webApi.baseUrl}`); - } - - if (config.webApi?.timeout && config.webApi.timeout < 1000) { - throw new Error('Web API timeout must be at least 1000ms'); - } - - if (config.webApi?.retries && (config.webApi.retries < 0 || config.webApi.retries > 10)) { - throw new Error('Web API retries must be between 0 and 10'); - } -} - -/** - * Print configuration summary for debugging - */ -export function printConfigSummary(config: MCPServerConfig): void { - logger.info('\n=== MCP Server Configuration ==='); - logger.info(`Default Project: ${config.defaultProjectId}`); - logger.info(`Web API URL: ${config.webApi.baseUrl}`); - logger.info(`Timeout: ${config.webApi.timeout}ms`); - logger.info(`Retries: ${config.webApi.retries}`); - logger.info(`Auto-discover: ${config.webApi.autoDiscover}`); - logger.info('================================\n'); -} From d92bd0da46594ca093772c69f48e00722dcf0329 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 22 Aug 2025 11:36:18 +0800 Subject: [PATCH 16/50] feat: update routing to use name-based identifiers for projects and devlogs, enhancing URL clarity and consistency --- packages/web/ROUTING.md | 23 +++++-- .../[devlogId]/notes/[noteId]/route.ts | 6 +- .../[name]/devlogs/[devlogId]/notes/route.ts | 6 +- .../[name]/devlogs/[devlogId]/route.ts | 6 +- packages/web/app/lib/api/api-utils.ts | 2 +- packages/web/app/lib/api/devlog-api-client.ts | 17 ++++- packages/web/app/lib/api/index.ts | 1 - packages/web/app/lib/api/note-api-client.ts | 65 ------------------- packages/web/app/lib/routing/route-params.ts | 39 +---------- .../app/projects/[name]/ProjectProvider.tsx | 16 ++--- .../[name]/devlogs/DevlogProvider.tsx | 39 +++++++++++ .../[name]/devlogs/[devlogId]/page.tsx | 15 ----- .../ProjectDevlogDetailsPage.tsx | 11 ++-- .../app/projects/[name]/devlogs/[id]/page.tsx | 5 ++ packages/web/app/projects/[name]/layout.tsx | 46 ++++--------- 15 files changed, 110 insertions(+), 187 deletions(-) delete mode 100644 packages/web/app/lib/api/note-api-client.ts create mode 100644 packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx delete mode 100644 packages/web/app/projects/[name]/devlogs/[devlogId]/page.tsx rename packages/web/app/projects/[name]/devlogs/{[devlogId] => [id]}/ProjectDevlogDetailsPage.tsx (95%) create mode 100644 packages/web/app/projects/[name]/devlogs/[id]/page.tsx diff --git a/packages/web/ROUTING.md b/packages/web/ROUTING.md index 1fcf533d..9eb51a6c 100644 --- a/packages/web/ROUTING.md +++ b/packages/web/ROUTING.md @@ -2,18 +2,19 @@ ## Overview -The web package uses Next.js 14 App Router with hierarchical routing structure that matches the API endpoints. This provides better organization and clearer URL structure for project-scoped operations. +The web package uses Next.js 14 App Router with hierarchical routing structure that matches the API endpoints. This +provides better organization and clearer URL structure for project-scoped operations. ## Route Structure ### Hierarchical Project-Based Routes (Primary) + ``` / - Dashboard (homepage) /projects - Project management page -/projects/[id] - Project details page -/projects/[id]/devlogs - List of devlogs for specific project -/projects/[id]/devlogs/create - Create new devlog in specific project -/projects/[id]/devlogs/[devlogId] - Individual devlog details within project +/projects/[name] - Project details page +/projects/[name]/devlogs - List of devlogs for specific project +/projects/[name]/devlogs/[id] - Individual devlog details within project ``` ````markdown @@ -21,11 +22,13 @@ The web package uses Next.js 14 App Router with hierarchical routing structure t ## Overview -The web package uses Next.js 14 App Router with hierarchical routing structure that matches the API endpoints. This provides better organization and clearer URL structure for project-scoped operations. +The web package uses Next.js 14 App Router with hierarchical routing structure that matches the API endpoints. This +provides better organization and clearer URL structure for project-scoped operations. ## Route Structure ### Hierarchical Project-Based Routes (Primary) + ``` / - Redirects to /projects /projects - Project list page (main entry point) @@ -38,11 +41,14 @@ The web package uses Next.js 14 App Router with hierarchical routing structure t ## Key Changes Made ### Route Purpose Updates + - **`/` (Homepage)**: Now redirects to `/projects` as the main entry point - **`/projects`**: Project list/management page - browse and manage all projects -- **`/projects/[id]`**: Project dashboard - overview with stats, charts, and recent devlogs (formerly the homepage dashboard) +- **`/projects/[id]`**: Project dashboard - overview with stats, charts, and recent devlogs (formerly the homepage + dashboard) ### Navigation Flow + 1. **User visits `/`** → Automatically redirected to `/projects` 2. **User browses projects** at `/projects` → Can view all projects and create new ones 3. **User selects a project** → Goes to `/projects/[id]` for that project's dashboard @@ -79,16 +85,19 @@ app/ ## Component Responsibilities ### ProjectManagementPage (/projects) + - **Primary function**: Project list and management interface - **Features**: Browse projects, create new projects, view project cards - **Navigation**: Links to individual project dashboards ### ProjectDetailsPage (/projects/[id]) + - **Primary function**: Project-specific dashboard and overview - **Features**: Project stats, time series charts, recent devlogs, overview stats - **Context**: Sets project context for the entire project section ### DashboardPage Component + - **Usage**: Embedded in ProjectDetailsPage for project-specific dashboards - **Features**: Stats visualization, time series data, recent devlogs display - **Scope**: Project-scoped rather than global diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts index 29d06d15..e4088329 100644 --- a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts @@ -14,7 +14,7 @@ const UpdateNoteBodySchema = z.object({ category: z.string().optional(), }); -// GET /api/projects/[name]/devlogs/[devlogId]/notes/[noteId] - Get specific note +// GET /api/projects/[name]/devlogs/[id]/notes/[noteId] - Get specific note export async function GET( request: NextRequest, { params }: { params: { name: string; devlogId: string; noteId: string } }, @@ -53,7 +53,7 @@ export async function GET( } } -// PUT /api/projects/[name]/devlogs/[devlogId]/notes/[noteId] - Update specific note +// PUT /api/projects/[name]/devlogs/[id]/notes/[noteId] - Update specific note export async function PUT( request: NextRequest, { params }: { params: { name: string; devlogId: string; noteId: string } }, @@ -106,7 +106,7 @@ export async function PUT( } } -// DELETE /api/projects/[name]/devlogs/[devlogId]/notes/[noteId] - Delete specific note +// DELETE /api/projects/[name]/devlogs/[id]/notes/[noteId] - Delete specific note export async function DELETE( request: NextRequest, { params }: { params: { name: string; devlogId: string; noteId: string } }, diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts index be2d960c..dbf92d67 100644 --- a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts @@ -8,7 +8,7 @@ import { DevlogAddNoteBodySchema, DevlogUpdateWithNoteBodySchema } from '@/schem // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[name]/devlogs/[devlogId]/notes - List notes for a devlog entry +// GET /api/projects/[name]/devlogs/[id]/notes - List notes for a devlog entry export async function GET( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, @@ -68,7 +68,7 @@ export async function GET( } } -// POST /api/projects/[name]/devlogs/[devlogId]/notes - Add note to devlog entry +// POST /api/projects/[name]/devlogs/[id]/notes - Add note to devlog entry export async function POST( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, @@ -118,7 +118,7 @@ export async function POST( } } -// PUT /api/projects/[name]/devlogs/[devlogId]/notes - Update devlog and add note in one operation +// PUT /api/projects/[name]/devlogs/[id]/notes - Update devlog and add note in one operation export async function PUT( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts index 6343cc3c..610d7f3e 100644 --- a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts @@ -6,7 +6,7 @@ import { RealtimeEventType } from '@/lib/realtime'; // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[name]/devlogs/[devlogId] - Get specific devlog entry +// GET /api/projects/[name]/devlogs/[id] - Get specific devlog entry export async function GET( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, @@ -55,7 +55,7 @@ export async function GET( } } -// PUT /api/projects/[name]/devlogs/[devlogId] - Update devlog entry +// PUT /api/projects/[name]/devlogs/[id] - Update devlog entry export async function PUT( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, @@ -106,7 +106,7 @@ export async function PUT( } } -// DELETE /api/projects/[name]/devlogs/[devlogId] - Delete devlog entry +// DELETE /api/projects/[name]/devlogs/[id] - Delete devlog entry export async function DELETE( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, diff --git a/packages/web/app/lib/api/api-utils.ts b/packages/web/app/lib/api/api-utils.ts index 1353bb8b..4cc2ccaa 100644 --- a/packages/web/app/lib/api/api-utils.ts +++ b/packages/web/app/lib/api/api-utils.ts @@ -53,7 +53,7 @@ export const RouteParams = { /** * Parse project name and devlog ID parameters (name-only routing for projects) - * Usage: /api/projects/[name]/devlogs/[devlogId] + * Usage: /api/projects/[name]/devlogs/[id] */ parseProjectNameAndDevlogId(params: { name: string; devlogId: string }) { try { diff --git a/packages/web/app/lib/api/devlog-api-client.ts b/packages/web/app/lib/api/devlog-api-client.ts index 9f8ecc24..d521f282 100644 --- a/packages/web/app/lib/api/devlog-api-client.ts +++ b/packages/web/app/lib/api/devlog-api-client.ts @@ -3,6 +3,7 @@ import type { DevlogFilter, DevlogId, DevlogNote, + DevlogNoteCategory, DevlogPriority, DevlogStats, DevlogStatus, @@ -13,7 +14,6 @@ import type { TimeSeriesStats, } from '@codervisor/devlog-core'; import { apiClient } from './api-client'; -import { CreateNoteRequest, UpdateNoteRequest } from '@/lib'; export interface CreateDevlogRequest { title: string; @@ -40,6 +40,16 @@ export interface BatchUpdateRequest { tags?: string[]; } +export interface CreateNoteRequest { + content: string; + category?: DevlogNoteCategory; +} + +export interface UpdateNoteRequest { + content?: string; + category?: DevlogNoteCategory; +} + export interface BatchNoteRequest { content: string; category?: string; @@ -107,7 +117,10 @@ export class DevlogApiClient { * Update an existing devlog */ async update(devlogId: DevlogId, data: UpdateDevlogRequest): Promise { - return apiClient.put(`/api/projects/${this.projectName}/devlogs/${devlogId}`, data); + return apiClient.put( + `/api/projects/${this.projectName}/devlogs/${devlogId}`, + data, + ); } /** diff --git a/packages/web/app/lib/api/index.ts b/packages/web/app/lib/api/index.ts index b0f999c7..9d18e536 100644 --- a/packages/web/app/lib/api/index.ts +++ b/packages/web/app/lib/api/index.ts @@ -5,4 +5,3 @@ export * from './api-client'; export * from './devlog-api-client'; -export * from './note-api-client'; diff --git a/packages/web/app/lib/api/note-api-client.ts b/packages/web/app/lib/api/note-api-client.ts deleted file mode 100644 index f96c709f..00000000 --- a/packages/web/app/lib/api/note-api-client.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { DevlogNote, DevlogNoteCategory } from '@codervisor/devlog-core'; -import { apiClient } from './api-client'; - -export interface CreateNoteRequest { - content: string; - category?: DevlogNoteCategory; -} - -export interface UpdateNoteRequest { - content?: string; - category?: DevlogNoteCategory; -} - -export class NoteApiClient { - constructor(private projectName: string) {} - - /** - * Add a note to a devlog - */ - async addNote(devlogId: string, data: CreateNoteRequest): Promise { - return apiClient.post( - `/api/projects/${this.projectName}/devlogs/${devlogId}/notes`, - data, - ); - } - - /** - * Get a specific note by ID - */ - async getNote(devlogId: string, noteId: string): Promise { - return apiClient.get( - `/api/projects/${this.projectName}/devlogs/${devlogId}/notes/${noteId}`, - ); - } - - /** - * Update a specific note - */ - async updateNote(devlogId: string, noteId: string, data: UpdateNoteRequest): Promise { - return apiClient.put( - `/api/projects/${this.projectName}/devlogs/${devlogId}/notes/${noteId}`, - data, - ); - } - - /** - * Delete a specific note - */ - async deleteNote(devlogId: string, noteId: string): Promise { - return apiClient.delete( - `/api/projects/${this.projectName}/devlogs/${devlogId}/notes/${noteId}`, - ); - } - - /** - * Get all notes for a devlog - */ - async getNotes(devlogId: string, limit?: number): Promise { - const params = limit ? `?limit=${limit}` : ''; - const response = await apiClient.get<{ devlogId: number; total: number; notes: DevlogNote[] }>( - `/api/projects/${this.projectName}/devlogs/${devlogId}/notes${params}`, - ); - return response.notes; - } -} diff --git a/packages/web/app/lib/routing/route-params.ts b/packages/web/app/lib/routing/route-params.ts index 76204f22..9bc5a8b4 100644 --- a/packages/web/app/lib/routing/route-params.ts +++ b/packages/web/app/lib/routing/route-params.ts @@ -63,46 +63,9 @@ export const RouteParamParsers = { /** * Parse project route parameters * For routes like: /projects/[name]/... + * @deprecated */ parseProjectParams(params: { id: string }): ParsedProjectParams { return parseProjectIdentifier(params.id, 'project identifier'); }, - - /** - * Parse project + devlog route parameters - * For routes like: /projects/[name]/devlogs/[devlogId]/... - */ - parseDevlogParams(params: { id: string; devlogId: string }): ParsedDevlogParams { - const projectInfo = parseProjectIdentifier(params.id, 'project identifier'); - return { - ...projectInfo, - devlogId: parseId(params.devlogId, 'devlog ID') as DevlogId, - }; - }, - - /** - * Parse single devlog ID parameter - * For routes like: /devlogs/[name]/... - */ - parseDevlogId(params: { id: string }): { devlogId: DevlogId } { - return { - devlogId: parseId(params.id, 'devlog ID') as DevlogId, - }; - }, }; - -/** - * Hook for parsing route parameters in client components - */ -export function useRouteParams

, T>( - params: P, - parser: (params: P) => T, -): T { - try { - return parser(params); - } catch (error) { - throw new Error( - `Route parameter error: ${error instanceof Error ? error.message : 'Invalid parameters'}`, - ); - } -} diff --git a/packages/web/app/projects/[name]/ProjectProvider.tsx b/packages/web/app/projects/[name]/ProjectProvider.tsx index bae43d28..e6c3672e 100644 --- a/packages/web/app/projects/[name]/ProjectProvider.tsx +++ b/packages/web/app/projects/[name]/ProjectProvider.tsx @@ -10,10 +10,10 @@ interface ProjectContextValue { const ProjectContext = createContext(null); -export function ProjectProvider({ - children, - project -}: { +export function ProjectProvider({ + children, + project, +}: { children: React.ReactNode; project: Project; }) { @@ -22,11 +22,7 @@ export function ProjectProvider({ projectName: project.name, }; - return ( - - {children} - - ); + return {children}; } export function useProject(): ProjectContextValue { @@ -40,4 +36,4 @@ export function useProject(): ProjectContextValue { export function useProjectName(): string { const { projectName } = useProject(); return projectName; -} \ No newline at end of file +} diff --git a/packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx b/packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx new file mode 100644 index 00000000..21a2c041 --- /dev/null +++ b/packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx @@ -0,0 +1,39 @@ +'use client'; + +import React, { createContext, useContext } from 'react'; +import type { DevlogEntry } from '@codervisor/devlog-core'; + +interface DevlogContextValue { + devlog: DevlogEntry; + devlogId: number; +} + +const DevlogContext = createContext(null); + +export function DevlogProvider({ + children, + devlog, +}: { + children: React.ReactNode; + devlog: DevlogEntry; +}) { + const value: DevlogContextValue = { + devlog, + devlogId: devlog.id!, + }; + + return {children}; +} + +export function useDevlog(): DevlogContextValue { + const context = useContext(DevlogContext); + if (!context) { + throw new Error('useDevlog must be used within a DevlogProvider'); + } + return context; +} + +export function useDevlogId(): number { + const { devlogId } = useDevlog(); + return devlogId; +} diff --git a/packages/web/app/projects/[name]/devlogs/[devlogId]/page.tsx b/packages/web/app/projects/[name]/devlogs/[devlogId]/page.tsx deleted file mode 100644 index d9f1fbb4..00000000 --- a/packages/web/app/projects/[name]/devlogs/[devlogId]/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ProjectDevlogDetailsPage } from './ProjectDevlogDetailsPage'; -import { RouteParamParsers } from '@/lib'; - -interface ProjectDevlogPageProps { - params: { - id: string; - devlogId: string; - }; -} - -export default function ProjectDevlogPage({ params }: ProjectDevlogPageProps) { - const { devlogId } = RouteParamParsers.parseDevlogParams(params); - - return ; -} diff --git a/packages/web/app/projects/[name]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx b/packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx similarity index 95% rename from packages/web/app/projects/[name]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx rename to packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx index 0f5b1dc1..55d797ed 100644 --- a/packages/web/app/projects/[name]/devlogs/[devlogId]/ProjectDevlogDetailsPage.tsx +++ b/packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx @@ -8,15 +8,12 @@ import { useRouter } from 'next/navigation'; import { ArrowLeftIcon, SaveIcon, TrashIcon, UndoIcon } from 'lucide-react'; import { toast } from 'sonner'; import { DevlogEntry } from '@codervisor/devlog-core'; -import { RealtimeEventType } from '@/lib/realtime'; -import { useProjectName } from '../../ProjectProvider'; +import { useProjectName } from '@/projects/[name]/ProjectProvider'; +import { useDevlogId } from '@/projects/[name]/devlogs/DevlogProvider'; -interface ProjectDevlogDetailsPageProps { - devlogId: number; -} - -export function ProjectDevlogDetailsPage({ devlogId }: ProjectDevlogDetailsPageProps) { +export function ProjectDevlogDetailsPage() { const projectName = useProjectName(); + const devlogId = useDevlogId(); const router = useRouter(); const { setCurrentProjectName } = useProjectStore(); diff --git a/packages/web/app/projects/[name]/devlogs/[id]/page.tsx b/packages/web/app/projects/[name]/devlogs/[id]/page.tsx new file mode 100644 index 00000000..112d6fbe --- /dev/null +++ b/packages/web/app/projects/[name]/devlogs/[id]/page.tsx @@ -0,0 +1,5 @@ +import { ProjectDevlogDetailsPage } from './ProjectDevlogDetailsPage'; + +export default function ProjectDevlogPage() { + return ; +} diff --git a/packages/web/app/projects/[name]/layout.tsx b/packages/web/app/projects/[name]/layout.tsx index 7b1bc3a7..4662b41a 100644 --- a/packages/web/app/projects/[name]/layout.tsx +++ b/packages/web/app/projects/[name]/layout.tsx @@ -1,8 +1,6 @@ import React from 'react'; import { ProjectService } from '@codervisor/devlog-core/server'; import { generateSlugFromName } from '@codervisor/devlog-core'; -import type { Project } from '@codervisor/devlog-core'; -import { RouteParamParsers } from '@/lib'; import { ProjectNotFound } from '@/components/ProjectNotFound'; import { redirect } from 'next/navigation'; import { ProjectProvider } from './ProjectProvider'; @@ -10,7 +8,7 @@ import { ProjectProvider } from './ProjectProvider'; interface ProjectLayoutProps { children: React.ReactNode; params: { - id: string; + name: string; // The project name from the URL }; } @@ -18,31 +16,19 @@ interface ProjectLayoutProps { * Server layout that resolves project data and provides it to all child pages */ export default async function ProjectLayout({ children, params }: ProjectLayoutProps) { - const { projectIdentifier, identifierType } = RouteParamParsers.parseProjectParams(params); - + const projectName = params.name; try { const projectService = ProjectService.getInstance(); - - let project: Project | null = null; - - if (identifierType === 'name') { - project = await projectService.getByName(projectIdentifier); - - // If project exists but identifier doesn't match canonical slug, redirect - if (project) { - const canonicalSlug = generateSlugFromName(project.name); - if (projectIdentifier !== canonicalSlug) { - // Redirect to canonical URL - const currentPath = `/projects/${projectIdentifier}`; - const newPath = `/projects/${canonicalSlug}`; - redirect(newPath); - } - } - } else { - // For ID-based routing (fallback/legacy support) - const projectId = parseInt(projectIdentifier, 10); - if (!isNaN(projectId)) { - project = await projectService.get(projectId); + + const project = await projectService.getByName(projectName); + + // If project exists but identifier doesn't match canonical slug, redirect + if (project) { + const canonicalSlug = generateSlugFromName(project.name); + if (projectName !== canonicalSlug) { + // Redirect to canonical URL + const newPath = `/projects/${canonicalSlug}`; + redirect(newPath); } } @@ -50,13 +36,9 @@ export default async function ProjectLayout({ children, params }: ProjectLayoutP return ; } - return ( - - {children} - - ); + return {children}; } catch (error) { console.error('Error resolving project:', error); return ; } -} \ No newline at end of file +} From 1acdd0457fd4198503f76d9a7184d28527fa3f7d Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 22 Aug 2025 13:43:14 +0800 Subject: [PATCH 17/50] Refactor test suite: Remove unit tests, add integration tests, and implement project API client tests - Removed unit tests from `api.test.ts` to streamline the testing approach. - Introduced a new integration test suite in `api-integration.test.ts` for end-to-end testing of the Devlog Web API. - Added comprehensive tests for project operations, devlog operations, and error handling in the integration test suite. - Created a new test file `project-api-client.test.ts` to test the `ProjectApiClient` with mocked API responses. - Updated README documentation to reflect changes in the testing architecture and removed outdated mock configurations. --- packages/web/app/lib/api/index.ts | 1 + .../web/app/lib/api/project-api-client.ts | 310 +++++++++ packages/web/tests/README.md | 40 +- packages/web/tests/api.test.ts | 632 ------------------ .../{ => lib/api}/api-integration.test.ts | 2 +- .../tests/lib/api/project-api-client.test.ts | 299 +++++++++ 6 files changed, 614 insertions(+), 670 deletions(-) create mode 100644 packages/web/app/lib/api/project-api-client.ts delete mode 100644 packages/web/tests/api.test.ts rename packages/web/tests/{ => lib/api}/api-integration.test.ts (99%) create mode 100644 packages/web/tests/lib/api/project-api-client.test.ts diff --git a/packages/web/app/lib/api/index.ts b/packages/web/app/lib/api/index.ts index 9d18e536..b3b5f6b7 100644 --- a/packages/web/app/lib/api/index.ts +++ b/packages/web/app/lib/api/index.ts @@ -5,3 +5,4 @@ export * from './api-client'; export * from './devlog-api-client'; +export * from './project-api-client'; diff --git a/packages/web/app/lib/api/project-api-client.ts b/packages/web/app/lib/api/project-api-client.ts new file mode 100644 index 00000000..b507c538 --- /dev/null +++ b/packages/web/app/lib/api/project-api-client.ts @@ -0,0 +1,310 @@ +/** + * Project API client for handling project-related HTTP requests + * + * This client provides a higher-level interface for project operations, + * building on top of the base ApiClient for standardized error handling + * and response format processing. + */ + +import { ApiClient, ApiError } from './api-client.js'; +import type { Project } from '@codervisor/devlog-core'; + +/** + * Project creation request data + */ +export interface CreateProjectRequest { + name: string; + description?: string; +} + +/** + * Project update request data + */ +export interface UpdateProjectRequest { + name?: string; + description?: string; +} + +/** + * Project deletion response + */ +export interface DeleteProjectResponse { + deleted: boolean; + projectId: number; +} + +/** + * Client for project-related API operations + * + * Provides typed methods for all project CRUD operations while leveraging + * the base ApiClient for consistent error handling and response processing. + * + * @example + * ```typescript + * const projectClient = new ProjectApiClient(); + * + * // List all projects + * const projects = await projectClient.list(); + * + * // Get a specific project + * const project = await projectClient.get('my-project'); + * + * // Create a new project + * const newProject = await projectClient.create({ + * name: 'New Project', + * description: 'A new project for testing' + * }); + * ``` + */ +export class ProjectApiClient { + private apiClient: ApiClient; + + /** + * Create a new ProjectApiClient instance + * + * @param baseUrl - Optional base URL for API requests (defaults to current origin) + */ + constructor(baseUrl?: string) { + this.apiClient = new ApiClient({ baseUrl: baseUrl || '' }); + } + + /** + * List all projects + * + * @returns Promise resolving to array of projects + * @throws {ApiError} When the request fails or server returns an error + */ + async list(): Promise { + try { + return await this.apiClient.get('/api/projects'); + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + throw new ApiError( + 'PROJECT_LIST_FAILED', + 'Failed to fetch projects list', + 500, + { originalError: error } + ); + } + } + + /** + * Get a specific project by name + * + * @param projectName - The name of the project to retrieve + * @returns Promise resolving to the project data + * @throws {ApiError} When the project is not found or request fails + */ + async get(projectName: string): Promise { + if (!projectName || typeof projectName !== 'string') { + throw new ApiError( + 'INVALID_PROJECT_NAME', + 'Project name must be a non-empty string', + 400 + ); + } + + try { + return await this.apiClient.get(`/api/projects/${encodeURIComponent(projectName)}`); + } catch (error) { + if (error instanceof ApiError) { + // Re-throw API errors with additional context + if (error.isNotFound()) { + throw new ApiError( + 'PROJECT_NOT_FOUND', + `Project '${projectName}' not found`, + 404, + { projectName } + ); + } + throw error; + } + throw new ApiError( + 'PROJECT_GET_FAILED', + `Failed to fetch project '${projectName}'`, + 500, + { projectName, originalError: error } + ); + } + } + + /** + * Create a new project + * + * @param projectData - The project data for creation + * @returns Promise resolving to the created project + * @throws {ApiError} When validation fails or creation fails + */ + async create(projectData: CreateProjectRequest): Promise { + if (!projectData || !projectData.name) { + throw new ApiError( + 'INVALID_PROJECT_DATA', + 'Project name is required', + 400, + { providedData: projectData } + ); + } + + try { + return await this.apiClient.post('/api/projects', projectData); + } catch (error) { + if (error instanceof ApiError) { + // Enhance validation errors with more context + if (error.isValidation()) { + throw new ApiError( + 'PROJECT_VALIDATION_FAILED', + error.message, + 422, + { ...error.details, projectData } + ); + } + throw error; + } + throw new ApiError( + 'PROJECT_CREATE_FAILED', + 'Failed to create project', + 500, + { projectData, originalError: error } + ); + } + } + + /** + * Update an existing project + * + * @param projectName - The name of the project to update + * @param updates - The updates to apply to the project + * @returns Promise resolving to the updated project + * @throws {ApiError} When the project is not found or update fails + */ + async update(projectName: string, updates: UpdateProjectRequest): Promise { + if (!projectName || typeof projectName !== 'string') { + throw new ApiError( + 'INVALID_PROJECT_NAME', + 'Project name must be a non-empty string', + 400 + ); + } + + if (!updates || Object.keys(updates).length === 0) { + throw new ApiError( + 'INVALID_UPDATE_DATA', + 'At least one field must be provided for update', + 400, + { providedData: updates } + ); + } + + try { + return await this.apiClient.put( + `/api/projects/${encodeURIComponent(projectName)}`, + updates + ); + } catch (error) { + if (error instanceof ApiError) { + if (error.isNotFound()) { + throw new ApiError( + 'PROJECT_NOT_FOUND', + `Project '${projectName}' not found`, + 404, + { projectName } + ); + } + if (error.isValidation()) { + throw new ApiError( + 'PROJECT_VALIDATION_FAILED', + error.message, + 422, + { ...error.details, projectName, updates } + ); + } + throw error; + } + throw new ApiError( + 'PROJECT_UPDATE_FAILED', + `Failed to update project '${projectName}'`, + 500, + { projectName, updates, originalError: error } + ); + } + } + + /** + * Delete a project + * + * @param projectName - The name of the project to delete + * @returns Promise resolving to deletion confirmation + * @throws {ApiError} When the project is not found or deletion fails + */ + async delete(projectName: string): Promise { + if (!projectName || typeof projectName !== 'string') { + throw new ApiError( + 'INVALID_PROJECT_NAME', + 'Project name must be a non-empty string', + 400 + ); + } + + try { + return await this.apiClient.delete( + `/api/projects/${encodeURIComponent(projectName)}` + ); + } catch (error) { + if (error instanceof ApiError) { + if (error.isNotFound()) { + throw new ApiError( + 'PROJECT_NOT_FOUND', + `Project '${projectName}' not found`, + 404, + { projectName } + ); + } + throw error; + } + throw new ApiError( + 'PROJECT_DELETE_FAILED', + `Failed to delete project '${projectName}'`, + 500, + { projectName, originalError: error } + ); + } + } + + /** + * Check if a project exists + * + * @param projectName - The name of the project to check + * @returns Promise resolving to true if project exists, false otherwise + */ + async exists(projectName: string): Promise { + try { + await this.get(projectName); + return true; + } catch (error) { + if (error instanceof ApiError && error.isNotFound()) { + return false; + } + // Re-throw non-404 errors + throw error; + } + } +} + +/** + * Default project API client instance + * + * Pre-configured client ready to use throughout the application + */ +export const projectApiClient = new ProjectApiClient(); + +/** + * Type guard to check if an error is related to project operations + */ +export function isProjectApiError(error: unknown): error is ApiError { + return error instanceof ApiError && ( + error.code.startsWith('PROJECT_') || + error.code.includes('PROJECT') + ); +} \ No newline at end of file diff --git a/packages/web/tests/README.md b/packages/web/tests/README.md index bd7f87de..13118e69 100644 --- a/packages/web/tests/README.md +++ b/packages/web/tests/README.md @@ -6,13 +6,6 @@ This document describes the comprehensive test suite for the Devlog Web API, des ## Test Architecture -### 🔬 **Unit Tests** (`tests/api.test.ts`) - -- **Isolated testing** using mocks and no external dependencies -- **Fast execution** - runs in milliseconds -- **Safe for CI/CD** - no database or network dependencies -- **Always run** during development and deployment - ### 🚀 **Integration Tests** (`tests/api-integration.test.ts`) - **End-to-end testing** against actual API endpoints @@ -153,26 +146,6 @@ DATABASE_URL=sqlite::memory: # In-memory database for unit tests NODE_ENV=test # Test environment marker ``` -### Mock Configuration - -```typescript -// Service mocks -const mockProjectService = { - getInstance: vi.fn(), - get: vi.fn(), - update: vi.fn(), - delete: vi.fn(), -}; - -const mockDevlogService = { - getInstance: vi.fn(), - get: vi.fn(), - save: vi.fn(), - delete: vi.fn(), - // ... other methods -}; -``` - ## Test Safety Features ### 🛡️ **Production Protection** @@ -200,10 +173,9 @@ const mockDevlogService = { ### Adding New Tests -1. **Unit tests**: Add to `tests/api.test.ts` with proper mocking -2. **Integration tests**: Add to `tests/api-integration.test.ts` with safety guards -3. **New utilities**: Mock in test setup and add comprehensive unit tests -4. **New endpoints**: Follow existing patterns for parameter validation +1. **Integration tests**: Add to `tests/api-integration.test.ts` with safety guards +2. **New utilities**: Mock in test setup and add comprehensive unit tests +3. **New endpoints**: Follow existing patterns for parameter validation ### Test Data Management @@ -267,9 +239,3 @@ DEBUG=* pnpm --filter @codervisor/devlog-web test - ✅ All service integration patterns verified - ✅ Response format consistency validated - ✅ No production data dependencies - ---- - -**Status**: ✅ **COMPLETE - Comprehensive test suite implemented with safety isolation** - -The test suite provides robust coverage of the API route overhaul while maintaining complete isolation from production systems through extensive mocking and environment-specific configurations. diff --git a/packages/web/tests/api.test.ts b/packages/web/tests/api.test.ts deleted file mode 100644 index ee3fca2e..00000000 --- a/packages/web/tests/api.test.ts +++ /dev/null @@ -1,632 +0,0 @@ -/** - * API Test Suite for Devlog Web API - * - * Isolated unit tests using mocks to avoid hitting actual database or services. - * Tests parameter validation, error handling, and service integration patterns. - */ - -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { NextRequest, NextResponse } from 'next/server'; -import { RouteParams, ServiceHelper, ApiErrors, ApiResponses, withErrorHandling } from '@/lib/api/api-utils'; - -// Mock the devlog-core services -const mockProjectService = { - getInstance: vi.fn(), - get: vi.fn(), - update: vi.fn(), - delete: vi.fn(), -}; - -const mockDevlogService = { - getInstance: vi.fn(), - get: vi.fn(), - save: vi.fn(), - delete: vi.fn(), - list: vi.fn(), - search: vi.fn(), - getStats: vi.fn(), - getTimeSeriesStats: vi.fn(), - getNextId: vi.fn(), -}; - -// Mock the core module -vi.mock('@codervisor/devlog-core', () => ({ - ProjectService: mockProjectService, - DevlogService: mockDevlogService, -})); - -describe('API Utilities Test Suite', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('RouteParams', () => { - describe('parseProjectId', () => { - it('should parse valid project name', () => { - const params = { id: 'my-project' }; - const result = RouteParams.parseProjectId(params); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.identifier).toBe('my-project'); - expect(result.data.identifierType).toBe('name'); - } - }); - - it('should accept project names with underscores', () => { - const params = { id: 'my_project_name' }; - const result = RouteParams.parseProjectId(params); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.identifier).toBe('my_project_name'); - expect(result.data.identifierType).toBe('name'); - } - }); - - it('should accept mixed case project names', () => { - const params = { id: 'MyProject' }; - const result = RouteParams.parseProjectId(params); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.identifier).toBe('MyProject'); - expect(result.data.identifierType).toBe('name'); - } - }); - - it('should reject project names starting with hyphen', () => { - const params = { id: '-invalid' }; - const result = RouteParams.parseProjectId(params); - - expect(result.success).toBe(false); - }); - - it('should reject project names ending with hyphen', () => { - const params = { id: 'invalid-' }; - const result = RouteParams.parseProjectId(params); - - expect(result.success).toBe(false); - }); - - it('should reject project names with special characters', () => { - const params = { id: 'invalid@name' }; - const result = RouteParams.parseProjectId(params); - - expect(result.success).toBe(false); - }); - - it('should reject numeric IDs (name-only routing)', () => { - const params = { id: '123' }; - const result = RouteParams.parseProjectId(params); - - expect(result.success).toBe(false); - }); - }); - - describe('parseProjectAndDevlogId', () => { - it('should parse valid project name and devlog ID', () => { - const params = { id: 'my-project', devlogId: '456' }; - const result = RouteParams.parseProjectAndDevlogId(params); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.identifier).toBe('my-project'); - expect(result.data.identifierType).toBe('name'); - expect(result.data.devlogId).toBe(456); - } - }); - - it('should reject invalid project name', () => { - const params = { id: '-invalid', devlogId: '456' }; - const result = RouteParams.parseProjectAndDevlogId(params); - - expect(result.success).toBe(false); - }); - - it('should reject invalid devlog ID', () => { - const params = { id: 'my-project', devlogId: 'invalid' }; - const result = RouteParams.parseProjectAndDevlogId(params); - - expect(result.success).toBe(false); - }); - - it('should reject numeric project identifiers (name-only routing)', () => { - const params = { id: '123', devlogId: '456' }; - const result = RouteParams.parseProjectAndDevlogId(params); - - expect(result.success).toBe(false); - }); - - it('should provide descriptive error messages', async () => { - const params = { id: '-invalid', devlogId: '456' }; - const result = RouteParams.parseProjectAndDevlogId(params); - - expect(result.success).toBe(false); - if (!result.success) { - const responseJson = await result.response.json(); - expect(responseJson.error).toContain('Invalid project name'); - } - }); - }); - }); - - describe('ServiceHelper', () => { - describe('getProjectOrFail', () => { - it('should return project when it exists', async () => { - const mockProject = { id: 1, name: 'Test Project' }; - mockProjectService.getInstance.mockReturnValue(mockProjectService); - mockProjectService.get.mockResolvedValue(mockProject); - - const result = await ServiceHelper.getProjectOrFail(1); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.project).toEqual(mockProject); - expect(result.data.projectService).toBe(mockProjectService); - } - expect(mockProjectService.get).toHaveBeenCalledWith(1); - }); - - it('should return error when project does not exist', async () => { - mockProjectService.getInstance.mockReturnValue(mockProjectService); - mockProjectService.get.mockResolvedValue(null); - - const result = await ServiceHelper.getProjectOrFail(999); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.response).toBeInstanceOf(NextResponse); - } - }); - - it('should handle service initialization errors', async () => { - mockProjectService.getInstance.mockReturnValue(mockProjectService); - mockProjectService.get.mockRejectedValue(new Error('Database connection failed')); - - await expect(ServiceHelper.getProjectOrFail(1)).rejects.toThrow( - 'Database connection failed', - ); - }); - }); - - describe('getDevlogService', () => { - it('should return initialized devlog service', async () => { - mockDevlogService.getInstance.mockReturnValue(mockDevlogService); - - const result = await ServiceHelper.getDevlogService(1); - - expect(result).toBe(mockDevlogService); - expect(mockDevlogService.getInstance).toHaveBeenCalledWith(1); - }); - }); - - describe('getDevlogOrFail', () => { - it('should return devlog when it exists', async () => { - const mockDevlog = { id: 300, title: 'Test Devlog', projectId: 1 }; - mockDevlogService.getInstance.mockReturnValue(mockDevlogService); - mockDevlogService.get.mockResolvedValue(mockDevlog); - - const result = await ServiceHelper.getDevlogOrFail(1, 300); - - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.entry).toEqual(mockDevlog); - expect(result.data.devlogService).toBe(mockDevlogService); - } - }); - - it('should return error when devlog does not exist', async () => { - mockDevlogService.getInstance.mockReturnValue(mockDevlogService); - mockDevlogService.get.mockResolvedValue(null); - - const result = await ServiceHelper.getDevlogOrFail(1, 999); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.response).toBeInstanceOf(NextResponse); - } - }); - }); - }); - - describe('ApiErrors', () => { - it('should create consistent error responses', async () => { - const errors = [ - ApiErrors.projectNotFound(), - ApiErrors.devlogNotFound(), - ApiErrors.invalidRequest('Test message'), - ApiErrors.internalError('Test error'), - ApiErrors.forbidden(), - ApiErrors.unauthorized(), - ]; - - for (const error of errors) { - expect(error).toBeInstanceOf(NextResponse); - const json = await error.json(); - expect(json).toHaveProperty('error'); - expect(typeof json.error).toBe('string'); - } - }); - - it('should have correct HTTP status codes', () => { - expect(ApiErrors.projectNotFound().status).toBe(404); - expect(ApiErrors.devlogNotFound().status).toBe(404); - expect(ApiErrors.invalidRequest('test').status).toBe(400); - expect(ApiErrors.internalError().status).toBe(500); - expect(ApiErrors.forbidden().status).toBe(403); - expect(ApiErrors.unauthorized().status).toBe(401); - }); - - it('should include custom messages', async () => { - const customMessage = 'Custom error message'; - const error = ApiErrors.invalidRequest(customMessage); - const json = await error.json(); - expect(json.error).toBe(customMessage); - }); - }); - - describe('ApiResponses', () => { - it('should create success responses', async () => { - const successResponse = ApiResponses.success(null); - expect(successResponse).toBeInstanceOf(NextResponse); - expect(successResponse.status).toBe(200); - - const json = await successResponse.json(); - expect(json).toEqual({ success: true }); - }); - - it('should create success responses with data', async () => { - const testData = { id: 1, name: 'test' }; - const response = ApiResponses.success(testData); - const json = await response.json(); - expect(json).toEqual(testData); - }); - - it('should create created responses', () => { - const testData = { id: 1, name: 'created' }; - const response = ApiResponses.created(testData); - expect(response.status).toBe(201); - }); - - it('should create no content responses', () => { - const response = ApiResponses.noContent(); - expect(response.status).toBe(204); - }); - }); - - describe('withErrorHandling', () => { - it('should pass through successful responses', async () => { - const mockHandler = vi.fn().mockResolvedValue(NextResponse.json({ success: true })); - const wrappedHandler = withErrorHandling(mockHandler); - - const result = await wrappedHandler('arg1', 'arg2'); - - expect(mockHandler).toHaveBeenCalledWith('arg1', 'arg2'); - expect(result).toBeInstanceOf(NextResponse); - const json = await result.json(); - expect(json).toEqual({ success: true }); - }); - - it('should catch and handle thrown errors', async () => { - const mockHandler = vi.fn().mockRejectedValue(new Error('Test error')); - const wrappedHandler = withErrorHandling(mockHandler); - - const result = await wrappedHandler(); - - expect(result).toBeInstanceOf(NextResponse); - expect(result.status).toBe(500); - const json = await result.json(); - expect(json.error).toBe('Test error'); - }); - - it('should handle "not found" errors specifically', async () => { - const mockHandler = vi.fn().mockRejectedValue(new Error('Resource not found')); - const wrappedHandler = withErrorHandling(mockHandler); - - const result = await wrappedHandler(); - - expect(result.status).toBe(404); - const json = await result.json(); - expect(json.error).toBe('Resource not found'); - }); - - it('should handle "Invalid" errors as bad requests', async () => { - const mockHandler = vi.fn().mockRejectedValue(new Error('Invalid input provided')); - const wrappedHandler = withErrorHandling(mockHandler); - - const result = await wrappedHandler(); - - expect(result.status).toBe(400); - const json = await result.json(); - expect(json.error).toBe('Invalid input provided'); - }); - - it('should handle non-Error objects', async () => { - const mockHandler = vi.fn().mockRejectedValue('String error'); - const wrappedHandler = withErrorHandling(mockHandler); - - const result = await wrappedHandler(); - - expect(result.status).toBe(500); - const json = await result.json(); - expect(json.error).toBe('Internal server error'); - }); - }); -}); - -describe('Route Handler Integration Tests', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('Project Route Patterns', () => { - it('should handle valid project GET request', async () => { - const mockProject = { id: 1, name: 'Test Project', createdAt: '2025-07-29T00:00:00Z' }; - mockProjectService.getInstance.mockReturnValue(mockProjectService); - mockProjectService.get.mockResolvedValue(mockProject); - - // Simulate route handler pattern - const handler = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { - const paramResult = RouteParams.parseProjectId(params); - if (!paramResult.success) return paramResult.response; - - const projectResult = await ServiceHelper.getProjectOrFail(paramResult.data.projectId); - if (!projectResult.success) return projectResult.response; - - return NextResponse.json(projectResult.data.project); - }, - ); - - const mockRequest = new NextRequest('http://localhost/api/projects/1'); - const result = await handler(mockRequest, { params: { id: '1' } }); - - expect(result.status).toBe(200); - const json = await result.json(); - expect(json).toEqual(mockProject); - }); - - it('should handle invalid project ID in GET request', async () => { - const handler = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { - const paramResult = RouteParams.parseProjectId(params); - if (!paramResult.success) return paramResult.response; - - return NextResponse.json({ success: true }); - }, - ); - - const mockRequest = new NextRequest('http://localhost/api/projects/invalid'); - const result = await handler(mockRequest, { params: { id: 'invalid' } }); - - expect(result.status).toBe(400); - const json = await result.json(); - expect(json.error).toContain('Invalid project name'); - }); - - it('should handle nonexistent project', async () => { - mockProjectService.getInstance.mockReturnValue(mockProjectService); - mockProjectService.get.mockResolvedValue(null); - - const handler = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { - const paramResult = RouteParams.parseProjectId(params); - if (!paramResult.success) return paramResult.response; - - const projectResult = await ServiceHelper.getProjectOrFail(paramResult.data.projectId); - if (!projectResult.success) return projectResult.response; - - return NextResponse.json(projectResult.data.project); - }, - ); - - const mockRequest = new NextRequest('http://localhost/api/projects/999'); - const result = await handler(mockRequest, { params: { id: '999' } }); - - expect(result.status).toBe(404); - const json = await result.json(); - expect(json.error).toBe('Project not found'); - }); - }); - - describe('Devlog Route Patterns', () => { - it('should handle valid devlog GET request', async () => { - const mockProject = { id: 1, name: 'Test Project' }; - const mockDevlog = { id: 300, title: 'Test Devlog', projectId: 1 }; - - mockProjectService.getInstance.mockReturnValue(mockProjectService); - mockProjectService.get.mockResolvedValue(mockProject); - mockDevlogService.getInstance.mockReturnValue(mockDevlogService); - mockDevlogService.get.mockResolvedValue(mockDevlog); - - const handler = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string; devlogId: string } }) => { - const paramResult = RouteParams.parseProjectAndDevlogId(params); - if (!paramResult.success) return paramResult.response; - - const projectResult = await ServiceHelper.getProjectOrFail(paramResult.data.projectId); - if (!projectResult.success) return projectResult.response; - - const devlogResult = await ServiceHelper.getDevlogOrFail( - paramResult.data.projectId, - paramResult.data.devlogId, - ); - if (!devlogResult.success) return devlogResult.response; - - return NextResponse.json(devlogResult.data.entry); - }, - ); - - const mockRequest = new NextRequest('http://localhost/api/projects/1/devlogs/300'); - const result = await handler(mockRequest, { params: { id: '1', devlogId: '300' } }); - - expect(result.status).toBe(200); - const json = await result.json(); - expect(json).toEqual(mockDevlog); - }); - - it('should handle invalid devlog ID', async () => { - const handler = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string; devlogId: string } }) => { - const paramResult = RouteParams.parseProjectAndDevlogId(params); - if (!paramResult.success) return paramResult.response; - - return NextResponse.json({ success: true }); - }, - ); - - const mockRequest = new NextRequest('http://localhost/api/projects/1/devlogs/invalid'); - const result = await handler(mockRequest, { params: { id: '1', devlogId: 'invalid' } }); - - expect(result.status).toBe(400); - const json = await result.json(); - expect(json.error).toContain('Invalid devlog ID: must be a positive integer'); - }); - }); - - describe('Batch Operation Patterns', () => { - it('should handle valid batch update request', async () => { - const mockProject = { id: 1, name: 'Test Project' }; - const mockDevlog1 = { id: 100, title: 'Devlog 1', projectId: 1, priority: 'low' }; - const mockDevlog2 = { id: 101, title: 'Devlog 2', projectId: 1, priority: 'low' }; - - mockProjectService.getInstance.mockReturnValue(mockProjectService); - mockProjectService.get.mockResolvedValue(mockProject); - mockDevlogService.getInstance.mockReturnValue(mockDevlogService); - mockDevlogService.get.mockResolvedValueOnce(mockDevlog1).mockResolvedValueOnce(mockDevlog2); - mockDevlogService.save.mockResolvedValue(undefined); - - const handler = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { - const paramResult = RouteParams.parseProjectId(params); - if (!paramResult.success) return paramResult.response; - - const projectResult = await ServiceHelper.getProjectOrFail(paramResult.data.projectId); - if (!projectResult.success) return projectResult.response; - - const { ids, updates } = await request.json(); - - if (!Array.isArray(ids) || !updates) { - return ApiErrors.invalidRequest('ids (array) and updates (object) are required'); - } - - const devlogService = await ServiceHelper.getDevlogService(paramResult.data.projectId); - const updatedEntries = []; - - for (const id of ids) { - const devlogId = parseInt(id); - if (isNaN(devlogId)) continue; - - const existingEntry = await devlogService.get(devlogId); - if (!existingEntry) continue; - - const updatedEntry = { - ...existingEntry, - ...updates, - updatedAt: new Date().toISOString(), - }; - await devlogService.save(updatedEntry); - updatedEntries.push(updatedEntry); - } - - return NextResponse.json({ success: true, updated: updatedEntries }); - }, - ); - - const requestBody = JSON.stringify({ ids: [100, 101], updates: { priority: 'high' } }); - const mockRequest = new NextRequest('http://localhost/api/projects/1/devlogs/batch/update', { - method: 'POST', - body: requestBody, - headers: { 'Content-Type': 'application/json' }, - }); - - const result = await handler(mockRequest, { params: { id: '1' } }); - - expect(result.status).toBe(200); - const json = await result.json(); - expect(json.success).toBe(true); - expect(Array.isArray(json.updated)).toBe(true); - expect(json.updated).toHaveLength(2); - }); - - it('should handle invalid batch request body', async () => { - const mockProject = { id: 1, name: 'Test Project' }; - mockProjectService.getInstance.mockReturnValue(mockProjectService); - mockProjectService.get.mockResolvedValue(mockProject); - - const handler = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { - const paramResult = RouteParams.parseProjectId(params); - if (!paramResult.success) return paramResult.response; - - const projectResult = await ServiceHelper.getProjectOrFail(paramResult.data.projectId); - if (!projectResult.success) return projectResult.response; - - const { ids, updates } = await request.json(); - - if (!Array.isArray(ids) || !updates) { - return ApiErrors.invalidRequest('ids (array) and updates (object) are required'); - } - - return NextResponse.json({ success: true }); - }, - ); - - const requestBody = JSON.stringify({ ids: 'not-an-array', updates: { priority: 'high' } }); - const mockRequest = new NextRequest('http://localhost/api/projects/1/devlogs/batch/update', { - method: 'POST', - body: requestBody, - headers: { 'Content-Type': 'application/json' }, - }); - - const result = await handler(mockRequest, { params: { id: '1' } }); - - expect(result.status).toBe(400); - const json = await result.json(); - expect(json.error).toBe('ids (array) and updates (object) are required'); - }); - }); - - describe('Error Boundary Integration', () => { - it('should handle service layer exceptions', async () => { - mockProjectService.getInstance.mockReturnValue(mockProjectService); - mockProjectService.get.mockRejectedValue(new Error('Database connection failed')); - - const handler = withErrorHandling( - async (request: NextRequest, { params }: { params: { id: string } }) => { - const paramResult = RouteParams.parseProjectId(params); - if (!paramResult.success) return paramResult.response; - - const projectResult = await ServiceHelper.getProjectOrFail(paramResult.data.projectId); - if (!projectResult.success) return projectResult.response; - - return NextResponse.json(projectResult.data.project); - }, - ); - - const mockRequest = new NextRequest('http://localhost/api/projects/1'); - const result = await handler(mockRequest, { params: { id: '1' } }); - - expect(result.status).toBe(500); - const json = await result.json(); - expect(json.error).toBe('Database connection failed'); - }); - - it('should handle malformed JSON in request body', async () => { - const handler = withErrorHandling(async (request: NextRequest) => { - await request.json(); // This will throw for malformed JSON - return NextResponse.json({ success: true }); - }); - - const mockRequest = new NextRequest('http://localhost/api/test', { - method: 'POST', - body: '{ invalid json', - headers: { 'Content-Type': 'application/json' }, - }); - - const result = await handler(mockRequest); - expect(result.status).toBe(500); - }); - }); -}); diff --git a/packages/web/tests/api-integration.test.ts b/packages/web/tests/lib/api/api-integration.test.ts similarity index 99% rename from packages/web/tests/api-integration.test.ts rename to packages/web/tests/lib/api/api-integration.test.ts index ba75cbcb..e09ee2fc 100644 --- a/packages/web/tests/api-integration.test.ts +++ b/packages/web/tests/lib/api/api-integration.test.ts @@ -10,7 +10,7 @@ import { createTestEnvironment, isTestServerAvailable, type TestApiClient, -} from './utils/test-server.js'; +} from '../../utils/test-server.js'; // Skip integration tests by default unless explicitly enabled or server is available const runIntegrationTests = process.env.RUN_INTEGRATION_TESTS === 'true'; diff --git a/packages/web/tests/lib/api/project-api-client.test.ts b/packages/web/tests/lib/api/project-api-client.test.ts new file mode 100644 index 00000000..9f2556af --- /dev/null +++ b/packages/web/tests/lib/api/project-api-client.test.ts @@ -0,0 +1,299 @@ +/** + * Tests for ProjectApiClient + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { Project } from '@codervisor/devlog-core'; + +// Mock the ApiClient first +const mockApiClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), +}; + +const MockApiError = class extends Error { + constructor( + public code: string, + message: string, + public status: number, + public details?: any, + ) { + super(message); + this.name = 'ApiError'; + } + + is(code: string): boolean { + return this.code === code; + } + + isNotFound(): boolean { + return this.code.endsWith('_NOT_FOUND') || this.status === 404; + } + + isValidation(): boolean { + return this.code === 'VALIDATION_FAILED' || this.status === 422; + } + + isClientError(): boolean { + return this.status >= 400 && this.status < 500; + } + + isServerError(): boolean { + return this.status >= 500; + } +}; + +vi.mock('../../../app/lib/api/api-client', () => ({ + ApiClient: vi.fn(() => mockApiClient), + ApiError: MockApiError, +})); + +// Now import the modules that depend on the mocked modules +import { ProjectApiClient, CreateProjectRequest, UpdateProjectRequest } from '../../../app/lib/api/project-api-client'; + +// Create a type alias for our mock error class for tests +const ApiError = MockApiError; + +describe('ProjectApiClient', () => { + let projectClient: ProjectApiClient; + + beforeEach(() => { + vi.clearAllMocks(); + projectClient = new ProjectApiClient(); + }); + + describe('list()', () => { + it('should fetch and return projects list', async () => { + const mockProjects: Project[] = [ + { + id: 1, + name: 'Test Project', + description: 'A test project', + createdAt: new Date('2023-01-01'), + lastAccessedAt: new Date('2023-01-02'), + }, + ]; + + mockApiClient.get.mockResolvedValue(mockProjects); + + const result = await projectClient.list(); + + expect(mockApiClient.get).toHaveBeenCalledWith('/api/projects'); + expect(result).toEqual(mockProjects); + }); + + it('should handle API errors correctly', async () => { + const apiError = new ApiError('SERVER_ERROR', 'Server error', 500); + mockApiClient.get.mockRejectedValue(apiError); + + await expect(projectClient.list()).rejects.toThrow(apiError); + }); + + it('should wrap non-API errors', async () => { + const genericError = new Error('Network error'); + mockApiClient.get.mockRejectedValue(genericError); + + await expect(projectClient.list()).rejects.toThrow( + expect.objectContaining({ + code: 'PROJECT_LIST_FAILED', + message: 'Failed to fetch projects list', + }) + ); + }); + }); + + describe('get()', () => { + it('should fetch a specific project', async () => { + const mockProject: Project = { + id: 1, + name: 'Test Project', + description: 'A test project', + createdAt: new Date('2023-01-01'), + lastAccessedAt: new Date('2023-01-02'), + }; + + mockApiClient.get.mockResolvedValue(mockProject); + + const result = await projectClient.get('test-project'); + + expect(mockApiClient.get).toHaveBeenCalledWith('/api/projects/test-project'); + expect(result).toEqual(mockProject); + }); + + it('should validate project name parameter', async () => { + await expect(projectClient.get('')).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_PROJECT_NAME', + status: 400, + }) + ); + + await expect(projectClient.get(null as any)).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_PROJECT_NAME', + status: 400, + }) + ); + }); + + it('should handle project not found', async () => { + const notFoundError = new ApiError('PROJECT_NOT_FOUND', 'Not found', 404); + mockApiClient.get.mockRejectedValue(notFoundError); + + await expect(projectClient.get('nonexistent')).rejects.toThrow( + expect.objectContaining({ + code: 'PROJECT_NOT_FOUND', + message: "Project 'nonexistent' not found", + }) + ); + }); + + it('should encode project names in URLs', async () => { + mockApiClient.get.mockResolvedValue({}); + + await projectClient.get('project with spaces'); + + expect(mockApiClient.get).toHaveBeenCalledWith('/api/projects/project%20with%20spaces'); + }); + }); + + describe('create()', () => { + it('should create a new project', async () => { + const createData: CreateProjectRequest = { + name: 'New Project', + description: 'A new project', + }; + + const mockCreatedProject: Project = { + id: 2, + name: 'New Project', + description: 'A new project', + createdAt: new Date('2023-01-01'), + lastAccessedAt: new Date('2023-01-01'), + }; + + mockApiClient.post.mockResolvedValue(mockCreatedProject); + + const result = await projectClient.create(createData); + + expect(mockApiClient.post).toHaveBeenCalledWith('/api/projects', createData); + expect(result).toEqual(mockCreatedProject); + }); + + it('should validate create data', async () => { + await expect(projectClient.create({} as any)).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_PROJECT_DATA', + status: 400, + }) + ); + + await expect(projectClient.create({ name: '' })).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_PROJECT_DATA', + status: 400, + }) + ); + }); + + it('should handle validation errors from API', async () => { + const validationError = new ApiError('VALIDATION_FAILED', 'Invalid name', 422); + mockApiClient.post.mockRejectedValue(validationError); + + await expect(projectClient.create({ name: 'Test' })).rejects.toThrow( + expect.objectContaining({ + code: 'PROJECT_VALIDATION_FAILED', + status: 422, + }) + ); + }); + }); + + describe('update()', () => { + it('should update a project', async () => { + const updateData: UpdateProjectRequest = { + description: 'Updated description', + }; + + const mockUpdatedProject: Project = { + id: 1, + name: 'Test Project', + description: 'Updated description', + createdAt: new Date('2023-01-01'), + lastAccessedAt: new Date('2023-01-02'), + }; + + mockApiClient.put.mockResolvedValue(mockUpdatedProject); + + const result = await projectClient.update('test-project', updateData); + + expect(mockApiClient.put).toHaveBeenCalledWith('/api/projects/test-project', updateData); + expect(result).toEqual(mockUpdatedProject); + }); + + it('should validate update parameters', async () => { + await expect(projectClient.update('', {})).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_PROJECT_NAME', + status: 400, + }) + ); + + await expect(projectClient.update('test', {})).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_UPDATE_DATA', + status: 400, + }) + ); + }); + }); + + describe('delete()', () => { + it('should delete a project', async () => { + const mockDeleteResponse = { deleted: true, projectId: 1 }; + mockApiClient.delete.mockResolvedValue(mockDeleteResponse); + + const result = await projectClient.delete('test-project'); + + expect(mockApiClient.delete).toHaveBeenCalledWith('/api/projects/test-project'); + expect(result).toEqual(mockDeleteResponse); + }); + + it('should validate project name', async () => { + await expect(projectClient.delete('')).rejects.toThrow( + expect.objectContaining({ + code: 'INVALID_PROJECT_NAME', + status: 400, + }) + ); + }); + }); + + describe('exists()', () => { + it('should return true when project exists', async () => { + mockApiClient.get.mockResolvedValue({}); + + const result = await projectClient.exists('test-project'); + + expect(result).toBe(true); + }); + + it('should return false when project does not exist', async () => { + const notFoundError = new ApiError('PROJECT_NOT_FOUND', 'Not found', 404); + mockApiClient.get.mockRejectedValue(notFoundError); + + const result = await projectClient.exists('nonexistent'); + + expect(result).toBe(false); + }); + + it('should re-throw non-404 errors', async () => { + const serverError = new ApiError('SERVER_ERROR', 'Server error', 500); + mockApiClient.get.mockRejectedValue(serverError); + + await expect(projectClient.exists('test-project')).rejects.toThrow(serverError); + }); + }); +}); \ No newline at end of file From 3ee496b86c59e8fbdc59bc7e9848d99881703395 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 22 Aug 2025 13:47:49 +0800 Subject: [PATCH 18/50] feat: refactor project API interactions to use ProjectApiClient for improved consistency and error handling --- packages/web/app/lib/api/project-api-client.ts | 2 +- packages/web/app/projects/ProjectListPage.tsx | 15 ++------------- packages/web/app/stores/project-store.ts | 11 ++++++----- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/web/app/lib/api/project-api-client.ts b/packages/web/app/lib/api/project-api-client.ts index b507c538..94c7b0da 100644 --- a/packages/web/app/lib/api/project-api-client.ts +++ b/packages/web/app/lib/api/project-api-client.ts @@ -6,7 +6,7 @@ * and response format processing. */ -import { ApiClient, ApiError } from './api-client.js'; +import { ApiClient, ApiError } from './api-client'; import type { Project } from '@codervisor/devlog-core'; /** diff --git a/packages/web/app/projects/ProjectListPage.tsx b/packages/web/app/projects/ProjectListPage.tsx index f5fdb4a3..ac74c41a 100644 --- a/packages/web/app/projects/ProjectListPage.tsx +++ b/packages/web/app/projects/ProjectListPage.tsx @@ -28,6 +28,7 @@ import { } from 'lucide-react'; import { toast } from 'sonner'; import { RealtimeEventType } from '@/lib'; +import { projectApiClient } from '@/lib/api'; interface ProjectFormData { name: string; @@ -67,19 +68,7 @@ export function ProjectListPage() { try { setCreating(true); - const response = await fetch('/api/projects', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formData), - }); - - if (!response.ok) { - throw new Error('Failed to create project'); - } - - const newProject = await response.json(); + const newProject = await projectApiClient.create(formData); toast.success(`Project "${newProject.name}" created successfully`); setIsModalVisible(false); diff --git a/packages/web/app/stores/project-store.ts b/packages/web/app/stores/project-store.ts index 2430a9af..6a268543 100644 --- a/packages/web/app/stores/project-store.ts +++ b/packages/web/app/stores/project-store.ts @@ -2,7 +2,8 @@ import { create } from 'zustand'; import { subscribeWithSelector } from 'zustand/middleware'; -import { apiClient, debounce, handleApiError } from '@/lib'; +import { debounce, handleApiError } from '@/lib'; +import { projectApiClient } from '@/lib/api'; import { Project } from '@codervisor/devlog-core'; import { DataContext, getDefaultDataContext } from '@/stores/base'; @@ -46,7 +47,7 @@ export const useProjectStore = create()( error: null, }, })); - const currentProject = await apiClient.get(`/api/projects/${currentProjectName}`); + const currentProject = await projectApiClient.get(currentProjectName); set((state) => ({ currentProjectContext: { ...state.currentProjectContext, @@ -99,7 +100,7 @@ export const useProjectStore = create()( error: null, }, })); - const projectsList = await apiClient.get('/api/projects'); + const projectsList = await projectApiClient.list(); set((state) => ({ projectsContext: { ...state.projectsContext, @@ -141,7 +142,7 @@ export const useProjectStore = create()( updateProject: async (name: string, updates: Partial) => { try { - const updatedProject = await apiClient.put(`/api/projects/${name}`, updates); + const updatedProject = await projectApiClient.update(name, updates); // Update current project if it's the one being updated const currentProjectName = get().currentProjectName; @@ -184,7 +185,7 @@ export const useProjectStore = create()( deleteProject: async (name: string) => { try { - await apiClient.delete(`/api/projects/${name}`); + await projectApiClient.delete(name); // Clear current project if it's the one being deleted const currentProjectName = get().currentProjectName; From 03e79f0594757acef9aabf76049132a06dae28da Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 22 Aug 2025 14:20:06 +0800 Subject: [PATCH 19/50] feat: update project name validation to follow GitHub naming conventions and improve user guidance --- packages/core/src/utils/project-name.ts | 14 +--- .../layout/NavigationBreadcrumb.tsx | 76 ++++++++++++------- packages/web/app/projects/ProjectListPage.tsx | 5 +- .../app/projects/[name]/ProjectProvider.tsx | 1 + .../[name]/devlogs/DevlogProvider.tsx | 1 + .../projects/[name]/devlogs/[id]/layout.tsx | 48 ++++++++++++ packages/web/app/schemas/project.ts | 2 +- 7 files changed, 106 insertions(+), 41 deletions(-) create mode 100644 packages/web/app/projects/[name]/devlogs/[id]/layout.tsx diff --git a/packages/core/src/utils/project-name.ts b/packages/core/src/utils/project-name.ts index 9e803a0d..c1aadc77 100644 --- a/packages/core/src/utils/project-name.ts +++ b/packages/core/src/utils/project-name.ts @@ -44,9 +44,8 @@ export function generateSlugFromName(name: string): string { } /** - * Validate a project display name (more permissive than slugs): - * - Can contain letters, numbers, spaces, hyphens, underscores, dots - * - Cannot start or end with whitespace + * Validate a project display name following GitHub repository naming rules: + * - Can only contain ASCII letters, digits, and the characters -, ., and _ * - Must not be empty * - Length between 1-100 characters */ @@ -55,13 +54,8 @@ export function validateProjectDisplayName(name: string): boolean { return false; } - // Check for leading/trailing whitespace - if (name.trim() !== name) { - return false; - } - - // Must contain only valid display characters - if (!/^[a-zA-Z0-9\s._-]+$/.test(name)) { + // Must contain only ASCII letters, digits, hyphens, dots, and underscores + if (!/^[a-zA-Z0-9._-]+$/.test(name)) { return false; } diff --git a/packages/web/app/components/layout/NavigationBreadcrumb.tsx b/packages/web/app/components/layout/NavigationBreadcrumb.tsx index 6c85fd21..56008f66 100644 --- a/packages/web/app/components/layout/NavigationBreadcrumb.tsx +++ b/packages/web/app/components/layout/NavigationBreadcrumb.tsx @@ -20,14 +20,34 @@ import { Skeleton } from '@/components/ui/skeleton'; import { toast } from 'sonner'; export function NavigationBreadcrumb() { - const router = useRouter(); const pathname = usePathname(); + const router = useRouter(); + + // Parse project name and devlog ID from URL instead of using context hooks + // since this component is rendered at app level, outside of the provider hierarchy + const pathSegments = pathname.split('/').filter(Boolean); + let projectName: string | null = null; + let devlogId: number | null = null; + + // Check if we're in a project path: /projects/[name] or /projects/[name]/devlogs/[id] + if (pathSegments[0] === 'projects' && pathSegments[1]) { + projectName = pathSegments[1]; + + // Check if we're in a devlog path + if (pathSegments[2] === 'devlogs' && pathSegments[3]) { + const parsedDevlogId = parseInt(pathSegments[3], 10); + if (!isNaN(parsedDevlogId)) { + devlogId = parsedDevlogId; + } + } + } + const { currentProjectContext, currentProjectName, projectsContext, fetchProjects } = useProjectStore(); - const { currentDevlogContext, currentDevlogId } = useDevlogStore(); + const { currentDevlogContext } = useDevlogStore(); - // Don't show breadcrumb on the home or project list page - if (['/', '/projects'].includes(pathname)) { + // If we are not in a project context, do not render the breadcrumb + if (!projectName) { return null; } @@ -50,12 +70,15 @@ export function NavigationBreadcrumb() { } }; - const handleDropdownOpenChange = (open: boolean) => { - if (open) { - // Load projects when dropdown is opened - fetchProjects(); - } - }; + const dropdownSkeletons = Array.from({ length: 3 }).map((_, index) => ( + + +

+ + +
+ + )); const renderProjectDropdown = () => { // Show skeleton if current project is loading @@ -68,29 +91,24 @@ export function NavigationBreadcrumb() { } return ( - + { + if (open) await fetchProjects(); + }} + >
- {currentProjectContext.data?.name} + {projectName}
{/* Show skeleton items if projects list is loading */} {projectsContext.loading - ? Array.from({ length: 3 }).map((_, index) => ( - - -
- - -
-
- )) + ? dropdownSkeletons : projectsContext.data?.map((project) => { - const isCurrentProject = currentProjectName === project.name; - + const isCurrentProject = projectName === project.name; return (
{project.name}
-
{project.description}
+
+ {project.description} +
- {isCurrentProject && ( - - )} + {isCurrentProject && }
); })} @@ -134,8 +152,8 @@ export function NavigationBreadcrumb() { return ( - {currentProjectName && {renderProjectDropdown()}} - {currentDevlogId && ( + {renderProjectDropdown()} + {devlogId && ( <> {renderDevlogDropdown()} diff --git a/packages/web/app/projects/ProjectListPage.tsx b/packages/web/app/projects/ProjectListPage.tsx index ac74c41a..ba368bdc 100644 --- a/packages/web/app/projects/ProjectListPage.tsx +++ b/packages/web/app/projects/ProjectListPage.tsx @@ -201,11 +201,14 @@ export function ProjectListPage() { setFormData({ ...formData, name: e.target.value })} required /> +

+ Can only contain ASCII letters, digits, and the characters -, ., and _ +

diff --git a/packages/web/app/projects/[name]/ProjectProvider.tsx b/packages/web/app/projects/[name]/ProjectProvider.tsx index e6c3672e..235c8611 100644 --- a/packages/web/app/projects/[name]/ProjectProvider.tsx +++ b/packages/web/app/projects/[name]/ProjectProvider.tsx @@ -27,6 +27,7 @@ export function ProjectProvider({ export function useProject(): ProjectContextValue { const context = useContext(ProjectContext); + console.debug('useProject', 'context', context); if (!context) { throw new Error('useProject must be used within a ProjectProvider'); } diff --git a/packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx b/packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx index 21a2c041..38d68af1 100644 --- a/packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx +++ b/packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx @@ -27,6 +27,7 @@ export function DevlogProvider({ export function useDevlog(): DevlogContextValue { const context = useContext(DevlogContext); + console.debug('useDevlog', 'context', context); if (!context) { throw new Error('useDevlog must be used within a DevlogProvider'); } diff --git a/packages/web/app/projects/[name]/devlogs/[id]/layout.tsx b/packages/web/app/projects/[name]/devlogs/[id]/layout.tsx new file mode 100644 index 00000000..68b1893a --- /dev/null +++ b/packages/web/app/projects/[name]/devlogs/[id]/layout.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { DevlogService, ProjectService } from '@codervisor/devlog-core/server'; +import { notFound } from 'next/navigation'; +import { DevlogProvider } from '../DevlogProvider'; + +interface DevlogLayoutProps { + children: React.ReactNode; + params: { + name: string; // The project name from the URL + id: string; // The devlog ID from the URL + }; +} + +/** + * Server layout that resolves devlog data and provides it to all child pages + */ +export default async function DevlogLayout({ children, params }: DevlogLayoutProps) { + const projectName = params.name; + const devlogId = parseInt(params.id, 10); + + // Validate devlog ID + if (isNaN(devlogId) || devlogId <= 0) { + notFound(); + } + + try { + // Get project to ensure it exists and get project ID + const projectService = ProjectService.getInstance(); + const project = await projectService.getByName(projectName); + + if (!project) { + notFound(); + } + + // Get devlog service and fetch the devlog + const devlogService = DevlogService.getInstance(project.id); + const devlog = await devlogService.get(devlogId); + + if (!devlog) { + notFound(); + } + + return {children}; + } catch (error) { + console.error('Error resolving devlog:', error); + notFound(); + } +} \ No newline at end of file diff --git a/packages/web/app/schemas/project.ts b/packages/web/app/schemas/project.ts index 426aec24..c4573bdf 100644 --- a/packages/web/app/schemas/project.ts +++ b/packages/web/app/schemas/project.ts @@ -27,7 +27,7 @@ export const CreateProjectBodySchema = z.object({ name: z .string() .min(1, 'Project name is required') - .refine(validateProjectDisplayName, 'Project name can contain letters, numbers, spaces, hyphens, underscores, and dots. Cannot start or end with whitespace.'), + .refine(validateProjectDisplayName, 'The repository name can only contain ASCII letters, digits, and the characters -, ., and _.'), description: z.string().optional(), repositoryUrl: z.string().optional(), settings: z From 4aaa80553764f545f0a99930768c7e8fb903f0d0 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 22 Aug 2025 14:41:01 +0800 Subject: [PATCH 20/50] feat: consolidate docker-compose configuration and update development instructions --- .github/copilot-instructions.md | 2 +- .github/instructions/all.instructions.md | 2 +- GEMINI.md | 2 +- docker-compose.dev.yml | 20 --- docker-compose.yml | 17 ++ packages/cli/src/cli/dev.ts | 2 +- .../layout/NavigationBreadcrumb.tsx | 127 ++++++++++++++- packages/web/app/components/ui/command.tsx | 153 ++++++++++++++++++ packages/web/app/components/ui/dialog.tsx | 2 +- packages/web/app/stores/devlog-store.ts | 66 +++++++- packages/web/package.json | 1 + packages/web/tests/README.md | 2 +- pnpm-lock.yaml | 21 +++ 13 files changed, 382 insertions(+), 35 deletions(-) delete mode 100644 docker-compose.dev.yml create mode 100644 packages/web/app/components/ui/command.tsx diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d596ad1a..b273ac36 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -20,7 +20,7 @@ - **Temp files**: Use `tmp/` folder for experiments (gitignored) - **Build packages**: Use `pnpm build` (builds all packages) -- **Containers**: `docker compose -f docker-compose.dev.yml up web-dev -d --wait` +- **Containers**: `docker compose up web-dev -d --wait` - **Validating**: Use `pnpm validate` - **Testing**: Use `pnpm test` diff --git a/.github/instructions/all.instructions.md b/.github/instructions/all.instructions.md index 1f603ae1..62b85c38 100644 --- a/.github/instructions/all.instructions.md +++ b/.github/instructions/all.instructions.md @@ -273,7 +273,7 @@ pnpm --filter @codervisor/devlog-mcp build pnpm --filter @codervisor/devlog-web build # Start containerized development -docker compose -f docker-compose.dev.yml up web-dev -d --wait +docker compose up web-dev -d --wait # Test build without breaking dev server pnpm build diff --git a/GEMINI.md b/GEMINI.md index 618ef186..58eb1b54 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -297,7 +297,7 @@ pnpm --filter @codervisor/devlog-web build ### Development Environment ```bash # Start containerized development -docker compose -f docker-compose.dev.yml up web-dev -d --wait +docker compose up web-dev -d --wait # Test build without breaking dev server pnpm build diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 81b3788f..00000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Development-specific services and overrides -# Use: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up - -services: - # Development web service - web-dev: - build: - context: . - dockerfile: Dockerfile.dev - container_name: devlog-web-dev - command: pnpm dev:web - ports: - - "3200:3000" - volumes: - - .:/app - - /app/node_modules - - /app/packages/web/.next - - './scripts:/app/scripts' - env_file: - - .env diff --git a/docker-compose.yml b/docker-compose.yml index 441b97c5..a4eae890 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,6 +43,23 @@ services: networks: - devlog-network + # Development web service + web-dev: + build: + context: . + dockerfile: Dockerfile.dev + container_name: devlog-web-dev + command: pnpm dev:web + ports: + - "3200:3000" + volumes: + - .:/app + - /app/node_modules + - /app/packages/web/.next + - './scripts:/app/scripts' + env_file: + - .env + networks: devlog-network: driver: bridge diff --git a/packages/cli/src/cli/dev.ts b/packages/cli/src/cli/dev.ts index 3a095d65..3ed68a21 100644 --- a/packages/cli/src/cli/dev.ts +++ b/packages/cli/src/cli/dev.ts @@ -10,7 +10,7 @@ import chalk from 'chalk'; import { exec } from 'child_process'; import ora from 'ora'; -const DEV_COMPOSE_FILE = 'docker-compose.dev.yml'; +const DEV_COMPOSE_FILE = 'docker-compose.yml'; // Helper function to run shell commands const runCommand = (command: string): Promise => { diff --git a/packages/web/app/components/layout/NavigationBreadcrumb.tsx b/packages/web/app/components/layout/NavigationBreadcrumb.tsx index 56008f66..758b5a0a 100644 --- a/packages/web/app/components/layout/NavigationBreadcrumb.tsx +++ b/packages/web/app/components/layout/NavigationBreadcrumb.tsx @@ -1,9 +1,9 @@ 'use client'; -import React from 'react'; +import React, { useState, useMemo } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import { useDevlogStore, useProjectStore } from '@/stores'; -import { Check, ChevronsUpDown, NotepadText, Package } from 'lucide-react'; +import { Check, ChevronsUpDown, NotepadText, Package, Search } from 'lucide-react'; import { Breadcrumb, BreadcrumbItem, @@ -16,12 +16,16 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; import { Skeleton } from '@/components/ui/skeleton'; import { toast } from 'sonner'; export function NavigationBreadcrumb() { const pathname = usePathname(); const router = useRouter(); + + // State for devlog search + const [devlogSearchText, setDevlogSearchText] = useState(''); // Parse project name and devlog ID from URL instead of using context hooks // since this component is rendered at app level, outside of the provider hierarchy @@ -44,7 +48,28 @@ export function NavigationBreadcrumb() { const { currentProjectContext, currentProjectName, projectsContext, fetchProjects } = useProjectStore(); - const { currentDevlogContext } = useDevlogStore(); + const { currentDevlogContext, navigationDevlogsContext, fetchNavigationDevlogs } = useDevlogStore(); + + // Filter devlogs for search + const filteredDevlogs = useMemo(() => { + const devlogs = navigationDevlogsContext.data || []; + const sorted = devlogs.sort((a, b) => { + // Sort by ID descending (most recent first) + return (b.id ?? 0) - (a.id ?? 0); + }); + + // Apply search filter + if (!devlogSearchText.trim()) { + return sorted; + } + + return sorted.filter(devlog => + devlog.title?.toLowerCase().includes(devlogSearchText.toLowerCase()) || + devlog.id?.toString().includes(devlogSearchText) || + devlog.status?.toLowerCase().includes(devlogSearchText.toLowerCase()) || + devlog.type?.toLowerCase().includes(devlogSearchText.toLowerCase()) + ); + }, [navigationDevlogsContext.data, devlogSearchText]); // If we are not in a project context, do not render the breadcrumb if (!projectName) { @@ -70,6 +95,20 @@ export function NavigationBreadcrumb() { } }; + const switchDevlog = async (devlogId: number) => { + if (!projectName) return; + + try { + toast.success(`Switched to devlog: #${devlogId}`); + + // Navigate to the devlog detail page + router.push(`/projects/${projectName}/devlogs/${devlogId}`); + } catch (error) { + console.error('Error switching devlog:', error); + toast.error('Failed to switch devlog'); + } + }; + const dropdownSkeletons = Array.from({ length: 3 }).map((_, index) => ( @@ -141,11 +180,83 @@ export function NavigationBreadcrumb() { } return ( -
- - {currentDevlogContext.data?.id} - -
+ { + if (open) { + // Fetch devlogs when opening the dropdown + await fetchNavigationDevlogs(); + // Reset search when opening + setDevlogSearchText(''); + } + }} + > + +
+ + #{currentDevlogContext.data?.id || devlogId} + +
+
+ + {/* Search Input */} +
+
+ + setDevlogSearchText(e.target.value)} + className="pl-8" + /> +
+
+ + {/* Loading State */} + {navigationDevlogsContext.loading ? ( + <> + {Array.from({ length: 5 }).map((_, index) => ( + + + + + ))} + + ) : ( + <> + {/* No Results */} + {filteredDevlogs.length === 0 && ( + + {devlogSearchText ? 'No devlogs found' : 'No devlogs available'} + + )} + + {/* Devlog Items */} + {filteredDevlogs.map((devlog) => { + const isCurrentDevlog = devlogId === devlog.id; + return ( + !isCurrentDevlog && switchDevlog(devlog.id!)} + className="flex items-center gap-3 p-3 cursor-pointer" + > + + #{devlog.id} + +
+
{devlog.title}
+
+ {devlog.status} • {devlog.type} +
+
+ {isCurrentDevlog && } +
+ ); + })} + + )} +
+
); }; diff --git a/packages/web/app/components/ui/command.tsx b/packages/web/app/components/ui/command.tsx new file mode 100644 index 00000000..51caec9d --- /dev/null +++ b/packages/web/app/components/ui/command.tsx @@ -0,0 +1,153 @@ +"use client" + +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/packages/web/app/components/ui/dialog.tsx b/packages/web/app/components/ui/dialog.tsx index ef671238..af9e16fa 100644 --- a/packages/web/app/components/ui/dialog.tsx +++ b/packages/web/app/components/ui/dialog.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import * as DialogPrimitive from '@radix-ui/react-dialog'; import { X } from 'lucide-react'; -import { cn } from '@/lib'; +import { cn } from '@/lib/index'; const Dialog = DialogPrimitive.Root; diff --git a/packages/web/app/stores/devlog-store.ts b/packages/web/app/stores/devlog-store.ts index adda178b..67485067 100644 --- a/packages/web/app/stores/devlog-store.ts +++ b/packages/web/app/stores/devlog-store.ts @@ -33,6 +33,9 @@ interface DevlogState { // Devlogs state devlogsContext: TableDataContext; + // Navigation devlogs state (for dropdowns/navigation - separate from main list) + navigationDevlogsContext: DataContext; + // Current devlog state (for detail views) currentDevlogId: DevlogId | null; currentDevlogContext: DataContext; @@ -48,8 +51,9 @@ interface DevlogState { setCurrentDevlogId: (id: DevlogId) => void; setDevlogsFilters: (filters: DevlogFilter) => void; setDevlogsPagination: (pagination: PaginationMeta) => void; - setDevlogsSortOptions: (sortOptions: { field: string; direction: 'asc' | 'desc' }) => void; + setDevlogsSortOptions: (sortOptions: SortOptions) => void; fetchDevlogs: () => Promise; + fetchNavigationDevlogs: () => Promise; fetchStats: () => Promise; fetchTimeSeriesStats: () => Promise; fetchCurrentDevlog: () => Promise; @@ -71,6 +75,9 @@ export const useDevlogStore = create()( // Initial state devlogsContext: getDefaultTableDataContext(), + // Navigation devlogs state + navigationDevlogsContext: getDefaultDataContext(), + // Selected devlog state currentDevlogId: null, currentDevlogContext: getDefaultDataContext(), @@ -202,6 +209,59 @@ export const useDevlogStore = create()( } }, + fetchNavigationDevlogs: async () => { + const devlogApiClient = getDevlogApiClient(); + + if (!devlogApiClient) { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + loading: false, + }, + })); + return; + } + + try { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + loading: true, + error: null, + }, + })); + + // Fetch recent devlogs for navigation - limit to 50 most recent + const { items: data } = await devlogApiClient.list( + {}, // No filters + { page: 1, limit: 50 }, // Simple pagination + { sortBy: 'id', sortOrder: 'desc' } // Sort by ID descending + ); + + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + data, + error: null, + }, + })); + } catch (err) { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + error: handleApiError(err), + }, + })); + } finally { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + loading: false, + }, + })); + } + }, + fetchStats: async () => { const devlogApiClient = getDevlogApiClient(); @@ -535,6 +595,10 @@ export const useDevlogStore = create()( ...state.devlogsContext, error: null, }, + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + error: null, + }, statsContext: { ...state.statsContext, error: null, diff --git a/packages/web/package.json b/packages/web/package.json index 497fc860..e81d84dd 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -40,6 +40,7 @@ "class-variance-authority": "0.7.1", "classnames": "2.5.1", "clsx": "2.1.1", + "cmdk": "1.1.1", "highlight.js": "11.11.1", "next": "^14.0.4", "next-themes": "0.4.6", diff --git a/packages/web/tests/README.md b/packages/web/tests/README.md index 13118e69..ade0e6b1 100644 --- a/packages/web/tests/README.md +++ b/packages/web/tests/README.md @@ -35,7 +35,7 @@ pnpm --filter @codervisor/devlog-web test:ui ```bash # Start development server first -docker compose -f docker-compose.dev.yml up web-dev -d --wait +docker compose up web-dev -d --wait # Run integration tests pnpm --filter @codervisor/devlog-web test:integration diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7abea1ae..94c43144 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -319,6 +319,9 @@ importers: clsx: specifier: 2.1.1 version: 2.1.1 + cmdk: + specifier: 1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) highlight.js: specifier: 11.11.1 version: 11.11.1 @@ -1867,6 +1870,12 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -5480,6 +5489,18 @@ snapshots: clsx@2.1.1: {} + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + color-convert@2.0.1: dependencies: color-name: 1.1.4 From 2e69036c7fdee4848446b4681e690ea50a4a75fe Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 22 Aug 2025 14:58:40 +0800 Subject: [PATCH 21/50] feat: add Zustand stores for layout, project, and realtime management - Created layout-store for managing sidebar state. - Implemented project-store for handling project-related actions and state. - Added realtime-store for managing WebSocket connections and subscriptions. - Introduced base CSS styles and font definitions for consistent styling. - Developed layout and responsive styles for improved UI structure. - Updated API integration tests to reflect changes in devlog terminology. - Removed obsolete project-api-client tests to streamline test suite. - Adjusted TypeScript paths for better module resolution. --- docs/design/visual-design-system.md | 4 +- packages/core/README.md | 8 +- packages/core/src/types/chat.ts | 2 +- packages/core/src/types/core.ts | 6 +- packages/mcp/src/api/devlog-api-client.ts | 30 +- .../[devlogId]/notes/[noteId]/route.ts | 6 +- .../[name]/devlogs/[devlogId]/notes/route.ts | 6 +- .../[name]/devlogs/[devlogId]/route.ts | 6 +- .../app/api/projects/[name]/devlogs/route.ts | 10 +- .../projects/[name]/devlogs/search/route.ts | 2 +- .../[name]/devlogs/stats/overview/route.ts | 2 +- .../[name]/devlogs/stats/timeseries/route.ts | 2 +- .../web/app/components/ProjectResolver.tsx | 56 ---- .../app/components/features/devlogs/index.ts | 3 - .../layout/StickyHeadingsWrapper.tsx | 38 --- packages/web/app/components/layout/index.ts | 5 - packages/web/app/components/project/index.ts | 1 - packages/web/app/layout.tsx | 6 +- .../projects/[name]/ProjectDetailsPage.tsx | 4 +- .../[name]/devlogs/ProjectDevlogListPage.tsx | 8 +- .../devlogs/[id]/ProjectDevlogDetailsPage.tsx | 7 +- .../projects/[name]/devlogs/[id]/layout.tsx | 4 +- packages/web/app/projects/[name]/layout.tsx | 4 +- .../[name]/settings/ProjectSettingsPage.tsx | 2 +- .../web/app/projects/[name]/settings/page.tsx | 2 +- packages/web/app/styles/README.md | 52 --- .../components/common/Pagination.tsx | 0 .../web/{app => }/components/common/index.ts | 0 .../common/overview-stats/OverviewStats.tsx | 0 .../ProjectCardSkeleton.tsx | 0 .../common/project-card-skeleton/index.ts | 0 .../components/custom/DevlogTags.tsx | 0 .../components/custom/EditableField.tsx | 0 .../components/custom/ErrorBoundary.tsx | 0 .../components/custom/LoadingPage.tsx | 0 .../components/custom/MarkdownEditor.tsx | 0 .../components/custom/MarkdownRenderer.tsx | 0 .../components/custom/StickyHeadings.tsx | 0 .../web/{app => }/components/custom/index.ts | 1 - .../custom/project}/ProjectNotFound.tsx | 0 .../feature}/dashboard/Dashboard.tsx | 0 .../feature}/dashboard/chart-utils.ts | 0 .../feature}/dashboard/index.ts | 0 .../feature/devlog}/DevlogAnchorNav.tsx | 0 .../feature/devlog}/DevlogDetails.tsx | 0 .../feature/devlog}/DevlogList.tsx | 22 +- .../{app => }/components/forms/DevlogForm.tsx | 0 .../web/{app => }/components/forms/index.ts | 0 packages/web/{app => }/components/index.ts | 6 +- .../{app => }/components/layout/TopNavbar.tsx | 2 +- .../layout/app-layout-skeleton.tsx} | 0 .../layout/app-layout.tsx} | 7 +- packages/web/components/layout/index.ts | 4 + .../layout/navigation-breadcrumb.tsx} | 34 +- .../layout/navigation-sidebar.tsx} | 0 .../provider}/ProjectProvider.tsx | 0 .../provider/app-providers.tsx} | 6 +- .../provider/devlog-provider.tsx} | 0 .../provider}/store-provider.tsx | 2 +- .../provider}/theme-provider.tsx | 0 .../components/realtime/realtime-status.tsx | 0 .../web/{app => }/components/ui/accordion.tsx | 0 .../{app => }/components/ui/alert-dialog.tsx | 0 .../web/{app => }/components/ui/alert.tsx | 0 .../web/{app => }/components/ui/badge.tsx | 0 .../{app => }/components/ui/breadcrumb.tsx | 0 .../web/{app => }/components/ui/button.tsx | 0 packages/web/{app => }/components/ui/card.tsx | 0 .../web/{app => }/components/ui/checkbox.tsx | 0 .../web/{app => }/components/ui/command.tsx | 0 .../web/{app => }/components/ui/dialog.tsx | 0 .../{app => }/components/ui/dropdown-menu.tsx | 0 packages/web/{app => }/components/ui/form.tsx | 0 packages/web/{app => }/components/ui/index.ts | 0 .../web/{app => }/components/ui/input.tsx | 0 .../web/{app => }/components/ui/label.tsx | 0 .../components/ui/navigation-menu.tsx | 0 .../web/{app => }/components/ui/popover.tsx | 0 .../web/{app => }/components/ui/progress.tsx | 0 .../web/{app => }/components/ui/select.tsx | 0 .../web/{app => }/components/ui/separator.tsx | 0 .../web/{app => }/components/ui/sheet.tsx | 0 .../web/{app => }/components/ui/sidebar.tsx | 0 .../web/{app => }/components/ui/skeleton.tsx | 0 .../web/{app => }/components/ui/switch.tsx | 0 .../web/{app => }/components/ui/table.tsx | 0 packages/web/{app => }/components/ui/tabs.tsx | 0 .../web/{app => }/components/ui/textarea.tsx | 0 .../{app => }/components/ui/theme-toggle.tsx | 0 .../web/{app => }/components/ui/tooltip.tsx | 0 packages/web/{app => }/hooks/index.ts | 0 packages/web/{app => }/hooks/use-mobile.tsx | 0 packages/web/{app => }/hooks/use-realtime.ts | 0 packages/web/{app => }/lib/api/api-client.ts | 0 packages/web/{app => }/lib/api/api-utils.ts | 2 +- .../{app => }/lib/api/devlog-api-client.ts | 6 +- packages/web/{app => }/lib/api/index.ts | 0 .../{app => }/lib/api/project-api-client.ts | 0 .../web/{app => }/lib/api/server-realtime.ts | 0 .../{app => }/lib/devlog/devlog-options.ts | 0 .../{app => }/lib/devlog/devlog-ui-utils.tsx | 0 packages/web/{app => }/lib/devlog/index.ts | 0 .../web/{app => }/lib/devlog/note-utils.tsx | 0 packages/web/{app => }/lib/index.ts | 0 packages/web/{app => }/lib/project-urls.ts | 2 +- packages/web/{app => }/lib/realtime/config.ts | 0 packages/web/{app => }/lib/realtime/index.ts | 0 .../{app => }/lib/realtime/pusher-provider.ts | 0 .../lib/realtime/realtime-service.ts | 0 .../{app => }/lib/realtime/sse-provider.ts | 0 packages/web/{app => }/lib/realtime/types.ts | 0 packages/web/{app => }/lib/routing/index.ts | 0 .../web/{app => }/lib/routing/route-params.ts | 0 packages/web/{app => }/lib/utils/debounce.ts | 0 packages/web/{app => }/lib/utils/index.ts | 0 .../web/{app => }/lib/utils/time-utils.ts | 0 packages/web/{app => }/lib/utils/utils.ts | 0 packages/web/{app => }/schemas/bridge.ts | 0 packages/web/{app => }/schemas/devlog.ts | 0 packages/web/{app => }/schemas/index.ts | 0 packages/web/{app => }/schemas/project.ts | 0 packages/web/{app => }/schemas/responses.ts | 0 packages/web/{app => }/schemas/validation.ts | 0 packages/web/{app => }/stores/base.ts | 0 packages/web/{app => }/stores/devlog-store.ts | 8 +- packages/web/{app => }/stores/index.ts | 0 packages/web/{app => }/stores/layout-store.ts | 0 .../web/{app => }/stores/project-store.ts | 0 .../web/{app => }/stores/realtime-store.ts | 0 packages/web/{app => }/styles/base.css | 0 packages/web/{app => styles}/fonts.css | 0 packages/web/{app => styles}/globals.css | 85 ++--- packages/web/{app => }/styles/layout.css | 0 packages/web/{app => }/styles/responsive.css | 0 .../web/tests/lib/api/api-integration.test.ts | 10 +- .../tests/lib/api/project-api-client.test.ts | 299 ------------------ packages/web/tsconfig.json | 2 +- 137 files changed, 175 insertions(+), 599 deletions(-) delete mode 100644 packages/web/app/components/ProjectResolver.tsx delete mode 100644 packages/web/app/components/features/devlogs/index.ts delete mode 100644 packages/web/app/components/layout/StickyHeadingsWrapper.tsx delete mode 100644 packages/web/app/components/layout/index.ts delete mode 100644 packages/web/app/components/project/index.ts delete mode 100644 packages/web/app/styles/README.md rename packages/web/{app => }/components/common/Pagination.tsx (100%) rename packages/web/{app => }/components/common/index.ts (100%) rename packages/web/{app => }/components/common/overview-stats/OverviewStats.tsx (100%) rename packages/web/{app => }/components/common/project-card-skeleton/ProjectCardSkeleton.tsx (100%) rename packages/web/{app => }/components/common/project-card-skeleton/index.ts (100%) rename packages/web/{app => }/components/custom/DevlogTags.tsx (100%) rename packages/web/{app => }/components/custom/EditableField.tsx (100%) rename packages/web/{app => }/components/custom/ErrorBoundary.tsx (100%) rename packages/web/{app => }/components/custom/LoadingPage.tsx (100%) rename packages/web/{app => }/components/custom/MarkdownEditor.tsx (100%) rename packages/web/{app => }/components/custom/MarkdownRenderer.tsx (100%) rename packages/web/{app => }/components/custom/StickyHeadings.tsx (100%) rename packages/web/{app => }/components/custom/index.ts (85%) rename packages/web/{app/components => components/custom/project}/ProjectNotFound.tsx (100%) rename packages/web/{app/components/features => components/feature}/dashboard/Dashboard.tsx (100%) rename packages/web/{app/components/features => components/feature}/dashboard/chart-utils.ts (100%) rename packages/web/{app/components/features => components/feature}/dashboard/index.ts (100%) rename packages/web/{app/components/features/devlogs => components/feature/devlog}/DevlogAnchorNav.tsx (100%) rename packages/web/{app/components/features/devlogs => components/feature/devlog}/DevlogDetails.tsx (100%) rename packages/web/{app/components/features/devlogs => components/feature/devlog}/DevlogList.tsx (98%) rename packages/web/{app => }/components/forms/DevlogForm.tsx (100%) rename packages/web/{app => }/components/forms/index.ts (100%) rename packages/web/{app => }/components/index.ts (69%) rename packages/web/{app => }/components/layout/TopNavbar.tsx (94%) rename packages/web/{app/components/layout/AppLayoutSkeleton.tsx => components/layout/app-layout-skeleton.tsx} (100%) rename packages/web/{app/AppLayout.tsx => components/layout/app-layout.tsx} (71%) create mode 100644 packages/web/components/layout/index.ts rename packages/web/{app/components/layout/NavigationBreadcrumb.tsx => components/layout/navigation-breadcrumb.tsx} (93%) rename packages/web/{app/components/layout/NavigationSidebar.tsx => components/layout/navigation-sidebar.tsx} (100%) rename packages/web/{app/projects/[name] => components/provider}/ProjectProvider.tsx (100%) rename packages/web/{app/AppProviders.tsx => components/provider/app-providers.tsx} (54%) rename packages/web/{app/projects/[name]/devlogs/DevlogProvider.tsx => components/provider/devlog-provider.tsx} (100%) rename packages/web/{app/components/providers => components/provider}/store-provider.tsx (87%) rename packages/web/{app/components/providers => components/provider}/theme-provider.tsx (100%) rename packages/web/{app => }/components/realtime/realtime-status.tsx (100%) rename packages/web/{app => }/components/ui/accordion.tsx (100%) rename packages/web/{app => }/components/ui/alert-dialog.tsx (100%) rename packages/web/{app => }/components/ui/alert.tsx (100%) rename packages/web/{app => }/components/ui/badge.tsx (100%) rename packages/web/{app => }/components/ui/breadcrumb.tsx (100%) rename packages/web/{app => }/components/ui/button.tsx (100%) rename packages/web/{app => }/components/ui/card.tsx (100%) rename packages/web/{app => }/components/ui/checkbox.tsx (100%) rename packages/web/{app => }/components/ui/command.tsx (100%) rename packages/web/{app => }/components/ui/dialog.tsx (100%) rename packages/web/{app => }/components/ui/dropdown-menu.tsx (100%) rename packages/web/{app => }/components/ui/form.tsx (100%) rename packages/web/{app => }/components/ui/index.ts (100%) rename packages/web/{app => }/components/ui/input.tsx (100%) rename packages/web/{app => }/components/ui/label.tsx (100%) rename packages/web/{app => }/components/ui/navigation-menu.tsx (100%) rename packages/web/{app => }/components/ui/popover.tsx (100%) rename packages/web/{app => }/components/ui/progress.tsx (100%) rename packages/web/{app => }/components/ui/select.tsx (100%) rename packages/web/{app => }/components/ui/separator.tsx (100%) rename packages/web/{app => }/components/ui/sheet.tsx (100%) rename packages/web/{app => }/components/ui/sidebar.tsx (100%) rename packages/web/{app => }/components/ui/skeleton.tsx (100%) rename packages/web/{app => }/components/ui/switch.tsx (100%) rename packages/web/{app => }/components/ui/table.tsx (100%) rename packages/web/{app => }/components/ui/tabs.tsx (100%) rename packages/web/{app => }/components/ui/textarea.tsx (100%) rename packages/web/{app => }/components/ui/theme-toggle.tsx (100%) rename packages/web/{app => }/components/ui/tooltip.tsx (100%) rename packages/web/{app => }/hooks/index.ts (100%) rename packages/web/{app => }/hooks/use-mobile.tsx (100%) rename packages/web/{app => }/hooks/use-realtime.ts (100%) rename packages/web/{app => }/lib/api/api-client.ts (100%) rename packages/web/{app => }/lib/api/api-utils.ts (99%) rename packages/web/{app => }/lib/api/devlog-api-client.ts (98%) rename packages/web/{app => }/lib/api/index.ts (100%) rename packages/web/{app => }/lib/api/project-api-client.ts (100%) rename packages/web/{app => }/lib/api/server-realtime.ts (100%) rename packages/web/{app => }/lib/devlog/devlog-options.ts (100%) rename packages/web/{app => }/lib/devlog/devlog-ui-utils.tsx (100%) rename packages/web/{app => }/lib/devlog/index.ts (100%) rename packages/web/{app => }/lib/devlog/note-utils.tsx (100%) rename packages/web/{app => }/lib/index.ts (100%) rename packages/web/{app => }/lib/project-urls.ts (95%) rename packages/web/{app => }/lib/realtime/config.ts (100%) rename packages/web/{app => }/lib/realtime/index.ts (100%) rename packages/web/{app => }/lib/realtime/pusher-provider.ts (100%) rename packages/web/{app => }/lib/realtime/realtime-service.ts (100%) rename packages/web/{app => }/lib/realtime/sse-provider.ts (100%) rename packages/web/{app => }/lib/realtime/types.ts (100%) rename packages/web/{app => }/lib/routing/index.ts (100%) rename packages/web/{app => }/lib/routing/route-params.ts (100%) rename packages/web/{app => }/lib/utils/debounce.ts (100%) rename packages/web/{app => }/lib/utils/index.ts (100%) rename packages/web/{app => }/lib/utils/time-utils.ts (100%) rename packages/web/{app => }/lib/utils/utils.ts (100%) rename packages/web/{app => }/schemas/bridge.ts (100%) rename packages/web/{app => }/schemas/devlog.ts (100%) rename packages/web/{app => }/schemas/index.ts (100%) rename packages/web/{app => }/schemas/project.ts (100%) rename packages/web/{app => }/schemas/responses.ts (100%) rename packages/web/{app => }/schemas/validation.ts (100%) rename packages/web/{app => }/stores/base.ts (100%) rename packages/web/{app => }/stores/devlog-store.ts (98%) rename packages/web/{app => }/stores/index.ts (100%) rename packages/web/{app => }/stores/layout-store.ts (100%) rename packages/web/{app => }/stores/project-store.ts (100%) rename packages/web/{app => }/stores/realtime-store.ts (100%) rename packages/web/{app => }/styles/base.css (100%) rename packages/web/{app => styles}/fonts.css (100%) rename packages/web/{app => styles}/globals.css (87%) rename packages/web/{app => }/styles/layout.css (100%) rename packages/web/{app => }/styles/responsive.css (100%) delete mode 100644 packages/web/tests/lib/api/project-api-client.test.ts diff --git a/docs/design/visual-design-system.md b/docs/design/visual-design-system.md index bb0d0884..2e8d5b3b 100644 --- a/docs/design/visual-design-system.md +++ b/docs/design/visual-design-system.md @@ -43,8 +43,8 @@ This document outlines the revised color scheme and icon system for devlog statu 5. **Distinctiveness**: Each category is easily distinguishable ## Implementation Files -- `packages/web/app/lib/devlog-ui-utils.tsx` - Core color and icon functions -- `packages/web/app/components/ui/DevlogTags.tsx` - Tag components using the utilities +- `packages/web/lib/devlog-ui-utils.tsx` - Core color and icon functions +- `packages/web/components/ui/DevlogTags.tsx` - Tag components using the utilities ## Benefits - **Faster Scanning**: Users can quickly identify work types and status diff --git a/packages/core/README.md b/packages/core/README.md index 1fde9773..250b66f2 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -59,7 +59,7 @@ const entry = await devlog.createDevlog({ type: 'feature', description: 'Add JWT-based authentication system', priority: 'high', - businessContext: 'Users need secure login to access protected features', + businessContext: 'Users need secure login to access protected feature', technicalContext: 'Using JWT tokens with refresh mechanism', acceptanceCriteria: [ 'Users can register with email/password', @@ -81,16 +81,16 @@ await devlog.addNote(entry.id, { content: 'Fixed validation issues with email format', }); -// List all devlogs +// List all devlog const allDevlogs = await devlog.listDevlogs(); -// Filter devlogs +// Filter devlog const inProgressTasks = await devlog.listDevlogs({ status: ['in-progress'], type: ['feature', 'bugfix'], }); -// Search devlogs +// Search devlog const authDevlogs = await devlog.searchDevlogs('authentication'); // Get active context for AI assistants diff --git a/packages/core/src/types/chat.ts b/packages/core/src/types/chat.ts index da2eee70..404bc6d5 100644 --- a/packages/core/src/types/chat.ts +++ b/packages/core/src/types/chat.ts @@ -180,7 +180,7 @@ export interface ChatStats { linkageStats: { linked: number; unlinked: number; - multiLinked: number; // Sessions linked to multiple devlogs + multiLinked: number; // Sessions linked to multiple devlog }; } diff --git a/packages/core/src/types/core.ts b/packages/core/src/types/core.ts index 3c5c9270..82417732 100644 --- a/packages/core/src/types/core.ts +++ b/packages/core/src/types/core.ts @@ -37,7 +37,7 @@ export type DevlogStatus = * **In Progress** - Work is actively being developed * - Developer/AI is actively working on the implementation * - Main development phase where code is being written - * - Use when: Starting work, making changes, implementing features + * - Use when: Starting work, making changes, implementing feature */ | 'in-progress' /** @@ -292,8 +292,8 @@ export interface TimeSeriesDataPoint { date: string; // ISO date string (YYYY-MM-DD) // Cumulative data (primary Y-axis) - shows total project progress over time - totalCreated: number; // Running total of all created devlogs - totalClosed: number; // Running total of closed devlogs (based on closedAt timestamp) + totalCreated: number; // Running total of all created devlog + totalClosed: number; // Running total of closed devlog (based on closedAt timestamp) // Snapshot data (secondary Y-axis) - shows workload at this point in time open: number; // Entries that were open as of this date (totalCreated - totalClosed) diff --git a/packages/mcp/src/api/devlog-api-client.ts b/packages/mcp/src/api/devlog-api-client.ts index f4a99515..78068e0d 100644 --- a/packages/mcp/src/api/devlog-api-client.ts +++ b/packages/mcp/src/api/devlog-api-client.ts @@ -51,11 +51,11 @@ export class DevlogApiClient { constructor(config: DevlogApiClientConfig) { this.retries = config.retries || 3; - + // Create HTTPS agent for proxy tunneling to fix redirect loops let httpsAgent; const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY; - + if (proxyUrl && config.baseUrl.includes('devlog.codervisor.dev')) { // Use tunnel agent for devlog.codervisor.dev to avoid redirect loops const proxyMatch = proxyUrl.match(/https?:\/\/([^:]+):(\d+)/); @@ -68,7 +68,7 @@ export class DevlogApiClient { }); } } - + this.axiosInstance = axios.create({ baseURL: config.baseUrl.replace(/\/$/, ''), // Remove trailing slash timeout: config.timeout || 30000, @@ -76,7 +76,7 @@ export class DevlogApiClient { 'Content-Type': 'application/json', }, // Use custom agent if we created one, otherwise let axios handle proxy normally - ...(httpsAgent && { + ...(httpsAgent && { httpsAgent, proxy: false, // Disable built-in proxy when using custom agent }), @@ -87,7 +87,7 @@ export class DevlogApiClient { (response) => response, (error: AxiosError) => { throw this.handleAxiosError(error); - } + }, ); } @@ -118,7 +118,7 @@ export class DevlogApiClient { // Server responded with error status statusCode = error.response.status; responseData = error.response.data; - + // Try to extract error message from response if (responseData) { if (typeof responseData === 'string') { @@ -127,12 +127,12 @@ export class DevlogApiClient { errorMessage = responseData.error?.message || responseData.message || error.message; } } - + errorMessage = `HTTP ${statusCode}: ${errorMessage}`; } else if (error.request) { // Request was made but no response received errorMessage = `Network error: ${error.message}`; - + // Handle specific proxy/network related errors with helpful context if (error.code === 'ERR_FR_TOO_MANY_REDIRECTS') { errorMessage += '. Fixed: Using tunnel agent to avoid proxy redirect loops.'; @@ -162,19 +162,17 @@ export class DevlogApiClient { return response.data; } catch (error) { const axiosError = error as AxiosError; - + // Only retry on network errors or 5xx server errors, not on client errors - const shouldRetry = attempt < this.retries && ( - !axiosError.response || - (axiosError.response.status >= 500) - ); - + const shouldRetry = + attempt < this.retries && (!axiosError.response || axiosError.response.status >= 500); + if (shouldRetry) { logger.warn(`Request failed (attempt ${attempt}/${this.retries}), retrying...`); await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)); return this.makeRequest(endpoint, options, attempt + 1); } - + // Re-throw the error (will be handled by the response interceptor) throw error; } @@ -194,7 +192,7 @@ export class DevlogApiClient { return response.data; } - // Handle paginated response format (devlogs list API) + // Handle paginated response format (devlog list API) if ( response && typeof response === 'object' && diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts index e4088329..55b580a9 100644 --- a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts @@ -14,7 +14,7 @@ const UpdateNoteBodySchema = z.object({ category: z.string().optional(), }); -// GET /api/projects/[name]/devlogs/[id]/notes/[noteId] - Get specific note +// GET /api/projects/[name]/devlog/[id]/notes/[noteId] - Get specific note export async function GET( request: NextRequest, { params }: { params: { name: string; devlogId: string; noteId: string } }, @@ -53,7 +53,7 @@ export async function GET( } } -// PUT /api/projects/[name]/devlogs/[id]/notes/[noteId] - Update specific note +// PUT /api/projects/[name]/devlog/[id]/notes/[noteId] - Update specific note export async function PUT( request: NextRequest, { params }: { params: { name: string; devlogId: string; noteId: string } }, @@ -106,7 +106,7 @@ export async function PUT( } } -// DELETE /api/projects/[name]/devlogs/[id]/notes/[noteId] - Delete specific note +// DELETE /api/projects/[name]/devlog/[id]/notes/[noteId] - Delete specific note export async function DELETE( request: NextRequest, { params }: { params: { name: string; devlogId: string; noteId: string } }, diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts index dbf92d67..2043c80a 100644 --- a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts @@ -8,7 +8,7 @@ import { DevlogAddNoteBodySchema, DevlogUpdateWithNoteBodySchema } from '@/schem // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[name]/devlogs/[id]/notes - List notes for a devlog entry +// GET /api/projects/[name]/devlog/[id]/notes - List notes for a devlog entry export async function GET( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, @@ -68,7 +68,7 @@ export async function GET( } } -// POST /api/projects/[name]/devlogs/[id]/notes - Add note to devlog entry +// POST /api/projects/[name]/devlog/[id]/notes - Add note to devlog entry export async function POST( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, @@ -118,7 +118,7 @@ export async function POST( } } -// PUT /api/projects/[name]/devlogs/[id]/notes - Update devlog and add note in one operation +// PUT /api/projects/[name]/devlog/[id]/notes - Update devlog and add note in one operation export async function PUT( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts index 610d7f3e..37f85aee 100644 --- a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts @@ -6,7 +6,7 @@ import { RealtimeEventType } from '@/lib/realtime'; // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[name]/devlogs/[id] - Get specific devlog entry +// GET /api/projects/[name]/devlog/[id] - Get specific devlog entry export async function GET( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, @@ -55,7 +55,7 @@ export async function GET( } } -// PUT /api/projects/[name]/devlogs/[id] - Update devlog entry +// PUT /api/projects/[name]/devlog/[id] - Update devlog entry export async function PUT( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, @@ -106,7 +106,7 @@ export async function PUT( } } -// DELETE /api/projects/[name]/devlogs/[id] - Delete devlog entry +// DELETE /api/projects/[name]/devlog/[id] - Delete devlog entry export async function DELETE( request: NextRequest, { params }: { params: { name: string; devlogId: string } }, diff --git a/packages/web/app/api/projects/[name]/devlogs/route.ts b/packages/web/app/api/projects/[name]/devlogs/route.ts index ac205be8..321a04a9 100644 --- a/packages/web/app/api/projects/[name]/devlogs/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/route.ts @@ -15,7 +15,7 @@ import { RealtimeEventType } from '@/lib/realtime'; // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[name]/devlogs - List devlogs for a project +// GET /api/projects/[name]/devlog - List devlog for a project export async function GET(request: NextRequest, { params }: { params: { name: string } }) { try { // Parse and validate project identifier @@ -84,16 +84,16 @@ export async function GET(request: NextRequest, { params }: { params: { name: st if (result.pagination) { return createCollectionResponse(result.items, result.pagination); } else { - // Transform devlogs and return as simple collection + // Transform devlog and return as simple collection return createSimpleCollectionResponse(result.items); } } catch (error) { - console.error('Error fetching devlogs:', error); - return ApiErrors.internalError('Failed to fetch devlogs'); + console.error('Error fetching devlog:', error); + return ApiErrors.internalError('Failed to fetch devlog'); } } -// POST /api/projects/[name]/devlogs - Create new devlog entry +// POST /api/projects/[name]/devlog - Create new devlog entry export async function POST(request: NextRequest, { params }: { params: { name: string } }) { try { // Parse and validate project identifier diff --git a/packages/web/app/api/projects/[name]/devlogs/search/route.ts b/packages/web/app/api/projects/[name]/devlogs/search/route.ts index 992ad01d..7ca1f891 100644 --- a/packages/web/app/api/projects/[name]/devlogs/search/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/search/route.ts @@ -28,7 +28,7 @@ interface SearchResponse { }; } -// GET /api/projects/[name]/devlogs/search - Enhanced search for devlogs +// GET /api/projects/[name]/devlog/search - Enhanced search for devlog export async function GET(request: NextRequest, { params }: { params: { name: string } }) { try { // Parse and validate project name parameter diff --git a/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts b/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts index e5f541e5..7c3a332f 100644 --- a/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts @@ -10,7 +10,7 @@ import { // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[name]/devlogs/stats/overview - Get overview statistics +// GET /api/projects/[name]/devlog/stats/overview - Get overview statistics export const GET = withErrorHandling( async (request: NextRequest, { params }: { params: { name: string } }) => { // Parse and validate parameters diff --git a/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts b/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts index 995c9ecc..766fc09c 100644 --- a/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts +++ b/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts @@ -10,7 +10,7 @@ import { // Mark this route as dynamic to prevent static generation export const dynamic = 'force-dynamic'; -// GET /api/projects/[name]/devlogs/stats/timeseries - Get time series statistics +// GET /api/projects/[name]/devlog/stats/timeseries - Get time series statistics export const GET = withErrorHandling( async (request: NextRequest, { params }: { params: { name: string } }) => { // Parse and validate parameters diff --git a/packages/web/app/components/ProjectResolver.tsx b/packages/web/app/components/ProjectResolver.tsx deleted file mode 100644 index 2d336ab3..00000000 --- a/packages/web/app/components/ProjectResolver.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { redirect } from 'next/navigation'; -import { ProjectService } from '@codervisor/devlog-core/server'; -import { generateSlugFromName } from '@codervisor/devlog-core'; -import type { Project } from '@codervisor/devlog-core'; -import { ProjectNotFound } from './ProjectNotFound'; - -interface ProjectResolverProps { - identifier: string; - identifierType: 'id' | 'name'; - children: (projectName: string, project: Project) => React.ReactNode; -} - -/** - * Server component that resolves a project identifier to project data - * Handles URL redirects when using name-based routing - */ -export async function ProjectResolver({ - identifier, - identifierType, - children, -}: ProjectResolverProps) { - try { - const projectService = ProjectService.getInstance(); - - let project: Project | null = null; - - if (identifierType === 'name') { - project = await projectService.getByName(identifier); - - // If project exists but identifier doesn't match canonical slug, redirect - if (project) { - const canonicalSlug = generateSlugFromName(project.name); - if (identifier !== canonicalSlug) { - // Redirect to canonical URL - redirect(`/projects/${canonicalSlug}`); - } - } - } else { - // For ID-based routing (fallback/legacy support) - const projectId = parseInt(identifier, 10); - if (!isNaN(projectId)) { - project = await projectService.get(projectId); - } - } - - if (!project) { - return ; - } - - return <>{children(project.name, project)}; - } catch (error) { - console.error('Error resolving project:', error); - return ; - } -} diff --git a/packages/web/app/components/features/devlogs/index.ts b/packages/web/app/components/features/devlogs/index.ts deleted file mode 100644 index 83c3c0b8..00000000 --- a/packages/web/app/components/features/devlogs/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { DevlogDetails } from './DevlogDetails'; -export { DevlogList } from './DevlogList'; -export { DevlogAnchorNav } from './DevlogAnchorNav'; diff --git a/packages/web/app/components/layout/StickyHeadingsWrapper.tsx b/packages/web/app/components/layout/StickyHeadingsWrapper.tsx deleted file mode 100644 index 36a9b643..00000000 --- a/packages/web/app/components/layout/StickyHeadingsWrapper.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client'; - -import React, { useRef } from 'react'; - -interface StickyHeadingsWrapperProps { - children: React.ReactNode; - /** - * Enable sticky headings feature - */ - enabled?: boolean; - /** - * Top offset for sticky headings (height of any fixed headers) - */ - topOffset?: number; - /** - * CSS selector for the scrollable container - */ - scrollContainerSelector?: string; - /** - * CSS selector for headings to track - */ - headingSelector?: string; -} - -export function StickyHeadingsWrapper({ - children, - enabled = true, - topOffset = 48, - scrollContainerSelector = '.page-content', - headingSelector = 'h1, h2, h3, h4, h5, h6', -}: StickyHeadingsWrapperProps) { - const contentRef = useRef(null); - - // TODO: Implement StickyHeadings functionality with shadcn/ui - // For now, just render children without sticky functionality - - return
{children}
; -} diff --git a/packages/web/app/components/layout/index.ts b/packages/web/app/components/layout/index.ts deleted file mode 100644 index acab062c..00000000 --- a/packages/web/app/components/layout/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { NavigationSidebar } from './NavigationSidebar'; -export { NavigationBreadcrumb } from './NavigationBreadcrumb'; -export { AppLayoutSkeleton } from './AppLayoutSkeleton'; -export { StickyHeadingsWrapper } from './StickyHeadingsWrapper'; -export * from './TopNavbar'; diff --git a/packages/web/app/components/project/index.ts b/packages/web/app/components/project/index.ts deleted file mode 100644 index f121e4b5..00000000 --- a/packages/web/app/components/project/index.ts +++ /dev/null @@ -1 +0,0 @@ -// Project components would be exported here if needed diff --git a/packages/web/app/layout.tsx b/packages/web/app/layout.tsx index 2ff590fa..794f0dee 100644 --- a/packages/web/app/layout.tsx +++ b/packages/web/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from 'next'; -import { AppProviders } from './AppProviders'; -import './globals.css'; -import './fonts.css'; +import { AppProviders } from '@/components/provider/app-providers'; +import '@/styles/globals.css'; +import '@/styles/fonts.css'; export const metadata: Metadata = { title: 'Devlog Management', diff --git a/packages/web/app/projects/[name]/ProjectDetailsPage.tsx b/packages/web/app/projects/[name]/ProjectDetailsPage.tsx index 2358794a..ff04e9c2 100644 --- a/packages/web/app/projects/[name]/ProjectDetailsPage.tsx +++ b/packages/web/app/projects/[name]/ProjectDetailsPage.tsx @@ -4,9 +4,9 @@ import React, { useEffect } from 'react'; import { Dashboard } from '@/components'; import { useDevlogStore, useProjectStore } from '@/stores'; import { useDevlogEvents } from '@/hooks/use-realtime'; -import { DevlogEntry, Project } from '@codervisor/devlog-core'; +import { DevlogEntry } from '@codervisor/devlog-core'; import { useRouter } from 'next/navigation'; -import { useProjectName } from './ProjectProvider'; +import { useProjectName } from '@/components/provider/ProjectProvider'; export function ProjectDetailsPage() { const projectName = useProjectName(); diff --git a/packages/web/app/projects/[name]/devlogs/ProjectDevlogListPage.tsx b/packages/web/app/projects/[name]/devlogs/ProjectDevlogListPage.tsx index c9076a68..aefe2aab 100644 --- a/packages/web/app/projects/[name]/devlogs/ProjectDevlogListPage.tsx +++ b/packages/web/app/projects/[name]/devlogs/ProjectDevlogListPage.tsx @@ -1,12 +1,12 @@ 'use client'; import React, { useEffect } from 'react'; -import { DevlogList } from '@/components'; import { useDevlogStore, useProjectStore } from '@/stores'; import { useDevlogEvents } from '@/hooks/use-realtime'; import { DevlogEntry, DevlogId } from '@codervisor/devlog-core'; import { useRouter } from 'next/navigation'; -import { useProjectName } from '../ProjectProvider'; +import { useProjectName } from '@/components/provider/ProjectProvider'; +import { DevlogList } from '@/components/feature/devlog/DevlogList'; export function ProjectDevlogListPage() { const projectName = useProjectName(); @@ -70,7 +70,7 @@ export function ProjectDevlogListPage() { try { await batchUpdate(ids, updates); } catch (error) { - console.error('Failed to batch update devlogs:', error); + console.error('Failed to batch update devlog:', error); throw error; } }; @@ -79,7 +79,7 @@ export function ProjectDevlogListPage() { try { await batchDelete(ids); } catch (error) { - console.error('Failed to batch delete devlogs:', error); + console.error('Failed to batch delete devlog:', error); throw error; } }; diff --git a/packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx b/packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx index 55d797ed..e68a350a 100644 --- a/packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx +++ b/packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx @@ -1,15 +1,16 @@ 'use client'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Button, DevlogDetails, Popover, PopoverContent, PopoverTrigger } from '@/components'; +import { Button, Popover, PopoverContent, PopoverTrigger } from '@/components'; import { useDevlogStore, useProjectStore } from '@/stores'; import { useDevlogEvents, useNoteEvents } from '@/hooks/use-realtime'; import { useRouter } from 'next/navigation'; import { ArrowLeftIcon, SaveIcon, TrashIcon, UndoIcon } from 'lucide-react'; import { toast } from 'sonner'; import { DevlogEntry } from '@codervisor/devlog-core'; -import { useProjectName } from '@/projects/[name]/ProjectProvider'; -import { useDevlogId } from '@/projects/[name]/devlogs/DevlogProvider'; +import { useProjectName } from '@/components/provider/ProjectProvider'; +import { useDevlogId } from '@/components/provider/devlog-provider'; +import { DevlogDetails } from '@/components/feature/devlog/DevlogDetails'; export function ProjectDevlogDetailsPage() { const projectName = useProjectName(); diff --git a/packages/web/app/projects/[name]/devlogs/[id]/layout.tsx b/packages/web/app/projects/[name]/devlogs/[id]/layout.tsx index 68b1893a..822e1b55 100644 --- a/packages/web/app/projects/[name]/devlogs/[id]/layout.tsx +++ b/packages/web/app/projects/[name]/devlogs/[id]/layout.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { DevlogService, ProjectService } from '@codervisor/devlog-core/server'; import { notFound } from 'next/navigation'; -import { DevlogProvider } from '../DevlogProvider'; +import { DevlogProvider } from '../../../../../components/provider/devlog-provider'; interface DevlogLayoutProps { children: React.ReactNode; @@ -45,4 +45,4 @@ export default async function DevlogLayout({ children, params }: DevlogLayoutPro console.error('Error resolving devlog:', error); notFound(); } -} \ No newline at end of file +} diff --git a/packages/web/app/projects/[name]/layout.tsx b/packages/web/app/projects/[name]/layout.tsx index 4662b41a..935d2c3a 100644 --- a/packages/web/app/projects/[name]/layout.tsx +++ b/packages/web/app/projects/[name]/layout.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { ProjectService } from '@codervisor/devlog-core/server'; import { generateSlugFromName } from '@codervisor/devlog-core'; -import { ProjectNotFound } from '@/components/ProjectNotFound'; +import { ProjectNotFound } from '@/components/custom/project/ProjectNotFound'; import { redirect } from 'next/navigation'; -import { ProjectProvider } from './ProjectProvider'; +import { ProjectProvider } from '@/components/provider/ProjectProvider'; interface ProjectLayoutProps { children: React.ReactNode; diff --git a/packages/web/app/projects/[name]/settings/ProjectSettingsPage.tsx b/packages/web/app/projects/[name]/settings/ProjectSettingsPage.tsx index 6a57f3f8..e64c2969 100644 --- a/packages/web/app/projects/[name]/settings/ProjectSettingsPage.tsx +++ b/packages/web/app/projects/[name]/settings/ProjectSettingsPage.tsx @@ -25,7 +25,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { LoaderIcon, SaveIcon, TrashIcon, AlertTriangleIcon } from 'lucide-react'; import { toast } from 'sonner'; import { Project } from '@codervisor/devlog-core'; -import { useProjectName } from '../ProjectProvider'; +import { useProjectName } from '@/components/provider/ProjectProvider'; interface ProjectFormData { name: string; diff --git a/packages/web/app/projects/[name]/settings/page.tsx b/packages/web/app/projects/[name]/settings/page.tsx index 5f0f0d94..4f446705 100644 --- a/packages/web/app/projects/[name]/settings/page.tsx +++ b/packages/web/app/projects/[name]/settings/page.tsx @@ -1,6 +1,6 @@ import { ProjectSettingsPage } from './ProjectSettingsPage'; -// Disable static generation for this page since it uses client-side features +// Disable static generation for this page since it uses client-side feature export const dynamic = 'force-dynamic'; export default function ProjectSettings() { diff --git a/packages/web/app/styles/README.md b/packages/web/app/styles/README.md deleted file mode 100644 index a21e24a2..00000000 --- a/packages/web/app/styles/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# CSS Architecture - -This directory contains modular CSS files that are imported into the main `globals.css` file. - -## Structure - -``` -styles/ -├── base.css # Base styles, CSS resets, and typography -├── antd-overrides.css # Ant Design component customizations -├── layout.css # App layout, navigation, and component styles -└── responsive.css # Responsive design and mobile styles -``` - -## Import Order - -The CSS files are imported in a specific order to maintain the cascade: - -1. **Tailwind CSS** - Base framework -2. **Third-party styles** - External libraries (highlight.js) -3. **Base styles** - Custom base styles and typography -4. **Ant Design overrides** - Framework component customizations -5. **Layout styles** - App structure, navigation, and component styles -6. **Responsive styles** - Media queries and mobile overrides - -## Guidelines - -### Adding New Styles - -- **Base styles**: Global typography, colors, and resets go in `base.css` -- **Component styles**: Specific component styles go in `components.css` -- **Layout styles**: App structure and navigation go in `layout.css` -- **Responsive styles**: Media queries go in `responsive.css` -- **Ant Design**: Component overrides go in `antd-overrides.css` - -### Naming Conventions - -- Use BEM-like naming: `.component-name`, `.component-name__element`, `.component-name--modifier` -- Prefix component-specific classes with the component name (e.g., `.dashboard-container`, `.devlog-item`) -- Use semantic names that describe purpose, not appearance - -### Best Practices - -- Keep specificity low -- Avoid `!important` unless absolutely necessary -- Group related styles together -- Add comments for complex or non-obvious styles -- Test responsive behavior across breakpoints - -## Migration Notes - -This modular structure was created from a large `globals.css` file to improve maintainability and reduce risk when making changes. All styles have been preserved and organized by concern. diff --git a/packages/web/app/components/common/Pagination.tsx b/packages/web/components/common/Pagination.tsx similarity index 100% rename from packages/web/app/components/common/Pagination.tsx rename to packages/web/components/common/Pagination.tsx diff --git a/packages/web/app/components/common/index.ts b/packages/web/components/common/index.ts similarity index 100% rename from packages/web/app/components/common/index.ts rename to packages/web/components/common/index.ts diff --git a/packages/web/app/components/common/overview-stats/OverviewStats.tsx b/packages/web/components/common/overview-stats/OverviewStats.tsx similarity index 100% rename from packages/web/app/components/common/overview-stats/OverviewStats.tsx rename to packages/web/components/common/overview-stats/OverviewStats.tsx diff --git a/packages/web/app/components/common/project-card-skeleton/ProjectCardSkeleton.tsx b/packages/web/components/common/project-card-skeleton/ProjectCardSkeleton.tsx similarity index 100% rename from packages/web/app/components/common/project-card-skeleton/ProjectCardSkeleton.tsx rename to packages/web/components/common/project-card-skeleton/ProjectCardSkeleton.tsx diff --git a/packages/web/app/components/common/project-card-skeleton/index.ts b/packages/web/components/common/project-card-skeleton/index.ts similarity index 100% rename from packages/web/app/components/common/project-card-skeleton/index.ts rename to packages/web/components/common/project-card-skeleton/index.ts diff --git a/packages/web/app/components/custom/DevlogTags.tsx b/packages/web/components/custom/DevlogTags.tsx similarity index 100% rename from packages/web/app/components/custom/DevlogTags.tsx rename to packages/web/components/custom/DevlogTags.tsx diff --git a/packages/web/app/components/custom/EditableField.tsx b/packages/web/components/custom/EditableField.tsx similarity index 100% rename from packages/web/app/components/custom/EditableField.tsx rename to packages/web/components/custom/EditableField.tsx diff --git a/packages/web/app/components/custom/ErrorBoundary.tsx b/packages/web/components/custom/ErrorBoundary.tsx similarity index 100% rename from packages/web/app/components/custom/ErrorBoundary.tsx rename to packages/web/components/custom/ErrorBoundary.tsx diff --git a/packages/web/app/components/custom/LoadingPage.tsx b/packages/web/components/custom/LoadingPage.tsx similarity index 100% rename from packages/web/app/components/custom/LoadingPage.tsx rename to packages/web/components/custom/LoadingPage.tsx diff --git a/packages/web/app/components/custom/MarkdownEditor.tsx b/packages/web/components/custom/MarkdownEditor.tsx similarity index 100% rename from packages/web/app/components/custom/MarkdownEditor.tsx rename to packages/web/components/custom/MarkdownEditor.tsx diff --git a/packages/web/app/components/custom/MarkdownRenderer.tsx b/packages/web/components/custom/MarkdownRenderer.tsx similarity index 100% rename from packages/web/app/components/custom/MarkdownRenderer.tsx rename to packages/web/components/custom/MarkdownRenderer.tsx diff --git a/packages/web/app/components/custom/StickyHeadings.tsx b/packages/web/components/custom/StickyHeadings.tsx similarity index 100% rename from packages/web/app/components/custom/StickyHeadings.tsx rename to packages/web/components/custom/StickyHeadings.tsx diff --git a/packages/web/app/components/custom/index.ts b/packages/web/components/custom/index.ts similarity index 85% rename from packages/web/app/components/custom/index.ts rename to packages/web/components/custom/index.ts index 11a991a1..bd138903 100644 --- a/packages/web/app/components/custom/index.ts +++ b/packages/web/components/custom/index.ts @@ -1,5 +1,4 @@ export { LoadingPage } from './LoadingPage'; -export { ErrorBoundary } from './ErrorBoundary'; export { MarkdownRenderer } from './MarkdownRenderer'; export { EditableField } from './EditableField'; export { DevlogStatusTag, DevlogPriorityTag, DevlogTypeTag } from './DevlogTags'; diff --git a/packages/web/app/components/ProjectNotFound.tsx b/packages/web/components/custom/project/ProjectNotFound.tsx similarity index 100% rename from packages/web/app/components/ProjectNotFound.tsx rename to packages/web/components/custom/project/ProjectNotFound.tsx diff --git a/packages/web/app/components/features/dashboard/Dashboard.tsx b/packages/web/components/feature/dashboard/Dashboard.tsx similarity index 100% rename from packages/web/app/components/features/dashboard/Dashboard.tsx rename to packages/web/components/feature/dashboard/Dashboard.tsx diff --git a/packages/web/app/components/features/dashboard/chart-utils.ts b/packages/web/components/feature/dashboard/chart-utils.ts similarity index 100% rename from packages/web/app/components/features/dashboard/chart-utils.ts rename to packages/web/components/feature/dashboard/chart-utils.ts diff --git a/packages/web/app/components/features/dashboard/index.ts b/packages/web/components/feature/dashboard/index.ts similarity index 100% rename from packages/web/app/components/features/dashboard/index.ts rename to packages/web/components/feature/dashboard/index.ts diff --git a/packages/web/app/components/features/devlogs/DevlogAnchorNav.tsx b/packages/web/components/feature/devlog/DevlogAnchorNav.tsx similarity index 100% rename from packages/web/app/components/features/devlogs/DevlogAnchorNav.tsx rename to packages/web/components/feature/devlog/DevlogAnchorNav.tsx diff --git a/packages/web/app/components/features/devlogs/DevlogDetails.tsx b/packages/web/components/feature/devlog/DevlogDetails.tsx similarity index 100% rename from packages/web/app/components/features/devlogs/DevlogDetails.tsx rename to packages/web/components/feature/devlog/DevlogDetails.tsx diff --git a/packages/web/app/components/features/devlogs/DevlogList.tsx b/packages/web/components/feature/devlog/DevlogList.tsx similarity index 98% rename from packages/web/app/components/features/devlogs/DevlogList.tsx rename to packages/web/components/feature/devlog/DevlogList.tsx index e2e0e4c9..7065a285 100644 --- a/packages/web/app/components/features/devlogs/DevlogList.tsx +++ b/packages/web/components/feature/devlog/DevlogList.tsx @@ -44,7 +44,14 @@ import { toast } from 'sonner'; import { Edit, Eye, Search, Trash2, X } from 'lucide-react'; import { DevlogEntry, DevlogFilter, DevlogId, DevlogNoteCategory } from '@codervisor/devlog-core'; import { DevlogPriorityTag, DevlogStatusTag, DevlogTypeTag, Pagination } from '@/components'; -import { cn, debounce, formatTimeAgoWithTooltip, priorityOptions, statusOptions, typeOptions } from '@/lib'; +import { + cn, + debounce, + formatTimeAgoWithTooltip, + priorityOptions, + statusOptions, + typeOptions, +} from '@/lib'; import { TableDataContext } from '@/stores/base'; interface DevlogListProps { @@ -114,7 +121,7 @@ export function DevlogList({ visible: true, current: 0, total: selectedRowKeys.length, - operation: 'Updating devlogs...', + operation: 'Updating devlog...', }); try { @@ -152,7 +159,7 @@ export function DevlogList({ visible: true, current: 0, total: selectedRowKeys.length, - operation: 'Deleting devlogs...', + operation: 'Deleting devlog...', }); try { @@ -335,7 +342,12 @@ export function DevlogList({

No devlogs found

) : ( -
+
@@ -461,7 +473,7 @@ export function DevlogList({
{/* Gutter */} -
+
{/* Pagination */} {pagination && ( diff --git a/packages/web/app/components/forms/DevlogForm.tsx b/packages/web/components/forms/DevlogForm.tsx similarity index 100% rename from packages/web/app/components/forms/DevlogForm.tsx rename to packages/web/components/forms/DevlogForm.tsx diff --git a/packages/web/app/components/forms/index.ts b/packages/web/components/forms/index.ts similarity index 100% rename from packages/web/app/components/forms/index.ts rename to packages/web/components/forms/index.ts diff --git a/packages/web/app/components/index.ts b/packages/web/components/index.ts similarity index 69% rename from packages/web/app/components/index.ts rename to packages/web/components/index.ts index e8f1f492..8a7b648d 100644 --- a/packages/web/app/components/index.ts +++ b/packages/web/components/index.ts @@ -14,10 +14,8 @@ export * from './custom'; export * from './forms'; // Feature Components -export * from './features/dashboard'; -export * from './features/devlogs'; +export * from '@/components/feature/dashboard'; // Project Components // Note: ProjectResolver is not exported as it's only used server-side in layout.tsx -export { ProjectNotFound } from './ProjectNotFound'; -export * from './project'; +export { ProjectNotFound } from './custom/project/ProjectNotFound'; diff --git a/packages/web/app/components/layout/TopNavbar.tsx b/packages/web/components/layout/TopNavbar.tsx similarity index 94% rename from packages/web/app/components/layout/TopNavbar.tsx rename to packages/web/components/layout/TopNavbar.tsx index 908ca9b4..31db9232 100644 --- a/packages/web/app/components/layout/TopNavbar.tsx +++ b/packages/web/components/layout/TopNavbar.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Link from 'next/link'; import Image from 'next/image'; -import { NavigationBreadcrumb } from './NavigationBreadcrumb'; +import { NavigationBreadcrumb } from './navigation-breadcrumb'; import { ThemeToggle } from '@/components/ui/theme-toggle'; export function TopNavbar() { diff --git a/packages/web/app/components/layout/AppLayoutSkeleton.tsx b/packages/web/components/layout/app-layout-skeleton.tsx similarity index 100% rename from packages/web/app/components/layout/AppLayoutSkeleton.tsx rename to packages/web/components/layout/app-layout-skeleton.tsx diff --git a/packages/web/app/AppLayout.tsx b/packages/web/components/layout/app-layout.tsx similarity index 71% rename from packages/web/app/AppLayout.tsx rename to packages/web/components/layout/app-layout.tsx index 9d02397d..f2c956f0 100644 --- a/packages/web/app/AppLayout.tsx +++ b/packages/web/components/layout/app-layout.tsx @@ -1,9 +1,12 @@ 'use client'; import React, { useEffect, useState } from 'react'; -import { AppLayoutSkeleton, ErrorBoundary, NavigationSidebar, TopNavbar } from '@/components'; -import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'; +import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; import { Toaster } from 'sonner'; +import { AppLayoutSkeleton } from '@/components/layout/app-layout-skeleton'; +import { ErrorBoundary } from '@/components/custom/ErrorBoundary'; +import { TopNavbar } from '@/components/layout/TopNavbar'; +import { NavigationSidebar } from '@/components/layout/navigation-sidebar'; interface AppLayoutProps { children: React.ReactNode; diff --git a/packages/web/components/layout/index.ts b/packages/web/components/layout/index.ts new file mode 100644 index 00000000..2b2a666a --- /dev/null +++ b/packages/web/components/layout/index.ts @@ -0,0 +1,4 @@ +// export { NavigationSidebar } from './navigation-sidebar'; +// export { NavigationBreadcrumb } from './navigation-breadcrumb'; +// export { AppLayoutSkeleton } from './app-layout-skeleton'; +// export * from './TopNavbar'; diff --git a/packages/web/app/components/layout/NavigationBreadcrumb.tsx b/packages/web/components/layout/navigation-breadcrumb.tsx similarity index 93% rename from packages/web/app/components/layout/NavigationBreadcrumb.tsx rename to packages/web/components/layout/navigation-breadcrumb.tsx index 758b5a0a..0ea68fdc 100644 --- a/packages/web/app/components/layout/NavigationBreadcrumb.tsx +++ b/packages/web/components/layout/navigation-breadcrumb.tsx @@ -23,7 +23,7 @@ import { toast } from 'sonner'; export function NavigationBreadcrumb() { const pathname = usePathname(); const router = useRouter(); - + // State for devlog search const [devlogSearchText, setDevlogSearchText] = useState(''); @@ -33,10 +33,10 @@ export function NavigationBreadcrumb() { let projectName: string | null = null; let devlogId: number | null = null; - // Check if we're in a project path: /projects/[name] or /projects/[name]/devlogs/[id] + // Check if we're in a project path: /projects/[name] or /projects/[name]/devlog/[id] if (pathSegments[0] === 'projects' && pathSegments[1]) { projectName = pathSegments[1]; - + // Check if we're in a devlog path if (pathSegments[2] === 'devlogs' && pathSegments[3]) { const parsedDevlogId = parseInt(pathSegments[3], 10); @@ -48,26 +48,28 @@ export function NavigationBreadcrumb() { const { currentProjectContext, currentProjectName, projectsContext, fetchProjects } = useProjectStore(); - const { currentDevlogContext, navigationDevlogsContext, fetchNavigationDevlogs } = useDevlogStore(); + const { currentDevlogContext, navigationDevlogsContext, fetchNavigationDevlogs } = + useDevlogStore(); - // Filter devlogs for search + // Filter devlog for search const filteredDevlogs = useMemo(() => { const devlogs = navigationDevlogsContext.data || []; const sorted = devlogs.sort((a, b) => { // Sort by ID descending (most recent first) return (b.id ?? 0) - (a.id ?? 0); }); - + // Apply search filter if (!devlogSearchText.trim()) { return sorted; } - - return sorted.filter(devlog => - devlog.title?.toLowerCase().includes(devlogSearchText.toLowerCase()) || - devlog.id?.toString().includes(devlogSearchText) || - devlog.status?.toLowerCase().includes(devlogSearchText.toLowerCase()) || - devlog.type?.toLowerCase().includes(devlogSearchText.toLowerCase()) + + return sorted.filter( + (devlog) => + devlog.title?.toLowerCase().includes(devlogSearchText.toLowerCase()) || + devlog.id?.toString().includes(devlogSearchText) || + devlog.status?.toLowerCase().includes(devlogSearchText.toLowerCase()) || + devlog.type?.toLowerCase().includes(devlogSearchText.toLowerCase()), ); }, [navigationDevlogsContext.data, devlogSearchText]); @@ -183,7 +185,7 @@ export function NavigationBreadcrumb() { { if (open) { - // Fetch devlogs when opening the dropdown + // Fetch devlog when opening the dropdown await fetchNavigationDevlogs(); // Reset search when opening setDevlogSearchText(''); @@ -210,7 +212,7 @@ export function NavigationBreadcrumb() { />
- + {/* Loading State */} {navigationDevlogsContext.loading ? ( <> @@ -226,10 +228,10 @@ export function NavigationBreadcrumb() { {/* No Results */} {filteredDevlogs.length === 0 && ( - {devlogSearchText ? 'No devlogs found' : 'No devlogs available'} + {devlogSearchText ? 'No devlog found' : 'No devlog available'} )} - + {/* Devlog Items */} {filteredDevlogs.map((devlog) => { const isCurrentDevlog = devlogId === devlog.id; diff --git a/packages/web/app/components/layout/NavigationSidebar.tsx b/packages/web/components/layout/navigation-sidebar.tsx similarity index 100% rename from packages/web/app/components/layout/NavigationSidebar.tsx rename to packages/web/components/layout/navigation-sidebar.tsx diff --git a/packages/web/app/projects/[name]/ProjectProvider.tsx b/packages/web/components/provider/ProjectProvider.tsx similarity index 100% rename from packages/web/app/projects/[name]/ProjectProvider.tsx rename to packages/web/components/provider/ProjectProvider.tsx diff --git a/packages/web/app/AppProviders.tsx b/packages/web/components/provider/app-providers.tsx similarity index 54% rename from packages/web/app/AppProviders.tsx rename to packages/web/components/provider/app-providers.tsx index 5a717dcf..d1448297 100644 --- a/packages/web/app/AppProviders.tsx +++ b/packages/web/components/provider/app-providers.tsx @@ -1,8 +1,8 @@ 'use client'; -import { ThemeProvider } from '@/components/providers/theme-provider'; -import { AppLayout } from './AppLayout'; -import { StoreProvider } from '@/components/providers/store-provider'; +import { ThemeProvider } from '@/components/provider/theme-provider'; +import { AppLayout } from '@/components/layout/app-layout'; +import { StoreProvider } from '@/components/provider/store-provider'; export function AppProviders({ children }: { children: React.ReactNode }) { return ( diff --git a/packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx b/packages/web/components/provider/devlog-provider.tsx similarity index 100% rename from packages/web/app/projects/[name]/devlogs/DevlogProvider.tsx rename to packages/web/components/provider/devlog-provider.tsx diff --git a/packages/web/app/components/providers/store-provider.tsx b/packages/web/components/provider/store-provider.tsx similarity index 87% rename from packages/web/app/components/providers/store-provider.tsx rename to packages/web/components/provider/store-provider.tsx index 7d7b120c..9b65a412 100644 --- a/packages/web/app/components/providers/store-provider.tsx +++ b/packages/web/components/provider/store-provider.tsx @@ -5,7 +5,7 @@ import { initializeProjectStore } from '@/stores'; /** * Provider component that initializes Zustand stores and sets up real-time events - * This replaces the need for React Context providers + * This replaces the need for React Context provider */ export function StoreProvider({ children }: { children: React.ReactNode }) { // Initialize stores on mount diff --git a/packages/web/app/components/providers/theme-provider.tsx b/packages/web/components/provider/theme-provider.tsx similarity index 100% rename from packages/web/app/components/providers/theme-provider.tsx rename to packages/web/components/provider/theme-provider.tsx diff --git a/packages/web/app/components/realtime/realtime-status.tsx b/packages/web/components/realtime/realtime-status.tsx similarity index 100% rename from packages/web/app/components/realtime/realtime-status.tsx rename to packages/web/components/realtime/realtime-status.tsx diff --git a/packages/web/app/components/ui/accordion.tsx b/packages/web/components/ui/accordion.tsx similarity index 100% rename from packages/web/app/components/ui/accordion.tsx rename to packages/web/components/ui/accordion.tsx diff --git a/packages/web/app/components/ui/alert-dialog.tsx b/packages/web/components/ui/alert-dialog.tsx similarity index 100% rename from packages/web/app/components/ui/alert-dialog.tsx rename to packages/web/components/ui/alert-dialog.tsx diff --git a/packages/web/app/components/ui/alert.tsx b/packages/web/components/ui/alert.tsx similarity index 100% rename from packages/web/app/components/ui/alert.tsx rename to packages/web/components/ui/alert.tsx diff --git a/packages/web/app/components/ui/badge.tsx b/packages/web/components/ui/badge.tsx similarity index 100% rename from packages/web/app/components/ui/badge.tsx rename to packages/web/components/ui/badge.tsx diff --git a/packages/web/app/components/ui/breadcrumb.tsx b/packages/web/components/ui/breadcrumb.tsx similarity index 100% rename from packages/web/app/components/ui/breadcrumb.tsx rename to packages/web/components/ui/breadcrumb.tsx diff --git a/packages/web/app/components/ui/button.tsx b/packages/web/components/ui/button.tsx similarity index 100% rename from packages/web/app/components/ui/button.tsx rename to packages/web/components/ui/button.tsx diff --git a/packages/web/app/components/ui/card.tsx b/packages/web/components/ui/card.tsx similarity index 100% rename from packages/web/app/components/ui/card.tsx rename to packages/web/components/ui/card.tsx diff --git a/packages/web/app/components/ui/checkbox.tsx b/packages/web/components/ui/checkbox.tsx similarity index 100% rename from packages/web/app/components/ui/checkbox.tsx rename to packages/web/components/ui/checkbox.tsx diff --git a/packages/web/app/components/ui/command.tsx b/packages/web/components/ui/command.tsx similarity index 100% rename from packages/web/app/components/ui/command.tsx rename to packages/web/components/ui/command.tsx diff --git a/packages/web/app/components/ui/dialog.tsx b/packages/web/components/ui/dialog.tsx similarity index 100% rename from packages/web/app/components/ui/dialog.tsx rename to packages/web/components/ui/dialog.tsx diff --git a/packages/web/app/components/ui/dropdown-menu.tsx b/packages/web/components/ui/dropdown-menu.tsx similarity index 100% rename from packages/web/app/components/ui/dropdown-menu.tsx rename to packages/web/components/ui/dropdown-menu.tsx diff --git a/packages/web/app/components/ui/form.tsx b/packages/web/components/ui/form.tsx similarity index 100% rename from packages/web/app/components/ui/form.tsx rename to packages/web/components/ui/form.tsx diff --git a/packages/web/app/components/ui/index.ts b/packages/web/components/ui/index.ts similarity index 100% rename from packages/web/app/components/ui/index.ts rename to packages/web/components/ui/index.ts diff --git a/packages/web/app/components/ui/input.tsx b/packages/web/components/ui/input.tsx similarity index 100% rename from packages/web/app/components/ui/input.tsx rename to packages/web/components/ui/input.tsx diff --git a/packages/web/app/components/ui/label.tsx b/packages/web/components/ui/label.tsx similarity index 100% rename from packages/web/app/components/ui/label.tsx rename to packages/web/components/ui/label.tsx diff --git a/packages/web/app/components/ui/navigation-menu.tsx b/packages/web/components/ui/navigation-menu.tsx similarity index 100% rename from packages/web/app/components/ui/navigation-menu.tsx rename to packages/web/components/ui/navigation-menu.tsx diff --git a/packages/web/app/components/ui/popover.tsx b/packages/web/components/ui/popover.tsx similarity index 100% rename from packages/web/app/components/ui/popover.tsx rename to packages/web/components/ui/popover.tsx diff --git a/packages/web/app/components/ui/progress.tsx b/packages/web/components/ui/progress.tsx similarity index 100% rename from packages/web/app/components/ui/progress.tsx rename to packages/web/components/ui/progress.tsx diff --git a/packages/web/app/components/ui/select.tsx b/packages/web/components/ui/select.tsx similarity index 100% rename from packages/web/app/components/ui/select.tsx rename to packages/web/components/ui/select.tsx diff --git a/packages/web/app/components/ui/separator.tsx b/packages/web/components/ui/separator.tsx similarity index 100% rename from packages/web/app/components/ui/separator.tsx rename to packages/web/components/ui/separator.tsx diff --git a/packages/web/app/components/ui/sheet.tsx b/packages/web/components/ui/sheet.tsx similarity index 100% rename from packages/web/app/components/ui/sheet.tsx rename to packages/web/components/ui/sheet.tsx diff --git a/packages/web/app/components/ui/sidebar.tsx b/packages/web/components/ui/sidebar.tsx similarity index 100% rename from packages/web/app/components/ui/sidebar.tsx rename to packages/web/components/ui/sidebar.tsx diff --git a/packages/web/app/components/ui/skeleton.tsx b/packages/web/components/ui/skeleton.tsx similarity index 100% rename from packages/web/app/components/ui/skeleton.tsx rename to packages/web/components/ui/skeleton.tsx diff --git a/packages/web/app/components/ui/switch.tsx b/packages/web/components/ui/switch.tsx similarity index 100% rename from packages/web/app/components/ui/switch.tsx rename to packages/web/components/ui/switch.tsx diff --git a/packages/web/app/components/ui/table.tsx b/packages/web/components/ui/table.tsx similarity index 100% rename from packages/web/app/components/ui/table.tsx rename to packages/web/components/ui/table.tsx diff --git a/packages/web/app/components/ui/tabs.tsx b/packages/web/components/ui/tabs.tsx similarity index 100% rename from packages/web/app/components/ui/tabs.tsx rename to packages/web/components/ui/tabs.tsx diff --git a/packages/web/app/components/ui/textarea.tsx b/packages/web/components/ui/textarea.tsx similarity index 100% rename from packages/web/app/components/ui/textarea.tsx rename to packages/web/components/ui/textarea.tsx diff --git a/packages/web/app/components/ui/theme-toggle.tsx b/packages/web/components/ui/theme-toggle.tsx similarity index 100% rename from packages/web/app/components/ui/theme-toggle.tsx rename to packages/web/components/ui/theme-toggle.tsx diff --git a/packages/web/app/components/ui/tooltip.tsx b/packages/web/components/ui/tooltip.tsx similarity index 100% rename from packages/web/app/components/ui/tooltip.tsx rename to packages/web/components/ui/tooltip.tsx diff --git a/packages/web/app/hooks/index.ts b/packages/web/hooks/index.ts similarity index 100% rename from packages/web/app/hooks/index.ts rename to packages/web/hooks/index.ts diff --git a/packages/web/app/hooks/use-mobile.tsx b/packages/web/hooks/use-mobile.tsx similarity index 100% rename from packages/web/app/hooks/use-mobile.tsx rename to packages/web/hooks/use-mobile.tsx diff --git a/packages/web/app/hooks/use-realtime.ts b/packages/web/hooks/use-realtime.ts similarity index 100% rename from packages/web/app/hooks/use-realtime.ts rename to packages/web/hooks/use-realtime.ts diff --git a/packages/web/app/lib/api/api-client.ts b/packages/web/lib/api/api-client.ts similarity index 100% rename from packages/web/app/lib/api/api-client.ts rename to packages/web/lib/api/api-client.ts diff --git a/packages/web/app/lib/api/api-utils.ts b/packages/web/lib/api/api-utils.ts similarity index 99% rename from packages/web/app/lib/api/api-utils.ts rename to packages/web/lib/api/api-utils.ts index 4cc2ccaa..2b9364e2 100644 --- a/packages/web/app/lib/api/api-utils.ts +++ b/packages/web/lib/api/api-utils.ts @@ -53,7 +53,7 @@ export const RouteParams = { /** * Parse project name and devlog ID parameters (name-only routing for projects) - * Usage: /api/projects/[name]/devlogs/[id] + * Usage: /api/projects/[name]/devlog/[id] */ parseProjectNameAndDevlogId(params: { name: string; devlogId: string }) { try { diff --git a/packages/web/app/lib/api/devlog-api-client.ts b/packages/web/lib/api/devlog-api-client.ts similarity index 98% rename from packages/web/app/lib/api/devlog-api-client.ts rename to packages/web/lib/api/devlog-api-client.ts index d521f282..120b5453 100644 --- a/packages/web/app/lib/api/devlog-api-client.ts +++ b/packages/web/lib/api/devlog-api-client.ts @@ -59,7 +59,7 @@ export class DevlogApiClient { constructor(private projectName: string) {} /** - * Get all devlogs for the project + * Get all devlog for the project */ async list( filter?: DevlogFilter, @@ -131,7 +131,7 @@ export class DevlogApiClient { } /** - * Batch update multiple devlogs + * Batch update multiple devlog */ async batchUpdate( devlogIds: DevlogId[], @@ -144,7 +144,7 @@ export class DevlogApiClient { } /** - * Batch delete multiple devlogs + * Batch delete multiple devlog */ async batchDelete(devlogIds: DevlogId[]): Promise { return apiClient.post(`/api/projects/${this.projectName}/devlogs/batch/delete`, { diff --git a/packages/web/app/lib/api/index.ts b/packages/web/lib/api/index.ts similarity index 100% rename from packages/web/app/lib/api/index.ts rename to packages/web/lib/api/index.ts diff --git a/packages/web/app/lib/api/project-api-client.ts b/packages/web/lib/api/project-api-client.ts similarity index 100% rename from packages/web/app/lib/api/project-api-client.ts rename to packages/web/lib/api/project-api-client.ts diff --git a/packages/web/app/lib/api/server-realtime.ts b/packages/web/lib/api/server-realtime.ts similarity index 100% rename from packages/web/app/lib/api/server-realtime.ts rename to packages/web/lib/api/server-realtime.ts diff --git a/packages/web/app/lib/devlog/devlog-options.ts b/packages/web/lib/devlog/devlog-options.ts similarity index 100% rename from packages/web/app/lib/devlog/devlog-options.ts rename to packages/web/lib/devlog/devlog-options.ts diff --git a/packages/web/app/lib/devlog/devlog-ui-utils.tsx b/packages/web/lib/devlog/devlog-ui-utils.tsx similarity index 100% rename from packages/web/app/lib/devlog/devlog-ui-utils.tsx rename to packages/web/lib/devlog/devlog-ui-utils.tsx diff --git a/packages/web/app/lib/devlog/index.ts b/packages/web/lib/devlog/index.ts similarity index 100% rename from packages/web/app/lib/devlog/index.ts rename to packages/web/lib/devlog/index.ts diff --git a/packages/web/app/lib/devlog/note-utils.tsx b/packages/web/lib/devlog/note-utils.tsx similarity index 100% rename from packages/web/app/lib/devlog/note-utils.tsx rename to packages/web/lib/devlog/note-utils.tsx diff --git a/packages/web/app/lib/index.ts b/packages/web/lib/index.ts similarity index 100% rename from packages/web/app/lib/index.ts rename to packages/web/lib/index.ts diff --git a/packages/web/app/lib/project-urls.ts b/packages/web/lib/project-urls.ts similarity index 95% rename from packages/web/app/lib/project-urls.ts rename to packages/web/lib/project-urls.ts index e3b5c019..eadbf2af 100644 --- a/packages/web/app/lib/project-urls.ts +++ b/packages/web/lib/project-urls.ts @@ -16,7 +16,7 @@ export class ProjectUrls { } /** - * Generate URL for project devlogs list + * Generate URL for project devlog list */ static devlogs(projectName: string): string { return `${this.project(projectName)}/devlogs`; diff --git a/packages/web/app/lib/realtime/config.ts b/packages/web/lib/realtime/config.ts similarity index 100% rename from packages/web/app/lib/realtime/config.ts rename to packages/web/lib/realtime/config.ts diff --git a/packages/web/app/lib/realtime/index.ts b/packages/web/lib/realtime/index.ts similarity index 100% rename from packages/web/app/lib/realtime/index.ts rename to packages/web/lib/realtime/index.ts diff --git a/packages/web/app/lib/realtime/pusher-provider.ts b/packages/web/lib/realtime/pusher-provider.ts similarity index 100% rename from packages/web/app/lib/realtime/pusher-provider.ts rename to packages/web/lib/realtime/pusher-provider.ts diff --git a/packages/web/app/lib/realtime/realtime-service.ts b/packages/web/lib/realtime/realtime-service.ts similarity index 100% rename from packages/web/app/lib/realtime/realtime-service.ts rename to packages/web/lib/realtime/realtime-service.ts diff --git a/packages/web/app/lib/realtime/sse-provider.ts b/packages/web/lib/realtime/sse-provider.ts similarity index 100% rename from packages/web/app/lib/realtime/sse-provider.ts rename to packages/web/lib/realtime/sse-provider.ts diff --git a/packages/web/app/lib/realtime/types.ts b/packages/web/lib/realtime/types.ts similarity index 100% rename from packages/web/app/lib/realtime/types.ts rename to packages/web/lib/realtime/types.ts diff --git a/packages/web/app/lib/routing/index.ts b/packages/web/lib/routing/index.ts similarity index 100% rename from packages/web/app/lib/routing/index.ts rename to packages/web/lib/routing/index.ts diff --git a/packages/web/app/lib/routing/route-params.ts b/packages/web/lib/routing/route-params.ts similarity index 100% rename from packages/web/app/lib/routing/route-params.ts rename to packages/web/lib/routing/route-params.ts diff --git a/packages/web/app/lib/utils/debounce.ts b/packages/web/lib/utils/debounce.ts similarity index 100% rename from packages/web/app/lib/utils/debounce.ts rename to packages/web/lib/utils/debounce.ts diff --git a/packages/web/app/lib/utils/index.ts b/packages/web/lib/utils/index.ts similarity index 100% rename from packages/web/app/lib/utils/index.ts rename to packages/web/lib/utils/index.ts diff --git a/packages/web/app/lib/utils/time-utils.ts b/packages/web/lib/utils/time-utils.ts similarity index 100% rename from packages/web/app/lib/utils/time-utils.ts rename to packages/web/lib/utils/time-utils.ts diff --git a/packages/web/app/lib/utils/utils.ts b/packages/web/lib/utils/utils.ts similarity index 100% rename from packages/web/app/lib/utils/utils.ts rename to packages/web/lib/utils/utils.ts diff --git a/packages/web/app/schemas/bridge.ts b/packages/web/schemas/bridge.ts similarity index 100% rename from packages/web/app/schemas/bridge.ts rename to packages/web/schemas/bridge.ts diff --git a/packages/web/app/schemas/devlog.ts b/packages/web/schemas/devlog.ts similarity index 100% rename from packages/web/app/schemas/devlog.ts rename to packages/web/schemas/devlog.ts diff --git a/packages/web/app/schemas/index.ts b/packages/web/schemas/index.ts similarity index 100% rename from packages/web/app/schemas/index.ts rename to packages/web/schemas/index.ts diff --git a/packages/web/app/schemas/project.ts b/packages/web/schemas/project.ts similarity index 100% rename from packages/web/app/schemas/project.ts rename to packages/web/schemas/project.ts diff --git a/packages/web/app/schemas/responses.ts b/packages/web/schemas/responses.ts similarity index 100% rename from packages/web/app/schemas/responses.ts rename to packages/web/schemas/responses.ts diff --git a/packages/web/app/schemas/validation.ts b/packages/web/schemas/validation.ts similarity index 100% rename from packages/web/app/schemas/validation.ts rename to packages/web/schemas/validation.ts diff --git a/packages/web/app/stores/base.ts b/packages/web/stores/base.ts similarity index 100% rename from packages/web/app/stores/base.ts rename to packages/web/stores/base.ts diff --git a/packages/web/app/stores/devlog-store.ts b/packages/web/stores/devlog-store.ts similarity index 98% rename from packages/web/app/stores/devlog-store.ts rename to packages/web/stores/devlog-store.ts index 67485067..f1cd58b7 100644 --- a/packages/web/app/stores/devlog-store.ts +++ b/packages/web/stores/devlog-store.ts @@ -33,7 +33,7 @@ interface DevlogState { // Devlogs state devlogsContext: TableDataContext; - // Navigation devlogs state (for dropdowns/navigation - separate from main list) + // Navigation devlog state (for dropdowns/navigation - separate from main list) navigationDevlogsContext: DataContext; // Current devlog state (for detail views) @@ -75,7 +75,7 @@ export const useDevlogStore = create()( // Initial state devlogsContext: getDefaultTableDataContext(), - // Navigation devlogs state + // Navigation devlog state navigationDevlogsContext: getDefaultDataContext(), // Selected devlog state @@ -231,11 +231,11 @@ export const useDevlogStore = create()( }, })); - // Fetch recent devlogs for navigation - limit to 50 most recent + // Fetch recent devlog for navigation - limit to 50 most recent const { items: data } = await devlogApiClient.list( {}, // No filters { page: 1, limit: 50 }, // Simple pagination - { sortBy: 'id', sortOrder: 'desc' } // Sort by ID descending + { sortBy: 'id', sortOrder: 'desc' }, // Sort by ID descending ); set((state) => ({ diff --git a/packages/web/app/stores/index.ts b/packages/web/stores/index.ts similarity index 100% rename from packages/web/app/stores/index.ts rename to packages/web/stores/index.ts diff --git a/packages/web/app/stores/layout-store.ts b/packages/web/stores/layout-store.ts similarity index 100% rename from packages/web/app/stores/layout-store.ts rename to packages/web/stores/layout-store.ts diff --git a/packages/web/app/stores/project-store.ts b/packages/web/stores/project-store.ts similarity index 100% rename from packages/web/app/stores/project-store.ts rename to packages/web/stores/project-store.ts diff --git a/packages/web/app/stores/realtime-store.ts b/packages/web/stores/realtime-store.ts similarity index 100% rename from packages/web/app/stores/realtime-store.ts rename to packages/web/stores/realtime-store.ts diff --git a/packages/web/app/styles/base.css b/packages/web/styles/base.css similarity index 100% rename from packages/web/app/styles/base.css rename to packages/web/styles/base.css diff --git a/packages/web/app/fonts.css b/packages/web/styles/fonts.css similarity index 100% rename from packages/web/app/fonts.css rename to packages/web/styles/fonts.css diff --git a/packages/web/app/globals.css b/packages/web/styles/globals.css similarity index 87% rename from packages/web/app/globals.css rename to packages/web/styles/globals.css index 684e0a3a..8bee29fd 100644 --- a/packages/web/app/globals.css +++ b/packages/web/styles/globals.css @@ -3,94 +3,94 @@ @tailwind components; @tailwind utilities; -@import './styles/base.css'; -@import './styles/layout.css'; -@import './styles/responsive.css'; +@import 'base.css'; +@import 'layout.css'; +@import 'responsive.css'; @layer base { :root { --background: 0 0% 100%; --foreground: 0 0% 3.9%; - + --card: 0 0% 100%; --card-foreground: 0 0% 3.9%; - + --popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%; - + --primary: 0 0% 9%; --primary-foreground: 0 0% 98%; - + --secondary: 0 0% 96.1%; --secondary-foreground: 0 0% 9%; - + --muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%; - + --accent: 0 0% 96.1%; --accent-foreground: 0 0% 9%; - + --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; --border: 0 0% 89.8%; --input: 0 0% 89.8%; --ring: 0 0% 3.9%; - + --radius: 0.5rem; - + --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%; - + --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: 0 0% 3.9%; --foreground: 0 0% 98%; - + --card: 0 0% 3.9%; --card-foreground: 0 0% 98%; - + --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%; - + --primary: 0 0% 98%; --primary-foreground: 0 0% 9%; - + --secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%; - + --muted: 0 0% 14.9%; --muted-foreground: 0 0% 63.9%; - + --accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%; - + --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; - + --border: 0 0% 14.9%; --input: 0 0% 14.9%; --ring: 0 0% 83.1%; @@ -109,7 +109,7 @@ --sidebar-ring: 217.2 91.2% 59.8%; } } - + @layer base { * { @apply border-border; @@ -121,5 +121,18 @@ /* Font class for Inter */ .font-inter { - font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -} \ No newline at end of file + font-family: + 'Inter', + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; +} diff --git a/packages/web/app/styles/layout.css b/packages/web/styles/layout.css similarity index 100% rename from packages/web/app/styles/layout.css rename to packages/web/styles/layout.css diff --git a/packages/web/app/styles/responsive.css b/packages/web/styles/responsive.css similarity index 100% rename from packages/web/app/styles/responsive.css rename to packages/web/styles/responsive.css diff --git a/packages/web/tests/lib/api/api-integration.test.ts b/packages/web/tests/lib/api/api-integration.test.ts index e09ee2fc..e3633861 100644 --- a/packages/web/tests/lib/api/api-integration.test.ts +++ b/packages/web/tests/lib/api/api-integration.test.ts @@ -73,7 +73,7 @@ describe.skipIf(!runIntegrationTests)('API Integration Tests', () => { } }); - it('should list devlogs with pagination', async () => { + it('should list devlog with pagination', async () => { const result = await client.get(`/projects/${testProjectId}/devlogs`); expect(result.data).toHaveProperty('items'); expect(result.data).toHaveProperty('pagination'); @@ -104,7 +104,7 @@ describe.skipIf(!runIntegrationTests)('API Integration Tests', () => { expect(result.data).toHaveProperty('error', 'Devlog entry not found'); }); - it('should filter devlogs by status', async () => { + it('should filter devlog by status', async () => { const result = await client.get(`/projects/${testProjectId}/devlogs?status=done`); expect(result.data).toHaveProperty('items'); @@ -114,7 +114,7 @@ describe.skipIf(!runIntegrationTests)('API Integration Tests', () => { }); }); - it('should search devlogs', async () => { + it('should search devlog', async () => { const result = await client.get(`/projects/${testProjectId}/devlogs?search=test`); expect(result.data).toHaveProperty('items'); expect(result.data).toHaveProperty('pagination'); @@ -167,7 +167,7 @@ describe.skipIf(!runIntegrationTests)('API Integration Tests', () => { testDevlogIds = result.data.items.map((item: any) => item.id); }); - it.skipIf(!testDevlogIds?.length)('should batch update devlogs', async () => { + it.skipIf(!testDevlogIds?.length)('should batch update devlog', async () => { if (!testDevlogIds?.length) return; // TypeScript guard const updateData = { ids: testDevlogIds.slice(0, 2), @@ -196,7 +196,7 @@ describe.skipIf(!runIntegrationTests)('API Integration Tests', () => { expect(result.data.error).toContain('ids (array) and updates (object) are required'); }); - it.skipIf(!testDevlogIds?.length)('should batch add notes to devlogs', async () => { + it.skipIf(!testDevlogIds?.length)('should batch add notes to devlog', async () => { if (!testDevlogIds?.length) return; // TypeScript guard const noteData = { ids: testDevlogIds.slice(0, 1), diff --git a/packages/web/tests/lib/api/project-api-client.test.ts b/packages/web/tests/lib/api/project-api-client.test.ts deleted file mode 100644 index 9f2556af..00000000 --- a/packages/web/tests/lib/api/project-api-client.test.ts +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Tests for ProjectApiClient - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import type { Project } from '@codervisor/devlog-core'; - -// Mock the ApiClient first -const mockApiClient = { - get: vi.fn(), - post: vi.fn(), - put: vi.fn(), - delete: vi.fn(), -}; - -const MockApiError = class extends Error { - constructor( - public code: string, - message: string, - public status: number, - public details?: any, - ) { - super(message); - this.name = 'ApiError'; - } - - is(code: string): boolean { - return this.code === code; - } - - isNotFound(): boolean { - return this.code.endsWith('_NOT_FOUND') || this.status === 404; - } - - isValidation(): boolean { - return this.code === 'VALIDATION_FAILED' || this.status === 422; - } - - isClientError(): boolean { - return this.status >= 400 && this.status < 500; - } - - isServerError(): boolean { - return this.status >= 500; - } -}; - -vi.mock('../../../app/lib/api/api-client', () => ({ - ApiClient: vi.fn(() => mockApiClient), - ApiError: MockApiError, -})); - -// Now import the modules that depend on the mocked modules -import { ProjectApiClient, CreateProjectRequest, UpdateProjectRequest } from '../../../app/lib/api/project-api-client'; - -// Create a type alias for our mock error class for tests -const ApiError = MockApiError; - -describe('ProjectApiClient', () => { - let projectClient: ProjectApiClient; - - beforeEach(() => { - vi.clearAllMocks(); - projectClient = new ProjectApiClient(); - }); - - describe('list()', () => { - it('should fetch and return projects list', async () => { - const mockProjects: Project[] = [ - { - id: 1, - name: 'Test Project', - description: 'A test project', - createdAt: new Date('2023-01-01'), - lastAccessedAt: new Date('2023-01-02'), - }, - ]; - - mockApiClient.get.mockResolvedValue(mockProjects); - - const result = await projectClient.list(); - - expect(mockApiClient.get).toHaveBeenCalledWith('/api/projects'); - expect(result).toEqual(mockProjects); - }); - - it('should handle API errors correctly', async () => { - const apiError = new ApiError('SERVER_ERROR', 'Server error', 500); - mockApiClient.get.mockRejectedValue(apiError); - - await expect(projectClient.list()).rejects.toThrow(apiError); - }); - - it('should wrap non-API errors', async () => { - const genericError = new Error('Network error'); - mockApiClient.get.mockRejectedValue(genericError); - - await expect(projectClient.list()).rejects.toThrow( - expect.objectContaining({ - code: 'PROJECT_LIST_FAILED', - message: 'Failed to fetch projects list', - }) - ); - }); - }); - - describe('get()', () => { - it('should fetch a specific project', async () => { - const mockProject: Project = { - id: 1, - name: 'Test Project', - description: 'A test project', - createdAt: new Date('2023-01-01'), - lastAccessedAt: new Date('2023-01-02'), - }; - - mockApiClient.get.mockResolvedValue(mockProject); - - const result = await projectClient.get('test-project'); - - expect(mockApiClient.get).toHaveBeenCalledWith('/api/projects/test-project'); - expect(result).toEqual(mockProject); - }); - - it('should validate project name parameter', async () => { - await expect(projectClient.get('')).rejects.toThrow( - expect.objectContaining({ - code: 'INVALID_PROJECT_NAME', - status: 400, - }) - ); - - await expect(projectClient.get(null as any)).rejects.toThrow( - expect.objectContaining({ - code: 'INVALID_PROJECT_NAME', - status: 400, - }) - ); - }); - - it('should handle project not found', async () => { - const notFoundError = new ApiError('PROJECT_NOT_FOUND', 'Not found', 404); - mockApiClient.get.mockRejectedValue(notFoundError); - - await expect(projectClient.get('nonexistent')).rejects.toThrow( - expect.objectContaining({ - code: 'PROJECT_NOT_FOUND', - message: "Project 'nonexistent' not found", - }) - ); - }); - - it('should encode project names in URLs', async () => { - mockApiClient.get.mockResolvedValue({}); - - await projectClient.get('project with spaces'); - - expect(mockApiClient.get).toHaveBeenCalledWith('/api/projects/project%20with%20spaces'); - }); - }); - - describe('create()', () => { - it('should create a new project', async () => { - const createData: CreateProjectRequest = { - name: 'New Project', - description: 'A new project', - }; - - const mockCreatedProject: Project = { - id: 2, - name: 'New Project', - description: 'A new project', - createdAt: new Date('2023-01-01'), - lastAccessedAt: new Date('2023-01-01'), - }; - - mockApiClient.post.mockResolvedValue(mockCreatedProject); - - const result = await projectClient.create(createData); - - expect(mockApiClient.post).toHaveBeenCalledWith('/api/projects', createData); - expect(result).toEqual(mockCreatedProject); - }); - - it('should validate create data', async () => { - await expect(projectClient.create({} as any)).rejects.toThrow( - expect.objectContaining({ - code: 'INVALID_PROJECT_DATA', - status: 400, - }) - ); - - await expect(projectClient.create({ name: '' })).rejects.toThrow( - expect.objectContaining({ - code: 'INVALID_PROJECT_DATA', - status: 400, - }) - ); - }); - - it('should handle validation errors from API', async () => { - const validationError = new ApiError('VALIDATION_FAILED', 'Invalid name', 422); - mockApiClient.post.mockRejectedValue(validationError); - - await expect(projectClient.create({ name: 'Test' })).rejects.toThrow( - expect.objectContaining({ - code: 'PROJECT_VALIDATION_FAILED', - status: 422, - }) - ); - }); - }); - - describe('update()', () => { - it('should update a project', async () => { - const updateData: UpdateProjectRequest = { - description: 'Updated description', - }; - - const mockUpdatedProject: Project = { - id: 1, - name: 'Test Project', - description: 'Updated description', - createdAt: new Date('2023-01-01'), - lastAccessedAt: new Date('2023-01-02'), - }; - - mockApiClient.put.mockResolvedValue(mockUpdatedProject); - - const result = await projectClient.update('test-project', updateData); - - expect(mockApiClient.put).toHaveBeenCalledWith('/api/projects/test-project', updateData); - expect(result).toEqual(mockUpdatedProject); - }); - - it('should validate update parameters', async () => { - await expect(projectClient.update('', {})).rejects.toThrow( - expect.objectContaining({ - code: 'INVALID_PROJECT_NAME', - status: 400, - }) - ); - - await expect(projectClient.update('test', {})).rejects.toThrow( - expect.objectContaining({ - code: 'INVALID_UPDATE_DATA', - status: 400, - }) - ); - }); - }); - - describe('delete()', () => { - it('should delete a project', async () => { - const mockDeleteResponse = { deleted: true, projectId: 1 }; - mockApiClient.delete.mockResolvedValue(mockDeleteResponse); - - const result = await projectClient.delete('test-project'); - - expect(mockApiClient.delete).toHaveBeenCalledWith('/api/projects/test-project'); - expect(result).toEqual(mockDeleteResponse); - }); - - it('should validate project name', async () => { - await expect(projectClient.delete('')).rejects.toThrow( - expect.objectContaining({ - code: 'INVALID_PROJECT_NAME', - status: 400, - }) - ); - }); - }); - - describe('exists()', () => { - it('should return true when project exists', async () => { - mockApiClient.get.mockResolvedValue({}); - - const result = await projectClient.exists('test-project'); - - expect(result).toBe(true); - }); - - it('should return false when project does not exist', async () => { - const notFoundError = new ApiError('PROJECT_NOT_FOUND', 'Not found', 404); - mockApiClient.get.mockRejectedValue(notFoundError); - - const result = await projectClient.exists('nonexistent'); - - expect(result).toBe(false); - }); - - it('should re-throw non-404 errors', async () => { - const serverError = new ApiError('SERVER_ERROR', 'Server error', 500); - mockApiClient.get.mockRejectedValue(serverError); - - await expect(projectClient.exists('test-project')).rejects.toThrow(serverError); - }); - }); -}); \ No newline at end of file diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json index a45d571c..eddcfb04 100644 --- a/packages/web/tsconfig.json +++ b/packages/web/tsconfig.json @@ -25,7 +25,7 @@ ], "baseUrl": ".", "paths": { - "@/*": ["./app/*"], + "@/*": ["./*"], "@codervisor/devlog-core": ["../core/build"], "@codervisor/devlog-ai": ["../ai/build"] } From 4a75056ac6b18dcf39b90708b5661aa38e9813f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 07:01:44 +0000 Subject: [PATCH 22/50] Initial plan From 20fd206f8024a2483cf91dd69c5d2c453480993b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 07:12:16 +0000 Subject: [PATCH 23/50] Complete file renaming to kebab-case and fix most import issues Co-authored-by: tikazyq <3393101+tikazyq@users.noreply.github.com> --- .../web/app/projects/[name]/devlogs/[id]/page.tsx | 2 +- ...tailsPage.tsx => project-devlog-details-page.tsx} | 6 +++--- packages/web/app/projects/[name]/devlogs/page.tsx | 2 +- ...vlogListPage.tsx => project-devlog-list-page.tsx} | 4 ++-- packages/web/app/projects/[name]/layout.tsx | 4 ++-- packages/web/app/projects/[name]/page.tsx | 2 +- ...ojectDetailsPage.tsx => project-details-page.tsx} | 2 +- packages/web/app/projects/[name]/settings/page.tsx | 2 +- ...ectSettingsPage.tsx => project-settings-page.tsx} | 2 +- packages/web/app/projects/page.tsx | 2 +- .../{ProjectListPage.tsx => project-list-page.tsx} | 0 packages/web/components/common/index.ts | 6 +++--- .../{OverviewStats.tsx => overview-stats.tsx} | 0 .../common/{Pagination.tsx => pagination.tsx} | 0 .../components/common/project-card-skeleton/index.ts | 2 +- ...ectCardSkeleton.tsx => project-card-skeleton.tsx} | 0 .../custom/{DevlogTags.tsx => devlog-tags.tsx} | 0 .../custom/{EditableField.tsx => editable-field.tsx} | 2 +- .../custom/{ErrorBoundary.tsx => error-boundary.tsx} | 0 packages/web/components/custom/index.ts | 12 +++++++----- .../custom/{LoadingPage.tsx => loading-page.tsx} | 0 .../{MarkdownEditor.tsx => markdown-editor.tsx} | 0 .../{MarkdownRenderer.tsx => markdown-renderer.tsx} | 2 +- .../{ProjectNotFound.tsx => project-not-found.tsx} | 0 .../{StickyHeadings.tsx => sticky-headings.tsx} | 0 .../dashboard/{Dashboard.tsx => dashboard.tsx} | 0 packages/web/components/feature/dashboard/index.ts | 2 +- .../{DevlogAnchorNav.tsx => devlog-anchor-nav.tsx} | 0 .../devlog/{DevlogDetails.tsx => devlog-details.tsx} | 9 ++++----- .../devlog/{DevlogList.tsx => devlog-list.tsx} | 0 packages/web/components/feature/devlog/index.ts | 3 +++ .../forms/{DevlogForm.tsx => devlog-form.tsx} | 0 packages/web/components/forms/index.ts | 2 +- packages/web/components/index.ts | 5 +++-- packages/web/components/layout/app-layout.tsx | 4 ++-- packages/web/components/layout/index.ts | 2 +- .../layout/{TopNavbar.tsx => top-navbar.tsx} | 0 .../{ProjectProvider.tsx => project-provider.tsx} | 0 38 files changed, 42 insertions(+), 37 deletions(-) rename packages/web/app/projects/[name]/devlogs/[id]/{ProjectDevlogDetailsPage.tsx => project-devlog-details-page.tsx} (97%) rename packages/web/app/projects/[name]/devlogs/{ProjectDevlogListPage.tsx => project-devlog-list-page.tsx} (95%) rename packages/web/app/projects/[name]/{ProjectDetailsPage.tsx => project-details-page.tsx} (96%) rename packages/web/app/projects/[name]/settings/{ProjectSettingsPage.tsx => project-settings-page.tsx} (99%) rename packages/web/app/projects/{ProjectListPage.tsx => project-list-page.tsx} (100%) rename packages/web/components/common/overview-stats/{OverviewStats.tsx => overview-stats.tsx} (100%) rename packages/web/components/common/{Pagination.tsx => pagination.tsx} (100%) rename packages/web/components/common/project-card-skeleton/{ProjectCardSkeleton.tsx => project-card-skeleton.tsx} (100%) rename packages/web/components/custom/{DevlogTags.tsx => devlog-tags.tsx} (100%) rename packages/web/components/custom/{EditableField.tsx => editable-field.tsx} (99%) rename packages/web/components/custom/{ErrorBoundary.tsx => error-boundary.tsx} (100%) rename packages/web/components/custom/{LoadingPage.tsx => loading-page.tsx} (100%) rename packages/web/components/custom/{MarkdownEditor.tsx => markdown-editor.tsx} (100%) rename packages/web/components/custom/{MarkdownRenderer.tsx => markdown-renderer.tsx} (99%) rename packages/web/components/custom/project/{ProjectNotFound.tsx => project-not-found.tsx} (100%) rename packages/web/components/custom/{StickyHeadings.tsx => sticky-headings.tsx} (100%) rename packages/web/components/feature/dashboard/{Dashboard.tsx => dashboard.tsx} (100%) rename packages/web/components/feature/devlog/{DevlogAnchorNav.tsx => devlog-anchor-nav.tsx} (100%) rename packages/web/components/feature/devlog/{DevlogDetails.tsx => devlog-details.tsx} (98%) rename packages/web/components/feature/devlog/{DevlogList.tsx => devlog-list.tsx} (100%) create mode 100644 packages/web/components/feature/devlog/index.ts rename packages/web/components/forms/{DevlogForm.tsx => devlog-form.tsx} (100%) rename packages/web/components/layout/{TopNavbar.tsx => top-navbar.tsx} (100%) rename packages/web/components/provider/{ProjectProvider.tsx => project-provider.tsx} (100%) diff --git a/packages/web/app/projects/[name]/devlogs/[id]/page.tsx b/packages/web/app/projects/[name]/devlogs/[id]/page.tsx index 112d6fbe..7f76393e 100644 --- a/packages/web/app/projects/[name]/devlogs/[id]/page.tsx +++ b/packages/web/app/projects/[name]/devlogs/[id]/page.tsx @@ -1,4 +1,4 @@ -import { ProjectDevlogDetailsPage } from './ProjectDevlogDetailsPage'; +import { ProjectDevlogDetailsPage } from './project-devlog-details-page'; export default function ProjectDevlogPage() { return ; diff --git a/packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx b/packages/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx similarity index 97% rename from packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx rename to packages/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx index e68a350a..fb995bcd 100644 --- a/packages/web/app/projects/[name]/devlogs/[id]/ProjectDevlogDetailsPage.tsx +++ b/packages/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx @@ -1,16 +1,16 @@ 'use client'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Button, Popover, PopoverContent, PopoverTrigger } from '@/components'; +import { Button, Popover, PopoverContent, PopoverTrigger } from '@/components/ui'; import { useDevlogStore, useProjectStore } from '@/stores'; import { useDevlogEvents, useNoteEvents } from '@/hooks/use-realtime'; import { useRouter } from 'next/navigation'; import { ArrowLeftIcon, SaveIcon, TrashIcon, UndoIcon } from 'lucide-react'; import { toast } from 'sonner'; import { DevlogEntry } from '@codervisor/devlog-core'; -import { useProjectName } from '@/components/provider/ProjectProvider'; +import { useProjectName } from '@/components/provider/project-provider'; import { useDevlogId } from '@/components/provider/devlog-provider'; -import { DevlogDetails } from '@/components/feature/devlog/DevlogDetails'; +import { DevlogDetails } from '@/components/feature/devlog/devlog-details'; export function ProjectDevlogDetailsPage() { const projectName = useProjectName(); diff --git a/packages/web/app/projects/[name]/devlogs/page.tsx b/packages/web/app/projects/[name]/devlogs/page.tsx index deabdc57..9e12052a 100644 --- a/packages/web/app/projects/[name]/devlogs/page.tsx +++ b/packages/web/app/projects/[name]/devlogs/page.tsx @@ -1,4 +1,4 @@ -import { ProjectDevlogListPage } from './ProjectDevlogListPage'; +import { ProjectDevlogListPage } from './project-devlog-list-page'; export default function ProjectDevlogsPage() { return ; diff --git a/packages/web/app/projects/[name]/devlogs/ProjectDevlogListPage.tsx b/packages/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx similarity index 95% rename from packages/web/app/projects/[name]/devlogs/ProjectDevlogListPage.tsx rename to packages/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx index aefe2aab..af984025 100644 --- a/packages/web/app/projects/[name]/devlogs/ProjectDevlogListPage.tsx +++ b/packages/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx @@ -5,8 +5,8 @@ import { useDevlogStore, useProjectStore } from '@/stores'; import { useDevlogEvents } from '@/hooks/use-realtime'; import { DevlogEntry, DevlogId } from '@codervisor/devlog-core'; import { useRouter } from 'next/navigation'; -import { useProjectName } from '@/components/provider/ProjectProvider'; -import { DevlogList } from '@/components/feature/devlog/DevlogList'; +import { useProjectName } from '@/components/provider/project-provider'; +import { DevlogList } from '@/components/feature/devlog/devlog-list'; export function ProjectDevlogListPage() { const projectName = useProjectName(); diff --git a/packages/web/app/projects/[name]/layout.tsx b/packages/web/app/projects/[name]/layout.tsx index 935d2c3a..a9575fa9 100644 --- a/packages/web/app/projects/[name]/layout.tsx +++ b/packages/web/app/projects/[name]/layout.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { ProjectService } from '@codervisor/devlog-core/server'; import { generateSlugFromName } from '@codervisor/devlog-core'; -import { ProjectNotFound } from '@/components/custom/project/ProjectNotFound'; +import { ProjectNotFound } from '@/components/custom/project/project-not-found'; import { redirect } from 'next/navigation'; -import { ProjectProvider } from '@/components/provider/ProjectProvider'; +import { ProjectProvider } from '@/components/provider/project-provider'; interface ProjectLayoutProps { children: React.ReactNode; diff --git a/packages/web/app/projects/[name]/page.tsx b/packages/web/app/projects/[name]/page.tsx index 4bbb13db..6fec4595 100644 --- a/packages/web/app/projects/[name]/page.tsx +++ b/packages/web/app/projects/[name]/page.tsx @@ -1,4 +1,4 @@ -import { ProjectDetailsPage } from './ProjectDetailsPage'; +import { ProjectDetailsPage } from './project-details-page'; export default function ProjectPage() { return ; diff --git a/packages/web/app/projects/[name]/ProjectDetailsPage.tsx b/packages/web/app/projects/[name]/project-details-page.tsx similarity index 96% rename from packages/web/app/projects/[name]/ProjectDetailsPage.tsx rename to packages/web/app/projects/[name]/project-details-page.tsx index ff04e9c2..9a453366 100644 --- a/packages/web/app/projects/[name]/ProjectDetailsPage.tsx +++ b/packages/web/app/projects/[name]/project-details-page.tsx @@ -6,7 +6,7 @@ import { useDevlogStore, useProjectStore } from '@/stores'; import { useDevlogEvents } from '@/hooks/use-realtime'; import { DevlogEntry } from '@codervisor/devlog-core'; import { useRouter } from 'next/navigation'; -import { useProjectName } from '@/components/provider/ProjectProvider'; +import { useProjectName } from '@/components/provider/project-provider'; export function ProjectDetailsPage() { const projectName = useProjectName(); diff --git a/packages/web/app/projects/[name]/settings/page.tsx b/packages/web/app/projects/[name]/settings/page.tsx index 4f446705..5c4271e5 100644 --- a/packages/web/app/projects/[name]/settings/page.tsx +++ b/packages/web/app/projects/[name]/settings/page.tsx @@ -1,4 +1,4 @@ -import { ProjectSettingsPage } from './ProjectSettingsPage'; +import { ProjectSettingsPage } from './project-settings-page'; // Disable static generation for this page since it uses client-side feature export const dynamic = 'force-dynamic'; diff --git a/packages/web/app/projects/[name]/settings/ProjectSettingsPage.tsx b/packages/web/app/projects/[name]/settings/project-settings-page.tsx similarity index 99% rename from packages/web/app/projects/[name]/settings/ProjectSettingsPage.tsx rename to packages/web/app/projects/[name]/settings/project-settings-page.tsx index e64c2969..3ec09783 100644 --- a/packages/web/app/projects/[name]/settings/ProjectSettingsPage.tsx +++ b/packages/web/app/projects/[name]/settings/project-settings-page.tsx @@ -25,7 +25,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { LoaderIcon, SaveIcon, TrashIcon, AlertTriangleIcon } from 'lucide-react'; import { toast } from 'sonner'; import { Project } from '@codervisor/devlog-core'; -import { useProjectName } from '@/components/provider/ProjectProvider'; +import { useProjectName } from '@/components/provider/project-provider'; interface ProjectFormData { name: string; diff --git a/packages/web/app/projects/page.tsx b/packages/web/app/projects/page.tsx index ac4dfdc3..8f99e993 100644 --- a/packages/web/app/projects/page.tsx +++ b/packages/web/app/projects/page.tsx @@ -1,4 +1,4 @@ -import { ProjectListPage } from './ProjectListPage'; +import { ProjectListPage } from './project-list-page'; export const dynamic = 'force-dynamic'; diff --git a/packages/web/app/projects/ProjectListPage.tsx b/packages/web/app/projects/project-list-page.tsx similarity index 100% rename from packages/web/app/projects/ProjectListPage.tsx rename to packages/web/app/projects/project-list-page.tsx diff --git a/packages/web/components/common/index.ts b/packages/web/components/common/index.ts index ae19f22b..d82e96ea 100644 --- a/packages/web/components/common/index.ts +++ b/packages/web/components/common/index.ts @@ -1,4 +1,4 @@ // Common Components -export * from '@/components/common/overview-stats/OverviewStats'; -export * from '@/components/common/project-card-skeleton'; -export { Pagination } from './Pagination'; +export { OverviewStats, type OverviewStatsVariant } from './overview-stats/overview-stats'; +export * from './project-card-skeleton'; +export { Pagination } from './pagination'; diff --git a/packages/web/components/common/overview-stats/OverviewStats.tsx b/packages/web/components/common/overview-stats/overview-stats.tsx similarity index 100% rename from packages/web/components/common/overview-stats/OverviewStats.tsx rename to packages/web/components/common/overview-stats/overview-stats.tsx diff --git a/packages/web/components/common/Pagination.tsx b/packages/web/components/common/pagination.tsx similarity index 100% rename from packages/web/components/common/Pagination.tsx rename to packages/web/components/common/pagination.tsx diff --git a/packages/web/components/common/project-card-skeleton/index.ts b/packages/web/components/common/project-card-skeleton/index.ts index bb8ce7ee..1a9e411f 100644 --- a/packages/web/components/common/project-card-skeleton/index.ts +++ b/packages/web/components/common/project-card-skeleton/index.ts @@ -1 +1 @@ -export { ProjectCardSkeleton, ProjectGridSkeleton } from './ProjectCardSkeleton'; +export { ProjectCardSkeleton, ProjectGridSkeleton } from './project-card-skeleton'; diff --git a/packages/web/components/common/project-card-skeleton/ProjectCardSkeleton.tsx b/packages/web/components/common/project-card-skeleton/project-card-skeleton.tsx similarity index 100% rename from packages/web/components/common/project-card-skeleton/ProjectCardSkeleton.tsx rename to packages/web/components/common/project-card-skeleton/project-card-skeleton.tsx diff --git a/packages/web/components/custom/DevlogTags.tsx b/packages/web/components/custom/devlog-tags.tsx similarity index 100% rename from packages/web/components/custom/DevlogTags.tsx rename to packages/web/components/custom/devlog-tags.tsx diff --git a/packages/web/components/custom/EditableField.tsx b/packages/web/components/custom/editable-field.tsx similarity index 99% rename from packages/web/components/custom/EditableField.tsx rename to packages/web/components/custom/editable-field.tsx index 2a58b41d..d21fa0dc 100644 --- a/packages/web/components/custom/EditableField.tsx +++ b/packages/web/components/custom/editable-field.tsx @@ -11,7 +11,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Edit2 } from 'lucide-react'; -import { MarkdownEditor } from './MarkdownEditor'; +import { MarkdownEditor } from './markdown-editor'; import { cn } from '@/lib'; interface EditableFieldProps { diff --git a/packages/web/components/custom/ErrorBoundary.tsx b/packages/web/components/custom/error-boundary.tsx similarity index 100% rename from packages/web/components/custom/ErrorBoundary.tsx rename to packages/web/components/custom/error-boundary.tsx diff --git a/packages/web/components/custom/index.ts b/packages/web/components/custom/index.ts index bd138903..f3ae042d 100644 --- a/packages/web/components/custom/index.ts +++ b/packages/web/components/custom/index.ts @@ -1,5 +1,7 @@ -export { LoadingPage } from './LoadingPage'; -export { MarkdownRenderer } from './MarkdownRenderer'; -export { EditableField } from './EditableField'; -export { DevlogStatusTag, DevlogPriorityTag, DevlogTypeTag } from './DevlogTags'; -export { StickyHeadings } from './StickyHeadings'; +export { LoadingPage } from './loading-page'; +export { ErrorBoundary } from './error-boundary'; +export { MarkdownRenderer } from './markdown-renderer'; +export { EditableField } from './editable-field'; +export { DevlogStatusTag, DevlogPriorityTag, DevlogTypeTag } from './devlog-tags'; +export { StickyHeadings } from './sticky-headings'; +export { MarkdownEditor } from './markdown-editor'; diff --git a/packages/web/components/custom/LoadingPage.tsx b/packages/web/components/custom/loading-page.tsx similarity index 100% rename from packages/web/components/custom/LoadingPage.tsx rename to packages/web/components/custom/loading-page.tsx diff --git a/packages/web/components/custom/MarkdownEditor.tsx b/packages/web/components/custom/markdown-editor.tsx similarity index 100% rename from packages/web/components/custom/MarkdownEditor.tsx rename to packages/web/components/custom/markdown-editor.tsx diff --git a/packages/web/components/custom/MarkdownRenderer.tsx b/packages/web/components/custom/markdown-renderer.tsx similarity index 99% rename from packages/web/components/custom/MarkdownRenderer.tsx rename to packages/web/components/custom/markdown-renderer.tsx index 1506442c..9ea796e8 100644 --- a/packages/web/components/custom/MarkdownRenderer.tsx +++ b/packages/web/components/custom/markdown-renderer.tsx @@ -5,7 +5,7 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeHighlight from 'rehype-highlight'; import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; -import { StickyHeadings } from './StickyHeadings'; +import { StickyHeadings } from './sticky-headings'; import { cn } from '@/lib'; // Import highlight.js CSS theme diff --git a/packages/web/components/custom/project/ProjectNotFound.tsx b/packages/web/components/custom/project/project-not-found.tsx similarity index 100% rename from packages/web/components/custom/project/ProjectNotFound.tsx rename to packages/web/components/custom/project/project-not-found.tsx diff --git a/packages/web/components/custom/StickyHeadings.tsx b/packages/web/components/custom/sticky-headings.tsx similarity index 100% rename from packages/web/components/custom/StickyHeadings.tsx rename to packages/web/components/custom/sticky-headings.tsx diff --git a/packages/web/components/feature/dashboard/Dashboard.tsx b/packages/web/components/feature/dashboard/dashboard.tsx similarity index 100% rename from packages/web/components/feature/dashboard/Dashboard.tsx rename to packages/web/components/feature/dashboard/dashboard.tsx diff --git a/packages/web/components/feature/dashboard/index.ts b/packages/web/components/feature/dashboard/index.ts index 41a54b59..0fd1ab25 100644 --- a/packages/web/components/feature/dashboard/index.ts +++ b/packages/web/components/feature/dashboard/index.ts @@ -1 +1 @@ -export { Dashboard } from './Dashboard'; +export { Dashboard } from './dashboard'; diff --git a/packages/web/components/feature/devlog/DevlogAnchorNav.tsx b/packages/web/components/feature/devlog/devlog-anchor-nav.tsx similarity index 100% rename from packages/web/components/feature/devlog/DevlogAnchorNav.tsx rename to packages/web/components/feature/devlog/devlog-anchor-nav.tsx diff --git a/packages/web/components/feature/devlog/DevlogDetails.tsx b/packages/web/components/feature/devlog/devlog-details.tsx similarity index 98% rename from packages/web/components/feature/devlog/DevlogDetails.tsx rename to packages/web/components/feature/devlog/devlog-details.tsx index a04cbe08..e2a8a768 100644 --- a/packages/web/components/feature/devlog/DevlogDetails.tsx +++ b/packages/web/components/feature/devlog/devlog-details.tsx @@ -16,8 +16,8 @@ import { Wrench, } from 'lucide-react'; import { DevlogEntry, DevlogNote, DevlogNoteCategory } from '@codervisor/devlog-core'; -import { EditableField } from '@/components/custom/EditableField'; -import { MarkdownRenderer } from '@/components/custom/MarkdownRenderer'; +import { EditableField } from '@/components/custom/editable-field'; +import { MarkdownRenderer } from '@/components/custom/markdown-renderer'; import { cn, formatTimeAgoWithTooltip, @@ -26,14 +26,13 @@ import { statusOptions, typeOptions, } from '@/lib'; +import { Alert, AlertDescription } from '@/components/ui'; import { - Alert, - AlertDescription, DevlogPriorityTag, DevlogStatusTag, DevlogTypeTag, } from '@/components'; -import { DevlogAnchorNav } from './DevlogAnchorNav'; +import { DevlogAnchorNav } from './devlog-anchor-nav'; import { DataContext } from '@/stores/base'; interface DevlogDetailsProps { diff --git a/packages/web/components/feature/devlog/DevlogList.tsx b/packages/web/components/feature/devlog/devlog-list.tsx similarity index 100% rename from packages/web/components/feature/devlog/DevlogList.tsx rename to packages/web/components/feature/devlog/devlog-list.tsx diff --git a/packages/web/components/feature/devlog/index.ts b/packages/web/components/feature/devlog/index.ts new file mode 100644 index 00000000..adc9a10f --- /dev/null +++ b/packages/web/components/feature/devlog/index.ts @@ -0,0 +1,3 @@ +export { DevlogDetails } from './devlog-details'; +export { DevlogList } from './devlog-list'; +export { DevlogAnchorNav } from './devlog-anchor-nav'; \ No newline at end of file diff --git a/packages/web/components/forms/DevlogForm.tsx b/packages/web/components/forms/devlog-form.tsx similarity index 100% rename from packages/web/components/forms/DevlogForm.tsx rename to packages/web/components/forms/devlog-form.tsx diff --git a/packages/web/components/forms/index.ts b/packages/web/components/forms/index.ts index 76ae4404..32ed5ddd 100644 --- a/packages/web/components/forms/index.ts +++ b/packages/web/components/forms/index.ts @@ -1 +1 @@ -export { DevlogForm } from './DevlogForm'; +export { DevlogForm } from './devlog-form'; diff --git a/packages/web/components/index.ts b/packages/web/components/index.ts index 8a7b648d..08e49195 100644 --- a/packages/web/components/index.ts +++ b/packages/web/components/index.ts @@ -14,8 +14,9 @@ export * from './custom'; export * from './forms'; // Feature Components -export * from '@/components/feature/dashboard'; +export * from './feature/dashboard'; +export * from './feature/devlog'; // Project Components // Note: ProjectResolver is not exported as it's only used server-side in layout.tsx -export { ProjectNotFound } from './custom/project/ProjectNotFound'; +export { ProjectNotFound } from './custom/project/project-not-found'; diff --git a/packages/web/components/layout/app-layout.tsx b/packages/web/components/layout/app-layout.tsx index f2c956f0..dfd5d7d1 100644 --- a/packages/web/components/layout/app-layout.tsx +++ b/packages/web/components/layout/app-layout.tsx @@ -4,8 +4,8 @@ import React, { useEffect, useState } from 'react'; import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; import { Toaster } from 'sonner'; import { AppLayoutSkeleton } from '@/components/layout/app-layout-skeleton'; -import { ErrorBoundary } from '@/components/custom/ErrorBoundary'; -import { TopNavbar } from '@/components/layout/TopNavbar'; +import { ErrorBoundary } from '@/components/custom/error-boundary'; +import { TopNavbar } from '@/components/layout/top-navbar'; import { NavigationSidebar } from '@/components/layout/navigation-sidebar'; interface AppLayoutProps { diff --git a/packages/web/components/layout/index.ts b/packages/web/components/layout/index.ts index 2b2a666a..0942d5da 100644 --- a/packages/web/components/layout/index.ts +++ b/packages/web/components/layout/index.ts @@ -1,4 +1,4 @@ // export { NavigationSidebar } from './navigation-sidebar'; // export { NavigationBreadcrumb } from './navigation-breadcrumb'; // export { AppLayoutSkeleton } from './app-layout-skeleton'; -// export * from './TopNavbar'; +// export * from './top-navbar'; diff --git a/packages/web/components/layout/TopNavbar.tsx b/packages/web/components/layout/top-navbar.tsx similarity index 100% rename from packages/web/components/layout/TopNavbar.tsx rename to packages/web/components/layout/top-navbar.tsx diff --git a/packages/web/components/provider/ProjectProvider.tsx b/packages/web/components/provider/project-provider.tsx similarity index 100% rename from packages/web/components/provider/ProjectProvider.tsx rename to packages/web/components/provider/project-provider.tsx From d3380bed10e0703d44b6b678df465351a811c4e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 Aug 2025 07:18:35 +0000 Subject: [PATCH 24/50] Final fixes: Resolve remaining import issues and ensure successful build Co-authored-by: tikazyq <3393101+tikazyq@users.noreply.github.com> --- packages/web/app/projects/[name]/project-details-page.tsx | 2 +- packages/web/components/feature/dashboard/dashboard.tsx | 2 +- packages/web/components/feature/devlog/devlog-details.tsx | 2 +- packages/web/components/feature/devlog/devlog-list.tsx | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/web/app/projects/[name]/project-details-page.tsx b/packages/web/app/projects/[name]/project-details-page.tsx index 9a453366..7c384038 100644 --- a/packages/web/app/projects/[name]/project-details-page.tsx +++ b/packages/web/app/projects/[name]/project-details-page.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useEffect } from 'react'; -import { Dashboard } from '@/components'; +import { Dashboard } from '@/components/feature/dashboard/dashboard'; import { useDevlogStore, useProjectStore } from '@/stores'; import { useDevlogEvents } from '@/hooks/use-realtime'; import { DevlogEntry } from '@codervisor/devlog-core'; diff --git a/packages/web/components/feature/dashboard/dashboard.tsx b/packages/web/components/feature/dashboard/dashboard.tsx index a8d2087c..e1be128f 100644 --- a/packages/web/components/feature/dashboard/dashboard.tsx +++ b/packages/web/components/feature/dashboard/dashboard.tsx @@ -20,7 +20,7 @@ import { import { DevlogEntry, DevlogStats, TimeSeriesStats } from '@codervisor/devlog-core'; import { useRouter } from 'next/navigation'; import { formatTimeAgoWithTooltip, getStatusChartColor } from '@/lib'; -import { DevlogPriorityTag, DevlogStatusTag, DevlogTypeTag } from '@/components'; +import { DevlogPriorityTag, DevlogStatusTag, DevlogTypeTag } from '@/components/custom/devlog-tags'; import { CHART_COLORS, CHART_OPACITY, diff --git a/packages/web/components/feature/devlog/devlog-details.tsx b/packages/web/components/feature/devlog/devlog-details.tsx index e2a8a768..c0af6823 100644 --- a/packages/web/components/feature/devlog/devlog-details.tsx +++ b/packages/web/components/feature/devlog/devlog-details.tsx @@ -31,7 +31,7 @@ import { DevlogPriorityTag, DevlogStatusTag, DevlogTypeTag, -} from '@/components'; +} from '@/components/custom/devlog-tags'; import { DevlogAnchorNav } from './devlog-anchor-nav'; import { DataContext } from '@/stores/base'; diff --git a/packages/web/components/feature/devlog/devlog-list.tsx b/packages/web/components/feature/devlog/devlog-list.tsx index 7065a285..4c4c56cc 100644 --- a/packages/web/components/feature/devlog/devlog-list.tsx +++ b/packages/web/components/feature/devlog/devlog-list.tsx @@ -43,7 +43,8 @@ import { import { toast } from 'sonner'; import { Edit, Eye, Search, Trash2, X } from 'lucide-react'; import { DevlogEntry, DevlogFilter, DevlogId, DevlogNoteCategory } from '@codervisor/devlog-core'; -import { DevlogPriorityTag, DevlogStatusTag, DevlogTypeTag, Pagination } from '@/components'; +import { DevlogPriorityTag, DevlogStatusTag, DevlogTypeTag } from '@/components/custom/devlog-tags'; +import { Pagination } from '@/components/common/pagination'; import { cn, debounce, From 811d12f2d3760a443629eebbf101e5a957182d45 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 20:51:05 +0800 Subject: [PATCH 25/50] fix: update init-db.sql path in docker-compose configuration --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a4eae890..17e051d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: POSTGRES_PASSWORD: postgres volumes: - postgres_data:/var/lib/postgresql/data - - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro + - ./scripts/database/init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro ports: - "5432:5432" healthcheck: From e94e87cde3bc86f535d87d82e7c11e2068e20697 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 21:09:45 +0800 Subject: [PATCH 26/50] refactor: rename files to simplify paths and improve organization --- {packages => apps}/web/README.md | 0 .../web/app/api/events/route.ts | 0 .../web/app/api/health/route.ts | 0 .../[devlogId]/notes/[noteId]/route.ts | 0 .../[name]/devlogs/[devlogId]/notes/route.ts | 0 .../[name]/devlogs/[devlogId]/route.ts | 0 .../app/api/projects/[name]/devlogs/route.ts | 0 .../projects/[name]/devlogs/search/route.ts | 0 .../[name]/devlogs/stats/overview/route.ts | 0 .../[name]/devlogs/stats/timeseries/route.ts | 0 .../web/app/api/projects/[name]/route.ts | 0 .../web/app/api/projects/route.ts | 0 .../web/app/api/realtime/config/route.ts | 0 {packages => apps}/web/app/layout.tsx | 0 {packages => apps}/web/app/not-found.tsx | 0 {packages => apps}/web/app/page.tsx | 0 .../projects/[name]/devlogs/[id]/layout.tsx | 0 .../app/projects/[name]/devlogs/[id]/page.tsx | 0 .../[id]/project-devlog-details-page.tsx | 0 .../web/app/projects/[name]/devlogs/page.tsx | 0 .../devlogs/project-devlog-list-page.tsx | 0 .../web/app/projects/[name]/layout.tsx | 0 .../web/app/projects/[name]/page.tsx | 0 .../projects/[name]/project-details-page.tsx | 0 .../web/app/projects/[name]/settings/page.tsx | 0 .../[name]/settings/project-settings-page.tsx | 0 {packages => apps}/web/app/projects/page.tsx | 0 .../web/app/projects/project-list-page.tsx | 0 {packages => apps}/web/components.json | 0 .../web/components/common/index.ts | 0 .../common/overview-stats/overview-stats.tsx | 0 .../web/components/common/pagination.tsx | 0 .../common/project-card-skeleton/index.ts | 0 .../project-card-skeleton.tsx | 0 .../web/components/custom/devlog-tags.tsx | 0 .../web/components/custom/editable-field.tsx | 0 .../web/components/custom/error-boundary.tsx | 0 .../web/components/custom/index.ts | 0 .../web/components/custom/loading-page.tsx | 0 .../web/components/custom/markdown-editor.tsx | 0 .../components/custom/markdown-renderer.tsx | 0 .../custom/project/project-not-found.tsx | 0 .../web/components/custom/sticky-headings.tsx | 0 .../feature/dashboard/chart-utils.ts | 0 .../feature/dashboard/dashboard.tsx | 0 .../web/components/feature/dashboard/index.ts | 0 .../feature/devlog/devlog-anchor-nav.tsx | 0 .../feature/devlog/devlog-details.tsx | 0 .../components/feature/devlog/devlog-list.tsx | 0 .../web/components/feature/devlog/index.ts | 0 .../web/components/forms/devlog-form.tsx | 0 .../web/components/forms/index.ts | 0 {packages => apps}/web/components/index.ts | 0 .../components/layout/app-layout-skeleton.tsx | 0 .../web/components/layout/app-layout.tsx | 0 .../web/components/layout/index.ts | 0 .../layout/navigation-breadcrumb.tsx | 0 .../components/layout/navigation-sidebar.tsx | 0 .../web/components/layout/top-navbar.tsx | 0 .../web/components/provider/app-providers.tsx | 0 .../components/provider/devlog-provider.tsx | 0 .../components/provider/project-provider.tsx | 0 .../components/provider/store-provider.tsx | 0 .../components/provider/theme-provider.tsx | 0 .../components/realtime/realtime-status.tsx | 0 .../web/components/ui/accordion.tsx | 0 .../web/components/ui/alert-dialog.tsx | 0 .../web/components/ui/alert.tsx | 0 .../web/components/ui/badge.tsx | 0 .../web/components/ui/breadcrumb.tsx | 0 .../web/components/ui/button.tsx | 0 {packages => apps}/web/components/ui/card.tsx | 0 .../web/components/ui/checkbox.tsx | 0 .../web/components/ui/command.tsx | 0 .../web/components/ui/dialog.tsx | 0 .../web/components/ui/dropdown-menu.tsx | 0 {packages => apps}/web/components/ui/form.tsx | 0 {packages => apps}/web/components/ui/index.ts | 0 .../web/components/ui/input.tsx | 0 .../web/components/ui/label.tsx | 0 .../web/components/ui/navigation-menu.tsx | 0 .../web/components/ui/popover.tsx | 0 .../web/components/ui/progress.tsx | 0 .../web/components/ui/select.tsx | 0 .../web/components/ui/separator.tsx | 0 .../web/components/ui/sheet.tsx | 0 .../web/components/ui/sidebar.tsx | 0 .../web/components/ui/skeleton.tsx | 0 .../web/components/ui/switch.tsx | 0 .../web/components/ui/table.tsx | 0 {packages => apps}/web/components/ui/tabs.tsx | 0 .../web/components/ui/textarea.tsx | 0 .../web/components/ui/theme-toggle.tsx | 0 .../web/components/ui/tooltip.tsx | 0 {packages => apps}/web/hooks/index.ts | 0 {packages => apps}/web/hooks/use-mobile.tsx | 0 {packages => apps}/web/hooks/use-realtime.ts | 0 {packages => apps}/web/lib/api/api-client.ts | 0 {packages => apps}/web/lib/api/api-utils.ts | 0 .../web/lib/api/devlog-api-client.ts | 0 {packages => apps}/web/lib/api/index.ts | 0 .../web/lib/api/project-api-client.ts | 0 .../web/lib/api/server-realtime.ts | 0 .../web/lib/devlog/devlog-options.ts | 0 .../web/lib/devlog/devlog-ui-utils.tsx | 0 {packages => apps}/web/lib/devlog/index.ts | 0 .../web/lib/devlog/note-utils.tsx | 0 {packages => apps}/web/lib/index.ts | 0 {packages => apps}/web/lib/project-urls.ts | 0 {packages => apps}/web/lib/realtime/config.ts | 0 {packages => apps}/web/lib/realtime/index.ts | 0 .../web/lib/realtime/pusher-provider.ts | 0 .../web/lib/realtime/realtime-service.ts | 0 .../web/lib/realtime/sse-provider.ts | 0 {packages => apps}/web/lib/realtime/types.ts | 0 {packages => apps}/web/lib/routing/index.ts | 0 .../web/lib/routing/route-params.ts | 0 {packages => apps}/web/lib/utils/debounce.ts | 0 {packages => apps}/web/lib/utils/index.ts | 0 .../web/lib/utils/time-utils.ts | 0 {packages => apps}/web/lib/utils/utils.ts | 0 {packages => apps}/web/next-env.d.ts | 0 {packages => apps}/web/next.config.js | 0 {packages => apps}/web/package.json | 0 {packages => apps}/web/postcss.config.mjs | 0 .../web/public/devlog-logo-text.svg | 0 {packages => apps}/web/public/devlog-logo.svg | 0 .../web/public/inter-medium.woff2 | 0 .../web/public/inter-regular.woff2 | 0 .../web/public/inter-semibold.woff2 | 0 {packages => apps}/web/schemas/bridge.ts | 0 {packages => apps}/web/schemas/devlog.ts | 0 {packages => apps}/web/schemas/index.ts | 0 {packages => apps}/web/schemas/project.ts | 0 {packages => apps}/web/schemas/responses.ts | 0 {packages => apps}/web/schemas/validation.ts | 0 {packages => apps}/web/stores/base.ts | 0 {packages => apps}/web/stores/devlog-store.ts | 0 {packages => apps}/web/stores/index.ts | 0 {packages => apps}/web/stores/layout-store.ts | 0 .../web/stores/project-store.ts | 0 .../web/stores/realtime-store.ts | 0 {packages => apps}/web/styles/base.css | 0 {packages => apps}/web/styles/fonts.css | 0 {packages => apps}/web/styles/globals.css | 0 {packages => apps}/web/styles/layout.css | 0 {packages => apps}/web/styles/responsive.css | 0 {packages => apps}/web/tailwind.config.js | 0 {packages => apps}/web/tests/README.md | 0 .../web/tests/lib/api/api-integration.test.ts | 0 {packages => apps}/web/tests/setup.ts | 0 .../web/tests/utils/test-server.ts | 0 {packages => apps}/web/tsconfig.json | 0 {packages => apps}/web/vercel.json | 0 {packages => apps}/web/vitest.config.ts | 0 packages/web/NAMING_CONVENTIONS.md | 95 ------- packages/web/REALTIME.md | 233 ------------------ packages/web/ROUTING.md | 133 ---------- 158 files changed, 461 deletions(-) rename {packages => apps}/web/README.md (100%) rename {packages => apps}/web/app/api/events/route.ts (100%) rename {packages => apps}/web/app/api/health/route.ts (100%) rename {packages => apps}/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts (100%) rename {packages => apps}/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts (100%) rename {packages => apps}/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts (100%) rename {packages => apps}/web/app/api/projects/[name]/devlogs/route.ts (100%) rename {packages => apps}/web/app/api/projects/[name]/devlogs/search/route.ts (100%) rename {packages => apps}/web/app/api/projects/[name]/devlogs/stats/overview/route.ts (100%) rename {packages => apps}/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts (100%) rename {packages => apps}/web/app/api/projects/[name]/route.ts (100%) rename {packages => apps}/web/app/api/projects/route.ts (100%) rename {packages => apps}/web/app/api/realtime/config/route.ts (100%) rename {packages => apps}/web/app/layout.tsx (100%) rename {packages => apps}/web/app/not-found.tsx (100%) rename {packages => apps}/web/app/page.tsx (100%) rename {packages => apps}/web/app/projects/[name]/devlogs/[id]/layout.tsx (100%) rename {packages => apps}/web/app/projects/[name]/devlogs/[id]/page.tsx (100%) rename {packages => apps}/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx (100%) rename {packages => apps}/web/app/projects/[name]/devlogs/page.tsx (100%) rename {packages => apps}/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx (100%) rename {packages => apps}/web/app/projects/[name]/layout.tsx (100%) rename {packages => apps}/web/app/projects/[name]/page.tsx (100%) rename {packages => apps}/web/app/projects/[name]/project-details-page.tsx (100%) rename {packages => apps}/web/app/projects/[name]/settings/page.tsx (100%) rename {packages => apps}/web/app/projects/[name]/settings/project-settings-page.tsx (100%) rename {packages => apps}/web/app/projects/page.tsx (100%) rename {packages => apps}/web/app/projects/project-list-page.tsx (100%) rename {packages => apps}/web/components.json (100%) rename {packages => apps}/web/components/common/index.ts (100%) rename {packages => apps}/web/components/common/overview-stats/overview-stats.tsx (100%) rename {packages => apps}/web/components/common/pagination.tsx (100%) rename {packages => apps}/web/components/common/project-card-skeleton/index.ts (100%) rename {packages => apps}/web/components/common/project-card-skeleton/project-card-skeleton.tsx (100%) rename {packages => apps}/web/components/custom/devlog-tags.tsx (100%) rename {packages => apps}/web/components/custom/editable-field.tsx (100%) rename {packages => apps}/web/components/custom/error-boundary.tsx (100%) rename {packages => apps}/web/components/custom/index.ts (100%) rename {packages => apps}/web/components/custom/loading-page.tsx (100%) rename {packages => apps}/web/components/custom/markdown-editor.tsx (100%) rename {packages => apps}/web/components/custom/markdown-renderer.tsx (100%) rename {packages => apps}/web/components/custom/project/project-not-found.tsx (100%) rename {packages => apps}/web/components/custom/sticky-headings.tsx (100%) rename {packages => apps}/web/components/feature/dashboard/chart-utils.ts (100%) rename {packages => apps}/web/components/feature/dashboard/dashboard.tsx (100%) rename {packages => apps}/web/components/feature/dashboard/index.ts (100%) rename {packages => apps}/web/components/feature/devlog/devlog-anchor-nav.tsx (100%) rename {packages => apps}/web/components/feature/devlog/devlog-details.tsx (100%) rename {packages => apps}/web/components/feature/devlog/devlog-list.tsx (100%) rename {packages => apps}/web/components/feature/devlog/index.ts (100%) rename {packages => apps}/web/components/forms/devlog-form.tsx (100%) rename {packages => apps}/web/components/forms/index.ts (100%) rename {packages => apps}/web/components/index.ts (100%) rename {packages => apps}/web/components/layout/app-layout-skeleton.tsx (100%) rename {packages => apps}/web/components/layout/app-layout.tsx (100%) rename {packages => apps}/web/components/layout/index.ts (100%) rename {packages => apps}/web/components/layout/navigation-breadcrumb.tsx (100%) rename {packages => apps}/web/components/layout/navigation-sidebar.tsx (100%) rename {packages => apps}/web/components/layout/top-navbar.tsx (100%) rename {packages => apps}/web/components/provider/app-providers.tsx (100%) rename {packages => apps}/web/components/provider/devlog-provider.tsx (100%) rename {packages => apps}/web/components/provider/project-provider.tsx (100%) rename {packages => apps}/web/components/provider/store-provider.tsx (100%) rename {packages => apps}/web/components/provider/theme-provider.tsx (100%) rename {packages => apps}/web/components/realtime/realtime-status.tsx (100%) rename {packages => apps}/web/components/ui/accordion.tsx (100%) rename {packages => apps}/web/components/ui/alert-dialog.tsx (100%) rename {packages => apps}/web/components/ui/alert.tsx (100%) rename {packages => apps}/web/components/ui/badge.tsx (100%) rename {packages => apps}/web/components/ui/breadcrumb.tsx (100%) rename {packages => apps}/web/components/ui/button.tsx (100%) rename {packages => apps}/web/components/ui/card.tsx (100%) rename {packages => apps}/web/components/ui/checkbox.tsx (100%) rename {packages => apps}/web/components/ui/command.tsx (100%) rename {packages => apps}/web/components/ui/dialog.tsx (100%) rename {packages => apps}/web/components/ui/dropdown-menu.tsx (100%) rename {packages => apps}/web/components/ui/form.tsx (100%) rename {packages => apps}/web/components/ui/index.ts (100%) rename {packages => apps}/web/components/ui/input.tsx (100%) rename {packages => apps}/web/components/ui/label.tsx (100%) rename {packages => apps}/web/components/ui/navigation-menu.tsx (100%) rename {packages => apps}/web/components/ui/popover.tsx (100%) rename {packages => apps}/web/components/ui/progress.tsx (100%) rename {packages => apps}/web/components/ui/select.tsx (100%) rename {packages => apps}/web/components/ui/separator.tsx (100%) rename {packages => apps}/web/components/ui/sheet.tsx (100%) rename {packages => apps}/web/components/ui/sidebar.tsx (100%) rename {packages => apps}/web/components/ui/skeleton.tsx (100%) rename {packages => apps}/web/components/ui/switch.tsx (100%) rename {packages => apps}/web/components/ui/table.tsx (100%) rename {packages => apps}/web/components/ui/tabs.tsx (100%) rename {packages => apps}/web/components/ui/textarea.tsx (100%) rename {packages => apps}/web/components/ui/theme-toggle.tsx (100%) rename {packages => apps}/web/components/ui/tooltip.tsx (100%) rename {packages => apps}/web/hooks/index.ts (100%) rename {packages => apps}/web/hooks/use-mobile.tsx (100%) rename {packages => apps}/web/hooks/use-realtime.ts (100%) rename {packages => apps}/web/lib/api/api-client.ts (100%) rename {packages => apps}/web/lib/api/api-utils.ts (100%) rename {packages => apps}/web/lib/api/devlog-api-client.ts (100%) rename {packages => apps}/web/lib/api/index.ts (100%) rename {packages => apps}/web/lib/api/project-api-client.ts (100%) rename {packages => apps}/web/lib/api/server-realtime.ts (100%) rename {packages => apps}/web/lib/devlog/devlog-options.ts (100%) rename {packages => apps}/web/lib/devlog/devlog-ui-utils.tsx (100%) rename {packages => apps}/web/lib/devlog/index.ts (100%) rename {packages => apps}/web/lib/devlog/note-utils.tsx (100%) rename {packages => apps}/web/lib/index.ts (100%) rename {packages => apps}/web/lib/project-urls.ts (100%) rename {packages => apps}/web/lib/realtime/config.ts (100%) rename {packages => apps}/web/lib/realtime/index.ts (100%) rename {packages => apps}/web/lib/realtime/pusher-provider.ts (100%) rename {packages => apps}/web/lib/realtime/realtime-service.ts (100%) rename {packages => apps}/web/lib/realtime/sse-provider.ts (100%) rename {packages => apps}/web/lib/realtime/types.ts (100%) rename {packages => apps}/web/lib/routing/index.ts (100%) rename {packages => apps}/web/lib/routing/route-params.ts (100%) rename {packages => apps}/web/lib/utils/debounce.ts (100%) rename {packages => apps}/web/lib/utils/index.ts (100%) rename {packages => apps}/web/lib/utils/time-utils.ts (100%) rename {packages => apps}/web/lib/utils/utils.ts (100%) rename {packages => apps}/web/next-env.d.ts (100%) rename {packages => apps}/web/next.config.js (100%) rename {packages => apps}/web/package.json (100%) rename {packages => apps}/web/postcss.config.mjs (100%) rename {packages => apps}/web/public/devlog-logo-text.svg (100%) rename {packages => apps}/web/public/devlog-logo.svg (100%) rename {packages => apps}/web/public/inter-medium.woff2 (100%) rename {packages => apps}/web/public/inter-regular.woff2 (100%) rename {packages => apps}/web/public/inter-semibold.woff2 (100%) rename {packages => apps}/web/schemas/bridge.ts (100%) rename {packages => apps}/web/schemas/devlog.ts (100%) rename {packages => apps}/web/schemas/index.ts (100%) rename {packages => apps}/web/schemas/project.ts (100%) rename {packages => apps}/web/schemas/responses.ts (100%) rename {packages => apps}/web/schemas/validation.ts (100%) rename {packages => apps}/web/stores/base.ts (100%) rename {packages => apps}/web/stores/devlog-store.ts (100%) rename {packages => apps}/web/stores/index.ts (100%) rename {packages => apps}/web/stores/layout-store.ts (100%) rename {packages => apps}/web/stores/project-store.ts (100%) rename {packages => apps}/web/stores/realtime-store.ts (100%) rename {packages => apps}/web/styles/base.css (100%) rename {packages => apps}/web/styles/fonts.css (100%) rename {packages => apps}/web/styles/globals.css (100%) rename {packages => apps}/web/styles/layout.css (100%) rename {packages => apps}/web/styles/responsive.css (100%) rename {packages => apps}/web/tailwind.config.js (100%) rename {packages => apps}/web/tests/README.md (100%) rename {packages => apps}/web/tests/lib/api/api-integration.test.ts (100%) rename {packages => apps}/web/tests/setup.ts (100%) rename {packages => apps}/web/tests/utils/test-server.ts (100%) rename {packages => apps}/web/tsconfig.json (100%) rename {packages => apps}/web/vercel.json (100%) rename {packages => apps}/web/vitest.config.ts (100%) delete mode 100644 packages/web/NAMING_CONVENTIONS.md delete mode 100644 packages/web/REALTIME.md delete mode 100644 packages/web/ROUTING.md diff --git a/packages/web/README.md b/apps/web/README.md similarity index 100% rename from packages/web/README.md rename to apps/web/README.md diff --git a/packages/web/app/api/events/route.ts b/apps/web/app/api/events/route.ts similarity index 100% rename from packages/web/app/api/events/route.ts rename to apps/web/app/api/events/route.ts diff --git a/packages/web/app/api/health/route.ts b/apps/web/app/api/health/route.ts similarity index 100% rename from packages/web/app/api/health/route.ts rename to apps/web/app/api/health/route.ts diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts b/apps/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts similarity index 100% rename from packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts rename to apps/web/app/api/projects/[name]/devlogs/[devlogId]/notes/[noteId]/route.ts diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts b/apps/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts similarity index 100% rename from packages/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts rename to apps/web/app/api/projects/[name]/devlogs/[devlogId]/notes/route.ts diff --git a/packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts b/apps/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts similarity index 100% rename from packages/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts rename to apps/web/app/api/projects/[name]/devlogs/[devlogId]/route.ts diff --git a/packages/web/app/api/projects/[name]/devlogs/route.ts b/apps/web/app/api/projects/[name]/devlogs/route.ts similarity index 100% rename from packages/web/app/api/projects/[name]/devlogs/route.ts rename to apps/web/app/api/projects/[name]/devlogs/route.ts diff --git a/packages/web/app/api/projects/[name]/devlogs/search/route.ts b/apps/web/app/api/projects/[name]/devlogs/search/route.ts similarity index 100% rename from packages/web/app/api/projects/[name]/devlogs/search/route.ts rename to apps/web/app/api/projects/[name]/devlogs/search/route.ts diff --git a/packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts b/apps/web/app/api/projects/[name]/devlogs/stats/overview/route.ts similarity index 100% rename from packages/web/app/api/projects/[name]/devlogs/stats/overview/route.ts rename to apps/web/app/api/projects/[name]/devlogs/stats/overview/route.ts diff --git a/packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts b/apps/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts similarity index 100% rename from packages/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts rename to apps/web/app/api/projects/[name]/devlogs/stats/timeseries/route.ts diff --git a/packages/web/app/api/projects/[name]/route.ts b/apps/web/app/api/projects/[name]/route.ts similarity index 100% rename from packages/web/app/api/projects/[name]/route.ts rename to apps/web/app/api/projects/[name]/route.ts diff --git a/packages/web/app/api/projects/route.ts b/apps/web/app/api/projects/route.ts similarity index 100% rename from packages/web/app/api/projects/route.ts rename to apps/web/app/api/projects/route.ts diff --git a/packages/web/app/api/realtime/config/route.ts b/apps/web/app/api/realtime/config/route.ts similarity index 100% rename from packages/web/app/api/realtime/config/route.ts rename to apps/web/app/api/realtime/config/route.ts diff --git a/packages/web/app/layout.tsx b/apps/web/app/layout.tsx similarity index 100% rename from packages/web/app/layout.tsx rename to apps/web/app/layout.tsx diff --git a/packages/web/app/not-found.tsx b/apps/web/app/not-found.tsx similarity index 100% rename from packages/web/app/not-found.tsx rename to apps/web/app/not-found.tsx diff --git a/packages/web/app/page.tsx b/apps/web/app/page.tsx similarity index 100% rename from packages/web/app/page.tsx rename to apps/web/app/page.tsx diff --git a/packages/web/app/projects/[name]/devlogs/[id]/layout.tsx b/apps/web/app/projects/[name]/devlogs/[id]/layout.tsx similarity index 100% rename from packages/web/app/projects/[name]/devlogs/[id]/layout.tsx rename to apps/web/app/projects/[name]/devlogs/[id]/layout.tsx diff --git a/packages/web/app/projects/[name]/devlogs/[id]/page.tsx b/apps/web/app/projects/[name]/devlogs/[id]/page.tsx similarity index 100% rename from packages/web/app/projects/[name]/devlogs/[id]/page.tsx rename to apps/web/app/projects/[name]/devlogs/[id]/page.tsx diff --git a/packages/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx b/apps/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx similarity index 100% rename from packages/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx rename to apps/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx diff --git a/packages/web/app/projects/[name]/devlogs/page.tsx b/apps/web/app/projects/[name]/devlogs/page.tsx similarity index 100% rename from packages/web/app/projects/[name]/devlogs/page.tsx rename to apps/web/app/projects/[name]/devlogs/page.tsx diff --git a/packages/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx b/apps/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx similarity index 100% rename from packages/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx rename to apps/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx diff --git a/packages/web/app/projects/[name]/layout.tsx b/apps/web/app/projects/[name]/layout.tsx similarity index 100% rename from packages/web/app/projects/[name]/layout.tsx rename to apps/web/app/projects/[name]/layout.tsx diff --git a/packages/web/app/projects/[name]/page.tsx b/apps/web/app/projects/[name]/page.tsx similarity index 100% rename from packages/web/app/projects/[name]/page.tsx rename to apps/web/app/projects/[name]/page.tsx diff --git a/packages/web/app/projects/[name]/project-details-page.tsx b/apps/web/app/projects/[name]/project-details-page.tsx similarity index 100% rename from packages/web/app/projects/[name]/project-details-page.tsx rename to apps/web/app/projects/[name]/project-details-page.tsx diff --git a/packages/web/app/projects/[name]/settings/page.tsx b/apps/web/app/projects/[name]/settings/page.tsx similarity index 100% rename from packages/web/app/projects/[name]/settings/page.tsx rename to apps/web/app/projects/[name]/settings/page.tsx diff --git a/packages/web/app/projects/[name]/settings/project-settings-page.tsx b/apps/web/app/projects/[name]/settings/project-settings-page.tsx similarity index 100% rename from packages/web/app/projects/[name]/settings/project-settings-page.tsx rename to apps/web/app/projects/[name]/settings/project-settings-page.tsx diff --git a/packages/web/app/projects/page.tsx b/apps/web/app/projects/page.tsx similarity index 100% rename from packages/web/app/projects/page.tsx rename to apps/web/app/projects/page.tsx diff --git a/packages/web/app/projects/project-list-page.tsx b/apps/web/app/projects/project-list-page.tsx similarity index 100% rename from packages/web/app/projects/project-list-page.tsx rename to apps/web/app/projects/project-list-page.tsx diff --git a/packages/web/components.json b/apps/web/components.json similarity index 100% rename from packages/web/components.json rename to apps/web/components.json diff --git a/packages/web/components/common/index.ts b/apps/web/components/common/index.ts similarity index 100% rename from packages/web/components/common/index.ts rename to apps/web/components/common/index.ts diff --git a/packages/web/components/common/overview-stats/overview-stats.tsx b/apps/web/components/common/overview-stats/overview-stats.tsx similarity index 100% rename from packages/web/components/common/overview-stats/overview-stats.tsx rename to apps/web/components/common/overview-stats/overview-stats.tsx diff --git a/packages/web/components/common/pagination.tsx b/apps/web/components/common/pagination.tsx similarity index 100% rename from packages/web/components/common/pagination.tsx rename to apps/web/components/common/pagination.tsx diff --git a/packages/web/components/common/project-card-skeleton/index.ts b/apps/web/components/common/project-card-skeleton/index.ts similarity index 100% rename from packages/web/components/common/project-card-skeleton/index.ts rename to apps/web/components/common/project-card-skeleton/index.ts diff --git a/packages/web/components/common/project-card-skeleton/project-card-skeleton.tsx b/apps/web/components/common/project-card-skeleton/project-card-skeleton.tsx similarity index 100% rename from packages/web/components/common/project-card-skeleton/project-card-skeleton.tsx rename to apps/web/components/common/project-card-skeleton/project-card-skeleton.tsx diff --git a/packages/web/components/custom/devlog-tags.tsx b/apps/web/components/custom/devlog-tags.tsx similarity index 100% rename from packages/web/components/custom/devlog-tags.tsx rename to apps/web/components/custom/devlog-tags.tsx diff --git a/packages/web/components/custom/editable-field.tsx b/apps/web/components/custom/editable-field.tsx similarity index 100% rename from packages/web/components/custom/editable-field.tsx rename to apps/web/components/custom/editable-field.tsx diff --git a/packages/web/components/custom/error-boundary.tsx b/apps/web/components/custom/error-boundary.tsx similarity index 100% rename from packages/web/components/custom/error-boundary.tsx rename to apps/web/components/custom/error-boundary.tsx diff --git a/packages/web/components/custom/index.ts b/apps/web/components/custom/index.ts similarity index 100% rename from packages/web/components/custom/index.ts rename to apps/web/components/custom/index.ts diff --git a/packages/web/components/custom/loading-page.tsx b/apps/web/components/custom/loading-page.tsx similarity index 100% rename from packages/web/components/custom/loading-page.tsx rename to apps/web/components/custom/loading-page.tsx diff --git a/packages/web/components/custom/markdown-editor.tsx b/apps/web/components/custom/markdown-editor.tsx similarity index 100% rename from packages/web/components/custom/markdown-editor.tsx rename to apps/web/components/custom/markdown-editor.tsx diff --git a/packages/web/components/custom/markdown-renderer.tsx b/apps/web/components/custom/markdown-renderer.tsx similarity index 100% rename from packages/web/components/custom/markdown-renderer.tsx rename to apps/web/components/custom/markdown-renderer.tsx diff --git a/packages/web/components/custom/project/project-not-found.tsx b/apps/web/components/custom/project/project-not-found.tsx similarity index 100% rename from packages/web/components/custom/project/project-not-found.tsx rename to apps/web/components/custom/project/project-not-found.tsx diff --git a/packages/web/components/custom/sticky-headings.tsx b/apps/web/components/custom/sticky-headings.tsx similarity index 100% rename from packages/web/components/custom/sticky-headings.tsx rename to apps/web/components/custom/sticky-headings.tsx diff --git a/packages/web/components/feature/dashboard/chart-utils.ts b/apps/web/components/feature/dashboard/chart-utils.ts similarity index 100% rename from packages/web/components/feature/dashboard/chart-utils.ts rename to apps/web/components/feature/dashboard/chart-utils.ts diff --git a/packages/web/components/feature/dashboard/dashboard.tsx b/apps/web/components/feature/dashboard/dashboard.tsx similarity index 100% rename from packages/web/components/feature/dashboard/dashboard.tsx rename to apps/web/components/feature/dashboard/dashboard.tsx diff --git a/packages/web/components/feature/dashboard/index.ts b/apps/web/components/feature/dashboard/index.ts similarity index 100% rename from packages/web/components/feature/dashboard/index.ts rename to apps/web/components/feature/dashboard/index.ts diff --git a/packages/web/components/feature/devlog/devlog-anchor-nav.tsx b/apps/web/components/feature/devlog/devlog-anchor-nav.tsx similarity index 100% rename from packages/web/components/feature/devlog/devlog-anchor-nav.tsx rename to apps/web/components/feature/devlog/devlog-anchor-nav.tsx diff --git a/packages/web/components/feature/devlog/devlog-details.tsx b/apps/web/components/feature/devlog/devlog-details.tsx similarity index 100% rename from packages/web/components/feature/devlog/devlog-details.tsx rename to apps/web/components/feature/devlog/devlog-details.tsx diff --git a/packages/web/components/feature/devlog/devlog-list.tsx b/apps/web/components/feature/devlog/devlog-list.tsx similarity index 100% rename from packages/web/components/feature/devlog/devlog-list.tsx rename to apps/web/components/feature/devlog/devlog-list.tsx diff --git a/packages/web/components/feature/devlog/index.ts b/apps/web/components/feature/devlog/index.ts similarity index 100% rename from packages/web/components/feature/devlog/index.ts rename to apps/web/components/feature/devlog/index.ts diff --git a/packages/web/components/forms/devlog-form.tsx b/apps/web/components/forms/devlog-form.tsx similarity index 100% rename from packages/web/components/forms/devlog-form.tsx rename to apps/web/components/forms/devlog-form.tsx diff --git a/packages/web/components/forms/index.ts b/apps/web/components/forms/index.ts similarity index 100% rename from packages/web/components/forms/index.ts rename to apps/web/components/forms/index.ts diff --git a/packages/web/components/index.ts b/apps/web/components/index.ts similarity index 100% rename from packages/web/components/index.ts rename to apps/web/components/index.ts diff --git a/packages/web/components/layout/app-layout-skeleton.tsx b/apps/web/components/layout/app-layout-skeleton.tsx similarity index 100% rename from packages/web/components/layout/app-layout-skeleton.tsx rename to apps/web/components/layout/app-layout-skeleton.tsx diff --git a/packages/web/components/layout/app-layout.tsx b/apps/web/components/layout/app-layout.tsx similarity index 100% rename from packages/web/components/layout/app-layout.tsx rename to apps/web/components/layout/app-layout.tsx diff --git a/packages/web/components/layout/index.ts b/apps/web/components/layout/index.ts similarity index 100% rename from packages/web/components/layout/index.ts rename to apps/web/components/layout/index.ts diff --git a/packages/web/components/layout/navigation-breadcrumb.tsx b/apps/web/components/layout/navigation-breadcrumb.tsx similarity index 100% rename from packages/web/components/layout/navigation-breadcrumb.tsx rename to apps/web/components/layout/navigation-breadcrumb.tsx diff --git a/packages/web/components/layout/navigation-sidebar.tsx b/apps/web/components/layout/navigation-sidebar.tsx similarity index 100% rename from packages/web/components/layout/navigation-sidebar.tsx rename to apps/web/components/layout/navigation-sidebar.tsx diff --git a/packages/web/components/layout/top-navbar.tsx b/apps/web/components/layout/top-navbar.tsx similarity index 100% rename from packages/web/components/layout/top-navbar.tsx rename to apps/web/components/layout/top-navbar.tsx diff --git a/packages/web/components/provider/app-providers.tsx b/apps/web/components/provider/app-providers.tsx similarity index 100% rename from packages/web/components/provider/app-providers.tsx rename to apps/web/components/provider/app-providers.tsx diff --git a/packages/web/components/provider/devlog-provider.tsx b/apps/web/components/provider/devlog-provider.tsx similarity index 100% rename from packages/web/components/provider/devlog-provider.tsx rename to apps/web/components/provider/devlog-provider.tsx diff --git a/packages/web/components/provider/project-provider.tsx b/apps/web/components/provider/project-provider.tsx similarity index 100% rename from packages/web/components/provider/project-provider.tsx rename to apps/web/components/provider/project-provider.tsx diff --git a/packages/web/components/provider/store-provider.tsx b/apps/web/components/provider/store-provider.tsx similarity index 100% rename from packages/web/components/provider/store-provider.tsx rename to apps/web/components/provider/store-provider.tsx diff --git a/packages/web/components/provider/theme-provider.tsx b/apps/web/components/provider/theme-provider.tsx similarity index 100% rename from packages/web/components/provider/theme-provider.tsx rename to apps/web/components/provider/theme-provider.tsx diff --git a/packages/web/components/realtime/realtime-status.tsx b/apps/web/components/realtime/realtime-status.tsx similarity index 100% rename from packages/web/components/realtime/realtime-status.tsx rename to apps/web/components/realtime/realtime-status.tsx diff --git a/packages/web/components/ui/accordion.tsx b/apps/web/components/ui/accordion.tsx similarity index 100% rename from packages/web/components/ui/accordion.tsx rename to apps/web/components/ui/accordion.tsx diff --git a/packages/web/components/ui/alert-dialog.tsx b/apps/web/components/ui/alert-dialog.tsx similarity index 100% rename from packages/web/components/ui/alert-dialog.tsx rename to apps/web/components/ui/alert-dialog.tsx diff --git a/packages/web/components/ui/alert.tsx b/apps/web/components/ui/alert.tsx similarity index 100% rename from packages/web/components/ui/alert.tsx rename to apps/web/components/ui/alert.tsx diff --git a/packages/web/components/ui/badge.tsx b/apps/web/components/ui/badge.tsx similarity index 100% rename from packages/web/components/ui/badge.tsx rename to apps/web/components/ui/badge.tsx diff --git a/packages/web/components/ui/breadcrumb.tsx b/apps/web/components/ui/breadcrumb.tsx similarity index 100% rename from packages/web/components/ui/breadcrumb.tsx rename to apps/web/components/ui/breadcrumb.tsx diff --git a/packages/web/components/ui/button.tsx b/apps/web/components/ui/button.tsx similarity index 100% rename from packages/web/components/ui/button.tsx rename to apps/web/components/ui/button.tsx diff --git a/packages/web/components/ui/card.tsx b/apps/web/components/ui/card.tsx similarity index 100% rename from packages/web/components/ui/card.tsx rename to apps/web/components/ui/card.tsx diff --git a/packages/web/components/ui/checkbox.tsx b/apps/web/components/ui/checkbox.tsx similarity index 100% rename from packages/web/components/ui/checkbox.tsx rename to apps/web/components/ui/checkbox.tsx diff --git a/packages/web/components/ui/command.tsx b/apps/web/components/ui/command.tsx similarity index 100% rename from packages/web/components/ui/command.tsx rename to apps/web/components/ui/command.tsx diff --git a/packages/web/components/ui/dialog.tsx b/apps/web/components/ui/dialog.tsx similarity index 100% rename from packages/web/components/ui/dialog.tsx rename to apps/web/components/ui/dialog.tsx diff --git a/packages/web/components/ui/dropdown-menu.tsx b/apps/web/components/ui/dropdown-menu.tsx similarity index 100% rename from packages/web/components/ui/dropdown-menu.tsx rename to apps/web/components/ui/dropdown-menu.tsx diff --git a/packages/web/components/ui/form.tsx b/apps/web/components/ui/form.tsx similarity index 100% rename from packages/web/components/ui/form.tsx rename to apps/web/components/ui/form.tsx diff --git a/packages/web/components/ui/index.ts b/apps/web/components/ui/index.ts similarity index 100% rename from packages/web/components/ui/index.ts rename to apps/web/components/ui/index.ts diff --git a/packages/web/components/ui/input.tsx b/apps/web/components/ui/input.tsx similarity index 100% rename from packages/web/components/ui/input.tsx rename to apps/web/components/ui/input.tsx diff --git a/packages/web/components/ui/label.tsx b/apps/web/components/ui/label.tsx similarity index 100% rename from packages/web/components/ui/label.tsx rename to apps/web/components/ui/label.tsx diff --git a/packages/web/components/ui/navigation-menu.tsx b/apps/web/components/ui/navigation-menu.tsx similarity index 100% rename from packages/web/components/ui/navigation-menu.tsx rename to apps/web/components/ui/navigation-menu.tsx diff --git a/packages/web/components/ui/popover.tsx b/apps/web/components/ui/popover.tsx similarity index 100% rename from packages/web/components/ui/popover.tsx rename to apps/web/components/ui/popover.tsx diff --git a/packages/web/components/ui/progress.tsx b/apps/web/components/ui/progress.tsx similarity index 100% rename from packages/web/components/ui/progress.tsx rename to apps/web/components/ui/progress.tsx diff --git a/packages/web/components/ui/select.tsx b/apps/web/components/ui/select.tsx similarity index 100% rename from packages/web/components/ui/select.tsx rename to apps/web/components/ui/select.tsx diff --git a/packages/web/components/ui/separator.tsx b/apps/web/components/ui/separator.tsx similarity index 100% rename from packages/web/components/ui/separator.tsx rename to apps/web/components/ui/separator.tsx diff --git a/packages/web/components/ui/sheet.tsx b/apps/web/components/ui/sheet.tsx similarity index 100% rename from packages/web/components/ui/sheet.tsx rename to apps/web/components/ui/sheet.tsx diff --git a/packages/web/components/ui/sidebar.tsx b/apps/web/components/ui/sidebar.tsx similarity index 100% rename from packages/web/components/ui/sidebar.tsx rename to apps/web/components/ui/sidebar.tsx diff --git a/packages/web/components/ui/skeleton.tsx b/apps/web/components/ui/skeleton.tsx similarity index 100% rename from packages/web/components/ui/skeleton.tsx rename to apps/web/components/ui/skeleton.tsx diff --git a/packages/web/components/ui/switch.tsx b/apps/web/components/ui/switch.tsx similarity index 100% rename from packages/web/components/ui/switch.tsx rename to apps/web/components/ui/switch.tsx diff --git a/packages/web/components/ui/table.tsx b/apps/web/components/ui/table.tsx similarity index 100% rename from packages/web/components/ui/table.tsx rename to apps/web/components/ui/table.tsx diff --git a/packages/web/components/ui/tabs.tsx b/apps/web/components/ui/tabs.tsx similarity index 100% rename from packages/web/components/ui/tabs.tsx rename to apps/web/components/ui/tabs.tsx diff --git a/packages/web/components/ui/textarea.tsx b/apps/web/components/ui/textarea.tsx similarity index 100% rename from packages/web/components/ui/textarea.tsx rename to apps/web/components/ui/textarea.tsx diff --git a/packages/web/components/ui/theme-toggle.tsx b/apps/web/components/ui/theme-toggle.tsx similarity index 100% rename from packages/web/components/ui/theme-toggle.tsx rename to apps/web/components/ui/theme-toggle.tsx diff --git a/packages/web/components/ui/tooltip.tsx b/apps/web/components/ui/tooltip.tsx similarity index 100% rename from packages/web/components/ui/tooltip.tsx rename to apps/web/components/ui/tooltip.tsx diff --git a/packages/web/hooks/index.ts b/apps/web/hooks/index.ts similarity index 100% rename from packages/web/hooks/index.ts rename to apps/web/hooks/index.ts diff --git a/packages/web/hooks/use-mobile.tsx b/apps/web/hooks/use-mobile.tsx similarity index 100% rename from packages/web/hooks/use-mobile.tsx rename to apps/web/hooks/use-mobile.tsx diff --git a/packages/web/hooks/use-realtime.ts b/apps/web/hooks/use-realtime.ts similarity index 100% rename from packages/web/hooks/use-realtime.ts rename to apps/web/hooks/use-realtime.ts diff --git a/packages/web/lib/api/api-client.ts b/apps/web/lib/api/api-client.ts similarity index 100% rename from packages/web/lib/api/api-client.ts rename to apps/web/lib/api/api-client.ts diff --git a/packages/web/lib/api/api-utils.ts b/apps/web/lib/api/api-utils.ts similarity index 100% rename from packages/web/lib/api/api-utils.ts rename to apps/web/lib/api/api-utils.ts diff --git a/packages/web/lib/api/devlog-api-client.ts b/apps/web/lib/api/devlog-api-client.ts similarity index 100% rename from packages/web/lib/api/devlog-api-client.ts rename to apps/web/lib/api/devlog-api-client.ts diff --git a/packages/web/lib/api/index.ts b/apps/web/lib/api/index.ts similarity index 100% rename from packages/web/lib/api/index.ts rename to apps/web/lib/api/index.ts diff --git a/packages/web/lib/api/project-api-client.ts b/apps/web/lib/api/project-api-client.ts similarity index 100% rename from packages/web/lib/api/project-api-client.ts rename to apps/web/lib/api/project-api-client.ts diff --git a/packages/web/lib/api/server-realtime.ts b/apps/web/lib/api/server-realtime.ts similarity index 100% rename from packages/web/lib/api/server-realtime.ts rename to apps/web/lib/api/server-realtime.ts diff --git a/packages/web/lib/devlog/devlog-options.ts b/apps/web/lib/devlog/devlog-options.ts similarity index 100% rename from packages/web/lib/devlog/devlog-options.ts rename to apps/web/lib/devlog/devlog-options.ts diff --git a/packages/web/lib/devlog/devlog-ui-utils.tsx b/apps/web/lib/devlog/devlog-ui-utils.tsx similarity index 100% rename from packages/web/lib/devlog/devlog-ui-utils.tsx rename to apps/web/lib/devlog/devlog-ui-utils.tsx diff --git a/packages/web/lib/devlog/index.ts b/apps/web/lib/devlog/index.ts similarity index 100% rename from packages/web/lib/devlog/index.ts rename to apps/web/lib/devlog/index.ts diff --git a/packages/web/lib/devlog/note-utils.tsx b/apps/web/lib/devlog/note-utils.tsx similarity index 100% rename from packages/web/lib/devlog/note-utils.tsx rename to apps/web/lib/devlog/note-utils.tsx diff --git a/packages/web/lib/index.ts b/apps/web/lib/index.ts similarity index 100% rename from packages/web/lib/index.ts rename to apps/web/lib/index.ts diff --git a/packages/web/lib/project-urls.ts b/apps/web/lib/project-urls.ts similarity index 100% rename from packages/web/lib/project-urls.ts rename to apps/web/lib/project-urls.ts diff --git a/packages/web/lib/realtime/config.ts b/apps/web/lib/realtime/config.ts similarity index 100% rename from packages/web/lib/realtime/config.ts rename to apps/web/lib/realtime/config.ts diff --git a/packages/web/lib/realtime/index.ts b/apps/web/lib/realtime/index.ts similarity index 100% rename from packages/web/lib/realtime/index.ts rename to apps/web/lib/realtime/index.ts diff --git a/packages/web/lib/realtime/pusher-provider.ts b/apps/web/lib/realtime/pusher-provider.ts similarity index 100% rename from packages/web/lib/realtime/pusher-provider.ts rename to apps/web/lib/realtime/pusher-provider.ts diff --git a/packages/web/lib/realtime/realtime-service.ts b/apps/web/lib/realtime/realtime-service.ts similarity index 100% rename from packages/web/lib/realtime/realtime-service.ts rename to apps/web/lib/realtime/realtime-service.ts diff --git a/packages/web/lib/realtime/sse-provider.ts b/apps/web/lib/realtime/sse-provider.ts similarity index 100% rename from packages/web/lib/realtime/sse-provider.ts rename to apps/web/lib/realtime/sse-provider.ts diff --git a/packages/web/lib/realtime/types.ts b/apps/web/lib/realtime/types.ts similarity index 100% rename from packages/web/lib/realtime/types.ts rename to apps/web/lib/realtime/types.ts diff --git a/packages/web/lib/routing/index.ts b/apps/web/lib/routing/index.ts similarity index 100% rename from packages/web/lib/routing/index.ts rename to apps/web/lib/routing/index.ts diff --git a/packages/web/lib/routing/route-params.ts b/apps/web/lib/routing/route-params.ts similarity index 100% rename from packages/web/lib/routing/route-params.ts rename to apps/web/lib/routing/route-params.ts diff --git a/packages/web/lib/utils/debounce.ts b/apps/web/lib/utils/debounce.ts similarity index 100% rename from packages/web/lib/utils/debounce.ts rename to apps/web/lib/utils/debounce.ts diff --git a/packages/web/lib/utils/index.ts b/apps/web/lib/utils/index.ts similarity index 100% rename from packages/web/lib/utils/index.ts rename to apps/web/lib/utils/index.ts diff --git a/packages/web/lib/utils/time-utils.ts b/apps/web/lib/utils/time-utils.ts similarity index 100% rename from packages/web/lib/utils/time-utils.ts rename to apps/web/lib/utils/time-utils.ts diff --git a/packages/web/lib/utils/utils.ts b/apps/web/lib/utils/utils.ts similarity index 100% rename from packages/web/lib/utils/utils.ts rename to apps/web/lib/utils/utils.ts diff --git a/packages/web/next-env.d.ts b/apps/web/next-env.d.ts similarity index 100% rename from packages/web/next-env.d.ts rename to apps/web/next-env.d.ts diff --git a/packages/web/next.config.js b/apps/web/next.config.js similarity index 100% rename from packages/web/next.config.js rename to apps/web/next.config.js diff --git a/packages/web/package.json b/apps/web/package.json similarity index 100% rename from packages/web/package.json rename to apps/web/package.json diff --git a/packages/web/postcss.config.mjs b/apps/web/postcss.config.mjs similarity index 100% rename from packages/web/postcss.config.mjs rename to apps/web/postcss.config.mjs diff --git a/packages/web/public/devlog-logo-text.svg b/apps/web/public/devlog-logo-text.svg similarity index 100% rename from packages/web/public/devlog-logo-text.svg rename to apps/web/public/devlog-logo-text.svg diff --git a/packages/web/public/devlog-logo.svg b/apps/web/public/devlog-logo.svg similarity index 100% rename from packages/web/public/devlog-logo.svg rename to apps/web/public/devlog-logo.svg diff --git a/packages/web/public/inter-medium.woff2 b/apps/web/public/inter-medium.woff2 similarity index 100% rename from packages/web/public/inter-medium.woff2 rename to apps/web/public/inter-medium.woff2 diff --git a/packages/web/public/inter-regular.woff2 b/apps/web/public/inter-regular.woff2 similarity index 100% rename from packages/web/public/inter-regular.woff2 rename to apps/web/public/inter-regular.woff2 diff --git a/packages/web/public/inter-semibold.woff2 b/apps/web/public/inter-semibold.woff2 similarity index 100% rename from packages/web/public/inter-semibold.woff2 rename to apps/web/public/inter-semibold.woff2 diff --git a/packages/web/schemas/bridge.ts b/apps/web/schemas/bridge.ts similarity index 100% rename from packages/web/schemas/bridge.ts rename to apps/web/schemas/bridge.ts diff --git a/packages/web/schemas/devlog.ts b/apps/web/schemas/devlog.ts similarity index 100% rename from packages/web/schemas/devlog.ts rename to apps/web/schemas/devlog.ts diff --git a/packages/web/schemas/index.ts b/apps/web/schemas/index.ts similarity index 100% rename from packages/web/schemas/index.ts rename to apps/web/schemas/index.ts diff --git a/packages/web/schemas/project.ts b/apps/web/schemas/project.ts similarity index 100% rename from packages/web/schemas/project.ts rename to apps/web/schemas/project.ts diff --git a/packages/web/schemas/responses.ts b/apps/web/schemas/responses.ts similarity index 100% rename from packages/web/schemas/responses.ts rename to apps/web/schemas/responses.ts diff --git a/packages/web/schemas/validation.ts b/apps/web/schemas/validation.ts similarity index 100% rename from packages/web/schemas/validation.ts rename to apps/web/schemas/validation.ts diff --git a/packages/web/stores/base.ts b/apps/web/stores/base.ts similarity index 100% rename from packages/web/stores/base.ts rename to apps/web/stores/base.ts diff --git a/packages/web/stores/devlog-store.ts b/apps/web/stores/devlog-store.ts similarity index 100% rename from packages/web/stores/devlog-store.ts rename to apps/web/stores/devlog-store.ts diff --git a/packages/web/stores/index.ts b/apps/web/stores/index.ts similarity index 100% rename from packages/web/stores/index.ts rename to apps/web/stores/index.ts diff --git a/packages/web/stores/layout-store.ts b/apps/web/stores/layout-store.ts similarity index 100% rename from packages/web/stores/layout-store.ts rename to apps/web/stores/layout-store.ts diff --git a/packages/web/stores/project-store.ts b/apps/web/stores/project-store.ts similarity index 100% rename from packages/web/stores/project-store.ts rename to apps/web/stores/project-store.ts diff --git a/packages/web/stores/realtime-store.ts b/apps/web/stores/realtime-store.ts similarity index 100% rename from packages/web/stores/realtime-store.ts rename to apps/web/stores/realtime-store.ts diff --git a/packages/web/styles/base.css b/apps/web/styles/base.css similarity index 100% rename from packages/web/styles/base.css rename to apps/web/styles/base.css diff --git a/packages/web/styles/fonts.css b/apps/web/styles/fonts.css similarity index 100% rename from packages/web/styles/fonts.css rename to apps/web/styles/fonts.css diff --git a/packages/web/styles/globals.css b/apps/web/styles/globals.css similarity index 100% rename from packages/web/styles/globals.css rename to apps/web/styles/globals.css diff --git a/packages/web/styles/layout.css b/apps/web/styles/layout.css similarity index 100% rename from packages/web/styles/layout.css rename to apps/web/styles/layout.css diff --git a/packages/web/styles/responsive.css b/apps/web/styles/responsive.css similarity index 100% rename from packages/web/styles/responsive.css rename to apps/web/styles/responsive.css diff --git a/packages/web/tailwind.config.js b/apps/web/tailwind.config.js similarity index 100% rename from packages/web/tailwind.config.js rename to apps/web/tailwind.config.js diff --git a/packages/web/tests/README.md b/apps/web/tests/README.md similarity index 100% rename from packages/web/tests/README.md rename to apps/web/tests/README.md diff --git a/packages/web/tests/lib/api/api-integration.test.ts b/apps/web/tests/lib/api/api-integration.test.ts similarity index 100% rename from packages/web/tests/lib/api/api-integration.test.ts rename to apps/web/tests/lib/api/api-integration.test.ts diff --git a/packages/web/tests/setup.ts b/apps/web/tests/setup.ts similarity index 100% rename from packages/web/tests/setup.ts rename to apps/web/tests/setup.ts diff --git a/packages/web/tests/utils/test-server.ts b/apps/web/tests/utils/test-server.ts similarity index 100% rename from packages/web/tests/utils/test-server.ts rename to apps/web/tests/utils/test-server.ts diff --git a/packages/web/tsconfig.json b/apps/web/tsconfig.json similarity index 100% rename from packages/web/tsconfig.json rename to apps/web/tsconfig.json diff --git a/packages/web/vercel.json b/apps/web/vercel.json similarity index 100% rename from packages/web/vercel.json rename to apps/web/vercel.json diff --git a/packages/web/vitest.config.ts b/apps/web/vitest.config.ts similarity index 100% rename from packages/web/vitest.config.ts rename to apps/web/vitest.config.ts diff --git a/packages/web/NAMING_CONVENTIONS.md b/packages/web/NAMING_CONVENTIONS.md deleted file mode 100644 index ffdd6a8f..00000000 --- a/packages/web/NAMING_CONVENTIONS.md +++ /dev/null @@ -1,95 +0,0 @@ -# File Naming Conventions - -This document outlines the standardized file naming conventions for the web package. - -## React Components (`.tsx` files) - -### Page Components - -- **Pattern**: `PascalCase.tsx` or `page.tsx` (Next.js convention) -- **Examples**: - - `ProjectDetailsPage.tsx` - - `page.tsx` (Next.js route files) - -### Layout Components - -- **Pattern**: `PascalCase.tsx` -- **Examples**: - - `AppLayout.tsx` - - `NavigationSidebar.tsx` - -### Feature Components - -- **Pattern**: `PascalCase.tsx` -- **Examples**: - - `DevlogDetails.tsx` - - `DevlogList.tsx` - - `MarkdownEditor.tsx` - -## React Hooks (`.ts` or `.tsx` files) - -- **Pattern**: `use-kebab-case.ts` -- **Examples**: - - `use-mobile.tsx` - - `use-sse.ts` - -## UI Components (shadcn/ui convention) - -- **Pattern**: `kebab-case.tsx` -- **Examples**: - - `alert-dialog.tsx` - - `dropdown-menu.tsx` - - `theme-toggle.tsx` - -## Providers - -- **Pattern**: `kebab-case.tsx` (follows shadcn convention) -- **Examples**: - - `theme-provider.tsx` - -## Utility/Library Files (`.ts` files) - -- **Pattern**: `kebab-case.ts` -- **Examples**: - - `api-client.ts` - - `time-utils.ts` - - `route-params.ts` - -## Schema/Type Files - -- **Pattern**: `kebab-case.ts` -- **Examples**: - - `devlog.ts` - - `project.ts` - - `validation.ts` - -## CSS Files - -- **Pattern**: `kebab-case.css` -- **Examples**: - - `globals.css` - - `base.css` - - `layout.css` - -## API Route Files (Next.js) - -- **Pattern**: `route.ts` (Next.js App Router convention) -- **Location**: Within appropriately named directory structures - -## Directory Naming - -- **Pattern**: `kebab-case` for most directories -- **Examples**: - - `components/` - - `contexts/` - - `schemas/` - - `hooks/` - -## Notes - -- Follow existing conventions in the codebase -- UI components use kebab-case to align with shadcn/ui library conventions -- Hooks always start with "use" prefix and use camelCase -- React components use PascalCase -- Utility files use kebab-case for readability -- When in doubt, check existing similar files in the codebase diff --git a/packages/web/REALTIME.md b/packages/web/REALTIME.md deleted file mode 100644 index 87e1eda6..00000000 --- a/packages/web/REALTIME.md +++ /dev/null @@ -1,233 +0,0 @@ -# Realtime Updates System - -This document describes the optimized realtime update system that supports both Server-Sent Events (SSE) and Pusher for different deployment environments. - -## Overview - -The realtime system automatically chooses the best transport method based on your deployment environment: - -- **SSE (Server-Sent Events)**: Default for traditional deployments (Docker, self-hosted) -- **Pusher**: Automatically selected for serverless deployments (Vercel, Netlify) when configured - -## Architecture - -### Client-Side Components - -- `RealtimeService`: Main service that manages provider selection and operation -- `SSEProvider`: Handles Server-Sent Events connections -- `PusherProvider`: Handles Pusher Channels connections -- `useRealtime()`: React hook for easy integration - -### Server-Side Components - -- `ServerRealtimeService`: Broadcasts messages to both SSE and Pusher -- Backward-compatible with existing `broadcastUpdate()` function - -## Configuration - -### Environment Variables - -```bash -# Optional: Force a specific provider (auto-detected if not set) -NEXT_PUBLIC_REALTIME_PROVIDER="auto" # "sse", "pusher", or "auto" - -# SSE Configuration (used by default for non-serverless) -NEXT_PUBLIC_SSE_ENDPOINT="/api/events" -NEXT_PUBLIC_SSE_RECONNECT_INTERVAL="3000" - -# Pusher Configuration (required for Pusher support) -PUSHER_APP_ID="your-app-id" -NEXT_PUBLIC_PUSHER_KEY="your-pusher-key" -PUSHER_SECRET="your-pusher-secret" -NEXT_PUBLIC_PUSHER_CLUSTER="us2" -PUSHER_USE_TLS="true" -``` - -### Auto-Detection Logic - -1. Check `NEXT_PUBLIC_REALTIME_PROVIDER` for explicit preference -2. Detect if running on Vercel (`VERCEL=1`) or Netlify -3. Check if Pusher is properly configured -4. Use Pusher for serverless + configured, otherwise SSE - -## Usage - -### Basic React Hook - -```typescript -import { useRealtime } from '@/hooks/use-realtime'; - -function MyComponent() { - const { connected, providerType, subscribe } = useRealtime(); - - useEffect(() => { - const unsubscribe = subscribe('devlog-updated', (devlog) => { - console.log('Devlog updated:', devlog); - }); - - return unsubscribe; - }, [subscribe]); - - return ( -
- Status: {connected ? 'Connected' : 'Disconnected'} - ({providerType}) -
- ); -} -``` - -### Event-Specific Hooks - -```typescript -import { useDevlogEvents } from '@/hooks/use-realtime'; - -function DevlogList() { - const { onDevlogCreated, onDevlogUpdated } = useDevlogEvents(); - - useEffect(() => { - const unsubscribeCreated = onDevlogCreated((devlog) => { - // Handle new devlog - }); - - const unsubscribeUpdated = onDevlogUpdated((devlog) => { - // Handle updated devlog - }); - - return () => { - unsubscribeCreated(); - unsubscribeUpdated(); - }; - }, [onDevlogCreated, onDevlogUpdated]); -} -``` - -### Server-Side Broadcasting - -```typescript -import { serverRealtimeService } from '@/lib/api/server-realtime'; - -// In your API route or server action -await serverRealtimeService.broadcastDevlogCreated(newDevlog); - -// Or use the generic broadcast method -await serverRealtimeService.broadcast('custom-event', data); -``` - -### Status Components - -```typescript -import { RealtimeStatus, RealtimeDebugInfo } from '@/components/realtime/realtime-status'; - -// Minimal status indicator - - -// Detailed status (development only) - -``` - -## Event Types - -The system supports the following standard events: - -- `project-created` -- `project-updated` -- `project-deleted` -- `devlog-created` -- `devlog-updated` -- `devlog-deleted` -- `devlog-note-created` -- `devlog-note-updated` -- `devlog-note-deleted` - -## Deployment Scenarios - -### Traditional Docker Deployment - -Uses SSE by default. No additional configuration needed. - -```bash -# .env -# No realtime configuration needed - SSE works out of the box -``` - -### Vercel Deployment - -1. Sign up for Pusher at https://pusher.com/channels -2. Create a new app and get your credentials -3. Add to Vercel environment variables: - -```bash -PUSHER_APP_ID="123456" -NEXT_PUBLIC_PUSHER_KEY="abcdef123456" -PUSHER_SECRET="your-secret-key" -NEXT_PUBLIC_PUSHER_CLUSTER="us2" -``` - -The system will automatically detect Vercel and use Pusher. - -### Local Development - -SSE is used by default. To test Pusher locally: - -```bash -# .env.local -NEXT_PUBLIC_REALTIME_PROVIDER="pusher" -PUSHER_APP_ID="123456" -NEXT_PUBLIC_PUSHER_KEY="abcdef123456" -PUSHER_SECRET="your-secret-key" -NEXT_PUBLIC_PUSHER_CLUSTER="us2" -``` - -## Migration from Previous System - -The new system is backward compatible. Existing code using `useRealtimeStore` will continue to work without changes. - -### Old API (still supported) -```typescript -const { connected, subscribe } = useRealtimeStore(); -``` - -### New API (recommended) -```typescript -const { connected, subscribe } = useRealtime(); -``` - -## Troubleshooting - -### Debug Information - -Enable debug mode in development: - -```typescript -import { logRealtimeConfig } from '@/lib/realtime'; - -// Log current configuration -logRealtimeConfig(); -``` - -### Common Issues - -1. **Pusher not connecting**: Check that all environment variables are set correctly -2. **SSE disconnecting**: Verify your deployment supports long-running connections -3. **No events received**: Ensure server-side code is using `serverRealtimeService.broadcast()` - -### Testing - -You can test the system by checking the browser console and network tab: - -- SSE: Look for `/api/events` connection in Network tab -- Pusher: Look for WebSocket connections to `ws-*.pusherapp.com` - -## Performance Considerations - -- **SSE**: Lower latency, uses one HTTP connection per client -- **Pusher**: Higher latency but better for serverless, uses WebSockets -- Both providers include automatic reconnection logic -- Connection state is managed automatically - -## Security - -- Pusher keys marked as `NEXT_PUBLIC_*` are safe for client-side use -- `PUSHER_SECRET` must be kept server-side only -- SSE endpoints should validate client permissions as needed diff --git a/packages/web/ROUTING.md b/packages/web/ROUTING.md deleted file mode 100644 index 9eb51a6c..00000000 --- a/packages/web/ROUTING.md +++ /dev/null @@ -1,133 +0,0 @@ -# Routing Implementation for @codervisor/devlog-web - -## Overview - -The web package uses Next.js 14 App Router with hierarchical routing structure that matches the API endpoints. This -provides better organization and clearer URL structure for project-scoped operations. - -## Route Structure - -### Hierarchical Project-Based Routes (Primary) - -``` -/ - Dashboard (homepage) -/projects - Project management page -/projects/[name] - Project details page -/projects/[name]/devlogs - List of devlogs for specific project -/projects/[name]/devlogs/[id] - Individual devlog details within project -``` - -````markdown -# Routing Implementation for @codervisor/devlog-web - -## Overview - -The web package uses Next.js 14 App Router with hierarchical routing structure that matches the API endpoints. This -provides better organization and clearer URL structure for project-scoped operations. - -## Route Structure - -### Hierarchical Project-Based Routes (Primary) - -``` -/ - Redirects to /projects -/projects - Project list page (main entry point) -/projects/[id] - Project dashboard/overview page -/projects/[id]/devlogs - List of devlogs for specific project -/projects/[id]/devlogs/create - Create new devlog in specific project -/projects/[id]/devlogs/[devlogId] - Individual devlog details within project -``` - -## Key Changes Made - -### Route Purpose Updates - -- **`/` (Homepage)**: Now redirects to `/projects` as the main entry point -- **`/projects`**: Project list/management page - browse and manage all projects -- **`/projects/[id]`**: Project dashboard - overview with stats, charts, and recent devlogs (formerly the homepage - dashboard) - -### Navigation Flow - -1. **User visits `/`** → Automatically redirected to `/projects` -2. **User browses projects** at `/projects` → Can view all projects and create new ones -3. **User selects a project** → Goes to `/projects/[id]` for that project's dashboard -4. **From project dashboard** → Can navigate to devlogs, create entries, etc. - -## File Structure - -``` -app/ -├── layout.tsx - Root layout with AppLayout wrapper -├── page.tsx - Homepage (redirects to /projects) -├── AppLayout.tsx - Shared layout with sidebar, header, and navigation -├── projects/ -│ ├── page.tsx - Project list page (/projects) - NEW MAIN ENTRY -│ ├── ProjectManagementPage.tsx - Project list component (updated) -│ └── [id]/ -│ ├── page.tsx - Project dashboard page (/projects/[id]) -│ ├── ProjectDetailsPage.tsx - Project dashboard component (now uses Dashboard) -│ └── devlogs/ -│ ├── page.tsx - Project devlog list (/projects/[id]/devlogs) -│ ├── ProjectDevlogListPage.tsx - Project-scoped devlog list -│ ├── create/ -│ │ ├── page.tsx - Create devlog in project -│ │ └── ProjectDevlogCreatePage.tsx - Project-scoped create form -│ └── [devlogId]/ -│ ├── page.tsx - Dynamic devlog details page -│ └── ProjectDevlogDetailsPage.tsx - Project-scoped details -└── components/ - ├── NavigationSidebar.tsx - Sidebar with project-aware routing - ├── NavigationBreadcrumb.tsx - Hierarchical breadcrumb navigation - └── LoadingPage.tsx - Shared loading component -``` - -## Component Responsibilities - -### ProjectManagementPage (/projects) - -- **Primary function**: Project list and management interface -- **Features**: Browse projects, create new projects, view project cards -- **Navigation**: Links to individual project dashboards - -### ProjectDetailsPage (/projects/[id]) - -- **Primary function**: Project-specific dashboard and overview -- **Features**: Project stats, time series charts, recent devlogs, overview stats -- **Context**: Sets project context for the entire project section - -### DashboardPage Component - -- **Usage**: Embedded in ProjectDetailsPage for project-specific dashboards -- **Features**: Stats visualization, time series data, recent devlogs display -- **Scope**: Project-scoped rather than global - -```` - -## File Structure - -``` -app/ -├── layout.tsx - Root layout with AppLayout wrapper -├── page.tsx - Dashboard page (/) -├── AppLayout.tsx - Shared layout with sidebar, header, and navigation -├── projects/ -│ ├── page.tsx - Project management page (/projects) -│ ├── ProjectManagementPage.tsx - Project management component -│ └── [id]/ -│ ├── page.tsx - Project details page (/projects/[id]) -│ ├── ProjectDetailsPage.tsx - Project details component -│ └── devlogs/ -│ ├── page.tsx - Project devlog list (/projects/[id]/devlogs) -│ ├── ProjectDevlogListPage.tsx - Project-scoped devlog list -│ ├── create/ -│ │ ├── page.tsx - Create devlog in project -│ │ └── ProjectDevlogCreatePage.tsx - Project-scoped create form -│ └── [devlogId]/ -│ ├── page.tsx - Dynamic devlog details page -│ └── ProjectDevlogDetailsPage.tsx - Project-scoped details -└── components/ - ├── NavigationSidebar.tsx - Sidebar with project-aware routing - ├── NavigationBreadcrumb.tsx - Hierarchical breadcrumb navigation - └── LoadingPage.tsx - Shared loading component -``` From 5bf2a9f67ce4a1e1c7b94ae7c7ef618bf40df52e Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 21:13:28 +0800 Subject: [PATCH 27/50] chore: update pnpm workspace configuration to include apps directory --- pnpm-lock.yaml | 1182 +++++++++++++++++++++---------------------- pnpm-workspace.yaml | 1 + 2 files changed, 580 insertions(+), 603 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94c43144..6c174a1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,184 @@ importers: specifier: ^2.1.9 version: 2.1.9(@types/node@20.19.1)(@vitest/ui@2.1.9)(lightningcss@1.30.1)(terser@5.43.1) + apps/web: + dependencies: + '@codervisor/devlog-ai': + specifier: workspace:* + version: link:../../packages/ai + '@codervisor/devlog-core': + specifier: workspace:* + version: link:../../packages/core + '@hookform/resolvers': + specifier: 5.2.0 + version: 5.2.0(react-hook-form@7.61.1(react@18.3.1)) + '@radix-ui/react-accordion': + specifier: 1.2.11 + version: 1.2.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: 1.1.14 + version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: 1.3.2 + version: 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: 1.1.14 + version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: 2.1.15 + version: 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: 2.1.7 + version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-navigation-menu': + specifier: 1.2.13 + version: 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: 1.1.14 + version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': + specifier: 1.1.7 + version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: 2.2.5 + version: 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: 1.1.7 + version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: 1.2.3 + version: 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-switch': + specifier: 1.2.5 + version: 1.2.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: 1.1.12 + version: 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: 1.2.7 + version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tailwindcss/typography': + specifier: 0.5.16 + version: 0.5.16(tailwindcss@3.4.17) + '@uiw/react-textarea-code-editor': + specifier: 3.1.1 + version: 3.1.1(@babel/runtime@7.28.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + class-variance-authority: + specifier: 0.7.1 + version: 0.7.1 + classnames: + specifier: 2.5.1 + version: 2.5.1 + clsx: + specifier: 2.1.1 + version: 2.1.1 + cmdk: + specifier: 1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + highlight.js: + specifier: 11.11.1 + version: 11.11.1 + next: + specifier: ^14.0.4 + version: 14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes: + specifier: 0.4.6 + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + pg: + specifier: ^8.12.0 + version: 8.16.2 + pusher: + specifier: 5.2.0 + version: 5.2.0 + pusher-js: + specifier: 8.4.0 + version: 8.4.0 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: 7.61.1 + version: 7.61.1(react@18.3.1) + react-markdown: + specifier: 10.1.0 + version: 10.1.0(@types/react@18.3.24)(react@18.3.1) + rehype-highlight: + specifier: 7.0.2 + version: 7.0.2 + rehype-sanitize: + specifier: 6.0.0 + version: 6.0.0 + remark-gfm: + specifier: 4.0.1 + version: 4.0.1 + sonner: + specifier: 2.0.6 + version: 2.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: 3.3.1 + version: 3.3.1 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + tailwindcss-animate: + specifier: 1.0.7 + version: 1.0.7(tailwindcss@3.4.17) + typeorm: + specifier: 0.3.25 + version: 0.3.25(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.2)(reflect-metadata@0.2.2) + ws: + specifier: ^8.14.2 + version: 8.18.3 + zod: + specifier: ^3.25.67 + version: 3.25.67 + zustand: + specifier: 5.0.7 + version: 5.0.7(@types/react@18.3.24)(react@18.3.1) + devDependencies: + '@types/node': + specifier: ^20.10.6 + version: 20.19.1 + '@types/pg': + specifier: ^8.11.0 + version: 8.15.4 + '@types/react': + specifier: ^18.2.48 + version: 18.3.24 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.7(@types/react@18.3.24) + '@types/ws': + specifier: ^8.5.10 + version: 8.18.1 + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.6) + concurrently: + specifier: 9.2.0 + version: 9.2.0 + date-fns: + specifier: ^3.2.0 + version: 3.6.0 + lucide-react: + specifier: ^0.323.0 + version: 0.323.0(react@18.3.1) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + recharts: + specifier: ^2.10.3 + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + typescript: + specifier: ^5.3.3 + version: 5.8.3 + packages/ai: dependencies: '@codervisor/devlog-core': @@ -248,184 +426,6 @@ importers: specifier: ^2.1.9 version: 2.1.9(@types/node@20.19.1)(@vitest/ui@2.1.9)(lightningcss@1.30.1)(terser@5.43.1) - packages/web: - dependencies: - '@codervisor/devlog-ai': - specifier: workspace:* - version: link:../ai - '@codervisor/devlog-core': - specifier: workspace:* - version: link:../core - '@hookform/resolvers': - specifier: 5.2.0 - version: 5.2.0(react-hook-form@7.61.1(react@18.3.1)) - '@radix-ui/react-accordion': - specifier: 1.2.11 - version: 1.2.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-alert-dialog': - specifier: 1.1.14 - version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-checkbox': - specifier: 1.3.2 - version: 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-dialog': - specifier: 1.1.14 - version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-dropdown-menu': - specifier: 2.1.15 - version: 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-label': - specifier: 2.1.7 - version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-navigation-menu': - specifier: 1.2.13 - version: 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-popover': - specifier: 1.1.14 - version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-progress': - specifier: 1.1.7 - version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-select': - specifier: 2.2.5 - version: 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-separator': - specifier: 1.1.7 - version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': - specifier: 1.2.3 - version: 1.2.3(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-switch': - specifier: 1.2.5 - version: 1.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-tabs': - specifier: 1.1.12 - version: 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-tooltip': - specifier: 1.2.7 - version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tailwindcss/typography': - specifier: 0.5.16 - version: 0.5.16(tailwindcss@3.4.17) - '@uiw/react-textarea-code-editor': - specifier: 3.1.1 - version: 3.1.1(@babel/runtime@7.27.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - class-variance-authority: - specifier: 0.7.1 - version: 0.7.1 - classnames: - specifier: 2.5.1 - version: 2.5.1 - clsx: - specifier: 2.1.1 - version: 2.1.1 - cmdk: - specifier: 1.1.1 - version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - highlight.js: - specifier: 11.11.1 - version: 11.11.1 - next: - specifier: ^14.0.4 - version: 14.2.30(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next-themes: - specifier: 0.4.6 - version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - pg: - specifier: ^8.12.0 - version: 8.16.2 - pusher: - specifier: 5.2.0 - version: 5.2.0 - pusher-js: - specifier: 8.4.0 - version: 8.4.0 - react: - specifier: ^18.2.0 - version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) - react-hook-form: - specifier: 7.61.1 - version: 7.61.1(react@18.3.1) - react-markdown: - specifier: 10.1.0 - version: 10.1.0(@types/react@18.3.23)(react@18.3.1) - rehype-highlight: - specifier: 7.0.2 - version: 7.0.2 - rehype-sanitize: - specifier: 6.0.0 - version: 6.0.0 - remark-gfm: - specifier: 4.0.1 - version: 4.0.1 - sonner: - specifier: 2.0.6 - version: 2.0.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - tailwind-merge: - specifier: 3.3.1 - version: 3.3.1 - tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 - tailwindcss-animate: - specifier: 1.0.7 - version: 1.0.7(tailwindcss@3.4.17) - typeorm: - specifier: 0.3.25 - version: 0.3.25(better-sqlite3@11.10.0)(mysql2@3.14.1)(pg@8.16.2)(reflect-metadata@0.2.2) - ws: - specifier: ^8.14.2 - version: 8.18.2 - zod: - specifier: ^3.25.67 - version: 3.25.67 - zustand: - specifier: 5.0.7 - version: 5.0.7(@types/react@18.3.23)(react@18.3.1) - devDependencies: - '@types/node': - specifier: ^20.10.6 - version: 20.19.1 - '@types/pg': - specifier: ^8.11.0 - version: 8.15.4 - '@types/react': - specifier: ^18.2.48 - version: 18.3.23 - '@types/react-dom': - specifier: ^18.2.18 - version: 18.3.7(@types/react@18.3.23) - '@types/ws': - specifier: ^8.5.10 - version: 8.18.1 - autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.6) - concurrently: - specifier: 9.2.0 - version: 9.2.0 - date-fns: - specifier: ^3.2.0 - version: 3.6.0 - lucide-react: - specifier: ^0.323.0 - version: 0.323.0(react@18.3.1) - postcss: - specifier: ^8.5.6 - version: 8.5.6 - recharts: - specifier: ^2.10.3 - version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - rimraf: - specifier: ^5.0.5 - version: 5.0.10 - typescript: - specifier: ^5.3.3 - version: 5.8.3 - packages: '@alloc/quick-lru@5.2.0': @@ -449,8 +449,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + '@babel/runtime@7.28.3': + resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} engines: {node: '>=6.9.0'} '@babel/types@7.28.1': @@ -752,14 +752,14 @@ packages: cpu: [x64] os: [win32] - '@floating-ui/core@1.7.2': - resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} - '@floating-ui/dom@1.7.2': - resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} - '@floating-ui/react-dom@2.1.4': - resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==} + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' @@ -783,27 +783,16 @@ packages: '@jridgewell/gen-mapping@0.3.12': resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.10': resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} @@ -811,59 +800,59 @@ packages: resolution: {integrity: sha512-P5FZsXU0kY881F6Hbk9GhsYx02/KgWK1DYf7/tyE/1lcFKhDYPQR9iYjhQXJn+Sg6hQleMo3DB7h7+p4wgp2Lw==} engines: {node: '>=18'} - '@next/env@14.2.30': - resolution: {integrity: sha512-KBiBKrDY6kxTQWGzKjQB7QirL3PiiOkV7KW98leHFjtVRKtft76Ra5qSA/SL75xT44dp6hOcqiiJ6iievLOYug==} + '@next/env@14.2.32': + resolution: {integrity: sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==} - '@next/swc-darwin-arm64@14.2.30': - resolution: {integrity: sha512-EAqfOTb3bTGh9+ewpO/jC59uACadRHM6TSA9DdxJB/6gxOpyV+zrbqeXiFTDy9uV6bmipFDkfpAskeaDcO+7/g==} + '@next/swc-darwin-arm64@14.2.32': + resolution: {integrity: sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.30': - resolution: {integrity: sha512-TyO7Wz1IKE2kGv8dwQ0bmPL3s44EKVencOqwIY69myoS3rdpO1NPg5xPM5ymKu7nfX4oYJrpMxv8G9iqLsnL4A==} + '@next/swc-darwin-x64@14.2.32': + resolution: {integrity: sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.30': - resolution: {integrity: sha512-I5lg1fgPJ7I5dk6mr3qCH1hJYKJu1FsfKSiTKoYwcuUf53HWTrEkwmMI0t5ojFKeA6Vu+SfT2zVy5NS0QLXV4Q==} + '@next/swc-linux-arm64-gnu@14.2.32': + resolution: {integrity: sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.30': - resolution: {integrity: sha512-8GkNA+sLclQyxgzCDs2/2GSwBc92QLMrmYAmoP2xehe5MUKBLB2cgo34Yu242L1siSkwQkiV4YLdCnjwc/Micw==} + '@next/swc-linux-arm64-musl@14.2.32': + resolution: {integrity: sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.30': - resolution: {integrity: sha512-8Ly7okjssLuBoe8qaRCcjGtcMsv79hwzn/63wNeIkzJVFVX06h5S737XNr7DZwlsbTBDOyI6qbL2BJB5n6TV/w==} + '@next/swc-linux-x64-gnu@14.2.32': + resolution: {integrity: sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.30': - resolution: {integrity: sha512-dBmV1lLNeX4mR7uI7KNVHsGQU+OgTG5RGFPi3tBJpsKPvOPtg9poyav/BYWrB3GPQL4dW5YGGgalwZ79WukbKQ==} + '@next/swc-linux-x64-musl@14.2.32': + resolution: {integrity: sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.30': - resolution: {integrity: sha512-6MMHi2Qc1Gkq+4YLXAgbYslE1f9zMGBikKMdmQRHXjkGPot1JY3n5/Qrbg40Uvbi8//wYnydPnyvNhI1DMUW1g==} + '@next/swc-win32-arm64-msvc@14.2.32': + resolution: {integrity: sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.30': - resolution: {integrity: sha512-pVZMnFok5qEX4RT59mK2hEVtJX+XFfak+/rjHpyFh7juiT52r177bfFKhnlafm0UOSldhXjj32b+LZIOdswGTg==} + '@next/swc-win32-ia32-msvc@14.2.32': + resolution: {integrity: sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.30': - resolution: {integrity: sha512-4KCo8hMZXMjpTzs3HOqOGYYwAXymXIy7PEPAXNEcEOyKqkjiDlECumrWziy+JEF0Oi4ILHGxzgQ3YiMGG2t/Lg==} + '@next/swc-win32-x64-msvc@14.2.32': + resolution: {integrity: sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1551,8 +1540,8 @@ packages: peerDependencies: '@types/react': ^18.0.0 - '@types/react@18.3.23': - resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + '@types/react@18.3.24': + resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==} '@types/semver@7.7.0': resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} @@ -1747,8 +1736,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.25.0: - resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==} + browserslist@4.25.3: + resolution: {integrity: sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1789,8 +1778,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001724: - resolution: {integrity: sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==} + caniuse-lite@1.0.30001737: + resolution: {integrity: sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2106,8 +2095,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.171: - resolution: {integrity: sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==} + electron-to-chromium@1.5.208: + resolution: {integrity: sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==} emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -2964,8 +2953,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@14.2.30: - resolution: {integrity: sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==} + next@14.2.32: + resolution: {integrity: sha512-fg5g0GZ7/nFc09X8wLe6pNSU8cLWbLRG3TZzPJ1BJvi2s9m7eF991se67wliM9kR5yLHRkyGKU49MMx58s3LJg==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -3981,8 +3970,8 @@ packages: vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} - vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -4097,8 +4086,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.2: - resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4176,7 +4165,7 @@ snapshots: dependencies: '@babel/types': 7.28.1 - '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.3': {} '@babel/types@7.28.1': dependencies: @@ -4332,18 +4321,18 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@floating-ui/core@1.7.2': + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.2': + '@floating-ui/dom@1.7.4': dependencies: - '@floating-ui/core': 1.7.2 + '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.7.2 + '@floating-ui/dom': 1.7.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -4370,16 +4359,8 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.29 - '@jridgewell/gen-mapping@0.3.8': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.10': dependencies: '@jridgewell/gen-mapping': 0.3.12 @@ -4388,11 +4369,6 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -4414,33 +4390,33 @@ snapshots: transitivePeerDependencies: - supports-color - '@next/env@14.2.30': {} + '@next/env@14.2.32': {} - '@next/swc-darwin-arm64@14.2.30': + '@next/swc-darwin-arm64@14.2.32': optional: true - '@next/swc-darwin-x64@14.2.30': + '@next/swc-darwin-x64@14.2.32': optional: true - '@next/swc-linux-arm64-gnu@14.2.30': + '@next/swc-linux-arm64-gnu@14.2.32': optional: true - '@next/swc-linux-arm64-musl@14.2.30': + '@next/swc-linux-arm64-musl@14.2.32': optional: true - '@next/swc-linux-x64-gnu@14.2.30': + '@next/swc-linux-x64-gnu@14.2.32': optional: true - '@next/swc-linux-x64-musl@14.2.30': + '@next/swc-linux-x64-musl@14.2.32': optional: true - '@next/swc-win32-arm64-msvc@14.2.30': + '@next/swc-win32-arm64-msvc@14.2.32': optional: true - '@next/swc-win32-ia32-msvc@14.2.30': + '@next/swc-win32-ia32-msvc@14.2.32': optional: true - '@next/swc-win32-x64-msvc@14.2.30': + '@next/swc-win32-x64-msvc@14.2.32': optional: true '@nodelib/fs.scandir@2.1.5': @@ -4464,494 +4440,494 @@ snapshots: '@radix-ui/primitive@1.1.2': {} - '@radix-ui/react-accordion@1.2.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-accordion@1.2.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-alert-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-checkbox@1.3.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-checkbox@1.3.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-collapsible@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collapsible@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.24)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-context@1.1.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-context@1.1.2(@types/react@18.3.24)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-direction@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-direction@1.1.1(@types/react@18.3.24)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.24)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-id@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-id@1.1.1(@types/react@18.3.24)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-label@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-label@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-navigation-menu@1.2.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-navigation-menu@1.2.13(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-popover@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-popover@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) - - '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) '@radix-ui/rect': 1.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-progress@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-progress@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-select@2.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-select@2.2.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) aria-hidden: 1.2.6 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-separator@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-separator@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-slot@1.2.3(@types/react@18.3.24)(react@18.3.1)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-switch@1.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-switch@1.2.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-tabs@1.1.12(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tabs@1.1.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.24)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.24)(react@18.3.1)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.24)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.24)(react@18.3.1)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.24)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.24)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.24)(react@18.3.1)': dependencies: '@radix-ui/rect': 1.1.1 react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-use-size@1.1.1(@types/react@18.3.23)(react@18.3.1)': + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.24)(react@18.3.1)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) react: 18.3.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) '@radix-ui/rect@1.1.1': {} @@ -5109,11 +5085,11 @@ snapshots: '@types/prop-types@15.7.15': {} - '@types/react-dom@18.3.7(@types/react@18.3.23)': + '@types/react-dom@18.3.7(@types/react@18.3.24)': dependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - '@types/react@18.3.23': + '@types/react@18.3.24': dependencies: '@types/prop-types': 15.7.15 csstype: 3.1.3 @@ -5134,9 +5110,9 @@ snapshots: dependencies: '@types/node': 20.19.1 - '@uiw/react-textarea-code-editor@3.1.1(@babel/runtime@7.27.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@uiw/react-textarea-code-editor@3.1.1(@babel/runtime@7.28.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) rehype: 13.0.2 @@ -5269,8 +5245,8 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: - browserslist: 4.25.0 - caniuse-lite: 1.0.30001724 + browserslist: 4.25.3 + caniuse-lite: 1.0.30001737 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -5343,12 +5319,12 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.25.0: + browserslist@4.25.3: dependencies: - caniuse-lite: 1.0.30001724 - electron-to-chromium: 1.5.171 + caniuse-lite: 1.0.30001737 + electron-to-chromium: 1.5.208 node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.0) + update-browserslist-db: 1.1.3(browserslist@4.25.3) buffer-from@1.1.2: optional: true @@ -5390,7 +5366,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001724: {} + caniuse-lite@1.0.30001737: {} ccount@2.0.1: {} @@ -5489,12 +5465,12 @@ snapshots: clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -5667,7 +5643,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.3 csstype: 3.1.3 dom-serializer@2.0.0: @@ -5700,7 +5676,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.171: {} + electron-to-chromium@1.5.208: {} emoji-regex@10.4.0: {} @@ -6019,7 +5995,7 @@ snapshots: hast-util-from-parse5: 8.0.3 parse5: 7.3.0 vfile: 6.0.3 - vfile-message: 4.0.2 + vfile-message: 4.0.3 hast-util-from-parse5@8.0.3: dependencies: @@ -6080,7 +6056,7 @@ snapshots: space-separated-tokens: 2.0.2 style-to-js: 1.1.17 unist-util-position: 5.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 transitivePeerDependencies: - supports-color @@ -6494,7 +6470,7 @@ snapshots: parse-entities: 4.0.2 stringify-entities: 4.0.4 unist-util-stringify-position: 4.0.0 - vfile-message: 4.0.2 + vfile-message: 4.0.3 transitivePeerDependencies: - supports-color @@ -6813,27 +6789,27 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@14.2.30(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.32(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.2.30 + '@next/env': 14.2.32 '@swc/helpers': 0.5.5 busboy: 1.6.0 - caniuse-lite: 1.0.30001724 + caniuse-lite: 1.0.30001737 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.30 - '@next/swc-darwin-x64': 14.2.30 - '@next/swc-linux-arm64-gnu': 14.2.30 - '@next/swc-linux-arm64-musl': 14.2.30 - '@next/swc-linux-x64-gnu': 14.2.30 - '@next/swc-linux-x64-musl': 14.2.30 - '@next/swc-win32-arm64-msvc': 14.2.30 - '@next/swc-win32-ia32-msvc': 14.2.30 - '@next/swc-win32-x64-msvc': 14.2.30 + '@next/swc-darwin-arm64': 14.2.32 + '@next/swc-darwin-x64': 14.2.32 + '@next/swc-linux-arm64-gnu': 14.2.32 + '@next/swc-linux-arm64-musl': 14.2.32 + '@next/swc-linux-x64-gnu': 14.2.32 + '@next/swc-linux-x64-musl': 14.2.32 + '@next/swc-win32-arm64-msvc': 14.2.32 + '@next/swc-win32-ia32-msvc': 14.2.32 + '@next/swc-win32-x64-msvc': 14.2.32 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -7148,11 +7124,11 @@ snapshots: react-is@18.3.1: {} - react-markdown@10.1.0(@types/react@18.3.23)(react@18.3.1): + react-markdown@10.1.0(@types/react@18.3.24)(react@18.3.1): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 18.3.23 + '@types/react': 18.3.24 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 @@ -7166,24 +7142,24 @@ snapshots: transitivePeerDependencies: - supports-color - react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1): + react-remove-scroll-bar@2.3.8(@types/react@18.3.24)(react@18.3.1): dependencies: react: 18.3.1 - react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.24)(react@18.3.1) tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - react-remove-scroll@2.7.1(@types/react@18.3.23)(react@18.3.1): + react-remove-scroll@2.7.1(@types/react@18.3.24)(react@18.3.1): dependencies: react: 18.3.1 - react-remove-scroll-bar: 2.3.8(@types/react@18.3.23)(react@18.3.1) - react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + react-remove-scroll-bar: 2.3.8(@types/react@18.3.24)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.24)(react@18.3.1) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@18.3.23)(react@18.3.1) - use-sidecar: 1.1.3(@types/react@18.3.23)(react@18.3.1) + use-callback-ref: 1.3.3(@types/react@18.3.24)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.24)(react@18.3.1) optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -7193,17 +7169,17 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-style-singleton@2.2.3(@types/react@18.3.23)(react@18.3.1): + react-style-singleton@2.2.3(@types/react@18.3.24)(react@18.3.1): dependencies: get-nonce: 1.0.1 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.3 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -7608,7 +7584,7 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.12 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4 @@ -7880,9 +7856,9 @@ snapshots: unpipe@1.0.0: {} - update-browserslist-db@1.1.3(browserslist@4.25.0): + update-browserslist-db@1.1.3(browserslist@4.25.3): dependencies: - browserslist: 4.25.0 + browserslist: 4.25.3 escalade: 3.2.0 picocolors: 1.1.1 @@ -7890,20 +7866,20 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@18.3.23)(react@18.3.1): + use-callback-ref@1.3.3(@types/react@18.3.24)(react@18.3.1): dependencies: react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 - use-sidecar@1.1.3(@types/react@18.3.23)(react@18.3.1): + use-sidecar@1.1.3(@types/react@18.3.24)(react@18.3.1): dependencies: detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 util-deprecate@1.0.2: {} @@ -7918,7 +7894,7 @@ snapshots: '@types/unist': 3.0.3 vfile: 6.0.3 - vfile-message@4.0.2: + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 @@ -7926,7 +7902,7 @@ snapshots: vfile@6.0.3: dependencies: '@types/unist': 3.0.3 - vfile-message: 4.0.2 + vfile-message: 4.0.3 victory-vendor@36.9.2: dependencies: @@ -8064,7 +8040,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.2: {} + ws@8.18.3: {} xtend@4.0.2: {} @@ -8090,9 +8066,9 @@ snapshots: zod@3.25.67: {} - zustand@5.0.7(@types/react@18.3.23)(react@18.3.1): + zustand@5.0.7(@types/react@18.3.24)(react@18.3.1): optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 18.3.24 react: 18.3.1 zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c209b25c..efb95e02 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - packages/* + - apps/* onlyBuiltDependencies: - better-sqlite3 From dcc9ad75044951834462d8174afa07a60623854f Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 21:19:08 +0800 Subject: [PATCH 28/50] feat: add file naming validation script to enforce kebab-case conventions --- package.json | 1 + scripts/validation/validate-all.ts | 174 +++++++++++++++ scripts/validation/validate-file-naming.ts | 240 +++++++++++++++++++++ 3 files changed, 415 insertions(+) create mode 100644 scripts/validation/validate-all.ts create mode 100644 scripts/validation/validate-file-naming.ts diff --git a/package.json b/package.json index c8eb5bfd..18be1dc5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "validate:list": "pnpm exec tsx scripts/validation/validate-all.ts --list", "validate:quick": "pnpm exec tsx scripts/validation/validate-all.ts --quick", "validate:imports": "pnpm exec tsx scripts/validation/validate-imports.ts", + "validate:naming": "pnpm exec tsx scripts/validation/validate-file-naming.ts", "validate:api": "pnpm exec tsx scripts/validation/validate-api-standardization-ast.ts", "validate:envelopes": "pnpm exec tsx scripts/validation/validate-response-envelopes-ast.ts", "validate:architecture": "pnpm exec tsx scripts/validation/validate-architecture-patterns-ast.ts", diff --git a/scripts/validation/validate-all.ts b/scripts/validation/validate-all.ts new file mode 100644 index 00000000..0acc32f8 --- /dev/null +++ b/scripts/validation/validate-all.ts @@ -0,0 +1,174 @@ +#!/usr/bin/env -S pnpm exec tsx + +/** + * Validation Runner - Runs all validation scripts + * Orchestrates multiple validation checks across the codebase + */ + +import { spawn } from 'child_process'; +import path from 'path'; + +interface ValidationResult { + name: string; + script: string; + passed: boolean; + output: string; + error?: string; +} + +const VALIDATION_SCRIPTS = [ + { + name: 'Import Patterns', + script: 'validate-imports.ts', + description: 'Validates ESM import patterns and cross-package imports', + }, + { + name: 'File Naming Conventions', + script: 'validate-file-naming.ts', + description: 'Validates kebab-case naming conventions in web directory', + }, + { + name: 'API Standardization', + script: 'validate-api-standardization-ast.ts', + description: 'Validates API response format standardization', + }, + { + name: 'Response Envelopes', + script: 'validate-response-envelopes-ast.ts', + description: 'Validates response envelope patterns', + }, + { + name: 'Architecture Patterns', + script: 'validate-architecture-patterns-ast.ts', + description: 'Validates architectural consistency patterns', + }, +]; + +/** + * Run a single validation script + */ +async function runValidation(scriptPath: string): Promise<{ passed: boolean; output: string; error?: string }> { + return new Promise((resolve) => { + const child = spawn('pnpm', ['exec', 'tsx', scriptPath], { + stdio: 'pipe', + cwd: process.cwd(), + }); + + let output = ''; + let error = ''; + + child.stdout?.on('data', (data) => { + output += data.toString(); + }); + + child.stderr?.on('data', (data) => { + error += data.toString(); + }); + + child.on('close', (code) => { + resolve({ + passed: code === 0, + output: output.trim(), + error: error.trim() || undefined, + }); + }); + + child.on('error', (err) => { + resolve({ + passed: false, + output: '', + error: err.message, + }); + }); + }); +} + +/** + * Main validation runner + */ +async function runAllValidations(): Promise { + const args = process.argv.slice(2); + const showList = args.includes('--list'); + const quickMode = args.includes('--quick'); + + if (showList) { + console.log('📋 Available validation scripts:\n'); + VALIDATION_SCRIPTS.forEach((script, index) => { + console.log(`${index + 1}. ${script.name}`); + console.log(` ${script.description}`); + console.log(` Script: ${script.script}\n`); + }); + return; + } + + console.log('🔍 Running all validation scripts...\n'); + + const results: ValidationResult[] = []; + const scriptsToRun = quickMode + ? VALIDATION_SCRIPTS.filter(s => ['validate-imports.ts', 'validate-file-naming.ts'].includes(s.script)) + : VALIDATION_SCRIPTS; + + // Run validations sequentially to avoid overwhelming output + for (const script of scriptsToRun) { + const scriptPath = path.join('scripts/validation', script.script); + console.log(`🔍 Running: ${script.name}...`); + + const result = await runValidation(scriptPath); + results.push({ + name: script.name, + script: script.script, + ...result, + }); + + if (result.passed) { + console.log(`✅ ${script.name} - PASSED`); + } else { + console.log(`❌ ${script.name} - FAILED`); + if (!quickMode) { + console.log(result.output); + if (result.error) { + console.log(`Error: ${result.error}`); + } + } + } + console.log(''); + } + + // Summary + const passed = results.filter(r => r.passed).length; + const total = results.length; + const failed = results.filter(r => !r.passed); + + console.log('📊 Validation Summary:'); + console.log(` Passed: ${passed}/${total}`); + + if (failed.length > 0) { + console.log(` Failed: ${failed.length}`); + console.log('\n❌ Failed validations:'); + failed.forEach(result => { + console.log(` - ${result.name}`); + }); + + if (quickMode && failed.length > 0) { + console.log('\n💡 Run without --quick flag to see detailed error messages'); + } + } + + if (passed === total) { + console.log('\n✅ All validations passed!'); + process.exit(0); + } else { + console.log('\n❌ Some validations failed. Please review and fix the issues above.'); + process.exit(1); + } +} + +// Run validation if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + runAllValidations().catch((error) => { + console.error('💥 Validation runner failed:', error); + process.exit(1); + }); +} + +export { runAllValidations }; \ No newline at end of file diff --git a/scripts/validation/validate-file-naming.ts b/scripts/validation/validate-file-naming.ts new file mode 100644 index 00000000..d445e9fb --- /dev/null +++ b/scripts/validation/validate-file-naming.ts @@ -0,0 +1,240 @@ +#!/usr/bin/env -S pnpm exec tsx + +/** + * File Naming Convention Validation + * Validates that all files in the web directory follow kebab-case naming conventions + */ + +import fs from 'fs'; +import path from 'path'; + +interface ValidationError { + file: string; + message: string; + suggestion: string; +} + +const ERRORS: ValidationError[] = []; + +/** + * Check if a filename follows kebab-case convention + */ +function isKebabCase(filename: string): boolean { + // Handle special file patterns (e.g., .test.ts, .spec.ts, .d.ts) + // Extract the base name before any special suffixes + let nameToValidate = filename; + + // Remove common file suffixes that should be preserved + const specialSuffixes = ['.test', '.spec', '.d']; + const extensions = ['.ts', '.tsx', '.js', '.jsx', '.css', '.scss', '.md', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.otf']; + + // Remove extension first (handle compound extensions like .woff2) + let hasExtension = ''; + for (const ext of extensions.sort((a, b) => b.length - a.length)) { // Sort by length to match longer extensions first + if (filename.endsWith(ext)) { + hasExtension = ext; + nameToValidate = filename.slice(0, -ext.length); + break; + } + } + + // Remove special suffixes + const hasSpecialSuffix = specialSuffixes.find(suffix => nameToValidate.endsWith(suffix)); + if (hasSpecialSuffix) { + nameToValidate = nameToValidate.slice(0, -hasSpecialSuffix.length); + } + + // Kebab-case pattern: lowercase letters, numbers, and hyphens only + // Cannot start or end with hyphens, cannot have consecutive hyphens + const kebabCasePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/; + + return kebabCasePattern.test(nameToValidate); +} + +/** + * Convert a filename to suggested kebab-case format + */ +function toKebabCase(filename: string): string { + // Handle special file patterns (e.g., .test.ts, .spec.ts, .d.ts) + let nameToConvert = filename; + let preservedSuffix = ''; + + // Extract and preserve special suffixes and extensions + const specialSuffixes = ['.test', '.spec', '.d']; + const extensions = ['.ts', '.tsx', '.js', '.jsx', '.css', '.scss', '.md']; + + // Remove and preserve extension + const hasExtension = extensions.find(ext => filename.endsWith(ext)); + if (hasExtension) { + preservedSuffix = hasExtension + preservedSuffix; + nameToConvert = filename.slice(0, -hasExtension.length); + } + + // Remove and preserve special suffixes + const hasSpecialSuffix = specialSuffixes.find(suffix => nameToConvert.endsWith(suffix)); + if (hasSpecialSuffix) { + preservedSuffix = hasSpecialSuffix + preservedSuffix; + nameToConvert = nameToConvert.slice(0, -hasSpecialSuffix.length); + } + + // Convert to kebab-case + const kebabName = nameToConvert + .replace(/([a-z])([A-Z])/g, '$1-$2') // Convert camelCase to kebab-case + .replace(/[_\s]+/g, '-') // Replace underscores and spaces with hyphens + .replace(/[^a-zA-Z0-9-]/g, '') // Remove special characters except hyphens + .toLowerCase() + .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen + .replace(/^-+|-+$/g, ''); // Remove leading and trailing hyphens + + return kebabName + preservedSuffix; +} + +/** + * Check if a file should be excluded from validation + */ +function shouldExcludeFile(filePath: string, filename: string): boolean { + // Exclude certain file patterns that are standard or generated + const excludePatterns = [ + // Next.js specific files + /^layout\.(tsx?|jsx?)$/, + /^page\.(tsx?|jsx?)$/, + /^loading\.(tsx?|jsx?)$/, + /^error\.(tsx?|jsx?)$/, + /^not-found\.(tsx?|jsx?)$/, + /^route\.(tsx?|jsx?)$/, + /^middleware\.(tsx?|jsx?)$/, + /^globals\.css$/, + + // Common config/meta files + /^index\.(tsx?|jsx?|js|ts)$/, + /^README\.md$/, + /^\..*$/, // Hidden files + + // Generated or build files + /^next-env\.d\.ts$/, + /.*\.d\.ts$/, + + // Config files + /.*\.config\.(js|ts|mjs|cjs)$/, + /.*\.json$/, + + // Asset files (images, fonts, icons, etc.) + /.*\.(svg|png|jpg|jpeg|gif|webp|ico|bmp|tiff?)$/i, + /.*\.(woff2?|ttf|eot|otf)$/i, + /.*\.(mp4|webm|ogg|mp3|wav|flac|aac)$/i, + /.*\.(pdf|doc|docx|xls|xlsx|ppt|pptx)$/i, + + // Dynamic route files (Next.js pattern) + /^\[.*\]\.(tsx?|jsx?)$/, + /^\[.*\]$/, + ]; + + // Check if filename matches any exclude pattern + const isExcluded = excludePatterns.some(pattern => pattern.test(filename)); + + // Also exclude directories that are dynamic routes + if (fs.statSync(filePath).isDirectory() && /^\[.*\]$/.test(filename)) { + return true; + } + + // Exclude entire public directory (assets) + if (filePath.includes('/public/')) { + return true; + } + + return isExcluded; +} + +/** + * Validate file naming in a directory recursively + */ +function validateDirectory(dirPath: string): void { + if (!fs.existsSync(dirPath)) { + return; + } + + const entries = fs.readdirSync(dirPath); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + // Skip certain directories + if (['node_modules', '.next', '.next-build', 'build', 'dist', 'coverage', '.turbo'].includes(entry)) { + continue; + } + + // Validate directory name if not excluded + if (!shouldExcludeFile(fullPath, entry)) { + if (!isKebabCase(entry)) { + ERRORS.push({ + file: fullPath, + message: `Directory name "${entry}" should follow kebab-case convention`, + suggestion: `Rename to "${toKebabCase(entry)}"`, + }); + } + } + + // Recursively validate subdirectory + validateDirectory(fullPath); + } else { + // Validate file name if not excluded + if (!shouldExcludeFile(fullPath, entry)) { + if (!isKebabCase(entry)) { + ERRORS.push({ + file: fullPath, + message: `File name "${entry}" should follow kebab-case convention`, + suggestion: `Rename to "${toKebabCase(entry)}"`, + }); + } + } + } + } +} + +/** + * Main validation function + */ +function validateFileNaming(): void { + console.log('🔍 Validating file naming conventions for web directory...'); + + const webDir = path.join(process.cwd(), 'apps/web'); + + if (!fs.existsSync(webDir)) { + console.log('❌ Web directory not found at apps/web'); + process.exit(1); + } + + console.log(` Checking files in: ${webDir}`); + validateDirectory(webDir); + + // Report results + if (ERRORS.length === 0) { + console.log('✅ All files follow kebab-case naming conventions!'); + process.exit(0); + } else { + console.log(`\n❌ Found ${ERRORS.length} file naming violations:\n`); + + ERRORS.forEach((error) => { + console.log(`📁 ${error.file}`); + console.log(` ${error.message}`); + console.log(` 💡 ${error.suggestion}\n`); + }); + + console.log('💡 Kebab-case convention:'); + console.log(' - Use lowercase letters and numbers only'); + console.log(' - Separate words with hyphens (-)'); + console.log(' - No underscores, spaces, or camelCase'); + console.log(' - Examples: user-profile.tsx, auth-button.tsx, api-client.ts'); + + process.exit(1); + } +} + +// Run validation if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + validateFileNaming(); +} + +export { validateFileNaming }; \ No newline at end of file From 17831aa6a909e4e3784ae751f6ec50c53cba7762 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 21:21:04 +0800 Subject: [PATCH 29/50] refactor: update package structure from 'packages/web' to 'apps/web' - Changed references in build scripts, Dockerfiles, and workflows to reflect the new package structure. - Updated documentation to guide users on the new directory layout. - Adjusted validation scripts to ensure compatibility with the new structure. --- .github/scripts/README.md | 2 +- .github/scripts/verify-build.sh | 2 +- .github/workflows/main.yml | 6 +- Dockerfile | 16 +- Dockerfile.dev | 2 +- docker-compose.yml | 2 +- docs/design/visual-design-system.md | 4 +- docs/guides/VERCEL_DEPLOYMENT.md | 4 +- next.config.js | 2 +- .../validate-api-standardization-ast.ts | 224 ++++++++++------ scripts/validation/validate-imports.ts | 22 +- .../validate-response-envelopes-ast.ts | 250 +++++++++++------- vitest.workspace.ts | 4 +- 13 files changed, 321 insertions(+), 219 deletions(-) diff --git a/.github/scripts/README.md b/.github/scripts/README.md index d73c9f98..0a104b09 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -28,7 +28,7 @@ Builds all packages in dependency order (core → ai → mcp → cli → web). ./.github/scripts/build-packages.sh ``` - **Dependencies**: Requires pnpm workspace setup -- **Output**: Build artifacts in `packages/*/build` and `packages/web/.next-build` +- **Output**: Build artifacts in `packages/*/build` and `apps/web/.next-build` #### `verify-build.sh` Verifies that all expected build artifacts exist. diff --git a/.github/scripts/verify-build.sh b/.github/scripts/verify-build.sh index 4af57a9b..6e578a28 100755 --- a/.github/scripts/verify-build.sh +++ b/.github/scripts/verify-build.sh @@ -39,7 +39,7 @@ else fi # Check web package -if [ -d "packages/web/.next" ]; then +if [ -d "apps/web/.next" ]; then echo "✅ Web package build artifacts verified" else echo "❌ Web package build artifacts missing" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c5c972d0..ac3e5aa2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -77,7 +77,7 @@ jobs: with: path: | packages/*/build - packages/web/.next-build + apps/web/.next-build key: build-${{ github.sha }}-${{ matrix.node-version }} - name: Verify build artifacts @@ -191,7 +191,7 @@ jobs: with: path: | packages/*/build - packages/web/.next-build + apps/web/.next-build key: build-${{ github.sha }}-20 - name: Check versions and determine what to publish @@ -275,7 +275,7 @@ jobs: with: path: | packages/*/build - packages/web/.next-build + apps/web/.next-build key: build-${{ github.sha }}-20 - name: Bump to dev prerelease versions diff --git a/Dockerfile b/Dockerfile index 0b00041b..9bc2df71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ FROM base AS deps # Copy package.json files for proper dependency resolution COPY packages/core/package.json ./packages/core/ COPY packages/ai/package.json ./packages/ai/ -COPY packages/web/package.json ./packages/web/ +COPY apps/web/package.json ./apps/web/ # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile @@ -39,12 +39,12 @@ FROM base AS builder COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/packages/core/node_modules ./packages/core/node_modules COPY --from=deps /app/packages/ai/node_modules ./packages/ai/node_modules -COPY --from=deps /app/packages/web/node_modules ./packages/web/node_modules +COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules # Copy source code (excluding MCP package) COPY packages/core ./packages/core COPY packages/ai ./packages/ai -COPY packages/web ./packages/web +COPY apps/web ./apps/web COPY tsconfig.json ./ COPY vitest.config.base.ts ./ @@ -75,12 +75,12 @@ RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs # Copy the standalone build output and static files -COPY --from=builder /app/packages/web/.next-build/standalone ./ -COPY --from=builder /app/packages/web/.next-build/static ./packages/web/.next-build/static -COPY --from=builder /app/packages/web/public ./packages/web/public +COPY --from=builder /app/apps/web/.next-build/standalone ./ +COPY --from=builder /app/apps/web/.next-build/static ./apps/web/.next-build/static +COPY --from=builder /app/apps/web/public ./apps/web/public # Create directories that the application might need and set permissions -RUN mkdir -p /app/packages/web/.devlog /app/.devlog && \ +RUN mkdir -p /app/apps/web/.devlog /app/.devlog && \ chown -R nextjs:nodejs /app # Set correct permissions @@ -93,4 +93,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD node -e "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))" # Start the Next.js application using the standalone server -CMD ["node", "packages/web/server.js"] +CMD ["node", "apps/web/server.js"] diff --git a/Dockerfile.dev b/Dockerfile.dev index 1ca67103..24a553c3 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -18,7 +18,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ # Copy package files for web application dependencies only COPY packages/ai/package.json ./packages/ai/ COPY packages/core/package.json ./packages/core/ -COPY packages/web/package.json ./packages/web/ +COPY apps/web/package.json ./apps/web/ # Install dependencies RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install diff --git a/docker-compose.yml b/docker-compose.yml index 17e051d8..ea057ea9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,7 +55,7 @@ services: volumes: - .:/app - /app/node_modules - - /app/packages/web/.next + - /app/apps/web/.next - './scripts:/app/scripts' env_file: - .env diff --git a/docs/design/visual-design-system.md b/docs/design/visual-design-system.md index 2e8d5b3b..9cd0e5c0 100644 --- a/docs/design/visual-design-system.md +++ b/docs/design/visual-design-system.md @@ -43,8 +43,8 @@ This document outlines the revised color scheme and icon system for devlog statu 5. **Distinctiveness**: Each category is easily distinguishable ## Implementation Files -- `packages/web/lib/devlog-ui-utils.tsx` - Core color and icon functions -- `packages/web/components/ui/DevlogTags.tsx` - Tag components using the utilities +- `apps/web/lib/devlog-ui-utils.tsx` - Core color and icon functions +- `apps/web/components/ui/DevlogTags.tsx` - Tag components using the utilities ## Benefits - **Faster Scanning**: Users can quickly identify work types and status diff --git a/docs/guides/VERCEL_DEPLOYMENT.md b/docs/guides/VERCEL_DEPLOYMENT.md index e94495c3..d7281b2f 100644 --- a/docs/guides/VERCEL_DEPLOYMENT.md +++ b/docs/guides/VERCEL_DEPLOYMENT.md @@ -15,7 +15,7 @@ This guide walks you through deploying the devlog web interface to Vercel with P 1. Go to [vercel.com](https://vercel.com) and sign in 2. Click "Import Project" 3. Connect your GitHub account and select this repository -4. **Important**: Set the **Root Directory** to `/` (repository root, not `packages/web`) +4. **Important**: Set the **Root Directory** to `/` (repository root, not `apps/web`) ### Step 2: Configure Build Settings @@ -25,7 +25,7 @@ Vercel should automatically detect the `vercel.json` configuration, but verify: - **Root Directory**: `/` (repository root) - **Build Command**: `pnpm run build:vercel` - **Install Command**: `pnpm install --frozen-lockfile` -- **Output Directory**: `packages/web/.next-build` +- **Output Directory**: `apps/web/.next-build` ### Step 3: Add PostgreSQL Database diff --git a/next.config.js b/next.config.js index 55176d36..37f5617c 100644 --- a/next.config.js +++ b/next.config.js @@ -1,2 +1,2 @@ // Vercel Next.js detection file - points to the actual web package -module.exports = require('./packages/web/next.config.js'); +module.exports = require('./apps/web/next.config.js'); diff --git a/scripts/validation/validate-api-standardization-ast.ts b/scripts/validation/validate-api-standardization-ast.ts index bc93e4f5..19470d88 100755 --- a/scripts/validation/validate-api-standardization-ast.ts +++ b/scripts/validation/validate-api-standardization-ast.ts @@ -23,7 +23,7 @@ const WARNINGS: ValidationIssue[] = []; // Standard error codes const STANDARD_ERROR_CODES = [ 'PROJECT_NOT_FOUND', - 'DEVLOG_NOT_FOUND', + 'DEVLOG_NOT_FOUND', 'NOTE_NOT_FOUND', 'BAD_REQUEST', 'VALIDATION_FAILED', @@ -31,7 +31,7 @@ const STANDARD_ERROR_CODES = [ 'UNAUTHORIZED', 'FORBIDDEN', 'METHOD_NOT_ALLOWED', - 'RATE_LIMITED' + 'RATE_LIMITED', ] as const; /** @@ -59,13 +59,19 @@ function createProgram(filePaths: string[]): ts.Program { /** * Visit AST nodes recursively with proper error handling */ -function visitNode(node: ts.Node, sourceFile: ts.SourceFile, visitor: (node: ts.Node, sourceFile: ts.SourceFile) => void): void { +function visitNode( + node: ts.Node, + sourceFile: ts.SourceFile, + visitor: (node: ts.Node, sourceFile: ts.SourceFile) => void, +): void { try { visitor(node, sourceFile); - node.forEachChild(child => visitNode(child, sourceFile, visitor)); + node.forEachChild((child) => visitNode(child, sourceFile, visitor)); } catch (error) { // Skip problematic nodes and continue - console.warn(`⚠️ Skipping problematic node in ${sourceFile.fileName}: ${error instanceof Error ? error.message : String(error)}`); + console.warn( + `⚠️ Skipping problematic node in ${sourceFile.fileName}: ${error instanceof Error ? error.message : String(error)}`, + ); } } @@ -105,11 +111,13 @@ function isApiRoute(filePath: string): boolean { * Check if node is a frontend file */ function isFrontendFile(filePath: string): boolean { - return (filePath.includes('/contexts/') || - filePath.includes('/hooks/') || - filePath.includes('/lib/') || - filePath.includes('/components/')) && - !filePath.includes('/api/'); + return ( + (filePath.includes('/contexts/') || + filePath.includes('/hooks/') || + filePath.includes('/lib/') || + filePath.includes('/components/')) && + !filePath.includes('/api/') + ); } /** @@ -123,19 +131,26 @@ function validateAPIEndpointAST(filePath: string, sourceFile: ts.SourceFile): vo visitNode(sourceFile, sourceFile, (node, sourceFile) => { const lineNum = getLineNumber(sourceFile, node); - + // Check imports if (ts.isImportDeclaration(node)) { const importPath = (node.moduleSpecifier as ts.StringLiteral)?.text; if (importPath) { imports.add(importPath); - + // Check for api-utils imports if (importPath.includes('api-utils')) { - if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) { - node.importClause.namedBindings.elements.forEach(element => { + if ( + node.importClause?.namedBindings && + ts.isNamedImports(node.importClause.namedBindings) + ) { + node.importClause.namedBindings.elements.forEach((element) => { const importName = element.name.text; - if (['apiResponse', 'apiError', 'apiCollection', 'withErrorHandling'].includes(importName)) { + if ( + ['apiResponse', 'apiError', 'apiCollection', 'withErrorHandling'].includes( + importName, + ) + ) { hasApiResponseUtil = true; } if (importName === 'withErrorHandling') { @@ -150,7 +165,7 @@ function validateAPIEndpointAST(filePath: string, sourceFile: ts.SourceFile): vo // Check function calls if (ts.isCallExpression(node)) { const callText = getNodeText(sourceFile, node); - + // Check for standardized response utilities if (node.expression && ts.isIdentifier(node.expression)) { const functionName = node.expression.text; @@ -161,13 +176,15 @@ function validateAPIEndpointAST(filePath: string, sourceFile: ts.SourceFile): vo hasWithErrorHandling = true; } } - + // Check for manual Response.json calls - if (ts.isPropertyAccessExpression(node.expression) && - node.expression.expression && ts.isIdentifier(node.expression.expression) && - node.expression.expression.text === 'Response' && - node.expression.name.text === 'json') { - + if ( + ts.isPropertyAccessExpression(node.expression) && + node.expression.expression && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === 'Response' && + node.expression.name.text === 'json' + ) { // Check if it's not using standardized format const parentText = getNodeText(sourceFile, node.parent!); if (!parentText.includes('.success')) { @@ -183,8 +200,12 @@ function validateAPIEndpointAST(filePath: string, sourceFile: ts.SourceFile): vo } // Check for apiError calls with proper error codes - if (node.expression && ts.isIdentifier(node.expression) && - node.expression.text === 'apiError' && node.arguments.length > 0) { + if ( + node.expression && + ts.isIdentifier(node.expression) && + node.expression.text === 'apiError' && + node.arguments.length > 0 + ) { const firstArg = node.arguments[0]; if (ts.isStringLiteral(firstArg)) { const errorCode = firstArg.text; @@ -210,21 +231,30 @@ function validateAPIEndpointAST(filePath: string, sourceFile: ts.SourceFile): vo } // Check for export assignments (Next.js route handlers) - if (ts.isExportAssignment(node) || - (ts.isVariableStatement(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword))) { - + if ( + ts.isExportAssignment(node) || + (ts.isVariableStatement(node) && + node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) + ) { // Look for route handler exports (GET, POST, etc.) if (ts.isVariableStatement(node)) { - node.declarationList.declarations.forEach(decl => { - if (ts.isIdentifier(decl.name) && ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(decl.name.text)) { + node.declarationList.declarations.forEach((decl) => { + if ( + ts.isIdentifier(decl.name) && + ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(decl.name.text) + ) { // Check if it uses withErrorHandling - if (decl.initializer && !getNodeText(sourceFile, decl.initializer).includes('withErrorHandling')) { + if ( + decl.initializer && + !getNodeText(sourceFile, decl.initializer).includes('withErrorHandling') + ) { WARNINGS.push({ file: filePath, line: getLineNumber(sourceFile, decl), type: 'API_ERROR_HANDLING', message: `${decl.name.text} handler should use withErrorHandling() wrapper`, - suggestion: 'Wrap your handler with withErrorHandling() for consistent error responses', + suggestion: + 'Wrap your handler with withErrorHandling() for consistent error responses', }); } } @@ -235,13 +265,18 @@ function validateAPIEndpointAST(filePath: string, sourceFile: ts.SourceFile): vo // File-level validations if (isApiRoute(filePath)) { - if (!imports.has('./api-utils') && !imports.has('../api-utils') && !Array.from(imports).some(i => i.includes('api-utils'))) { + if ( + !imports.has('./api-utils') && + !imports.has('../api-utils') && + !Array.from(imports).some((i) => i.includes('api-utils')) + ) { WARNINGS.push({ file: filePath, line: 1, type: 'API_UTILS_IMPORT', message: 'API endpoint should import standardized utilities', - suggestion: 'Import { apiResponse, apiError, apiCollection, withErrorHandling } from api-utils', + suggestion: + 'Import { apiResponse, apiError, apiCollection, withErrorHandling } from api-utils', }); } } @@ -257,15 +292,18 @@ function validateFrontendAPIUsageAST(filePath: string, sourceFile: ts.SourceFile visitNode(sourceFile, sourceFile, (node, sourceFile) => { const lineNum = getLineNumber(sourceFile, node); - + // Check imports if (ts.isImportDeclaration(node)) { const importPath = (node.moduleSpecifier as ts.StringLiteral)?.text; if (importPath) { imports.add(importPath); - - if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) { - node.importClause.namedBindings.elements.forEach(element => { + + if ( + node.importClause?.namedBindings && + ts.isNamedImports(node.importClause.namedBindings) + ) { + node.importClause.namedBindings.elements.forEach((element) => { const importName = element.name.text; if (importName === 'ApiClient' || importName === 'apiClient') { hasApiClientImport = true; @@ -278,10 +316,9 @@ function validateFrontendAPIUsageAST(filePath: string, sourceFile: ts.SourceFile // Check function calls if (ts.isCallExpression(node)) { // Check for manual fetch calls - if (node.expression && ts.isIdentifier(node.expression) && - node.expression.text === 'fetch') { + if (node.expression && ts.isIdentifier(node.expression) && node.expression.text === 'fetch') { hasManualFetch = true; - + // Skip warning for ApiClient implementation itself if (!filePath.includes('/lib/api-client.ts')) { WARNINGS.push({ @@ -295,9 +332,12 @@ function validateFrontendAPIUsageAST(filePath: string, sourceFile: ts.SourceFile } // Check for response.json() calls in frontend - if (ts.isPropertyAccessExpression(node.expression) && - node.expression.name.text === 'json' && - hasManualFetch && !filePath.includes('/lib/api-client.ts')) { + if ( + ts.isPropertyAccessExpression(node.expression) && + node.expression.name.text === 'json' && + hasManualFetch && + !filePath.includes('/lib/api-client.ts') + ) { ERRORS.push({ file: filePath, line: lineNum, @@ -311,16 +351,19 @@ function validateFrontendAPIUsageAST(filePath: string, sourceFile: ts.SourceFile // Check catch clauses for proper error handling if (ts.isCatchClause(node) && hasApiClientImport) { const catchText = getNodeText(sourceFile, node); - if (!catchText.includes('ApiError') && - !catchText.includes('.isNotFound') && - !catchText.includes('.isValidation') && - !catchText.includes('.code')) { + if ( + !catchText.includes('ApiError') && + !catchText.includes('.isNotFound') && + !catchText.includes('.isValidation') && + !catchText.includes('.code') + ) { WARNINGS.push({ file: filePath, line: lineNum, type: 'FRONTEND_ERROR_HANDLING', message: 'Error handling should check for ApiError type', - suggestion: 'Use error.isNotFound(), error.isValidation(), etc. for proper error handling', + suggestion: + 'Use error.isNotFound(), error.isValidation(), etc. for proper error handling', }); } } @@ -329,24 +372,27 @@ function validateFrontendAPIUsageAST(filePath: string, sourceFile: ts.SourceFile if (ts.isPropertyAccessExpression(node)) { const propertyName = node.name.text; const objectText = getNodeText(sourceFile, node.expression); - + // Check for direct response property access (not .data, .success, .error) - if (objectText.includes('response') && - !['data', 'success', 'error', 'meta', 'status', 'headers', 'ok'].includes(propertyName) && - isFrontendFile(filePath)) { + if ( + objectText.includes('response') && + !['data', 'success', 'error', 'meta', 'status', 'headers', 'ok'].includes(propertyName) && + isFrontendFile(filePath) + ) { WARNINGS.push({ file: filePath, line: lineNum, type: 'FRONTEND_ENVELOPE_ACCESS', message: 'Direct response property access - should use envelope format', - suggestion: 'Access data through response.data, check response.success, handle response.error', + suggestion: + 'Access data through response.data, check response.success, handle response.error', }); } } }); // Check for legacy API client usage - if (Array.from(imports).some(i => i.includes('note-api-client')) && !hasApiClientImport) { + if (Array.from(imports).some((i) => i.includes('note-api-client')) && !hasApiClientImport) { WARNINGS.push({ file: filePath, line: 1, @@ -363,24 +409,28 @@ function validateFrontendAPIUsageAST(filePath: string, sourceFile: ts.SourceFile function validateTypeDefinitionsAST(filePath: string, sourceFile: ts.SourceFile): void { visitNode(sourceFile, sourceFile, (node, sourceFile) => { const lineNum = getLineNumber(sourceFile, node); - + // Check interface declarations if (ts.isInterfaceDeclaration(node)) { const interfaceName = node.name.text; - + // Check response interfaces if (interfaceName.includes('Response')) { const members = node.members; - const memberNames = members.map(member => { - if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) { - return member.name.text; - } - return null; - }).filter(Boolean) as string[]; - - if (!memberNames.includes('success') || - !memberNames.includes('data') || - !memberNames.includes('meta')) { + const memberNames = members + .map((member) => { + if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) { + return member.name.text; + } + return null; + }) + .filter(Boolean) as string[]; + + if ( + !memberNames.includes('success') || + !memberNames.includes('data') || + !memberNames.includes('meta') + ) { WARNINGS.push({ file: filePath, line: lineNum, @@ -390,17 +440,19 @@ function validateTypeDefinitionsAST(filePath: string, sourceFile: ts.SourceFile) }); } } - + // Check error interfaces if (interfaceName.includes('Error')) { const members = node.members; - const memberNames = members.map(member => { - if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) { - return member.name.text; - } - return null; - }).filter(Boolean) as string[]; - + const memberNames = members + .map((member) => { + if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) { + return member.name.text; + } + return null; + }) + .filter(Boolean) as string[]; + if (!memberNames.includes('code') || !memberNames.includes('message')) { WARNINGS.push({ file: filePath, @@ -420,10 +472,10 @@ function validateTypeDefinitionsAST(filePath: string, sourceFile: ts.SourceFile) */ function validateFilesWithAST(filePaths: string[]): void { console.log(`🔍 Creating TypeScript program for ${filePaths.length} files...`); - + const program = createProgram(filePaths); - - filePaths.forEach(filePath => { + + filePaths.forEach((filePath) => { const sourceFile = program.getSourceFile(filePath); if (!sourceFile) { console.warn(`⚠️ Could not parse ${filePath}`); @@ -434,11 +486,11 @@ function validateFilesWithAST(filePaths: string[]): void { if (isApiRoute(filePath)) { validateAPIEndpointAST(filePath, sourceFile); } - + if (isFrontendFile(filePath)) { validateFrontendAPIUsageAST(filePath, sourceFile); } - + if (filePath.includes('/types/') || filePath.includes('/schemas/')) { validateTypeDefinitionsAST(filePath, sourceFile); } @@ -453,7 +505,7 @@ function findValidationFiles(): string[] { function findFilesRecursive(dir: string, predicate: (file: string) => boolean): void { if (!fs.existsSync(dir)) return; - + const entries = fs.readdirSync(dir); for (const entry of entries) { @@ -461,7 +513,9 @@ function findValidationFiles(): string[] { const stat = fs.statSync(fullPath); if (stat.isDirectory()) { - if (!['node_modules', 'build', 'dist', '.next', '.next-build', 'coverage'].includes(entry)) { + if ( + !['node_modules', 'build', 'dist', '.next', '.next-build', 'coverage'].includes(entry) + ) { findFilesRecursive(fullPath, predicate); } } else if (predicate(fullPath)) { @@ -471,11 +525,11 @@ function findValidationFiles(): string[] { } // Find TypeScript files in web package - const webAppDir = path.join(process.cwd(), 'packages/web/app'); + const webAppDir = path.join(process.cwd(), 'apps/web/app'); if (fs.existsSync(webAppDir)) { - findFilesRecursive(webAppDir, (file) => - (file.endsWith('.ts') || file.endsWith('.tsx')) && - !file.endsWith('.d.ts') + findFilesRecursive( + webAppDir, + (file) => (file.endsWith('.ts') || file.endsWith('.tsx')) && !file.endsWith('.d.ts'), ); } diff --git a/scripts/validation/validate-imports.ts b/scripts/validation/validate-imports.ts index e45bd201..19e9d52a 100755 --- a/scripts/validation/validate-imports.ts +++ b/scripts/validation/validate-imports.ts @@ -64,8 +64,8 @@ function validateFile(filePath: string): void { // Rule 2: Avoid self-referencing @/ aliases within same package if (importPath.startsWith('@/')) { - // Check if we're in packages/web (where @/ is allowed for Next.js) - if (!filePath.includes('packages/web/')) { + // Check if we're in apps/web (where @/ is allowed for Next.js) + if (!filePath.includes('apps/web/')) { ERRORS.push({ file: filePath, line: lineNum, @@ -82,12 +82,12 @@ function validateFile(filePath: string): void { const currentPackageMatch = filePath.match(/packages\/([^\/]+)\//); if (currentPackageMatch) { const currentPackage = currentPackageMatch[1]; - + // Resolve the relative path to see if it crosses package boundaries const importSegments = importPath.split('/'); let currentDir = filePath.split('/'); currentDir.pop(); // Remove filename - + for (const segment of importSegments) { if (segment === '..') { currentDir.pop(); @@ -95,10 +95,10 @@ function validateFile(filePath: string): void { currentDir.push(segment); } } - + const resolvedPath = currentDir.join('/'); const targetPackageMatch = resolvedPath.match(/packages\/([^\/]+)\//); - + // Only flag if it actually crosses package boundaries if (targetPackageMatch && targetPackageMatch[1] !== currentPackage) { const targetPackage = targetPackageMatch[1]; @@ -135,7 +135,7 @@ function validateFile(filePath: string): void { file: filePath, line: lineNum, message: `Invalid package name in cross-package import: @codervisor/devlog-${packageName}`, - suggestion: `Valid packages are: ${validPackages.map(p => `@codervisor/devlog-${p}`).join(', ')}`, + suggestion: `Valid packages are: ${validPackages.map((p) => `@codervisor/devlog-${p}`).join(', ')}`, }); } } @@ -147,12 +147,12 @@ function validateFile(filePath: string): void { const currentPackageMatch = filePath.match(/packages\/([^\/]+)\//); if (currentPackageMatch) { const currentPackage = currentPackageMatch[1]; - + // Check if the relative import might be going to a different package const importSegments = importPath.split('/'); let currentDir = filePath.split('/'); currentDir.pop(); // Remove filename - + // Resolve the relative path for (const segment of importSegments) { if (segment === '..') { @@ -161,10 +161,10 @@ function validateFile(filePath: string): void { currentDir.push(segment); } } - + const resolvedPath = currentDir.join('/'); const targetPackageMatch = resolvedPath.match(/packages\/([^\/]+)\//); - + if (targetPackageMatch && targetPackageMatch[1] !== currentPackage) { const targetPackage = targetPackageMatch[1]; ERRORS.push({ diff --git a/scripts/validation/validate-response-envelopes-ast.ts b/scripts/validation/validate-response-envelopes-ast.ts index 94c3a27b..1c0dfb78 100755 --- a/scripts/validation/validate-response-envelopes-ast.ts +++ b/scripts/validation/validate-response-envelopes-ast.ts @@ -23,7 +23,7 @@ const WARNINGS: ValidationIssue[] = []; // Standard error codes const STANDARD_ERROR_CODES = [ 'PROJECT_NOT_FOUND', - 'DEVLOG_NOT_FOUND', + 'DEVLOG_NOT_FOUND', 'NOTE_NOT_FOUND', 'BAD_REQUEST', 'VALIDATION_FAILED', @@ -31,7 +31,7 @@ const STANDARD_ERROR_CODES = [ 'UNAUTHORIZED', 'FORBIDDEN', 'METHOD_NOT_ALLOWED', - 'RATE_LIMITED' + 'RATE_LIMITED', ] as const; interface EnvelopeStructure { @@ -67,13 +67,19 @@ function createProgram(filePaths: string[]): ts.Program { /** * Visit AST nodes recursively with proper error handling */ -function visitNode(node: ts.Node, sourceFile: ts.SourceFile, visitor: (node: ts.Node, sourceFile: ts.SourceFile) => void): void { +function visitNode( + node: ts.Node, + sourceFile: ts.SourceFile, + visitor: (node: ts.Node, sourceFile: ts.SourceFile) => void, +): void { try { visitor(node, sourceFile); - node.forEachChild(child => visitNode(child, sourceFile, visitor)); + node.forEachChild((child) => visitNode(child, sourceFile, visitor)); } catch (error) { // Skip problematic nodes and continue - console.warn(`⚠️ Skipping problematic node in ${sourceFile.fileName}: ${error instanceof Error ? error.message : String(error)}`); + console.warn( + `⚠️ Skipping problematic node in ${sourceFile.fileName}: ${error instanceof Error ? error.message : String(error)}`, + ); } } @@ -107,12 +113,14 @@ function getNodeText(sourceFile: ts.SourceFile, node: ts.Node): string { */ function hasEnvelopeStructure(objectLiteral: ts.ObjectLiteralExpression): EnvelopeStructure { const properties = objectLiteral.properties; - const propNames = properties.map(prop => { - if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { - return prop.name.text; - } - return null; - }).filter(Boolean) as string[]; + const propNames = properties + .map((prop) => { + if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { + return prop.name.text; + } + return null; + }) + .filter(Boolean) as string[]; const hasSuccess = propNames.includes('success'); const hasData = propNames.includes('data'); @@ -125,21 +133,24 @@ function hasEnvelopeStructure(objectLiteral: ts.ObjectLiteralExpression): Envelo /** * Check if success response has proper envelope format */ -function validateSuccessEnvelope(node: ts.ObjectLiteralExpression, sourceFile: ts.SourceFile, filePath: string): void { +function validateSuccessEnvelope( + node: ts.ObjectLiteralExpression, + sourceFile: ts.SourceFile, + filePath: string, +): void { const lineNum = getLineNumber(sourceFile, node); const envelope = hasEnvelopeStructure(node); - + if (envelope.hasSuccess) { // Find success property value - const successProp = node.properties.find(prop => - ts.isPropertyAssignment(prop) && - ts.isIdentifier(prop.name) && - prop.name.text === 'success' + const successProp = node.properties.find( + (prop) => + ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'success', ) as ts.PropertyAssignment | undefined; - + if (successProp && successProp.initializer) { const isSuccessTrue = successProp.initializer.kind === ts.SyntaxKind.TrueKeyword; - + if (isSuccessTrue) { // This is a success response, validate structure if (!envelope.hasData) { @@ -151,31 +162,35 @@ function validateSuccessEnvelope(node: ts.ObjectLiteralExpression, sourceFile: t suggestion: 'Include data property in success response envelope', }); } - + if (!envelope.hasMeta) { ERRORS.push({ file: filePath, line: lineNum, type: 'SUCCESS_MISSING_META', message: 'Success response missing meta field', - suggestion: 'Include meta: { timestamp: new Date().toISOString() } or use apiResponse()', + suggestion: + 'Include meta: { timestamp: new Date().toISOString() } or use apiResponse()', }); } else { // Check meta structure - const metaProp = node.properties.find(prop => - ts.isPropertyAssignment(prop) && - ts.isIdentifier(prop.name) && - prop.name.text === 'meta' + const metaProp = node.properties.find( + (prop) => + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === 'meta', ) as ts.PropertyAssignment | undefined; - + if (metaProp && ts.isObjectLiteralExpression(metaProp.initializer)) { - const metaProps = metaProp.initializer.properties.map(prop => { - if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { - return prop.name.text; - } - return null; - }).filter(Boolean) as string[]; - + const metaProps = metaProp.initializer.properties + .map((prop) => { + if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { + return prop.name.text; + } + return null; + }) + .filter(Boolean) as string[]; + if (!metaProps.includes('timestamp')) { WARNINGS.push({ file: filePath, @@ -195,31 +210,36 @@ function validateSuccessEnvelope(node: ts.ObjectLiteralExpression, sourceFile: t line: lineNum, type: 'ERROR_MISSING_STRUCTURE', message: 'Error response missing error field', - suggestion: 'Include error: { code: "ERROR_CODE", message: "Description" } or use apiError()', + suggestion: + 'Include error: { code: "ERROR_CODE", message: "Description" } or use apiError()', }); } else { // Check error structure - const errorProp = node.properties.find(prop => - ts.isPropertyAssignment(prop) && - ts.isIdentifier(prop.name) && - prop.name.text === 'error' + const errorProp = node.properties.find( + (prop) => + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === 'error', ) as ts.PropertyAssignment | undefined; - + if (errorProp && ts.isObjectLiteralExpression(errorProp.initializer)) { - const errorProps = errorProp.initializer.properties.map(prop => { - if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { - return prop.name.text; - } - return null; - }).filter(Boolean) as string[]; - + const errorProps = errorProp.initializer.properties + .map((prop) => { + if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { + return prop.name.text; + } + return null; + }) + .filter(Boolean) as string[]; + if (!errorProps.includes('code') || !errorProps.includes('message')) { ERRORS.push({ file: filePath, line: lineNum, type: 'ERROR_MISSING_STRUCTURE', message: 'Error response missing required code/message fields', - suggestion: 'Include error: { code: "ERROR_CODE", message: "Description" } or use apiError()', + suggestion: + 'Include error: { code: "ERROR_CODE", message: "Description" } or use apiError()', }); } } @@ -239,13 +259,13 @@ function validateResponseEnvelopeAST(filePath: string, sourceFile: ts.SourceFile visitNode(sourceFile, sourceFile, (node, sourceFile) => { const lineNum = getLineNumber(sourceFile, node); - + // Check for standardized response utilities usage if (ts.isCallExpression(node) && node.expression && ts.isIdentifier(node.expression)) { const functionName = node.expression.text; if (['apiResponse', 'apiError', 'apiCollection'].includes(functionName)) { usesResponseUtils = true; - + // Check apiError calls for proper error codes if (functionName === 'apiError' && node.arguments.length > 0) { const firstArg = node.arguments[0]; @@ -266,19 +286,21 @@ function validateResponseEnvelopeAST(filePath: string, sourceFile: ts.SourceFile } // Check for manual Response.json calls - if (ts.isCallExpression(node) && - ts.isPropertyAccessExpression(node.expression) && - node.expression.expression && ts.isIdentifier(node.expression.expression) && - node.expression.expression.text === 'Response' && - node.expression.name.text === 'json') { - + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.expression && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === 'Response' && + node.expression.name.text === 'json' + ) { // Check the argument to Response.json() if (node.arguments.length > 0) { const jsonArg = node.arguments[0]; - + if (ts.isObjectLiteralExpression(jsonArg)) { const envelope = hasEnvelopeStructure(jsonArg); - + if (envelope.hasSuccess) { hasSuccessEnvelope = true; validateSuccessEnvelope(jsonArg, sourceFile, filePath); @@ -299,28 +321,31 @@ function validateResponseEnvelopeAST(filePath: string, sourceFile: ts.SourceFile // Check for object literals that might be response envelopes if (ts.isObjectLiteralExpression(node)) { const envelope = hasEnvelopeStructure(node); - + if (envelope.hasSuccess) { const parentNode = node.parent; // Check if this is likely a response object (not just any object with success property) - if (ts.isReturnStatement(parentNode) || - ts.isCallExpression(parentNode) || - ts.isPropertyAssignment(parentNode)) { + if ( + ts.isReturnStatement(parentNode) || + ts.isCallExpression(parentNode) || + ts.isPropertyAssignment(parentNode) + ) { validateSuccessEnvelope(node, sourceFile, filePath); } } } // Check for status code assignments - if (ts.isCallExpression(node) && - ts.isPropertyAccessExpression(node.expression) && - node.expression.name.text === 'status' && - node.arguments.length > 0) { - + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.name.text === 'status' && + node.arguments.length > 0 + ) { const statusArg = node.arguments[0]; if (ts.isNumericLiteral(statusArg)) { const status = parseInt(statusArg.text); - + if (status >= 400 && !hasErrorEnvelope && !usesResponseUtils) { ERRORS.push({ file: filePath, @@ -340,13 +365,19 @@ function validateResponseEnvelopeAST(filePath: string, sourceFile: ts.SourceFile line: lineNum, type: 'UNHANDLED_ERROR', message: 'Thrown error may not be properly caught and formatted', - suggestion: 'Use withErrorHandling() wrapper or ensure error is caught and formatted as envelope', + suggestion: + 'Use withErrorHandling() wrapper or ensure error is caught and formatted as envelope', }); } }); // File-level validations - if (filePath.includes('/api/') && !usesResponseUtils && !hasSuccessEnvelope && !hasErrorEnvelope) { + if ( + filePath.includes('/api/') && + !usesResponseUtils && + !hasSuccessEnvelope && + !hasErrorEnvelope + ) { WARNINGS.push({ file: filePath, line: 1, @@ -365,11 +396,14 @@ function validateEnvelopeHandlingAST(filePath: string, sourceFile: ts.SourceFile visitNode(sourceFile, sourceFile, (node, sourceFile) => { const lineNum = getLineNumber(sourceFile, node); - + // Check imports for ApiClient - if (ts.isImportDeclaration(node) && node.importClause?.namedBindings && - ts.isNamedImports(node.importClause.namedBindings)) { - node.importClause.namedBindings.elements.forEach(element => { + if ( + ts.isImportDeclaration(node) && + node.importClause?.namedBindings && + ts.isNamedImports(node.importClause.namedBindings) + ) { + node.importClause.namedBindings.elements.forEach((element) => { if (element.name.text === 'ApiClient' || element.name.text === 'apiClient') { hasApiClientUsage = true; } @@ -380,7 +414,7 @@ function validateEnvelopeHandlingAST(filePath: string, sourceFile: ts.SourceFile if (ts.isPropertyAccessExpression(node)) { const propertyName = node.name.text; const objectText = getNodeText(sourceFile, node.expression); - + // Check for pagination access patterns if (propertyName === 'pagination' && !objectText.includes('.meta')) { if (hasApiClientUsage || objectText.includes('response')) { @@ -395,14 +429,17 @@ function validateEnvelopeHandlingAST(filePath: string, sourceFile: ts.SourceFile } // Check for direct response property access - if (objectText.includes('response') && - !['data', 'success', 'error', 'meta', 'status', 'headers', 'ok'].includes(propertyName)) { + if ( + objectText.includes('response') && + !['data', 'success', 'error', 'meta', 'status', 'headers', 'ok'].includes(propertyName) + ) { WARNINGS.push({ file: filePath, line: lineNum, type: 'DIRECT_RESPONSE_ACCESS', message: 'Direct response property access - may not handle envelope format', - suggestion: 'Access data through response.data, check response.success, handle response.error', + suggestion: + 'Access data through response.data, check response.success, handle response.error', }); } } @@ -410,25 +447,32 @@ function validateEnvelopeHandlingAST(filePath: string, sourceFile: ts.SourceFile // Check catch clauses for proper error handling if (ts.isCatchClause(node) && hasApiClientUsage) { const catchText = getNodeText(sourceFile, node); - if (!catchText.includes('ApiError') && - !catchText.includes('.isNotFound') && - !catchText.includes('.isValidation') && - !catchText.includes('.code')) { + if ( + !catchText.includes('ApiError') && + !catchText.includes('.isNotFound') && + !catchText.includes('.isValidation') && + !catchText.includes('.code') + ) { WARNINGS.push({ file: filePath, line: lineNum, type: 'INCOMPLETE_ERROR_HANDLING', message: 'Error handling may not properly handle ApiError types', - suggestion: 'Use ApiError methods like .isNotFound(), .isValidation(), or check .code property', + suggestion: + 'Use ApiError methods like .isNotFound(), .isValidation(), or check .code property', }); } } // Check for manual fetch without envelope handling - if (ts.isCallExpression(node) && - node.expression && ts.isIdentifier(node.expression) && - node.expression.text === 'fetch' && - !hasApiClientUsage && !filePath.includes('/lib/api-client.ts')) { + if ( + ts.isCallExpression(node) && + node.expression && + ts.isIdentifier(node.expression) && + node.expression.text === 'fetch' && + !hasApiClientUsage && + !filePath.includes('/lib/api-client.ts') + ) { WARNINGS.push({ file: filePath, line: lineNum, @@ -445,10 +489,10 @@ function validateEnvelopeHandlingAST(filePath: string, sourceFile: ts.SourceFile */ function validateFilesWithAST(filePaths: string[]): void { console.log(`🔍 Creating TypeScript program for ${filePaths.length} files...`); - + const program = createProgram(filePaths); - - filePaths.forEach(filePath => { + + filePaths.forEach((filePath) => { const sourceFile = program.getSourceFile(filePath); if (!sourceFile) { console.warn(`⚠️ Could not parse ${filePath}`); @@ -459,13 +503,15 @@ function validateFilesWithAST(filePaths: string[]): void { if (filePath.includes('/api/') && filePath.endsWith('route.ts')) { validateResponseEnvelopeAST(filePath, sourceFile); } - + // Validate frontend files for envelope handling - if ((filePath.includes('/contexts/') || - filePath.includes('/hooks/') || - filePath.includes('/lib/') || - filePath.includes('/components/')) && - !filePath.includes('/api/')) { + if ( + (filePath.includes('/contexts/') || + filePath.includes('/hooks/') || + filePath.includes('/lib/') || + filePath.includes('/components/')) && + !filePath.includes('/api/') + ) { validateEnvelopeHandlingAST(filePath, sourceFile); } }); @@ -479,7 +525,7 @@ function findEnvelopeValidationFiles(): string[] { function findFilesRecursive(dir: string, predicate: (file: string) => boolean): void { if (!fs.existsSync(dir)) return; - + const entries = fs.readdirSync(dir); for (const entry of entries) { @@ -487,7 +533,9 @@ function findEnvelopeValidationFiles(): string[] { const stat = fs.statSync(fullPath); if (stat.isDirectory()) { - if (!['node_modules', 'build', 'dist', '.next', '.next-build', 'coverage'].includes(entry)) { + if ( + !['node_modules', 'build', 'dist', '.next', '.next-build', 'coverage'].includes(entry) + ) { findFilesRecursive(fullPath, predicate); } } else if (predicate(fullPath)) { @@ -497,11 +545,11 @@ function findEnvelopeValidationFiles(): string[] { } // Find TypeScript files in web package - const webAppDir = path.join(process.cwd(), 'packages/web/app'); + const webAppDir = path.join(process.cwd(), 'apps/web/app'); if (fs.existsSync(webAppDir)) { - findFilesRecursive(webAppDir, (file) => - (file.endsWith('.ts') || file.endsWith('.tsx')) && - !file.endsWith('.d.ts') + findFilesRecursive( + webAppDir, + (file) => (file.endsWith('.ts') || file.endsWith('.tsx')) && !file.endsWith('.d.ts'), ); } diff --git a/vitest.workspace.ts b/vitest.workspace.ts index de6ce748..f80108ef 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -7,8 +7,8 @@ import { defineWorkspace } from 'vitest/config'; export default defineWorkspace([ // Include all packages with tests 'packages/core', - 'packages/mcp', + 'packages/mcp', 'packages/ai', // Add web package when it gets tests - // 'packages/web', + // 'apps/web', ]); From 4407735eb05ff9ec84577a5b32efba517fee9457 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 21:31:42 +0800 Subject: [PATCH 30/50] fix: add missing network configuration for development web service --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index ea057ea9..1cf845e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,6 +59,8 @@ services: - './scripts:/app/scripts' env_file: - .env + networks: + - devlog-network networks: devlog-network: From aca43b07d72b9646fac4bd5948843ac93ce4a559 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 21:48:24 +0800 Subject: [PATCH 31/50] feat: implement batch delete functionality for devlog entries --- .../app/api/projects/[name]/devlogs/route.ts | 94 ++++++++++++++++++- apps/web/lib/api/api-client.ts | 8 +- apps/web/lib/api/devlog-api-client.ts | 7 +- apps/web/schemas/devlog.ts | 5 + 4 files changed, 105 insertions(+), 9 deletions(-) diff --git a/apps/web/app/api/projects/[name]/devlogs/route.ts b/apps/web/app/api/projects/[name]/devlogs/route.ts index 321a04a9..fc67a10b 100644 --- a/apps/web/app/api/projects/[name]/devlogs/route.ts +++ b/apps/web/app/api/projects/[name]/devlogs/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from 'next/server'; import { PaginationMeta, SortOptions } from '@codervisor/devlog-core'; -import { DevlogService, ProjectService } from '@codervisor/devlog-core/server'; -import { ApiValidator, CreateDevlogBodySchema, DevlogListQuerySchema } from '@/schemas'; +import { DevlogService } from '@codervisor/devlog-core/server'; +import { ApiValidator, CreateDevlogBodySchema, DevlogListQuerySchema, BatchDeleteDevlogsBodySchema } from '@/schemas'; import { ApiErrors, createCollectionResponse, @@ -75,9 +75,9 @@ export async function GET(request: NextRequest, { params }: { params: { name: st let result; if (queryData.search) { - result = await devlogService.search(queryData.search, filter); + result = await devlogService.search(queryData.search, filter, pagination, sortOptions); } else { - result = await devlogService.list(filter); + result = await devlogService.list(filter, pagination, sortOptions); } // Check if result has pagination metadata @@ -153,3 +153,89 @@ export async function POST(request: NextRequest, { params }: { params: { name: s return ApiErrors.internalError('Failed to create devlog'); } } + +// DELETE /api/projects/[name]/devlogs - Batch delete devlog entries +export async function DELETE(request: NextRequest, { params }: { params: { name: string } }) { + try { + // Parse and validate project identifier + const paramResult = RouteParams.parseProjectName(params); + if (!paramResult.success) { + return paramResult.response; + } + + const { projectName } = paramResult.data; + + // Validate request body + const bodyValidation = await ApiValidator.validateJsonBody(request, BatchDeleteDevlogsBodySchema); + if (!bodyValidation.success) { + return bodyValidation.response; + } + + const { ids } = bodyValidation.data; + + // Get project using helper + const projectResult = await ServiceHelper.getProjectByNameOrFail(projectName); + if (!projectResult.success) { + return projectResult.response; + } + + const project = projectResult.data.project; + + // Create project-aware devlog service + const devlogService = DevlogService.getInstance(project.id); + + // Track successful and failed deletions + const results = { + deleted: [] as number[], + failed: [] as { id: number; error: string }[], + }; + + // Delete devlogs one by one and collect results + for (const id of ids) { + try { + await devlogService.delete(id); + results.deleted.push(id); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + results.failed.push({ id, error: errorMessage }); + } + } + + // Return results with appropriate status + if (results.failed.length === 0) { + // All deletions successful + return createSuccessResponse( + { + deleted: results.deleted, + deletedCount: results.deleted.length, + }, + { + status: 200, + sseEventType: RealtimeEventType.DEVLOG_DELETED, + } + ); + } else if (results.deleted.length === 0) { + // All deletions failed + return ApiErrors.badRequest('Failed to delete any devlogs', { + failures: results.failed + }); + } else { + // Partial success + return createSuccessResponse( + { + deleted: results.deleted, + failed: results.failed, + deletedCount: results.deleted.length, + failedCount: results.failed.length, + }, + { + status: 207, // Multi-status for partial success + sseEventType: RealtimeEventType.DEVLOG_DELETED, + } + ); + } + } catch (error) { + console.error('Error batch deleting devlogs:', error); + return ApiErrors.internalError('Failed to delete devlogs'); + } +} diff --git a/apps/web/lib/api/api-client.ts b/apps/web/lib/api/api-client.ts index 393911b9..adabe549 100644 --- a/apps/web/lib/api/api-client.ts +++ b/apps/web/lib/api/api-client.ts @@ -226,8 +226,12 @@ export class ApiClient { /** * DELETE request */ - async delete(url: string, options?: Omit): Promise { - return this.request(url, { ...options, method: 'DELETE' }); + async delete(url: string, data?: any, options?: Omit): Promise { + return this.request(url, { + ...options, + method: 'DELETE', + body: data ? JSON.stringify(data) : undefined, + }); } /** diff --git a/apps/web/lib/api/devlog-api-client.ts b/apps/web/lib/api/devlog-api-client.ts index 120b5453..20dcb62b 100644 --- a/apps/web/lib/api/devlog-api-client.ts +++ b/apps/web/lib/api/devlog-api-client.ts @@ -147,9 +147,10 @@ export class DevlogApiClient { * Batch delete multiple devlog */ async batchDelete(devlogIds: DevlogId[]): Promise { - return apiClient.post(`/api/projects/${this.projectName}/devlogs/batch/delete`, { - ids: devlogIds, - }); + return apiClient.delete( + `/api/projects/${this.projectName}/devlogs`, + { ids: devlogIds }, + ); } /** diff --git a/apps/web/schemas/devlog.ts b/apps/web/schemas/devlog.ts index bf9a71ee..6bbb3f2c 100644 --- a/apps/web/schemas/devlog.ts +++ b/apps/web/schemas/devlog.ts @@ -80,3 +80,8 @@ export const DevlogUpdateWithNoteBodySchema = z.object({ technicalContext: z.string().nullable().optional(), acceptanceCriteria: z.array(z.string()).optional(), }); + +// Schema for batch delete request body +export const BatchDeleteDevlogsBodySchema = z.object({ + ids: z.array(z.number().int().positive()).min(1, 'At least one devlog ID is required'), +}); From e76545616e84390d64d154f63695fd505901ab50 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 21:59:35 +0800 Subject: [PATCH 32/50] feat: add devlog details and list pages with CRUD functionality --- ...tails-page.tsx => devlog-details-page.tsx} | 2 +- .../app/projects/[name]/devlogs/[id]/page.tsx | 4 +- ...log-list-page.tsx => devlog-list-page.tsx} | 2 +- apps/web/app/projects/[name]/devlogs/page.tsx | 4 +- apps/web/stores/devlog-store.ts | 854 +++++++++--------- apps/web/stores/project-store.ts | 6 +- 6 files changed, 439 insertions(+), 433 deletions(-) rename apps/web/app/projects/[name]/devlogs/[id]/{project-devlog-details-page.tsx => devlog-details-page.tsx} (99%) rename apps/web/app/projects/[name]/devlogs/{project-devlog-list-page.tsx => devlog-list-page.tsx} (98%) diff --git a/apps/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx b/apps/web/app/projects/[name]/devlogs/[id]/devlog-details-page.tsx similarity index 99% rename from apps/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx rename to apps/web/app/projects/[name]/devlogs/[id]/devlog-details-page.tsx index fb995bcd..14af2294 100644 --- a/apps/web/app/projects/[name]/devlogs/[id]/project-devlog-details-page.tsx +++ b/apps/web/app/projects/[name]/devlogs/[id]/devlog-details-page.tsx @@ -12,7 +12,7 @@ import { useProjectName } from '@/components/provider/project-provider'; import { useDevlogId } from '@/components/provider/devlog-provider'; import { DevlogDetails } from '@/components/feature/devlog/devlog-details'; -export function ProjectDevlogDetailsPage() { +export function DevlogDetailsPage() { const projectName = useProjectName(); const devlogId = useDevlogId(); const router = useRouter(); diff --git a/apps/web/app/projects/[name]/devlogs/[id]/page.tsx b/apps/web/app/projects/[name]/devlogs/[id]/page.tsx index 7f76393e..c24d19c4 100644 --- a/apps/web/app/projects/[name]/devlogs/[id]/page.tsx +++ b/apps/web/app/projects/[name]/devlogs/[id]/page.tsx @@ -1,5 +1,5 @@ -import { ProjectDevlogDetailsPage } from './project-devlog-details-page'; +import { DevlogDetailsPage } from './devlog-details-page'; export default function ProjectDevlogPage() { - return ; + return ; } diff --git a/apps/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx b/apps/web/app/projects/[name]/devlogs/devlog-list-page.tsx similarity index 98% rename from apps/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx rename to apps/web/app/projects/[name]/devlogs/devlog-list-page.tsx index af984025..b8350f67 100644 --- a/apps/web/app/projects/[name]/devlogs/project-devlog-list-page.tsx +++ b/apps/web/app/projects/[name]/devlogs/devlog-list-page.tsx @@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation'; import { useProjectName } from '@/components/provider/project-provider'; import { DevlogList } from '@/components/feature/devlog/devlog-list'; -export function ProjectDevlogListPage() { +export function DevlogListPage() { const projectName = useProjectName(); const router = useRouter(); diff --git a/apps/web/app/projects/[name]/devlogs/page.tsx b/apps/web/app/projects/[name]/devlogs/page.tsx index 9e12052a..584f839b 100644 --- a/apps/web/app/projects/[name]/devlogs/page.tsx +++ b/apps/web/app/projects/[name]/devlogs/page.tsx @@ -1,5 +1,5 @@ -import { ProjectDevlogListPage } from './project-devlog-list-page'; +import { DevlogListPage } from './devlog-list-page'; export default function ProjectDevlogsPage() { - return ; + return ; } diff --git a/apps/web/stores/devlog-store.ts b/apps/web/stores/devlog-store.ts index f1cd58b7..44906b23 100644 --- a/apps/web/stores/devlog-store.ts +++ b/apps/web/stores/devlog-store.ts @@ -14,7 +14,7 @@ import { SortOptions, TimeSeriesStats, } from '@codervisor/devlog-core'; -import { DevlogApiClient, handleApiError } from '@/lib'; +import { debounce, DevlogApiClient, handleApiError } from '@/lib'; import { useProjectStore } from './project-store'; import { DataContext, @@ -71,67 +71,8 @@ interface DevlogState { } export const useDevlogStore = create()( - subscribeWithSelector((set, get) => ({ - // Initial state - devlogsContext: getDefaultTableDataContext(), - - // Navigation devlog state - navigationDevlogsContext: getDefaultDataContext(), - - // Selected devlog state - currentDevlogId: null, - currentDevlogContext: getDefaultDataContext(), - currentDevlogNotesContext: getDefaultDataContext(), - - // Stats state - statsContext: getDefaultDataContext(), - - // Time series stats state - timeSeriesStatsContext: getDefaultDataContext(), - - // Actions - setCurrentDevlogId: (id: DevlogId) => { - set({ - currentDevlogId: id, - currentDevlogContext: getDefaultDataContext(), - }); - }, - - setDevlogsFilters: (filters) => { - const currentFilters = get().devlogsContext.filters; - set((state) => ({ - devlogsContext: { - ...state.devlogsContext, - filters: { - ...currentFilters, - ...filters, - }, - }, - })); - }, - - setDevlogsPagination: (pagination: PaginationMeta) => { - set((state) => ({ - devlogsContext: { - ...state.devlogsContext, - pagination: { - ...state.devlogsContext.pagination, - ...pagination, - }, - }, - })); - }, - - setDevlogsSortOptions: (sortOptions: SortOptions) => { - set((state) => ({ - devlogsContext: { - ...state.devlogsContext, - sortOptions, - }, - })); - }, - - fetchDevlogs: async () => { + subscribeWithSelector((set, get) => { + const debouncedFetchDevlogs = debounce(async () => { const devlogApiClient = getDevlogApiClient(); if (!devlogApiClient) { @@ -207,415 +148,480 @@ export const useDevlogStore = create()( }, })); } - }, + }); - fetchNavigationDevlogs: async () => { - const devlogApiClient = getDevlogApiClient(); + return { + // Initial state + devlogsContext: getDefaultTableDataContext(), - if (!devlogApiClient) { - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - loading: false, - }, - })); - return; - } + // Navigation devlog state + navigationDevlogsContext: getDefaultDataContext(), - try { - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - loading: true, - error: null, - }, - })); + // Selected devlog state + currentDevlogId: null, + currentDevlogContext: getDefaultDataContext(), + currentDevlogNotesContext: getDefaultDataContext(), - // Fetch recent devlog for navigation - limit to 50 most recent - const { items: data } = await devlogApiClient.list( - {}, // No filters - { page: 1, limit: 50 }, // Simple pagination - { sortBy: 'id', sortOrder: 'desc' }, // Sort by ID descending - ); + // Stats state + statsContext: getDefaultDataContext(), - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - data, - error: null, - }, - })); - } catch (err) { - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - error: handleApiError(err), - }, - })); - } finally { - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - loading: false, - }, - })); - } - }, + // Time series stats state + timeSeriesStatsContext: getDefaultDataContext(), - fetchStats: async () => { - const devlogApiClient = getDevlogApiClient(); + // Actions + setCurrentDevlogId: (id: DevlogId) => { + set({ + currentDevlogId: id, + currentDevlogContext: getDefaultDataContext(), + }); + }, - if (!devlogApiClient) { + setDevlogsFilters: (filters) => { + const currentFilters = get().devlogsContext.filters; set((state) => ({ - statsContext: { - ...state.statsContext, - loading: false, + devlogsContext: { + ...state.devlogsContext, + filters: { + ...currentFilters, + ...filters, + }, }, })); - return; - } + }, - try { + setDevlogsPagination: (pagination: PaginationMeta) => { set((state) => ({ - statsContext: { - ...state.statsContext, - loading: true, - error: null, - }, - })); - const statsData = await devlogApiClient.getStatsOverview(); - set((state) => ({ - statsContext: { - ...state.statsContext, - data: statsData, - error: null, - }, - })); - } catch (err) { - const errorMessage = handleApiError(err); - console.error('Failed to fetch stats:', err); - set((state) => ({ - statsContext: { - ...state.statsContext, - error: errorMessage, - }, - })); - } finally { - set((state) => ({ - statsContext: { - ...state.statsContext, - loading: false, + devlogsContext: { + ...state.devlogsContext, + pagination: { + ...state.devlogsContext.pagination, + ...pagination, + }, }, })); - } - }, - - fetchTimeSeriesStats: async () => { - const devlogApiClient = getDevlogApiClient(); + }, - if (!devlogApiClient) { + setDevlogsSortOptions: (sortOptions: SortOptions) => { set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - loading: false, + devlogsContext: { + ...state.devlogsContext, + sortOptions, }, })); - return; - } + }, - try { - set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - loading: true, - error: null, - }, - })); - const timeSeriesData = await devlogApiClient.getStatsTimeseries('month'); - set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - data: timeSeriesData, - error: null, - }, - })); - } catch (err) { - const errorMessage = handleApiError(err); - console.error('Failed to fetch time series stats:', err); - set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - error: errorMessage, - }, - })); - } finally { - set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - loading: false, - }, - })); - } - }, + fetchDevlogs: async () => { + debouncedFetchDevlogs(); + }, - fetchCurrentDevlog: async () => { - const { currentDevlogId } = get(); - const devlogApiClient = getDevlogApiClient(); - if (!currentDevlogId || !devlogApiClient) { - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - loading: false, - error: 'No devlog selected or API client unavailable', - }, - })); - return; - } + fetchNavigationDevlogs: async () => { + const devlogApiClient = getDevlogApiClient(); - try { - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - loading: true, - error: null, - }, - })); - const currentDevlog = await devlogApiClient.get(currentDevlogId); - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - data: currentDevlog, - error: null, - }, - })); - } catch (err) { - const errorMessage = handleApiError(err); - console.error('Failed to fetch selected devlog:', err); - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - error: errorMessage, - }, - })); - } finally { - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - loading: false, - }, - })); - } - }, + if (!devlogApiClient) { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + loading: false, + }, + })); + return; + } - clearCurrentDevlog: () => { - set({ - currentDevlogContext: getDefaultDataContext(), - }); - }, + try { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + loading: true, + error: null, + }, + })); - fetchCurrentDevlogNotes: async () => { - const { currentDevlogId } = get(); - const devlogApiClient = getDevlogApiClient(); + // Fetch recent devlog for navigation - limit to 50 most recent + const { items: data } = await devlogApiClient.list( + {}, // No filters + { page: 1, limit: 50 }, // Simple pagination + { sortBy: 'id', sortOrder: 'desc' }, // Sort by ID descending + ); - if (!currentDevlogId || !devlogApiClient) { - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - loading: false, - error: 'No devlog selected or API client unavailable', - }, - })); - return; - } + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + data, + error: null, + }, + })); + } catch (err) { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + error: handleApiError(err), + }, + })); + } finally { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + loading: false, + }, + })); + } + }, - try { - // set({ currentDevlogNotesLoading: true, currentDevlogNotesError: null }); - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - loading: true, - error: null, - }, - })); - const notes = await devlogApiClient.getNotes(currentDevlogId); - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - data: notes, - error: null, - }, - })); - } catch (err) { - const errorMessage = handleApiError(err); - console.error('Failed to fetch devlog notes:', err); - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - error: errorMessage, - }, - })); - // set({ currentDevlogNotesError: errorMessage }); - } finally { - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - loading: false, - }, - })); - } - }, + fetchStats: async () => { + const devlogApiClient = getDevlogApiClient(); - clearCurrentDevlogNotes: () => { - set({ currentDevlogNotesContext: getDefaultDataContext() }); - }, + if (!devlogApiClient) { + set((state) => ({ + statsContext: { + ...state.statsContext, + loading: false, + }, + })); + return; + } - createDevlog: async (data: Partial) => { - const currentProject = useProjectStore.getState().currentProjectContext.data; - const devlogApiClient = getDevlogApiClient(); + try { + set((state) => ({ + statsContext: { + ...state.statsContext, + loading: true, + error: null, + }, + })); + const statsData = await devlogApiClient.getStatsOverview(); + set((state) => ({ + statsContext: { + ...state.statsContext, + data: statsData, + error: null, + }, + })); + } catch (err) { + const errorMessage = handleApiError(err); + console.error('Failed to fetch stats:', err); + set((state) => ({ + statsContext: { + ...state.statsContext, + error: errorMessage, + }, + })); + } finally { + set((state) => ({ + statsContext: { + ...state.statsContext, + loading: false, + }, + })); + } + }, - if (!currentProject || !devlogApiClient) { - throw new Error('No project selected or API client unavailable'); - } + fetchTimeSeriesStats: async () => { + const devlogApiClient = getDevlogApiClient(); - return devlogApiClient.create(data as any); - }, + if (!devlogApiClient) { + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + loading: false, + }, + })); + return; + } - updateDevlog: async (data: Partial & { id: DevlogId }) => { - const currentProject = useProjectStore.getState().currentProjectContext.data; - const devlogApiClient = getDevlogApiClient(); + try { + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + loading: true, + error: null, + }, + })); + const timeSeriesData = await devlogApiClient.getStatsTimeseries('month'); + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + data: timeSeriesData, + error: null, + }, + })); + } catch (err) { + const errorMessage = handleApiError(err); + console.error('Failed to fetch time series stats:', err); + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + error: errorMessage, + }, + })); + } finally { + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + loading: false, + }, + })); + } + }, - if (!currentProject || !devlogApiClient) { - throw new Error('No project selected or API client unavailable'); - } + fetchCurrentDevlog: async () => { + const { currentDevlogId } = get(); + const devlogApiClient = getDevlogApiClient(); + if (!currentDevlogId || !devlogApiClient) { + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + loading: false, + error: 'No devlog selected or API client unavailable', + }, + })); + return; + } - const { id, ...updateData } = data; - return devlogApiClient.update(id, updateData as any); - }, + try { + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + loading: true, + error: null, + }, + })); + const currentDevlog = await devlogApiClient.get(currentDevlogId); + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + data: currentDevlog, + error: null, + }, + })); + } catch (err) { + const errorMessage = handleApiError(err); + console.error('Failed to fetch selected devlog:', err); + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + error: errorMessage, + }, + })); + } finally { + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + loading: false, + }, + })); + } + }, - updateSelectedDevlog: async (data: Partial & { id: DevlogId }) => { - const currentProject = useProjectStore.getState().currentProjectContext.data; - const devlogApiClient = getDevlogApiClient(); + clearCurrentDevlog: () => { + set({ + currentDevlogContext: getDefaultDataContext(), + }); + }, - if (!currentProject || !devlogApiClient) { - throw new Error('No project selected or API client unavailable'); - } + fetchCurrentDevlogNotes: async () => { + const { currentDevlogId } = get(); + const devlogApiClient = getDevlogApiClient(); - const { id, ...updateData } = data; - const updatedDevlog = await devlogApiClient.update(id, updateData as any); - - // Update both selected devlog and list if the devlog exists in the list - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - data: updatedDevlog, - error: null, - }, - })); - - const { devlogsContext } = get(); - const devlogs = devlogsContext.data; - if (devlogs) { - const index = devlogs.findIndex((devlog) => devlog.id === updatedDevlog.id); - if (index >= 0) { - const updated = [...devlogs]; - updated[index] = updatedDevlog; + if (!currentDevlogId || !devlogApiClient) { set((state) => ({ - devlogsContext: { - ...state.devlogsContext, - data: updated, + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + loading: false, + error: 'No devlog selected or API client unavailable', }, })); + return; } - } - return updatedDevlog; - }, + try { + // set({ currentDevlogNotesLoading: true, currentDevlogNotesError: null }); + set((state) => ({ + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + loading: true, + error: null, + }, + })); + const notes = await devlogApiClient.getNotes(currentDevlogId); + set((state) => ({ + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + data: notes, + error: null, + }, + })); + } catch (err) { + const errorMessage = handleApiError(err); + console.error('Failed to fetch devlog notes:', err); + set((state) => ({ + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + error: errorMessage, + }, + })); + // set({ currentDevlogNotesError: errorMessage }); + } finally { + set((state) => ({ + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + loading: false, + }, + })); + } + }, - deleteDevlog: async (id: DevlogId) => { - const currentProject = useProjectStore.getState().currentProjectContext.data; - const devlogApiClient = getDevlogApiClient(); + clearCurrentDevlogNotes: () => { + set({ currentDevlogNotesContext: getDefaultDataContext() }); + }, - if (!currentProject || !devlogApiClient) { - throw new Error('No project selected or API client unavailable'); - } + createDevlog: async (data: Partial) => { + const currentProject = useProjectStore.getState().currentProjectContext.data; + const devlogApiClient = getDevlogApiClient(); - try { - await devlogApiClient.delete(id); - } catch (error) { - throw error; - } finally { - await get().fetchDevlogs(); - } - }, + if (!currentProject || !devlogApiClient) { + throw new Error('No project selected or API client unavailable'); + } - batchUpdate: async (ids: DevlogId[], updates: any) => { - const currentProject = useProjectStore.getState().currentProjectContext.data; - const devlogApiClient = getDevlogApiClient(); + return devlogApiClient.create(data as any); + }, - if (!currentProject || !devlogApiClient) { - throw new Error('No project selected or API client unavailable'); - } + updateDevlog: async (data: Partial & { id: DevlogId }) => { + const currentProject = useProjectStore.getState().currentProjectContext.data; + const devlogApiClient = getDevlogApiClient(); - const result = await devlogApiClient.batchUpdate(ids, updates); - await get().fetchDevlogs(); - return result; - }, + if (!currentProject || !devlogApiClient) { + throw new Error('No project selected or API client unavailable'); + } - batchDelete: async (ids: DevlogId[]) => { - const currentProject = useProjectStore.getState().currentProjectContext.data; - const devlogApiClient = getDevlogApiClient(); + const { id, ...updateData } = data; + return devlogApiClient.update(id, updateData as any); + }, - if (!currentProject || !devlogApiClient) { - throw new Error('No project selected or API client unavailable'); - } + updateSelectedDevlog: async (data: Partial & { id: DevlogId }) => { + const currentProject = useProjectStore.getState().currentProjectContext.data; + const devlogApiClient = getDevlogApiClient(); - await devlogApiClient.batchDelete(ids); - await get().fetchDevlogs(); - }, - - handleStatusFilter: (filterValue: FilterType | DevlogStatus) => { - const { devlogsContext } = get(); - const { filters } = devlogsContext; - if (['total', 'open', 'closed'].includes(filterValue)) { - get().setDevlogsFilters({ - ...filters, - status: undefined, - }); - } else { - get().setDevlogsFilters({ - ...filters, - status: [filterValue as DevlogStatus], - }); - } - }, - - clearErrors: () => { - set((state) => ({ - devlogsContext: { - ...state.devlogsContext, - error: null, - }, - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - error: null, - }, - statsContext: { - ...state.statsContext, - error: null, - }, - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - error: null, - }, - currentDevlogContext: { - ...state.currentDevlogContext, - error: null, - }, - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - error: null, - }, - })); - }, - })), + if (!currentProject || !devlogApiClient) { + throw new Error('No project selected or API client unavailable'); + } + + const { id, ...updateData } = data; + const updatedDevlog = await devlogApiClient.update(id, updateData as any); + + // Update both selected devlog and list if the devlog exists in the list + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + data: updatedDevlog, + error: null, + }, + })); + + const { devlogsContext } = get(); + const devlogs = devlogsContext.data; + if (devlogs) { + const index = devlogs.findIndex((devlog) => devlog.id === updatedDevlog.id); + if (index >= 0) { + const updated = [...devlogs]; + updated[index] = updatedDevlog; + set((state) => ({ + devlogsContext: { + ...state.devlogsContext, + data: updated, + }, + })); + } + } + + return updatedDevlog; + }, + + deleteDevlog: async (id: DevlogId) => { + const currentProject = useProjectStore.getState().currentProjectContext.data; + const devlogApiClient = getDevlogApiClient(); + + if (!currentProject || !devlogApiClient) { + throw new Error('No project selected or API client unavailable'); + } + + try { + await devlogApiClient.delete(id); + } catch (error) { + throw error; + } finally { + await get().fetchDevlogs(); + } + }, + + batchUpdate: async (ids: DevlogId[], updates: any) => { + const currentProject = useProjectStore.getState().currentProjectContext.data; + const devlogApiClient = getDevlogApiClient(); + + if (!currentProject || !devlogApiClient) { + throw new Error('No project selected or API client unavailable'); + } + + const result = await devlogApiClient.batchUpdate(ids, updates); + await get().fetchDevlogs(); + return result; + }, + + batchDelete: async (ids: DevlogId[]) => { + const currentProject = useProjectStore.getState().currentProjectContext.data; + const devlogApiClient = getDevlogApiClient(); + + if (!currentProject || !devlogApiClient) { + throw new Error('No project selected or API client unavailable'); + } + + await devlogApiClient.batchDelete(ids); + await get().fetchDevlogs(); + }, + + handleStatusFilter: (filterValue: FilterType | DevlogStatus) => { + const { devlogsContext } = get(); + const { filters } = devlogsContext; + if (['total', 'open', 'closed'].includes(filterValue)) { + get().setDevlogsFilters({ + ...filters, + status: undefined, + }); + } else { + get().setDevlogsFilters({ + ...filters, + status: [filterValue as DevlogStatus], + }); + } + }, + + clearErrors: () => { + set((state) => ({ + devlogsContext: { + ...state.devlogsContext, + error: null, + }, + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + error: null, + }, + statsContext: { + ...state.statsContext, + error: null, + }, + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + error: null, + }, + currentDevlogContext: { + ...state.currentDevlogContext, + error: null, + }, + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + error: null, + }, + })); + }, + }; + }), ); diff --git a/apps/web/stores/project-store.ts b/apps/web/stores/project-store.ts index 6a268543..0470f00c 100644 --- a/apps/web/stores/project-store.ts +++ b/apps/web/stores/project-store.ts @@ -25,9 +25,9 @@ interface ProjectState { export const useProjectStore = create()( subscribeWithSelector((set, get) => { // Create debounced version of the actual fetch function - const debouncedFetch = debounce(async () => { + const debouncedFetchCurrentProject = debounce(async () => { const { currentProjectName } = get(); - + if (!currentProjectName) { set((state) => ({ currentProjectContext: { @@ -88,7 +88,7 @@ export const useProjectStore = create()( }, fetchCurrentProject: async () => { - debouncedFetch(); + debouncedFetchCurrentProject(); }, fetchProjects: async () => { From a1b7e2fca081cdd92e9e7a07c88f3f4fbbfc95c6 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 22:02:55 +0800 Subject: [PATCH 33/50] feat: implement debounced fetching for navigation devlogs, stats, and current devlog details --- apps/web/stores/devlog-store.ts | 488 +++++++++++++++++--------------- 1 file changed, 253 insertions(+), 235 deletions(-) diff --git a/apps/web/stores/devlog-store.ts b/apps/web/stores/devlog-store.ts index 44906b23..23f99561 100644 --- a/apps/web/stores/devlog-store.ts +++ b/apps/web/stores/devlog-store.ts @@ -150,6 +150,254 @@ export const useDevlogStore = create()( } }); + const debouncedFetchNavigationDevlogs = debounce(async () => { + const devlogApiClient = getDevlogApiClient(); + + if (!devlogApiClient) { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + loading: false, + }, + })); + return; + } + + try { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + loading: true, + error: null, + }, + })); + + // Fetch recent devlog for navigation - limit to 50 most recent + const { items: data } = await devlogApiClient.list( + {}, // No filters + { page: 1, limit: 50 }, // Simple pagination + { sortBy: 'id', sortOrder: 'desc' }, // Sort by ID descending + ); + + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + data, + error: null, + }, + })); + } catch (err) { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + error: handleApiError(err), + }, + })); + } finally { + set((state) => ({ + navigationDevlogsContext: { + ...state.navigationDevlogsContext, + loading: false, + }, + })); + } + }); + + const debouncedFetchStats = debounce(async () => { + const devlogApiClient = getDevlogApiClient(); + + if (!devlogApiClient) { + set((state) => ({ + statsContext: { + ...state.statsContext, + loading: false, + }, + })); + return; + } + + try { + set((state) => ({ + statsContext: { + ...state.statsContext, + loading: true, + error: null, + }, + })); + const statsData = await devlogApiClient.getStatsOverview(); + set((state) => ({ + statsContext: { + ...state.statsContext, + data: statsData, + error: null, + }, + })); + } catch (err) { + const errorMessage = handleApiError(err); + console.error('Failed to fetch stats:', err); + set((state) => ({ + statsContext: { + ...state.statsContext, + error: errorMessage, + }, + })); + } finally { + set((state) => ({ + statsContext: { + ...state.statsContext, + loading: false, + }, + })); + } + }); + + const debouncedFetchTimeSeriesStats = debounce(async () => { + const devlogApiClient = getDevlogApiClient(); + + if (!devlogApiClient) { + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + loading: false, + }, + })); + return; + } + + try { + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + loading: true, + error: null, + }, + })); + const timeSeriesData = await devlogApiClient.getStatsTimeseries('month'); + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + data: timeSeriesData, + error: null, + }, + })); + } catch (err) { + const errorMessage = handleApiError(err); + console.error('Failed to fetch time series stats:', err); + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + error: errorMessage, + }, + })); + } finally { + set((state) => ({ + timeSeriesStatsContext: { + ...state.timeSeriesStatsContext, + loading: false, + }, + })); + } + }); + + const debouncedFetchCurrentDevlog = debounce(async () => { + const { currentDevlogId } = get(); + const devlogApiClient = getDevlogApiClient(); + if (!currentDevlogId || !devlogApiClient) { + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + loading: false, + error: 'No devlog selected or API client unavailable', + }, + })); + return; + } + + try { + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + loading: true, + error: null, + }, + })); + const currentDevlog = await devlogApiClient.get(currentDevlogId); + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + data: currentDevlog, + error: null, + }, + })); + } catch (err) { + const errorMessage = handleApiError(err); + console.error('Failed to fetch selected devlog:', err); + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + error: errorMessage, + }, + })); + } finally { + set((state) => ({ + currentDevlogContext: { + ...state.currentDevlogContext, + loading: false, + }, + })); + } + }); + + const debouncedFetchCurrentDevlogNotes = debounce(async () => { + const { currentDevlogId } = get(); + const devlogApiClient = getDevlogApiClient(); + + if (!currentDevlogId || !devlogApiClient) { + set((state) => ({ + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + loading: false, + error: 'No devlog selected or API client unavailable', + }, + })); + return; + } + + try { + set((state) => ({ + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + loading: true, + error: null, + }, + })); + const notes = await devlogApiClient.getNotes(currentDevlogId); + set((state) => ({ + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + data: notes, + error: null, + }, + })); + } catch (err) { + const errorMessage = handleApiError(err); + console.error('Failed to fetch devlog notes:', err); + set((state) => ({ + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + error: errorMessage, + }, + })); + } finally { + set((state) => ({ + currentDevlogNotesContext: { + ...state.currentDevlogNotesContext, + loading: false, + }, + })); + } + }); + return { // Initial state devlogsContext: getDefaultTableDataContext(), @@ -215,201 +463,19 @@ export const useDevlogStore = create()( }, fetchNavigationDevlogs: async () => { - const devlogApiClient = getDevlogApiClient(); - - if (!devlogApiClient) { - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - loading: false, - }, - })); - return; - } - - try { - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - loading: true, - error: null, - }, - })); - - // Fetch recent devlog for navigation - limit to 50 most recent - const { items: data } = await devlogApiClient.list( - {}, // No filters - { page: 1, limit: 50 }, // Simple pagination - { sortBy: 'id', sortOrder: 'desc' }, // Sort by ID descending - ); - - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - data, - error: null, - }, - })); - } catch (err) { - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - error: handleApiError(err), - }, - })); - } finally { - set((state) => ({ - navigationDevlogsContext: { - ...state.navigationDevlogsContext, - loading: false, - }, - })); - } + debouncedFetchNavigationDevlogs(); }, fetchStats: async () => { - const devlogApiClient = getDevlogApiClient(); - - if (!devlogApiClient) { - set((state) => ({ - statsContext: { - ...state.statsContext, - loading: false, - }, - })); - return; - } - - try { - set((state) => ({ - statsContext: { - ...state.statsContext, - loading: true, - error: null, - }, - })); - const statsData = await devlogApiClient.getStatsOverview(); - set((state) => ({ - statsContext: { - ...state.statsContext, - data: statsData, - error: null, - }, - })); - } catch (err) { - const errorMessage = handleApiError(err); - console.error('Failed to fetch stats:', err); - set((state) => ({ - statsContext: { - ...state.statsContext, - error: errorMessage, - }, - })); - } finally { - set((state) => ({ - statsContext: { - ...state.statsContext, - loading: false, - }, - })); - } + debouncedFetchStats(); }, fetchTimeSeriesStats: async () => { - const devlogApiClient = getDevlogApiClient(); - - if (!devlogApiClient) { - set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - loading: false, - }, - })); - return; - } - - try { - set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - loading: true, - error: null, - }, - })); - const timeSeriesData = await devlogApiClient.getStatsTimeseries('month'); - set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - data: timeSeriesData, - error: null, - }, - })); - } catch (err) { - const errorMessage = handleApiError(err); - console.error('Failed to fetch time series stats:', err); - set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - error: errorMessage, - }, - })); - } finally { - set((state) => ({ - timeSeriesStatsContext: { - ...state.timeSeriesStatsContext, - loading: false, - }, - })); - } + debouncedFetchTimeSeriesStats(); }, fetchCurrentDevlog: async () => { - const { currentDevlogId } = get(); - const devlogApiClient = getDevlogApiClient(); - if (!currentDevlogId || !devlogApiClient) { - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - loading: false, - error: 'No devlog selected or API client unavailable', - }, - })); - return; - } - - try { - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - loading: true, - error: null, - }, - })); - const currentDevlog = await devlogApiClient.get(currentDevlogId); - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - data: currentDevlog, - error: null, - }, - })); - } catch (err) { - const errorMessage = handleApiError(err); - console.error('Failed to fetch selected devlog:', err); - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - error: errorMessage, - }, - })); - } finally { - set((state) => ({ - currentDevlogContext: { - ...state.currentDevlogContext, - loading: false, - }, - })); - } + debouncedFetchCurrentDevlog(); }, clearCurrentDevlog: () => { @@ -419,55 +485,7 @@ export const useDevlogStore = create()( }, fetchCurrentDevlogNotes: async () => { - const { currentDevlogId } = get(); - const devlogApiClient = getDevlogApiClient(); - - if (!currentDevlogId || !devlogApiClient) { - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - loading: false, - error: 'No devlog selected or API client unavailable', - }, - })); - return; - } - - try { - // set({ currentDevlogNotesLoading: true, currentDevlogNotesError: null }); - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - loading: true, - error: null, - }, - })); - const notes = await devlogApiClient.getNotes(currentDevlogId); - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - data: notes, - error: null, - }, - })); - } catch (err) { - const errorMessage = handleApiError(err); - console.error('Failed to fetch devlog notes:', err); - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - error: errorMessage, - }, - })); - // set({ currentDevlogNotesError: errorMessage }); - } finally { - set((state) => ({ - currentDevlogNotesContext: { - ...state.currentDevlogNotesContext, - loading: false, - }, - })); - } + debouncedFetchCurrentDevlogNotes(); }, clearCurrentDevlogNotes: () => { From 06633915a531f61d4ada4686f554a692e563ea97 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 22:04:52 +0800 Subject: [PATCH 34/50] feat: implement debounced fetching for projects in project store --- apps/web/stores/project-store.ts | 70 +++++++++++++++++--------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/apps/web/stores/project-store.ts b/apps/web/stores/project-store.ts index 0470f00c..2b8c1d5b 100644 --- a/apps/web/stores/project-store.ts +++ b/apps/web/stores/project-store.ts @@ -76,6 +76,42 @@ export const useProjectStore = create()( } }); + const debouncedFetchProjects = debounce(async () => { + try { + set((state) => ({ + projectsContext: { + ...state.projectsContext, + loading: true, + error: null, + }, + })); + const projectsList = await projectApiClient.list(); + set((state) => ({ + projectsContext: { + ...state.projectsContext, + data: projectsList, + error: null, + }, + })); + } catch (err) { + const errorMessage = handleApiError(err); + set((state) => ({ + projectsContext: { + ...state.projectsContext, + error: errorMessage, + }, + })); + console.error('Error loading projects:', err); + } finally { + set((state) => ({ + projectsContext: { + ...state.projectsContext, + loading: false, + }, + })); + } + }); + return { // Initial state currentProjectName: null, @@ -92,39 +128,7 @@ export const useProjectStore = create()( }, fetchProjects: async () => { - try { - set((state) => ({ - projectsContext: { - ...state.projectsContext, - loading: true, - error: null, - }, - })); - const projectsList = await projectApiClient.list(); - set((state) => ({ - projectsContext: { - ...state.projectsContext, - data: projectsList, - error: null, - }, - })); - } catch (err) { - const errorMessage = handleApiError(err); - set((state) => ({ - projectsContext: { - ...state.projectsContext, - error: errorMessage, - }, - })); - console.error('Error loading projects:', err); - } finally { - set((state) => ({ - projectsContext: { - ...state.projectsContext, - loading: false, - }, - })); - } + debouncedFetchProjects(); }, clearErrors: () => { From 89b500d181052538cbc7080f3ca1933056b9c2a1 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 22:11:15 +0800 Subject: [PATCH 35/50] feat: optimize search functionality with debounced input handling --- .../components/feature/devlog/devlog-list.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/web/components/feature/devlog/devlog-list.tsx b/apps/web/components/feature/devlog/devlog-list.tsx index 4c4c56cc..609dfd38 100644 --- a/apps/web/components/feature/devlog/devlog-list.tsx +++ b/apps/web/components/feature/devlog/devlog-list.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { @@ -185,15 +185,22 @@ export function DevlogList({ }; // Handle search - const handleSearch = debounce((value: string) => { + const debouncedFilterChange = useMemo( + () => debounce((value: string) => { + if (onFilterChange) { + onFilterChange({ + ...filters, + search: value || undefined, + }); + } + }, 300), + [onFilterChange, filters] + ); + + const handleSearch = useCallback((value: string) => { setSearchText(value); - if (onFilterChange) { - onFilterChange({ - ...filters, - search: value || undefined, - }); - } - }); + debouncedFilterChange(value); + }, [debouncedFilterChange]); // Handle filter changes const handleFilterChange = (key: string, value: string | undefined) => { From 11f75e67b31395dfdc852fef68d5249c9e25dcb1 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Sun, 24 Aug 2025 22:38:01 +0800 Subject: [PATCH 36/50] feat: refactor project and devlog components to use useCallback for improved performance --- .../[name]/settings/project-settings-page.tsx | 23 +++--- apps/web/app/projects/project-list-page.tsx | 40 ++++++---- .../common/overview-stats/overview-stats.tsx | 30 ++++---- apps/web/components/custom/editable-field.tsx | 74 +++++++++++-------- .../components/feature/devlog/devlog-list.tsx | 18 ++++- 5 files changed, 112 insertions(+), 73 deletions(-) diff --git a/apps/web/app/projects/[name]/settings/project-settings-page.tsx b/apps/web/app/projects/[name]/settings/project-settings-page.tsx index 3ec09783..547e704b 100644 --- a/apps/web/app/projects/[name]/settings/project-settings-page.tsx +++ b/apps/web/app/projects/[name]/settings/project-settings-page.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { useProjectStore } from '@/stores'; import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; @@ -79,7 +79,7 @@ export function ProjectSettingsPage() { fetchCurrentProject(); }, [currentProjectName]); - const handleUpdateProject = async (e: React.FormEvent) => { + const handleUpdateProject = useCallback(async (e: React.FormEvent) => { e.preventDefault(); if (!formData.name.trim()) { @@ -109,9 +109,9 @@ export function ProjectSettingsPage() { } finally { setIsUpdating(false); } - }; + }, [formData, project, updateProject]); - const handleDeleteProject = async () => { + const handleDeleteProject = useCallback(async () => { if (!project) { toast.error('Project not found'); return; @@ -130,9 +130,9 @@ export function ProjectSettingsPage() { } finally { setIsDeleting(false); } - }; + }, [project, deleteProject, router]); - const handleResetForm = () => { + const handleResetForm = useCallback(() => { if (project) { setFormData({ name: project.name, @@ -140,7 +140,12 @@ export function ProjectSettingsPage() { }); setHasChanges(false); } - }; + }, [project]); + + const handleFormChange = useCallback((field: keyof ProjectFormData, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + setHasChanges(true); + }, []); if (currentProjectContext.loading || !project) { return ( @@ -250,7 +255,7 @@ export function ProjectSettingsPage() { id="name" placeholder="e.g., My Development Project" value={formData.name} - onChange={(e) => setFormData({ ...formData, name: e.target.value })} + onChange={(e) => handleFormChange('name', e.target.value)} required />
@@ -261,7 +266,7 @@ export function ProjectSettingsPage() { id="description" placeholder="Describe what this project is about..." value={formData.description || ''} - onChange={(e) => setFormData({ ...formData, description: e.target.value })} + onChange={(e) => handleFormChange('description', e.target.value)} rows={3} />
diff --git a/apps/web/app/projects/project-list-page.tsx b/apps/web/app/projects/project-list-page.tsx index ba368bdc..83c89a0a 100644 --- a/apps/web/app/projects/project-list-page.tsx +++ b/apps/web/app/projects/project-list-page.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { useProjectStore, useRealtimeStore } from '@/stores'; import { useRouter } from 'next/navigation'; import { ProjectGridSkeleton } from '@/components/common'; @@ -57,7 +57,7 @@ export function ProjectListPage() { const { data: projects, loading: isLoadingProjects } = projectsContext; - const handleCreateProject = async (e: React.FormEvent) => { + const handleCreateProject = useCallback(async (e: React.FormEvent) => { e.preventDefault(); if (!formData.name.trim()) { @@ -80,16 +80,29 @@ export function ProjectListPage() { } finally { setCreating(false); } - }; + }, [formData, fetchProjects]); - const handleViewProject = (projectName: string) => { + const handleViewProject = useCallback((projectName: string) => { router.push(`/projects/${projectName}`); - }; + }, [router]); - const handleProjectSettings = (e: React.MouseEvent, projectName: string) => { + const handleProjectSettings = useCallback((e: React.MouseEvent, projectName: string) => { e.stopPropagation(); // Prevent card click from triggering router.push(`/projects/${projectName}/settings`); - }; + }, [router]); + + const handleOpenModal = useCallback(() => { + setIsModalVisible(true); + }, []); + + const handleCloseModal = useCallback(() => { + setIsModalVisible(false); + setFormData({ name: '', description: '' }); + }, []); + + const handleFormChange = useCallback((field: keyof ProjectFormData, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }, []); if (projectsContext.error) { return ( @@ -108,7 +121,7 @@ export function ProjectListPage() {
-
@@ -177,7 +190,7 @@ export function ProjectListPage() {

@@ -224,10 +237,7 @@ export function ProjectListPage() { diff --git a/apps/web/components/common/overview-stats/overview-stats.tsx b/apps/web/components/common/overview-stats/overview-stats.tsx index 05319fda..f36ca460 100644 --- a/apps/web/components/common/overview-stats/overview-stats.tsx +++ b/apps/web/components/common/overview-stats/overview-stats.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { BarChart3 } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Skeleton } from '@/components/ui/skeleton'; @@ -52,32 +52,32 @@ export function OverviewStats({ return null; } - const isStatusActive = (status: DevlogStatus) => { + const isStatusActive = useCallback((status: DevlogStatus) => { return !!currentFilters?.status?.includes(status); - }; + }, [currentFilters]); - const isTotalActive = () => { + const isTotalActive = useCallback(() => { return ( (!currentFilters?.filterType || currentFilters.filterType === 'total') && (!currentFilters?.status || currentFilters.status.length === 0) ); - }; + }, [currentFilters]); - const isOpenActive = () => { + const isOpenActive = useCallback(() => { return currentFilters?.filterType === 'open'; - }; + }, [currentFilters]); - const isClosedActive = () => { + const isClosedActive = useCallback(() => { return currentFilters?.filterType === 'closed'; - }; + }, [currentFilters]); - const handleStatClick = (status: FilterType) => { + const handleStatClick = useCallback((status: FilterType) => { if (onFilterToggle) { onFilterToggle(status); } - }; + }, [onFilterToggle]); - const getStatClasses = (filterType: FilterType, isIndividualStatus = false) => { + const getStatClasses = useCallback((filterType: FilterType, isIndividualStatus = false) => { let isActive: boolean; if (filterType === 'total') { isActive = isTotalActive(); @@ -99,9 +99,9 @@ export function OverviewStats({ 'hover:bg-muted': isClickable && !isActive, }, ); - }; + }, [isTotalActive, isOpenActive, isClosedActive, isStatusActive, onFilterToggle]); - const getStatusColor = (status: DevlogStatus) => { + const getStatusColor = useCallback((status: DevlogStatus) => { const colors = { new: 'text-blue-600', 'in-progress': 'text-orange-600', @@ -112,7 +112,7 @@ export function OverviewStats({ cancelled: 'text-gray-600', }; return colors[status] || 'text-foreground'; - }; + }, []); const StatItem = ({ value, diff --git a/apps/web/components/custom/editable-field.tsx b/apps/web/components/custom/editable-field.tsx index d21fa0dc..1c36a6e8 100644 --- a/apps/web/components/custom/editable-field.tsx +++ b/apps/web/components/custom/editable-field.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback } from 'react'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { @@ -66,30 +66,26 @@ export function EditableField({ } }, [isEditing]); - const handleSave = () => { + const handleSave = useCallback(() => { onSave(editValue); setIsEditing(false); - }; + }, [editValue, onSave]); - const handleCancel = () => { + const handleCancel = useCallback(() => { setEditValue(value); setIsEditing(false); - }; + }, [value]); - const handleKeyPress = (e: React.KeyboardEvent) => { + const handleKeyPress = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' && !multiline && type !== 'textarea') { e.preventDefault(); handleSave(); } else if (e.key === 'Escape') { handleCancel(); } - }; - - const handleBlur = () => { - handleBlurWithValue(editValue); - }; + }, [multiline, type, handleSave, handleCancel]); - const handleBlurWithValue = (currentValue: string) => { + const handleBlurWithValue = useCallback((currentValue: string) => { if (draftMode) { // In draft mode, just save the local value and exit edit mode // The parent component will handle when to actually save @@ -101,20 +97,44 @@ export function EditableField({ // Original behavior: save changes when losing focus handleSave(); } - }; + }, [draftMode, value, onSave, handleSave]); + + const handleBlur = useCallback(() => { + handleBlurWithValue(editValue); + }, [editValue, handleBlurWithValue]); - const handleEnterEdit = () => { + const handleEnterEdit = useCallback(() => { setIsEditing(true); - }; + }, []); + + const handleEditValueChange = useCallback((newValue: string) => { + setEditValue(newValue); + }, []); + + const handleSelectValueChange = useCallback((newValue: string) => { + setEditValue(newValue); + if (draftMode) { + if (newValue !== value) { + onSave(newValue); + } + setIsEditing(false); + } + }, [draftMode, value, onSave]); + + const handleMouseEnter = useCallback(() => { + setIsHovered(true); + }, []); + + const handleMouseLeave = useCallback(() => { + setIsHovered(false); + }, []); const renderInput = () => { if (type === 'markdown') { return ( { - setEditValue(value); - }} + onChange={handleEditValueChange} onBlur={handleBlurWithValue} onCancel={handleCancel} placeholder={placeholder} @@ -127,15 +147,7 @@ export function EditableField({ return (