Skip to content
Merged
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
145 changes: 67 additions & 78 deletions frontend/src/components/ItemCard.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Badge from './ui/Badge';
import { ShieldCheck, MapPin } from './ui/icons';
import { cardStyles } from '../lib/utils';
import { MapPin } from 'lucide-react';
import PropTypes from 'prop-types';

// Constants for better maintainability
const PLACEHOLDER_IMAGE = '/placeholder.svg';
Expand Down Expand Up @@ -67,80 +65,70 @@ const ItemCard = ({ item }) => {
const statusText = item.kind ? item.kind.charAt(0).toUpperCase() + item.kind.slice(1) : 'Unknown';

return (
<Link to={`/items/${item.id}`} className="block group">
<div className={`${cardStyles.hover} h-full transition-all duration-200 hover:scale-[1.02]`}>
<div className="p-6">
<div className="flex gap-4">
{/* Item Image */}
<div className="relative">
<div className="w-24 h-24 rounded-lg border border-gray-200 overflow-hidden bg-gray-100 flex-shrink-0 group-hover:border-emerald-300 transition-colors">
{imageLoading && (
<div className="w-full h-full flex items-center justify-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-emerald-600"></div>
</div>
)}
<img
src={imageSrc}
alt={imageAlt}
className={`object-cover w-full h-full transition-opacity duration-200 ${
imageLoading ? 'opacity-0' : 'opacity-100'
}`}
onLoad={handleImageLoad}
onError={handleImageError}
/>
</div>
{item.reporter?.trust && (
<div className="absolute -top-2 -right-2">
<Badge
variant="outline"
className="gap-1 border-emerald-300 text-emerald-700 bg-emerald-50 text-xs font-medium px-2 py-1"
>
<ShieldCheck className="h-3 w-3" />
Trusted
</Badge>
</div>
)}
</div>

{/* Item Details */}
<div className="flex-1 min-w-0">
{/* Status and Category Badges */}
<div className="flex flex-wrap items-center gap-2 mb-3">
<Badge
className={`capitalize text-xs font-medium border ${getStatusColor(statusText)}`}
>
{statusText}
</Badge>
{item.claimed && (
<Badge className="bg-slate-600 text-white text-xs font-medium">Claimed</Badge>
)}
<Badge
className="bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-medium capitalize"
>
{item.category || 'Uncategorized'}
</Badge>
</div>
{/* Title */}
<h3 className="font-semibold text-gray-900 text-sm mb-2 leading-tight line-clamp-2 group-hover:text-emerald-700 transition-colors">
{item.title || 'Untitled Item'}
</h3>
{/* Description */}
<p className="text-xs text-gray-600 mb-3 leading-relaxed line-clamp-2">
{item.description || 'No description provided'}
</p>
{/* Location and Date */}
<div className="flex items-center gap-2 text-xs text-gray-500">
<span className="flex items-center gap-1 truncate">
<MapPin className="h-3 w-3" />
{item.location || 'Unknown location'}
</span>
<span className="text-gray-400">β€’</span>
<span className="truncate">
πŸ•’ {formatDate(item.date)}
</span>
</div>
</div>
<Link
to={`/items/${item.id}`}
className="block bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-200 flex flex-col h-full"
>
{/* Image Section */}
<div className="relative h-48 bg-gray-200 flex-shrink-0">
{imageLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-600"></div>
</div>
)}
<img
src={imageSrc}
alt={imageAlt}
className={`w-full h-full object-cover ${imageLoading ? 'opacity-0' : 'opacity-100'} transition-opacity duration-200`}
onLoad={handleImageLoad}
onError={handleImageError}
/>
</div>

{/* Location and Date - Full width under image */}
<div className="flex items-center gap-3 text-xs text-gray-500 px-4 py-2 border-b border-gray-100 flex-shrink-0">
<span className="flex items-center gap-1.5 flex-1 min-w-0">
<MapPin className="h-3.5 w-3.5 flex-shrink-0" />
<span className="truncate">{item.location || 'Unknown location'}</span>
</span>
<span className="flex items-center gap-1.5 whitespace-nowrap">
<span className="flex-shrink-0">πŸ•’</span>
<span>{formatDate(item.date)}</span>
Comment on lines +95 to +96
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

[nitpick] Improve semantics and screen reader support by marking the emoji as decorative and wrapping the date with a time element. For example: πŸ•’ <time dateTime={new Date(item.date).toISOString()}>{formatDate(item.date)}.

Suggested change
<span className="flex-shrink-0">πŸ•’</span>
<span>{formatDate(item.date)}</span>
<span aria-hidden="true" className="flex-shrink-0">πŸ•’</span>
<time dateTime={new Date(item.date).toISOString()}>{formatDate(item.date)}</time>

Copilot uses AI. Check for mistakes.
</span>
</div>

{/* Content Section */}
<div className="p-4 flex flex-col flex-grow">
{/* Title */}
<h3 className="font-semibold text-lg text-gray-900 mb-2 line-clamp-1">
{item.title}
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

[nitpick] Title no longer has a fallback and may render empty when item.title is missing. Consider restoring the prior fallback for robustness: {item.title || 'Untitled Item'}.

Suggested change
{item.title}
{item.title || 'Untitled Item'}

Copilot uses AI. Check for mistakes.
</h3>

{/* Description */}
<p className="text-gray-600 text-sm mb-3 line-clamp-2 flex-grow">
{item.description || 'No description available'}
</p>

{/* Status and Category Tags - Stick to bottom */}
<div className="flex flex-wrap gap-2 mt-auto">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${getStatusColor(item.kind)}`}>
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

getStatusColor(item.kind) will throw if item.kind is undefined or null because getStatusColor calls status.toLowerCase(). Use the already computed statusText (which always falls back to a string) or guard the input, e.g. getStatusColor(statusText) or getStatusColor(item.kind || '').

Suggested change
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${getStatusColor(item.kind)}`}>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${getStatusColor(statusText)}`}>

Copilot uses AI. Check for mistakes.
{statusText}
</span>
{item.category && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700 border border-blue-200">
{item.category}
</span>
)}
Comment on lines +113 to +121
Copy link

Copilot AI Oct 19, 2025

Choose a reason for hiding this comment

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

[nitpick] The 'Claimed' badge present in the previous UI has been removed, which drops an important state indicator. If that was unintentional, consider re-adding it in this tag section, e.g.: {item.claimed && (Claimed)}.

Copilot uses AI. Check for mistakes.
{item.claimed && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-700 border border-yellow-200">
Claimed
</span>
)}
{item.reporter?.trust && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-700 border border-purple-200">
βœ“ Verified
</span>
)}
</div>
</div>
</Link>
Expand All @@ -157,10 +145,11 @@ ItemCard.propTypes = {
imageUrl: PropTypes.string,
kind: PropTypes.string,
category: PropTypes.string,
claimed: PropTypes.bool,
reporter: PropTypes.shape({
trust: PropTypes.bool
})
}).isRequired
};

export default ItemCard;
export default ItemCard;
37 changes: 23 additions & 14 deletions frontend/src/pages/ItemDetail.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { useParams, useNavigate, Link } from 'react-router-dom';
import Badge from '../components/ui/Badge';
import { ArrowLeft, ShieldCheck, MapPin } from '../components/ui/icons';
import { ArrowLeft, MapPin } from '../components/ui/icons';
import { db } from '../firebase/config';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
import {
Expand Down Expand Up @@ -259,21 +258,31 @@ const ItemDetailPage = () => {
className="rounded-lg border aspect-video object-cover"
/>
<div>
<div className="flex flex-wrap items-center gap-2 mb-2">
<Badge variant="secondary" className="capitalize">
{item.kind || item.status}
</Badge>
{/* Status and Category Tags */}
<div className="flex flex-wrap gap-2 mb-4">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${
item.kind === 'lost'
? 'bg-red-100 text-red-700 border-red-200'
: item.kind === 'found'
? 'bg-green-100 text-green-700 border-green-200'
: 'bg-gray-100 text-gray-700 border-gray-200'
}`}>
{item.kind?.charAt(0).toUpperCase() + item.kind?.slice(1) || 'Unknown'}
</span>
{item.category && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700 border border-blue-200">
{item.category}
</span>
)}
{item.claimed && (
<Badge className="bg-slate-600 text-white">Claimed</Badge>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-700 border border-yellow-200">
Claimed
</span>
)}
<Badge className="bg-emerald-600 hover:bg-emerald-700">
{item.category || item.type}
</Badge>
{userInfo?.trust && (
<Badge variant="outline" className="gap-1">
<ShieldCheck />
Trusted
</Badge>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-700 border border-purple-200">
βœ“ Verified
</span>
)}
</div>
<h1 className="text-2xl font-semibold">{item.title}</h1>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/__tests__/ItemDetail.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ describe('ItemDetailPage', () => {
});
};

const assertItemStatusBadge = async (status = 'lost') => {
const assertItemStatusBadge = async (status = 'Lost') => {
await waitFor(() => {
expect(screen.getByText(status)).toBeInTheDocument();
});
Expand Down Expand Up @@ -262,14 +262,14 @@ describe('ItemDetailPage', () => {
test('displays item status badge correctly', async () => {
setupOnSnapshotMock();
renderItemDetailPage();
await assertItemStatusBadge('lost');
await assertItemStatusBadge('Lost');
});

test('displays found item status badge correctly', async () => {
const foundItem = { ...mockItem, kind: 'found' };
setupOnSnapshotMock(foundItem);
renderItemDetailPage();
await assertItemStatusBadge('found');
await assertItemStatusBadge('Found');
});

test('displays claimed badge when item is claimed', async () => {
Expand Down