added listings

This commit is contained in:
2025-12-26 20:01:28 -05:00
parent 06a5ff98d7
commit f08432e417
17 changed files with 2200 additions and 101 deletions

40
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@popperjs/core": "^2.11.8",
"lucide-svelte": "^0.544.0",
"tailwind-merge": "^1.14.0"
},
"devDependencies": {
@@ -45,7 +46,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
@@ -443,7 +443,6 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@@ -457,7 +456,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -466,7 +464,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -474,14 +471,12 @@
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -1037,8 +1032,7 @@
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
"node_modules/@types/resolve": {
"version": "1.20.2",
@@ -1050,7 +1044,6 @@
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1132,7 +1125,6 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -1178,7 +1170,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -1302,7 +1293,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
"integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15",
"@types/estree": "^1.0.1",
@@ -1315,7 +1305,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -1390,7 +1379,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dev": true,
"dependencies": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
@@ -1903,8 +1891,7 @@
"node_modules/locate-character": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"dev": true
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
},
"node_modules/lru-cache": {
"version": "10.4.3",
@@ -1912,11 +1899,18 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
},
"node_modules/lucide-svelte": {
"version": "0.544.0",
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.544.0.tgz",
"integrity": "sha512-8kBxSivf8SJdEUJRHBpu9bRw0S/qfVK+Yfb92KQnRRBdP425RzT6aQfrIfZctG1oucPVTBQe1ZXgmth/3qVICg==",
"peerDependencies": {
"svelte": "^3 || ^4 || ^5.0.0-next.42"
}
},
"node_modules/magic-string": {
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
@@ -1924,8 +1918,7 @@
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"dev": true
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
},
"node_modules/merge2": {
"version": "1.4.1",
@@ -2131,7 +2124,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
"integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^3.0.0",
@@ -2142,7 +2134,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -2151,7 +2142,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
"dev": true,
"dependencies": {
"@types/estree": "*"
}
@@ -2533,7 +2523,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2678,7 +2667,6 @@
"version": "4.2.19",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
"integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
@@ -2756,7 +2744,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -2765,7 +2752,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
"dev": true,
"dependencies": {
"@types/estree": "*"
}

View File

@@ -29,6 +29,7 @@
"type": "module",
"dependencies": {
"@popperjs/core": "^2.11.8",
"lucide-svelte": "^0.544.0",
"tailwind-merge": "^1.14.0"
}
}

View File

@@ -19,3 +19,7 @@
.text-orange-oil {
color: #ff6600;
}
.btn-state {
@apply btn btn-outline btn-secondary btn-xs px-1 py-1 text-xs leading-none;
}

View File

@@ -18,3 +18,25 @@
username: string;
password: string;
}
export interface User {
id: number;
username: string;
email: string;
// Add other fields as needed
}
export interface Company {
id: number;
active: boolean;
created: string; // Assuming ISO date string
name: string;
address?: string | null;
town?: string | null;
state?: string | null;
phone?: number | null;
owner_name?: string | null;
owner_phone_number?: number | null;
email?: string | null;
user_id?: number | null;
}

View File

