feat(api): implement centralized API client and refactor vendor pages

Introduced a new API client in src/lib/api/ to handle requests securely. Refactored vendor pages to use this client. Updated authentication logic in layout and login pages.
This commit is contained in:
2026-02-09 16:25:55 -05:00
parent bd602d58ab
commit a5df1bcacb
14 changed files with 722 additions and 722 deletions

View File

@@ -4,6 +4,7 @@
import type { Writable } from 'svelte/store';
import '../../app.postcss'; // Import Tailwind CSS
import { user, darkMode, type User } from '$lib/states';
import { authApi } from '$lib/api';
// Initialize dark mode on mount to ensure data-theme is set
onMount(() => {
@@ -18,17 +19,15 @@
// Placeholder for user store - in a real app, this would be managed by an auth library or context
let storedUser: User | null = null;
// Check for user session on mount (this is a placeholder, actual implementation may vary)
// Check for user session on mount
onMount(() => {
const storedUserString = localStorage.getItem('user');
const token = localStorage.getItem('auth_token');
if (storedUserString && token) {
if (storedUserString) {
storedUser = JSON.parse(storedUserString);
user.set(storedUser);
} else {
// Clear if inconsistent
// No user stored
localStorage.removeItem('user');
localStorage.removeItem('auth_token');
user.set(null);
}
});
@@ -41,11 +40,10 @@
}
});
// Logout function
const logout = () => {
// Logout function - now async to call API to clear httpOnly cookie
const logout = async () => {
await authApi.logout();
user.set(null);
localStorage.removeItem('user');
localStorage.removeItem('auth_token');
window.location.href = '/';
};

View File

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

View File

@@ -14,12 +14,14 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { api } from '$lib/api';
import type { County } from '$lib/api';
const { stateSlug } = $page.params as { stateSlug: string };
let stateData: NewEnglandState | undefined;
let stateCounties: NewEnglandState[] = [];
let filteredCounties: any[] = [];
let filteredCounties: County[] = [];
let loading = false;
let error: string | null = null;
let hoveredCounty: string | null = null;
@@ -41,7 +43,7 @@
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) =>
const apiCounty = filteredCounties.find((c: County) =>
cleanMapName === c.name.toLowerCase().replace(/[^a-z]/g, '')
);
@@ -65,16 +67,16 @@
if (stateData) {
// Load API county data
loading = true;
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {}
});
filteredCounties = response.ok ? await response.json() : [];
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to fetch counties';
const result = await api.state.getCounties(stateSlug);
if (result.error) {
error = result.error;
filteredCounties = [];
} else {
filteredCounties = result.data || [];
}
loading = false;
// Load map county data

View File

