nightly - 2025-09-24

This commit is contained in:
2025-09-25 00:13:34 -04:00
parent fc2d95d465
commit c71397cfbd
10 changed files with 311 additions and 10 deletions

View File

@@ -8,4 +8,12 @@ Goals for this project:
- Web-based version
- Also have a desktop app available (eventually, maybe Electron or Tauri based)
**This is under _active_ development**
**This is under _active_ development**
## Setting up
After cloning, cd'ing into webapp, and running `npm install` followed by `npm run dev` there's one thing you have to do before testing the rest.
That is making prefixes.
On the main menu press "Alt (or Option) + a" to toggle admin mode. Then click Prefix editor to open the form to edit prefixes.

View File

@@ -7,6 +7,9 @@
"": {
"name": "webapp",
"version": "0.0.1",
"dependencies": {
"hotkeys-js": "^3.13.15"
},
"devDependencies": {
"@libsql/client": "^0.14.0",
"@sveltejs/adapter-node": "^5.2.12",
@@ -2164,6 +2167,15 @@
"node": ">= 0.4"
}
},
"node_modules/hotkeys-js": {
"version": "3.13.15",
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.15.tgz",
"integrity": "sha512-gHh8a/cPTCpanraePpjRxyIlxDFrIhYqjuh01UHWEwDpglJKCnvLW8kqSx5gQtOuSsJogNZXLhOdbSExpgUiqg==",
"license": "MIT",
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",

View File

@@ -23,5 +23,8 @@
"drizzle-orm": "^0.40.0",
"svelte": "^5.0.0",
"vite": "^7.0.4"
},
"dependencies": {
"hotkeys-js": "^3.13.15"
}
}

View File

@@ -0,0 +1,46 @@
<script>
let {
prefix,
pagerForm = $bindable(),
functions
} = $props()
</script>
<div id="formheader" class="{prefix.color}">
<div class="flex-row-space tb-margin">
<div class="flex-row">
<input type="number" bind:value={pagerForm.id_from}>
<div style="font-size: 22pt">-</div>
<input type="number" bind:value={pagerForm.id_to}>
<button class="styled" onclick={functions.refreshPage}>Refresh</button>
</div>
<div class="flex-row">
<button class="styled" tabindex="-1" onclick={functions.prevPage}>Prev Page</button>
<button class="styled" tabindex="-1" onclick={functions.nextPage}>Next Page</button>
</div>
</div>
<div class="flex-row-space tb-margin">
<div class="flex-row">
<button class="styled" tabindex="-1" onclick={functions.duplicateDown}>Duplicate Down</button>
<button class="styled" tabindex="-1" onclick={functions.duplicateUp}>Duplicate Up</button>
<button class="styled" tabindex="-1" onclick={functions.gotoNext}>Next</button>
<button class="styled" tabindex="-1" onclick={functions.gotoPrev}>Previous</button>
<button class="styled" tabindex="-1" onclick={functions.copy}>Copy</button>
<button class="styled" tabindex="-1" onclick={functions.paste}>Paste</button>
</div>
<div class="flex-row">
<button class="styled" tabindex="-1" onclick={functions.saveAll}>Save All</button>
</div>
</div>
</div>
<style>
#formheader {
position: sticky;
top: 0;
padding-bottom: 0.25rem;
border-bottom: solid 1px #000000;
background-color: #ffffff;
z-index: 100;
}
</style>

View File

@@ -19,9 +19,22 @@ a.styled:hover, button.styled:hover {
text-decoration: underline;
}
.tb-margin {
padding-top: 0.25rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.flex-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.75rem;
}
.flex-row-space {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}

View File

