Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4f268be
feat: implement UI enhancements and display issue numbers
claude Nov 18, 2025
35bd289
feat: add DetailPanel, UserHoverCard, and Open Source Score
claude Nov 18, 2025
b1101ed
Merge remote-tracking branch 'origin/main' into claude/lint-fixes-ui-…
ArjinAlbay Nov 18, 2025
6831589
Merge pull request #24 from ArjinAlbay/claude/lint-fixes-ui-enhanceme…
ArjinAlbay Nov 18, 2025
5499adc
fix: resolve code quality issues in UI components and API client
claude Nov 18, 2025
52d8cbd
Merge pull request #25 from ArjinAlbay/claude/lint-fixes-ui-enhanceme…
ArjinAlbay Nov 18, 2025
b44f478
fix: resolve all ESLint errors across components
claude Nov 18, 2025
8a171d4
fix: add wrapper function for updateColumn to match expected signature
claude Nov 18, 2025
352cf22
fix: use setUserToken instead of setToken in KanbanBoard
claude Nov 18, 2025
9744e5f
fix: use orgName instead of organizationName in Sidebar
claude Nov 18, 2025
0766e35
feat: add missing hover-card UI component
claude Nov 18, 2025
a47ade8
chore: install @radix-ui/react-hover-card dependency
claude Nov 18, 2025
fbedf53
fix: use GitHubUserDetailed type instead of custom UserProfile
claude Nov 18, 2025
dc878a1
feat: add getHeaders method to GitHubAPIClient
claude Nov 19, 2025
363ce4b
Merge branch 'main' into claude/fix-eslint-errors-01UJHXN6P2vKAAAZmx2…
ArjinAlbay Nov 19, 2025
4f22ece
Merge pull request #26 from ArjinAlbay/claude/fix-eslint-errors-01UJH…
ArjinAlbay Nov 19, 2025
49e5186
refactor: remove unused imports and commented-out code in Layout, Pag…
ArjinAlbay Nov 19, 2025
c5f19c3
refactor: update Sidebar links to replace Settings with Favorites
ArjinAlbay Nov 19, 2025
986f3ed
feat: implement user hover card with open source scoring system
claude Nov 19, 2025
e9fb8eb
Merge pull request #27 from ArjinAlbay/claude/user-hover-card-scoring…
ArjinAlbay Nov 19, 2025
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
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
Expand Down
12 changes: 11 additions & 1 deletion src/app/action-required/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import {
ExternalLink,
RefreshCw,
LucideIcon,
GitMerge,
AlertCircle,
CheckCircle2,
XCircle,
Expand Down Expand Up @@ -74,6 +73,12 @@ interface ActionItem {
const VALID_TABS = ["assigned", "mentions", "stale"] as const;
type ValidTab = (typeof VALID_TABS)[number];

function extractIssueNumber(url?: string): string | null {
if (!url) return null;
const match = url.match(/\/(issues|pull)\/(\d+)/);
return match ? match[2] : null;
}

function formatTimeAgo(dateString: string): string {
const now = new Date();
const past = new Date(dateString);
Expand Down Expand Up @@ -436,6 +441,11 @@ function ActionRequiredContent() {
<div className="flex items-center gap-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
{extractIssueNumber(item.url) && (
<span className="text-sm text-gray-500 dark:text-gray-400 font-mono flex-shrink-0">
#{extractIssueNumber(item.url)}
</span>
)}
<a
href={isValidUrl(item.url) ? item.url : "#"}
target="_blank"
Expand Down
2 changes: 1 addition & 1 deletion src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Suspense } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useRequireGuest } from "@/hooks/useAuth";

Expand Down
3 changes: 0 additions & 3 deletions src/components/command/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,13 @@ import {
import { useNavigationStore, useSearchStore, usePreferencesStore, useAuthStore } from "@/stores"
import { menuItems } from "@/config/menu"
import {
Home,
Search,
Settings,
Moon,
Sun,
LogOut,
Monitor,
Clock,
Star,
Users,
GitBranch,
} from "lucide-react"
import { signOut } from "next-auth/react"
Expand Down
241 changes: 241 additions & 0 deletions src/components/common/DetailPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
"use client";

import { X, ExternalLink, Calendar, MessageCircle, Tag, User as UserIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import { useEffect, useState } from "react";

export interface IssueDetail {
number: number;
title: string;
body: string | null;
url: string;
repository: string;
repositoryUrl: string;
state: "open" | "closed";
author: {
login: string;
avatar_url: string;
};
labels: Array<{
name: string;
color: string;
}>;
created_at: string;
updated_at: string;
comments: number;
assignees?: Array<{
login: string;
avatar_url: string;
}>;
}

interface DetailPanelProps {
isOpen: boolean;
onClose: () => void;
issue: IssueDetail | null;
comments?: Array<{
id: number;
user: {
login: string;
avatar_url: string;
};
body: string;
created_at: string;
}>;
loading?: boolean;
}

function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}

export function DetailPanel({ isOpen, onClose, issue, comments, loading }: DetailPanelProps) {
const [isVisible, setIsVisible] = useState(false);

useEffect(() => {
if (isOpen) {
setTimeout(() => setIsVisible(true), 10);
} else {
setIsVisible(false);
}
}, [isOpen]);

if (!isOpen && !isVisible) return null;

return (
<>
<div
className={`fixed inset-0 bg-black/50 z-40 transition-opacity duration-300 ${
isVisible ? "opacity-100" : "opacity-0"
}`}
onClick={onClose}
/>

<div
className={`fixed right-0 top-0 h-full w-full md:w-[600px] bg-background border-l border-border z-50 shadow-2xl transform transition-transform duration-300 ${
isVisible ? "translate-x-0" : "translate-x-full"
}`}
>
<div className="h-full flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-border">
<h2 className="text-lg font-semibold">Issue Details</h2>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="w-5 h-5" />
</Button>
</div>

{loading && (
<div className="flex-1 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
)}

{!loading && issue && (
<div className="flex-1 overflow-y-auto p-6 space-y-6">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
<span className="font-mono">#{issue.number}</span>
<Badge variant={issue.state === "open" ? "default" : "secondary"}>
{issue.state}
</Badge>
</div>
<h3 className="text-xl font-bold mb-2">{issue.title}</h3>
<a
href={issue.repositoryUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground flex items-center gap-1"
>
{issue.repository}
<ExternalLink className="w-3 h-3" />
</a>
</div>
</div>

<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Avatar className="w-6 h-6">
<AvatarImage src={issue.author.avatar_url} alt={issue.author.login} />
<AvatarFallback>{issue.author.login.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<span>{issue.author.login}</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
<span>Opened {formatDate(issue.created_at)}</span>
</div>
<div className="flex items-center gap-1">
<MessageCircle className="w-4 h-4" />
<span>{issue.comments} comments</span>
</div>
</div>

{issue.labels && issue.labels.length > 0 && (
<div className="flex items-start gap-2">
<Tag className="w-4 h-4 mt-1 text-muted-foreground" />
<div className="flex flex-wrap gap-2">
{issue.labels.map((label, idx) => (
<Badge
key={idx}
variant="outline"
style={{
borderColor: `#${label.color}`,
backgroundColor: `#${label.color}20`,
color: `#${label.color}`,
}}
>
{label.name}
</Badge>
))}
Comment on lines +146 to +158
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use unique identifier as key instead of array index.

Using the array index idx as the key can cause React rendering issues if the label order changes. If labels have unique identifiers, use those; otherwise, use label.name as the key (assuming names are unique within an issue).

-                    {issue.labels.map((label, idx) => (
+                    {issue.labels.map((label) => (
                       <Badge
-                        key={idx}
+                        key={label.name}
                         variant="outline"
📝 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
{issue.labels.map((label, idx) => (
<Badge
key={idx}
variant="outline"
style={{
borderColor: `#${label.color}`,
backgroundColor: `#${label.color}20`,
color: `#${label.color}`,
}}
>
{label.name}
</Badge>
))}
{issue.labels.map((label) => (
<Badge
key={label.name}
variant="outline"
style={{
borderColor: `#${label.color}`,
backgroundColor: `#${label.color}20`,
color: `#${label.color}`,
}}
>
{label.name}
</Badge>
))}
🤖 Prompt for AI Agents
In src/components/common/DetailPanel.tsx around lines 146 to 158, the Badge list
uses the array index (idx) as the React key which can cause rendering issues;
change the key to a stable unique identifier instead (e.g., use label.id if
present, otherwise fallback to label.name) so use key={label.id ?? label.name}
or similar to ensure stable keys across reorders.

⚠️ Potential issue | 🟠 Major

Critical: Potential XSS vulnerability with unsanitized color values.

The label.color value is directly injected into inline styles without validation. If a malicious actor can control label colors (e.g., through API manipulation), they could inject CSS that executes JavaScript via url('javascript:...') or other CSS-based attacks.

Apply this diff to sanitize the color value:

+function sanitizeHexColor(color: string): string {
+  // Ensure color is a valid 3 or 6 character hex code
+  const hexMatch = color.match(/^[0-9A-Fa-f]{6}$|^[0-9A-Fa-f]{3}$/);
+  return hexMatch ? color : '808080'; // fallback to gray if invalid
+}
+
 export function DetailPanel({ isOpen, onClose, issue, comments, loading }: DetailPanelProps) {
   // ... rest of component
   
                     {issue.labels.map((label, idx) => (
+                      const safeColor = sanitizeHexColor(label.color);
                       <Badge
                         key={idx}
                         variant="outline"
                         style={{
-                          borderColor: `#${label.color}`,
+                          borderColor: `#${safeColor}`,
-                          backgroundColor: `#${label.color}20`,
+                          backgroundColor: `#${safeColor}20`,
-                          color: `#${label.color}`,
+                          color: `#${safeColor}`,
                         }}
                       >
                         {label.name}
                       </Badge>
                     ))}

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

🤖 Prompt for AI Agents
In src/components/common/DetailPanel.tsx around lines 146 to 158, label.color is
injected directly into inline styles which can lead to CSS/XSS injection;
validate and sanitize the color before use by allowing only hex colors (3 or 6
hex digits) via a regex, normalize to lowercase and prepend '#' if missing, fall
back to a safe default color when validation fails, and construct derived values
(e.g. backgroundColor with appended alpha) from the sanitized hex only; replace
direct usage of label.color in the JSX with the sanitized value computed
beforehand.

</div>
</div>
)}

{issue.assignees && issue.assignees.length > 0 && (
<div className="flex items-start gap-2">
<UserIcon className="w-4 h-4 mt-1 text-muted-foreground" />
<div className="flex flex-wrap gap-2">
{issue.assignees.map((assignee, idx) => (
<div key={idx} className="flex items-center gap-2">
<Avatar className="w-6 h-6">
<AvatarImage src={assignee.avatar_url} alt={assignee.login} />
<AvatarFallback>{assignee.login.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<span className="text-sm">{assignee.login}</span>
</div>
))}
Comment on lines +167 to +175
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use unique identifier as key instead of array index.

Similar to the labels issue, using the array index idx as the key for assignees can cause rendering issues. Use a unique identifier like assignee.login (assuming it's unique) as the key instead.

-                    {issue.assignees.map((assignee, idx) => (
-                      <div key={idx} className="flex items-center gap-2">
+                    {issue.assignees.map((assignee) => (
+                      <div key={assignee.login} className="flex items-center gap-2">
                         <Avatar className="w-6 h-6">
                           <AvatarImage src={assignee.avatar_url} alt={assignee.login} />
                           <AvatarFallback>{assignee.login.charAt(0).toUpperCase()}</AvatarFallback>
                         </Avatar>
                         <span className="text-sm">{assignee.login}</span>
                       </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
{issue.assignees.map((assignee, idx) => (
<div key={idx} className="flex items-center gap-2">
<Avatar className="w-6 h-6">
<AvatarImage src={assignee.avatar_url} alt={assignee.login} />
<AvatarFallback>{assignee.login.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<span className="text-sm">{assignee.login}</span>
</div>
))}
{issue.assignees.map((assignee) => (
<div key={assignee.login} className="flex items-center gap-2">
<Avatar className="w-6 h-6">
<AvatarImage src={assignee.avatar_url} alt={assignee.login} />
<AvatarFallback>{assignee.login.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<span className="text-sm">{assignee.login}</span>
</div>
))}
🤖 Prompt for AI Agents
In src/components/common/DetailPanel.tsx around lines 167 to 175, the assignees
list is using the array index (idx) as the React key which can cause rendering
bugs; replace the index with a stable unique identifier such as assignee.login
(or another unique id on the assignee object) by using that property as the key
for each mapped element so React can correctly track list items during
re-renders.

</div>
</div>
)}

<div>
<h4 className="font-semibold mb-3">Description</h4>
<Card>
<CardContent className="p-4">
{issue.body ? (
<div className="text-sm whitespace-pre-wrap break-words">
{issue.body}
</div>
) : (
<p className="text-muted-foreground italic">No description provided.</p>
)}
</CardContent>
</Card>
</div>

{comments && comments.length > 0 && (
<div>
<h4 className="font-semibold mb-3">Recent Comments ({comments.length})</h4>
<div className="space-y-3">
{comments.slice(0, 5).map((comment) => (
<Card key={comment.id}>
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-2">
<Avatar className="w-6 h-6">
<AvatarImage src={comment.user.avatar_url} alt={comment.user.login} />
<AvatarFallback>{comment.user.login.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<span className="text-sm font-medium">{comment.user.login}</span>
<span className="text-xs text-muted-foreground">
{formatDate(comment.created_at)}
</span>
</div>
<div className="text-sm whitespace-pre-wrap break-words">
{comment.body}
</div>
</CardContent>
</Card>
))}
{comments.length > 5 && (
<p className="text-sm text-muted-foreground text-center">
And {comments.length - 5} more comments...
</p>
)}
</div>
</div>
)}

<div className="flex gap-2 pt-4">
<Button asChild className="flex-1">
<a href={issue.url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="w-4 h-4 mr-2" />
View on GitHub
</a>
</Button>
</div>
</div>
)}
</div>
</div>
</>
);
}
8 changes: 6 additions & 2 deletions src/components/kanban/ColumnManagementModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export function ColumnManagementModal({
isOpen,
onClose,
}: ColumnManagementModalProps) {
const { columns, columnOrder, updateColumn, addColumn, deleteColumn, reorderColumns, tasks } =
const { columns, columnOrder, updateColumn, addColumn, deleteColumn, reorderColumns } =
useKanbanStore();
const [showAddColumn, setShowAddColumn] = useState(false);
const [newColumnTitle, setNewColumnTitle] = useState("");
Expand Down Expand Up @@ -206,6 +206,10 @@ export function ColumnManagementModal({
}
};

const handleUpdateColumn = (id: string, title: string, color: string) => {
updateColumn(id, { title, color });
};

const handleDeleteColumn = (columnId: string) => {
const column = columns[columnId];
const taskCount = column?.taskIds.length || 0;
Expand Down Expand Up @@ -262,7 +266,7 @@ export function ColumnManagementModal({
title={column.title}
color={column.color}
taskCount={column.taskIds.length}
onUpdate={updateColumn}
onUpdate={handleUpdateColumn}
onDelete={handleDeleteColumn}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/kanban/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ export function KanbanBoard() {
const repo = urlParts[urlParts.length - 3];
const number = parseInt(urlParts[urlParts.length - 1], 10);

githubAPIClient.setToken(orgData.token);
githubAPIClient.setUserToken(orgData.token);

const result =
task.type === "github-issue"
Expand Down
Loading