feat: improve public listing detail
Quality / test (push) Canceled after 0s

Expose public condition labels and safe market values on listing details, with Storybook coverage for market value presentation.
This commit is contained in:
2026-09-09 02:11:09 -04:00
parent 6dd138260b
commit ec078423e2
5 changed files with 30 additions and 3 deletions
@@ -1,8 +1,11 @@
import { and, asc, count, desc, eq, ilike, inArray, or, sql } from 'drizzle-orm'; import { and, asc, count, desc, eq, ilike, inArray, or, sql } from 'drizzle-orm';
import { import {
conditions,
inventoryItems, inventoryItems,
listings, listings,
marketValueOverrides,
marketValuations,
products, products,
} from '../../../infrastructure/database/schema.js'; } from '../../../infrastructure/database/schema.js';
@@ -134,17 +137,31 @@ export function createListingRepository(db) {
async findPublicById(listingId) { async findPublicById(listingId) {
const [result] = await db const [result] = await db
.select({ listing: listings }) .select({ listing: listings, condition: conditions })
.from(listings) .from(listings)
.innerJoin(inventoryItems, eq(inventoryItems.id, listings.inventoryItemId)) .innerJoin(inventoryItems, eq(inventoryItems.id, listings.inventoryItemId))
.innerJoin(products, eq(products.id, inventoryItems.productId)) .innerJoin(products, eq(products.id, inventoryItems.productId))
.innerJoin(conditions, eq(conditions.id, inventoryItems.conditionId))
.where(and( .where(and(
eq(listings.id, listingId), eq(listings.id, listingId),
inArray(listings.status, ['AVAILABLE', 'PENDING']), inArray(listings.status, ['AVAILABLE', 'PENDING']),
eq(inventoryItems.status, 'ACTIVE'), eq(inventoryItems.status, 'ACTIVE'),
)); ));
return result?.listing ?? null; if (!result) return null;
const [override] = await db.select().from(marketValueOverrides)
.where(eq(marketValueOverrides.inventoryItemId, result.listing.inventoryItemId))
.orderBy(desc(marketValueOverrides.createdAt)).limit(1);
const [valuation] = await db.select().from(marketValuations)
.where(eq(marketValuations.inventoryItemId, result.listing.inventoryItemId))
.orderBy(desc(marketValuations.calculatedAt)).limit(1);
return {
...result.listing,
conditionName: result.condition.name,
conditionCode: result.condition.code,
marketValueMinor: override?.valueMinor ?? valuation?.valueMinor ?? null,
marketValueCurrency: override?.currency ?? valuation?.currency ?? null,
};
}, },
async updateStatus(listingId, status) { async updateStatus(listingId, status) {
@@ -7,6 +7,7 @@ export const ListingDetail = {
title: 'Charizard ex #199/165', title: 'Charizard ex #199/165',
status: 'AVAILABLE', status: 'AVAILABLE',
priceMinor: 11500, priceMinor: 11500,
marketValueMinor: 12000,
condition: 'Near Mint', condition: 'Near Mint',
description: 'A clean collector copy with a clear front and back.', description: 'A clean collector copy with a clear front and back.',
facts: [ facts: [
@@ -8,6 +8,8 @@ defineProps({
title: { type: String, required: true }, title: { type: String, required: true },
status: { type: String, default: '' }, status: { type: String, default: '' },
priceMinor: { type: Number, default: null }, priceMinor: { type: Number, default: null },
marketValueMinor: { type: Number, default: null },
marketValueCurrency: { type: String, default: 'USD' },
condition: { type: String, default: '' }, condition: { type: String, default: '' },
description: { type: String, default: '' }, description: { type: String, default: '' },
facts: { type: Array, default: () => [] }, facts: { type: Array, default: () => [] },
@@ -27,6 +29,7 @@ defineProps({
<p v-if="description" class="detail-notes">{{ description }}</p> <p v-if="description" class="detail-notes">{{ description }}</p>
<div class="collectible-detail__summary"> <div class="collectible-detail__summary">
<PriceDisplay v-if="priceMinor !== null" :amount-minor="priceMinor" /> <PriceDisplay v-if="priceMinor !== null" :amount-minor="priceMinor" />
<PriceDisplay v-if="marketValueMinor !== null" label="Market value" :amount-minor="marketValueMinor" />
<ConditionBadge v-if="condition" :label="condition" /> <ConditionBadge v-if="condition" :label="condition" />
</div> </div>
<dl v-if="facts.length" class="product-facts"> <dl v-if="facts.length" class="product-facts">
+3 -1
View File
@@ -31,7 +31,9 @@ onMounted(async () => {
:title="listing.productName" :title="listing.productName"
:status="listing.status" :status="listing.status"
:price-minor="listing.askingPriceMinor" :price-minor="listing.askingPriceMinor"
:condition="listing.conditionId" :market-value-minor="listing.marketValueMinor"
:market-value-currency="listing.marketValueCurrency"
:condition="listing.conditionName ? `${listing.conditionName} (${listing.conditionCode})` : listing.conditionId"
:description="listing.notes" :description="listing.notes"
:images="listing.images" :images="listing.images"
/> />
+4
View File
@@ -8,7 +8,11 @@ export function toPublicListingDto(listing) {
productName: listing.productName, productName: listing.productName,
categoryId: listing.categoryId, categoryId: listing.categoryId,
conditionId: listing.conditionId, conditionId: listing.conditionId,
conditionName: listing.conditionName ?? null,
conditionCode: listing.conditionCode ?? null,
askingPriceMinor: listing.askingPriceMinor, askingPriceMinor: listing.askingPriceMinor,
marketValueMinor: listing.marketValueMinor ?? null,
marketValueCurrency: listing.marketValueCurrency ?? null,
status: listing.status, status: listing.status,
notes: listing.notes ?? null, notes: listing.notes ?? null,
images: listing.images ?? [], images: listing.images ?? [],