Skip to content

Commit d30ad14

Browse files
author
committed
Deployed 80bd399 with MkDocs version: 1.6.1
0 parents  commit d30ad14

238 files changed

Lines changed: 169024 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.nojekyll

Whitespace-only changes.

404.html

Lines changed: 1755 additions & 0 deletions
Large diffs are not rendered by default.

admin.html

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Quiz Progress — Teacher Dashboard</title>
6+
<style>
7+
body { font-family: sans-serif; max-width: 1100px; margin: 2rem auto; padding: 0 1rem; }
8+
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
9+
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
10+
th { background: #f4f4f4; }
11+
tr:nth-child(even) { background: #fafafa; }
12+
select, input, textarea { padding: 6px; margin: 0 8px 1rem 0; }
13+
#auth-bar { margin-bottom: 1.5rem; }
14+
#message { color: #666; font-style: italic; }
15+
#request-section { border: 1px solid #ddd; border-radius: 6px; padding: 1rem 1.25rem; max-width: 480px; margin-bottom: 1.5rem; }
16+
#request-section textarea { display: block; width: 100%; box-sizing: border-box; min-height: 4.5rem; margin: 0.5rem 0; }
17+
#request-status { font-style: italic; color: #666; }
18+
</style>
19+
</head>
20+
<body>
21+
<h1>Quiz Progress Dashboard</h1>
22+
23+
<div id="auth-bar">
24+
<button id="sign-in-btn" onclick="signIn()" style="display:none">Sign in with GitHub</button>
25+
<span id="user-label"></span>
26+
</div>
27+
28+
<div id="request-section" style="display:none">
29+
<h3 style="margin-top:0">Request instructor access</h3>
30+
<p id="request-status"></p>
31+
<div id="request-form">
32+
<textarea id="request-reason" placeholder="Why do you need access to quiz results? (optional)"></textarea>
33+
<button onclick="submitTeacherRequest()">Request access</button>
34+
</div>
35+
</div>
36+
37+
<div id="controls" style="display:none">
38+
<label>Filter by student:
39+
<input id="filter-student" type="text" placeholder="github login">
40+
</label>
41+
<label>Filter by quiz:
42+
<input id="filter-quiz" type="text" placeholder="quiz id">
43+
</label>
44+
<button onclick="loadResults()">Refresh</button>
45+
</div>
46+
47+
<p id="message"></p>
48+
49+
<table id="results-table" style="display:none">
50+
<thead>
51+
<tr>
52+
<th>Student</th>
53+
<th>GitHub Login</th>
54+
<th>Quiz ID</th>
55+
<th>Page</th>
56+
<th>Score</th>
57+
<th>Pct</th>
58+
<th>Submitted</th>
59+
</tr>
60+
</thead>
61+
<tbody id="results-body"></tbody>
62+
</table>
63+
64+
<script>
65+
const SUPABASE_URL = 'https://wltmawdleuvcjxqtzkmj.supabase.co';
66+
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndsdG1hd2RsZXV2Y2p4cXR6a21qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY4MDI5MDEsImV4cCI6MjA5MjM3ODkwMX0.MGZavTM0z6n1RjOJaxR1jj1Emr0zsKCPt38L4k_CO5U';
67+
68+
const projectId = new URL(SUPABASE_URL).hostname.split('.')[0];
69+
const SESSION_KEY = `sb-${projectId}-auth-token`;
70+
71+
function handleOAuthCallback() {
72+
const hash = window.location.hash;
73+
if (!hash.includes('access_token=')) return;
74+
75+
const params = new URLSearchParams(hash.slice(1));
76+
const accessToken = params.get('access_token');
77+
const refreshToken = params.get('refresh_token');
78+
const expiresIn = parseInt(params.get('expires_in') || '3600', 10);
79+
80+
if (!accessToken) return;
81+
82+
fetch(`${SUPABASE_URL}/auth/v1/user`, {
83+
headers: {
84+
'apikey': SUPABASE_ANON_KEY,
85+
'Authorization': `Bearer ${accessToken}`,
86+
},
87+
})
88+
.then(r => r.json())
89+
.then(user => {
90+
const session = {
91+
access_token: accessToken,
92+
refresh_token: refreshToken,
93+
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
94+
user,
95+
};
96+
localStorage.setItem(SESSION_KEY, JSON.stringify(session));
97+
history.replaceState(null, '', window.location.pathname + window.location.search);
98+
init(); // re-run now that session is stored
99+
});
100+
}
101+
102+
function getSession() {
103+
try {
104+
return JSON.parse(localStorage.getItem(SESSION_KEY));
105+
} catch (_) { return null; }
106+
}
107+
108+
function isSessionValid(session) {
109+
if (!session?.access_token) return false;
110+
if (!session.expires_at) return false;
111+
const nowInSeconds = Math.floor(Date.now() / 1000);
112+
return session.expires_at > nowInSeconds;
113+
}
114+
115+
async function refreshSession(session) {
116+
if (!session?.refresh_token) return null;
117+
118+
try {
119+
const res = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
120+
method: 'POST',
121+
headers: {
122+
'Content-Type': 'application/json',
123+
'apikey': SUPABASE_ANON_KEY,
124+
},
125+
body: JSON.stringify({
126+
refresh_token: session.refresh_token,
127+
}),
128+
});
129+
130+
if (!res.ok) return null;
131+
132+
const newSession = await res.json();
133+
const updated = {
134+
access_token: newSession.access_token,
135+
refresh_token: newSession.refresh_token || session.refresh_token,
136+
expires_at: Math.floor(Date.now() / 1000) + (newSession.expires_in || 3600),
137+
user: session.user,
138+
};
139+
localStorage.setItem(SESSION_KEY, JSON.stringify(updated));
140+
return updated;
141+
} catch (_) {
142+
return null;
143+
}
144+
}
145+
146+
function signIn() {
147+
const redirectTo = window.location.origin + window.location.pathname + window.location.search;
148+
const url = `${SUPABASE_URL}/auth/v1/authorize?provider=github&redirect_to=${encodeURIComponent(redirectTo)}`;
149+
window.location.href = url;
150+
}
151+
152+
function signOut() {
153+
localStorage.removeItem(SESSION_KEY);
154+
init();
155+
}
156+
157+
async function loadResults() {
158+
const session = getSession();
159+
if (!session?.access_token) return;
160+
161+
const student = document.getElementById('filter-student').value.trim();
162+
const quiz = document.getElementById('filter-quiz').value.trim();
163+
164+
let url = `${SUPABASE_URL}/rest/v1/quiz_results?order=submitted_at.desc&limit=500`;
165+
if (student) url += `&github_login=eq.${encodeURIComponent(student)}`;
166+
if (quiz) url += `&quiz_id=eq.${encodeURIComponent(quiz)}`;
167+
168+
const res = await fetch(url, {
169+
headers: {
170+
'apikey': SUPABASE_ANON_KEY,
171+
'Authorization': `Bearer ${session.access_token}`,
172+
}
173+
});
174+
175+
// If token expired during the API call, handle it
176+
if (res.status === 401) {
177+
localStorage.removeItem(SESSION_KEY);
178+
document.getElementById('message').textContent = 'Session expired. Please sign in again.';
179+
document.getElementById('sign-in-btn').style.display = '';
180+
document.getElementById('user-label').innerHTML = '<button onclick="signIn()" style="padding:4px 8px;cursor:pointer;">Sign in</button>';
181+
document.getElementById('controls').style.display = 'none';
182+
document.getElementById('results-table').style.display = 'none';
183+
document.getElementById('request-section').style.display = 'none';
184+
return;
185+
}
186+
187+
const rows = await res.json();
188+
const msg = document.getElementById('message');
189+
190+
if (!Array.isArray(rows) || rows.length === 0) {
191+
msg.textContent = rows.length === 0
192+
? 'No results found.'
193+
: 'No results returned — your GitHub account may not be in the teachers table.';
194+
document.getElementById('results-table').style.display = 'none';
195+
return;
196+
}
197+
198+
msg.textContent = '';
199+
const table = document.getElementById('results-table');
200+
table.style.display = '';
201+
document.getElementById('results-body').innerHTML = rows.map(r => `
202+
<tr>
203+
<td>${r.student_name}</td>
204+
<td><a href="https://github.com/${r.github_login}" target="_blank">${r.github_login}</a></td>
205+
<td>${r.quiz_id}</td>
206+
<td>${r.page_url}</td>
207+
<td>${r.score} / ${r.total}</td>
208+
<td>${r.pct}%</td>
209+
<td>${new Date(r.submitted_at).toLocaleString()}</td>
210+
</tr>
211+
`).join('');
212+
}
213+
214+
async function checkTeacherRequestStatus(session) {
215+
const section = document.getElementById('request-section');
216+
const status = document.getElementById('request-status');
217+
const form = document.getElementById('request-form');
218+
section.style.display = '';
219+
220+
const res = await fetch(
221+
`${SUPABASE_URL}/rest/v1/teacher_requests?user_id=eq.${session.user.id}&select=status,requested_at&order=requested_at.desc&limit=1`,
222+
{
223+
headers: {
224+
'apikey': SUPABASE_ANON_KEY,
225+
'Authorization': `Bearer ${session.access_token}`,
226+
},
227+
}
228+
);
229+
const rows = res.ok ? await res.json() : [];
230+
const latest = rows[0];
231+
232+
if (!latest || latest.status === 'denied') {
233+
status.textContent = latest?.status === 'denied'
234+
? 'Your previous request was denied. You can submit a new one below.'
235+
: '';
236+
form.style.display = '';
237+
} else if (latest.status === 'pending') {
238+
status.textContent = `Request submitted ${new Date(latest.requested_at).toLocaleString()} — waiting on review.`;
239+
form.style.display = 'none';
240+
} else if (latest.status === 'approved') {
241+
status.textContent = 'Your request was approved — you should already have access above.';
242+
form.style.display = 'none';
243+
}
244+
}
245+
246+
async function submitTeacherRequest() {
247+
const session = getSession();
248+
if (!session?.access_token) return;
249+
250+
const reason = document.getElementById('request-reason').value.trim();
251+
const user = session.user;
252+
253+
const res = await fetch(`${SUPABASE_URL}/rest/v1/teacher_requests`, {
254+
method: 'POST',
255+
headers: {
256+
'Content-Type': 'application/json',
257+
'apikey': SUPABASE_ANON_KEY,
258+
'Authorization': `Bearer ${session.access_token}`,
259+
'Prefer': 'return=minimal',
260+
},
261+
body: JSON.stringify({
262+
user_id: user.id,
263+
github_login: user.user_metadata?.user_name || null,
264+
full_name: user.user_metadata?.full_name || null,
265+
reason: reason || null,
266+
}),
267+
});
268+
269+
if (res.ok) {
270+
await checkTeacherRequestStatus(session);
271+
} else if (res.status === 409) {
272+
document.getElementById('request-status').textContent = 'You already have a pending request.';
273+
} else {
274+
document.getElementById('request-status').textContent = 'Could not submit request — please try again later.';
275+
}
276+
}
277+
278+
async function init() {
279+
let session = getSession();
280+
const msg = document.getElementById('message');
281+
282+
// Check if session exists and is valid
283+
if (session && !isSessionValid(session)) {
284+
// Try to refresh the token
285+
session = await refreshSession(session);
286+
if (!session) {
287+
localStorage.removeItem(SESSION_KEY);
288+
}
289+
}
290+
291+
if (!session?.access_token) {
292+
document.getElementById('sign-in-btn').style.display = '';
293+
document.getElementById('user-label').innerHTML = '<button onclick="signIn()" style="padding:4px 8px;cursor:pointer;">Sign in</button>';
294+
msg.textContent = 'Sign in with your GitHub account to view results.';
295+
document.getElementById('controls').style.display = 'none';
296+
document.getElementById('results-table').style.display = 'none';
297+
return;
298+
}
299+
300+
const login = session.user?.user_metadata?.user_name || session.user?.email;
301+
document.getElementById('user-label').innerHTML = `Signed in as <strong>${login}</strong> <button onclick="signOut()" style="padding:4px 8px;margin-left:1rem;cursor:pointer;">Sign out</button>`;
302+
document.getElementById('controls').style.display = '';
303+
await checkTeacherRequestStatus(session);
304+
await loadResults();
305+
}
306+
307+
if (window.location.hash.includes('access_token=')) {
308+
handleOAuthCallback(); // calls init() when the session is stored
309+
} else {
310+
init();
311+
}
312+
</script>
313+
</body>
314+
</html>

0 commit comments

Comments
 (0)