feat: add fixes and more support for wierd api bugs

This commit is contained in:
Jet 2026-03-26 01:05:11 -07:00
parent 1149597139
commit 691394445a
No known key found for this signature in database
6 changed files with 686 additions and 242 deletions

View file

@ -1,4 +1,11 @@
import { getQuestions, submitQuestion } from "~/lib/api";
import { getQuestions, submitQuestion, type Question } from "~/lib/api";
import {
clearQuestionDraft,
formatExactTimestamp,
formatRelativeTimestamp,
readQuestionDraft,
writeQuestionDraft,
} from "~/lib/qa";
import { frostedBox } from "~/components/frosted-box";
function escapeHtml(str: string): string {
@ -7,35 +14,148 @@ function escapeHtml(str: string): string {
return div.innerHTML;
}
function formatQuestionDates(question: Question): string {
const asked = formatRelativeTimestamp(question.created_at);
const answered = formatRelativeTimestamp(question.answered_at);
const askedExact = formatExactTimestamp(question.created_at);
const answeredExact = formatExactTimestamp(question.answered_at);
return `
<span title="${escapeHtml(askedExact)}">Asked ${escapeHtml(asked)}</span>
&middot;
<span title="${escapeHtml(answeredExact)}">Answered ${escapeHtml(answered)}</span>`;
}
function renderQuestions(list: HTMLElement, questions: Question[]) {
if (questions.length === 0) {
list.innerHTML = `
<fieldset class="border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0">
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--dark-gray);">No answers yet</legend>
<p>No questions have been answered yet.</p>
<p class="qa-meta">Ask something above and check back once a reply is posted.</p>
</fieldset>`;
return;
}
list.innerHTML = questions
.map(
(q) => `
<fieldset class="border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0 mb-[2ch]">
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--dark-gray);">#${String(q.id)}</legend>
<p style="color: var(--light-cyan);">${escapeHtml(q.question)}</p>
<p class="mt-[1ch]" style="color: var(--light-green);">${escapeHtml(q.answer)}</p>
<p class="qa-meta">${formatQuestionDates(q)}</p>
</fieldset>`,
)
.join("");
}
export async function qaPage(outlet: HTMLElement) {
const draft = readQuestionDraft();
outlet.innerHTML = `
<div class="flex flex-col items-center justify-start px-4">
${frostedBox(`
<form id="qa-form">
<form id="qa-form" novalidate>
<fieldset class="border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-[1ch]">
<legend class="-mx-[0.5ch] px-[0.5ch] text-white">Ask a Question</legend>
<label class="sr-only" for="qa-input">Question</label>
<textarea id="qa-input" maxlength="200" rows="3"
class="qa-textarea"
placeholder="Type your question..."></textarea>
<div class="flex justify-between mt-[1ch]">
<span id="char-count" style="color: var(--dark-gray);">0/200</span>
<button type="submit" class="qa-button">[SUBMIT]</button>
aria-describedby="qa-helper qa-validation char-count"
placeholder="Type your question...">${escapeHtml(draft)}</textarea>
<p id="qa-helper" class="qa-helper">Press Enter to send. Press Shift+Enter for a new line.</p>
<div class="mt-[0.5ch] flex justify-between gap-[1ch]">
<p id="qa-validation" class="qa-meta" aria-live="polite"></p>
<span id="char-count" class="qa-meta">${draft.length}/200</span>
</div>
<div class="mt-[1ch] flex justify-between items-center gap-[1ch]">
<p id="qa-status" class="qa-meta" aria-live="polite"></p>
<button id="qa-submit" type="submit" class="qa-button">[SUBMIT]</button>
</div>
<p id="qa-status" class="mt-[0.5ch]" aria-live="polite"></p>
</fieldset>
</form>
<div id="qa-list" class="mt-[2ch]">Loading...</div>
<div id="qa-list" class="mt-[2ch]" aria-live="polite">Loading answered questions...</div>
`)}
</div>`;
const form = document.getElementById("qa-form") as HTMLFormElement;
const input = document.getElementById("qa-input") as HTMLTextAreaElement;
const charCount = document.getElementById("char-count")!;
const status = document.getElementById("qa-status")!;
const list = document.getElementById("qa-list")!;
const submitButton = document.getElementById(
"qa-submit",
) as HTMLButtonElement;
const charCount = document.getElementById("char-count") as HTMLSpanElement;
const validation = document.getElementById(
"qa-validation",
) as HTMLParagraphElement;
const status = document.getElementById("qa-status") as HTMLParagraphElement;
const list = document.getElementById("qa-list") as HTMLDivElement;
let isSubmitting = false;
let hasInteracted = draft.trim().length > 0;
function setStatus(message: string, color: string) {
status.textContent = message;
status.style.color = color;
}
function updateValidation() {
const trimmed = input.value.trim();
const remaining = 200 - input.value.length;
charCount.textContent = `${input.value.length}/200`;
if (trimmed.length === 0 && hasInteracted) {
validation.textContent = "Question cannot be empty.";
validation.style.color = "var(--light-red)";
input.setAttribute("aria-invalid", "true");
return false;
}
input.removeAttribute("aria-invalid");
if (remaining <= 20) {
validation.textContent = `${remaining} characters left.`;
validation.style.color =
remaining <= 5 ? "var(--yellow)" : "var(--dark-gray)";
return true;
}
validation.textContent = "";
return true;
}
async function loadQuestions() {
list.textContent = "Loading answered questions...";
list.style.color = "var(--light-gray)";
list.style.textAlign = "left";
try {
const questions = await getQuestions();
renderQuestions(list, questions);
} catch {
list.innerHTML = `
<fieldset class="border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0">
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--light-red);">Load failed</legend>
<p>Failed to load answered questions.</p>
<p class="qa-meta">
<button type="button" id="qa-retry" class="qa-inline-action">Retry loading questions</button>
</p>
</fieldset>`;
list.style.color = "var(--light-red)";
const retryButton = document.getElementById(
"qa-retry",
) as HTMLButtonElement | null;
retryButton?.addEventListener("click", () => {
void loadQuestions();
});
}
}
input.addEventListener("input", () => {
charCount.textContent = `${input.value.length}/200`;
hasInteracted = true;
writeQuestionDraft(input.value);
updateValidation();
});
input.addEventListener("keydown", (e) => {
@ -47,50 +167,44 @@ export async function qaPage(outlet: HTMLElement) {
form.addEventListener("submit", (e) => {
e.preventDefault();
const question = input.value.trim();
if (!question) return;
if (isSubmitting) return;
status.textContent = "Submitting...";
status.style.color = "var(--light-gray)";
const question = input.value.trim();
hasInteracted = true;
if (!updateValidation() || !question) {
setStatus("Write a question before submitting.", "var(--light-red)");
input.focus();
return;
}
isSubmitting = true;
submitButton.disabled = true;
setStatus("Submitting...", "var(--light-gray)");
submitQuestion(question)
.then(() => {
input.value = "";
charCount.textContent = "0/200";
status.textContent =
"Question submitted! It will appear here once answered.";
status.style.color = "var(--light-green)";
clearQuestionDraft();
hasInteracted = false;
updateValidation();
setStatus(
"Question submitted! It will appear here once answered.",
"var(--light-green)",
);
input.focus();
})
.catch((err: unknown) => {
status.textContent =
err instanceof Error ? err.message : "Failed to submit question.";
status.style.color = "var(--light-red)";
setStatus(
err instanceof Error ? err.message : "Failed to submit question.",
"var(--light-red)",
);
})
.finally(() => {
isSubmitting = false;
submitButton.disabled = false;
});
});
try {
const questions = await getQuestions();
if (questions.length === 0) {
list.textContent = "No questions answered yet.";
list.style.color = "var(--dark-gray)";
} else {
list.innerHTML = questions
.map(
(q) => `
<fieldset class="border-2 border-white px-[calc(1.5ch-0.5px)] pb-[1ch] pt-0 mb-[2ch]">
<legend class="-mx-[0.5ch] px-[0.5ch]" style="color: var(--dark-gray);">#${String(q.id)}</legend>
<p style="color: var(--light-cyan);">${escapeHtml(q.question)}</p>
<p class="mt-[1ch]" style="color: var(--light-green);">${escapeHtml(q.answer)}</p>
<p class="mt-[0.5ch]" style="color: var(--dark-gray);">
Asked ${q.created_at} · Answered ${q.answered_at}
</p>
</fieldset>`,
)
.join("");
}
} catch {
list.textContent = "Failed to load questions.";
list.style.color = "var(--light-red)";
list.style.textAlign = "center";
}
updateValidation();
await loadQuestions();
}