mirror of
https://github.com/sakaljurgis/best-choice.git
synced 2026-07-08 21:47:40 +00:00
show default image
This commit is contained in:
@@ -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)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Vendored
+1
@@ -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>;
|
||||||
|
|||||||
Reference in New Issue
Block a user