show default image

This commit is contained in:
Jurgis Sakalauskas
2025-10-16 10:22:50 +03:00
parent 91ded3de2d
commit 210cf50578
4 changed files with 77 additions and 48 deletions
@@ -3,6 +3,7 @@ import { ExternalLink } from 'lucide-react';
import type { Item } from '@shared/models/item'; import type { Item } from '@shared/models/item';
import { ItemPricesPanel } from './item-prices-panel'; import { ItemPricesPanel } from './item-prices-panel';
import { formatAttributeValue, getPriceSummaryDisplay } from './project-items-utils'; import { formatAttributeValue, getPriceSummaryDisplay } from './project-items-utils';
import { ItemThumbnail } from './item-thumbnail';
interface ProjectItemsDesktopTableProps { interface ProjectItemsDesktopTableProps {
items: Item[]; items: Item[];
@@ -49,13 +50,18 @@ export function ProjectItemsDesktopTable({
const priceDisplay = getPriceSummaryDisplay(item.priceSummary); const priceDisplay = getPriceSummaryDisplay(item.priceSummary);
const isExpanded = expandedItemId === item.id; const isExpanded = expandedItemId === item.id;
const panelId = `item-${item.id}-prices-desktop`; const panelId = `item-${item.id}-prices-desktop`;
const displayName = item.manufacturer
? `${item.manufacturer} ${item.model}`
: item.model;
return ( return (
<Fragment key={item.id}> <Fragment key={item.id}>
<tr className={`hover:bg-slate-50 ${isExpanded ? 'bg-slate-50' : ''}`}> <tr className={`hover:bg-slate-50 ${isExpanded ? 'bg-slate-50' : ''}`}>
<td className="px-4 py-3 align-top"> <td className="px-4 py-3 align-top">
<div className="flex flex-col gap-1"> <div className="flex items-start gap-3">
<div className="flex items-center gap-2"> <ItemThumbnail url={item.defaultImageUrl} alt={displayName} size="sm" />
<div className="flex flex-1 flex-col gap-1">
<div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => onEditItem(item)} onClick={() => onEditItem(item)}
@@ -69,7 +75,7 @@ export function ProjectItemsDesktopTable({
}`} }`}
/> />
<span className="font-semibold group-hover:underline"> <span className="font-semibold group-hover:underline">
{item.manufacturer ? `${item.manufacturer} ${item.model}` : item.model} {displayName}
</span> </span>
</button> </button>
{item.sourceUrl ? ( {item.sourceUrl ? (
@@ -85,9 +91,10 @@ export function ProjectItemsDesktopTable({
</a> </a>
) : null} ) : null}
</div> </div>
{item.note ? ( {item.note ? (
<span className="text-xs text-slate-500">{item.note}</span> <span className="text-xs text-slate-500">{item.note}</span>
) : null} ) : null}
</div>
</div> </div>
</td> </td>
{visibleAttributes.map((attribute) => ( {visibleAttributes.map((attribute) => (
@@ -2,6 +2,7 @@ import { ExternalLink } from 'lucide-react';
import type { Item } from '@shared/models/item'; import type { Item } from '@shared/models/item';
import { ItemPricesPanel } from './item-prices-panel'; import { ItemPricesPanel } from './item-prices-panel';
import { formatAttributeValue, getPriceSummaryDisplay } from './project-items-utils'; import { formatAttributeValue, getPriceSummaryDisplay } from './project-items-utils';
import { ItemThumbnail } from './item-thumbnail';
interface ProjectItemsMobileListProps { interface ProjectItemsMobileListProps {
items: Item[]; items: Item[];
@@ -27,53 +28,59 @@ export function ProjectItemsMobileList({
const priceDisplay = getPriceSummaryDisplay(item.priceSummary); const priceDisplay = getPriceSummaryDisplay(item.priceSummary);
const isExpanded = expandedItemId === item.id; const isExpanded = expandedItemId === item.id;
const panelId = `item-${item.id}-prices-mobile`; const panelId = `item-${item.id}-prices-mobile`;
const displayName = item.manufacturer ? `${item.manufacturer} ${item.model}` : item.model;
return ( return (
<div key={item.id} className="rounded-xl border border-slate-200 p-4 shadow-sm"> <div key={item.id} className="rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-start gap-3"> <div className="flex items-start gap-4">
<button <ItemThumbnail url={item.defaultImageUrl} alt={displayName} size="md" />
type="button" <div className="flex flex-1 flex-col gap-2">
onClick={() => onEditItem(item)} <div className="flex items-start gap-2">
className="group flex flex-1 items-center gap-2 text-left text-slate-900 transition hover:text-blue-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white" <button
title="Edit item details" type="button"
> onClick={() => onEditItem(item)}
<span className="group flex flex-1 items-center gap-2 text-left text-slate-900 transition hover:text-blue-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
aria-hidden title="Edit item details"
className={`inline-block h-2.5 w-2.5 rounded-full ${ >
item.status === 'active' ? 'bg-emerald-500' : 'bg-slate-300' <span
}`} aria-hidden
/> className={`inline-block h-2.5 w-2.5 rounded-full ${
<span className="font-semibold group-hover:underline"> item.status === 'active' ? 'bg-emerald-500' : 'bg-slate-300'
{item.manufacturer ? `${item.manufacturer} ${item.model}` : item.model} }`}
</span> />
</button> <span className="font-semibold group-hover:underline">{displayName}</span>
{item.sourceUrl ? ( </button>
<a {item.sourceUrl ? (
href={item.sourceUrl} <a
target="_blank" href={item.sourceUrl}
rel="noreferrer" target="_blank"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-blue-600 transition focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white" rel="noreferrer"
title="Open source link" className="inline-flex h-9 w-9 items-center justify-center rounded-md text-blue-600 transition focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
> title="Open source link"
<ExternalLink aria-hidden className="h-5 w-5" /> >
<span className="sr-only">Open source link</span> <ExternalLink aria-hidden className="h-5 w-5" />
</a> <span className="sr-only">Open source link</span>
) : null} </a>
</div> ) : null}
{item.note ? <p className="mt-1 text-sm text-slate-600">Note: {item.note}</p> : null}
<div className="mt-3 space-y-2">
{visibleAttributes.map((attribute) => (
<div key={attribute} className="flex justify-between text-xs text-slate-600">
<span className="font-semibold uppercase tracking-wide text-slate-500">
{attribute}
</span>
<span className="text-right">{formatAttributeValue(attributes[attribute])}</span>
</div> </div>
))}
{item.note ? <p className="text-sm text-slate-600">Note: {item.note}</p> : null}
<div className="space-y-2">
{visibleAttributes.map((attribute) => (
<div key={attribute} className="flex justify-between text-xs text-slate-600">
<span className="font-semibold uppercase tracking-wide text-slate-500">
{attribute}
</span>
<span className="text-right">
{formatAttributeValue(attributes[attribute])}
</span>
</div>
))}
</div>
</div>
</div> </div>
<div className="mt-3 flex flex-col gap-0.5 text-xs"> <div className="mt-4 flex flex-col gap-0.5 text-xs">
<button <button
type="button" type="button"
onClick={() => onTogglePrices(item.id)} onClick={() => onTogglePrices(item.id)}
+14
View File
@@ -10,6 +10,7 @@ const itemColumns = `
i.model, i.model,
i.source_url_id, i.source_url_id,
i.default_image_id, i.default_image_id,
default_image.url AS default_image_url,
i.status, i.status,
i.note, i.note,
i.attributes, i.attributes,
@@ -38,6 +39,7 @@ export interface ItemRow {
model: string; model: string;
source_url_id: string | null; source_url_id: string | null;
default_image_id: string | null; default_image_id: string | null;
default_image_url: string | null;
status: ItemStatus; status: ItemStatus;
note: string | null; note: string | null;
attributes: Record<string, unknown>; attributes: Record<string, unknown>;
@@ -84,6 +86,15 @@ const mapItemRow = (row: ItemRow, images?: ItemImage[]): ItemRecord => {
} }
: null; : null;
let defaultImageUrl = row.default_image_url ?? null;
if (!defaultImageUrl && row.default_image_id && images) {
const defaultImage = images.find((image) => image.id === row.default_image_id);
if (defaultImage) {
defaultImageUrl = defaultImage.url;
}
}
return { return {
id: row.id, id: row.id,
projectId: row.project_id, projectId: row.project_id,
@@ -91,6 +102,7 @@ const mapItemRow = (row: ItemRow, images?: ItemImage[]): ItemRecord => {
model: row.model, model: row.model,
sourceUrlId: row.source_url_id, sourceUrlId: row.source_url_id,
defaultImageId: row.default_image_id, defaultImageId: row.default_image_id,
defaultImageUrl,
sourceUrl: row.source_url, sourceUrl: row.source_url,
status: row.status, status: row.status,
note: row.note, note: row.note,
@@ -128,6 +140,7 @@ export const listItems = async (options: ListItemsOptions): Promise<ItemRecord[]
SELECT ${itemColumns} SELECT ${itemColumns}
FROM items i FROM items i
LEFT JOIN urls u ON u.id = i.source_url_id LEFT JOIN urls u ON u.id = i.source_url_id
LEFT JOIN images default_image ON default_image.id = i.default_image_id
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT SELECT
MIN(ip.amount) AS min_amount, MIN(ip.amount) AS min_amount,
@@ -215,6 +228,7 @@ export const getItemById = async (id: string): Promise<ItemRecord | null> => {
SELECT ${itemColumns} SELECT ${itemColumns}
FROM items i FROM items i
LEFT JOIN urls u ON u.id = i.source_url_id LEFT JOIN urls u ON u.id = i.source_url_id
LEFT JOIN images default_image ON default_image.id = i.default_image_id
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT SELECT
MIN(ip.amount) AS min_amount, MIN(ip.amount) AS min_amount,
+1
View File
@@ -26,6 +26,7 @@ export interface Item {
sourceUrlId: string | null; sourceUrlId: string | null;
sourceUrl: string | null; sourceUrl: string | null;
defaultImageId: string | null; defaultImageId: string | null;
defaultImageUrl: string | null;
status: ItemStatus; status: ItemStatus;
note: string | null; note: string | null;
attributes: Record<string, unknown>; attributes: Record<string, unknown>;