From 7ac2c7c59ecec6038fcec8f07a12feb3b3754cc9 Mon Sep 17 00:00:00 2001 From: Edwin Eames Date: Fri, 6 Mar 2026 11:34:31 -0500 Subject: [PATCH] feat: add admin panel, stats API, and url support across frontend - Add adminApi with full CRUD for users, companies, listings, oil-prices - Add statsApi for fetching latest market price aggregates - Add AdminTable component and /admin page for site management - Add StatsPrice, UpdateUserRequest, UpdateOilPriceRequest types - Add url field support in listing create/edit forms - Update state SVG data for all 6 New England states - Update county page to display richer listing info (phone, url, bio%) - Misc layout and style updates across vendor and public routes Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 6 +- src/lib/api/client.ts | 90 ++++++- src/lib/api/index.ts | 0 src/lib/api/types.ts | 26 +- src/lib/components/AdminTable.svelte | 100 ++++++++ src/lib/states.ts | 23 +- src/lib/states/connecticut.ts | 9 +- src/lib/states/maine.ts | 5 +- src/lib/states/massachusetts.ts | 4 + src/lib/states/newhampshire.ts | 5 +- src/lib/states/rhodeisland.ts | 5 +- src/lib/states/vermont.ts | 5 +- src/routes/(app)/+layout.svelte | 7 +- src/routes/(app)/+page.svelte | 36 ++- src/routes/(app)/[stateSlug]/+page.svelte | 30 ++- .../[stateSlug]/[countySlug]/+page.svelte | 206 +++++++++++++-- src/routes/(app)/admin/+page.svelte | 241 ++++++++++++++++++ src/routes/(app)/login/+page.svelte | 24 +- src/routes/(app)/register/+page.svelte | 26 +- src/routes/(app)/vendor/+layout.svelte | 0 src/routes/(app)/vendor/+page.svelte | 4 +- src/routes/(app)/vendor/listing/+page.svelte | 21 +- .../(app)/vendor/listing/[id]/+page.svelte | 21 +- src/routes/(app)/vendor/profile/+page.svelte | 0 src/routes/+error.svelte | 0 .../appspecific/com.chrome.devtools.json | 0 26 files changed, 808 insertions(+), 86 deletions(-) mode change 100644 => 100755 src/lib/api/client.ts mode change 100644 => 100755 src/lib/api/index.ts mode change 100644 => 100755 src/lib/api/types.ts create mode 100755 src/lib/components/AdminTable.svelte mode change 100644 => 100755 src/lib/states/newhampshire.ts mode change 100644 => 100755 src/routes/(app)/[stateSlug]/[countySlug]/+page.svelte create mode 100755 src/routes/(app)/admin/+page.svelte mode change 100644 => 100755 src/routes/(app)/vendor/+layout.svelte mode change 100644 => 100755 src/routes/(app)/vendor/+page.svelte mode change 100644 => 100755 src/routes/(app)/vendor/listing/+page.svelte mode change 100644 => 100755 src/routes/(app)/vendor/listing/[id]/+page.svelte mode change 100644 => 100755 src/routes/(app)/vendor/profile/+page.svelte mode change 100644 => 100755 src/routes/+error.svelte mode change 100644 => 100755 static/.well-known/appspecific/com.chrome.devtools.json diff --git a/package-lock.json b/package-lock.json index 552c7dc..4c0154a 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1255,9 +1255,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001664", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", - "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "dev": true, "funding": [ { diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts old mode 100644 new mode 100755 index 7480d13..d9fefad --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -14,7 +14,10 @@ import type { UpdateListingRequest, OilPrice, County, - ServiceCategory + ServiceCategory, + StatsPrice, + UpdateUserRequest, + UpdateOilPriceRequest } from './types'; /** @@ -297,6 +300,18 @@ export const stateApi = { } }; +/** + * Stats API methods + */ +export const statsApi = { + /** + * Get latest market stats + */ + async getLatest(): Promise> { + return request('/stats'); + } +}; + /** * Categories API methods */ @@ -309,6 +324,75 @@ export const categoriesApi = { } }; +/** + * Admin API methods + */ +export const adminApi = { + // Users + async getUsers(): Promise> { + return request('/admin/users', {}, true); + }, + async updateUser(id: number, data: UpdateUserRequest): Promise> { + return request(`/admin/users/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }, true); + }, + async deleteUser(id: number): Promise> { + return request(`/admin/users/${id}`, { + method: 'DELETE', + }, true); + }, + + // Companies + async getCompanies(): Promise> { + return request('/admin/companies', {}, true); + }, + async updateCompany(id: number, data: UpdateCompanyRequest): Promise> { + return request(`/admin/companies/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }, true); + }, + async deleteCompany(id: number): Promise> { + return request(`/admin/companies/${id}`, { + method: 'DELETE', + }, true); + }, + + // Listings + async getListings(): Promise> { + return request('/admin/listings', {}, true); + }, + async updateListing(id: number, data: UpdateListingRequest): Promise> { + return request(`/admin/listings/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }, true); + }, + async deleteListing(id: number): Promise> { + return request(`/admin/listings/${id}`, { + method: 'DELETE', + }, true); + }, + + // Oil Prices + async getOilPrices(): Promise> { + return request('/admin/oil-prices', {}, true); + }, + async updateOilPrice(id: number, data: UpdateOilPriceRequest): Promise> { + return request(`/admin/oil-prices/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }, true); + }, + async deleteOilPrice(id: number): Promise> { + return request(`/admin/oil-prices/${id}`, { + method: 'DELETE', + }, true); + } +}; + /** * Unified API object for convenient imports */ @@ -319,5 +403,7 @@ export const api = { listings: listingsApi, oilPrices: oilPricesApi, state: stateApi, - categories: categoriesApi + categories: categoriesApi, + stats: statsApi, + admin: adminApi }; diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts old mode 100644 new mode 100755 diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts old mode 100644 new mode 100755 index 20cb421..b7fa604 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -5,7 +5,7 @@ * Success: { data: T, error: null } * Error: { data: null, error: string } */ -export type ApiResponse = +export type ApiResponse = | { data: T; error: null } | { data: null; error: string }; @@ -77,6 +77,7 @@ export interface Listing { online_ordering: string; county_id: number; town: string | null; + url: string | null; user_id: number; created_at?: string; last_edited?: string; @@ -95,6 +96,7 @@ export interface CreateListingRequest { online_ordering?: string; county_id: number; town?: string | null; + url?: string | null; } export type UpdateListingRequest = Partial; @@ -109,6 +111,8 @@ export interface OilPrice { date: string | null; scrapetimestamp: string | null; county_id: number | null; + phone: string | null; + url: string | null; } // State/County Types @@ -126,3 +130,23 @@ export interface ServiceCategory { clicks_total: number; total_companies: number; } + +export interface StatsPrice { + id: number; + state: string; + price: number; + created_at?: string; +} +export interface UpdateUserRequest { + username?: string; + email?: string; + owner?: number | null; +} + +export interface UpdateOilPriceRequest { + price?: number; + name?: string; + url?: string; + phone?: string; + +} diff --git a/src/lib/components/AdminTable.svelte b/src/lib/components/AdminTable.svelte new file mode 100755 index 0000000..a18f960 --- /dev/null +++ b/src/lib/components/AdminTable.svelte @@ -0,0 +1,100 @@ + + +
+ + + + {#each columns as col} + + {/each} + + + + + {#each data as item (item.id)} + {@const isEditing = editingId === item.id} + + {#each columns as col} + + {/each} + + + {/each} + +
{col.label}Actions
+ {#if isEditing && col.editable} + {#if col.type === 'boolean'} + + {:else if col.type === 'number'} + + {:else} + + {/if} + {:else} + {#if col.type === 'boolean'} + + {:else} +
+ {item[col.key] ?? ''} +
+ {/if} + {/if} +
+ {#if isEditing} + + + {:else} + + + {/if} +
+
diff --git a/src/lib/states.ts b/src/lib/states.ts index 1c2dd98..f0b136d 100755 --- a/src/lib/states.ts +++ b/src/lib/states.ts @@ -1,11 +1,11 @@ // src/lib/states.ts import { writable } from 'svelte/store'; -import { massachusettsCounties } from './states/massachusetts'; -import { maineCounties } from './states/maine'; -import { vermontCounties } from './states/vermont'; -import { newHampshireCounties } from './states/newhampshire'; -import { rhodeIslandCounties } from './states/rhodeisland'; -import { connecticutCounties } from './states/connecticut'; +import { massachusettsCounties, mapViewBox as maMapViewBox } from './states/massachusetts'; +import { maineCounties, mapViewBox as maineMapViewBox } from './states/maine'; +import { vermontCounties, mapViewBox as vermontMapViewBox } from './states/vermont'; +import { newHampshireCounties, mapViewBox as nhMapViewBox } from './states/newhampshire'; +import { rhodeIslandCounties, mapViewBox as riMapViewBox } from './states/rhodeisland'; +import { connecticutCounties, mapViewBox as ctMapViewBox } from './states/connecticut'; export interface NewEnglandState { id: string; @@ -96,6 +96,17 @@ export const newEnglandStates: NewEnglandState[] = [ export const mapViewBox: string = "890 20 95 155"; +// CRIT-013: Per-state county map viewBoxes (tighter crop = bigger on mobile) +// States not listed here use the default "0 0 1000 600" +export const countyMapViewBox: Record = { + VT: vermontMapViewBox, + NH: nhMapViewBox, + ME: maineMapViewBox, + RI: riMapViewBox, + CT: ctMapViewBox, + MA: maMapViewBox, +}; + /** * SVG transform string to apply to the group of states to straighten the map. * Format: "rotate(angle cx cy)" diff --git a/src/lib/states/connecticut.ts b/src/lib/states/connecticut.ts index 95da3fc..067eeff 100755 --- a/src/lib/states/connecticut.ts +++ b/src/lib/states/connecticut.ts @@ -15,7 +15,7 @@ export const connecticutCounties: NewEnglandState[] = [ name: 'Fairfield County', slug: 'fairfield', image: '/images/counties/ct/fairfield.png', - pathD: "M 776.1 12.71 793.94 13.63 902.92 15.76 903.51 24.54 904.53 69.52 904.84 83.14 905.65 118.93 905.65 119.47 906.23 137.24 906.24 138.18 907.54 184.57 907.54 184.67 907.63 232.28 838 235.22 792.97 225.41 775.53 221.43 774.63 221.3 768.46 219.95 754.55 217.86 752.8 222.37 740.56 211.87 718.98 191.09 718.31 190.51 753.32 168.36 753.08 168.18 749.85 122.43 727.49 122.27 727.8 52.78 755.28 52.13 766.69 52.28 767.07 52.32 777.33 52.33 776.1 12.71 Z", + pathD: "M 180.79 217.29 181.4 217.68 181.92 217.72 183.45 217.38 183.77 217.38 185.22 217.77 212.76 315.89 258.98 326.35 261 328.12 266.85 329.3 267.86 328.51 311.09 355.6 320.69 363.59 319.39 368.64 321.36 371.77 325.53 376.32 330.62 376.18 336.38 385.23 341.22 394.48 349.76 405.12 360.2 412.37 362 415.04 365.24 416.66 365.79 418.85 366.75 420.02 369.91 422.6 404.72 459.89 404.23 460.37 399.31 466.41 396.92 470.55 395.27 473.4 392.78 476.04 392.56 476.27 392.16 476.7 389.97 476.81 389.96 476.73 389.95 476.62 389.95 476.55 389.93 476.36 389.85 475.52 388.96 475.08 388.02 474.94 379.27 473.7 377.14 473.4 375.34 474.37 373.48 475.36 365.8 481.8 365.03 482.45 360.08 487.96 359.75 488.32 359.69 488.39 354.56 494.1 354.08 494.64 354.07 494.64 353.03 496.57 352.03 501.62 352.2 503.13 352.87 504.96 351.74 506.6 343.66 508.83 329.31 502.26 326.91 501.16 326.65 498.69 323.67 497.71 313.21 502.52 302.42 511.17 299.58 510.4 294.15 520.22 288.09 525.19 277.83 519.39 273.79 520.62 267.27 525.86 259.36 529.38 241.85 532.72 233.5 541.83 230.13 542.61 224.45 549.88 215.45 559.12 209.01 558.9 201.5 562.11 197.66 570.67 190.94 563.88 181.03 569.14 181.08 574.29 178.6 579.98 175.99 581.6 175.03 578.52 175.44 574.7 174.35 573.08 173.27 572.94 166.48 577.17 162.07 581.37 159.67 584.66 158.66 589.85 152.82 590.24 152.39 584.86 147.88 581.82 144.41 582.35 127.79 589.53 124.55 593.4 121.96 599 123.08 584.22 122.78 583.6 116.43 573.96 112.44 567.43 109.41 562.47 106.42 557.67 92.33 534.57 129.39 511.79 132.55 509.88 140.01 505.36 181.99 479.98 184.15 478.65 195.41 471.97 166.7 425.72 169.32 385.97 169.78 380.14 172.29 348.11 172.38 346.99 172.59 344.07 172.97 338.33 173 337.36 173.04 336.14 173.05 335.87 173.09 335.06 173.68 324.54 173.72 324.26 173.72 323.97 173.77 323.33 173.81 322.82 173.85 322.41 175.49 295.73 179.11 245.56 180.79 217.29 Z", fill: 'fill-green-500', hoverFill: 'fill-green-700' }, @@ -78,13 +78,16 @@ export const connecticutCounties: NewEnglandState[] = [ name: 'Windham County', slug: 'windham', image: '/images/counties/ct/windham.png', - pathD: "M 180.79 217.29 181.4 217.68 181.92 217.72 183.45 217.38 183.77 217.38 185.22 217.77 212.76 315.89 258.98 326.35 261 328.12 266.85 329.3 267.86 328.51 311.09 355.6 320.69 363.59 319.39 368.64 321.36 371.77 325.53 376.32 330.62 376.18 336.38 385.23 341.22 394.48 349.76 405.12 360.2 412.37 362 415.04 365.24 416.66 365.79 418.85 366.75 420.02 369.91 422.6 404.72 459.89 404.23 460.37 399.31 466.41 396.92 470.55 395.27 473.4 392.78 476.04 392.56 476.27 392.16 476.7 389.97 476.81 389.96 476.73 389.95 476.62 389.95 476.55 389.93 476.36 389.85 475.52 388.96 475.08 388.02 474.94 379.27 473.7 377.14 473.4 375.34 474.37 373.48 475.36 365.8 481.8 365.03 482.45 360.08 487.96 359.75 488.32 359.69 488.39 354.56 494.1 354.08 494.64 354.07 494.64 353.03 496.57 352.03 501.62 352.2 503.13 352.87 504.96 351.74 506.6 343.66 508.83 329.31 502.26 326.91 501.16 326.65 498.69 323.67 497.71 313.21 502.52 302.42 511.17 299.58 510.4 294.15 520.22 288.09 525.19 277.83 519.39 273.79 520.62 267.27 525.86 259.36 529.38 241.85 532.72 233.5 541.83 230.13 542.61 224.45 549.88 215.45 559.12 209.01 558.9 201.5 562.11 197.66 570.67 190.94 563.88 181.03 569.14 181.08 574.29 178.6 579.98 175.99 581.6 175.03 578.52 175.44 574.7 174.35 573.08 173.27 572.94 166.48 577.17 162.07 581.37 159.67 584.66 158.66 589.85 152.82 590.24 152.39 584.86 147.88 581.82 144.41 582.35 127.79 589.53 124.55 593.4 121.96 599 123.08 584.22 122.78 583.6 116.43 573.96 112.44 567.43 109.41 562.47 106.42 557.67 92.33 534.57 129.39 511.79 132.55 509.88 140.01 505.36 181.99 479.98 184.15 478.65 195.41 471.97 166.7 425.72 169.32 385.97 169.78 380.14 172.29 348.11 172.38 346.99 172.59 344.07 172.97 338.33 173 337.36 173.04 336.14 173.05 335.87 173.09 335.06 173.68 324.54 173.72 324.26 173.72 323.97 173.77 323.33 173.81 322.82 173.85 322.41 175.49 295.73 179.11 245.56 180.79 217.29 Z", + pathD: "M 776.1 12.71 793.94 13.63 902.92 15.76 903.51 24.54 904.53 69.52 904.84 83.14 905.65 118.93 905.65 119.47 906.23 137.24 906.24 138.18 907.54 184.57 907.54 184.67 907.63 232.28 838 235.22 792.97 225.41 775.53 221.43 774.63 221.3 768.46 219.95 754.55 217.86 752.8 222.37 740.56 211.87 718.98 191.09 718.31 190.51 753.32 168.36 753.08 168.18 749.85 122.43 727.49 122.27 727.8 52.78 755.28 52.13 766.69 52.28 767.07 52.32 777.33 52.33 776.1 12.71 Z", fill: 'fill-green-500', hoverFill: 'fill-green-700' } ]; -export const mapViewBox: string = "890 20 95 155"; +// CRIT-013: Old viewBox was a copy-paste from the home page map and not used for county view +// export const mapViewBox: string = "890 20 95 155"; +// New viewBox: tight crop around CT county paths (x:92-908, y:1-599) + 25px padding +export const mapViewBox: string = "65 -25 870 650"; /** * SVG transform string to apply to the group of states to straighten the map. diff --git a/src/lib/states/maine.ts b/src/lib/states/maine.ts index b47205e..03468f5 100755 --- a/src/lib/states/maine.ts +++ b/src/lib/states/maine.ts @@ -157,6 +157,9 @@ export const maineCounties: NewEnglandState[] = [ } ]; -export const mapViewBox: string = "0 0 1000 600"; +// CRIT-013: Old viewBox used full 1000x600 with lots of dead space +// export const mapViewBox: string = "0 0 1000 600"; +// New viewBox: tight crop around Maine county paths (x:313-687, y:18-582) + 25px padding +export const mapViewBox: string = "285 -10 430 620"; export const mapTransform: string | undefined = undefined; diff --git a/src/lib/states/massachusetts.ts b/src/lib/states/massachusetts.ts index ed69134..e7775ef 100755 --- a/src/lib/states/massachusetts.ts +++ b/src/lib/states/massachusetts.ts @@ -138,3 +138,7 @@ export const massachusettsCounties: NewEnglandState[] = [ } ]; + +// CRIT-013: MA paths already fill most of 1000x600 (x:18-982, y:1-599), so gain is small (~2%) +// but applied for consistency across all 6 states +export const mapViewBox: string = "8 -10 985 620"; diff --git a/src/lib/states/newhampshire.ts b/src/lib/states/newhampshire.ts old mode 100644 new mode 100755 index c38899f..808e7f9 --- a/src/lib/states/newhampshire.ts +++ b/src/lib/states/newhampshire.ts @@ -102,7 +102,10 @@ export const newHampshireCounties: NewEnglandState[] = [ } ]; -export const mapViewBox: string = "890 20 95 155"; +// CRIT-013: Old viewBox was a copy-paste from the home page map and not used for county view +// export const mapViewBox: string = "890 20 95 155"; +// New viewBox: tight crop around NH county paths (x:347-653, y:1-599) + 25px padding +export const mapViewBox: string = "320 -25 360 650"; /** * SVG transform string to apply to the group of states to straighten the map. diff --git a/src/lib/states/rhodeisland.ts b/src/lib/states/rhodeisland.ts index 5ca0a59..9ce4931 100755 --- a/src/lib/states/rhodeisland.ts +++ b/src/lib/states/rhodeisland.ts @@ -58,7 +58,10 @@ export const rhodeIslandCounties: NewEnglandState[] = [ ]; -export const mapViewBox: string = "890 20 95 155"; +// CRIT-013: Old viewBox was a copy-paste from the home page map and not used for county view +// export const mapViewBox: string = "890 20 95 155"; +// New viewBox: tight crop around RI county paths (x:310-690, y:1-599) + 25px padding +export const mapViewBox: string = "285 -25 430 650"; /** * SVG transform string to apply to the group of states to straighten the map. diff --git a/src/lib/states/vermont.ts b/src/lib/states/vermont.ts index 3b1579c..e581570 100755 --- a/src/lib/states/vermont.ts +++ b/src/lib/states/vermont.ts @@ -138,7 +138,10 @@ export const vermontCounties: NewEnglandState[] = [ } ]; -export const mapViewBox: string = "890 20 95 155"; +// CRIT-013: Old viewBox was a copy-paste from the home page map and not used for county view +// export const mapViewBox: string = "890 20 95 155"; +// New viewBox: tight crop around Vermont county paths (x:314-686, y:1-599) + 25px padding +export const mapViewBox: string = "290 -25 420 650"; /** * SVG transform string to apply to the group of states to straighten the map. diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 2f3ab1b..8e6661c 100755 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -77,8 +77,11 @@ -