@@ -154,11 +154,15 @@ function UserCredentialsTimelinePage() {
154154 return ( _jsx ( "div" , { className : "space-y-4" , children : _jsxs ( Card , { children : [ _jsxs ( "div" , { className : "mb-3 flex items-center justify-between gap-2" , children : [ _jsxs ( "div" , { children : [ _jsx ( "h2" , { className : "font-semibold" , children : "Credential Timeline" } ) , _jsxs ( "p" , { className : "text-sm text-slate-600" , children : [ "User: " , userId || "unknown" ] } ) ] } ) , _jsxs ( "div" , { className : "flex items-center gap-2" , children : [ _jsx ( Link , { to : "/users" , className : "text-sm text-blue-700 underline" , children : "Back to users" } ) , _jsx ( Button , { onClick : loadTimeline , disabled : loading , children : loading ? "Loading..." : "Refresh" } ) ] } ) ] } ) , error && _jsx ( "p" , { className : "mb-3 text-sm text-red-700" , children : error } ) , ! loading && ! error && payload ?. caliperCredentials . length === 0 && ( _jsx ( "p" , { className : "text-sm text-slate-600" , children : "No credentials found for this user." } ) ) , payload ?. caliperCredentials . length ? ( _jsxs ( "div" , { className : "grid gap-4 lg:grid-cols-[1fr_1.4fr]" , children : [ _jsxs ( "div" , { className : "space-y-2" , children : [ _jsx ( "h3" , { className : "font-semibold" , children : "Snapshots" } ) , _jsx ( "p" , { className : "text-xs text-slate-600" , children : "Select zero, one, or multiple snapshots." } ) , payload . caliperCredentials . map ( ( credential ) => ( ( ( ) => {
155155 const selected = selectedCredentialIds . includes ( credential . credentialId ) ;
156156 const color = credentialColorsById [ credential . credentialId ] ;
157+ const detail = credentialDetailsById [ credential . credentialId ] ;
158+ const story = detail
159+ ? buildCredentialStory ( detail )
160+ : buildPlaceholderStory ( credential ) ;
157161 return ( _jsxs ( "div" , { className : "flex cursor-pointer items-start gap-3 rounded border border-slate-200 bg-white p-3 hover:bg-slate-50" , style : selected && color ? {
158162 borderLeftWidth : 6 ,
159163 borderLeftColor : color . border ,
160164 backgroundColor : color . bg
161- } : undefined , children : [ _jsx ( "input" , { type : "checkbox" , className : "mt-1" , "aria-label" : `Select snapshot ${ credential . credentialId } ` , checked : selected , onChange : ( event ) => toggleCredentialSelection ( credential . credentialId , event . target . checked ) } ) , _jsxs ( "div" , { children : [ _jsxs ( "p" , { className : "font-medium" , children : [ credential . credentialId , selected && color && ( _jsx ( "span" , { className : "ml-2 inline-block h-2.5 w-2.5 rounded-full align-middle" , style : { backgroundColor : color . chip } , "aria-hidden" : true } ) ) ] } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "Issued: " , formatTimestamp ( credential . issuedAt ) ] } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "Issuer: " , credential . issuerId ] } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "Events: " , credential . eventCount ] } ) ] } ) ] } , credential . credentialId ) ) ;
165+ } : undefined , children : [ _jsx ( "input" , { type : "checkbox" , className : "mt-1" , "aria-label" : `Select snapshot ${ credential . credentialId } ` , checked : selected , onChange : ( event ) => toggleCredentialSelection ( credential . credentialId , event . target . checked ) } ) , _jsxs ( "div" , { children : [ _jsxs ( "p" , { className : "font-medium" , children : [ credential . credentialId , selected && color && ( _jsx ( "span" , { className : "ml-2 inline-block h-2.5 w-2.5 rounded-full align-middle" , style : { backgroundColor : color . chip } , "aria-hidden" : true } ) ) ] } ) , _jsxs ( "div" , { className : "mb-1 mt-1 flex items-center gap-2" , children : [ _jsx ( "span" , { className : "rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-700" , children : story . tag } ) , _jsx ( "span" , { className : "text-xs text-slate-700" , children : story . title } ) ] } ) , _jsx ( "p" , { className : "text-xs text-slate-600" , children : story . subtitle } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "Issued: " , formatTimestamp ( credential . issuedAt ) ] } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "Issuer: " , credential . issuerId ] } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "Events: " , credential . eventCount ] } ) ] } ) ] } , credential . credentialId ) ) ;
162166 } ) ( ) ) ) ] } ) , _jsxs ( "div" , { className : "space-y-2" , children : [ _jsx ( "h3" , { className : "font-semibold" , children : "Event Timeline" } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "Selected snapshots: " , selectedCredentialIds . length , " | Events: " , mergedEvents . length ] } ) , detailsError && _jsx ( "p" , { className : "text-sm text-red-700" , children : detailsError } ) , detailsLoading && _jsx ( "p" , { className : "text-sm text-slate-600" , children : "Loading selected snapshot events..." } ) , selectedCredentialIds . length === 0 && ( _jsx ( "p" , { className : "rounded border border-dashed border-slate-300 p-3 text-sm text-slate-600" , children : "Select one or more snapshots to render the timeline." } ) ) , selectedCredentialIds . length > 0 && ! detailsLoading && mergedEvents . length === 0 && ( _jsx ( "p" , { className : "rounded border border-dashed border-slate-300 p-3 text-sm text-slate-600" , children : "No events were found in the selected snapshots." } ) ) , groupedEvents . length > 0 && ( _jsx ( "div" , { className : "space-y-0" , children : groupedEvents . map ( ( group , index ) => ( _jsx ( TimelineSecondGroup , { group : group , isLast : index === groupedEvents . length - 1 , credentialColorsById : credentialColorsById } , group . minuteKey ) ) ) } ) ) ] } ) ] } ) ) : null ] } ) } ) ) ;
163167}
164168const CREDENTIAL_COLORS = [
@@ -181,12 +185,7 @@ function TimelineSecondGroup(props) {
181185 } ) } ) ] } ) ) ;
182186}
183187function extractCredentialEvents ( detail ) {
184- const payload = detail . rawPayload ;
185- if ( ! payload || typeof payload !== "object" ) {
186- return [ ] ;
187- }
188- const credentialSubject = asRecord ( payload . credentialSubject ) ;
189- const events = Array . isArray ( credentialSubject ?. events ) ? credentialSubject . events : [ ] ;
188+ const events = readCredentialSubjectEvents ( detail ) ;
190189 return events
191190 . map ( ( item , index ) => {
192191 const event = asRecord ( item ) ;
@@ -213,6 +212,64 @@ function extractCredentialEvents(detail) {
213212 } )
214213 . filter ( ( event ) => Boolean ( event ) ) ;
215214}
215+ function buildCredentialStory ( detail ) {
216+ const events = readCredentialSubjectEvents ( detail )
217+ . map ( ( item ) => asRecord ( item ) )
218+ . filter ( ( event ) => Boolean ( event ) ) ;
219+ if ( events . length === 0 ) {
220+ return {
221+ tag : "Learning Story" ,
222+ title : "No event detail" ,
223+ subtitle : "This snapshot does not include event-level activity."
224+ } ;
225+ }
226+ const typeSet = new Set ( ) ;
227+ const actionSet = new Set ( ) ;
228+ for ( const event of events ) {
229+ if ( typeof event . type === "string" ) {
230+ typeSet . add ( event . type ) ;
231+ }
232+ if ( typeof event . action === "string" ) {
233+ actionSet . add ( event . action ) ;
234+ }
235+ }
236+ const types = [ ...typeSet ] ;
237+ const actions = [ ...actionSet ] ;
238+ const hasType = ( value ) => types . some ( ( type ) => type . toLowerCase ( ) . includes ( value ) ) ;
239+ let tag = "Learning Story" ;
240+ if ( hasType ( "assessment" ) )
241+ tag = "Assessment Story" ;
242+ else if ( hasType ( "assignable" ) )
243+ tag = "Assignment Story" ;
244+ else if ( hasType ( "session" ) )
245+ tag = "Session Story" ;
246+ else if ( hasType ( "navigation" ) || hasType ( "view" ) )
247+ tag = "Engagement Story" ;
248+ const primaryType = types [ 0 ] ? toDisplayEventType ( types [ 0 ] ) : "activity" ;
249+ const actionSummary = actions . length > 0
250+ ? actions . slice ( 0 , 3 ) . map ( ( action ) => action . toLowerCase ( ) ) . join ( ", " )
251+ : "captured activity" ;
252+ return {
253+ tag,
254+ title : `${ events . length } events in ${ primaryType } ` ,
255+ subtitle : `Story includes ${ actionSummary } ${ actions . length > 3 ? ", and more" : "" } .`
256+ } ;
257+ }
258+ function buildPlaceholderStory ( credential ) {
259+ return {
260+ tag : "Learning Story" ,
261+ title : `${ credential . eventCount } events captured` ,
262+ subtitle : "Select this snapshot to load detailed activity story."
263+ } ;
264+ }
265+ function readCredentialSubjectEvents ( detail ) {
266+ const payload = detail . rawPayload ;
267+ if ( ! payload || typeof payload !== "object" ) {
268+ return [ ] ;
269+ }
270+ const credentialSubject = asRecord ( payload . credentialSubject ) ;
271+ return Array . isArray ( credentialSubject ?. events ) ? credentialSubject . events : [ ] ;
272+ }
216273function asRecord ( value ) {
217274 if ( ! value || typeof value !== "object" ) {
218275 return undefined ;
0 commit comments