@@ -3,27 +3,11 @@
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;
town: string | null;
user_id: number;
last_edited: string;
}
import { api } from '$lib/api';
import type { Listing, County } from '$lib/api';
const { stateSlug, countySlug } = $page.params as { stateSlug: string; countySlug: string };
let countyData: { id: number; name: string; state: string } | null = null;
let countyData: County | null = null;
let listings: Listing[] = [];
let loading = true;
let listingsLoading = false;
@@ -33,29 +17,18 @@
let sortDirection = 'asc'; // 'asc' or 'desc' - lowest price first
onMount(async () => {
try {
// Ensure API URL matches the Docker port forwarding for API (9552)
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();
const result = await api.state.getCounty(stateSlug, countySlug);
if (result.error) {
error = result.error;
countyData = null;
} else {
countyData = result.data;
// Fetch listings for this county
await fetchListings();
} catch (err) {
error = err instanceof Error ? err.message : 'An error occurred while fetching county data.';
countyData = null;
} finally {
loading = false;
}
loading = false;
});
async function fetchListings() {
@@ -63,19 +36,17 @@
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;
const result = await api.listings.getByCounty(countyData.id);
if (result.error) {
listingsError = 'Failed to load listings';
} else {
listings = result.data || [];
sortListings();
}
listingsLoading = false;
}
function sortListings() {
@@ -260,7 +231,7 @@
</div>
</div>
</td>
<td>{new Date(listing.last_edited).toLocaleDateString()}</td>
<td>{listing.last_edited ? new Date(listing.last_edited).toLocaleDateString() : 'N/A'}</td>
</tr>
{/each}
</tbody>
@@ -319,7 +290,7 @@
</div>
<div>
<span class="font-semibold">Last Updated:</span><br>
<small>{new Date(listing.last_edited).toLocaleDateString()}</small>
<small>{listing.last_edited ? new Date(listing.last_edited).toLocaleDateString() : 'N/A'}</small>
</div>
</div>
</div>

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { user } from '$lib/states';
import type { LoginRequest } from '$lib/types/types';
import { api } from '$lib/api';
import type { LoginRequest } from '$lib/api';
let username = '';
let password = '';
@@ -17,39 +18,18 @@
isLoading = true;
errorMessage = '';
try {
const response = await fetch('http://localhost:9552/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(loginData)
});
const result = await api.auth.login(loginData);
if (!response.ok) {
const errorData = await response.json();
errorMessage = errorData.error || 'Login failed';
} else {
const data = await response.json();
// 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);
}
// Store the user object in localStorage
localStorage.setItem('user', JSON.stringify(data.user));
// Update the user store
user.set(data.user);
// Redirect to vendor page after successful login
goto('/vendor');
}
} catch (err) {
errorMessage = 'An error occurred. Please try again.';
} finally {
isLoading = false;
if (result.error) {
errorMessage = result.error;
} else if (result.data) {
// Update the user store
user.set(result.data.user);
// Redirect to vendor page after successful login
goto('/vendor');
}
isLoading = false;
}
</script>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import type { RegisterRequest } from '$lib/types/types';
import { api } from '$lib/api';
import type { RegisterRequest } from '$lib/api';
let email = '';
let username = '';
@@ -25,33 +25,16 @@
isLoading = true;
errorMessage = '';
try {
const response = await fetch('http://localhost:9552/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(registerData)
});
const result = await api.auth.register(registerData);
if (!response.ok) {
const errorData = await response.json();
errorMessage = errorData.error || 'Registration failed';
} else {
const data = await response.json();
// Store the token in localStorage for authentication checks
if (data.token) {
localStorage.setItem('auth_token', data.token);
}
// Redirect to vendor page after successful registration
goto('/vendor');
}
} catch (err) {
console.error('Registration error:', err);
errorMessage = 'An error occurred. Please try again. Check the console for details.';
} finally {
isLoading = false;
if (result.error) {
errorMessage = result.error;
} else {
// Redirect to login page after successful registration
goto('/login');
}
isLoading = false;
}
</script>

View File