@@ -3,12 +3,17 @@
import { writable } from "svelte/store";
import type { Writable } from 'svelte/store';
import '../../app.postcss'; // Import Tailwind CSS
import { user } from '$lib/states';
import { user, darkMode, type User } from '$lib/states';
// Define the type for the user store
interface User {
username: string;
// Initialize dark mode on mount to ensure data-theme is set
onMount(() => {
const isDark = $darkMode;
if (isDark) {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
});
// Placeholder for user store - in a real app, this would be managed by an auth library or context
let storedUser: User | null = null;
@@ -36,14 +41,32 @@
localStorage.removeItem('user');
window.location.href = '/';
};
// Toggle dark mode
const toggleDarkMode = () => {
darkMode.update((value: boolean) => !value);
};
</script>
<div class="min-h-screen bg-white flex flex-col">
<div class="min-h-screen flex flex-col">
<header class="navbar bg-primary text-primary-content shadow-lg">
<div class="flex-1">
<a href="/" class="btn btn-ghost normal-case text-xl">Heating Price Hero</a>
<a href="/" class="btn btn-ghost normal-case text-xl">Biz Hero</a>
</div>
<div class="flex-none">
<div class="flex-none flex items-center gap-2">
<button type="button" class="btn btn-ghost" on:click={toggleDarkMode}>
{#if !$darkMode}
<!-- Moon icon for dark mode -->
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" clip-rule="evenodd" />
</svg>
{:else}
<!-- Sun icon for light mode -->
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd" />
</svg>
{/if}
</button>
{#if $user}
<div class="dropdown dropdown-end">
<button type="button" class="btn btn-ghost normal-case text-lg">

View File

@@ -3,6 +3,45 @@
import { newEnglandStates, mapViewBox } from '$lib/states';
import { goto } from '$app/navigation';
import { browser } from '$app/environment'; // To ensure SVG interactions only run client-side
import { onMount } from 'svelte';
import {
Scissors,
Snowflake,
Hammer,
Wrench,
Zap,
Bug,
Home,
Sparkles,
Eye,
Trash2,
Palette,
TreePine,
Fence,
Waves,
Camera,
Lock,
Refrigerator,
DoorOpen,
Eye as InspectionIcon,
GlassWater,
Shield,
Flame,
Square,
Grid,
Fan,
Clock,
Construction,
ExternalLink
} from 'lucide-svelte';
interface ServiceCategory {
id: number;
name: string;
description: string;
clicks_total: number;
total_companies: number;
}
let hoveredState: string | null = null;
@@ -23,8 +62,9 @@
}
</script>
<div class="text-center mb-8">
<div class="text-center mb-8">
<p class="text-lg">Click your state to find prices.</p>
</div>
@@ -67,7 +107,10 @@
<ul class="flex flex-wrap justify-center gap-4">
{#each newEnglandStates as state}
<li>
<a href="/{state.id}" class="btn btn-outline btn-secondary">{state.name}</a>
<a href="/{state.id}" class="text-blue-600 hover:text-blue-800 underline flex items-center gap-1">
<span>{state.name}</span>
<ExternalLink size={12} />
</a>
</li>
{/each}
</ul>

View File

@@ -0,0 +1,167 @@
<!-- Catch-all route for state and county pages: /[stateSlug] and /[stateSlug]/[countyId] -->
<script lang="ts">
import { newEnglandStates, type NewEnglandState } from '$lib/states';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import { onMount } from 'svelte';
interface County {
id: number;
name: string;
state: string;
}
const { slugs } = $page.params as { slugs: string };
let currentState: NewEnglandState | null = null;
let counties: County[] = [];
let currentCounty: County | null = null;
let loading = false;
let error: string | null = null;
let hoveredState: string | null = null;
// Parse the URL slugs: /stateSlug(/countyId)
$: {
if (slugs) {
const slugParts = slugs.split('/');
// First part is state slug: /MA(/123)
if (slugParts.length >= 1) {
const stateSlug = slugParts[0];
currentState = newEnglandStates.find(s => s.id === stateSlug) || null;
// Second part is county ID: /MA/123
if (slugParts.length >= 2 && !isNaN(Number(slugParts[1]))) {
const countyId = Number(slugParts[1]);
currentCounty = counties.find(c => c.id === countyId) || null;
}
}
}
}
onMount(async () => {
loading = true;
try {
// Trigger reactive parsing
if (slugs) {
const slugParts = slugs.split('/');
// If we have state data, fetch counties
if (slugParts.length >= 1) {
const stateSlug = slugParts[0];
currentState = newEnglandStates.find(s => s.id === stateSlug) || null;
if (currentState) {
const countyResponse = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}`);
if (countyResponse.ok) {
counties = await countyResponse.json();
}
// Handle county data
if (slugParts.length >= 2 && !isNaN(Number(slugParts[1]))) {
const countyId = Number(slugParts[1]);
const countyResponse = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}/${countyId}`);
if (countyResponse.ok) {
currentCounty = await countyResponse.json();
}
}
}
}
}
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load data';
console.error('Error:', err);
} finally {
loading = false;
}
});
function handleStateClick(stateId: string) {
goto(`/${stateId}`);
}
function handleCountyClick(countyId: number) {
if (currentState) {
goto(`/${currentState.id}/${countyId}`);
}
}
function handleMouseEnter(stateId: string) {
if (browser) {
hoveredState = stateId;
}
}
function handleMouseLeave() {
if (browser) {
hoveredState = null;
}
}
</script>
{#if loading}
<div class="text-center py-8">
<p>Loading...</p>
</div>
{:else if error}
<div class="text-center py-8">
<p class="text-error">Error: {error}</p>
</div>
{:else if currentCounty && currentState}
<!-- County Details Page -->
<div class="card lg:card-side bg-base-100 shadow-xl">
<div class="card-body">
<h1 class="card-title text-3xl">{currentCounty.name}, {currentState.name}</h1>
<p>This is the page for {currentCounty.name} county in {currentState.name}.</p>
<p>The URL for this page is <code class="bg-base-300 p-1 rounded">/{currentState.id}/{currentCounty.id}</code></p>
<div class="card-actions justify-end mt-4">
<a href="/{currentState.id}" class="btn btn-primary">Back to State</a>
<a href="/" class="btn btn-secondary">Back to Map</a>
</div>
</div>
</div>
{:else if currentState}
<!-- State Page with Counties -->
<div class="card lg:card-side bg-base-100 shadow-xl">
<figure class="flex-shrink-0">
<img
src={currentState.image}
alt="Map or notable feature of {currentState.id}"
class="object-cover w-full h-64 lg:h-auto lg:w-64"
/>
</figure>
<div class="card-body">
<h1 class="card-title text-3xl">{currentState.name}</h1>
<p>The URL for this page is <code class="bg-base-300 p-1 rounded">/{currentState.id}</code></p>
{#if counties.length > 0}
<div class="mt-4">
<h2 class="text-xl font-semibold mb-2">Counties:</h2>
<ul class="list-disc pl-5 max-h-48 overflow-y-auto">
{#each counties as county}
<li>
<a
href="/{currentState.id}/{county.id}"
class="text-blue-600 hover:underline"
>
{county.name}
</a>
</li>
{/each}
</ul>
</div>
{/if}
<div class="card-actions justify-end mt-4">
<a href="/" class="btn btn-primary">Back to Map</a>
</div>
</div>
</div>
{:else}
<div class="text-center py-10">
<h1 class="text-3xl font-bold text-error">Invalid URL</h1>
<p class="mt-4">The requested page could not be found.</p>
<a href="/" class="btn btn-primary mt-6">Go Back to Map</a>
</div>
{/if}

View File

@@ -3,6 +3,7 @@
import { page } from '$app/stores';
import { newEnglandStates, allCounties, type NewEnglandState } from '$lib/states'; // <--- Import type and counties
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
// The stateSlug from $page.params is guaranteed to be a string if this route matches
// because [stateSlug] is a required parameter.
@@ -18,7 +19,14 @@
if (stateData) {
loading = true;
try {
const response = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}`);
const token = localStorage.getItem('auth_token');
const headers: Record<string, string> = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}`, {
headers
});
if (!response.ok) {
throw new Error(`API call failed with status ${response.status}`);
}
@@ -35,6 +43,15 @@
</script>
{#if stateData}
<!-- Breadcrumbs -->
<nav class="breadcrumb mb-6" aria-label="Breadcrumb">
<ul class="flex space-x-2 text-sm">
<li><a href="/" class="text-blue-500 hover:underline">Home</a></li>
<li class="text-gray-500">/</li>
<li class="text-gray-700 font-medium">{stateData.name}</li>
</ul>
</nav>
<div class="card lg:card-side bg-base-100 shadow-xl">
<figure class=" ">
<img
@@ -57,7 +74,7 @@
<ul class="list-disc pl-5">
{#each filteredCounties as county}
<li>
<a href="http://localhost:9551/{stateSlug.toUpperCase()}/{county.id}" class="text-blue-600 hover:underline">{county.name}</a>
<a href="/{stateSlug}/{county.id}" class="text-blue-600 hover:underline">{county.name}</a>
</li>
{/each}
</ul>

View File

@@ -1,21 +1,55 @@
<!-- src/routes/(app)/[stateSlug]/[countySlug]/+page.svelte -->
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/stores
';
import { onMount } from 'svelte';
import { newEnglandStates } from '$lib/states';
interface Listing {
id: number;
company_name: string;
is_active: boolean;
price_per_gallon: number;
price_per_gallon_cash: number | null;
note: string | null;
minimum_order: number | null;
service: boolean;
bio_percent: number;
phone: string | null;
online_ordering: string;
county_id: number;
user_id: number;
last_edited: string;
}
const { stateSlug, countySlug } = $page.params as { stateSlug: string; countySlug: string };
let countyData: { id: number; name: string; state: string } | null = null;
let listings: Listing[] = [];
let loading = true;
let listingsLoading = false;
let error: string | null = null;
let listingsError: string | null = null;
let sortColumn = 'price_per_gallon';
let sortDirection = 'asc'; // 'asc' or 'desc' - lowest price first
onMount(async () => {
try {
// Ensure API URL matches the Docker port forwarding for API (9552)
const response = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}/${countySlug}`);
const token = localStorage.getItem('auth_token');
const headers: Record<string, string> = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}/${countySlug}`, {
headers
});
if (!response.ok) {
throw new Error(`Failed to fetch county data: ${response.statusText}`);
}
countyData = await response.json();
// Fetch listings for this county
await fetchListings();
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred while fetching county data.';
countyData = null;
@@ -23,17 +57,190 @@
loading = false;
}
});
</script>3
async function fetchListings() {
if (!countyData) return;
listingsLoading = true;
listingsError = null;
try {
const response = await fetch(`http://localhost:9552/listings/county/${countyData.id}`);
if (response.ok) {
listings = await response.json();
sortListings();
} else {
listingsError = 'Failed to load listings';
}
} catch (err) {
listingsError = 'Network error loading listings';
} finally {
listingsLoading = false;
}
}
function sortListings() {
listings = [...listings].sort((a, b) => {
let aValue = a[sortColumn as keyof Listing];
let bValue = b[sortColumn as keyof Listing];
// Handle string sorting
if (typeof aValue === 'string' && typeof bValue === 'string') {
const comparison = aValue.localeCompare(bValue);
return sortDirection === 'asc' ? comparison : -comparison;
}
// Handle number sorting
if (typeof aValue === 'number' && typeof bValue === 'number') {
return sortDirection === 'asc' ? aValue - bValue : bValue - aValue;
}
// Handle boolean sorting
if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
const aNum = aValue ? 1 : 0;
const bNum = bValue ? 1 : 0;
return sortDirection === 'asc' ? aNum - bNum : bNum - aNum;
}
return 0;
});
}
function handleSort(column: string) {
if (sortColumn === column) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortColumn = column;
sortDirection = 'asc';
}
sortListings();
}
function getStateName(stateAbbr: string): string {
const state = newEnglandStates.find(s => s.id === stateAbbr);
return state ? state.name : stateAbbr;
}
</script>
{#if loading}
<div class="text-center py-10">
<p>Loading county data...</p>
</div>
{:else if countyData}
<!-- Breadcrumbs -->
<nav class="breadcrumb mb-6" aria-label="Breadcrumb">
<ul class="flex space-x-2 text-sm">
<li><a href="/" class="text-blue-500 hover:underline">Home</a></li>
<li class="text-gray-500">/</li>
<li><a href="/{stateSlug}" class="text-blue-500 hover:underline">{getStateName(countyData.state)}</a></li>
<li class="text-gray-500">/</li>
<li class="text-gray-700 font-medium">{countyData.name}</li>
</ul>
</nav>
<div class="text-center py-10">
<h1 class="text-3xl font-bold">{countyData.name}</h1>
<p class="text-xl mt-4">County ID: {countyData.id}</p>
<a href="/{stateSlug}" class="btn btn-primary mt-6">Back to State</a>
<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">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if listingsError}
<div class="alert alert-error">
<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>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('company_name')}>
Company
{#if sortColumn === 'company_name'}
{sortDirection === 'asc' ? '↑' : '↓'}
{/if}
</th>
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon')}>
Price per Gallon Card
{#if sortColumn === 'price_per_gallon'}
{sortDirection === 'asc' ? '↑' : '↓'}
{/if}
</th>
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon_cash')}>
Price per Gallon Cash
{#if sortColumn === 'price_per_gallon_cash'}
{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('online_ordering')}>
Online Ordering
{#if sortColumn === 'online_ordering'}
{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')}>
Phone
{#if sortColumn === 'phone'}
{sortDirection === 'asc' ? '↑' : '↓'}
{/if}
</th>
</tr>
</thead>
<tbody>
{#each listings as listing}
<tr>
<td>{listing.company_name}</td>
<td>${listing.price_per_gallon.toFixed(2)}</td>
<td>{listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}</td>
<td>{listing.bio_percent}%</td>
<td>
{#if listing.online_ordering === 'none'}
No
{:else if listing.online_ordering === 'online_only'}
Online Only
{:else if listing.online_ordering === 'both'}
Both
{/if}
</td>
<td>
{#if listing.service}
<span class="badge badge-success">Yes</span>
{:else}
<span class="badge badge-neutral">No</span>
{/if}
</td>
<td>
{#if listing.phone}
<a href="tel:{listing.phone}" class="text-blue-600 hover:underline">{listing.phone}</a>
{:else}
N/A
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
{:else if error}
<div class="text-center py-10">

View File

@@ -34,7 +34,7 @@
// Assuming the backend returns a token or some success indicator
// Store the token in localStorage for authentication checks
if (data.token) {
localStorage.setItem('auth_token', data.token);
localStorage.setItem('token', data.token);
}
// Store the user object in localStorage
localStorage.setItem('user', JSON.stringify(data.user));

View File

@@ -1,12 +1,375 @@
<script lang="ts">
// Vendor page logic can be added here
import { onMount } from 'svelte';
interface Listing {
id: number;
company_name: string;
is_active: boolean;
price_per_gallon: number;
price_per_gallon_cash: number | null;
note: string | null;
minimum_order: number | null;
service: boolean;
bio_percent: number;
phone: string | null;
online_ordering: string;
county_id: number;
user_id: number;
last_edited: string;
}
let listings: Listing[] = [];
let isLoading = true;
let error = '';
let successMessage = '';
// Inline editing state
let editingId: number | null = null;
let editingField: string | null = null;
let editingValue: string = '';
async function fetchListings() {
try {
const token = localStorage.getItem('token');
const response = await fetch('http://localhost:9552/listing', {
headers: {
'Authorization': token ? `Bearer ${token}` : ''
}
});
if (response.ok) {
listings = await response.json();
} else {
error = 'Failed to load listings';
}
} catch (err) {
error = 'Network error loading listings';
} finally {
isLoading = false;
}
}
async function deleteListing(id: number) {
if (!confirm('Are you sure you want to delete this listing?')) return;
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://localhost:9552/listing/${id}`, {
method: 'DELETE',
headers: {
'Authorization': token ? `Bearer ${token}` : ''
}
});
if (response.ok) {
// Remove from local state
listings = listings.filter(listing => listing.id !== id);
} else {
alert('Failed to delete listing');
}
} catch (err) {
alert('Network error deleting listing');
}
}
function editListing(listing: Listing) {
// Navigate to the edit page for this listing
window.location.href = `/vendor/price/${listing.id}`;
}
function startEditing(listing: Listing, field: string) {
editingId = listing.id;
editingField = field;
// Set initial value based on field type
if (field === 'price_per_gallon') {
editingValue = listing.price_per_gallon.toString();
} else if (field === 'price_per_gallon_cash') {
editingValue = listing.price_per_gallon_cash?.toString() || '';
} else if (field === 'minimum_order') {
editingValue = listing.minimum_order?.toString() || '';
} else if (field === 'note') {
editingValue = listing.note || '';
}
}
function cancelEditing() {
editingId = null;
editingField = null;
editingValue = '';
}
async function saveEditing(listing: Listing) {
if (!editingField) return;
let updateData: any = {};
let newValue: any;
try {
if (editingField === 'price_per_gallon' || editingField === 'price_per_gallon_cash') {
newValue = editingValue ? parseFloat(editingValue) : null;
if (editingField === 'price_per_gallon' && (!newValue || newValue <= 0)) {
alert('Please enter a valid price greater than 0');
return;
}
updateData[editingField] = newValue;
} else if (editingField === 'minimum_order') {
newValue = editingValue ? parseInt(editingValue) : null;
if (newValue && (newValue < 1 || newValue > 200)) {
alert('Minimum order must be between 1 and 200');
return;
}
updateData[editingField] = newValue;
} else if (editingField === 'note') {
newValue = editingValue.trim() || null;
if (newValue && newValue.length > 250) {
alert('Note cannot exceed 250 characters');
return;
}
updateData[editingField] = newValue;
}
const token = localStorage.getItem('token');
const response = await fetch(`http://localhost:9552/listing/${listing.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
body: JSON.stringify(updateData)
});
if (response.ok) {
// Update local state
listings = listings.map(l =>
l.id === listing.id
? { ...l, [editingField as string]: newValue }
: l
);
cancelEditing();
successMessage = `${editingField.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} updated successfully!`;
// Clear success message after 3 seconds
setTimeout(() => {
successMessage = '';
}, 3000);
} else {
alert(`Failed to update ${editingField}`);
}
} catch (err) {
alert('Network error updating field');
}
}
onMount(() => {
fetchListings();
});
</script>
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold mb-4">Vendor Dashboard</h1>
<!-- Success Banner -->
{#if successMessage}
<div class="alert alert-success mb-4">
<span>{successMessage}</span>
</div>
{/if}
<p>Welcome to the Vendor section. Navigate to specific pages using the links below.</p>
<div class="mt-4">
<a href="/vendor/profile" class="text-blue-500 hover:underline">Profile</a>
<a href="/vendor/price" class="text-blue-500 hover:underline ml-4">Price</a>
</div>
<!-- Listings Table -->
<div class="mt-8">
<h2 class="text-2xl font-bold mb-4">Your Listings</h2>
{#if isLoading}
<div class="flex justify-center">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if error}
<div class="alert alert-error">
<span>{error}</span>
</div>
{:else if listings.length === 0}
<div class="text-center py-8">
<p class="text-lg text-gray-600">No listings found.. create one :)</p>
<a href="/vendor/price" class="btn btn-primary mt-4">Create Listing</a>
</div>
{:else}
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>Company</th>
<th>Price Card</th>
<th>Price Cash</th>
<th>Min Order</th>
<th>Bio %</th>
<th>Service</th>
<th>Phone</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each listings as listing}
<tr>
<td>{listing.company_name}</td>
<!-- Price Card (editable) -->
<td>
{#if editingId === listing.id && editingField === 'price_per_gallon'}
<div class="flex items-center gap-2">
<input
type="number"
step="0.01"
min="0"
class="input input-bordered input-sm w-20"
bind:value={editingValue}
placeholder="0.00"
/>
<button
class="btn btn-xs btn-success"
on:click={() => saveEditing(listing)}
>
Save
</button>
<button
class="btn btn-xs btn-ghost"
on:click={cancelEditing}
>
Cancel
</button>
</div>
{:else}
<div class="flex items-center gap-2">
<span>${listing.price_per_gallon.toFixed(2)}</span>
<button
class="btn btn-xs btn-outline"
on:click={() => startEditing(listing, 'price_per_gallon')}
>
✏️
</button>
</div>
{/if}
</td>
<!-- Price Cash (editable) -->
<td>
{#if editingId === listing.id && editingField === 'price_per_gallon_cash'}
<div class="flex items-center gap-2">
<input
type="number"
step="0.01"
min="0"
class="input input-bordered input-sm w-20"
bind:value={editingValue}
placeholder="Optional"
/>
<button
class="btn btn-xs btn-success"
on:click={() => saveEditing(listing)}
>
Save
</button>
<button
class="btn btn-xs btn-ghost"
on:click={cancelEditing}
>
Cancel
</button>
</div>
{:else}
<div class="flex items-center gap-2">
<span>{listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}</span>
<button
class="btn btn-xs btn-outline"
on:click={() => startEditing(listing, 'price_per_gallon_cash')}
>
✏️
</button>
</div>
{/if}
</td>
<!-- Minimum Order (editable) -->
<td>
{#if editingId === listing.id && editingField === 'minimum_order'}
<div class="flex items-center gap-2">
<input
type="number"
min="1"
max="200"
class="input input-bordered input-sm w-20"
bind:value={editingValue}
placeholder="Optional"
/>
<button
class="btn btn-xs btn-success"
on:click={() => saveEditing(listing)}
>
Save
</button>
<button
class="btn btn-xs btn-ghost"
on:click={cancelEditing}
>
Cancel
</button>
</div>
{:else}
<div class="flex items-center gap-2">
<span>{listing.minimum_order || 'N/A'}</span>
<button
class="btn btn-xs btn-outline"
on:click={() => startEditing(listing, 'minimum_order')}
>
✏️
</button>
</div>
{/if}
</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}
</td>
<td>{listing.phone || 'N/A'}</td>
<td>
{#if listing.is_active}
<span class="badge badge-success">Active</span>
{:else}
<span class="badge badge-neutral">Inactive</span>
{/if}
</td>
<td>
<div class="flex gap-2">
<button
class="btn btn-sm btn-primary"
on:click={() => editListing(listing)}
>
Edit
</button>
<button
class="btn btn-sm btn-error"
on:click={() => deleteListing(listing.id)}
>
Delete
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>

View File

@@ -1,11 +1,476 @@
<script lang="ts">
// Vendor price page logic can be added here
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { newEnglandStates } from '$lib/states';
interface County {
id: number;
name: string;
state: string;
}
// Form data
let formData = {
companyName: '',
pricePerGallon: 0,
pricePerGallonCash: null as number | null,
minimumOrder: null as number | null,
labelService: false,
bioPercent: 0,
phone: '',
state: '',
countyId: 0
};
// Active status
let isActive = true;
// Online ordering
let onlineOrdering = 'none';
// Reactive: disable phone when online only
$: phoneDisabled = onlineOrdering === 'online_only';
$: if (phoneDisabled) formData.phone = '';
// Form validation errors
let errors = {
companyName: '',
pricePerGallon: '',
bioPercent: '',
phone: '',
state: '',
countyId: ''
};
// Form submission state
let isSubmitting = false;
let submitMessage = '';
// Location selector state
let counties: County[] = [];
let isLoadingCounties = false;
// Phone formatting function
function formatPhone(value: string): string {
// Remove all non-digits
const digits = value.replace(/\D/g, '');
// Format as 123-456-7890
if (digits.length <= 3) return digits;
if (digits.length <= 6) return `${digits.slice(0, 3)}-${digits.slice(3)}`;
return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
}
// Handle phone input
function handlePhoneInput(event: Event) {
const target = event.target as HTMLInputElement;
const formatted = formatPhone(target.value);
formData.phone = formatted;
// Don't set target.value here to avoid cursor jumping issues
}
// Validate form
function validateForm(): boolean {
errors.companyName = '';
errors.pricePerGallon = '';
errors.bioPercent = '';
errors.phone = '';
errors.state = '';
errors.countyId = '';
let isValid = true;
if (!formData.companyName.trim()) {
errors.companyName = 'Company name is required';
isValid = false;
}
if (formData.pricePerGallon <= 0) {
errors.pricePerGallon = 'Price per gallon must be greater than 0';
isValid = false;
}
if (formData.bioPercent < 0 || formData.bioPercent > 100 || !Number.isInteger(formData.bioPercent)) {
errors.bioPercent = 'Bio percent must be an integer between 0 and 100';
isValid = false;
}
const phonePattern = /^[0-9]{3}-[0-9]{3}-[0-9]{4}$/;
if (!phoneDisabled && !phonePattern.test(formData.phone)) {
errors.phone = 'Phone number must be in format 123-456-7890';
isValid = false;
}
if (!formData.state) {
errors.state = 'Please select a state';
isValid = false;
}
if (!formData.countyId) {
errors.countyId = 'Please select a county';
isValid = false;
}
return isValid;
}
// Handle form submission
async function handleSubmit(event: Event) {
event.preventDefault();
if (!validateForm()) return;
isSubmitting = true;
submitMessage = '';
try {
const token = localStorage.getItem('token');
const response = await fetch('http://localhost:9552/listing', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
body: JSON.stringify({
company_name: formData.companyName,
is_active: isActive,
price_per_gallon: formData.pricePerGallon,
price_per_gallon_cash: formData.pricePerGallonCash,
minimum_order: formData.minimumOrder,
service: formData.labelService,
bio_percent: formData.bioPercent,
phone: formData.phone,
online_ordering: onlineOrdering,
county_id: formData.countyId
})
});
if (response.ok) {
// Redirect to vendor dashboard on success
goto('/vendor');
} else {
submitMessage = 'Failed to create listing. Please try again.';
}
} catch (error) {
submitMessage = 'Network error. Please check your connection and try again.';
} finally {
isSubmitting = false;
}
}
// Format price for display
function formatPrice(value: number): string {
return value.toFixed(2);
}
// Handle state change
async function handleStateChange() {
formData.countyId = 0; // Reset county selection
errors.state = '';
errors.countyId = '';
counties = [];
if (formData.state) {
isLoadingCounties = true;
try {
const response = await fetch(`http://localhost:9552/state/${formData.state}`);
if (response.ok) {
counties = await response.json();
if (counties.length === 0) {
errors.countyId = 'No counties found for selected state';
}
} else {
const errorData = await response.json();
errors.countyId = errorData.error || 'Failed to load counties';
}
} catch (error) {
errors.countyId = 'Network error loading counties';
} finally {
isLoadingCounties = false;
}
}
}
</script>
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold mb-4">Vendor Price Management</h1>
<p>This is the Vendor Price page. Here you can manage pricing information.</p>
<div class="mt-4">
<!-- Breadcrumbs (full width, left aligned) -->
<nav class="breadcrumbs text-sm mb-4 px-4">
<ul>
<li><a href="/" class="text-blue-500 hover:underline">Home</a></li>
<li><a href="/vendor" class="text-blue-500 hover:underline">Vendor Dashboard</a></li>
<li>Price Management</li>
</ul>
</nav>
<div class="p-4 max-w-2xl mx-auto">
<!-- Existing Content -->
<p class="mb-4">This is the Vendor Price page. Here you can manage pricing information.</p>
<div class="mb-6">
<a href="/vendor" class="text-blue-500 hover:underline">Back to Vendor Dashboard</a>
</div>
<!-- New Vendor Price Listing Form -->
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body">
<h2 class="card-title text-xl mb-4">Create New Price Listing</h2>
<form on:submit={handleSubmit} class="space-y-6">
<!-- Active Checkbox -->
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Active</span>
<input
type="checkbox"
class="checkbox checkbox-primary"
bind:checked={isActive}
/>
</label>
</div>
<!-- Company Name -->
<div class="form-control">
<label class="label" for="companyName">
<span class="label-text">Company Name</span>
</label>
<input
id="companyName"
type="text"
class="input input-bordered w-full {errors.companyName ? 'input-error' : ''}"
bind:value={formData.companyName}
placeholder="Enter company name"
required
/>
{#if errors.companyName}
<label class="label">
<span class="label-text-alt text-error">{errors.companyName}</span>
</label>
{/if}
</div>
<!-- Pricing Section -->
<div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Pricing Information</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Price per Gallon Card -->
<div class="form-control">
<label class="label" for="pricePerGallon">
<span class="label-text">Price per Gallon Card ($)</span>
</label>
<input
id="pricePerGallon"
type="number"
step="0.01"
min="0"
required
class="input input-bordered w-full {errors.pricePerGallon ? 'input-error' : ''}"
bind:value={formData.pricePerGallon}
/>
{#if errors.pricePerGallon}
<label class="label">
<span class="label-text-alt text-error">{errors.pricePerGallon}</span>
</label>
{/if}
</div>
<!-- Price per Gallon Cash -->
<div class="form-control">
<label class="label" for="pricePerGallonCash">
<span class="label-text">Price per Gallon Cash ($)</span>
</label>
<input
id="pricePerGallonCash"
type="number"
step="0.01"
min="0"
class="input input-bordered w-full"
bind:value={formData.pricePerGallonCash}
placeholder="Optional"
/>
</div>
<!-- Bio Percent -->
<div class="form-control">
<label class="label" for="bioPercent">
<span class="label-text">Bio Percent (%)</span>
</label>
<input
id="bioPercent"
type="number"
min="0"
max="100"
step="1"
required
class="input input-bordered w-full {errors.bioPercent ? 'input-error' : ''}"
bind:value={formData.bioPercent}
/>
{#if errors.bioPercent}
<label class="label">
<span class="label-text-alt text-error">{errors.bioPercent}</span>
</label>
{/if}
</div>
<!-- Minimum Order -->
<div class="form-control">
<label class="label" for="minimumOrder">
<span class="label-text">Minimum Order</span>
</label>
<input
id="minimumOrder"
type="number"
min="1"
max="200"
step="1"
class="input input-bordered w-full"
bind:value={formData.minimumOrder}
placeholder="Optional (1-200)"
/>
</div>
</div>
</div>
<!-- Service Section -->
<div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Service Options</h3>
<div class="space-y-4">
<!-- Label Service Checkbox -->
<div class="form-control">
<label class="label cursor-pointer" for="labelService">
<span class="label-text">Company offers service? </span>
<input
id="labelService"
type="checkbox"
class="checkbox checkbox-primary"
bind:checked={formData.labelService}
/>
</label>
</div>
</div>
</div>
<!-- Contact & Location Section -->
<div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Contact & Location</h3>
<div class="space-y-4">
<!-- Phone Number -->
<div class="form-control">
<label class="label" for="phone">
<span class="label-text">Phone</span>
</label>
<input
id="phone"
type="tel"
required={!phoneDisabled}
disabled={phoneDisabled}
class="input input-bordered w-full {phoneDisabled ? 'input-disabled' : ''} {errors.phone ? 'input-error' : ''}"
bind:value={formData.phone}
on:input={handlePhoneInput}
placeholder="123-456-7890"
/>
{#if errors.phone}
<label class="label">
<span class="label-text-alt text-error">{errors.phone}</span>
</label>
{/if}
</div>
<!-- Online Ordering -->
<div class="form-control">
<fieldset class="fieldset">
<legend class="fieldset-legend">Online Ordering</legend>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="none" />
No online ordering
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="online_only" />
Yes, online only
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="both" />
Both online and phone
</label>
</fieldset>
</div>
<!-- State Dropdown -->
<div class="form-control">
<label class="label" for="state">
<span class="label-text">State</span>
</label>
<select
id="state"
class="select select-bordered w-full {errors.state ? 'select-error' : ''}"
bind:value={formData.state}
on:change={handleStateChange}
required
>
<option value="">Select State</option>
{#each newEnglandStates as state}
<option value={state.id}>{state.name}</option>
{/each}
</select>
{#if errors.state}
<label class="label">
<span class="label-text-alt text-error">{errors.state}</span>
</label>
{/if}
</div>
<!-- County Dropdown -->
<div class="form-control">
<label class="label" for="county">
<span class="label-text">County</span>
</label>
<select
id="county"
class="select select-bordered w-full {errors.countyId ? 'select-error' : ''}"
bind:value={formData.countyId}
disabled={!formData.state || isLoadingCounties}
required
>
<option value={0}>
{#if isLoadingCounties}
Loading counties...
{:else}
Select County
{/if}
</option>
{#each counties as county}
<option value={county.id}>{county.name}</option>
{/each}
</select>
{#if errors.countyId}
<label class="label">
<span class="label-text-alt text-error">{errors.countyId}</span>
</label>
{/if}
</div>
</div>
</div>
<!-- Submit Button -->
<div class="form-control mt-6">
<button
type="submit"
class="btn btn-primary w-full"
disabled={isSubmitting}
>
{#if isSubmitting}
<span class="loading loading-spinner loading-sm"></span>
Creating Listing...
{:else}
Create Listing
{/if}
</button>
</div>
<!-- Submit Message -->
{#if submitMessage}
<div class="alert {submitMessage.includes('successfully') ? 'alert-success' : 'alert-error'}">
<span>{submitMessage}</span>
</div>
{/if}
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,568 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { newEnglandStates } from '$lib/states';
interface County {
id: number;
name: string;
state: string;
}
interface Listing {
id: number;
company_name: string;
is_active: boolean;
price_per_gallon: number;
price_per_gallon_cash: number | null;
note: string | null;
minimum_order: number | null;
service: boolean;
bio_percent: number;
phone: string | null;
online_ordering: string;
county_id: number;
user_id: number;
last_edited: string;
}
// Get listing ID from URL params
let listingId: string;
$: listingId = $page.params.id;
// Form data
let formData = {
companyName: '',
pricePerGallon: 0,
pricePerGallonCash: null as number | null,
minimumOrder: null as number | null,
labelService: false,
bioPercent: 0,
phone: '',
state: '',
countyId: 0
};
// Active status
let isActive = true;
// Online ordering
let onlineOrdering = 'none';
// Reactive: disable phone when online only
$: phoneDisabled = onlineOrdering === 'online_only';
$: if (phoneDisabled) formData.phone = '';
// Form validation errors
let errors = {
companyName: '',
pricePerGallon: '',
bioPercent: '',
phone: '',
state: '',
countyId: ''
};
// Form submission state
let isSubmitting = false;
let submitMessage = '';
let isLoading = true;
// Location selector state
let counties: County[] = [];
let isLoadingCounties = false;
// Load existing listing data
async function loadListing() {
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://localhost:9552/listing/${listingId}`, {
headers: {
'Authorization': token ? `Bearer ${token}` : ''
}
});
if (response.ok) {
const listing: Listing = await response.json();
// Pre-populate form with existing data
formData.companyName = listing.company_name;
isActive = listing.is_active;
formData.pricePerGallon = listing.price_per_gallon;
formData.pricePerGallonCash = listing.price_per_gallon_cash;
formData.minimumOrder = listing.minimum_order;
formData.labelService = listing.service;
formData.bioPercent = listing.bio_percent;
formData.phone = listing.phone || '';
onlineOrdering = listing.online_ordering;
formData.countyId = listing.county_id;
// Load the state for this county
await loadStateForCounty(listing.county_id);
} else {
submitMessage = 'Failed to load listing data';
}
} catch (error) {
submitMessage = 'Network error loading listing data';
} finally {
isLoading = false;
}
}
// Load state information for a given county
async function loadStateForCounty(countyId: number) {
// We need to find which state this county belongs to
// Since we don't have a reverse lookup, we'll load counties for each state until we find it
for (const state of newEnglandStates) {
try {
const response = await fetch(`http://localhost:9552/state/${state.id}`);
if (response.ok) {
const stateCounties: County[] = await response.json();
const county = stateCounties.find(c => c.id === countyId);
if (county) {
formData.state = state.id;
counties = stateCounties;
break;
}
}
} catch (error) {
// Continue to next state
}
}
}
// Phone formatting function
function formatPhone(value: string): string {
// Remove all non-digits
const digits = value.replace(/\D/g, '');
// Format as 123-456-7890
if (digits.length <= 3) return digits;
if (digits.length <= 6) return `${digits.slice(0, 3)}-${digits.slice(3)}`;
return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
}
// Handle phone input
function handlePhoneInput(event: Event) {
const target = event.target as HTMLInputElement;
const formatted = formatPhone(target.value);
formData.phone = formatted;
// Don't set target.value here to avoid cursor jumping issues
}
// Validate form
function validateForm(): boolean {
errors.companyName = '';
errors.pricePerGallon = '';
errors.bioPercent = '';
errors.phone = '';
errors.state = '';
errors.countyId = '';
let isValid = true;
if (!formData.companyName.trim()) {
errors.companyName = 'Company name is required';
isValid = false;
}
if (formData.pricePerGallon <= 0) {
errors.pricePerGallon = 'Price per gallon must be greater than 0';
isValid = false;
}
if (formData.bioPercent < 0 || formData.bioPercent > 100 || !Number.isInteger(formData.bioPercent)) {
errors.bioPercent = 'Bio percent must be an integer between 0 and 100';
isValid = false;
}
const phonePattern = /^[0-9]{3}-[0-9]{3}-[0-9]{4}$/;
if (!phoneDisabled && !phonePattern.test(formData.phone)) {
errors.phone = 'Phone number must be in format 123-456-7890';
isValid = false;
}
if (!formData.state) {
errors.state = 'Please select a state';
isValid = false;
}
if (!formData.countyId) {
errors.countyId = 'Please select a county';
isValid = false;
}
return isValid;
}
// Handle form submission
async function handleSubmit(event: Event) {
event.preventDefault();
if (!validateForm()) return;
isSubmitting = true;
submitMessage = '';
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://localhost:9552/listing/${listingId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
body: JSON.stringify({
company_name: formData.companyName,
is_active: isActive,
price_per_gallon: formData.pricePerGallon,
price_per_gallon_cash: formData.pricePerGallonCash,
minimum_order: formData.minimumOrder,
service: formData.labelService,
bio_percent: formData.bioPercent,
phone: formData.phone,
online_ordering: onlineOrdering,
county_id: formData.countyId
})
});
if (response.ok) {
// Redirect to vendor dashboard on success
goto('/vendor');
} else {
submitMessage = 'Failed to update listing. Please try again.';
}
} catch (error) {
submitMessage = 'Network error. Please check your connection and try again.';
} finally {
isSubmitting = false;
}
}
// Format price for display
function formatPrice(value: number): string {
return value.toFixed(2);
}
// Handle state change
async function handleStateChange() {
formData.countyId = 0; // Reset county selection
errors.state = '';
errors.countyId = '';
counties = [];
if (formData.state) {
isLoadingCounties = true;
try {
const response = await fetch(`http://localhost:9552/state/${formData.state}`);
if (response.ok) {
counties = await response.json();
if (counties.length === 0) {
errors.countyId = 'No counties found for selected state';
}
} else {
const errorData = await response.json();
errors.countyId = errorData.error || 'Failed to load counties';
}
} catch (error) {
errors.countyId = 'Network error loading counties';
} finally {
isLoadingCounties = false;
}
}
}
onMount(() => {
loadListing();
});
</script>
<!-- Breadcrumbs (full width, left aligned) -->
<nav class="breadcrumbs text-sm mb-4 px-4">
<ul>
<li><a href="/" class="text-blue-500 hover:underline">Home</a></li>
<li><a href="/vendor" class="text-blue-500 hover:underline">Vendor Dashboard</a></li>
<li><a href="/vendor/price" class="text-blue-500 hover:underline">Price Management</a></li>
<li>Edit Listing</li>
</ul>
</nav>
<div class="p-4 max-w-2xl mx-auto">
{#if isLoading}
<div class="flex justify-center py-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else}
<!-- Existing Content -->
<p class="mb-4">Edit the pricing information for this listing.</p>
<div class="mb-6">
<a href="/vendor" class="text-blue-500 hover:underline">Back to Vendor Dashboard</a>
</div>
<!-- Edit Listing Form -->
<div class="card bg-base-100 shadow-xl mb-8">
<div class="card-body">
<h2 class="card-title text-xl mb-4">Edit Price Listing</h2>
<form on:submit={handleSubmit} class="space-y-6">
<!-- Active Checkbox -->
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Active</span>
<input
type="checkbox"
class="checkbox checkbox-primary"
bind:checked={isActive}
/>
</label>
</div>
<!-- Company Name -->
<div class="form-control">
<label class="label" for="companyName">
<span class="label-text">Company Name</span>
</label>
<input
id="companyName"
type="text"
class="input input-bordered w-full {errors.companyName ? 'input-error' : ''}"
bind:value={formData.companyName}
placeholder="Enter company name"
required
/>
{#if errors.companyName}
<label class="label">
<span class="label-text-alt text-error">{errors.companyName}</span>
</label>
{/if}
</div>
<!-- Pricing Section -->
<div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Pricing Information</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Price per Gallon Card -->
<div class="form-control">
<label class="label" for="pricePerGallon">
<span class="label-text">Price per Gallon Card ($)</span>
</label>
<input
id="pricePerGallon"
type="number"
step="0.01"
min="0"
required
class="input input-bordered w-full {errors.pricePerGallon ? 'input-error' : ''}"
bind:value={formData.pricePerGallon}
/>
{#if errors.pricePerGallon}
<label class="label">
<span class="label-text-alt text-error">{errors.pricePerGallon}</span>
</label>
{/if}
</div>
<!-- Price per Gallon Cash -->
<div class="form-control">
<label class="label" for="pricePerGallonCash">
<span class="label-text">Price per Gallon Cash ($)</span>
</label>
<input
id="pricePerGallonCash"
type="number"
step="0.01"
min="0"
class="input input-bordered w-full"
bind:value={formData.pricePerGallonCash}
placeholder="Optional"
/>
</div>
<!-- Bio Percent -->
<div class="form-control">
<label class="label" for="bioPercent">
<span class="label-text">Bio Percent (%)</span>
</label>
<input
id="bioPercent"
type="number"
min="0"
max="100"
step="1"
required
class="input input-bordered w-full {errors.bioPercent ? 'input-error' : ''}"
bind:value={formData.bioPercent}
/>
{#if errors.bioPercent}
<label class="label">
<span class="label-text-alt text-error">{errors.bioPercent}</span>
</label>
{/if}
</div>
<!-- Minimum Order -->
<div class="form-control">
<label class="label" for="minimumOrder">
<span class="label-text">Minimum Order</span>
</label>
<input
id="minimumOrder"
type="number"
min="1"
max="200"
step="1"
class="input input-bordered w-full"
bind:value={formData.minimumOrder}
placeholder="Optional (1-200)"
/>
</div>
</div>
</div>
<!-- Service Section -->
<div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Service Options</h3>
<div class="space-y-4">
<!-- Label Service Checkbox -->
<div class="form-control">
<label class="label cursor-pointer" for="labelService">
<span class="label-text">Company offers service? </span>
<input
id="labelService"
type="checkbox"
class="checkbox checkbox-primary"
bind:checked={formData.labelService}
/>
</label>
</div>
</div>
</div>
<!-- Contact & Location Section -->
<div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Contact & Location</h3>
<div class="space-y-4">
<!-- Phone Number -->
<div class="form-control">
<label class="label" for="phone">
<span class="label-text">Phone</span>
</label>
<input
id="phone"
type="tel"
required={!phoneDisabled}
disabled={phoneDisabled}
class="input input-bordered w-full {phoneDisabled ? 'input-disabled' : ''} {errors.phone ? 'input-error' : ''}"
bind:value={formData.phone}
on:input={handlePhoneInput}
placeholder="123-456-7890"
/>
{#if errors.phone}
<label class="label">
<span class="label-text-alt text-error">{errors.phone}</span>
</label>
{/if}
</div>
<!-- Online Ordering -->
<div class="form-control">
<fieldset class="fieldset">
<legend class="fieldset-legend">Online Ordering</legend>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="none" />
No online ordering
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="online_only" />
Yes, online only
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="both" />
Both online and phone
</label>
</fieldset>
</div>
<!-- State Dropdown -->
<div class="form-control">
<label class="label" for="state">
<span class="label-text">State</span>
</label>
<select
id="state"
class="select select-bordered w-full {errors.state ? 'select-error' : ''}"
bind:value={formData.state}
on:change={handleStateChange}
required
>
<option value="">Select State</option>
{#each newEnglandStates as state}
<option value={state.id}>{state.name}</option>
{/each}
</select>
{#if errors.state}
<label class="label">
<span class="label-text-alt text-error">{errors.state}</span>
</label>
{/if}
</div>
<!-- County Dropdown -->
<div class="form-control">
<label class="label" for="county">
<span class="label-text">County</span>
</label>
<select
id="county"
class="select select-bordered w-full {errors.countyId ? 'select-error' : ''}"
bind:value={formData.countyId}
disabled={!formData.state || isLoadingCounties}
required
>
<option value={0}>
{#if isLoadingCounties}
Loading counties...
{:else}
Select County
{/if}
</option>
{#each counties as county}
<option value={county.id}>{county.name}</option>
{/each}
</select>
{#if errors.countyId}
<label class="label">
<span class="label-text-alt text-error">{errors.countyId}</span>
</label>
{/if}
</div>
</div>
</div>
<!-- Submit Button -->
<div class="form-control mt-6">
<button
type="submit"
class="btn btn-primary w-full"
disabled={isSubmitting}
>
{#if isSubmitting}
<span class="loading loading-spinner loading-sm"></span>
Updating Listing...
{:else}
Update Listing
{/if}
</button>
</div>
<!-- Submit Message -->
{#if submitMessage}
<div class="alert {submitMessage.includes('successfully') ? 'alert-success' : 'alert-error'}">
<span>{submitMessage}</span>
</div>
{/if}
</form>
</div>
</div>
{/if}
</div>

View File

@@ -1,49 +1,252 @@
<script lang="ts">
// Vendor profile page logic can be added here
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
const states = ['ME', 'NH', 'VT', 'MA', 'RI', 'CT'];
let company = {
name: '',
address: '',
town: '',
state: '',
phone: '',
owner_name: '',
owner_phone_number: '',
email: ''
};
let loading = true;
let saving = false;
let deleting = false;
async function fetchCompany() {
try {
const response = await fetch('http://localhost:9552/company', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
const data = await response.json();
company = {
name: data.name || '',
address: data.address || '',
town: data.town || '',
state: data.state || '',
phone: data.phone ? data.phone.toString() : '',
owner_name: data.owner_name || '',
owner_phone_number: data.owner_phone_number ? data.owner_phone_number.toString() : '',
email: data.email || ''
};
}
} catch (error) {
console.error('Error fetching company:', error);
} finally {
loading = false;
}
}
async function saveCompany() {
saving = true;
try {
const payload = {
name: company.name,
address: company.address || null,
town: company.town || null,
state: company.state || null,
phone: company.phone || null,
owner_name: company.owner_name || null,
owner_phone_number: company.owner_phone_number || null,
email: company.email || null
};
console.log('Sending payload:', payload);
const response = await fetch('http://localhost:9552/company', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(payload)
});
if (response.ok) {
alert('Company profile saved successfully!');
} else if (response.status === 401) {
alert('Your session has expired. Please log in again.');
// Could redirect to login: goto('/login');
} else {
const errorText = await response.text();
console.log('Error response:', errorText);
try {
const error = JSON.parse(errorText);
alert('Error saving company: ' + error.error);
} catch (e) {
alert('Error saving company: ' + errorText);
}
}
} catch (error) {
console.error('Error saving company:', error);
alert('Error saving company');
} finally {
saving = false;
}
}
async function deleteCompany() {
if (!confirm('Are you sure you want to delete your company profile? This action cannot be undone.')) {
return;
}
deleting = true;
try {
const response = await fetch('http://localhost:9552/company', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
alert('Company profile deleted successfully!');
goto('/vendor');
} else if (response.status === 401) {
alert('Your session has expired. Please log in again.');
// Could redirect to login: goto('/login');
} else {
const error = await response.json();
alert('Error deleting company: ' + error.error);
}
} catch (error) {
console.error('Error deleting company:', error);
alert('Error deleting company');
} finally {
deleting = false;
}
}
onMount(() => {
const token = localStorage.getItem('token');
if (token) {
fetchCompany();
} else {
loading = false;
// Could optionally show a login prompt or redirect
// alert('Please log in to access your company profile.');
// goto('/login');
}
});
</script>
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold mb-4">Vendor Profile</h1>
<form class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="container mx-auto p-6 max-w-4xl">
<h1 class="text-3xl font-bold mb-6 text-gray-900 dark:text-gray-100">Vendor Profile</h1>
{#if loading}
<div class="text-center py-8">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p class="mt-4 text-gray-600 dark:text-gray-400">Loading company profile...</p>
</div>
{:else}
<!-- Delete Button -->
<div class="mb-6">
<button
type="button"
on:click={deleteCompany}
disabled={deleting}
class="bg-red-600 hover:bg-red-700 disabled:bg-red-400 text-white font-medium py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
{#if deleting}
<span class="inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></span>
Deleting...
{:else}
Delete Company Profile
{/if}
</button>
</div>
<form class="space-y-8" on:submit|preventDefault={saveCompany}>
<!-- Company Information Section -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Company Information</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="companyName" class="block text-gray-700 text-sm font-bold mb-2">Company Name:</label>
<input type="text" id="companyName" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
<label for="companyName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Company Name</label>
<input bind:value={company.name} type="text" id="companyName" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100" />
</div>
<div>
<label for="address" class="block text-gray-700 text-sm font-bold mb-2">Address:</label>
<input type="text" id="address" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
<label for="phone" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Phone</label>
<input bind:value={company.phone} type="text" id="phone" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100" />
</div>
</div>
</div>
<!-- Address Section -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Address</h2>
<div class="space-y-4">
<div>
<label for="address" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Street Address</label>
<input bind:value={company.address} type="text" id="address" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100" />
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="town" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Town</label>
<input bind:value={company.town} type="text" id="town" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100" />
</div>
<div>
<label for="town" class="block text-gray-700 text-sm font-bold mb-2">Town:</label>
<input type="text" id="town" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
</div>
<div>
<label for="state" class="block text-gray-700 text-sm font-bold mb-2">State:</label>
<select id="state" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
{#each states as state}
<option value={state}>{state}</option>
<label for="state" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">State</label>
<select bind:value={company.state} id="state" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100">
{#each states as stateOption}
<option value={stateOption}>{stateOption}</option>
{/each}
</select>
</div>
</div>
</div>
</div>
<!-- Contact Information Section -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Contact Information</h2>
<div>
<label for="phone" class="block text-gray-700 text-sm font-bold mb-2">Phone:</label>
<input type="text" id="phone" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Email</label>
<input bind:value={company.email} type="email" id="email" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100" />
</div>
</div>
<!-- Owner Information Section -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Owner Information</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="ownerName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Owner Name</label>
<input bind:value={company.owner_name} type="text" id="ownerName" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100" />
</div>
<div>
<label for="ownerName" class="block text-gray-700 text-sm font-bold mb-2">Owner Name:</label>
<input type="text" id="ownerName" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
<label for="ownerPhoneNumber" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Owner Phone Number</label>
<input bind:value={company.owner_phone_number} type="text" id="ownerPhoneNumber" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100" />
</div>
<div>
<label for="ownerPhoneNumber" class="block text-gray-700 text-sm font-bold mb-2">Owner Phone Number:</label>
<input type="text" id="ownerPhoneNumber" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
</div>
<div>
<label for="email" class="block text-gray-700 text-sm font-bold mb-2">Email:</label>
<input type="email" id="email" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" />
</div>
<!-- Form Actions -->
<div class="flex justify-between items-center pt-4">
<button
type="submit"
disabled={saving}
class="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 dark:bg-blue-700 dark:hover:bg-blue-800 dark:disabled:bg-blue-500 text-white font-medium py-2 px-6 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
{#if saving}
<span class="inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></span>
Saving...
{:else}
Save Profile
{/if}
</button>
<a href="/vendor" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 font-medium">Back to Vendor Dashboard</a>
</div>
</form>
<div class="mt-4">
<a href="/vendor" class="text-blue-500 hover:underline">Back to Vendor Dashboard</a>
</div>
{/if}
</div>

View File

@@ -0,0 +1 @@
{}

View File

@@ -7,19 +7,40 @@ const config = {
darkMode: 'class',
daisyui: {
themes: [{
mytheme: {
themes: [
{
light: {
"primary": "#0256bf",
"secondary": "#36363a",
"accent": "#7F00FF",
"neutral": "#2B2B36",
"base-100": "#252531",
"base-100": "#ffffff",
"base-200": "#f8f9fa",
"base-300": "#dee2e6",
"base-content": "#000000",
"info": "#74a0d5",
"success": "#33cc33",
"warning": "#97520C",
"error": "#da0e0e",
},
}, ],
},
{
dark: {
"primary": "#0256bf",
"secondary": "#36363a",
"accent": "#7F00FF",
"neutral": "#2B2B36",
"base-100": "#252531",
"base-200": "#1a1a1f",
"base-300": "#15151a",
"base-content": "#ffffff",
"info": "#74a0d5",
"success": "#33cc33",
"warning": "#97520C",
"error": "#da0e0e",
},
},
],
},
plugins: [
require("daisyui"),

View File

@@ -2,5 +2,13 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit()],
server: {
proxy: {
'/api': {
target: 'http://newenglandoil_api_rust:9552',
changeOrigin: true
}
}
}
});