nightly - 2025-09-24
This commit is contained in:
@@ -9,3 +9,11 @@ Goals for this project:
|
|||||||
- Also have a desktop app available (eventually, maybe Electron or Tauri based)
|
- 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.
|
||||||
12
webapp/package-lock.json
generated
12
webapp/package-lock.json
generated
@@ -7,6 +7,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "webapp",
|
"name": "webapp",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"hotkeys-js": "^3.13.15"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@libsql/client": "^0.14.0",
|
"@libsql/client": "^0.14.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
@@ -2164,6 +2167,15 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.16.1",
|
"version": "2.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
|
|||||||
@@ -23,5 +23,8 @@
|
|||||||
"drizzle-orm": "^0.40.0",
|
"drizzle-orm": "^0.40.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hotkeys-js": "^3.13.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
webapp/src/lib/components/FormHeader.svelte
Normal file
46
webapp/src/lib/components/FormHeader.svelte
Normal 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>
|
||||||
@@ -19,9 +19,22 @@ a.styled:hover, button.styled:hover {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tb-margin {
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-row {
|
.flex-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-row-space {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
import hotkeys from "hotkeys-js";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let prefix_name = $state("");
|
let prefix_name = $state("");
|
||||||
let all_prefixes = $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(() => {
|
$effect(() => {
|
||||||
const new_prefix = all_prefixes.find((prefix) => prefix.name === prefix_name);
|
const new_prefix = all_prefixes.find((prefix) => prefix.name === prefix_name);
|
||||||
@@ -15,12 +19,19 @@
|
|||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
all_prefixes = [...data.prefixes];
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="main-menu">
|
<div class="main-menu">
|
||||||
<h1>TAM3 - Main Menu</h1>
|
<h1>{venue} - Main Menu</h1>
|
||||||
<div class="prefix-selector">
|
<div class="prefix-selector">
|
||||||
<select style="width: 100%; box-sizing: border-box;" bind:value={prefix_name}>
|
<select style="width: 100%; box-sizing: border-box;" bind:value={prefix_name}>
|
||||||
{#each all_prefixes as prefix}
|
{#each all_prefixes as prefix}
|
||||||
@@ -34,6 +45,21 @@
|
|||||||
<a href="/baskets/{current_prefix.name}/" target="_blank" class="styled">Baskets</a>
|
<a href="/baskets/{current_prefix.name}/" target="_blank" class="styled">Baskets</a>
|
||||||
<a href="/drawing/{current_prefix.name}/" target="_blank" class="styled">Drawing</a>
|
<a href="/drawing/{current_prefix.name}/" target="_blank" class="styled">Drawing</a>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export async function GET({ params }) {
|
|||||||
} else {
|
} else {
|
||||||
const data = await db.select().from(prefixes).where(eq(prefixes.name, name));
|
const data = await db.select().from(prefixes).where(eq(prefixes.name, name));
|
||||||
if (data[0]) {
|
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 {
|
} else {
|
||||||
return new Response(JSON.stringify({status: "Issue loading prefix"}), {status: 404, statusText: "Prefix not found."})
|
return new Response(JSON.stringify({status: "Issue loading prefix"}), {status: 404, statusText: "Prefix not found."})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,46 @@ import { db } from "$lib/server/db";
|
|||||||
import { tickets } from "$lib/server/db/schema";
|
import { tickets } from "$lib/server/db/schema";
|
||||||
import { env } from "$env/dynamic/private";
|
import { env } from "$env/dynamic/private";
|
||||||
|
|
||||||
export async function GET({ params }) {
|
export async function GET() {
|
||||||
if (env.TAM3_REMOTE) {
|
if (env.TAM3_REMOTE) {
|
||||||
const res = await fetch(`${env.TAM3_REMOTE}/api/tickets/?api_key=${env.TAM3_REMOTE_KEY}`);
|
const res = await fetch(`${env.TAM3_REMOTE}/api/tickets/?api_key=${env.TAM3_REMOTE_KEY}`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return new Response(JSON.stringify({details: "Couldn't fetch tickets."}), {
|
return new Response(JSON.stringify({details: "Couldn't fetch tickets."}), {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
statusText: res.statusText
|
statusText: res.statusText,
|
||||||
});
|
headers: {'Content-Type': 'applicaiton/json'}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
const data = await res.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 {
|
} else {
|
||||||
const data = await db.select().from(tickets);
|
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."})
|
||||||
}
|
}
|
||||||
6
webapp/src/routes/tickets/[prefix]/+page.js
Normal file
6
webapp/src/routes/tickets/[prefix]/+page.js
Normal 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}
|
||||||
|
}
|
||||||
160
webapp/src/routes/tickets/[prefix]/+page.svelte
Normal file
160
webapp/src/routes/tickets/[prefix]/+page.svelte
Normal 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>
|
||||||
Reference in New Issue
Block a user