working pages no maps
This commit is contained in:
@@ -21,9 +21,15 @@
|
|||||||
// Check for user session on mount (this is a placeholder, actual implementation may vary)
|
// Check for user session on mount (this is a placeholder, actual implementation may vary)
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const storedUserString = localStorage.getItem('user');
|
const storedUserString = localStorage.getItem('user');
|
||||||
if (storedUserString) {
|
const token = localStorage.getItem('auth_token');
|
||||||
|
if (storedUserString && token) {
|
||||||
storedUser = JSON.parse(storedUserString);
|
storedUser = JSON.parse(storedUserString);
|
||||||
user.set(storedUser);
|
user.set(storedUser);
|
||||||
|
} else {
|
||||||
|
// Clear if inconsistent
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
user.set(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,6 +45,7 @@
|
|||||||
const logout = () => {
|
const logout = () => {
|
||||||
user.set(null);
|
user.set(null);
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,84 @@
|
|||||||
<!-- src/routes/[stateSlug]/+page.svelte -->
|
<!-- src/routes/[stateSlug]/+page.svelte -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { newEnglandStates, allCounties, type NewEnglandState } from '$lib/states'; // <--- Import type and counties
|
import {
|
||||||
|
newEnglandStates,
|
||||||
|
type NewEnglandState,
|
||||||
|
massachusettsCounties,
|
||||||
|
maineCounties,
|
||||||
|
vermontCounties,
|
||||||
|
newHampshireCounties,
|
||||||
|
rhodeIslandCounties,
|
||||||
|
connecticutCounties
|
||||||
|
} from '$lib/states';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
// The stateSlug from $page.params is guaranteed to be a string if this route matches
|
const { stateSlug } = $page.params as { stateSlug: string };
|
||||||
// because [stateSlug] is a required parameter.
|
|
||||||
const { stateSlug } = $page.params as { stateSlug: string }; // Type assertion for clarity
|
|
||||||
|
|
||||||
let stateData: NewEnglandState | undefined;
|
let stateData: NewEnglandState | undefined;
|
||||||
|
let stateCounties: NewEnglandState[] = [];
|
||||||
let filteredCounties: any[] = [];
|
let filteredCounties: any[] = [];
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let error: string | null = null;
|
let error: string | null = null;
|
||||||
|
let hoveredCounty: string | null = null;
|
||||||
|
|
||||||
|
// Clean county data access using object lookup
|
||||||
|
const countyDataMap: Record<string, NewEnglandState[]> = {
|
||||||
|
MA: massachusettsCounties,
|
||||||
|
ME: maineCounties,
|
||||||
|
VT: vermontCounties,
|
||||||
|
NH: newHampshireCounties,
|
||||||
|
RI: rhodeIslandCounties,
|
||||||
|
CT: connecticutCounties
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStateCounties(stateId: string): NewEnglandState[] {
|
||||||
|
return countyDataMap[stateId] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCountyClick(county: NewEnglandState) {
|
||||||
|
// Match county names between map data and API data
|
||||||
|
const cleanMapName = county.name.toLowerCase().replace(/\s+county$/, '').replace(/[^a-z]/g, '');
|
||||||
|
const apiCounty = filteredCounties.find((c: any) =>
|
||||||
|
cleanMapName === c.name.toLowerCase().replace(/[^a-z]/g, '')
|
||||||
|
);
|
||||||
|
|
||||||
|
goto(apiCounty ? `/${stateSlug}/${apiCounty.id}` : `/${stateSlug}/${county.slug}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseEnter(countyId: string) {
|
||||||
|
if (browser) {
|
||||||
|
hoveredCounty = countyId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
if (browser) {
|
||||||
|
hoveredCounty = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
stateData = newEnglandStates.find(s => s.id === stateSlug);
|
stateData = newEnglandStates.find((s: NewEnglandState) => s.id === stateSlug);
|
||||||
if (stateData) {
|
if (stateData) {
|
||||||
|
// Load API county data
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token');
|
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()}`, {
|
const response = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}`, {
|
||||||
headers
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
filteredCounties = response.ok ? await response.json() : [];
|
||||||
throw new Error(`API call failed with status ${response.status}`);
|
|
||||||
}
|
|
||||||
filteredCounties = await response.json();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : 'Failed to fetch counties';
|
error = err instanceof Error ? err.message : 'Failed to fetch counties';
|
||||||
filteredCounties = [];
|
filteredCounties = [];
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
}
|
||||||
|
loading = false;
|
||||||
|
|
||||||
|
// Load map county data
|
||||||
|
stateCounties = getStateCounties(stateSlug);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,39 +94,66 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="card lg:card-side bg-base-100 shadow-xl">
|
<!-- Centered State Name -->
|
||||||
<figure class=" ">
|
<div class="text-center mb-6">
|
||||||
<img
|
<h1 class="text-4xl font-bold">{stateData.name}</h1>
|
||||||
src={stateData.image}
|
</div>
|
||||||
alt="Map or notable feature of {stateData.id}"
|
|
||||||
class="object-cover"
|
<!-- Interactive County Map -->
|
||||||
/>
|
<div class="flex justify-center mb-6">
|
||||||
</figure>
|
{#if browser && stateCounties.length > 0}
|
||||||
<div class="card-body lg:w-1/2">
|
<svg
|
||||||
<h1 class="card-title text-4xl">{stateData.name}</h1>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<p>This is the page for {stateData.name}. Here you can add more specific information about this wonderful state!</p>
|
viewBox="0 0 1000 600"
|
||||||
<p>The URL for this page is <code class="bg-base-300 p-1 rounded">/{stateData.id}</code> which is {stateData.id.length} characters long.</p>
|
class="w-full max-w-2xl h-auto border border-gray-300 rounded-lg shadow-md"
|
||||||
<div class="mt-4">
|
aria-labelledby="countyMapTitle"
|
||||||
<h2 class="text-xl font-semibold mb-2">Counties:</h2>
|
role="img"
|
||||||
{#if loading}
|
>
|
||||||
<p>Loading counties...</p>
|
<title id="countyMapTitle">Interactive Map of {stateData.name} Counties</title>
|
||||||
{:else if error}
|
{#each stateCounties as county}
|
||||||
<p class="text-error">Error: {error}</p>
|
<path
|
||||||
{:else}
|
d={county.pathD}
|
||||||
<ul class="list-disc pl-5">
|
class={`stroke-black stroke-1 cursor-pointer transition-all duration-150 ease-in-out
|
||||||
{#each filteredCounties as county}
|
${hoveredCounty === county.id ? county.hoverFill : county.fill}`}
|
||||||
<li>
|
on:click={() => handleCountyClick(county)}
|
||||||
<a href="/{stateSlug}/{county.id}" class="text-blue-600 hover:underline">{county.name}</a>
|
on:mouseenter={() => handleMouseEnter(county.id)}
|
||||||
</li>
|
on:mouseleave={handleMouseLeave}
|
||||||
|
aria-label={county.name}
|
||||||
|
tabindex="0"
|
||||||
|
role="link"
|
||||||
|
on:keydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleCountyClick(county)}}
|
||||||
|
>
|
||||||
|
<title>{county.name}</title>
|
||||||
|
</path>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<div class="w-full max-w-2xl h-[400px] bg-gray-200 animate-pulse rounded-lg flex justify-center items-center">
|
||||||
|
<p>Loading county map...</p>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions justify-end mt-4">
|
|
||||||
|
<!-- Counties List -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<h2 class="text-xl font-semibold mb-2 text-center">Counties:</h2>
|
||||||
|
{#if loading}
|
||||||
|
<p class="text-center">Loading counties...</p>
|
||||||
|
{:else if error}
|
||||||
|
<p class="text-center text-error">Error: {error}</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
{#each filteredCounties as county}
|
||||||
|
<a href="/{stateSlug}/{county.id}" class="text-center text-blue-600 hover:underline block">{county.name}</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Back Button -->
|
||||||
|
<div class="text-center mt-6">
|
||||||
<a href="/" class="btn btn-primary">Back to Map</a>
|
<a href="/" class="btn btn-primary">Back to Map</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if !stateData && stateSlug} <!-- Check if stateData is still undefined after onMount attempted to find it -->
|
{:else if !stateData && stateSlug} <!-- Check if stateData is still undefined after onMount attempted to find it -->
|
||||||
<div class="text-center py-10">
|
<div class="text-center py-10">
|
||||||
<h1 class="text-3xl font-bold text-error">State Not Found</h1>
|
<h1 class="text-3xl font-bold text-error">State Not Found</h1>
|
||||||
@@ -92,11 +161,3 @@
|
|||||||
<a href="/" class="btn btn-primary mt-6">Go Back to Map</a>
|
<a href="/" class="btn btn-primary mt-6">Go Back to Map</a>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
figure img {
|
|
||||||
object-fit: cover;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<!-- src/routes/(app)/[stateSlug]/[countySlug]/+page.svelte -->
|
<!-- src/routes/(app)/[stateSlug]/[countySlug]/+page.svelte -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores
|
import { page } from '$app/stores';
|
||||||
';
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { newEnglandStates } from '$lib/states';
|
import { newEnglandStates } from '../../../../lib/states';
|
||||||
|
|
||||||
interface Listing {
|
interface Listing {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -18,6 +17,7 @@
|
|||||||
phone: string | null;
|
phone: string | null;
|
||||||
online_ordering: string;
|
online_ordering: string;
|
||||||
county_id: number;
|
county_id: number;
|
||||||
|
town: string | null;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
last_edited: string;
|
last_edited: string;
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,24 @@
|
|||||||
<p class="text-lg text-gray-600">No active listings found for this county.</p>
|
<p class="text-lg text-gray-600">No active listings found for this county.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-x-auto">
|
<!-- Sort Controls -->
|
||||||
|
<div class="mb-4 flex flex-wrap gap-2">
|
||||||
|
<select class="select select-bordered select-sm" bind:value={sortColumn} on:change={sortListings}>
|
||||||
|
<option value="company_name">Company</option>
|
||||||
|
<option value="town">Town</option>
|
||||||
|
<option value="price_per_gallon">Price per Gallon</option>
|
||||||
|
<option value="bio_percent">Bio %</option>
|
||||||
|
<option value="service">Service</option>
|
||||||
|
<option value="phone">Contact</option>
|
||||||
|
<option value="last_edited">Last Updated</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm" on:click={() => { sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; sortListings(); }}>
|
||||||
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Table View -->
|
||||||
|
<div class="hidden md:block overflow-x-auto">
|
||||||
<table class="table table-zebra w-full">
|
<table class="table table-zebra w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -167,15 +184,15 @@
|
|||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
{/if}
|
{/if}
|
||||||
</th>
|
</th>
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon')}>
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('town')}>
|
||||||
Price per Gallon Card
|
Town
|
||||||
{#if sortColumn === 'price_per_gallon'}
|
{#if sortColumn === 'town'}
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
{/if}
|
{/if}
|
||||||
</th>
|
</th>
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon_cash')}>
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon')}>
|
||||||
Price per Gallon Cash
|
Price per Gallon
|
||||||
{#if sortColumn === 'price_per_gallon_cash'}
|
{#if sortColumn === 'price_per_gallon'}
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
{/if}
|
{/if}
|
||||||
</th>
|
</th>
|
||||||
@@ -185,12 +202,6 @@
|
|||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
{/if}
|
{/if}
|
||||||
</th>
|
</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')}>
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('service')}>
|
||||||
Service
|
Service
|
||||||
{#if sortColumn === 'service'}
|
{#if sortColumn === 'service'}
|
||||||
@@ -198,29 +209,31 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</th>
|
</th>
|
||||||
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('phone')}>
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('phone')}>
|
||||||
Phone
|
Contact
|
||||||
{#if sortColumn === 'phone'}
|
{#if sortColumn === 'phone'}
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
{/if}
|
{/if}
|
||||||
</th>
|
</th>
|
||||||
|
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('last_edited')}>
|
||||||
|
Last Updated
|
||||||
|
{#if sortColumn === 'last_edited'}
|
||||||
|
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||||
|
{/if}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each listings as listing}
|
{#each listings as listing}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{listing.company_name}</td>
|
<td>{listing.company_name}</td>
|
||||||
<td>${listing.price_per_gallon.toFixed(2)}</td>
|
<td>{listing.town || 'N/A'}</td>
|
||||||
<td>{listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}</td>
|
|
||||||
<td>{listing.bio_percent}%</td>
|
|
||||||
<td>
|
<td>
|
||||||
{#if listing.online_ordering === 'none'}
|
<div class="text-sm">
|
||||||
No
|
<div><strong>Card:</strong> ${listing.price_per_gallon.toFixed(2)}</div>
|
||||||
{:else if listing.online_ordering === 'online_only'}
|
<div><strong>Cash:</strong> {listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}</div>
|
||||||
Online Only
|
</div>
|
||||||
{:else if listing.online_ordering === 'both'}
|
|
||||||
Both
|
|
||||||
{/if}
|
|
||||||
</td>
|
</td>
|
||||||
|
<td>{listing.bio_percent}%</td>
|
||||||
<td>
|
<td>
|
||||||
{#if listing.service}
|
{#if listing.service}
|
||||||
<span class="badge badge-success">Yes</span>
|
<span class="badge badge-success">Yes</span>
|
||||||
@@ -229,17 +242,90 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<div class="text-sm">
|
||||||
{#if listing.phone}
|
{#if listing.phone}
|
||||||
<a href="tel:{listing.phone}" class="text-blue-600 hover:underline">{listing.phone}</a>
|
<div><strong>Phone:</strong> <a href="tel:{listing.phone}" class="text-blue-600 hover:underline">{listing.phone}</a></div>
|
||||||
{:else}
|
{:else}
|
||||||
N/A
|
<div><strong>Phone:</strong> N/A</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div>
|
||||||
|
<strong>Online:</strong>
|
||||||
|
{#if listing.online_ordering === 'none'}
|
||||||
|
No
|
||||||
|
{:else if listing.online_ordering === 'online_only'}
|
||||||
|
Online Only
|
||||||
|
{:else if listing.online_ordering === 'both'}
|
||||||
|
Both
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{new Date(listing.last_edited).toLocaleDateString()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Card View -->
|
||||||
|
<div class="block md:hidden space-y-4">
|
||||||
|
{#each listings as listing}
|
||||||
|
<div class="card bg-base-100 shadow-lg">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3 class="card-title text-lg font-bold">
|
||||||
|
{listing.company_name}
|
||||||
|
{#if listing.town}
|
||||||
|
<br><small class="text-gray-500 font-normal">{listing.town}</small>
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Card Price:</span><br>
|
||||||
|
${listing.price_per_gallon.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Cash Price:</span><br>
|
||||||
|
{listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Bio %:</span><br>
|
||||||
|
{listing.bio_percent}%
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Service:</span><br>
|
||||||
|
{#if listing.service}
|
||||||
|
<span class="badge badge-success badge-sm">Yes</span>
|
||||||
|
{:else}
|
||||||
|
<span class="badge badge-neutral badge-sm">No</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Contact:</span><br>
|
||||||
|
{#if listing.phone}
|
||||||
|
<a href="tel:{listing.phone}" class="text-blue-600 hover:underline text-sm">{listing.phone}</a><br>
|
||||||
|
{:else}
|
||||||
|
N/A<br>
|
||||||
|
{/if}
|
||||||
|
<small>
|
||||||
|
Online:
|
||||||
|
{#if listing.online_ordering === 'none'}
|
||||||
|
No
|
||||||
|
{:else if listing.online_ordering === 'online_only'}
|
||||||
|
Online Only
|
||||||
|
{:else if listing.online_ordering === 'both'}
|
||||||
|
Both
|
||||||
|
{/if}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Last Updated:</span><br>
|
||||||
|
<small>{new Date(listing.last_edited).toLocaleDateString()}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
// Assuming the backend returns a token or some success indicator
|
// Assuming the backend returns a token or some success indicator
|
||||||
// Store the token in localStorage for authentication checks
|
// Store the token in localStorage for authentication checks
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('auth_token', data.token);
|
||||||
}
|
}
|
||||||
// Store the user object in localStorage
|
// Store the user object in localStorage
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
|||||||
203
src/routes/(app)/vendor/+page.svelte
vendored
203
src/routes/(app)/vendor/+page.svelte
vendored
@@ -14,13 +14,32 @@
|
|||||||
phone: string | null;
|
phone: string | null;
|
||||||
online_ordering: string;
|
online_ordering: string;
|
||||||
county_id: number;
|
county_id: number;
|
||||||
|
town: string | null;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
last_edited: string;
|
last_edited: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Company {
|
||||||
|
id: number;
|
||||||
|
active: boolean;
|
||||||
|
created: string;
|
||||||
|
name: string;
|
||||||
|
address?: string;
|
||||||
|
town?: string;
|
||||||
|
state?: string;
|
||||||
|
phone?: string;
|
||||||
|
owner_name?: string;
|
||||||
|
owner_phone_number?: string;
|
||||||
|
email?: string;
|
||||||
|
user_id?: number;
|
||||||
|
}
|
||||||
|
|
||||||
let listings: Listing[] = [];
|
let listings: Listing[] = [];
|
||||||
|
let company: Company | null = null;
|
||||||
let isLoading = true;
|
let isLoading = true;
|
||||||
|
let companyLoading = true;
|
||||||
let error = '';
|
let error = '';
|
||||||
|
let companyError = '';
|
||||||
let successMessage = '';
|
let successMessage = '';
|
||||||
|
|
||||||
// Inline editing state
|
// Inline editing state
|
||||||
@@ -28,16 +47,67 @@
|
|||||||
let editingField: string | null = null;
|
let editingField: string | null = null;
|
||||||
let editingValue: string = '';
|
let editingValue: string = '';
|
||||||
|
|
||||||
async function fetchListings() {
|
async function fetchCompany() {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch('http://localhost:9552/listing', {
|
|
||||||
|
if (!token) {
|
||||||
|
// Redirect to login if no token
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:9552/company', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': token ? `Bearer ${token}` : ''
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
company = await response.json();
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
// Token expired or invalid, redirect to login
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
// Company not found - that's okay, user can create one
|
||||||
|
company = null;
|
||||||
|
} else {
|
||||||
|
companyError = 'Failed to load company information';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
companyError = 'Network error loading company information';
|
||||||
|
} finally {
|
||||||
|
companyLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchListings() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
// Redirect to login if no token
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:9552/listing', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
listings = await response.json();
|
listings = await response.json();
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
// Token expired or invalid, redirect to login
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
error = 'Failed to load listings';
|
error = 'Failed to load listings';
|
||||||
}
|
}
|
||||||
@@ -52,7 +122,7 @@
|
|||||||
if (!confirm('Are you sure you want to delete this listing?')) return;
|
if (!confirm('Are you sure you want to delete this listing?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch(`http://localhost:9552/listing/${id}`, {
|
const response = await fetch(`http://localhost:9552/listing/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -73,7 +143,7 @@
|
|||||||
|
|
||||||
function editListing(listing: Listing) {
|
function editListing(listing: Listing) {
|
||||||
// Navigate to the edit page for this listing
|
// Navigate to the edit page for this listing
|
||||||
window.location.href = `/vendor/price/${listing.id}`;
|
window.location.href = `/vendor/listing/${listing.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEditing(listing: Listing, field: string) {
|
function startEditing(listing: Listing, field: string) {
|
||||||
@@ -128,7 +198,7 @@
|
|||||||
updateData[editingField] = newValue;
|
updateData[editingField] = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch(`http://localhost:9552/listing/${listing.id}`, {
|
const response = await fetch(`http://localhost:9552/listing/${listing.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -160,13 +230,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPhoneNumber(phone: string): string {
|
||||||
|
// Remove all non-digit characters
|
||||||
|
const cleaned = phone.replace(/\D/g, '');
|
||||||
|
|
||||||
|
// Check if it's a valid US phone number (10 or 11 digits)
|
||||||
|
if (cleaned.length === 10) {
|
||||||
|
// Format as (XXX) XXX-XXXX
|
||||||
|
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
|
||||||
|
} else if (cleaned.length === 11 && cleaned.startsWith('1')) {
|
||||||
|
// Format as +1 (XXX) XXX-XXXX for 1-prefixed numbers
|
||||||
|
return `+1 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return original if it doesn't match expected formats
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
fetchCompany();
|
||||||
fetchListings();
|
fetchListings();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-4">
|
<div class="container mx-auto p-4">
|
||||||
<h1 class="text-3xl font-bold mb-4">Vendor Dashboard</h1>
|
<h1 class="text-3xl font-bold mb-6">Vendor Dashboard</h1>
|
||||||
|
|
||||||
<!-- Success Banner -->
|
<!-- Success Banner -->
|
||||||
{#if successMessage}
|
{#if successMessage}
|
||||||
@@ -175,10 +263,91 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<p>Welcome to the Vendor section. Navigate to specific pages using the links below.</p>
|
<!-- Company Information Box (Facebook-like profile) -->
|
||||||
<div class="mt-4">
|
<div class="card bg-base-100 shadow-xl mb-8">
|
||||||
<a href="/vendor/profile" class="text-blue-500 hover:underline">Profile</a>
|
<div class="card-body">
|
||||||
<a href="/vendor/price" class="text-blue-500 hover:underline ml-4">Price</a>
|
{#if companyLoading}
|
||||||
|
<div class="flex justify-center py-8">
|
||||||
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
{:else if companyError}
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<span>{companyError}</span>
|
||||||
|
</div>
|
||||||
|
{:else if company}
|
||||||
|
<div class="flex flex-col md:flex-row gap-6">
|
||||||
|
<!-- Company Avatar/Icon -->
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="w-24 h-24 rounded-full bg-primary flex items-center justify-center">
|
||||||
|
<span class="text-3xl text-primary-content font-bold">
|
||||||
|
{company.name.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Company Details -->
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h2 class="card-title text-2xl mb-2">{company.name}</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
|
{#if company.address}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Address:</span><br>
|
||||||
|
{company.address}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if company.town && company.state}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Location:</span><br>
|
||||||
|
{company.town}, {company.state}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if company.phone}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Phone:</span><br>
|
||||||
|
<a href="tel:{company.phone}" class="text-blue-600 hover:underline">{formatPhoneNumber(company.phone)}</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if company.owner_name}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Owner:</span><br>
|
||||||
|
{company.owner_name}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if company.owner_phone_number}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Owner Phone:</span><br>
|
||||||
|
<a href="tel:{company.owner_phone_number}" class="text-blue-600 hover:underline">{formatPhoneNumber(company.owner_phone_number)}</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if company.email}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Email:</span><br>
|
||||||
|
<a href="mailto:{company.email}" class="text-blue-600 hover:underline">{company.email}</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Member Since:</span><br>
|
||||||
|
{new Date(company.created).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">No Company Profile Found</h3>
|
||||||
|
<p class="text-gray-600 mb-4">Create your company profile to get started.</p>
|
||||||
|
<a href="/vendor/profile" class="btn btn-primary">Create Company Profile</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation Links -->
|
||||||
|
<div class="flex flex-wrap gap-4 mb-8">
|
||||||
|
<a href="/vendor/profile" class="btn btn-outline">Company Profile</a>
|
||||||
|
<a href="/vendor/listing" class="btn btn-primary">Create Listing</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Listings Table -->
|
<!-- Listings Table -->
|
||||||
@@ -196,7 +365,7 @@
|
|||||||
{:else if listings.length === 0}
|
{:else if listings.length === 0}
|
||||||
<div class="text-center py-8">
|
<div class="text-center py-8">
|
||||||
<p class="text-lg text-gray-600">No listings found.. create one :)</p>
|
<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>
|
<a href="/vendor/listing" class="btn btn-primary mt-4">Create Listing</a>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
@@ -204,6 +373,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Company</th>
|
<th>Company</th>
|
||||||
|
<th>Location</th>
|
||||||
<th>Price Card</th>
|
<th>Price Card</th>
|
||||||
<th>Price Cash</th>
|
<th>Price Cash</th>
|
||||||
<th>Min Order</th>
|
<th>Min Order</th>
|
||||||
@@ -218,6 +388,13 @@
|
|||||||
{#each listings as listing}
|
{#each listings as listing}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{listing.company_name}</td>
|
<td>{listing.company_name}</td>
|
||||||
|
<td>
|
||||||
|
{#if listing.town}
|
||||||
|
{listing.town} (ID: {listing.county_id})
|
||||||
|
{:else}
|
||||||
|
County ID: {listing.county_id}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
|
||||||
<!-- Price Card (editable) -->
|
<!-- Price Card (editable) -->
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
bioPercent: 0,
|
bioPercent: 0,
|
||||||
phone: '',
|
phone: '',
|
||||||
state: '',
|
state: '',
|
||||||
countyId: 0
|
countyId: 0,
|
||||||
|
town: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Active status
|
// Active status
|
||||||
@@ -39,7 +40,9 @@
|
|||||||
bioPercent: '',
|
bioPercent: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
state: '',
|
state: '',
|
||||||
countyId: ''
|
countyId: '',
|
||||||
|
minimumOrder: '',
|
||||||
|
town: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Form submission state
|
// Form submission state
|
||||||
@@ -76,6 +79,8 @@
|
|||||||
errors.phone = '';
|
errors.phone = '';
|
||||||
errors.state = '';
|
errors.state = '';
|
||||||
errors.countyId = '';
|
errors.countyId = '';
|
||||||
|
errors.minimumOrder = '';
|
||||||
|
errors.town = '';
|
||||||
|
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
@@ -110,6 +115,16 @@
|
|||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (formData.minimumOrder === null || formData.minimumOrder < 1) {
|
||||||
|
errors.minimumOrder = 'Minimum order must be at least 1';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.town.trim()) {
|
||||||
|
errors.town = 'Town is required';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +155,8 @@
|
|||||||
bio_percent: formData.bioPercent,
|
bio_percent: formData.bioPercent,
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
online_ordering: onlineOrdering,
|
online_ordering: onlineOrdering,
|
||||||
county_id: formData.countyId
|
county_id: formData.countyId,
|
||||||
|
town: formData.town.trim() || null
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,7 +212,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/" class="text-blue-500 hover:underline">Home</a></li>
|
<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" class="text-blue-500 hover:underline">Vendor Dashboard</a></li>
|
||||||
<li>Price Management</li>
|
<li>Listing Management</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -320,10 +336,16 @@
|
|||||||
min="1"
|
min="1"
|
||||||
max="200"
|
max="200"
|
||||||
step="1"
|
step="1"
|
||||||
class="input input-bordered w-full"
|
required
|
||||||
|
class="input input-bordered w-full {errors.minimumOrder ? 'input-error' : ''}"
|
||||||
bind:value={formData.minimumOrder}
|
bind:value={formData.minimumOrder}
|
||||||
placeholder="Optional (1-200)"
|
placeholder="Enter minimum order quantity"
|
||||||
/>
|
/>
|
||||||
|
{#if errors.minimumOrder}
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt text-error">{errors.minimumOrder}</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -347,51 +369,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact & Location Section -->
|
<!-- Location Section -->
|
||||||
<div class="bg-base-200 p-4 rounded-lg">
|
<div class="bg-base-200 p-4 rounded-lg">
|
||||||
<h3 class="text-lg font-semibold mb-3">Contact & Location</h3>
|
<h3 class="text-lg font-semibold mb-3">Location</h3>
|
||||||
<div class="space-y-4">
|
<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 -->
|
<!-- State Dropdown -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="state">
|
<label class="label" for="state">
|
||||||
@@ -445,6 +426,77 @@
|
|||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Town Input -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="town">
|
||||||
|
<span class="label-text">Town</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="town"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
class="input input-bordered w-full {errors.town ? 'input-error' : ''}"
|
||||||
|
bind:value={formData.town}
|
||||||
|
placeholder="Enter town name"
|
||||||
|
maxlength="100"
|
||||||
|
/>
|
||||||
|
{#if errors.town}
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt text-error">{errors.town}</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt">Specify the town within the selected county</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Section -->
|
||||||
|
<div class="bg-base-200 p-4 rounded-lg">
|
||||||
|
<h3 class="text-lg font-semibold mb-3">Contact</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
phone: string | null;
|
phone: string | null;
|
||||||
online_ordering: string;
|
online_ordering: string;
|
||||||
county_id: number;
|
county_id: number;
|
||||||
|
town: string | null;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
last_edited: string;
|
last_edited: string;
|
||||||
}
|
}
|
||||||
@@ -41,7 +42,8 @@
|
|||||||
bioPercent: 0,
|
bioPercent: 0,
|
||||||
phone: '',
|
phone: '',
|
||||||
state: '',
|
state: '',
|
||||||
countyId: 0
|
countyId: 0,
|
||||||
|
town: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Active status
|
// Active status
|
||||||
@@ -61,7 +63,9 @@
|
|||||||
bioPercent: '',
|
bioPercent: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
state: '',
|
state: '',
|
||||||
countyId: ''
|
countyId: '',
|
||||||
|
minimumOrder: '',
|
||||||
|
town: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Form submission state
|
// Form submission state
|
||||||
@@ -76,7 +80,7 @@
|
|||||||
// Load existing listing data
|
// Load existing listing data
|
||||||
async function loadListing() {
|
async function loadListing() {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch(`http://localhost:9552/listing/${listingId}`, {
|
const response = await fetch(`http://localhost:9552/listing/${listingId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': token ? `Bearer ${token}` : ''
|
'Authorization': token ? `Bearer ${token}` : ''
|
||||||
@@ -97,6 +101,7 @@
|
|||||||
formData.phone = listing.phone || '';
|
formData.phone = listing.phone || '';
|
||||||
onlineOrdering = listing.online_ordering;
|
onlineOrdering = listing.online_ordering;
|
||||||
formData.countyId = listing.county_id;
|
formData.countyId = listing.county_id;
|
||||||
|
formData.town = listing.town || '';
|
||||||
|
|
||||||
// Load the state for this county
|
// Load the state for this county
|
||||||
await loadStateForCounty(listing.county_id);
|
await loadStateForCounty(listing.county_id);
|
||||||
@@ -158,6 +163,8 @@
|
|||||||
errors.phone = '';
|
errors.phone = '';
|
||||||
errors.state = '';
|
errors.state = '';
|
||||||
errors.countyId = '';
|
errors.countyId = '';
|
||||||
|
errors.minimumOrder = '';
|
||||||
|
errors.town = '';
|
||||||
|
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
@@ -192,6 +199,16 @@
|
|||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (formData.minimumOrder === null || formData.minimumOrder < 1) {
|
||||||
|
errors.minimumOrder = 'Minimum order must be at least 1';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.town.trim()) {
|
||||||
|
errors.town = 'Town is required';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +222,7 @@
|
|||||||
submitMessage = '';
|
submitMessage = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch(`http://localhost:9552/listing/${listingId}`, {
|
const response = await fetch(`http://localhost:9552/listing/${listingId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -222,7 +239,8 @@
|
|||||||
bio_percent: formData.bioPercent,
|
bio_percent: formData.bioPercent,
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
online_ordering: onlineOrdering,
|
online_ordering: onlineOrdering,
|
||||||
county_id: formData.countyId
|
county_id: formData.countyId,
|
||||||
|
town: formData.town.trim() || null
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -282,7 +300,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/" class="text-blue-500 hover:underline">Home</a></li>
|
<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" 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><a href="/vendor/listing" class="text-blue-500 hover:underline">Listing Management</a></li>
|
||||||
<li>Edit Listing</li>
|
<li>Edit Listing</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -411,10 +429,16 @@
|
|||||||
min="1"
|
min="1"
|
||||||
max="200"
|
max="200"
|
||||||
step="1"
|
step="1"
|
||||||
class="input input-bordered w-full"
|
required
|
||||||
|
class="input input-bordered w-full {errors.minimumOrder ? 'input-error' : ''}"
|
||||||
bind:value={formData.minimumOrder}
|
bind:value={formData.minimumOrder}
|
||||||
placeholder="Optional (1-200)"
|
placeholder="Enter minimum order quantity"
|
||||||
/>
|
/>
|
||||||
|
{#if errors.minimumOrder}
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt text-error">{errors.minimumOrder}</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -438,51 +462,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact & Location Section -->
|
<!-- Location Section -->
|
||||||
<div class="bg-base-200 p-4 rounded-lg">
|
<div class="bg-base-200 p-4 rounded-lg">
|
||||||
<h3 class="text-lg font-semibold mb-3">Contact & Location</h3>
|
<h3 class="text-lg font-semibold mb-3">Location</h3>
|
||||||
<div class="space-y-4">
|
<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 -->
|
<!-- State Dropdown -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="state">
|
<label class="label" for="state">
|
||||||
@@ -536,6 +519,77 @@
|
|||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Town Input -->
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="town">
|
||||||
|
<span class="label-text">Town</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="town"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
class="input input-bordered w-full {errors.town ? 'input-error' : ''}"
|
||||||
|
bind:value={formData.town}
|
||||||
|
placeholder="Enter town name"
|
||||||
|
maxlength="100"
|
||||||
|
/>
|
||||||
|
{#if errors.town}
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt text-error">{errors.town}</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt">Specify the town within the selected county</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Section -->
|
||||||
|
<div class="bg-base-200 p-4 rounded-lg">
|
||||||
|
<h3 class="text-lg font-semibold mb-3">Contact</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
8
src/routes/(app)/vendor/profile/+page.svelte
vendored
8
src/routes/(app)/vendor/profile/+page.svelte
vendored
@@ -23,7 +23,7 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:9552/company', {
|
const response = await fetch('http://localhost:9552/company', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
const response = await fetch('http://localhost:9552/company', {
|
const response = await fetch('http://localhost:9552/company', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('auth_token');
|
||||||
if (token) {
|
if (token) {
|
||||||
fetchCompany();
|
fetchCompany();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user