This commit is contained in:
@@ -13,7 +13,7 @@ h1, h2, h3, h4 { margin: 0; font-family: var(--font-display); line-height: var(-
|
|||||||
.catalog-page, .detail-page, .showcase-page { min-height: 100vh; background: var(--color-background); }
|
.catalog-page, .detail-page, .showcase-page { min-height: 100vh; background: var(--color-background); }
|
||||||
.catalog-header, .catalog-results { width: min(1120px, calc(100% - var(--space-8))); margin: 0 auto; }
|
.catalog-header, .catalog-results { width: min(1120px, calc(100% - var(--space-8))); margin: 0 auto; }
|
||||||
.catalog-header { padding: var(--space-24) 0 var(--space-16); }
|
.catalog-header { padding: var(--space-24) 0 var(--space-16); }
|
||||||
.catalog-kicker, .listing-card__status { color: var(--color-interactive); font-size: var(--text-xs); font-weight: var(--weight-bold); letter-spacing: 0.12em; text-transform: uppercase; }
|
.catalog-kicker, .listing-card__status { color: var(--color-interactive-text); font-size: var(--text-xs); font-weight: var(--weight-bold); letter-spacing: 0.12em; text-transform: uppercase; }
|
||||||
h1 { max-width: 760px; margin: var(--space-3) 0 var(--space-4); font-size: var(--text-display); }
|
h1 { max-width: 760px; margin: var(--space-3) 0 var(--space-4); font-size: var(--text-display); }
|
||||||
.catalog-intro { max-width: 560px; color: var(--color-text-muted); font-size: var(--text-lg); }
|
.catalog-intro { max-width: 560px; color: var(--color-text-muted); font-size: var(--text-lg); }
|
||||||
.search-field { display: block; max-width: 720px; margin-top: var(--space-12); color: var(--color-text-subtle); font-size: var(--text-sm); font-weight: var(--weight-bold); }
|
.search-field { display: block; max-width: 720px; margin-top: var(--space-12); color: var(--color-text-subtle); font-size: var(--text-sm); font-weight: var(--weight-bold); }
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import CollectibleDetail from './CollectibleDetail.vue';
|
||||||
|
|
||||||
|
export default { title: 'Organisms/CollectibleDetail', component: CollectibleDetail };
|
||||||
|
|
||||||
|
export const ListingDetail = {
|
||||||
|
args: {
|
||||||
|
title: 'Charizard ex #199/165',
|
||||||
|
status: 'AVAILABLE',
|
||||||
|
priceMinor: 11500,
|
||||||
|
condition: 'Near Mint',
|
||||||
|
description: 'A clean collector copy with a clear front and back.',
|
||||||
|
facts: [
|
||||||
|
{ label: 'Set', value: 'Scarlet & Violet 151' },
|
||||||
|
{ label: 'Card number', value: '199/165' },
|
||||||
|
{ label: 'Market context', value: '$120 reference' },
|
||||||
|
{ label: 'Updated', value: 'Today' },
|
||||||
|
],
|
||||||
|
images: [
|
||||||
|
{ id: 'front', label: 'FRONT', alt: 'Front of Charizard ex' },
|
||||||
|
{ id: 'back', label: 'BACK', alt: 'Back of Charizard ex' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProductWithoutImages = {
|
||||||
|
args: {
|
||||||
|
title: 'Pikachu World Collection',
|
||||||
|
description: 'A representative product identity with images still to be added.',
|
||||||
|
facts: [{ label: 'Category', value: 'Collectible merchandise' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
import ConditionBadge from '@/components/atoms/ConditionBadge.vue';
|
||||||
|
import PriceDisplay from '@/components/atoms/PriceDisplay.vue';
|
||||||
|
import StatusBadge from '@/components/atoms/StatusBadge.vue';
|
||||||
|
import ImageGallery from '@/components/molecules/ImageGallery.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
status: { type: String, default: '' },
|
||||||
|
priceMinor: { type: Number, default: null },
|
||||||
|
condition: { type: String, default: '' },
|
||||||
|
description: { type: String, default: '' },
|
||||||
|
facts: { type: Array, default: () => [] },
|
||||||
|
images: { type: Array, default: () => [] },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="collectible-detail">
|
||||||
|
<div class="collectible-detail__gallery">
|
||||||
|
<ImageGallery :images="images" />
|
||||||
|
</div>
|
||||||
|
<div class="collectible-detail__identity">
|
||||||
|
<StatusBadge v-if="status" :status="status" />
|
||||||
|
<p class="catalog-kicker">Collectible identity</p>
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<p v-if="description" class="detail-notes">{{ description }}</p>
|
||||||
|
<div class="collectible-detail__summary">
|
||||||
|
<PriceDisplay v-if="priceMinor !== null" :amount-minor="priceMinor" />
|
||||||
|
<ConditionBadge v-if="condition" :label="condition" />
|
||||||
|
</div>
|
||||||
|
<dl v-if="facts.length" class="product-facts">
|
||||||
|
<div v-for="fact in facts" :key="fact.label">
|
||||||
|
<dt>{{ fact.label }}</dt>
|
||||||
|
<dd>{{ fact.value }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.collectible-detail { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-12); align-items: start; }
|
||||||
|
.collectible-detail__identity { display: grid; gap: var(--space-4); padding-top: var(--space-6); }
|
||||||
|
.collectible-detail__identity h1 { margin: 0; }
|
||||||
|
.collectible-detail__summary { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); }
|
||||||
|
@media (max-width: 768px) { .collectible-detail { grid-template-columns: 1fr; gap: var(--space-6); } .collectible-detail__identity { padding-top: 0; } }
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,7 @@ import { onMounted, ref } from 'vue';
|
|||||||
import { RouterLink, useRoute } from 'vue-router';
|
import { RouterLink, useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { getPublicListing } from '@/services/catalog-service.js';
|
import { getPublicListing } from '@/services/catalog-service.js';
|
||||||
|
import CollectibleDetail from '@/components/organisms/CollectibleDetail.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const listing = ref(null);
|
const listing = ref(null);
|
||||||
@@ -25,11 +26,13 @@ onMounted(async () => {
|
|||||||
<RouterLink class="detail-back" :to="{ name: 'catalog' }">Back to catalog</RouterLink>
|
<RouterLink class="detail-back" :to="{ name: 'catalog' }">Back to catalog</RouterLink>
|
||||||
<p v-if="isLoading" class="catalog-state">Loading listing...</p>
|
<p v-if="isLoading" class="catalog-state">Loading listing...</p>
|
||||||
<p v-else-if="error" class="catalog-state catalog-state--error">{{ error.message }}</p>
|
<p v-else-if="error" class="catalog-state catalog-state--error">{{ error.message }}</p>
|
||||||
<article v-else-if="listing" class="detail-content">
|
<CollectibleDetail
|
||||||
<p class="catalog-kicker">{{ listing.status }}</p>
|
v-else-if="listing"
|
||||||
<h1>{{ listing.productName }}</h1>
|
:title="listing.productName"
|
||||||
<p class="detail-price">${{ (listing.askingPriceMinor / 100).toFixed(2) }}</p>
|
:status="listing.status"
|
||||||
<p v-if="listing.notes" class="detail-notes">{{ listing.notes }}</p>
|
:price-minor="listing.askingPriceMinor"
|
||||||
</article>
|
:condition="listing.conditionId"
|
||||||
|
:description="listing.notes"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
@@ -3,6 +3,7 @@ import { onMounted, ref } from 'vue';
|
|||||||
import { RouterLink, useRoute } from 'vue-router';
|
import { RouterLink, useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { getPublicProduct } from '@/services/catalog-service.js';
|
import { getPublicProduct } from '@/services/catalog-service.js';
|
||||||
|
import CollectibleDetail from '@/components/organisms/CollectibleDetail.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const product = ref(null);
|
const product = ref(null);
|
||||||
@@ -25,16 +26,16 @@ onMounted(async () => {
|
|||||||
<RouterLink class="detail-back" :to="{ name: 'catalog' }">Back to catalog</RouterLink>
|
<RouterLink class="detail-back" :to="{ name: 'catalog' }">Back to catalog</RouterLink>
|
||||||
<p v-if="isLoading" class="catalog-state">Loading product...</p>
|
<p v-if="isLoading" class="catalog-state">Loading product...</p>
|
||||||
<p v-else-if="error" class="catalog-state catalog-state--error">{{ error.message }}</p>
|
<p v-else-if="error" class="catalog-state catalog-state--error">{{ error.message }}</p>
|
||||||
<article v-else-if="product" class="detail-content">
|
<CollectibleDetail
|
||||||
<p class="catalog-kicker">Product catalog</p>
|
v-else-if="product"
|
||||||
<h1>{{ product.name }}</h1>
|
:title="product.name"
|
||||||
<p v-if="product.description" class="detail-notes">{{ product.description }}</p>
|
:description="product.description"
|
||||||
<dl class="product-facts">
|
:facts="[
|
||||||
<div v-if="product.brand"><dt>Brand</dt><dd>{{ product.brand }}</dd></div>
|
...(product.brand ? [{ label: 'Brand', value: product.brand }] : []),
|
||||||
<div v-if="product.setName"><dt>Set</dt><dd>{{ product.setName }}</dd></div>
|
...(product.setName ? [{ label: 'Set', value: product.setName }] : []),
|
||||||
<div v-if="product.cardNumber"><dt>Card number</dt><dd>{{ product.cardNumber }}</dd></div>
|
...(product.cardNumber ? [{ label: 'Card number', value: product.cardNumber }] : []),
|
||||||
<div v-if="product.releaseYear"><dt>Release year</dt><dd>{{ product.releaseYear }}</dd></div>
|
...(product.releaseYear ? [{ label: 'Release year', value: product.releaseYear }] : []),
|
||||||
</dl>
|
]"
|
||||||
</article>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
Reference in New Issue
Block a user