@@ -143,11 +143,42 @@ function UserCredentialsTimelinePage() {
143143 return b . eventTime . localeCompare ( a . eventTime ) ;
144144 } ) ;
145145 } , [ selectedCredentialIds , credentialDetailsById ] ) ;
146- 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 ) => ( _jsxs ( "div" , { className : "flex cursor-pointer items-start gap-3 rounded border border-slate-200 bg-white p-3 hover:bg-slate-50" , children : [ _jsx ( "input" , { type : "checkbox" , className : "mt-1" , "aria-label" : `Select snapshot ${ credential . credentialId } ` , checked : selectedCredentialIds . includes ( credential . credentialId ) , onChange : ( event ) => toggleCredentialSelection ( credential . credentialId , event . target . checked ) } ) , _jsxs ( "div" , { children : [ _jsx ( "p" , { className : "font-medium" , children : credential . credentialId } ) , _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 ) ) ) ] } ) , _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." } ) ) , mergedEvents . length > 0 && ( _jsx ( "div" , { className : "space-y-0" , children : mergedEvents . map ( ( timelineEvent , index ) => ( _jsx ( EventTimelineItem , { event : timelineEvent , isLast : index === mergedEvents . length - 1 } , timelineEvent . eventKey ) ) ) } ) ) ] } ) ] } ) ) : null ] } ) } ) ) ;
146+ const groupedEvents = useMemo ( ( ) => groupEventsBySecond ( mergedEvents ) , [ mergedEvents ] ) ;
147+ const credentialColorsById = useMemo ( ( ) => {
148+ const map = { } ;
149+ selectedCredentialIds . forEach ( ( credentialId , index ) => {
150+ map [ credentialId ] = pickCredentialColor ( index ) ;
151+ } ) ;
152+ return map ;
153+ } , [ selectedCredentialIds ] ) ;
154+ 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 ) => ( ( ( ) => {
155+ const selected = selectedCredentialIds . includes ( credential . credentialId ) ;
156+ const color = credentialColorsById [ credential . credentialId ] ;
157+ 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 ? {
158+ borderLeftWidth : 6 ,
159+ borderLeftColor : color . border ,
160+ 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 ) ) ;
162+ } ) ( ) ) ) ] } ) , _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 ] } ) } ) ) ;
147163}
148- function EventTimelineItem ( props ) {
149- const { event, isLast } = props ;
150- return ( _jsxs ( "div" , { className : "relative pl-8" , children : [ ! isLast && _jsx ( "span" , { className : "absolute left-[11px] top-6 h-[calc(100%-8px)] w-px bg-slate-300" , "aria-hidden" : true } ) , _jsx ( "span" , { className : "absolute left-2 top-3 h-2 w-2 rounded-full bg-blue-600" , "aria-hidden" : true } ) , _jsxs ( "div" , { className : "mb-4 rounded border border-slate-200 bg-slate-50 p-3" , children : [ _jsxs ( "p" , { className : "font-medium" , children : [ event . actorLabel , " " , event . actionLabel , " " , event . objectLabel ] } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "When: " , formatTimestamp ( event . eventTime ) ] } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "Category: " , toDisplayEventType ( event . eventType ) , " | Snapshot: " , toDisplayRef ( event . credentialId ) ] } ) ] } ) ] } ) ) ;
164+ const CREDENTIAL_COLORS = [
165+ { border : "#7c9cf5" , bg : "#f5f7ff" , chip : "#dbe5ff" } ,
166+ { border : "#5aa4b1" , bg : "#f2fbfd" , chip : "#d3eff4" } ,
167+ { border : "#a786d9" , bg : "#faf6ff" , chip : "#eadcfb" } ,
168+ { border : "#69a86f" , bg : "#f3fbf4" , chip : "#d9f1dc" } ,
169+ { border : "#c58b5c" , bg : "#fdf7f2" , chip : "#f4e1d2" } ,
170+ { border : "#c67a9b" , bg : "#fef5f9" , chip : "#f8d8e6" }
171+ ] ;
172+ function TimelineSecondGroup ( props ) {
173+ const { group, isLast, credentialColorsById } = props ;
174+ return ( _jsxs ( "div" , { className : "relative pb-2 pl-8" , children : [ ! isLast && _jsx ( "span" , { className : "absolute left-[11px] top-7 h-[calc(100%-6px)] w-px bg-slate-300" , "aria-hidden" : true } ) , _jsx ( "span" , { className : "absolute left-2 top-4 h-2 w-2 rounded-full bg-blue-600" , "aria-hidden" : true } ) , _jsx ( "p" , { className : "mb-2 pt-1 text-xs font-semibold text-slate-700" , children : formatTimestampToSecond ( group . minuteKey ) } ) , _jsx ( "div" , { className : "space-y-2 pb-2" , children : group . events . map ( ( event ) => {
175+ const color = credentialColorsById [ event . credentialId ] ;
176+ return ( _jsxs ( "div" , { className : "rounded border border-slate-200 p-3" , style : color ? {
177+ borderLeftWidth : 6 ,
178+ borderLeftColor : color . border ,
179+ backgroundColor : color . bg
180+ } : undefined , children : [ _jsxs ( "p" , { className : "font-medium" , children : [ event . actorLabel , " " , event . actionLabel , " " , event . objectLabel ] } ) , _jsxs ( "p" , { className : "text-xs text-slate-600" , children : [ "Category: " , toDisplayEventType ( event . eventType ) , " | Snapshot: " , toDisplayRef ( event . credentialId ) ] } ) ] } , event . eventKey ) ) ;
181+ } ) } ) ] } ) ) ;
151182}
152183function extractCredentialEvents ( detail ) {
153184 const payload = detail . rawPayload ;
@@ -251,6 +282,55 @@ function formatTimestamp(timestamp) {
251282 }
252283 return asDate . toLocaleString ( ) ;
253284}
285+ function formatTimestampToSecond ( timestamp ) {
286+ if ( ! timestamp ) {
287+ return "unknown time" ;
288+ }
289+ const asDate = new Date ( timestamp ) ;
290+ if ( Number . isNaN ( asDate . getTime ( ) ) ) {
291+ return timestamp ;
292+ }
293+ return asDate . toLocaleString ( undefined , {
294+ year : "numeric" ,
295+ month : "short" ,
296+ day : "numeric" ,
297+ hour : "numeric" ,
298+ minute : "2-digit" ,
299+ second : "2-digit"
300+ } ) ;
301+ }
302+ function toMinuteKey ( timestamp ) {
303+ const asDate = new Date ( timestamp ) ;
304+ if ( Number . isNaN ( asDate . getTime ( ) ) ) {
305+ return timestamp ;
306+ }
307+ return new Date ( Math . floor ( asDate . getTime ( ) / 60000 ) * 60000 ) . toISOString ( ) ;
308+ }
309+ function groupEventsBySecond ( events ) {
310+ const groupsByMinute = new Map ( ) ;
311+ const orderedMinuteKeys = [ ] ;
312+ for ( const event of events ) {
313+ const minuteKey = toMinuteKey ( event . eventTime ) ;
314+ const existing = groupsByMinute . get ( minuteKey ) ;
315+ if ( existing ) {
316+ existing . push ( event ) ;
317+ continue ;
318+ }
319+ groupsByMinute . set ( minuteKey , [ event ] ) ;
320+ orderedMinuteKeys . push ( minuteKey ) ;
321+ }
322+ return orderedMinuteKeys . map ( ( minuteKey ) => ( {
323+ minuteKey,
324+ events : groupsByMinute . get ( minuteKey ) ?? [ ]
325+ } ) ) ;
326+ }
327+ function pickCredentialColor ( index ) {
328+ const color = CREDENTIAL_COLORS [ index % CREDENTIAL_COLORS . length ] ;
329+ if ( color ) {
330+ return color ;
331+ }
332+ return { border : "#7c9cf5" , bg : "#f5f7ff" , chip : "#dbe5ff" } ;
333+ }
254334function LearnerPage ( ) {
255335 const { learnerId = "demo-learner" } = useParams ( ) ;
256336 const [ context , setContext ] = useState ( null ) ;
0 commit comments