@@ -180,6 +180,8 @@ <h2 class="font-bold text-slate-800 text-lg mb-3">Welcome!</h2>
180180 if ( data . is_host ) {
181181 document . getElementById ( 'act-action' ) . innerHTML =
182182 '<a href="/teach.html?activity_id=' + esc ( a . id ) + '" class="bg-white text-brand font-bold px-6 py-2.5 rounded-xl shadow hover:bg-indigo-50 transition text-sm">Manage Activity</a>' ;
183+ isHost = true ;
184+ document . getElementById ( 'btn-create-assignment' ) . classList . remove ( 'hidden' ) ;
183185 document . getElementById ( 'welcome-card' ) . querySelector ( '#welcome-text' ) . textContent =
184186 'You are the host of this activity. Use the Manage button to add sessions and update details.' ;
185187 } else if ( data . is_enrolled ) {
@@ -223,11 +225,254 @@ <h2 class="font-bold text-slate-800 text-lg mb-3">Welcome!</h2>
223225 }
224226 }
225227
228+
229+ // Assignments
230+ let currentAsgnId = null ;
231+ let currentSubId = null ;
232+ let isHost = false ;
233+
234+ function esc2 ( s ) { const d = document . createElement ( 'div' ) ; d . textContent = s || '' ; return d . innerHTML ; }
235+
236+ async function loadAssignments ( ) {
237+ if ( ! actId ) return ;
238+ try {
239+ const headers = token ? { Authorization : 'Bearer ' + token } : { } ;
240+ const res = await fetch ( '/api/activities/' + actId + '/assignments' , { headers } ) ;
241+ const data = await res . json ( ) ;
242+ if ( res . ok ) renderAssignments ( data . data || [ ] ) ;
243+ } catch ( e ) { console . error ( 'loadAssignments' , e ) ; }
244+ }
245+
246+ function renderAssignments ( assignments ) {
247+ const list = document . getElementById ( 'assignments-list' ) ;
248+ if ( ! assignments . length ) {
249+ list . innerHTML = '<p class="text-slate-400 text-sm">No assignments yet.</p>' ;
250+ return ;
251+ }
252+ list . innerHTML = assignments . map ( a => {
253+ const due = a . due_date ? '<span class="text-xs text-slate-400">Due: ' + new Date ( a . due_date ) . toLocaleDateString ( ) + '</span>' : '' ;
254+ const badge = a . status === 'published'
255+ ? '<span class="px-2 py-0.5 text-xs bg-green-100 text-green-700 rounded-full">Published</span>'
256+ : '<span class="px-2 py-0.5 text-xs bg-yellow-100 text-yellow-700 rounded-full">Draft</span>' ;
257+ const hostActions = isHost
258+ ? '<button type="button" class="asgn-view-btn text-xs text-indigo-500 hover:underline" data-id="' + a . id + '">Submissions (' + ( a . submission_count || 0 ) + ')</button>'
259+ : ( token ? '<button type="button" class="asgn-submit-btn text-xs text-indigo-500 hover:underline" data-id="' + a . id + '" data-title="' + esc2 ( a . title ) + '">Submit</button>' : '' ) ;
260+ return '<div class="bg-slate-50 rounded-xl p-4 border border-slate-100">' +
261+ '<div class="flex items-start justify-between mb-1">' +
262+ '<h3 class="font-semibold text-slate-800 text-sm">' + esc2 ( a . title ) + '</h3>' +
263+ '<div class="flex items-center gap-2">' + badge + '</div>' +
264+ '</div>' +
265+ ( a . description ? '<p class="text-xs text-slate-500 mb-2">' + esc2 ( a . description ) + '</p>' : '' ) +
266+ '<div class="flex items-center justify-between">' +
267+ '<div class="flex gap-3">' + due + '<span class="text-xs text-slate-400">Max: ' + a . max_score + ' pts</span></div>' +
268+ hostActions +
269+ '</div>' +
270+ '</div>' ;
271+ } ) . join ( '' ) ;
272+ }
273+
274+ function showCreateAssignment ( ) {
275+ document . getElementById ( 'create-assignment-form' ) . classList . remove ( 'hidden' ) ;
276+ }
277+ function hideCreateAssignment ( ) {
278+ document . getElementById ( 'create-assignment-form' ) . classList . add ( 'hidden' ) ;
279+ }
280+
281+ async function createAssignment ( ) {
282+ const title = document . getElementById ( 'asgn-title' ) . value . trim ( ) ;
283+ if ( ! title ) { alert ( 'Title is required' ) ; return ; }
284+ const payload = {
285+ title,
286+ description : document . getElementById ( 'asgn-desc' ) . value . trim ( ) ,
287+ due_date : document . getElementById ( 'asgn-due' ) . value || null ,
288+ max_score : parseInt ( document . getElementById ( 'asgn-score' ) . value ) || 100 ,
289+ status : document . getElementById ( 'asgn-status' ) . value ,
290+ allow_late : document . getElementById ( 'asgn-late' ) . checked ,
291+ } ;
292+ try {
293+ const res = await fetch ( '/api/activities/' + actId + '/assignments' , {
294+ method : 'POST' ,
295+ headers : { 'Content-Type' : 'application/json' , Authorization : 'Bearer ' + token } ,
296+ body : JSON . stringify ( payload )
297+ } ) ;
298+ const data = await res . json ( ) ;
299+ if ( res . ok ) {
300+ hideCreateAssignment ( ) ;
301+ document . getElementById ( 'asgn-title' ) . value = '' ;
302+ document . getElementById ( 'asgn-desc' ) . value = '' ;
303+ await loadAssignments ( ) ;
304+ } else { alert ( data . error || 'Failed' ) ; }
305+ } catch ( e ) { alert ( e . message ) ; }
306+ }
307+
308+ function showSubmitForm ( asgnId , title ) {
309+ currentAsgnId = asgnId ;
310+ document . getElementById ( 'submit-asgn-title' ) . textContent = title ;
311+ document . getElementById ( 'submit-form' ) . classList . remove ( 'hidden' ) ;
312+ document . getElementById ( 'submit-text' ) . focus ( ) ;
313+ }
314+ function hideSubmitForm ( ) {
315+ document . getElementById ( 'submit-form' ) . classList . add ( 'hidden' ) ;
316+ currentAsgnId = null ;
317+ }
318+
319+ async function submitAssignment ( ) {
320+ const text = document . getElementById ( 'submit-text' ) . value . trim ( ) ;
321+ if ( ! text ) { alert ( 'Please write a response' ) ; return ; }
322+ try {
323+ const res = await fetch ( '/api/assignments/' + currentAsgnId + '/submit' , {
324+ method : 'POST' ,
325+ headers : { 'Content-Type' : 'application/json' , Authorization : 'Bearer ' + token } ,
326+ body : JSON . stringify ( { text_response : text } )
327+ } ) ;
328+ const data = await res . json ( ) ;
329+ if ( res . ok ) {
330+ hideSubmitForm ( ) ;
331+ document . getElementById ( 'submit-text' ) . value = '' ;
332+ alert ( 'Submitted successfully!' ) ;
333+ await loadAssignments ( ) ;
334+ } else { alert ( data . error || 'Failed' ) ; }
335+ } catch ( e ) { alert ( e . message ) ; }
336+ }
337+
338+ async function viewSubmissions ( asgnId ) {
339+ try {
340+ const res = await fetch ( '/api/assignments/' + asgnId + '/submissions' , {
341+ headers : { Authorization : 'Bearer ' + token }
342+ } ) ;
343+ const data = await res . json ( ) ;
344+ if ( ! res . ok ) { alert ( data . error || 'Failed' ) ; return ; }
345+ const subs = data . data || [ ] ;
346+ if ( ! subs . length ) { alert ( 'No submissions yet' ) ; return ; }
347+ const list = document . getElementById ( 'assignments-list' ) ;
348+ list . innerHTML = '<button type="button" onclick="loadAssignments()" class="text-xs text-indigo-500 hover:underline mb-3 block">← Back to assignments</button>' +
349+ subs . map ( s => '<div class="bg-white rounded-xl p-4 border border-slate-200 mb-2">' +
350+ '<div class="flex justify-between mb-1">' +
351+ '<span class="font-semibold text-slate-800 text-sm">' + esc2 ( s . student_name ) + '</span>' +
352+ '<span class="text-xs ' + ( s . status === 'graded' ? 'text-green-600' : 'text-yellow-600' ) + '">' + s . status + ( s . score !== null ? ' — ' + s . score + ' pts' : '' ) + '</span>' +
353+ '</div>' +
354+ '<p class="text-xs text-slate-500 mb-2">' + esc2 ( s . text_response ) + '</p>' +
355+ '<button type="button" class="grade-btn text-xs text-indigo-500 hover:underline" data-id="' + s . id + '">Grade</button>' +
356+ '</div>' ) . join ( '' ) ;
357+ } catch ( e ) { alert ( e . message ) ; }
358+ }
359+
360+ function showGradeForm ( subId ) {
361+ currentSubId = subId ;
362+ document . getElementById ( 'grade-form' ) . classList . remove ( 'hidden' ) ;
363+ document . getElementById ( 'grade-score' ) . focus ( ) ;
364+ }
365+ function hideGradeForm ( ) {
366+ document . getElementById ( 'grade-form' ) . classList . add ( 'hidden' ) ;
367+ currentSubId = null ;
368+ }
369+
370+ async function gradeSubmission ( ) {
371+ const score = parseInt ( document . getElementById ( 'grade-score' ) . value ) ;
372+ const feedback = document . getElementById ( 'grade-feedback' ) . value . trim ( ) ;
373+ if ( isNaN ( score ) ) { alert ( 'Please enter a score' ) ; return ; }
374+ try {
375+ const res = await fetch ( '/api/submissions/' + currentSubId + '/grade' , {
376+ method : 'POST' ,
377+ headers : { 'Content-Type' : 'application/json' , Authorization : 'Bearer ' + token } ,
378+ body : JSON . stringify ( { score, feedback } )
379+ } ) ;
380+ const data = await res . json ( ) ;
381+ if ( res . ok ) {
382+ const gradedSubId = currentSubId ;
383+ hideGradeForm ( ) ;
384+ alert ( 'Graded successfully!' ) ;
385+ if ( gradedSubId ) viewSubmissions ( gradedSubId ) ;
386+ } else { alert ( data . error || 'Failed' ) ; }
387+ } catch ( e ) { alert ( e . message ) ; }
388+ }
389+
390+ // Delegated click handlers for assignment buttons
391+ document . addEventListener ( 'click' , function ( e ) {
392+ if ( e . target . classList . contains ( 'asgn-view-btn' ) ) {
393+ viewSubmissions ( e . target . dataset . id ) ;
394+ }
395+ if ( e . target . classList . contains ( 'asgn-submit-btn' ) ) {
396+ showSubmitForm ( e . target . dataset . id , e . target . dataset . title ) ;
397+ }
398+ if ( e . target . classList . contains ( 'grade-btn' ) ) {
399+ showGradeForm ( e . target . dataset . id ) ;
400+ }
401+ } ) ;
402+
403+ document . addEventListener ( 'DOMContentLoaded' , function ( ) {
404+ if ( actId ) loadAssignments ( ) ;
405+ } ) ;
226406 if ( ! actId ) {
227407 document . getElementById ( 'act-title' ) . textContent = 'No activity selected' ;
228408 } else {
229409 loadActivity ( ) . catch ( e => { document . getElementById ( 'act-title' ) . textContent = 'Error: ' + e . message ; } ) ;
230410 }
231411</ script >
412+
413+ <!-- Assignments Section -->
414+ < div class ="mt-8 bg-white rounded-2xl shadow-sm border border-slate-100 p-6 " id ="assignments-section ">
415+ < div class ="flex items-center justify-between mb-4 ">
416+ < h2 class ="font-bold text-slate-800 text-lg "> 📄 Assignments</ h2 >
417+ < button id ="btn-create-assignment " type ="button " class ="hidden bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-xl transition " onclick ="showCreateAssignment() "> + New Assignment</ button >
418+ </ div >
419+
420+ <!-- Create Assignment Form (host only) -->
421+ < div id ="create-assignment-form " class ="hidden mb-6 bg-slate-50 rounded-xl p-4 border border-slate-200 ">
422+ < h3 class ="font-semibold text-slate-700 mb-3 text-sm "> Create Assignment</ h3 >
423+ < label for ="asgn-title " class ="sr-only "> Assignment Title</ label >
424+ < input id ="asgn-title " type ="text " placeholder ="Title * " class ="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm mb-2 focus:outline-none focus:ring-2 focus:ring-indigo-300 ">
425+ < label for ="asgn-desc " class ="sr-only "> Assignment Description</ label >
426+ < textarea id ="asgn-desc " rows ="3 " placeholder ="Description (optional) " class ="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm mb-2 focus:outline-none focus:ring-2 focus:ring-indigo-300 resize-none "> </ textarea >
427+ < div class ="grid grid-cols-2 gap-2 mb-2 ">
428+ < div >
429+ < label for ="asgn-due " class ="text-xs text-slate-500 mb-1 block "> Due Date (optional)</ label >
430+ < input id ="asgn-due " type ="datetime-local " class ="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-300 ">
431+ </ div >
432+ < div >
433+ < label for ="asgn-score " class ="text-xs text-slate-500 mb-1 block "> Max Score</ label >
434+ < input id ="asgn-score " type ="number " value ="100 " min ="1 " max ="1000 " class ="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-300 ">
435+ </ div >
436+ </ div >
437+ < div class ="flex items-center gap-4 mb-3 ">
438+ < select id ="asgn-status " class ="border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-300 ">
439+ < option value ="draft "> Draft</ option >
440+ < option value ="published "> Published</ option >
441+ </ select >
442+ < label for ="asgn-late " class ="flex items-center gap-2 text-sm text-slate-600 ">
443+ < input type ="checkbox " id ="asgn-late "> Allow late submissions
444+ </ label >
445+ </ div >
446+ < div class ="flex gap-2 ">
447+ < button type ="button " onclick ="createAssignment() " class ="bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg transition "> Create</ button >
448+ < button type ="button " onclick ="hideCreateAssignment() " class ="bg-slate-100 hover:bg-slate-200 text-slate-600 text-sm font-semibold px-4 py-2 rounded-lg transition "> Cancel</ button >
449+ </ div >
450+ </ div >
451+
452+ < div id ="assignments-list " class ="space-y-3 "> </ div >
453+
454+ <!-- Submit Assignment Form (students) -->
455+ < div id ="submit-form " class ="hidden mt-4 bg-slate-50 rounded-xl p-4 border border-slate-200 ">
456+ < h3 class ="font-semibold text-slate-700 mb-3 text-sm "> Submit Assignment: < span id ="submit-asgn-title "> </ span > </ h3 >
457+ < label for ="submit-text " class ="sr-only "> Your response</ label >
458+ < textarea id ="submit-text " rows ="4 " placeholder ="Your response... " class ="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm mb-2 focus:outline-none focus:ring-2 focus:ring-indigo-300 resize-none "> </ textarea >
459+ < div class ="flex gap-2 ">
460+ < button type ="button " onclick ="submitAssignment() " class ="bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg transition "> Submit</ button >
461+ < button type ="button " onclick ="hideSubmitForm() " class ="bg-slate-100 hover:bg-slate-200 text-slate-600 text-sm font-semibold px-4 py-2 rounded-lg transition "> Cancel</ button >
462+ </ div >
463+ </ div >
464+
465+ <!-- Grade Form (host) -->
466+ < div id ="grade-form " class ="hidden mt-4 bg-slate-50 rounded-xl p-4 border border-slate-200 ">
467+ < h3 class ="font-semibold text-slate-700 mb-3 text-sm "> Grade Submission</ h3 >
468+ < label for ="grade-score " class ="sr-only "> Score</ label >
469+ < input id ="grade-score " type ="number " min ="0 " placeholder ="Score " class ="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm mb-2 focus:outline-none focus:ring-2 focus:ring-indigo-300 ">
470+ < textarea id ="grade-feedback " rows ="3 " placeholder ="Feedback (optional) " class ="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm mb-2 focus:outline-none focus:ring-2 focus:ring-indigo-300 resize-none "> </ textarea >
471+ < div class ="flex gap-2 ">
472+ < button type ="button " onclick ="gradeSubmission() " class ="bg-green-600 hover:bg-green-700 text-white text-sm font-semibold px-4 py-2 rounded-lg transition "> Save Grade</ button >
473+ < button type ="button " onclick ="hideGradeForm() " class ="bg-slate-100 hover:bg-slate-200 text-slate-600 text-sm font-semibold px-4 py-2 rounded-lg transition "> Cancel</ button >
474+ </ div >
475+ </ div >
476+ </ div >
232477</ body >
233478</ html >
0 commit comments