Skip to content

Instantly share code, notes, and snippets.

@ConnorNelson
Created September 26, 2025 16:35
Show Gist options
  • Select an option

  • Save ConnorNelson/84a0d9294b0a1400ccd766dd19694971 to your computer and use it in GitHub Desktop.

Select an option

Save ConnorNelson/84a0d9294b0a1400ccd766dd19694971 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Southwest Scholar Trivia Hub</title>
<style>
:root {
--brand-blue: #2f80ed;
--brand-purple: #6c5ce7;
--brand-gold: #f7b733;
--panel-radius: 18px;
--shadow-soft: 0 18px 45px rgba(15, 33, 54, 0.12);
--text-primary: #182b49;
--text-secondary: #405872;
--bg-body: #eef3fb;
--bg-quiz: #ffffff;
--bg-data: #f7f2ff;
--bg-highlight: rgba(47, 128, 237, 0.08);
--correct: #1b9c6d;
--incorrect: #d64550;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Segoe UI", Tahoma, sans-serif;
background: radial-gradient(circle at 20% 20%, rgba(47, 128, 237, 0.18), transparent 55%),
radial-gradient(circle at 80% 0%, rgba(108, 92, 231, 0.16), transparent 50%),
var(--bg-body);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px 60px;
}
.app {
width: min(1200px, 100%);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
border-radius: 28px;
box-shadow: var(--shadow-soft);
padding: 40px;
display: flex;
flex-direction: column;
gap: 28px;
}
header {
display: flex;
flex-direction: column;
gap: 8px;
}
header h1 {
margin: 0;
font-size: 2.1rem;
font-weight: 700;
letter-spacing: 0.4px;
}
header p {
margin: 0;
color: var(--text-secondary);
font-size: 1rem;
}
.layout {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
.panel {
border-radius: var(--panel-radius);
padding: 28px;
display: flex;
flex-direction: column;
gap: 20px;
box-shadow: inset 0 0 0 1px rgba(24, 43, 73, 0.06);
min-width: 0;
}
.quiz-panel {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.96), rgba(238, 244, 255, 0.96));
}
.data-panel {
background: linear-gradient(145deg, rgba(247, 242, 255, 0.98), rgba(236, 230, 255, 0.96));
}
.panel-header h2 {
margin: 0;
font-size: 1.5rem;
}
.panel-subtitle {
margin: 0;
color: var(--text-secondary);
font-size: 0.95rem;
}
.question-progress {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.95rem;
color: var(--text-secondary);
}
.question-card {
background: var(--bg-highlight);
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.question-card h3 {
margin: 0;
font-size: 1.2rem;
}
label {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-secondary);
}
input[type="text"],
textarea {
width: 100%;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid rgba(24, 43, 73, 0.18);
background: #fff;
font-size: 1rem;
transition: border 0.2s ease, box-shadow 0.2s ease;
resize: vertical;
min-height: 48px;
}
input[type="text"]:focus,
textarea:focus {
outline: none;
border-color: rgba(47, 128, 237, 0.55);
box-shadow: 0 0 0 4px rgba(47, 128, 237, 0.12);
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
}
.nav-buttons {
display: flex;
gap: 10px;
}
button {
cursor: pointer;
border: none;
border-radius: 999px;
padding: 12px 20px;
font-size: 0.95rem;
font-weight: 600;
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
transform: none;
box-shadow: none;
}
.ghost-button {
background: rgba(24, 43, 73, 0.08);
color: var(--text-primary);
}
.ghost-button:hover:not(:disabled) {
background: rgba(24, 43, 73, 0.16);
}
.primary-button {
background: linear-gradient(135deg, var(--brand-blue), #4ea5ff);
color: #fff;
box-shadow: 0 12px 24px rgba(47, 128, 237, 0.35);
}
.primary-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 14px 28px rgba(47, 128, 237, 0.4);
}
.status-message {
font-size: 0.9rem;
min-height: 20px;
color: var(--text-secondary);
}
.status-message.error {
color: var(--incorrect);
}
.status-message.success {
color: var(--correct);
}
.data-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.view-label {
font-size: 0.95rem;
color: var(--text-secondary);
}
.toggle-group {
background: rgba(108, 92, 231, 0.12);
border-radius: 999px;
padding: 6px;
display: inline-flex;
gap: 6px;
}
.toggle-button {
background: transparent;
color: var(--brand-purple);
padding: 10px 18px;
}
.toggle-button.active {
background: #fff;
color: var(--brand-purple);
box-shadow: 0 6px 14px rgba(108, 92, 231, 0.22);
}
.data-output {
background: rgba(255, 255, 255, 0.74);
border-radius: 16px;
padding: 20px;
min-height: 260px;
border: 1px solid rgba(108, 92, 231, 0.15);
overflow: hidden;
}
.data-output[data-view="csv"] {
overflow: auto;
max-width: 100%;
}
.data-output[data-view="table"] {
overflow: auto;
max-width: 100%;
}
.empty-state {
text-align: center;
color: var(--text-secondary);
font-style: italic;
margin-top: 40px;
}
.response-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 14px;
max-height: 380px;
overflow-y: auto;
padding-right: 6px;
}
.response-item {
background: rgba(47, 128, 237, 0.08);
border-radius: 12px;
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 4px;
border-left: 4px solid rgba(47, 128, 237, 0.45);
}
img {
max-width: 64px;
max-height: 64px;
width: 64px;
height: 64px;
object-fit: cover;
border-radius: 8px;
}
.csv-mode-toggle {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 0.85rem;
color: var(--text-secondary);
background: rgba(108, 92, 231, 0.08);
border-radius: 12px;
padding: 14px 16px;
margin-top: 8px;
}
.csv-toggle-label {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: var(--brand-purple);
}
.csv-toggle-label input {
width: 20px;
height: 20px;
accent-color: var(--brand-purple);
cursor: pointer;
}
.csv-toggle-hint {
font-size: 0.78rem;
color: var(--text-secondary);
}
.response-name {
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: baseline;
}
.response-meta {
font-size: 0.85rem;
color: var(--text-secondary);
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.status-pill {
font-size: 0.75rem;
letter-spacing: 0.3px;
text-transform: uppercase;
font-weight: 700;
padding: 4px 10px;
border-radius: 999px;
background: rgba(24, 43, 73, 0.1);
color: var(--text-primary);
}
.status-pill.correct {
background: rgba(27, 156, 109, 0.18);
color: var(--correct);
border: 1px solid rgba(27, 156, 109, 0.38);
}
.status-pill.incorrect {
background: rgba(214, 69, 80, 0.12);
color: var(--incorrect);
border: 1px solid rgba(214, 69, 80, 0.35);
}
.status-pill.warning {
background: rgba(247, 183, 51, 0.16);
color: #c07b00;
border: 1px solid rgba(247, 183, 51, 0.38);
}
table {
width: 100%;
min-width: 620px;
border-collapse: separate;
border-spacing: 0;
font-size: 0.9rem;
border: 1px solid rgba(108, 92, 231, 0.35);
border-radius: 12px;
overflow: hidden;
}
thead {
background: rgba(108, 92, 231, 0.16);
}
th, td {
text-align: left;
padding: 12px 14px;
border-right: 1px solid rgba(108, 92, 231, 0.35);
border-bottom: 1px solid rgba(108, 92, 231, 0.35);
background: rgba(255, 255, 255, 0.96);
}
th:last-child,
td:last-child {
border-right: none;
}
tbody tr:nth-child(even) td {
background: rgba(108, 92, 231, 0.06);
}
tbody tr:hover td {
background: rgba(108, 92, 231, 0.12);
}
.data-output pre {
margin: 0;
font-family: "Fira Code", "Courier New", monospace;
font-size: 0.86rem;
white-space: pre;
max-height: 360px;
overflow: auto;
background: rgba(0, 0, 0, 0.04);
border-radius: 12px;
padding: 16px;
border: 1px dashed rgba(108, 92, 231, 0.18);
}
.data-output[data-view="csv"] pre {
display: inline-block;
min-width: 100%;
max-width: 100%;
}
.submission-counter {
font-weight: 600;
color: var(--brand-purple);
}
@media (max-width: 1100px) {
.layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
body {
padding: 20px 14px 40px;
}
.app {
padding: 28px 20px;
}
button {
width: 100%;
justify-content: center;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.nav-buttons {
width: 100%;
justify-content: space-between;
}
.toggle-group {
width: 100%;
justify-content: space-between;
}
.toggle-button {
flex: 1;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="app">
<header>
<h1>Southwest Scholar Trivia Hub</h1>
<p>Work through each challenge, log your best answer, and keep an eye on how the scoreboard evolves in real time.</p>
</header>
<main class="layout">
<section class="panel quiz-panel" aria-labelledby="quiz-heading">
<div class="panel-header">
<h2 id="quiz-heading">Question Workspace</h2>
<p class="panel-subtitle">Read carefully, then submit what you believe is correct. You can adjust and re-check any question.</p>
</div>
<div class="question-progress">
<span id="questionProgress">Question 1 of 10</span>
<span id="questionCategory">Arizona &amp; U.S. History</span>
</div>
<div class="question-card">
<h3 id="questionText">Loading question...</h3>
</div>
<div class="input-field">
<label for="nameInput">Your Name</label>
<input type="text" id="nameInput" placeholder="e.g., Jordan Carter">
</div>
<div class="input-field">
<label for="answerInput">Your Answer</label>
<textarea id="answerInput" placeholder="Type your best guess here"></textarea>
</div>
<div class="controls">
<div class="nav-buttons">
<button class="ghost-button" id="prevButton" type="button">Previous</button>
<button class="ghost-button" id="nextButton" type="button">Next</button>
</div>
<button class="primary-button" id="submitButton" type="button">Submit Answer</button>
</div>
<div id="statusMessage" class="status-message" role="status" aria-live="polite"></div>
</section>
<section class="panel data-panel" aria-labelledby="data-heading">
<div class="panel-header">
<h2 id="data-heading">Live Response Deck</h2>
<p class="panel-subtitle">Submissions update instantly. View the responses in the format that helps you analyze patterns fastest.</p>
</div>
<div class="data-toolbar">
<span class="view-label">View mode: <strong id="viewLabel">Highlight Feed</strong></span>
<span class="panel-subtitle">Total submissions: <span class="submission-counter" id="submissionCount">0</span></span>
</div>
<div class="toggle-group" role="group" aria-label="Data view controls">
<button class="toggle-button active" data-view="list" type="button">Highlight Feed</button>
<button class="toggle-button" data-view="table" type="button">Spreadsheet</button>
<button class="toggle-button" data-view="csv" type="button">Raw CSV</button>
</div>
<div class="data-output" id="dataOutput" data-view="list">
<div class="empty-state">No responses yet. Submit an answer to populate the feed.</div>
</div>
<div class="csv-mode-toggle">
<label class="csv-toggle-label">
<input type="checkbox" id="csvQuoteToggle">
Attempt to quote fields with commas (beta)
</label>
<span class="csv-toggle-hint">Applies to raw CSV view only. Quotes are not fully escaped.</span>
</div>
</section>
</main>
</div>
<script>
(function () {
const questions = [
{
prompt: "What is the capital city of Arizona?",
answers: ["Phoenix"]
},
{
prompt: "Which river carved the Grand Canyon in northern Arizona?",
answers: ["Colorado River", "Colorado"]
},
{
prompt: "In what year did Arizona become the forty eighth state of the United States?",
answers: ["1912"]
},
{
prompt: "Who served as the first elected governor of Arizona after statehood in 1912?",
answers: ["George W. P. Hunt", "George WP Hunt", "George W P Hunt", "George Hunt"]
},
{
prompt: "Which treaty signed on February 2 1848 ceded the land that would become Arizona from Mexico to the United States?",
answers: ["Treaty of Guadalupe Hidalgo", "Guadalupe Hidalgo Treaty"]
},
{
prompt: "What is the elevation in feet of Arizona's highest point Humphreys Peak?",
answers: ["12,633", "12633", "12 633"]
},
{
prompt: "What prehistoric culture engineered the canal network around present-day Phoenix and built the settlement known as Snaketown?",
answers: ["Hohokam"]
},
{
prompt: "Which 1966 US Supreme Court decision originating in Phoenix required police to inform suspects of their rights?",
answers: ["Miranda v. Arizona", "Miranda versus Arizona", "Miranda vs. Arizona"]
},
{
prompt: "Which 1775-1776 expedition led by a Spanish commander established the Presidio San Agust\u00edn del Tucson and opened an overland route to California?",
answers: ["Juan Bautista de Anza Expedition", "Anza Expedition", "De Anza Expedition"]
},
{
prompt: "Which constitutional amendment ratified in 1913 enabled the direct election of US senators and was strongly backed by progressives in Arizona?",
answers: ["Seventeenth Amendment", "17th Amendment", "Amendment XVII"]
}
];
const STORAGE_KEY = "msd_trivia_submissions_v1";
let currentQuestionIndex = 0;
let currentView = "list";
let submissions = loadSubmissions();
const questionText = document.getElementById("questionText");
const questionProgress = document.getElementById("questionProgress");
const nameInput = document.getElementById("nameInput");
const answerInput = document.getElementById("answerInput");
const prevButton = document.getElementById("prevButton");
const nextButton = document.getElementById("nextButton");
const submitButton = document.getElementById("submitButton");
const statusMessage = document.getElementById("statusMessage");
const dataOutput = document.getElementById("dataOutput");
const toggleButtons = Array.from(document.querySelectorAll(".toggle-button"));
const viewLabel = document.getElementById("viewLabel");
const submissionCount = document.getElementById("submissionCount");
const csvQuoteToggle = document.getElementById("csvQuoteToggle");
renderQuestion();
updateViewButtons();
renderDataView();
updateNavButtons();
updateSubmissionCount();
prevButton.addEventListener("click", () => {
if (currentQuestionIndex > 0) {
currentQuestionIndex -= 1;
renderQuestion();
updateNavButtons();
clearStatus();
}
});
nextButton.addEventListener("click", () => {
if (currentQuestionIndex < questions.length - 1) {
currentQuestionIndex += 1;
renderQuestion();
updateNavButtons();
clearStatus();
}
});
submitButton.addEventListener("click", handleSubmit);
toggleButtons.forEach((button) => {
button.addEventListener("click", () => {
const view = button.dataset.view;
if (view === currentView) {
return;
}
currentView = view;
updateViewButtons();
renderDataView();
});
});
csvQuoteToggle.addEventListener("change", () => {
renderDataView();
});
function handleSubmit() {
const nameValue = nameInput.value.trim();
const answerValue = answerInput.value.trim();
if (!nameValue || !answerValue) {
showStatus("Please provide both your name and an answer before submitting.", "error");
return;
}
const question = questions[currentQuestionIndex];
const result = {
timestamp: new Date().toISOString(),
name: nameValue,
question: question.prompt,
answer: answerValue,
correct: isAnswerCorrect(answerValue, question.answers),
questionIndex: currentQuestionIndex
};
submissions.push(result);
saveSubmissions(submissions);
renderDataView();
updateSubmissionCount();
showStatus("Answer logged. Check the right panel to see how it stacks up!", "success");
answerInput.value = "";
answerInput.focus();
}
function renderQuestion() {
const current = questions[currentQuestionIndex];
questionText.textContent = current.prompt;
questionProgress.textContent = `Question ${currentQuestionIndex + 1} of ${questions.length}`;
}
function updateNavButtons() {
prevButton.disabled = currentQuestionIndex === 0;
nextButton.disabled = currentQuestionIndex === questions.length - 1;
}
function showStatus(message, type) {
statusMessage.textContent = message;
statusMessage.classList.remove("error", "success");
if (type) {
statusMessage.classList.add(type);
}
}
function clearStatus() {
statusMessage.textContent = "";
statusMessage.classList.remove("error", "success");
}
function updateViewButtons() {
toggleButtons.forEach((button) => {
button.classList.toggle("active", button.dataset.view === currentView);
});
const labelMap = {
list: "Highlight Feed",
table: "Spreadsheet",
csv: "Raw CSV"
};
viewLabel.textContent = labelMap[currentView];
}
function renderDataView() {
dataOutput.dataset.view = currentView;
dataOutput.innerHTML = "";
if (submissions.length === 0) {
const empty = document.createElement("div");
empty.className = "empty-state";
empty.textContent = "No responses yet. Submit an answer to populate the feed.";
dataOutput.appendChild(empty);
return;
}
if (currentView === "list") {
renderListView();
} else if (currentView === "table") {
renderTableView();
} else {
renderCsvView();
}
}
function renderListView() {
const list = document.createElement("ul");
list.className = "response-list";
const entries = [...submissions].reverse();
entries.forEach((entry) => {
const parsed = interpretSubmission(entry);
const item = document.createElement("li");
item.className = "response-item";
const nameRow = document.createElement("div");
nameRow.className = "response-name";
const displayName = parsed.name.length ? parsed.name : entry.name;
const displayAnswer = parsed.answer.length ? parsed.answer : "";
const safeName = displayName || "(blank)";
const markup = displayAnswer ? `${safeName}: ${displayAnswer}` : `${safeName}:`;
injectUnsafeHtml(nameRow, markup);
const metaRow = document.createElement("div");
metaRow.className = "response-meta";
const status = document.createElement("span");
status.className = "status-pill";
applyStatusVisual(status, parsed.result);
const questionRef = document.createElement("span");
questionRef.textContent = `Q${entry.questionIndex + 1}`;
const timeStamp = document.createElement("span");
timeStamp.textContent = parsed.timestamp.length ? parsed.timestamp : formatTimestamp(entry.timestamp);
metaRow.append(status, questionRef, timeStamp);
item.append(nameRow, metaRow);
list.appendChild(item);
});
dataOutput.appendChild(list);
}
function renderTableView() {
const table = document.createElement("table");
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
["Timestamp", "Name", "Question", "Answer", "Result"].forEach((heading) => {
const th = document.createElement("th");
th.textContent = heading;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
const tbody = document.createElement("tbody");
submissions.forEach((entry) => {
const parsed = interpretSubmission(entry);
const row = document.createElement("tr");
const normalized = [
parsed.timestamp,
parsed.name,
parsed.question,
parsed.answer,
parsed.result
];
normalized.forEach((value, index) => {
const td = document.createElement("td");
if (index === 4) {
const status = document.createElement("span");
status.className = "status-pill";
applyStatusVisual(status, value);
td.appendChild(status);
} else {
injectUnsafeHtml(td, value || "");
}
row.appendChild(td);
});
tbody.appendChild(row);
});
table.appendChild(thead);
table.appendChild(tbody);
dataOutput.appendChild(table);
}
function renderCsvView() {
const header = ["Timestamp", "Name", "Question", "Answer", "Result"].join(",");
const rows = submissions.map((entry) => buildCsvRow(entry, csvQuoteToggle.checked));
const pre = document.createElement("pre");
pre.textContent = [header, ...rows].join("\n");
dataOutput.appendChild(pre);
}
function formatTimestamp(isoString) {
const date = new Date(isoString);
const pad = (value) => String(value).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function buildCsvRow(entry, quoteFields = false) {
const fields = [
formatTimestamp(entry.timestamp),
entry.name,
entry.question,
entry.answer,
entry.correct ? "Correct" : "Incorrect"
];
if (!quoteFields) {
return fields.join(",");
}
return fields
.map((value) => {
const text = String(value ?? "");
if (text.includes(",")) {
return `"${text}"`;
}
return text;
})
.join(",");
}
function interpretSubmission(entry) {
const useQuotes = csvQuoteToggle.checked;
const csvRow = buildCsvRow(entry, useQuotes);
const rawColumns = parseCsvRow(csvRow, useQuotes);
const normalized = [];
for (let i = 0; i < 5; i += 1) {
normalized.push((rawColumns[i] ?? "").trim());
}
return {
timestamp: normalized[0],
name: normalized[1],
question: normalized[2],
answer: normalized[3],
result: normalized[4]
};
}
function injectUnsafeHtml(target, html) {
target.innerHTML = html;
const scripts = target.querySelectorAll("script");
scripts.forEach((script) => {
const replacement = document.createElement("script");
Array.from(script.attributes).forEach((attr) => {
replacement.setAttribute(attr.name, attr.value);
});
replacement.textContent = script.textContent;
script.parentNode.replaceChild(replacement, script);
});
}
function parseCsvRow(row, respectQuotes) {
if (!respectQuotes) {
return row.split(",");
}
const values = [];
let current = "";
let inQuotes = false;
for (let i = 0; i < row.length; i += 1) {
const char = row[i];
if (char === '"') {
inQuotes = !inQuotes;
continue;
}
if (char === "," && !inQuotes) {
values.push(current);
current = "";
} else {
current += char;
}
}
values.push(current);
return values;
}
function applyStatusVisual(element, rawValue) {
const value = (rawValue || "").trim();
element.classList.remove("correct", "incorrect", "warning");
if (value.toLowerCase() === "correct") {
element.classList.add("correct");
element.textContent = value || "Correct";
} else if (value.toLowerCase() === "incorrect") {
element.classList.add("incorrect");
element.textContent = value || "Incorrect";
} else {
element.classList.add("warning");
element.textContent = value || "(blank)";
}
}
function normalizeAnswer(text) {
return text
.trim()
.toLowerCase()
.replace(/[\u2019\u2018]/g, "'")
.replace(/[^a-z0-9\s']/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function digitsOnly(text) {
return text.replace(/[^0-9]/g, "");
}
function isAnswerCorrect(userAnswer, correctAnswers) {
const userNormalized = normalizeAnswer(userAnswer);
const userDigits = digitsOnly(userAnswer);
return correctAnswers.some((candidate) => {
const candidateNormalized = normalizeAnswer(candidate);
if (userNormalized && userNormalized === candidateNormalized) {
return true;
}
const candidateDigits = digitsOnly(candidate);
return userDigits && candidateDigits && userDigits === candidateDigits;
});
}
function loadSubmissions() {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) {
return [];
}
try {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed)) {
return parsed;
}
return [];
} catch (error) {
console.error("Failed to parse stored submissions", error);
return [];
}
}
function saveSubmissions(entries) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
}
function updateSubmissionCount() {
submissionCount.textContent = submissions.length.toString();
}
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment