This commit is contained in:
parent
3b51f4aee1
commit
9fba9cc5d7
8 changed files with 657 additions and 7 deletions
54
client/src/lib/api.ts
Normal file
54
client/src/lib/api.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
const BASE_URL = '/';
|
||||
|
||||
export async function fetchItems<T>(resource: 'users' | 'vehicles' | 'jobs'): Promise<T[]> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}${resource}`);
|
||||
if (!response.ok) throw new Error(`Failed to fetch ${resource}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createItem<T>(resource: 'users' | 'vehicles' | 'jobs', data: any): Promise<T> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}${resource}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Failed to create ${resource}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateItem<T>(resource: 'users' | 'vehicles' | 'jobs', id: number, data: any): Promise<T> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}${resource}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Failed to update ${resource}`);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteItem(resource: 'users' | 'vehicles' | 'jobs', id: number): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}${resource}/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) throw new Error(`Failed to delete ${resource}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
||||
27
client/src/lib/types.ts
Normal file
27
client/src/lib/types.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
export interface Job {
|
||||
id: number;
|
||||
clientName: string;
|
||||
pickup: string;
|
||||
dropoff: string;
|
||||
vehicleId: number;
|
||||
dateCreated: Date;
|
||||
lastUpdated?: Date;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
password?: string;
|
||||
displayName: string;
|
||||
dateCreated: Date;
|
||||
lastUpdated?: Date;
|
||||
}
|
||||
|
||||
export interface Vehicle {
|
||||
id: number;
|
||||
make: string;
|
||||
model: string;
|
||||
year: number;
|
||||
dateCreated: Date;
|
||||
lastUpdated?: Date;
|
||||
}
|
||||
|
|
@ -1,11 +1,24 @@
|
|||
<script lang="ts">
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
||||
|
||||
let { children } = $props();
|
||||
const queryClient = new QueryClient();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
{@render children()}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<main class="container">
|
||||
{@render children()}
|
||||
</main>
|
||||
</QueryClientProvider>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,67 @@
|
|||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<main class="container">
|
||||
<h1 class="title">Test Web</h1>
|
||||
|
||||
<div class="menu-grid">
|
||||
<a href="/jobs" class="card">
|
||||
<h2>Jobs</h2>
|
||||
<p>Manage job details.</p>
|
||||
</a>
|
||||
<a href="/users" class="card">
|
||||
<h2>Users</h2>
|
||||
<p>Manage user details.</p>
|
||||
</a>
|
||||
<a href="/vehicles" class="card">
|
||||
<h2>Vehicles</h2>
|
||||
<p>Manage vehicle details.</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
margin-left: 0.5rem;
|
||||
margin-bottom: 3rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.75rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: all 0.2s ease-in-out;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #2563eb;
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1f2937;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
163
client/src/routes/jobs/+page.svelte
Normal file
163
client/src/routes/jobs/+page.svelte
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<script lang="ts">
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import type { Job } from '$lib/types';
|
||||
import { fetchItems, createItem } from '$lib/api';
|
||||
|
||||
const query = createQuery<Job[]>(() => ({
|
||||
queryKey: ['jobs'],
|
||||
queryFn: () => fetchItems("jobs"),
|
||||
}));
|
||||
|
||||
let newRow = $state({
|
||||
clientName: "",
|
||||
pickup: "",
|
||||
dropoff: "",
|
||||
vehicleId: 0,
|
||||
} as Job);
|
||||
|
||||
async function addItem(e: SubmitEvent) {
|
||||
await createItem('jobs', newRow);
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1 class="title">Job Management</h1>
|
||||
|
||||
<section class="content-section">
|
||||
<h2 class="section-heading">Add New Job</h2>
|
||||
<form onsubmit={addItem} class="form">
|
||||
<div class="field">
|
||||
<label for="job_client_name" class="label">Client Name</label>
|
||||
<input id="job_client_name" bind:value={newRow.clientName} required class="input" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="job_pickup" class="label">Pickup</label>
|
||||
<input id="job_pickup" bind:value={newRow.pickup} required class="input" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="job_dropoff" class="label">Dropoff</label>
|
||||
<input id="job_dropoff" bind:value={newRow.dropoff} required class="input" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="job_vehicleId" class="label">Vehicle</label>
|
||||
<input type="numeric" id="job_vehicleId" bind:value={newRow.vehicleId} required class="input" />
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Add Job</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2 class="section-heading">Existing Jobs</h2>
|
||||
{#if query.error}
|
||||
<div class="alert error">
|
||||
{query.error}
|
||||
</div>
|
||||
{/if}
|
||||
{#if query.isLoading}
|
||||
<p>Loading...</p>
|
||||
{:else}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cell">ID</th>
|
||||
<th class="cell">Make</th>
|
||||
<th class="cell">Model</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each query.data || [] as row}
|
||||
<tr>
|
||||
<td class="cell">{row.clientName}</td>
|
||||
<td class="cell">{row.pickup}</td>
|
||||
<td class="cell">{row.dropoff}</td>
|
||||
<td class="cell">{row.vehicleId}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #fef2f2;
|
||||
color: #991b1b;
|
||||
border-color: #fee2e2;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0.625rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.data-table thead {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.cell {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
</style>
|
||||
166
client/src/routes/users/+page.svelte
Normal file
166
client/src/routes/users/+page.svelte
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<script lang="ts">
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import type { User } from '$lib/types';
|
||||
import { fetchItems, createItem } from '$lib/api';
|
||||
|
||||
const query = createQuery<User[]>(() => ({
|
||||
queryKey: ['users'],
|
||||
queryFn: () => fetchItems("users"),
|
||||
}));
|
||||
|
||||
let newRow = $state({
|
||||
username: "",
|
||||
password: "",
|
||||
displayName: ""
|
||||
} as User);
|
||||
|
||||
async function addItem(e: SubmitEvent) {
|
||||
await createItem('users', newRow);
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="container">
|
||||
<h1 class="title">User Management</h1>
|
||||
|
||||
<section class="content-section">
|
||||
<h2 class="section-heading">Add New User</h2>
|
||||
<form onsubmit={addItem} class="form">
|
||||
<div class="field">
|
||||
<label for="user_username" class="label">Username</label>
|
||||
<input id="user_username" bind:value={newRow.username} required class="input" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="user_password" class="label">Password</label>
|
||||
<input type="password" id="user_password" bind:value={newRow.password} required class="input" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="user_displayName" class="label">Display Name</label>
|
||||
<input id="user_displayName" bind:value={newRow.displayName} required class="input" />
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Add User</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2 class="section-heading">Existing Users</h2>
|
||||
{#if query.error}
|
||||
<div class="alert error">
|
||||
{query.error}
|
||||
</div>
|
||||
{/if}
|
||||
{#if query.isLoading}
|
||||
<p>Loading...</p>
|
||||
{:else}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cell">ID</th>
|
||||
<th class="cell">Username</th>
|
||||
<th class="cell">Password</th>
|
||||
<th class="cell">DisplayName</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each query.data || [] as row}
|
||||
<tr>
|
||||
<td class="cell">{row.id}</td>
|
||||
<td class="cell">{row.username}</td>
|
||||
<td class="cell">{row.password}</td>
|
||||
<td class="cell">{row.displayName}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #fef2f2;
|
||||
color: #991b1b;
|
||||
border-color: #fee2e2;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0.625rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.data-table thead {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.cell {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
</style>
|
||||
163
client/src/routes/vehicles/+page.svelte
Normal file
163
client/src/routes/vehicles/+page.svelte
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<script lang="ts">
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import type { Vehicle } from '$lib/types';
|
||||
import { fetchItems, createItem } from '$lib/api';
|
||||
|
||||
const query = createQuery<Vehicle[]>(() => ({
|
||||
queryKey: ['vehicles'],
|
||||
queryFn: () => fetchItems("vehicles"),
|
||||
}));
|
||||
|
||||
let newRow = $state({
|
||||
make: "",
|
||||
model: "",
|
||||
year: new Date().getFullYear()
|
||||
} as Vehicle);
|
||||
|
||||
async function addItem(e: SubmitEvent) {
|
||||
await createItem('vehicles', newRow);
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="container">
|
||||
<h1 class="title">Vehicle Management</h1>
|
||||
<section class="content-section">
|
||||
<h2 class="section-heading">Add New Vehicle</h2>
|
||||
<form onsubmit={addItem} class="form">
|
||||
<div class="field">
|
||||
<label for="user_name" class="label">Make</label>
|
||||
<input id="user_name" bind:value={newRow.make} required class="input" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="user_pass" class="label">Model</label>
|
||||
<input type="password" id="user_pass" bind:value={newRow.model} required class="input" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="user_disp" class="label">Year</label>
|
||||
<input id="user_disp" bind:value={newRow.year} required class="input" />
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Add Vehicle</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2 class="section-heading">Existing Vehicles</h2>
|
||||
{#if query.error}
|
||||
<div class="alert error">
|
||||
{query.error}
|
||||
</div>
|
||||
{/if}
|
||||
{#if query.isLoading}
|
||||
<p>Loading...</p>
|
||||
{:else}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cell">ID</th>
|
||||
<th class="cell">Make</th>
|
||||
<th class="cell">Model</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each query.data || [] as row}
|
||||
<tr>
|
||||
<td class="cell">{row.id}</td>
|
||||
<td class="cell">{row.make}</td>
|
||||
<td class="cell">{row.model}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #fef2f2;
|
||||
color: #991b1b;
|
||||
border-color: #fee2e2;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 0.625rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.data-table thead {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.cell {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue