@@ -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,241 @@ <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" onclick="viewSubmissions(\'' + a . id + '\')" class="text-xs text-indigo-500 hover:underline">Submissions (' + ( a . submission_count || 0 ) + ')</button>'
259+ : ( token ? '<button type="button" onclick="showSubmitForm(\'' + a . id + '\', \'' + esc2 ( a . title ) + '\')" class="text-xs text-indigo-500 hover:underline">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" onclick="showGradeForm(\'' + s . id + '\')" class="text-xs text-indigo-500 hover:underline">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+ hideGradeForm ( ) ;
383+ alert ( 'Graded successfully!' ) ;
384+ } else { alert ( data . error || 'Failed' ) ; }
385+ } catch ( e ) { alert ( e . message ) ; }
386+ }
387+
388+ // Load assignments after activity loads
389+ const _origLoadActivity = loadActivity ;
390+ async function loadActivity ( ) {
391+ await _origLoadActivity . call ( this , ...arguments ) ;
392+ }
393+
394+ document . addEventListener ( 'DOMContentLoaded' , function ( ) {
395+ if ( actId ) loadAssignments ( ) ;
396+ } ) ;
226397 if ( ! actId ) {
227398 document . getElementById ( 'act-title' ) . textContent = 'No activity selected' ;
228399 } else {
229400 loadActivity ( ) . catch ( e => { document . getElementById ( 'act-title' ) . textContent = 'Error: ' + e . message ; } ) ;
230401 }
231402</ script >
403+
404+ <!-- Assignments Section -->
405+ < div class ="mt-8 bg-white rounded-2xl shadow-sm border border-slate-100 p-6 " id ="assignments-section ">
406+ < div class ="flex items-center justify-between mb-4 ">
407+ < h2 class ="font-bold text-slate-800 text-lg "> 📄 Assignments</ h2 >
408+ < button id ="btn-create-assignment " 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 >
409+ </ div >
410+
411+ <!-- Create Assignment Form (host only) -->
412+ < div id ="create-assignment-form " class ="hidden mb-6 bg-slate-50 rounded-xl p-4 border border-slate-200 ">
413+ < h3 class ="font-semibold text-slate-700 mb-3 text-sm "> Create Assignment</ h3 >
414+ < 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 ">
415+ < 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 >
416+ < div class ="grid grid-cols-2 gap-2 mb-2 ">
417+ < div >
418+ < label class ="text-xs text-slate-500 mb-1 block "> Due Date (optional)</ label >
419+ < 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 ">
420+ </ div >
421+ < div >
422+ < label class ="text-xs text-slate-500 mb-1 block "> Max Score</ label >
423+ < 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 ">
424+ </ div >
425+ </ div >
426+ < div class ="flex items-center gap-4 mb-3 ">
427+ < 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 ">
428+ < option value ="draft "> Draft</ option >
429+ < option value ="published "> Published</ option >
430+ </ select >
431+ < label class ="flex items-center gap-2 text-sm text-slate-600 ">
432+ < input type ="checkbox " id ="asgn-late "> Allow late submissions
433+ </ label >
434+ </ div >
435+ < div class ="flex gap-2 ">
436+ < 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 >
437+ < 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 >
438+ </ div >
439+ </ div >
440+
441+ < div id ="assignments-list " class ="space-y-3 "> </ div >
442+
443+ <!-- Submit Assignment Form (students) -->
444+ < div id ="submit-form " class ="hidden mt-4 bg-slate-50 rounded-xl p-4 border border-slate-200 ">
445+ < h3 class ="font-semibold text-slate-700 mb-3 text-sm "> Submit Assignment: < span id ="submit-asgn-title "> </ span > </ h3 >
446+ < 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 >
447+ < div class ="flex gap-2 ">
448+ < 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 >
449+ < 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 >
450+ </ div >
451+ </ div >
452+
453+ <!-- Grade Form (host) -->
454+ < div id ="grade-form " class ="hidden mt-4 bg-slate-50 rounded-xl p-4 border border-slate-200 ">
455+ < h3 class ="font-semibold text-slate-700 mb-3 text-sm "> Grade Submission</ h3 >
456+ < 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 ">
457+ < 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 >
458+ < div class ="flex gap-2 ">
459+ < 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 >
460+ < 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 >
461+ </ div >
462+ </ div >
463+ </ div >
232464</ body >
233465</ html >
0 commit comments