Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
POSTGRES_URL=postgres://postgres:postgres@localhost:5432/forge
BACKEND_URL=http://localhost:8080
BACKEND_URL=http://localhost:8080
API_KEY=1234567890

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Strengthen API key security.

The example API key is too simple and predictable. While this is just an example file, it might lead developers to use similarly weak keys in production.

Replace with a more secure example and add guidelines:

-API_KEY=1234567890
+# API_KEY should be a secure random string of at least 32 characters
+API_KEY=example_7d4e3s2n8j9k4m5n7p9w3k4m5n7p9w3k
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
API_KEY=1234567890
# API_KEY should be a secure random string of at least 32 characters
API_KEY=example_7d4e3s2n8j9k4m5n7p9w3k4m5n7p9w3k

135 changes: 71 additions & 64 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,66 +1,73 @@
{
"name": "riven-frontend",
"version": "1.0.0",
"private": false,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"typecheck": "tsc --noEmit",
"format": "prettier --check \"**/*.{ts,tsx}\" --cache",
"format:fix": "prettier --write \"**/*.{ts,tsx}\" --cache",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"generate-client": "openapi-ts"
},
"dependencies": {
"@hey-api/client-fetch": "^0.8.1",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-select": "^2.1.5",
"@tailwindcss/postcss": "^4.0.3",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.1",
"drizzle-orm": "^0.39.3",
"jose": "^5.9.6",
"lucide-react": "^0.475.0",
"next": "^15.2.0-canary.54",
"next-themes": "^0.4.4",
"postgres": "^3.4.5",
"react": "19.0.0",
"react-dom": "19.0.0",
"sonner": "^1.7.4",
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.3",
"vaul": "^1.1.2",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@hey-api/openapi-ts": "^0.64.1",
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@next/bundle-analyzer": "^15.1.6",
"@next/eslint-plugin-next": "^15.1.6",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^22.13.0",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"drizzle-kit": "^0.30.4",
"eslint": "^9.19.0",
"eslint-config-next": "15.1.6",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-tailwindcss": "^3.18.0",
"globals": "^15.14.0",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.7.3",
"typescript-eslint": "^8.22.0"
}
"name": "riven-frontend",
"version": "1.0.0",
"private": false,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"typecheck": "tsc --noEmit",
"format": "prettier --check \"**/*.{ts,tsx}\" --cache",
"format:fix": "prettier --write \"**/*.{ts,tsx}\" --cache",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"generate-client": "openapi-ts",
"prepare": "next-ws patch"
},
"dependencies": {
"@formkit/auto-animate": "^0.8.2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Use more specific version constraints.

The caret (^) version range could lead to unexpected breaking changes.

Consider using exact versions or tilde (~) for minor updates:

-        "@formkit/auto-animate": "^0.8.2",
-        "@leo1305/custom-next-ws": "^1.0.0",
-        "@types/ws": "^8.5.14",
-        "ws": "^8.18.0",
+        "@formkit/auto-animate": "~0.8.2",
+        "@leo1305/custom-next-ws": "1.0.0",
+        "@types/ws": "~8.5.14",
+        "ws": "~8.18.0",

Also applies to: 20-20, 26-26, 42-42

"@hey-api/client-fetch": "^0.8.1",
"@leo1305/custom-next-ws": "^1.0.0",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-toast": "^1.2.5",
"@tailwindcss/postcss": "^4.0.3",
"@types/ws": "^8.5.14",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.1",
"drizzle-orm": "^0.39.3",
"jose": "^5.9.6",
"lucide-react": "^0.475.0",
"next": "^15.2.0-canary.54",
"next-themes": "^0.4.4",
"postgres": "^3.4.5",
"react": "19.0.0",
"react-dom": "19.0.0",
Comment on lines +38 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

❓ Verification inconclusive

Update React version to match Next.js requirements.

The React version (19.0.0) is outdated and may not be compatible with Next.js 15.


🌐 Web query:

What is the compatible React version for Next.js 15?

💡 Result:

Next.js 15's compatible React version depends on which routing system you use:

App Router
Requires React 19+ (RC or stable) due to architectural dependencies on new React features like Actions and Async Components[1][4][6]. Attempting to use React 18 with App Router will cause compatibility issues[1][6].

Pages Router
Supports React 18+ with backward compatibility maintained in Next.js 15[4][6]. This allows gradual migration paths for legacy projects[1][4].


Key Details

Aspect App Router Pages Router
React Version 19+ (mandatory) 18+ (supported)
Stability Requires RC/stable¹ Fully stable
Mixed Usage Not recommended² N/A

¹React 19 reached stable status in Next.js 15.1[5]
²Combining App Router (React 19) and Pages Router (React 18) in one project risks inconsistent behavior[4]


Migration Considerations

  • New projects using App Router must adopt React 19[1][4]
  • Legacy projects can stay on Pages Router + React 18 while upgrading Next.js core[4][6]
  • Critical dependencies not supporting React 19 may block App Router adoption[1][6]

The architecture split reflects Vercel's strategy to push React innovations through App Router while maintaining enterprise stability via Pages Router[1][4]. Developers should choose based on project requirements: bleeding-edge features vs dependency stability[1][4].

Citations:


React Version Compatibility – Verify Your Next.js Routing Setup

The current React version is set to 19.0.0 in package.json which aligns with Next.js 15's requirements if you are using the App Router (which mandates React 19+). However, if your project relies on the Pages Router, you should consider downgrading to React 18 to ensure optimal compatibility and stability.

  • Confirm which routing system your project uses.
  • If using the App Router, you can keep React 19.0.0.
  • If using the Pages Router, update React to version 18.x accordingly.

"sonner": "^1.7.4",
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.3",
"vaul": "^1.1.2",
"ws": "^8.18.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@hey-api/openapi-ts": "^0.64.1",
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove duplicate import sorting plugins.

Two different import sorting plugins are included:

  • @ianvs/prettier-plugin-sort-imports
  • @trivago/prettier-plugin-sort-imports

Choose one plugin to avoid conflicts:

         "@hey-api/openapi-ts": "^0.64.1",
         "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
         "@next/bundle-analyzer": "^15.1.6",
         "@next/eslint-plugin-next": "^15.1.6",
-        "@trivago/prettier-plugin-sort-imports": "^5.2.2",
         "@types/bcryptjs": "^2.4.6",

Also applies to: 53-53

"@next/bundle-analyzer": "^15.1.6",
"@next/eslint-plugin-next": "^15.1.6",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^22.13.0",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"drizzle-kit": "^0.30.4",
"eslint": "^9.19.0",
"eslint-config-next": "15.1.6",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-tailwindcss": "^3.18.0",
"globals": "^15.14.0",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.7.3",
"typescript-eslint": "^8.22.0"
}
}
37 changes: 37 additions & 0 deletions src/app/api/ws/[topic]/socket/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { env } from 'node:process'

