added listings
This commit is contained in:
40
package-lock.json
generated
40
package-lock.json
generated
@@ -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": "*"
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"lucide-svelte": "^0.544.0",
|
||||
"tailwind-merge": "^1.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,8 @@
|
||||
}
|
||||
.text-orange-oil {
|
||||
color: #ff6600;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-state {
|
||||
@apply btn btn-outline btn-secondary btn-xs px-1 py-1 text-xs leading-none;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
167
src/routes/(app)/[...slugs]/+page.svelte
Normal file
167
src/routes/(app)/[...slugs]/+page.svelte
Normal 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}
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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));
|
||||
|
||||
365
src/routes/(app)/vendor/+page.svelte
vendored
365
src/routes/(app)/vendor/+page.svelte
vendored
@@ -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>
|
||||
|
||||
475
src/routes/(app)/vendor/price/+page.svelte
vendored
475
src/routes/(app)/vendor/price/+page.svelte
vendored
@@ -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>
|
||||
|
||||
568
src/routes/(app)/vendor/price/[id]/+page.svelte
vendored
Normal file
568
src/routes/(app)/vendor/price/[id]/+page.svelte
vendored
Normal 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>
|
||||
275
src/routes/(app)/vendor/profile/+page.svelte
vendored
275
src/routes/(app)/vendor/profile/+page.svelte
vendored
@@ -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>
|
||||
<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" />
|
||||
<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>
|
||||
<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" />
|
||||
{: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>
|
||||
<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" />
|
||||
|
||||
<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-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="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>
|
||||
<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>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<!-- 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="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>
|
||||
<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" />
|
||||
|
||||
<!-- 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="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>
|
||||
<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" />
|
||||
|
||||
<!-- 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="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>
|
||||
</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" />
|
||||
|
||||
<!-- 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>
|
||||
|
||||
1
static/.well-known/appspecific/com.chrome.devtools.json
Normal file
1
static/.well-known/appspecific/com.chrome.devtools.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -7,19 +7,40 @@ const config = {
|
||||
darkMode: 'class',
|
||||
daisyui: {
|
||||
|
||||
themes: [{
|
||||
mytheme: {
|
||||
"primary": "#0256bf",
|
||||
"secondary": "#36363a",
|
||||
"accent": "#7F00FF",
|
||||
"neutral": "#2B2B36",
|
||||
"base-100": "#252531",
|
||||
"info": "#74a0d5",
|
||||
"success": "#33cc33",
|
||||
"warning": "#97520C",
|
||||
"error": "#da0e0e",
|
||||
themes: [
|
||||
{
|
||||
light: {
|
||||
"primary": "#0256bf",
|
||||
"secondary": "#36363a",
|
||||
"accent": "#7F00FF",
|
||||
"neutral": "#2B2B36",
|
||||
"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"),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user