-
Notifications
You must be signed in to change notification settings - Fork 339
Feature/add complete stellarmail features #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feature/add complete stellarmail features #81
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds three new React UI components: an authentication multi-view component, a link management dashboard with analytics, and a chat interface with conversations and messaging UI. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive StellarMail features by introducing two major UI components: a link management system with URL shortening and analytics, and a complete authentication flow with login, registration, and password recovery capabilities.
- Implements a link shortening service with real-time analytics tracking (clicks, unique visitors)
- Adds authentication UI with multiple views (login, register, forgot password, reset confirmation)
- Provides interactive data visualization using Recharts for click trends and visitor analytics
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| components/Links.tsx | Adds a complete link management component with URL shortening, custom short codes, filtering/search, analytics dashboard with charts, and CRUD operations for managing shortened links |
| components/Auth.tsx | Implements authentication UI with login, registration, forgot password, and reset confirmation views, including form state management and password visibility toggles |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| shortCode: 'bf2025', | ||
| title: 'Black Friday Campaign', | ||
| createdAt: new Date('2025-11-15'), | ||
| expiresAt: new Date('2025-11-30'), |
Copilot
AI
Dec 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expiresAt date is set to '2025-11-30' which is in the past (before today's date of 2025-12-13), correctly matching the 'expired' status. However, there's no logic in the component to automatically update link status based on expiration dates. Links with expiresAt dates in the past should be automatically marked as expired, but the current implementation relies on manually setting the status field.
| const handleToggleStatus = (id: string) => { | ||
| setLinks( | ||
| links.map((link) => { | ||
| if (link.id === id) { | ||
| const newStatus = | ||
| link.status === 'active' ? 'inactive' : 'active'; | ||
| toast.success( | ||
| `Link ${newStatus === 'active' ? 'activated' : 'deactivated'}` | ||
| ); | ||
| return { ...link, status: newStatus }; | ||
| } | ||
| return link; | ||
| }) | ||
| ); | ||
| }; |
Copilot
AI
Dec 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The toggle status function only switches between 'active' and 'inactive', but doesn't prevent toggling an 'expired' link. While the UI button is disabled for expired links, if this function is called programmatically with an expired link's ID, it would incorrectly change the status. Consider adding a check to prevent changing the status of expired links.
| <button | ||
| type="button" | ||
| onClick={() => setShowPassword(!showPassword)} | ||
| className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white" |
Copilot
AI
Dec 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The password visibility toggle button is missing an aria-label attribute. This would help screen reader users understand the button's purpose. Consider adding aria-label="Toggle password visibility" or similar text.
| className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white" | |
| className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white" | |
| aria-label="Toggle password visibility" |
| if (!newLink.originalUrl || !newLink.title) { | ||
| toast.error('Please fill in all required fields'); | ||
| return; | ||
| } |
Copilot
AI
Dec 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URL validation is missing. The originalUrl field should be validated to ensure it's a valid URL format before creating a link. Currently, any string can be entered which could lead to invalid shortened links being created.
| } | |
| } | |
| // Validate originalUrl is a valid URL | |
| try { | |
| // Throws if invalid | |
| new URL(newLink.originalUrl); | |
| } catch (e) { | |
| toast.error('Please enter a valid URL'); | |
| return; | |
| } |
| const shortCode = | ||
| newLink.customCode || Math.random().toString(36).substring(2, 8); |
Copilot
AI
Dec 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The custom short code should be validated and sanitized. Currently, any string can be used as a custom code without checking for duplicates, invalid characters, or reserved words. This could lead to collisions or security issues if malicious codes are entered.
| const handleSubmit = async (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| setIsLoading(true); | ||
| setTimeout(() => { | ||
| setIsLoading(false); | ||
| if (authView === 'forgot-password') { | ||
| setAuthView('reset-sent'); | ||
| } | ||
| }, 1500); | ||
| }; |
Copilot
AI
Dec 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The form submission handler doesn't perform any validation. For the register view, it should validate that the password meets the stated requirement of "at least 8 characters" and that password and confirmPassword match before proceeding. For the login view, it should validate the email format. The lack of validation could lead to poor user experience and security issues.
| <button | ||
| type="button" | ||
| onClick={() => setShowPassword(!showPassword)} | ||
| className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white" |
Copilot
AI
Dec 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the login form, this password visibility toggle button is missing an aria-label attribute for screen reader accessibility.
| className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white" | |
| className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white" | |
| aria-label={showPassword ? "Hide password" : "Show password"} |
| import { | ||
| Link as LinkIcon, | ||
| Plus, | ||
| Copy, | ||
| ExternalLink, | ||
| BarChart3, | ||
| Search, | ||
| Filter, | ||
| Eye, | ||
| EyeOff, | ||
| Trash2, | ||
| Edit, | ||
| TrendingUp, | ||
| Calendar, | ||
| MousePointerClick, | ||
| } from 'lucide-react'; |
Copilot
AI
Dec 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import Edit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (5)
components/Auth.tsx (1)
20-25: Cleanup timeout on unmount to prevent state updates on unmounted component.If the component unmounts while the timeout is pending,
setIsLoading(false)will attempt to update state on an unmounted component.Consider clearing the timeout on unmount or using an abort pattern:
+import React, { useState, useEffect, useRef } from 'react'; ... +const timeoutRef = useRef<NodeJS.Timeout>(); + +useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; +}, []); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); - setTimeout(() => { + timeoutRef.current = setTimeout(() => { setIsLoading(false); if (authView === 'forgot-password') { setAuthView('reset-sent'); } }, 1500); };components/Links.tsx (4)
63-63: Unused import:Edit.The
Editicon is imported but never used in the component.- Edit,
218-233: Guard against toggling expired links in the handler.The UI disables the toggle button for expired links, but the handler itself doesn't check. For defensive coding, add a guard in case the handler is called programmatically.
const handleToggleStatus = (id: string) => { setLinks( links.map((link) => { if (link.id === id) { + if (link.status === 'expired') return link; const newStatus = link.status === 'active' ? 'inactive' : 'active';
235-239: Consider adding a confirmation dialog before deletion.Deleting a link is destructive and irreversible. A confirmation step would prevent accidental data loss.
183-210: Validate URL format and check for short code uniqueness.
originalUrlisn't validated—users could enter invalid URLs.- Custom short codes aren't checked for uniqueness against existing links.
Math.random()for code generation is fine for demos but consider collision risk in production.const handleCreateLink = () => { if (!newLink.originalUrl || !newLink.title) { toast.error('Please fill in all required fields'); return; } + // Basic URL validation + try { + new URL(newLink.originalUrl); + } catch { + toast.error('Please enter a valid URL'); + return; + } const shortCode = newLink.customCode || Math.random().toString(36).substring(2, 8); + // Check for duplicate short code + if (links.some((l) => l.shortCode === shortCode)) { + toast.error('Short code already exists'); + return; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
components/Auth.tsx(1 hunks)components/Links.tsx(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Agent
🔇 Additional comments (4)
components/Auth.tsx (1)
315-322: Clean conditional rendering pattern.The view switching logic is straightforward and readable.
components/Links.tsx (3)
159-181: Good use ofuseMemofor filtering and stats.The memoization is correctly applied with appropriate dependencies, preventing unnecessary recalculations on every render.
678-686: Good security practice with external link attributes.Using
rel="noopener noreferrer"on external links prevents potential security issues withwindow.opener.
443-450: Index keys are acceptable here but consider tag uniqueness.Using array index as key for tags is fine since they're rendered in a stable context within each link. However, if tags could be duplicated, the index prevents React key warnings.
| const handleSubmit = async (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| setIsLoading(true); | ||
| setTimeout(() => { | ||
| setIsLoading(false); | ||
| if (authView === 'forgot-password') { | ||
| setAuthView('reset-sent'); | ||
| } | ||
| }, 1500); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Authentication handlers are incomplete placeholders.
The handleSubmit function only simulates a delay and handles the forgot-password view transition. Login and register submissions don't perform any actual authentication or validation—form data is collected but never used.
If this is intentional scaffolding, consider adding a TODO comment. Otherwise, integrate actual authentication logic or callbacks.
Would you like me to suggest a pattern for handling auth with callbacks/props or integration with an auth provider?
🤖 Prompt for AI Agents
In components/Auth.tsx around lines 17 to 26, handleSubmit is only a simulated
delay and only transitions the forgot-password view; it doesn't use form data,
perform validation, or call any authentication logic. Replace the placeholder
with real behavior: collect and validate the form values, setIsLoading(true),
then based on authView call the appropriate async handler (e.g. props.onLogin,
props.onRegister, or props.onForgotPassword) or an auth provider API, await the
result, handle success (change view or redirect) and errors (set error state),
and finally setIsLoading(false); if this is intentionally scaffolding instead,
add a clear TODO comment explaining expected handlers and props to be
implemented.
| <div className="flex items-center justify-between text-sm"> | ||
| <label className="flex items-center text-gray-400 cursor-pointer hover:text-white"> | ||
| <input type="checkbox" className="mr-2 rounded border-gray-700 bg-gray-950" /> | ||
| Remember me | ||
| </label> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Remember me" checkbox state is not tracked.
The checkbox is rendered but its value isn't stored in state or used anywhere. Either add state to track it or remove the checkbox if not needed.
🤖 Prompt for AI Agents
In components/Auth.tsx around lines 78 to 82, the "Remember me" checkbox is
rendered but its checked value isn't tracked; add a React state hook (e.g.,
useState<boolean>) to hold the remember flag, bind the checkbox's checked prop
to that state, add an onChange handler to toggle/update the state, and ensure
the form submission or login handler reads that state (or persist it to
localStorage/cookie as intended); alternatively, if the feature is not required,
remove the checkbox markup to avoid dead UI.
| <p className="mt-1 text-xs text-gray-500">Must be at least 8 characters</p> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-400 mb-2">Confirm Password</label> | ||
| <div className="relative"> | ||
| <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | ||
| <Lock size={18} className="text-gray-500" /> | ||
| </div> | ||
| <input | ||
| type={showPassword ? 'text' : 'password'} | ||
| value={formData.confirmPassword} | ||
| onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })} | ||
| className="w-full pl-10 pr-4 py-3 bg-gray-950 border border-gray-800 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 transition-colors" | ||
| placeholder="••••••••" | ||
| required | ||
| /> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Password validation is displayed but not enforced.
The UI shows "Must be at least 8 characters" (line 189), and there's a confirmPassword field, but neither constraint is validated before submission. Users could submit weak passwords or mismatched confirmations.
Add validation in handleSubmit for the register flow:
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
+ if (authView === 'register') {
+ if (formData.password.length < 8) {
+ // Show error: password too short
+ return;
+ }
+ if (formData.password !== formData.confirmPassword) {
+ // Show error: passwords don't match
+ return;
+ }
+ }
setIsLoading(true);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In components/Auth.tsx around lines 189 to 207, the form shows a password rule
and confirmPassword field but there is no enforcement in handleSubmit for the
register flow; update handleSubmit to validate that formData.password is at
least 8 characters and that formData.confirmPassword matches formData.password,
prevent submission if validations fail, and set/display appropriate error
state/messages (e.g., setError or formErrors) so the user sees why submission
was blocked; ensure early return on validation failure and only proceed with the
registration request when checks pass.
| // Handle copy link | ||
| const handleCopyLink = (shortUrl: string) => { | ||
| navigator.clipboard.writeText(shortUrl); | ||
| toast.success('Link copied to clipboard!'); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling for clipboard API.
navigator.clipboard.writeText can throw if clipboard access is denied or unavailable (non-HTTPS contexts, permissions). The promise rejection is unhandled.
-const handleCopyLink = (shortUrl: string) => {
- navigator.clipboard.writeText(shortUrl);
- toast.success('Link copied to clipboard!');
-};
+const handleCopyLink = async (shortUrl: string) => {
+ try {
+ await navigator.clipboard.writeText(shortUrl);
+ toast.success('Link copied to clipboard!');
+ } catch {
+ toast.error('Failed to copy link');
+ }
+};🤖 Prompt for AI Agents
In components/Links.tsx around lines 212 to 216, the call to
navigator.clipboard.writeText(shortUrl) is unhandled and can reject (permission
denied, non-HTTPS, etc.); update handleCopyLink to await the promise or attach
.then/.catch and handle failures: wrap the writeText call in a try/catch (or use
.catch) and on success call toast.success as before, and on error call
toast.error with a helpful message and optionally provide a fallback (e.g.,
select/copy via a hidden textarea) or log the error to console for debugging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (1)
components/Chat.tsx (1)
22-118: Seed data recreated on every render; make it stable and/or stateful.
conversations/messagesare allocated each render, and they can’t be updated (e.g., unread counts, last message) because they’reconstlocals. Consider moving them outside the component asinitial*constants and storing in state.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
components/Chat.tsx(1 hunks)
🔇 Additional comments (2)
components/Chat.tsx (2)
1-20: Types/models look fine; considertypeexports if reused elsewhere.
No correctness issues here; just noting these are file-local, so if other components will share models, exporting them may reduce duplication.
128-134: [Your rewritten review comment text here]
[Exactly ONE classification tag]
| const handleSendMessage = () => { | ||
| if (messageInput.trim()) { | ||
| // Handle sending message | ||
| console.log('Sending message:', messageInput); | ||
| setMessageInput(''); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t console.log user messages; “Send” should update UI state.
Right now “send” only logs and clears input, which is both a privacy concern and makes the UI feel broken. At minimum, append a new message to state (and ideally update the selected conversation’s lastMessage/timestamp).
- const messages: Message[] = [
+ const initialMessages: Message[] = [
{
id: '1',
text: 'Hey! How\'s the project going?',
sender: 'contact',
timestamp: '10:30 AM',
read: true,
},
@@
{
id: '6',
text: 'Hey, how are you doing?',
sender: 'contact',
timestamp: '2m ago',
read: false,
},
];
+
+ const [messages, setMessages] = useState<Message[]>(initialMessages);
@@
const handleSendMessage = () => {
- if (messageInput.trim()) {
- // Handle sending message
- console.log('Sending message:', messageInput);
- setMessageInput('');
- }
+ const text = messageInput.trim();
+ if (!text) return;
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: String(Date.now()),
+ text,
+ sender: 'user',
+ timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
+ read: false,
+ },
+ ]);
+ setMessageInput('');
};Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In components/Chat.tsx around lines 120 to 126, the current handleSendMessage
only console.logs and clears the input; remove the console.log and instead
create a new message object (id, sender/userId, content from messageInput,
timestamp = Date.now()) and append it to the messages state (or the selected
conversation's messages array) so the UI shows the sent message immediately;
also update the selected conversation's lastMessage and timestamp fields to
reflect the new message, call any upstream persistence/send function
asynchronously, and then clear the input.
| {/* Conversations List */} | ||
| <div className="flex-1 overflow-y-auto"> | ||
| {filteredConversations.map((conversation) => ( | ||
| <div | ||
| key={conversation.id} | ||
| onClick={() => setSelectedConversation(conversation.id)} | ||
| className={`p-4 cursor-pointer hover:bg-gray-800 transition-colors ${ | ||
| selectedConversation === conversation.id ? 'bg-gray-800' : '' | ||
| }`} | ||
| > | ||
| <div className="flex items-start gap-3"> | ||
| {/* Avatar with Online Indicator */} | ||
| <div className="relative"> | ||
| <div className="w-12 h-12 rounded-full bg-[#2dd4bf] flex items-center justify-center font-semibold"> | ||
| {conversation.avatar} | ||
| </div> | ||
| {conversation.online && ( | ||
| <div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-[#1a1a1a]"></div> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Conversation Info */} | ||
| <div className="flex-1 min-w-0"> | ||
| <div className="flex items-center justify-between mb-1"> | ||
| <h3 className="font-semibold truncate">{conversation.name}</h3> | ||
| <span className="text-xs text-gray-400">{conversation.timestamp}</span> | ||
| </div> | ||
| <div className="flex items-center justify-between"> | ||
| <p className="text-sm text-gray-400 truncate">{conversation.lastMessage}</p> | ||
| {conversation.unreadCount > 0 && ( | ||
| <span className="ml-2 bg-[#2dd4bf] text-[#1a1a1a] text-xs font-bold px-2 py-0.5 rounded-full"> | ||
| {conversation.unreadCount} | ||
| </span> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conversation rows must be keyboard-accessible (don’t use clickable <div>).
Using <div onClick> blocks keyboard users. Use a <button type="button"> (or add role="button", tabIndex, and key handlers).
- <div
+ <button
key={conversation.id}
onClick={() => setSelectedConversation(conversation.id)}
+ type="button"
className={`p-4 cursor-pointer hover:bg-gray-800 transition-colors ${
selectedConversation === conversation.id ? 'bg-gray-800' : ''
}`}
>
@@
- </div>
+ </button>📝 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.
| {/* Conversations List */} | |
| <div className="flex-1 overflow-y-auto"> | |
| {filteredConversations.map((conversation) => ( | |
| <div | |
| key={conversation.id} | |
| onClick={() => setSelectedConversation(conversation.id)} | |
| className={`p-4 cursor-pointer hover:bg-gray-800 transition-colors ${ | |
| selectedConversation === conversation.id ? 'bg-gray-800' : '' | |
| }`} | |
| > | |
| <div className="flex items-start gap-3"> | |
| {/* Avatar with Online Indicator */} | |
| <div className="relative"> | |
| <div className="w-12 h-12 rounded-full bg-[#2dd4bf] flex items-center justify-center font-semibold"> | |
| {conversation.avatar} | |
| </div> | |
| {conversation.online && ( | |
| <div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-[#1a1a1a]"></div> | |
| )} | |
| </div> | |
| {/* Conversation Info */} | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center justify-between mb-1"> | |
| <h3 className="font-semibold truncate">{conversation.name}</h3> | |
| <span className="text-xs text-gray-400">{conversation.timestamp}</span> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <p className="text-sm text-gray-400 truncate">{conversation.lastMessage}</p> | |
| {conversation.unreadCount > 0 && ( | |
| <span className="ml-2 bg-[#2dd4bf] text-[#1a1a1a] text-xs font-bold px-2 py-0.5 rounded-full"> | |
| {conversation.unreadCount} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Conversations List */} | |
| <div className="flex-1 overflow-y-auto"> | |
| {filteredConversations.map((conversation) => ( | |
| <button | |
| key={conversation.id} | |
| onClick={() => setSelectedConversation(conversation.id)} | |
| type="button" | |
| className={`p-4 cursor-pointer hover:bg-gray-800 transition-colors ${ | |
| selectedConversation === conversation.id ? 'bg-gray-800' : '' | |
| }`} | |
| > | |
| <div className="flex items-start gap-3"> | |
| {/* Avatar with Online Indicator */} | |
| <div className="relative"> | |
| <div className="w-12 h-12 rounded-full bg-[#2dd4bf] flex items-center justify-center font-semibold"> | |
| {conversation.avatar} | |
| </div> | |
| {conversation.online && ( | |
| <div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-[#1a1a1a]"></div> | |
| )} | |
| </div> | |
| {/* Conversation Info */} | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center justify-between mb-1"> | |
| <h3 className="font-semibold truncate">{conversation.name}</h3> | |
| <span className="text-xs text-gray-400">{conversation.timestamp}</span> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <p className="text-sm text-gray-400 truncate">{conversation.lastMessage}</p> | |
| {conversation.unreadCount > 0 && ( | |
| <span className="ml-2 bg-[#2dd4bf] text-[#1a1a1a] text-xs font-bold px-2 py-0.5 rounded-full"> | |
| {conversation.unreadCount} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </button> | |
| ))} | |
| </div> |
🤖 Prompt for AI Agents
In components/Chat.tsx around lines 162 to 201, the clickable conversation row
is a <div onClick> which is not keyboard-accessible; replace that outer
clickable div with a semantic <button type="button"> (or if you must keep a
non-button element, add role="button", tabIndex={0}, and onKeyDown handlers for
Enter/Space) so keyboard users can activate rows. Move the key and onClick to
the button, preserve all className styling (use utility classes to reset default
button styles if needed), and ensure focus styles are visible and any aria
attributes (e.g., aria-pressed or aria-current) are added as appropriate.
| {/* Action Buttons */} | ||
| <div className="flex items-center gap-2"> | ||
| <button className="p-2 hover:bg-gray-800 rounded-lg transition-colors"> | ||
| <Phone size={20} className="text-[#2dd4bf]" /> | ||
| </button> | ||
| <button className="p-2 hover:bg-gray-800 rounded-lg transition-colors"> | ||
| <Video size={20} className="text-[#2dd4bf]" /> | ||
| </button> | ||
| <button className="p-2 hover:bg-gray-800 rounded-lg transition-colors"> | ||
| <MoreVertical size={20} className="text-gray-400" /> | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Icon-only buttons need accessible names; add type="button" consistently.
These buttons have no text, so they should have aria-label (and type="button" to avoid accidental form-submit if this is ever wrapped in a <form>).
- <button className="p-2 hover:bg-gray-800 rounded-lg transition-colors">
+ <button type="button" aria-label="Call" className="p-2 hover:bg-gray-800 rounded-lg transition-colors">
<Phone size={20} className="text-[#2dd4bf]" />
</button>
- <button className="p-2 hover:bg-gray-800 rounded-lg transition-colors">
+ <button type="button" aria-label="Video call" className="p-2 hover:bg-gray-800 rounded-lg transition-colors">
<Video size={20} className="text-[#2dd4bf]" />
</button>
- <button className="p-2 hover:bg-gray-800 rounded-lg transition-colors">
+ <button type="button" aria-label="More actions" className="p-2 hover:bg-gray-800 rounded-lg transition-colors">
<MoreVertical size={20} className="text-gray-400" />
</button>
@@
- <button className="p-2 hover:bg-gray-800 rounded-lg transition-colors mb-1">
+ <button type="button" aria-label="Attach file" className="p-2 hover:bg-gray-800 rounded-lg transition-colors mb-1">
<Paperclip size={20} className="text-gray-400" />
</button>
@@
- <button className="p-2 hover:bg-gray-700 rounded-lg transition-colors mr-1 mb-1">
+ <button type="button" aria-label="Insert emoji" className="p-2 hover:bg-gray-700 rounded-lg transition-colors mr-1 mb-1">
<Smile size={20} className="text-gray-400" />
</button>
@@
<button
+ type="button"
+ aria-label="Send message"
onClick={handleSendMessage}
className="p-3 bg-[#2dd4bf] hover:bg-[#2dd4bf]/90 rounded-lg transition-colors mb-1"
>Also applies to: 282-305
🤖 Prompt for AI Agents
In components/Chat.tsx around lines 227-238 (and similarly at 282-305), the
icon-only buttons lack accessible names and don't specify button type; update
each <button> to include type="button" and a descriptive aria-label (e.g.,
aria-label="Start voice call", aria-label="Start video call", aria-label="More
options") so screen readers can identify them, and optionally add title
attributes for tooltips; apply the same changes to the other block at lines
282-305.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.