Watch
1
0
Fork
You've already forked test-web
0

Add admin ui
All checks were successful
Build / build (push) Successful in 47s

This commit is contained in:
Chris 2026-07-23 19:12:08 +01:00
commit 9fba9cc5d7
8 changed files with 657 additions and 7 deletions

54
client/src/lib/api.ts Normal file
View 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;
}
}

View file

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

27
client/src/lib/types.ts Normal file
View 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;
}

View file

@ -1,11 +1,24 @@
<script lang="ts"> <script lang="ts">
import favicon from '$lib/assets/favicon.svg'; import favicon from '$lib/assets/favicon.svg';
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
let { children } = $props(); let { children } = $props();
const queryClient = new QueryClient();
</script> </script>
<svelte:head> <svelte:head>
<link rel="icon" href={favicon} /> <link rel="icon" href={favicon} />
</svelte:head> </svelte:head>
{@render children()} <QueryClientProvider client={queryClient}>
<main class="container">
{@render children()}
</main>
</QueryClientProvider>
<style>
.container {
max-width: 1000px;
margin: 0 auto;
}
</style>

View file

@ -1,2 +1,67 @@
<h1>Welcome to SvelteKit</h1> <main class="container">
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p> <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>

View 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>

View 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>

View 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>