@@ -1,15 +1,21 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { authApi } from '$lib/api';
onMount(() => {
// Check if the user is logged in by looking for a token in localStorage or a cookie
// This assumes a token is stored upon successful login
const token = localStorage.getItem('auth_token');
if (!token) {
// Redirect to home page if not logged in
onMount(async () => {
// Check if the user is logged in by checking localStorage user
// and verifying with the API (which checks the httpOnly cookie)
const storedUser = authApi.getStoredUser();
if (!storedUser) {
// No user stored, redirect to home
goto('/');
return;
}
// Optional: Verify auth by making a request to /user endpoint
// This ensures the httpOnly cookie is still valid
// Note: We could skip this for faster loading, as protected API calls will handle 401
});
</script>

View File

@@ -1,38 +1,7 @@
<script lang="ts">
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;
town: string | null;
user_id: number;
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;
}
import { api } from '$lib/api';
import type { Listing, Company } from '$lib/api';
let listings: Listing[] = [];
let company: Company | null = null;
@@ -48,96 +17,44 @@
let editingValue: string = '';
async function fetchCompany() {
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/company', {
headers: {
'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) {
const result = await api.company.get();
if (result.error) {
if (result.error === 'Not found') {
// Company not found - that's okay, user can create one
company = null;
} else {
} else if (result.error !== 'Session expired. Please log in again.') {
companyError = 'Failed to load company information';
}
} catch (err) {
companyError = 'Network error loading company information';
} finally {
companyLoading = false;
} else {
company = result.data;
}
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) {
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 {
const result = await api.listing.getAll();
if (result.error) {
if (result.error !== 'Session expired. Please log in again.') {
error = 'Failed to load listings';
}
} catch (err) {
error = 'Network error loading listings';
} finally {
isLoading = false;
} else {
listings = result.data || [];
}
isLoading = false;
}
async function deleteListing(id: number) {
if (!confirm('Are you sure you want to delete this listing?')) return;
try {
const token = localStorage.getItem('auth_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');
const result = await api.listing.delete(id);
if (result.error) {
alert('Failed to delete listing');
} else {
// Remove from local state
listings = listings.filter(listing => listing.id !== id);
}
}
@@ -171,62 +88,50 @@
async function saveEditing(listing: Listing) {
if (!editingField) return;
let updateData: any = {};
let newValue: any;
let updateData: Record<string, unknown> = {};
let newValue: unknown;
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;
if (editingField === 'price_per_gallon' || editingField === 'price_per_gallon_cash') {
newValue = editingValue ? parseFloat(editingValue) : null;
if (editingField === 'price_per_gallon' && (!newValue || (newValue as number) <= 0)) {
alert('Please enter a valid price greater than 0');
return;
}
const token = localStorage.getItem('auth_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}`);
updateData[editingField] = newValue;
} else if (editingField === 'minimum_order') {
newValue = editingValue ? parseInt(editingValue) : null;
if (newValue && ((newValue as number) < 1 || (newValue as number) > 200)) {
alert('Minimum order must be between 1 and 200');
return;
}
} catch (err) {
alert('Network error updating field');
updateData[editingField] = newValue;
} else if (editingField === 'note') {
newValue = editingValue.trim() || null;
if (newValue && (newValue as string).length > 250) {
alert('Note cannot exceed 250 characters');
return;
}
updateData[editingField] = newValue;
}
const result = await api.listing.update(listing.id, updateData);
if (result.error) {
alert(`Failed to update ${editingField}`);
} else {
// 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);
}
}
@@ -248,6 +153,10 @@
}
onMount(() => {
if (!api.auth.isAuthenticated()) {
window.location.href = '/login';
return;
}
fetchCompany();
fetchListings();
});

View File

@@ -1,13 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { newEnglandStates } from '$lib/states';
interface County {
id: number;
name: string;
state: string;
}
import { api } from '$lib/api';
import type { County } from '$lib/api';
// Form data
let formData = {
@@ -137,45 +132,30 @@
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,
town: formData.town.trim() || null
})
});
const result = await api.listing.create({
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,
town: formData.town.trim() || null
});
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;
if (result.error) {
submitMessage = result.error === 'Session expired. Please log in again.'
? result.error
: 'Failed to create listing. Please try again.';
} else {
// Redirect to vendor dashboard on success
goto('/vendor');
}
}
// Format price for display
function formatPrice(value: number): string {
return value.toFixed(2);
isSubmitting = false;
}
// Handle state change
@@ -187,22 +167,19 @@
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';
const result = await api.state.getCounties(formData.state);
if (result.error) {
errors.countyId = result.error;
} else {
counties = result.data || [];
if (counties.length === 0) {
errors.countyId = 'No counties found for selected state';
}
} catch (error) {
errors.countyId = 'Network error loading counties';
} finally {
isLoadingCounties = false;
}
isLoadingCounties = false;
}
}
</script>

View File

@@ -3,30 +3,8 @@
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;
town: string | null;
user_id: number;
last_edited: string;
}
import { api } from '$lib/api';
import type { County, Listing } from '$lib/api';
// Get listing ID from URL params
let listingId: string;
@@ -79,40 +57,33 @@
// Load existing listing data
async function loadListing() {
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`http://localhost:9552/listing/${listingId}`, {
headers: {
'Authorization': token ? `Bearer ${token}` : ''
}
});
const result = await api.listing.getById(parseInt(listingId));
if (response.ok) {
const listing: Listing = await response.json();
if (result.error) {
submitMessage = result.error === 'Session expired. Please log in again.'
? result.error
: 'Failed to load listing data';
} else if (result.data) {
const listing = result.data;
// 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;
formData.town = listing.town || '';
// 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;
formData.town = listing.town || '';
// 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 the state for this county
await loadStateForCounty(listing.county_id);
}
isLoading = false;
}
// Load state information for a given county
@@ -120,19 +91,16 @@
// 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;
}
const result = await api.state.getCounties(state.id);
if (!result.error && result.data) {
const stateCounties = result.data;
const county = stateCounties.find(c => c.id === countyId);
if (county) {
formData.state = state.id;
counties = stateCounties;
break;
}
} catch (error) {
// Continue to next state
}
}
}
@@ -221,45 +189,30 @@
isSubmitting = true;
submitMessage = '';
try {
const token = localStorage.getItem('auth_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,
town: formData.town.trim() || null
})
});
const result = await api.listing.update(parseInt(listingId), {
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,
town: formData.town.trim() || null
});
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;
if (result.error) {
submitMessage = result.error === 'Session expired. Please log in again.'
? result.error
: 'Failed to update listing. Please try again.';
} else {
// Redirect to vendor dashboard on success
goto('/vendor');
}
}
// Format price for display
function formatPrice(value: number): string {
return value.toFixed(2);
isSubmitting = false;
}
// Handle state change
@@ -271,22 +224,19 @@
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';
const result = await api.state.getCounties(formData.state);
if (result.error) {
errors.countyId = result.error;
} else {
counties = result.data || [];
if (counties.length === 0) {
errors.countyId = 'No counties found for selected state';
}
} catch (error) {
errors.countyId = 'Network error loading counties';
} finally {
isLoadingCounties = false;
}
isLoadingCounties = false;
}
}

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { api } from '$lib/api';
const states = ['ME', 'NH', 'VT', 'MA', 'RI', 'CT'];
@@ -20,79 +21,54 @@
let deleting = false;
async function fetchCompany() {
try {
const response = await fetch('http://localhost:9552/company', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_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;
const result = await api.company.get();
if (result.data) {
company = {
name: result.data.name || '',
address: result.data.address || '',
town: result.data.town || '',
state: result.data.state || '',
phone: result.data.phone ? result.data.phone.toString() : '',
owner_name: result.data.owner_name || '',
owner_phone_number: result.data.owner_phone_number ? result.data.owner_phone_number.toString() : '',
email: result.data.email || ''
};
} else if (result.error && result.error !== 'Not found') {
console.error('Error fetching company:', result.error);
}
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
};
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);
console.log('Sending payload:', payload);
const response = await fetch('http://localhost:9552/company', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify(payload)
});
const result = await api.company.update(payload);
if (response.ok) {
alert('Company profile saved successfully!');
} else if (response.status === 401) {
if (result.error) {
if (result.error === 'Session expired. Please log in again.') {
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);
}
alert('Error saving company: ' + result.error);
}
} catch (error) {
console.error('Error saving company:', error);
alert('Error saving company');
} finally {
saving = false;
} else {
alert('Company profile saved successfully!');
}
saving = false;
}
async function deleteCompany() {
@@ -101,41 +77,28 @@
}
deleting = true;
try {
const response = await fetch('http://localhost:9552/company', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
const result = await api.company.delete();
if (response.ok) {
alert('Company profile deleted successfully!');
goto('/vendor');
} else if (response.status === 401) {
if (result.error) {
if (result.error === 'Session expired. Please log in again.') {
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);
alert('Error deleting company: ' + result.error);
}
} catch (error) {
console.error('Error deleting company:', error);
alert('Error deleting company');
} finally {
deleting = false;
} else {
alert('Company profile deleted successfully!');
goto('/vendor');
}
deleting = false;
}
onMount(() => {
const token = localStorage.getItem('auth_token');
if (token) {
if (api.auth.isAuthenticated()) {
fetchCompany();
} else {
loading = false;
// Could optionally show a login prompt or redirect
// alert('Please log in to access your company profile.');
// goto('/login');
}
});
</script>