first commit
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
||||
const form = document.getElementById("quote-form");
|
||||
const userSelect = document.getElementById("user-select");
|
||||
const quoteText = document.getElementById("quote-text");
|
||||
const attachmentInput = document.getElementById("attachment");
|
||||
const submitButton = document.getElementById("submit-btn");
|
||||
const statusEl = document.getElementById("status");
|
||||
const authText = document.getElementById("auth-text");
|
||||
const loginLink = document.getElementById("login-link");
|
||||
const logoutButton = document.getElementById("logout-btn");
|
||||
|
||||
function setFormLocked(locked) {
|
||||
form.classList.toggle("locked", locked);
|
||||
userSelect.disabled = locked;
|
||||
quoteText.disabled = locked;
|
||||
attachmentInput.disabled = locked;
|
||||
submitButton.disabled = locked;
|
||||
}
|
||||
|
||||
function setStatus(message, type = "") {
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = `status ${type}`.trim();
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
const response = await fetch("/api/users");
|
||||
const payload = await response.json();
|
||||
|
||||
userSelect.innerHTML = "";
|
||||
const placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "Select a speaker";
|
||||
userSelect.appendChild(placeholder);
|
||||
|
||||
for (const user of payload.users) {
|
||||
const option = document.createElement("option");
|
||||
option.value = user.id;
|
||||
option.textContent = user.displayName;
|
||||
userSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSession() {
|
||||
const response = await fetch("/api/session");
|
||||
const payload = await response.json();
|
||||
|
||||
if (!payload.authenticated) {
|
||||
authText.textContent = "Please log in with Discord to submit quotes.";
|
||||
loginLink.hidden = false;
|
||||
logoutButton.hidden = true;
|
||||
setFormLocked(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const userName = payload.user.globalName || payload.user.username;
|
||||
logoutButton.hidden = false;
|
||||
loginLink.hidden = true;
|
||||
|
||||
if (!payload.authorized) {
|
||||
authText.textContent = `Logged in as ${userName}, but this account is not allowed to submit.`;
|
||||
setFormLocked(true);
|
||||
return;
|
||||
}
|
||||
|
||||
authText.textContent = `Logged in as ${userName}. You can submit quotes.`;
|
||||
setFormLocked(false);
|
||||
}
|
||||
|
||||
logoutButton.addEventListener("click", async () => {
|
||||
await fetch("/auth/logout", { method: "POST" });
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const userId = userSelect.value;
|
||||
const quote = quoteText.value.trim();
|
||||
const file = attachmentInput.files && attachmentInput.files[0] ? attachmentInput.files[0] : null;
|
||||
|
||||
if (!userId || !quote) {
|
||||
setStatus("Please choose a speaker and enter a quote.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
submitButton.disabled = true;
|
||||
setStatus("Sending quote...");
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("userId", userId);
|
||||
formData.append("quoteText", quote);
|
||||
if (file) {
|
||||
formData.append("attachment", file);
|
||||
}
|
||||
|
||||
const response = await fetch("/api/quotes", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || "Failed to send quote.");
|
||||
}
|
||||
|
||||
quoteText.value = "";
|
||||
attachmentInput.value = "";
|
||||
setStatus("Quote sent to Discord.", "ok");
|
||||
} catch (error) {
|
||||
setStatus(error.message || "Something went wrong.", "error");
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
setFormLocked(true);
|
||||
|
||||
Promise.all([loadSession(), loadUsers()]).catch((error) => {
|
||||
console.error(error);
|
||||
setStatus("Failed to initialize page.", "error");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ZitateBot - Quote Submitter</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-glow bg-glow-a"></div>
|
||||
<div class="bg-glow bg-glow-b"></div>
|
||||
|
||||
<main class="app-shell">
|
||||
<section class="card fade-in-up">
|
||||
<h1>ZitateBot</h1>
|
||||
<p class="sub">Select a speaker and submit a quote directly to Discord.</p>
|
||||
|
||||
<div id="auth-panel" class="auth-panel">
|
||||
<p id="auth-text" class="auth-text">Checking Discord session...</p>
|
||||
<div class="auth-actions">
|
||||
<a id="login-link" class="auth-link" href="/auth/discord">Login with Discord</a>
|
||||
<button id="logout-btn" type="button" class="ghost-btn" hidden>Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="quote-form">
|
||||
<label for="user-select">Speaker</label>
|
||||
<select id="user-select" required>
|
||||
<option value="">Loading users...</option>
|
||||
</select>
|
||||
|
||||
<label for="quote-text">Quote</label>
|
||||
<textarea
|
||||
id="quote-text"
|
||||
maxlength="600"
|
||||
placeholder="Write the quote here..."
|
||||
required
|
||||
></textarea>
|
||||
|
||||
<label for="attachment">Attachment (optional)</label>
|
||||
<input id="attachment" type="file" />
|
||||
<p class="hint">Allowed: images, video, audio, txt, pdf, zip (max 100 MB).</p>
|
||||
|
||||
<button id="submit-btn" type="submit">Send Quote</button>
|
||||
<p id="status" class="status" aria-live="polite"></p>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
:root {
|
||||
--bg: #0b1020;
|
||||
--bg-soft: #121a2f;
|
||||
--text: #dbe4ff;
|
||||
--text-soft: #8b9cc8;
|
||||
--accent: #6d7cff;
|
||||
--accent-2: #30d1ff;
|
||||
--error: #ff6b8a;
|
||||
--ok: #6dffb5;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: radial-gradient(circle at top left, #111a32, var(--bg));
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: min(560px, 100%);
|
||||
background: linear-gradient(145deg, rgba(18, 26, 47, 0.85), rgba(11, 16, 32, 0.95));
|
||||
border: 1px solid rgba(109, 124, 255, 0.25);
|
||||
border-radius: 18px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
border: 1px solid rgba(139, 156, 200, 0.2);
|
||||
background: rgba(9, 13, 26, 0.45);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.auth-text {
|
||||
margin: 0;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.auth-link,
|
||||
.ghost-btn {
|
||||
appearance: none;
|
||||
border: 1px solid rgba(109, 124, 255, 0.45);
|
||||
background: rgba(109, 124, 255, 0.14);
|
||||
color: var(--text);
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-link:hover,
|
||||
.ghost-btn:hover {
|
||||
background: rgba(109, 124, 255, 0.28);
|
||||
}
|
||||
|
||||
#quote-form.locked {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 14px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
select,
|
||||
textarea,
|
||||
input[type="file"],
|
||||
button {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(139, 156, 200, 0.25);
|
||||
background: rgba(9, 13, 26, 0.75);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
select,
|
||||
textarea {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
input[type="file"]::file-selector-button {
|
||||
margin-right: 10px;
|
||||
border: 1px solid rgba(109, 124, 255, 0.45);
|
||||
background: rgba(109, 124, 255, 0.14);
|
||||
color: var(--text);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 8px 0 0;
|
||||
color: var(--text-soft);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 160px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
font-weight: 700;
|
||||
border: 0;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent-2));
|
||||
color: #061228;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(48, 209, 255, 0.35);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status {
|
||||
min-height: 1.2em;
|
||||
margin-top: 12px;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.status.ok {
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.status.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.bg-glow {
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
filter: blur(70px);
|
||||
opacity: 0.35;
|
||||
z-index: 1;
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.bg-glow-a {
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
left: 10%;
|
||||
top: 12%;
|
||||
background: #526dff;
|
||||
}
|
||||
|
||||
.bg-glow-b {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
right: 10%;
|
||||
bottom: 12%;
|
||||
background: #18ccff;
|
||||
animation-delay: -4s;
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.65s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-16px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(14px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user