feat(CRIT-010): show market prices on county pages alongside premium dealers
Add OilPrice type and oilPrices.getByCounty() API client method. County page now fetches both premium listings and scraped oil prices in parallel, displaying them in separate sections with appropriate table/card views. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import type {
|
||||
Listing,
|
||||
CreateListingRequest,
|
||||
UpdateListingRequest,
|
||||
OilPrice,
|
||||
County,
|
||||
ServiceCategory
|
||||
} from './types';
|
||||
@@ -265,6 +266,18 @@ export const listingsApi = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Oil Prices API methods (scraped market data, public)
|
||||
*/
|
||||
export const oilPricesApi = {
|
||||
/**
|
||||
* Get oil prices for a county (public)
|
||||
*/
|
||||
async getByCounty(countyId: number): Promise<ApiResponse<OilPrice[]>> {
|
||||
return request<OilPrice[]>(`/oil-prices/county/${countyId}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* State/County API methods
|
||||
*/
|
||||
@@ -304,6 +317,7 @@ export const api = {
|
||||
company: companyApi,
|
||||
listing: listingApi,
|
||||
listings: listingsApi,
|
||||
oilPrices: oilPricesApi,
|
||||
state: stateApi,
|
||||
categories: categoriesApi
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// API Client exports
|
||||
export { api, authApi, companyApi, listingApi, listingsApi, stateApi, categoriesApi } from './client';
|
||||
export { api, authApi, companyApi, listingApi, listingsApi, oilPricesApi, stateApi, categoriesApi } from './client';
|
||||
export * from './types';
|
||||
|
||||
@@ -99,6 +99,18 @@ export interface CreateListingRequest {
|
||||
|
||||
export type UpdateListingRequest = Partial<CreateListingRequest>;
|
||||
|
||||
// Oil Price Types (scraped market data)
|
||||
export interface OilPrice {
|
||||
id: number;
|
||||
state: string | null;
|
||||
zone: number | null;
|
||||
name: string | null;
|
||||
price: number | null;
|
||||
date: string | null;
|
||||
scrapetimestamp: string | null;
|
||||
county_id: number | null;
|
||||
}
|
||||
|
||||
// State/County Types
|
||||
export interface County {
|
||||
id: number;
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { newEnglandStates } from '../../../../lib/states';
|
||||
import { api } from '$lib/api';
|
||||
import type { Listing, County } from '$lib/api';
|
||||
import type { Listing, OilPrice, County } from '$lib/api';
|
||||
|
||||
const { stateSlug, countySlug } = $page.params as { stateSlug: string; countySlug: string };
|
||||
let countyData: County | null = null;
|
||||
let listings: Listing[] = [];
|
||||
let oilPrices: OilPrice[] = [];
|
||||
let loading = true;
|
||||
let listingsLoading = false;
|
||||
let error: string | null = null;
|
||||
@@ -24,28 +25,34 @@
|
||||
countyData = null;
|
||||
} else {
|
||||
countyData = result.data;
|
||||
// Fetch listings for this county
|
||||
await fetchListings();
|
||||
// Fetch both listings and oil prices in parallel
|
||||
await fetchAllPrices();
|
||||
}
|
||||
|
||||
loading = false;
|
||||
});
|
||||
|
||||
async function fetchListings() {
|
||||
async function fetchAllPrices() {
|
||||
if (!countyData) return;
|
||||
|
||||
listingsLoading = true;
|
||||
listingsError = null;
|
||||
|
||||
const result = await api.listings.getByCounty(countyData.id);
|
||||
const [listingsResult, oilPricesResult] = await Promise.all([
|
||||
api.listings.getByCounty(countyData.id),
|
||||
api.oilPrices.getByCounty(countyData.id)
|
||||
]);
|
||||
|
||||
if (result.error) {
|
||||
if (listingsResult.error) {
|
||||
listingsError = 'Failed to load listings';
|
||||
} else {
|
||||
listings = result.data || [];
|
||||
listings = listingsResult.data || [];
|
||||
sortListings();
|
||||
}
|
||||
|
||||
// Oil prices failure is non-critical - just show empty
|
||||
oilPrices = oilPricesResult.data || [];
|
||||
|
||||
listingsLoading = false;
|
||||
}
|
||||
|
||||
@@ -113,19 +120,22 @@
|
||||
<a href="/{stateSlug}" class="btn btn-primary mt-6">Back to {getStateName(countyData.state)}</a>
|
||||
</div>
|
||||
|
||||
<!-- Listings Table -->
|
||||
<div class="mt-8">
|
||||
{#if listingsLoading}
|
||||
<div class="flex justify-center">
|
||||
<div class="flex justify-center mt-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else if listingsError}
|
||||
<div class="alert alert-error">
|
||||
<div class="alert alert-error mt-8">
|
||||
<span>{listingsError}</span>
|
||||
</div>
|
||||
{:else if listings.length === 0}
|
||||
<div class="text-center py-8">
|
||||
<p class="text-lg text-gray-600">No active listings found for this county.</p>
|
||||
{:else}
|
||||
<!-- Premium Dealers Section -->
|
||||
<div class="mt-8">
|
||||
<h2 class="text-2xl font-bold mb-4">Premium Dealers</h2>
|
||||
|
||||
{#if listings.length === 0}
|
||||
<div class="text-center py-6">
|
||||
<p class="text-gray-600">No premium dealers listed for this county yet.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Sort Controls -->
|
||||
@@ -299,6 +309,60 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Market Prices Section (scraped oil prices) -->
|
||||
{#if oilPrices.length > 0}
|
||||
<div class="mt-12">
|
||||
<h2 class="text-2xl font-bold mb-4">Market Prices</h2>
|
||||
|
||||
<!-- Desktop Table View -->
|
||||
<div class="hidden md:block overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Company Name</th>
|
||||
<th>Price</th>
|
||||
<th>Date Posted</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each oilPrices as op}
|
||||
<tr>
|
||||
<td>{op.name || 'Unknown'}</td>
|
||||
<td>{op.price != null ? `$${op.price.toFixed(2)}` : 'N/A'}</td>
|
||||
<td>{op.date || 'N/A'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Card View -->
|
||||
<div class="block md:hidden space-y-3">
|
||||
{#each oilPrices as op}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-semibold">{op.name || 'Unknown'}</span>
|
||||
<span class="text-lg font-bold">{op.price != null ? `$${op.price.toFixed(2)}` : 'N/A'}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
Posted: {op.date || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Empty state when both sections are empty -->
|
||||
{#if listings.length === 0 && oilPrices.length === 0}
|
||||
<div class="text-center py-8 mt-4">
|
||||
<p class="text-lg text-gray-600">No pricing data available for this county yet.</p>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if error}
|
||||
<div class="text-center py-10">
|
||||
<h1 class="text-3xl font-bold text-error">County Not Found</h1>
|
||||
|
||||
Reference in New Issue
Block a user