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,
|
Listing,
|
||||||
CreateListingRequest,
|
CreateListingRequest,
|
||||||
UpdateListingRequest,
|
UpdateListingRequest,
|
||||||
|
OilPrice,
|
||||||
County,
|
County,
|
||||||
ServiceCategory
|
ServiceCategory
|
||||||
} from './types';
|
} 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
|
* State/County API methods
|
||||||
*/
|
*/
|
||||||
@@ -304,6 +317,7 @@ export const api = {
|
|||||||
company: companyApi,
|
company: companyApi,
|
||||||
listing: listingApi,
|
listing: listingApi,
|
||||||
listings: listingsApi,
|
listings: listingsApi,
|
||||||
|
oilPrices: oilPricesApi,
|
||||||
state: stateApi,
|
state: stateApi,
|
||||||
categories: categoriesApi
|
categories: categoriesApi
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// API Client exports
|
// 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';
|
export * from './types';
|
||||||
|
|||||||
@@ -99,6 +99,18 @@ export interface CreateListingRequest {
|
|||||||
|
|
||||||
export type UpdateListingRequest = Partial<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
|
// State/County Types
|
||||||
export interface County {
|
export interface County {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { newEnglandStates } from '../../../../lib/states';
|
import { newEnglandStates } from '../../../../lib/states';
|
||||||
import { api } from '$lib/api';
|
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 };
|
const { stateSlug, countySlug } = $page.params as { stateSlug: string; countySlug: string };
|
||||||
let countyData: County | null = null;
|
let countyData: County | null = null;
|
||||||
let listings: Listing[] = [];
|
let listings: Listing[] = [];
|
||||||
|
let oilPrices: OilPrice[] = [];
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let listingsLoading = false;
|
let listingsLoading = false;
|
||||||
let error: string | null = null;
|
let error: string | null = null;
|
||||||
@@ -24,28 +25,34 @@
|
|||||||
countyData = null;
|
countyData = null;
|
||||||
} else {
|
} else {
|
||||||
countyData = result.data;
|
countyData = result.data;
|
||||||
// Fetch listings for this county
|
// Fetch both listings and oil prices in parallel
|
||||||
await fetchListings();
|
await fetchAllPrices();
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchListings() {
|
async function fetchAllPrices() {
|
||||||
if (!countyData) return;
|
if (!countyData) return;
|
||||||
|
|
||||||
listingsLoading = true;
|
listingsLoading = true;
|
||||||
listingsError = null;
|
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';
|
listingsError = 'Failed to load listings';
|
||||||
} else {
|
} else {
|
||||||
listings = result.data || [];
|
listings = listingsResult.data || [];
|
||||||
sortListings();
|
sortListings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Oil prices failure is non-critical - just show empty
|
||||||
|
oilPrices = oilPricesResult.data || [];
|
||||||
|
|
||||||
listingsLoading = false;
|
listingsLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,114 +120,175 @@
|
|||||||
<a href="/{stateSlug}" class="btn btn-primary mt-6">Back to {getStateName(countyData.state)}</a>
|
<a href="/{stateSlug}" class="btn btn-primary mt-6">Back to {getStateName(countyData.state)}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Listings Table -->
|
{#if listingsLoading}
|
||||||
<div class="mt-8">
|
<div class="flex justify-center mt-8">
|
||||||
{#if listingsLoading}
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
<div class="flex justify-center">
|
</div>
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
{:else if listingsError}
|
||||||
</div>
|
<div class="alert alert-error mt-8">
|
||||||
{:else if listingsError}
|
<span>{listingsError}</span>
|
||||||
<div class="alert alert-error">
|
</div>
|
||||||
<span>{listingsError}</span>
|
{:else}
|
||||||
</div>
|
<!-- Premium Dealers Section -->
|
||||||
{:else if listings.length === 0}
|
<div class="mt-8">
|
||||||
<div class="text-center py-8">
|
<h2 class="text-2xl font-bold mb-4">Premium Dealers</h2>
|
||||||
<p class="text-lg text-gray-600">No active listings found for this county.</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<!-- Sort Controls -->
|
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
|
||||||
<select class="select select-bordered select-sm" bind:value={sortColumn} on:change={sortListings}>
|
|
||||||
<option value="company_name">Company</option>
|
|
||||||
<option value="town">Town</option>
|
|
||||||
<option value="price_per_gallon">Price per Gallon</option>
|
|
||||||
<option value="bio_percent">Bio %</option>
|
|
||||||
<option value="service">Service</option>
|
|
||||||
<option value="phone">Contact</option>
|
|
||||||
<option value="last_edited">Last Updated</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-sm" on:click={() => { sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; sortListings(); }}>
|
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Desktop Table View -->
|
{#if listings.length === 0}
|
||||||
<div class="hidden md:block overflow-x-auto">
|
<div class="text-center py-6">
|
||||||
<table class="table table-zebra w-full">
|
<p class="text-gray-600">No premium dealers listed for this county yet.</p>
|
||||||
<thead>
|
</div>
|
||||||
<tr>
|
{:else}
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('company_name')}>
|
<!-- Sort Controls -->
|
||||||
Company
|
<div class="mb-4 flex flex-wrap gap-2">
|
||||||
{#if sortColumn === 'company_name'}
|
<select class="select select-bordered select-sm" bind:value={sortColumn} on:change={sortListings}>
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
<option value="company_name">Company</option>
|
||||||
{/if}
|
<option value="town">Town</option>
|
||||||
</th>
|
<option value="price_per_gallon">Price per Gallon</option>
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('town')}>
|
<option value="bio_percent">Bio %</option>
|
||||||
Town
|
<option value="service">Service</option>
|
||||||
{#if sortColumn === 'town'}
|
<option value="phone">Contact</option>
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
<option value="last_edited">Last Updated</option>
|
||||||
{/if}
|
</select>
|
||||||
</th>
|
<button class="btn btn-sm" on:click={() => { sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; sortListings(); }}>
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon')}>
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
Price per Gallon
|
</button>
|
||||||
{#if sortColumn === 'price_per_gallon'}
|
</div>
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
|
||||||
{/if}
|
<!-- Desktop Table View -->
|
||||||
</th>
|
<div class="hidden md:block overflow-x-auto">
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('bio_percent')}>
|
<table class="table table-zebra w-full">
|
||||||
Bio %
|
<thead>
|
||||||
{#if sortColumn === 'bio_percent'}
|
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
|
||||||
{/if}
|
|
||||||
</th>
|
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('service')}>
|
|
||||||
Service
|
|
||||||
{#if sortColumn === 'service'}
|
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
|
||||||
{/if}
|
|
||||||
</th>
|
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('phone')}>
|
|
||||||
Contact
|
|
||||||
{#if sortColumn === 'phone'}
|
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
|
||||||
{/if}
|
|
||||||
</th>
|
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('last_edited')}>
|
|
||||||
Last Updated
|
|
||||||
{#if sortColumn === 'last_edited'}
|
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
|
||||||
{/if}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each listings as listing}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>{listing.company_name}</td>
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('company_name')}>
|
||||||
<td>{listing.town || 'N/A'}</td>
|
Company
|
||||||
<td>
|
{#if sortColumn === 'company_name'}
|
||||||
<div class="text-sm">
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
<div><strong>Card:</strong> ${listing.price_per_gallon.toFixed(2)}</div>
|
|
||||||
<div><strong>Cash:</strong> {listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{listing.bio_percent}%</td>
|
|
||||||
<td>
|
|
||||||
{#if listing.service}
|
|
||||||
<span class="badge badge-success">Yes</span>
|
|
||||||
{:else}
|
|
||||||
<span class="badge badge-neutral">No</span>
|
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</th>
|
||||||
<td>
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('town')}>
|
||||||
<div class="text-sm">
|
Town
|
||||||
{#if listing.phone}
|
{#if sortColumn === 'town'}
|
||||||
<div><strong>Phone:</strong> <a href="tel:{listing.phone}" class="text-blue-600 hover:underline">{listing.phone}</a></div>
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
|
{/if}
|
||||||
|
</th>
|
||||||
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon')}>
|
||||||
|
Price per Gallon
|
||||||
|
{#if sortColumn === 'price_per_gallon'}
|
||||||
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
|
{/if}
|
||||||
|
</th>
|
||||||
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('bio_percent')}>
|
||||||
|
Bio %
|
||||||
|
{#if sortColumn === 'bio_percent'}
|
||||||
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
|
{/if}
|
||||||
|
</th>
|
||||||
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('service')}>
|
||||||
|
Service
|
||||||
|
{#if sortColumn === 'service'}
|
||||||
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
|
{/if}
|
||||||
|
</th>
|
||||||
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('phone')}>
|
||||||
|
Contact
|
||||||
|
{#if sortColumn === 'phone'}
|
||||||
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
|
{/if}
|
||||||
|
</th>
|
||||||
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('last_edited')}>
|
||||||
|
Last Updated
|
||||||
|
{#if sortColumn === 'last_edited'}
|
||||||
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
|
{/if}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each listings as listing}
|
||||||
|
<tr>
|
||||||
|
<td>{listing.company_name}</td>
|
||||||
|
<td>{listing.town || 'N/A'}</td>
|
||||||
|
<td>
|
||||||
|
<div class="text-sm">
|
||||||
|
<div><strong>Card:</strong> ${listing.price_per_gallon.toFixed(2)}</div>
|
||||||
|
<div><strong>Cash:</strong> {listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{listing.bio_percent}%</td>
|
||||||
|
<td>
|
||||||
|
{#if listing.service}
|
||||||
|
<span class="badge badge-success">Yes</span>
|
||||||
{:else}
|
{:else}
|
||||||
<div><strong>Phone:</strong> N/A</div>
|
<span class="badge badge-neutral">No</span>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
</td>
|
||||||
<strong>Online:</strong>
|
<td>
|
||||||
|
<div class="text-sm">
|
||||||
|
{#if listing.phone}
|
||||||
|
<div><strong>Phone:</strong> <a href="tel:{listing.phone}" class="text-blue-600 hover:underline">{listing.phone}</a></div>
|
||||||
|
{:else}
|
||||||
|
<div><strong>Phone:</strong> N/A</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<strong>Online:</strong>
|
||||||
|
{#if listing.online_ordering === 'none'}
|
||||||
|
No
|
||||||
|
{:else if listing.online_ordering === 'online_only'}
|
||||||
|
Online Only
|
||||||
|
{:else if listing.online_ordering === 'both'}
|
||||||
|
Both
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{listing.last_edited ? new Date(listing.last_edited).toLocaleDateString() : 'N/A'}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Card View -->
|
||||||
|
<div class="block md:hidden space-y-4">
|
||||||
|
{#each listings as listing}
|
||||||
|
<div class="card bg-base-100 shadow-lg">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3 class="card-title text-lg font-bold">
|
||||||
|
{listing.company_name}
|
||||||
|
{#if listing.town}
|
||||||
|
<br><small class="text-gray-500 font-normal">{listing.town}</small>
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Card Price:</span><br>
|
||||||
|
${listing.price_per_gallon.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Cash Price:</span><br>
|
||||||
|
{listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Bio %:</span><br>
|
||||||
|
{listing.bio_percent}%
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Service:</span><br>
|
||||||
|
{#if listing.service}
|
||||||
|
<span class="badge badge-success badge-sm">Yes</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-neutral badge-sm">No</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Contact:</span><br>
|
||||||
|
{#if listing.phone}
|
||||||
|
<a href="tel:{listing.phone}" class="text-blue-600 hover:underline text-sm">{listing.phone}</a><br>
|
||||||
|
{:else}
|
||||||
|
N/A<br>
|
||||||
|
{/if}
|
||||||
|
<small>
|
||||||
|
Online:
|
||||||
{#if listing.online_ordering === 'none'}
|
{#if listing.online_ordering === 'none'}
|
||||||
No
|
No
|
||||||
{:else if listing.online_ordering === 'online_only'}
|
{:else if listing.online_ordering === 'online_only'}
|
||||||
@@ -228,77 +296,73 @@
|
|||||||
{:else if listing.online_ordering === 'both'}
|
{:else if listing.online_ordering === 'both'}
|
||||||
Both
|
Both
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Last Updated:</span><br>
|
||||||
|
<small>{listing.last_edited ? new Date(listing.last_edited).toLocaleDateString() : 'N/A'}</small>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
|
||||||
<td>{listing.last_edited ? new Date(listing.last_edited).toLocaleDateString() : 'N/A'}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile Card View -->
|
|
||||||
<div class="block md:hidden space-y-4">
|
|
||||||
{#each listings as listing}
|
|
||||||
<div class="card bg-base-100 shadow-lg">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<h3 class="card-title text-lg font-bold">
|
|
||||||
{listing.company_name}
|
|
||||||
{#if listing.town}
|
|
||||||
<br><small class="text-gray-500 font-normal">{listing.town}</small>
|
|
||||||
{/if}
|
|
||||||
</h3>
|
|
||||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold">Card Price:</span><br>
|
|
||||||
${listing.price_per_gallon.toFixed(2)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold">Cash Price:</span><br>
|
|
||||||
{listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold">Bio %:</span><br>
|
|
||||||
{listing.bio_percent}%
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold">Service:</span><br>
|
|
||||||
{#if listing.service}
|
|
||||||
<span class="badge badge-success badge-sm">Yes</span>
|
|
||||||
{:else}
|
|
||||||
<span class="badge badge-neutral badge-sm">No</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold">Contact:</span><br>
|
|
||||||
{#if listing.phone}
|
|
||||||
<a href="tel:{listing.phone}" class="text-blue-600 hover:underline text-sm">{listing.phone}</a><br>
|
|
||||||
{:else}
|
|
||||||
N/A<br>
|
|
||||||
{/if}
|
|
||||||
<small>
|
|
||||||
Online:
|
|
||||||
{#if listing.online_ordering === 'none'}
|
|
||||||
No
|
|
||||||
{:else if listing.online_ordering === 'online_only'}
|
|
||||||
Online Only
|
|
||||||
{:else if listing.online_ordering === 'both'}
|
|
||||||
Both
|
|
||||||
{/if}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="font-semibold">Last Updated:</span><br>
|
|
||||||
<small>{listing.last_edited ? new Date(listing.last_edited).toLocaleDateString() : 'N/A'}</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
</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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
<!-- 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}
|
{:else if error}
|
||||||
<div class="text-center py-10">
|
<div class="text-center py-10">
|
||||||
<h1 class="text-3xl font-bold text-error">County Not Found</h1>
|
<h1 class="text-3xl font-bold text-error">County Not Found</h1>
|
||||||
|
|||||||
Reference in New Issue
Block a user