const websocketUrl = (env.BACKEND_URL || 'http://localhost:8080').replace(/^http/, 'ws')
const apiKey = env.API_KEY
Comment on lines +3 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add validation for required environment variables.

The code assumes BACKEND_URL and API_KEY environment variables are available but doesn't validate their presence.

Add environment variable validation:

 const websocketUrl = (env.BACKEND_URL || 'http://localhost:8080').replace(/^http/, 'ws')
-const apiKey = env.API_KEY
+const apiKey = env.API_KEY
+if (!apiKey) {
+    throw new Error('API_KEY environment variable is required')
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const websocketUrl = (env.BACKEND_URL || 'http://localhost:8080').replace(/^http/, 'ws')
const apiKey = env.API_KEY
const websocketUrl = (env.BACKEND_URL || 'http://localhost:8080').replace(/^http/, 'ws')
const apiKey = env.API_KEY
if (!apiKey) {
throw new Error('API_KEY environment variable is required')
}


export function SOCKET(
client: import('ws').WebSocket,
request: import('http').IncomingMessage,
server: import('ws').WebSocketServer,
{ params }: { params: { topic: string } }
) {
Comment on lines +6 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add input validation for topic parameter.

The topic parameter is used directly without validation, which could lead to security vulnerabilities.

Add input validation to prevent injection attacks and ensure the topic follows expected format:

 export function SOCKET(
     client: import('ws').WebSocket,
     request: import('http').IncomingMessage,
     server: import('ws').WebSocketServer,
     { params }: { params: { topic: string } }
 ) {
+    // Validate topic parameter
+    if (!/^[a-zA-Z0-9_-]+$/.test(params.topic)) {
+        client.close(1008, 'Invalid topic format');
+        return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function SOCKET(
client: import('ws').WebSocket,
request: import('http').IncomingMessage,
server: import('ws').WebSocketServer,
{ params }: { params: { topic: string } }
) {
export function SOCKET(
client: import('ws').WebSocket,
request: import('http').IncomingMessage,
server: import('ws').WebSocketServer,
{ params }: { params: { topic: string } }
) {
// Validate topic parameter
if (!/^[a-zA-Z0-9_-]+$/.test(params.topic)) {
client.close(1008, 'Invalid topic format');
return;
}
// ... rest of the function implementation
}

console.log('A client connected')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Enhance logging with structured data.

Basic console logging doesn't provide enough context for production monitoring.

Add structured logging with relevant metadata:

-    console.log('A client connected')
+    console.log('WebSocket client connected', {
+        topic: params.topic,
+        timestamp: new Date().toISOString(),
+        clientId: request.headers['x-client-id'] || 'unknown'
+    })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log('A client connected')
console.log('WebSocket client connected', {
topic: params.topic,
timestamp: new Date().toISOString(),
clientId: request.headers['x-client-id'] || 'unknown'
})


const nativeWebSocket = new WebSocket(`${websocketUrl}/api/v1/ws/${params.topic}?api_key=${apiKey}`)

client.onmessage = (message) => {
nativeWebSocket.send(message.data.toString())
}
Comment on lines +16 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add message size validation.

The code forwards messages without validating their size, which could lead to memory issues.

Add message size validation:

     client.onmessage = (message) => {
+        const MAX_MESSAGE_SIZE = 1024 * 1024; // 1MB
+        if (message.data.length > MAX_MESSAGE_SIZE) {
+            client.close(1009, 'Message too large');
+            return;
+        }
         nativeWebSocket.send(message.data.toString())
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
client.onmessage = (message) => {
nativeWebSocket.send(message.data.toString())
}
client.onmessage = (message) => {
const MAX_MESSAGE_SIZE = 1024 * 1024; // 1MB
if (message.data.length > MAX_MESSAGE_SIZE) {
client.close(1009, 'Message too large');
return;
}
nativeWebSocket.send(message.data.toString())
}


nativeWebSocket.onmessage = (message) => {
client.send(message.data)
}
Comment on lines +14 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement error handling for WebSocket connection.

The native WebSocket connection lacks proper error handling for connection failures and invalid API key responses.

Add error handling for the native WebSocket connection:

-    const nativeWebSocket = new WebSocket(`${websocketUrl}/api/v1/ws/${params.topic}?api_key=${apiKey}`)
+    const nativeWebSocket = new WebSocket(`${websocketUrl}/api/v1/ws/${params.topic}?api_key=${apiKey}`)
+
+    // Add connection timeout
+    const connectionTimeout = setTimeout(() => {
+        if (nativeWebSocket.readyState !== WebSocket.OPEN) {
+            nativeWebSocket.close();
+            client.close(1006, 'Connection timeout');
+        }
+    }, 5000);
+
+    nativeWebSocket.onopen = () => {
+        clearTimeout(connectionTimeout);
+    }

     client.onmessage = (message) => {
+        if (nativeWebSocket.readyState !== WebSocket.OPEN) {
+            client.close(1006, 'Backend connection lost');
+            return;
+        }
         nativeWebSocket.send(message.data.toString())
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nativeWebSocket = new WebSocket(`${websocketUrl}/api/v1/ws/${params.topic}?api_key=${apiKey}`)
client.onmessage = (message) => {
nativeWebSocket.send(message.data.toString())
}
nativeWebSocket.onmessage = (message) => {
client.send(message.data)
}
const nativeWebSocket = new WebSocket(`${websocketUrl}/api/v1/ws/${params.topic}?api_key=${apiKey}`)
// Add connection timeout
const connectionTimeout = setTimeout(() => {
if (nativeWebSocket.readyState !== WebSocket.OPEN) {
nativeWebSocket.close();
client.close(1006, 'Connection timeout');
}
}, 5000);
nativeWebSocket.onopen = () => {
clearTimeout(connectionTimeout);
}
client.onmessage = (message) => {
if (nativeWebSocket.readyState !== WebSocket.OPEN) {
client.close(1006, 'Backend connection lost');
return;
}
nativeWebSocket.send(message.data.toString())
}
nativeWebSocket.onmessage = (message) => {
client.send(message.data)
}


nativeWebSocket.onerror = (error) => {
console.error('WebSocket error:', error)
}
Comment on lines +24 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and logging.

The error handling is basic and might miss important error details.

Improve error handling with more detailed logging:

     nativeWebSocket.onerror = (error) => {
-        console.error('WebSocket error:', error)
+        const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+        console.error('WebSocket error:', {
+            error: errorMessage,
+            topic: params.topic,
+            timestamp: new Date().toISOString()
+        });
+        client.close(1006, errorMessage);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nativeWebSocket.onerror = (error) => {
console.error('WebSocket error:', error)
}
nativeWebSocket.onerror = (error) => {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('WebSocket error:', {
error: errorMessage,
topic: params.topic,
timestamp: new Date().toISOString()
});
client.close(1006, errorMessage);
}


client.onclose = () => {
console.log('client closed')
nativeWebSocket.close()
}

nativeWebSocket.onclose = () => {
console.log('nativeWebSocket closed')
client.close()
}
}
103 changes: 103 additions & 0 deletions src/app/events/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use client'

import { useEffect, useState } from 'react'

import { Badge } from '@/components/ui/badge'
import { useAutoAnimate } from '@formkit/auto-animate/react'

type EventData = {
[key: string]: string[]
}

function getNewWebSocket() {
return new WebSocket('/api/ws/event_update/socket')
}
Comment on lines +12 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Configure WebSocket URL based on environment.

The WebSocket URL is hardcoded which could cause issues in different environments (development, staging, production).

Consider using environment variables:

+const WS_URL = process.env.NEXT_PUBLIC_WS_URL || '/api/ws/event_update/socket'
+
 function getNewWebSocket() {
-    return new WebSocket('/api/ws/event_update/socket')
+    return new WebSocket(WS_URL)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getNewWebSocket() {
return new WebSocket('/api/ws/event_update/socket')
}
const WS_URL = process.env.NEXT_PUBLIC_WS_URL || '/api/ws/event_update/socket'
function getNewWebSocket() {
return new WebSocket(WS_URL)
}


export default function EventsPage() {
const [events, setEvents] = useState<EventData | undefined>()
const [status, setStatus] = useState('Disconnected')
const [ws, setWs] = useState<WebSocket | undefined>()
Comment on lines +17 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider using a reducer for complex state management.

Multiple related state variables could be better managed using useReducer.

Consider refactoring to use useReducer:

-    const [events, setEvents] = useState<EventData | undefined>()
-    const [status, setStatus] = useState('Disconnected')
-    const [ws, setWs] = useState<WebSocket | undefined>()
+    const [state, dispatch] = useReducer(reducer, {
+        events: undefined,
+        status: 'Disconnected',
+        ws: undefined
+    })

Committable suggestion skipped: line range outside the PR's diff.

const [listRef] = useAutoAnimate()
const [itemsRef] = useAutoAnimate()

useEffect(() => {
const websocket = getNewWebSocket()

websocket.onopen = () => {
setStatus('Connected')
console.log('Connected to WebSocket')
}

websocket.onclose = () => {
setStatus('Disconnected')
console.log('Disconnected from WebSocket')

// Attempt to reconnect after 3 seconds
setTimeout(() => {
setStatus('Reconnecting...')
setWs(undefined)
const newWebsocket = getNewWebSocket()
setWs(newWebsocket)
}, 3000)
}

websocket.onerror = (error) => {
console.error('WebSocket error:', error)
setStatus('Error')
}

websocket.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('Received data:', data)
setEvents(data.data)
}

setWs(websocket)

// Cleanup on component unmount
return () => {
if (websocket) {
websocket.close()
}
}
Comment on lines +58 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Enhance WebSocket cleanup.

The cleanup function could be more robust by handling additional edge cases.

Consider enhancing the cleanup:

 return () => {
     if (websocket) {
+        // Remove all event listeners to prevent memory leaks
+        websocket.onclose = null;
+        websocket.onerror = null;
+        websocket.onmessage = null;
+        websocket.onopen = null;
+
+        // Only close if the connection is still open
+        if (websocket.readyState === WebSocket.OPEN) {
             websocket.close()
+        }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return () => {
if (websocket) {
websocket.close()
}
}
return () => {
if (websocket) {
// Remove all event listeners to prevent memory leaks
websocket.onclose = null;
websocket.onerror = null;
websocket.onmessage = null;
websocket.onopen = null;
// Only close if the connection is still open
if (websocket.readyState === WebSocket.OPEN) {
websocket.close()
}
}
}

}, [])

return (
<>
<div className='absolute top-4 right-4'>
{status === 'Connected' && <Badge variant={'secondary'}>Connected</Badge>}
{status === 'Disconnected' && <Badge variant={'destructive'}>Disconnected</Badge>}
{status === 'Reconnecting...' && <Badge variant={'gray'}>Reconnecting...</Badge>}
{status === 'Error' && <Badge variant={'destructive'}>Error</Badge>}
</div>
Comment on lines +67 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add initial connecting state.

The component lacks a visual indicator for the initial connection attempt.

Add a connecting state:

 <div className='absolute top-4 right-4'>
+    {status === 'Connecting...' && (
+        <Badge variant={'secondary'}>
+            <span className="animate-pulse">Connecting...</span>
+        </Badge>
+    )}
     {status === 'Connected' && <Badge variant={'secondary'}>Connected</Badge>}
     {status === 'Disconnected' && <Badge variant={'destructive'}>Disconnected</Badge>}
     {status === 'Reconnecting...' && <Badge variant={'gray'}>Reconnecting...</Badge>}
     {status === 'Error' && <Badge variant={'destructive'}>Error</Badge>}
 </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className='absolute top-4 right-4'>
{status === 'Connected' && <Badge variant={'secondary'}>Connected</Badge>}
{status === 'Disconnected' && <Badge variant={'destructive'}>Disconnected</Badge>}
{status === 'Reconnecting...' && <Badge variant={'gray'}>Reconnecting...</Badge>}
{status === 'Error' && <Badge variant={'destructive'}>Error</Badge>}
</div>
<div className='absolute top-4 right-4'>
{status === 'Connecting...' && (
<Badge variant={'secondary'}>
<span className="animate-pulse">Connecting...</span>
</Badge>
)}
{status === 'Connected' && <Badge variant={'secondary'}>Connected</Badge>}
{status === 'Disconnected' && <Badge variant={'destructive'}>Disconnected</Badge>}
{status === 'Reconnecting...' && <Badge variant={'gray'}>Reconnecting...</Badge>}
{status === 'Error' && <Badge variant={'destructive'}>Error</Badge>}
</div>

<div className='container mx-auto p-6'>
{events === undefined && <h1 className='text-xl font-bold'>Loading...</h1>}

{events === null && <h1 className='text-xl font-bold text-red-600'>Error</h1>}
Comment on lines +74 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add loading state timeout.

The loading state could persist indefinitely if the WebSocket connection fails.

Add a timeout for the loading state:

+    const LOADING_TIMEOUT = 10000; // 10 seconds
+    useEffect(() => {
+        if (events === undefined) {
+            const timer = setTimeout(() => {
+                setStatus('Error');
+                setEvents(null);
+            }, LOADING_TIMEOUT);
+            return () => clearTimeout(timer);
+        }
+    }, [events]);
+
     return (
         <>
             <div className='container mx-auto p-6'>
                 {events === undefined && <h1 className='text-xl font-bold'>Loading...</h1>}
                 {events === null && <h1 className='text-xl font-bold text-red-600'>Error</h1>}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{events === undefined && <h1 className='text-xl font-bold'>Loading...</h1>}
{events === null && <h1 className='text-xl font-bold text-red-600'>Error</h1>}
const LOADING_TIMEOUT = 10000; // 10 seconds
useEffect(() => {
if (events === undefined) {
const timer = setTimeout(() => {
setStatus('Error');
setEvents(null);
}, LOADING_TIMEOUT);
return () => clearTimeout(timer);
}
}, [events]);
return (
<>
<div className='container mx-auto p-6'>
{events === undefined && <h1 className='text-xl font-bold'>Loading...</h1>}
{events === null && <h1 className='text-xl font-bold text-red-600'>Error</h1>}
{/* Other component content */}
</div>
</>
);

{events && (
<div className='space-y-6'>
<h1 className='mb-6 text-2xl font-bold'>Events</h1>
{/* Apply animation to the container of event groups */}
<div ref={listRef} className='space-y-6'>
{Object.entries(events).map(([key, value]) => (
<div key={key} className='space-y-4'>
<h2 className='text-xl font-semibold'>{key}</h2>
{/* Apply animation to the container of individual events */}
<ul ref={itemsRef} className='flex flex-wrap space-y-4 space-x-4'>
{value.map((event) => (
<li key={event} className='list-none'>
<div className='rounded-lg border-2 border-black p-4 dark:border-white'>
<p>{event}</p>
</div>
</li>
))}
</ul>
Comment on lines +86 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Enhance accessibility.

The list elements could benefit from improved accessibility attributes.

Add ARIA attributes and semantic markup:

-<ul ref={itemsRef} className='flex flex-wrap space-y-4 space-x-4'>
+<ul ref={itemsRef} 
+    className='flex flex-wrap space-y-4 space-x-4'
+    role="list"
+    aria-label={`Events for ${key}`}>
     {value.map((event) => (
-        <li key={event} className='list-none'>
+        <li key={event} 
+            className='list-none'
+            role="listitem">
             <div className='rounded-lg border-2 border-black p-4 dark:border-white'>
-                <p>{event}</p>
+                <p role="text">{event}</p>
             </div>
         </li>
     ))}
 </ul>

Committable suggestion skipped: line range outside the PR's diff.

</div>
))}
</div>
</div>
)}
</div>
</>
)
}