@@ -1,10 +1,14 @@
<script>
import { browser } from "$app/environment";
import { env } from "$env/dynamic/public";
import hotkeys from "hotkeys-js";
let { data } = $props();
let prefix_name = $state("");
let all_prefixes = $state([]);
let current_prefix = $state({name: "", color: "", weight: 0})
let current_prefix = $state({name: "", color: "", weight: 0});
let admin_mode = $state(false);
const venue = env.PUBLIC_TAM3_VENUE || "TAM3";
$effect(() => {
const new_prefix = all_prefixes.find((prefix) => prefix.name === prefix_name);
@@ -15,12 +19,19 @@
if (browser) {
all_prefixes = [...data.prefixes];
document.title = "TAM3 - Main Menu"
document.title = `${venue} - Main Menu`;
hotkeys.filter = function(event) {return true};
hotkeys('alt+a', function(event) {event.preventDefault(); admin_mode = !admin_mode; return false;});
setTimeout(() => {
if (all_prefixes[0]) {
prefix_name = all_prefixes[0].name;
}
}, 100);
};
</script>
<div class="main-menu">
<h1>TAM3 - Main Menu</h1>
<h1>{venue} - Main Menu</h1>
<div class="prefix-selector">
<select style="width: 100%; box-sizing: border-box;" bind:value={prefix_name}>
{#each all_prefixes as prefix}
@@ -34,6 +45,21 @@
<a href="/baskets/{current_prefix.name}/" target="_blank" class="styled">Baskets</a>
<a href="/drawing/{current_prefix.name}/" target="_blank" class="styled">Drawing</a>
</div>
<div><h2>Reports:</h2></div>
<div class="flex-row {current_prefix.color}">
<a href="/report/byname/{current_prefix.name}/" target="_blank" class="styled">By Name</a>
<a href="/report/bybasket/{current_prefix.name}/" target="_blank" class="styled">By Basket ID</a>
</div>
{#if admin_mode}
<div><h2>Admin Mode:</h2></div>
<div class="flex-row {current_prefix.color}">
<a href="/prefixes" target="_blank" class="styled">Prefix Editor</a>
</div>
{/if}
</div>
<div class="annotation">
<p>Ticket Auction Manager 3 by Dilan Gilluly</p>
</div>
<style>

View File

@@ -15,7 +15,7 @@ export async function GET({ params }) {
} else {
const data = await db.select().from(prefixes).where(eq(prefixes.name, name));
if (data[0]) {
return new Response(JSON.stringify(data[0]), {status: 200, statusText: "Prefix loaded successfully."})
return new Response(JSON.stringify(data[0]), {status: 200, statusText: "Prefix loaded successfully.", headers: {'Content-Type': 'application/json'}})
} else {
return new Response(JSON.stringify({status: "Issue loading prefix"}), {status: 404, statusText: "Prefix not found."})
}

View File

@@ -2,19 +2,46 @@ import { db } from "$lib/server/db";
import { tickets } from "$lib/server/db/schema";
import { env } from "$env/dynamic/private";
export async function GET({ params }) {
export async function GET() {
if (env.TAM3_REMOTE) {
const res = await fetch(`${env.TAM3_REMOTE}/api/tickets/?api_key=${env.TAM3_REMOTE_KEY}`);
if (!res.ok) {
return new Response(JSON.stringify({details: "Couldn't fetch tickets."}), {
status: res.status,
statusText: res.statusText
});
statusText: res.statusText,
headers: {'Content-Type': 'applicaiton/json'}
})
};
const data = await res.json();
return new Response(JSON.stringify(data), {status: 200, statusText: "Tickets fetched successfully."})
return new Response(JSON.stringify(data), {
status: 200,
statusText: "Tickets fetched successfully.",
headers: {'Content-Type': 'application/json'}
})
} else {
const data = await db.select().from(tickets);
return new Response(JSON.stringify(data), {status: 200, statusText: "Tickets loaded successfully."})
return new Response(JSON.stringify(data), {
status: 200,
statusText: "Tickets loaded successfully.",
headers: {'Content-Type': 'application/json'}
})
};
};
export async function POST({ request }) {
const i_tickets = await request.json();
for (let ticket of i_tickets) {
await db.insert(tickets).values({prefix: ticket.prefix, t_id: ticket.t_id, first_name: ticket.first_name, last_name: ticket.last_name, phone_number: ticket.phone_number, preference: ticket.preference})
.onConflictDoUpdate({target: [tickets.prefix, tickets.t_id], set: {first_name: ticket.first_name, last_name: ticket.last_name, phone_number: ticket.phone_number, preference: ticket.preference}});
};
if (env.TAM3_REMOTE) {
const res = await fetch(`${env.TAM3_REMOTE}/api/tickets/?api_key=${env.TAM3_REMOTE_KEY}`, {
body: JSON.stringify([...i_tickets]), method: 'POST', headers: {'Content-Type': 'application/json'}
});
if (!res.ok) {
return new Response(JSON.stringify({details: "Issue posting tickets to remote."}), {status: res.status, statusText: res.statusText})
};
const data = await res.json();
};
return new Response(JSON.stringify({details: "Posted tickets successfully."}), {status: 200, statusText: "Posted tickets successfully."})
}

View File

@@ -0,0 +1,6 @@
export async function load({ fetch, params }) {
const { prefix } = await params;
const res = await fetch(`/api/prefixes/${prefix}`);
const prefix_data = await res.json();
return {prefix: prefix_data}
}

View File

@@ -0,0 +1,160 @@
<script>
import { browser } from '$app/environment';
import FormHeader from '$lib/components/FormHeader.svelte';
import hotkeys from 'hotkeys-js';
const { data } = $props();
let prefix = {...data.prefix};
let pagerForm = $state({id_from: 0, id_to: 0});
let current_idx = $state(0);
let current_tickets = $state([]);
let copy_buffer = $state({prefix: prefix.name, t_id: 0, first_name: "", last_name: "", phone_number: "", preference: "CALL", changed: true});
function changeFocus(idx) {
const focusFn = document.getElementById(`${idx}_fn`);
if (focusFn) {
focusFn.focus();
}
}
const functions = {
refreshPage: async () => {
if (current_tickets.length > 0) {
functions.saveAll();
};
const res = await fetch(`/api/tickets/${prefix.name}/${pagerForm.id_from}/${pagerForm.id_to}`);
if (res.ok) {
const data = await res.json();
current_tickets = [...data];
setTimeout(() => changeFocus(0), 100);
};
},
prevPage: () => {
const diff = pagerForm.id_to - pagerForm.id_from + 1;
pagerForm.id_from = pagerForm.id_from - diff;
pagerForm.id_to = pagerForm.id_to - diff;
functions.refreshPage()
},
nextPage: () => {
const diff = pagerForm.id_to - pagerForm.id_from + 1;
pagerForm.id_from = pagerForm.id_from + diff;
pagerForm.id_to = pagerForm.id_to + diff;
functions.refreshPage()
},
duplicateDown: () => {
const next_idx = current_idx + 1;
if (current_tickets[next_idx]) {
current_tickets[next_idx] = {...current_tickets[current_idx], t_id: current_tickets[next_idx].t_id, changed: true};
changeFocus(next_idx);
} else {
changeFocus(current_idx)
}
},
duplicateUp: () => {
const prev_idx = current_idx - 1;
if (prev_idx >= 0) {
current_tickets[prev_idx] = {...current_tickets[current_idx], t_id: current_tickets[prev_idx].t_id, changed: true};
changeFocus(prev_idx);
} else {
changeFocus(current_idx)
}
},
gotoNext: () => {
const next_idx = current_idx + 1;
if (current_tickets[next_idx]) {
changeFocus(next_idx);
} else {
changeFocus(current_idx);
}
},
gotoPrev: () => {
const prev_idx = current_idx - 1;
if (prev_idx >= 0) {
changeFocus(prev_idx);
} else {
changeFocus(current_idx);
}
},
copy: () => {
copy_buffer = {...current_tickets[current_idx]};
changeFocus(current_idx);
},
paste: () => {
current_tickets[current_idx] = {...copy_buffer, t_id: current_tickets[current_idx].t_id, changed: true};
changeFocus(current_idx);
},
saveAll: async () => {
const to_save = current_tickets.filter((ticket) => ticket.changed === true);
const res = await fetch(`/api/tickets`, {body: JSON.stringify(to_save), method: 'POST', headers: {'Content-Type': 'application/json'}});
if (res.ok) {
current_tickets.map((ticket) => ticket.changed = false);
changeFocus(0);
}
}
};
if (browser) {
document.title = `${prefix.name} Ticket Entry`
hotkeys.filter = function(event) {return true}
hotkeys('alt+n', function(event) {event.preventDefault(); functions.nextPage(); return false});
hotkeys('alt+b', function(event) {event.preventDefault(); functions.prevPage(); return false});
hotkeys('alt+j', function(event) {event.preventDefault(); functions.duplicateDown(); return false});
hotkeys('alt+u', function(event) {event.preventDefault(); functions.duplicateUp(); return false});
hotkeys('alt+l', function(event) {event.preventDefault(); functions.gotoNext(); return false});
hotkeys('alt+o', function(event) {event.preventDefault(); functions.gotoPrev(); return false});
hotkeys('alt+c', function(event) {event.preventDefault(); functions.copy(); return false});
hotkeys('alt+v', function(event) {event.preventDefault(); functions.paste(); return false});
hotkeys('alt+s', function(event) {event.preventDefault(); functions.saveAll(); return false});
}
</script>
<h1>{prefix.name} Ticket Entry</h1>
<FormHeader {prefix} {functions} bind:pagerForm />
<table>
<thead>
<tr>
<th style="width: 12ch">Ticket ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Phone Number</th>
<th>Preference</th>
<th>Changed</th>
</tr>
</thead>
<tbody>
{#each current_tickets as ticket, idx}
<tr onfocusin={() => current_idx = idx}>
<td>{ticket.t_id}</td>
<td><input id="{idx}_fn" type="text" bind:value={ticket.first_name} onchange={() => ticket.changed = true}></td>
<td><input id="{idx}_ln" type="text" bind:value={ticket.last_name} onchange={() => ticket.changed = true}></td>
<td><input id="{idx}_pn" type="text" bind:value={ticket.phone_number} onchange={() => ticket.changed = true}></td>
<td><select id="{idx}_pr" style="width: 100%" bind:value={ticket.preference} onchange={() => ticket.changed = true}>
<option value="CALL">Call</option>
<option value="TEXT">Text</option>
</select></td>
<td><button tabindex="-1" onclick={() => ticket.changed = !ticket.changed}>{ticket.changed ? "Y" : "N"}</button></td>
</tr>
{/each}
</tbody>
</table>
<style>
table {
width: 100%;
th {
text-align: left;
}
tbody tr:nth-child(2n) {
background-color: #eeeeee;
}
input {
background: transparent;
border: solid 1px #000000;
}
input, button {
display: block;
box-sizing: border-box;
width: 100%;
}
}
</style>