feat: add fixes and more support for wierd api bugs
This commit is contained in:
parent
1149597139
commit
691394445a
6 changed files with 686 additions and 242 deletions
210
src/pages/qa.ts
210
src/pages/qa.ts
|
|
@ -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>
|
||||
·
|
||||
<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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue