-
+
@@ -248,44 +250,61 @@
My Hosted Activities
async function loadHostedActivities() {
const loadVersion = ++hostedLoadVersion;
- const res = await fetch('/api/dashboard', { headers: { Authorization: 'Bearer ' + token } });
- const data = await res.json();
- if (loadVersion !== hostedLoadVersion) return;
- hostedActivities = data.hosted_activities || [];
- document.getElementById('hosted-count').textContent = hostedActivities.length + ' activities';
+ try {
+ const res = await fetch('/api/dashboard', { headers: { Authorization: 'Bearer ' + token } });
+ let data = null;
+ try {
+ data = await res.json();
+ } catch {
+ data = null;
+ }
+ if (!res.ok) {
+ const msg = (data && data.error) ? data.error : 'Failed to load hosted activities';
+ throw new Error(msg);
+ }
+ if (loadVersion !== hostedLoadVersion) return;
+ hostedActivities = data.hosted_activities || [];
+ document.getElementById('hosted-count').textContent = hostedActivities.length + ' activities';
- const sel = document.getElementById('s-activity');
- sel.innerHTML = '
' +
- hostedActivities.map(a =>
- '
'
- ).join('');
+ const sel = document.getElementById('s-activity');
+ sel.innerHTML = '
' +
+ hostedActivities.map(a =>
+ '
'
+ ).join('');
- const list = document.getElementById('hosted-list');
- if (!hostedActivities.length) {
- list.innerHTML = '
No activities yet. Create one above!
';
- return;
+ const list = document.getElementById('hosted-list');
+ if (!hostedActivities.length) {
+ list.innerHTML = '
No activities yet. Create one above!
';
+ return;
+ }
+ list.innerHTML = hostedActivities.map(a => {
+ const tc = typeColor[a.type] || 'bg-slate-100 text-slate-600';
+ const ic = typeIcon[a.type] || 'โจ';
+ const tags = (a.tags || []).slice(0, 4).map(t =>
+ '
' + esc(t) + '').join('');
+ return '
' +
+ '
' +
+ '
' + ic + ' ' + esc(a.title) + '
' +
+ '
' +
+ '' + esc(a.type) + '' +
+ '' + (fmtLabel[a.format] || a.format) + '' +
+ '๐ฅ ' + a.participant_count + '' +
+ '๐ ' + a.session_count + ' sessions' +
+ tags +
+ '
' +
+ '
' +
+ '
' +
+ '
';
+ }).join('');
+ } catch (err) {
+ if (loadVersion !== hostedLoadVersion) return;
+ hostedActivities = [];
+ document.getElementById('hosted-count').textContent = 'Unable to load';
+ document.getElementById('s-activity').innerHTML = '
';
+ document.getElementById('hosted-list').innerHTML = '
' + esc(err.message || 'Failed to load hosted activities') + '
';
}
- list.innerHTML = hostedActivities.map(a => {
- const tc = typeColor[a.type] || 'bg-slate-100 text-slate-600';
- const ic = typeIcon[a.type] || 'โจ';
- const tags = (a.tags || []).slice(0, 4).map(t =>
- '
' + esc(t) + '').join('');
- return '
' +
- '
' +
- '
' + ic + ' ' + esc(a.title) + '
' +
- '
' +
- '' + esc(a.type) + '' +
- '' + (fmtLabel[a.format] || a.format) + '' +
- '๐ฅ ' + a.participant_count + '' +
- '๐ ' + a.session_count + ' sessions' +
- tags +
- '
' +
- '
' +
- '
' +
- '
';
- }).join('');
}
function showMsg(id, msg, isErr) {
diff --git a/src/worker.py b/src/worker.py
index c99b9f2..83adc8f 100644
--- a/src/worker.py
+++ b/src/worker.py
@@ -985,15 +985,20 @@ async def api_join(req, env):
enr_id = new_id()
try:
await env.DB.prepare(
- "INSERT INTO enrollments (id,activity_id,user_id,role)"
+ "INSERT OR IGNORE INTO enrollments (id,activity_id,user_id,role)"
" VALUES (?,?,?,?)"
).bind(enr_id, act_id, user["id"], role).run()
except Exception as e:
- if "UNIQUE" in str(e):
- return ok(None, "Already joined activity")
capture_exception(e, req, env, "api_join.insert_enrollment")
return err("Failed to join activity โ please try again", 500)
+ # Distinguish fresh join vs concurrent duplicate using the row that now exists.
+ current = await env.DB.prepare(
+ "SELECT id FROM enrollments WHERE activity_id=? AND user_id=?"
+ ).bind(act_id, user["id"]).first()
+ if current and current.id != enr_id:
+ return ok(None, "Already joined activity")
+
return ok(None, "Joined activity successfully")