-
Notifications
You must be signed in to change notification settings - Fork 12
Item Card UI Update #92
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
Changes from all commits
7b03f5b
e65b9d3
e3a0cb5
2d0c660
a3a56ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||||||
|
|
@@ -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> | ||||||
| </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} | ||||||
|
||||||
| {item.title} | |
| {item.title || 'Untitled Item'} |
Copilot
AI
Oct 19, 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.
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 || '').
| <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
AI
Oct 19, 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.
[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)}.
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.
[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)}.