remove unwwatned components

This commit is contained in:
Steven Enamakel
2026-03-20 17:21:47 -07:00
parent 6561a6b08e
commit d2ffdc8886
4 changed files with 0 additions and 905 deletions
-99
View File
@@ -1,99 +0,0 @@
'use client';
import type { Feedback } from '../feedback/page';
interface FeedbackCardProps {
feedback: Feedback;
token: string | null;
onUpdate: () => void;
onClick: () => void;
}
export default function FeedbackCard({ feedback, token, onUpdate, onClick }: FeedbackCardProps) {
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const handleVote = async (voteType: 'upvote' | 'downvote') => {
if (!token) return;
try {
const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/vote`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ voteType }),
});
const data = await response.json();
if (data.success) {
onUpdate();
}
} catch (error) {
console.error('Failed to vote:', error);
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'feature':
return 'FEATURE REQUEST';
case 'bug':
return 'BUG';
case 'improvement':
return 'IMPROVEMENT';
default:
return type.toUpperCase();
}
};
return (
<div
className="cursor-pointer rounded-lg border border-zinc-800 bg-zinc-900/50 p-4 transition-colors hover:bg-zinc-900"
onClick={onClick}
>
<div className="mb-2 flex items-start justify-between">
<h3 className="font-semibold text-white">{feedback.title}</h3>
<span className="ml-2 rounded bg-zinc-800 px-2 py-1 text-xs text-zinc-400">
{getTypeLabel(feedback.type)}
</span>
</div>
{feedback.description && (
<p className="mb-3 line-clamp-2 text-sm text-zinc-400">{feedback.description}</p>
)}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<button
onClick={(e) => {
e.stopPropagation();
handleVote('upvote');
}}
className={`rounded px-2 py-1 text-sm transition-colors ${feedback.userVote === 'upvote'
? 'bg-blue-500 text-white'
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700'
}`}
disabled={!token}
>
{feedback.upvotes}
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleVote('downvote');
}}
className={`rounded px-2 py-1 text-sm transition-colors ${feedback.userVote === 'downvote'
? 'bg-red-500 text-white'
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700'
}`}
disabled={!token}
>
{feedback.downvotes}
</button>
</div>
<span className="text-xs text-zinc-500">
{feedback.comments.length} {feedback.comments.length === 1 ? 'comment' : 'comments'}
</span>
</div>
</div>
);
}
@@ -1,331 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import type { Feedback } from '../feedback/page';
interface FeedbackDetailModalProps {
feedback: Feedback;
token: string | null;
onClose: () => void;
onUpdate: () => void;
useMockData?: boolean;
mockComments?: Record<string, Array<{
_id: string;
content: string;
user: {
telegramId: number;
telegramFirstName?: string;
telegramLastName?: string;
telegramUsername?: string;
};
createdAt: string;
}>>;
feedbacks?: Feedback[];
setFeedbacks?: (feedbacks: Feedback[]) => void;
}
export default function FeedbackDetailModal({
feedback,
token,
onClose,
onUpdate,
useMockData = false,
mockComments,
feedbacks = [],
setFeedbacks,
}: FeedbackDetailModalProps) {
const [comments, setComments] = useState<Array<{
_id: string;
content: string;
user: {
telegramId: number;
telegramFirstName?: string;
telegramLastName?: string;
telegramUsername?: string;
};
createdAt: string;
}>>([]);
const [newComment, setNewComment] = useState('');
const [loading, setLoading] = useState(true);
const [currentFeedback, setCurrentFeedback] = useState(feedback);
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const loadComments = async () => {
try {
if (useMockData && mockComments) {
// Mock comments
await new Promise(resolve => setTimeout(resolve, 200));
const feedbackComments = mockComments[feedback._id] || [];
setComments(feedbackComments);
} else {
const headers: HeadersInit = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}`, { headers });
const data = await response.json();
if (data.success) {
setComments(data.data.comments || []);
setCurrentFeedback(data.data);
}
}
} catch (error) {
console.error('Failed to load comments:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
setCurrentFeedback(feedback);
loadComments();
}, [feedback._id, token]);
const handleAddComment = async () => {
if (!token || !newComment.trim()) return;
try {
if (useMockData && mockComments && setFeedbacks) {
// Mock add comment
await new Promise(resolve => setTimeout(resolve, 200));
const newCommentObj = {
_id: String(Date.now()),
content: newComment,
user: {
telegramId: 123456789,
telegramFirstName: 'John',
telegramLastName: 'Doe',
telegramUsername: 'johndoe',
},
createdAt: new Date().toISOString(),
};
if (!mockComments[feedback._id]) {
mockComments[feedback._id] = [];
}
mockComments[feedback._id].push(newCommentObj);
// Update feedback comments count
const updatedFeedbacks = feedbacks.map(f => {
if (f._id === feedback._id) {
return { ...f, comments: [...f.comments, newCommentObj._id] };
}
return f;
});
// Update global mock state
const { setMockFeedbacks } = require('../feedback/page');
setMockFeedbacks(updatedFeedbacks);
setFeedbacks(updatedFeedbacks);
setNewComment('');
await loadComments();
onUpdate();
} else {
const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ content: newComment }),
});
const data = await response.json();
if (data.success) {
setNewComment('');
await loadComments();
onUpdate();
}
}
} catch (error) {
console.error('Failed to add comment:', error);
}
};
const handleVote = async (voteType: 'upvote' | 'downvote') => {
if (!token) return;
try {
if (useMockData && setFeedbacks) {
// Mock vote
await new Promise(resolve => setTimeout(resolve, 200));
const updatedFeedbacks = feedbacks.map(f => {
if (f._id === feedback._id) {
const updated = { ...f };
// Toggle vote logic
if (updated.userVote === voteType) {
// Remove vote
updated.userVote = null;
if (voteType === 'upvote') {
updated.upvotes = Math.max(0, updated.upvotes - 1);
} else {
updated.downvotes = Math.max(0, updated.downvotes - 1);
}
} else if (updated.userVote) {
// Change vote
const oldVote = updated.userVote;
if (oldVote === 'upvote') {
updated.upvotes = Math.max(0, updated.upvotes - 1);
} else {
updated.downvotes = Math.max(0, updated.downvotes - 1);
}
updated.userVote = voteType;
if (voteType === 'upvote') {
updated.upvotes += 1;
} else {
updated.downvotes += 1;
}
} else {
// New vote
updated.userVote = voteType;
if (voteType === 'upvote') {
updated.upvotes += 1;
} else {
updated.downvotes += 1;
}
}
setCurrentFeedback(updated);
return updated;
}
return f;
});
// Update global mock state
const { setMockFeedbacks } = require('../feedback/page');
setMockFeedbacks(updatedFeedbacks);
setFeedbacks(updatedFeedbacks);
onUpdate();
} else {
const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/vote`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ voteType }),
});
const data = await response.json();
if (data.success) {
setCurrentFeedback({
...currentFeedback,
upvotes: data.data.upvotes,
downvotes: data.data.downvotes,
userVote: data.data.userVote,
});
onUpdate();
}
}
} catch (error) {
console.error('Failed to vote:', error);
}
};
return (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/50 backdrop-blur-sm p-0 sm:p-4">
<div className="w-full sm:max-w-3xl rounded-t-2xl sm:rounded-lg border-t sm:border border-zinc-800 bg-zinc-900 max-h-[95vh] sm:max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 border-b border-zinc-800 bg-zinc-900 p-4 sm:p-6">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<h2 className="text-lg sm:text-2xl font-semibold break-words">{currentFeedback.title}</h2>
{currentFeedback.description && (
<p className="mt-2 text-sm sm:text-base text-zinc-400 break-words">{currentFeedback.description}</p>
)}
<div className="mt-4 flex flex-wrap items-center gap-2 sm:gap-4">
<button
onClick={() => handleVote('upvote')}
className={`flex items-center gap-1.5 sm:gap-2 rounded-lg px-2.5 sm:px-3 py-1.5 text-xs sm:text-sm transition-colors touch-manipulation ${currentFeedback.userVote === 'upvote'
? 'bg-blue-500/20 text-blue-400'
: 'bg-zinc-800 text-zinc-400 active:bg-zinc-700 sm:hover:bg-zinc-700'
}`}
disabled={!token}
>
<svg className="h-3.5 w-3.5 sm:h-4 sm:w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
</svg>
{currentFeedback.upvotes}
</button>
<button
onClick={() => handleVote('downvote')}
className={`flex items-center gap-1.5 sm:gap-2 rounded-lg px-2.5 sm:px-3 py-1.5 text-xs sm:text-sm transition-colors touch-manipulation ${currentFeedback.userVote === 'downvote'
? 'bg-red-500/20 text-red-400'
: 'bg-zinc-800 text-zinc-400 active:bg-zinc-700 sm:hover:bg-zinc-700'
}`}
disabled={!token}
>
<svg className="h-3.5 w-3.5 sm:h-4 sm:w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
{currentFeedback.downvotes}
</button>
<span className="text-xs sm:text-sm text-zinc-500">
{comments.length} {comments.length === 1 ? 'comment' : 'comments'}
</span>
</div>
</div>
<button
onClick={onClose}
className="flex-shrink-0 p-2 text-zinc-400 active:text-white sm:hover:text-white touch-manipulation"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<div className="p-4 sm:p-6">
<div className="mb-4 sm:mb-6">
<h3 className="mb-3 sm:mb-4 text-sm sm:text-base font-semibold">Comments</h3>
{loading ? (
<div className="text-sm text-zinc-400">Loading comments...</div>
) : comments.length > 0 ? (
<div className="space-y-3 sm:space-y-4">
{comments.map((comment) => (
<div key={comment._id} className="rounded-lg border border-zinc-800 bg-zinc-950 p-3 sm:p-4">
<div className="mb-2 flex flex-wrap items-center gap-2">
<span className="text-xs sm:text-sm font-semibold break-words">
{comment.user.telegramFirstName || ''} {comment.user.telegramLastName || ''}
{comment.user.telegramUsername && ` (@${comment.user.telegramUsername})`}
</span>
<span className="text-xs text-zinc-500">
{new Date(comment.createdAt).toLocaleDateString()}
</span>
</div>
<p className="text-xs sm:text-sm text-zinc-300 break-words">{comment.content}</p>
</div>
))}
</div>
) : (
<div className="text-sm text-zinc-400">No comments yet.</div>
)}
</div>
{token && (
<div className="border-t border-zinc-800 pt-4">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
className="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-4 py-2.5 text-sm sm:text-base text-white focus:border-zinc-700 focus:outline-none"
rows={3}
placeholder="Add a comment..."
/>
<button
onClick={handleAddComment}
className="mt-2 w-full sm:w-auto rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-zinc-950 transition-colors active:bg-zinc-200 sm:hover:bg-zinc-200 touch-manipulation"
>
Post Comment
</button>
</div>
)}
</div>
</div>
</div>
);
}
-257
View File
@@ -1,257 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import FeedbackCard from './FeedbackCard';
import type { Feedback } from '../feedback/page';
interface FeedbackKanbanProps {
feedbacks: Feedback[];
token: string | null;
onUpdate: () => void;
}
export default function FeedbackKanban({ feedbacks, token, onUpdate }: FeedbackKanbanProps) {
const [selectedFeedback, setSelectedFeedback] = useState<Feedback | null>(null);
const planned = feedbacks.filter((f) => f.status === 'planned');
const inProgress = feedbacks.filter((f) => f.status === 'in_progress');
const complete = feedbacks.filter((f) => f.status === 'complete');
const columns = [
{
id: 'planned',
title: 'Planned',
items: planned,
color: 'blue',
},
{
id: 'in_progress',
title: 'In Progress',
items: inProgress,
color: 'purple',
},
{
id: 'complete',
title: 'Complete',
items: complete,
color: 'green',
},
];
return (
<>
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold">Boards</h2>
<div className="flex gap-2">
<button className="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-2 text-sm text-zinc-400 transition-colors hover:bg-zinc-800">
Feature Requests <span className="ml-2 text-zinc-500">({feedbacks.filter((f) => f.type === 'feature').length})</span>
</button>
<button className="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-2 text-sm text-zinc-400 transition-colors hover:bg-zinc-800">
Bugs <span className="ml-2 text-zinc-500">({feedbacks.filter((f) => f.type === 'bug').length})</span>
</button>
</div>
</div>
<button className="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-2 text-sm text-zinc-400 transition-colors hover:bg-zinc-800">
Filters
</button>
</div>
<div className="mb-8">
<h2 className="mb-4 text-lg font-semibold">Roadmap</h2>
<div className="grid gap-6 md:grid-cols-3">
{columns.map((column) => (
<div key={column.id} className="flex flex-col">
<div className="mb-4 flex items-center gap-2">
<div
className={`h-2 w-2 rounded-full ${column.color === 'blue'
? 'bg-blue-500'
: column.color === 'purple'
? 'bg-purple-500'
: 'bg-green-500'
}`}
/>
<h3 className="font-semibold">{column.title}</h3>
</div>
<div className="space-y-4">
{column.items.length > 0 ? (
column.items.map((feedback) => (
<FeedbackCard
key={feedback._id}
feedback={feedback}
token={token}
onUpdate={onUpdate}
onClick={() => setSelectedFeedback(feedback)}
/>
))
) : (
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-8 text-center">
<div className="mb-2 text-4xl">💡</div>
<p className="text-sm text-zinc-400">
Share your feedback and check back here for updates.
</p>
</div>
)}
</div>
</div>
))}
</div>
</div>
{selectedFeedback && (
<FeedbackDetailModal
feedback={selectedFeedback}
token={token}
onClose={() => setSelectedFeedback(null)}
onUpdate={() => {
onUpdate();
setSelectedFeedback(null);
}}
/>
)}
</>
);
}
function FeedbackDetailModal({
feedback,
token,
onClose,
onUpdate,
}: {
feedback: Feedback;
token: string | null;
onClose: () => void;
onUpdate: () => void;
}) {
const [comments, setComments] = useState<Array<{
_id: string;
content: string;
user: {
telegramId: number;
telegramFirstName?: string;
telegramLastName?: string;
telegramUsername?: string;
};
createdAt: string;
}>>([]);
const [newComment, setNewComment] = useState('');
const [loading, setLoading] = useState(true);
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const loadComments = async () => {
try {
const headers: HeadersInit = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}`, { headers });
const data = await response.json();
if (data.success) {
setComments(data.data.comments || []);
}
} catch (error) {
console.error('Failed to load comments:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadComments();
}, [feedback._id, token]);
const handleAddComment = async () => {
if (!token || !newComment.trim()) return;
try {
const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ content: newComment }),
});
const data = await response.json();
if (data.success) {
setNewComment('');
await loadComments();
}
} catch (error) {
console.error('Failed to add comment:', error);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="w-full max-w-2xl rounded-lg border border-zinc-800 bg-zinc-900 max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 border-b border-zinc-800 bg-zinc-900 p-6">
<div className="flex items-start justify-between">
<div className="flex-1">
<h2 className="text-2xl font-semibold">{feedback.title}</h2>
{feedback.description && (
<p className="mt-2 text-zinc-400">{feedback.description}</p>
)}
</div>
<button
onClick={onClose}
className="ml-4 text-zinc-400 hover:text-white"
>
</button>
</div>
</div>
<div className="p-6">
<div className="mb-6">
<h3 className="mb-4 font-semibold">Comments</h3>
{loading ? (
<div className="text-zinc-400">Loading comments...</div>
) : comments.length > 0 ? (
<div className="space-y-4">
{comments.map((comment) => (
<div key={comment._id} className="rounded-lg border border-zinc-800 bg-zinc-950 p-4">
<div className="mb-2 flex items-center gap-2">
<span className="text-sm font-semibold">
{comment.user.telegramFirstName || ''} {comment.user.telegramLastName || ''}
{comment.user.telegramUsername && ` (@${comment.user.telegramUsername})`}
</span>
<span className="text-xs text-zinc-500">
{new Date(comment.createdAt).toLocaleDateString()}
</span>
</div>
<p className="text-sm text-zinc-300">{comment.content}</p>
</div>
))}
</div>
) : (
<div className="text-zinc-400">No comments yet.</div>
)}
</div>
{token && (
<div className="border-t border-zinc-800 pt-4">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
className="w-full rounded-lg border border-zinc-800 bg-zinc-950 px-4 py-2 text-white focus:border-zinc-700 focus:outline-none"
rows={3}
placeholder="Add a comment..."
/>
<button
onClick={handleAddComment}
className="mt-2 rounded-lg bg-white px-4 py-2 text-sm font-semibold text-zinc-950 transition-colors hover:bg-zinc-200"
>
Post Comment
</button>
</div>
)}
</div>
</div>
</div>
);
}
-218
View File
@@ -1,218 +0,0 @@
'use client';
import { useState } from 'react';
import type { Feedback } from '../feedback/page';
import FeedbackDetailModal from './FeedbackDetailModal';
interface FeedbackListProps {
feedbacks: Feedback[];
token: string | null;
onUpdate: () => void;
useMockData?: boolean;
mockComments?: Record<string, Array<{
_id: string;
content: string;
user: {
telegramId: number;
telegramFirstName?: string;
telegramLastName?: string;
telegramUsername?: string;
};
createdAt: string;
}>>;
setFeedbacks?: (feedbacks: Feedback[]) => void;
}
export default function FeedbackList({ feedbacks, token, onUpdate, useMockData = false, mockComments, setFeedbacks }: FeedbackListProps) {
const [selectedFeedback, setSelectedFeedback] = useState<Feedback | null>(null);
if (feedbacks.length === 0) {
return (
<div className="rounded-lg border border-zinc-800 bg-zinc-900/50 p-8 sm:p-12 text-center">
<p className="text-sm sm:text-base text-zinc-400">No {feedbacks.length === 0 ? 'feedback' : 'results'} found.</p>
</div>
);
}
return (
<>
<div className="space-y-3 sm:space-y-4">
{feedbacks.map((feedback) => (
<FeedbackListItem
key={feedback._id}
feedback={feedback}
token={token}
onUpdate={onUpdate}
onClick={() => setSelectedFeedback(feedback)}
useMockData={useMockData}
setFeedbacks={setFeedbacks}
allFeedbacks={feedbacks}
/>
))}
</div>
{selectedFeedback && (
<FeedbackDetailModal
feedback={selectedFeedback}
token={token}
onClose={() => setSelectedFeedback(null)}
onUpdate={() => {
onUpdate();
setSelectedFeedback(null);
}}
useMockData={useMockData}
mockComments={mockComments}
feedbacks={feedbacks}
setFeedbacks={setFeedbacks}
/>
)}
</>
);
}
function FeedbackListItem({
feedback,
token,
onUpdate,
onClick,
useMockData = false,
setFeedbacks,
allFeedbacks,
}: {
feedback: Feedback;
token: string | null;
onUpdate: () => void;
onClick: () => void;
useMockData?: boolean;
setFeedbacks?: (feedbacks: Feedback[]) => void;
allFeedbacks: Feedback[];
}) {
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const handleVote = async (e: React.MouseEvent, voteType: 'upvote' | 'downvote') => {
e.stopPropagation();
if (!token) return;
try {
if (useMockData && setFeedbacks) {
// Mock vote - update local state
await new Promise(resolve => setTimeout(resolve, 200));
const updatedFeedbacks = allFeedbacks.map(f => {
if (f._id === feedback._id) {
const updated = { ...f };
// Toggle vote logic
if (updated.userVote === voteType) {
// Remove vote
updated.userVote = null;
if (voteType === 'upvote') {
updated.upvotes = Math.max(0, updated.upvotes - 1);
} else {
updated.downvotes = Math.max(0, updated.downvotes - 1);
}
} else if (updated.userVote) {
// Change vote
const oldVote = updated.userVote;
if (oldVote === 'upvote') {
updated.upvotes = Math.max(0, updated.upvotes - 1);
} else {
updated.downvotes = Math.max(0, updated.downvotes - 1);
}
updated.userVote = voteType;
if (voteType === 'upvote') {
updated.upvotes += 1;
} else {
updated.downvotes += 1;
}
} else {
// New vote
updated.userVote = voteType;
if (voteType === 'upvote') {
updated.upvotes += 1;
} else {
updated.downvotes += 1;
}
}
return updated;
}
return f;
});
// Update global mock state
const { setMockFeedbacks } = require('../feedback/page');
setMockFeedbacks(updatedFeedbacks);
setFeedbacks(updatedFeedbacks);
} else {
const response = await fetch(`${API_BASE_URL}/api/feedback/${feedback._id}/vote`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ voteType }),
});
const data = await response.json();
if (data.success) {
onUpdate();
}
}
} catch (error) {
console.error('Failed to vote:', error);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'planned':
return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
case 'in_progress':
return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
case 'complete':
return 'bg-green-500/20 text-green-400 border-green-500/30';
default:
return 'bg-zinc-800 text-zinc-400 border-zinc-700';
}
};
return (
<div
className="cursor-pointer rounded-lg border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6 transition-colors active:bg-zinc-900 sm:hover:bg-zinc-900"
onClick={onClick}
>
<div className="flex items-start justify-between gap-3 sm:gap-4">
<div className="flex-1 min-w-0">
<div className="mb-2 flex flex-wrap items-center gap-2 sm:gap-3">
<h3 className="text-base sm:text-lg font-semibold text-white break-words">{feedback.title}</h3>
<span className={`rounded-full border px-2 py-0.5 sm:py-1 text-xs font-medium whitespace-nowrap ${getStatusColor(feedback.status)}`}>
{feedback.status === 'in_progress' ? 'In Progress' : feedback.status.charAt(0).toUpperCase() + feedback.status.slice(1)}
</span>
</div>
{feedback.description && (
<p className="mb-3 sm:mb-4 line-clamp-2 text-xs sm:text-sm text-zinc-400">{feedback.description}</p>
)}
<div className="flex items-center gap-3 sm:gap-4">
<button
onClick={(e) => handleVote(e, 'upvote')}
className={`flex items-center gap-1.5 sm:gap-2 rounded-lg px-2.5 sm:px-3 py-1.5 text-xs sm:text-sm transition-colors touch-manipulation ${feedback.userVote === 'upvote'
? 'bg-blue-500/20 text-blue-400'
: 'bg-zinc-800 text-zinc-400 active:bg-zinc-700 sm:hover:bg-zinc-700'
}`}
disabled={!token}
>
<svg className="h-3.5 w-3.5 sm:h-4 sm:w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
</svg>
{feedback.upvotes}
</button>
<span className="text-xs sm:text-sm text-zinc-500">
{feedback.comments.length} {feedback.comments.length === 1 ? 'comment' : 'comments'}
</span>
</div>
</div>
</div>
</div>
);
}