feat: marketplace GPU spec cards with VRAM, CUDA, region, pricing
- Add GPU fields to OfferRecord type (gpu_model, gpu_memory_gb, etc.) - Replace flat table with responsive GPU offer cards - Show GPU model, VRAM, GPU count, CUDA version, capacity specs - Display pricing and SLA with provider + region - Add offer-card CSS with spec grid, hover effects, responsive layout
This commit is contained in:
@@ -9,6 +9,13 @@ interface OfferRecord {
|
|||||||
price: number;
|
price: number;
|
||||||
sla: string;
|
sla: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
created_at?: string;
|
||||||
|
gpu_model?: string;
|
||||||
|
gpu_memory_gb?: number;
|
||||||
|
gpu_count?: number;
|
||||||
|
cuda_version?: string;
|
||||||
|
price_per_hour?: number;
|
||||||
|
region?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OffersResponse {
|
interface OffersResponse {
|
||||||
|
|||||||
@@ -124,38 +124,43 @@ function renderOffers(offers: MarketplaceOffer[]): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = offers
|
const cards = offers
|
||||||
.map(
|
.map(
|
||||||
(offer) => `
|
(offer) => `
|
||||||
<tr>
|
<article class="offer-card">
|
||||||
<td>${offer.id}</td>
|
<div class="offer-card-header">
|
||||||
<td>${offer.provider}</td>
|
<div class="offer-gpu-name">${offer.gpu_model || 'Unknown GPU'}</div>
|
||||||
<td>${formatNumber(offer.capacity)} units</td>
|
<span class="${statusClass(offer.status)}">${offer.status}</span>
|
||||||
<td>${formatNumber(offer.price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</td>
|
</div>
|
||||||
<td>${offer.sla}</td>
|
<div class="offer-provider">${offer.provider}${offer.region ? ` · ${offer.region}` : ''}</div>
|
||||||
<td><span class="${statusClass(offer.status)}">${offer.status}</span></td>
|
<div class="offer-specs">
|
||||||
</tr>
|
<div class="spec-item">
|
||||||
|
<span class="spec-label">VRAM</span>
|
||||||
|
<span class="spec-value">${offer.gpu_memory_gb ? `${offer.gpu_memory_gb} GB` : '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="spec-item">
|
||||||
|
<span class="spec-label">GPUs</span>
|
||||||
|
<span class="spec-value">${offer.gpu_count ?? 1}×</span>
|
||||||
|
</div>
|
||||||
|
<div class="spec-item">
|
||||||
|
<span class="spec-label">CUDA</span>
|
||||||
|
<span class="spec-value">${offer.cuda_version || '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="spec-item">
|
||||||
|
<span class="spec-label">Capacity</span>
|
||||||
|
<span class="spec-value">${formatNumber(offer.capacity)} units</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="offer-pricing">
|
||||||
|
<div class="offer-price">${formatNumber(offer.price_per_hour ?? offer.price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <small>credits/hr</small></div>
|
||||||
|
<div class="offer-sla">${offer.sla}</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
selectors.offersWrapper.innerHTML = `
|
selectors.offersWrapper.innerHTML = `<div class="offers-grid">${cards}</div>`;
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="offers-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Provider</th>
|
|
||||||
<th>Capacity</th>
|
|
||||||
<th>Price</th>
|
|
||||||
<th>SLA</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>${rows}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message: string, duration = 2500): void {
|
function showToast(message: string, duration = 2500): void {
|
||||||
|
|||||||
@@ -112,6 +112,97 @@ body {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.offers-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5e9f1;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-card:hover {
|
||||||
|
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.12);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-gpu-name {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1d2736;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-provider {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-specs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-pricing {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-price {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-price small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offer-sla {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
.status-pill {
|
.status-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user