@@ -7,6 +7,7 @@ import type { Attempt } from "@/types/Attempt";
77import type { Question } from "@/types/Question" ;
88import {
99 fetchHistorySnapshot ,
10+ fetchHistorySnapshots ,
1011 normaliseHistorySnapshot ,
1112} from "@/api/historyService" ;
1213
@@ -42,6 +43,11 @@ const HistoryDetailPage: React.FC = () => {
4243 const [ entry , setEntry ] = useState < HistorySnapshot | null > ( initialEntry ) ;
4344 const [ loading , setLoading ] = useState ( ! initialEntry ) ;
4445 const [ error , setError ] = useState < string | null > ( null ) ;
46+ const [ attemptSnapshots , setAttemptSnapshots ] = useState < HistorySnapshot [ ] > (
47+ initialEntry ? [ initialEntry ] : [ ] ,
48+ ) ;
49+ const [ attemptsLoading , setAttemptsLoading ] = useState ( false ) ;
50+ const [ attemptsError , setAttemptsError ] = useState < string | null > ( null ) ;
4551
4652 useEffect ( ( ) => {
4753 if ( entry || ! historyId ) {
@@ -71,51 +77,137 @@ const HistoryDetailPage: React.FC = () => {
7177 return ( ) => controller . abort ( ) ;
7278 } , [ entry , historyId ] ) ;
7379
74- const attemptEntries = useMemo < Attempt [ ] > ( ( ) => {
80+ useEffect ( ( ) => {
7581 if ( ! entry ) {
76- return [ ] ;
82+ setAttemptSnapshots ( [ ] ) ;
83+ return ;
7784 }
85+ setAttemptSnapshots ( [ entry ] ) ;
86+ setAttemptsError ( null ) ;
87+ } , [ entry ] ) ;
88+
89+ useEffect ( ( ) => {
90+ if ( ! entry ?. userId || ! entry ?. questionId ) {
91+ return ;
92+ }
93+
94+ const controller = new AbortController ( ) ;
95+ setAttemptsLoading ( true ) ;
96+ fetchHistorySnapshots (
97+ {
98+ userId : entry . userId ,
99+ questionId : entry . questionId ,
100+ limit : 100 ,
101+ } ,
102+ controller . signal ,
103+ )
104+ . then ( ( result ) => {
105+ if ( controller . signal . aborted ) {
106+ return ;
107+ }
108+
109+ if ( ! result . items . length ) {
110+ setAttemptSnapshots ( entry ? [ entry ] : [ ] ) ;
111+ } else {
112+ const map = new Map < string , HistorySnapshot > ( ) ;
113+ result . items . forEach ( ( snapshot ) => map . set ( snapshot . id , snapshot ) ) ;
114+ if ( entry && ! map . has ( entry . id ) ) {
115+ map . set ( entry . id , entry ) ;
116+ }
117+ setAttemptSnapshots ( Array . from ( map . values ( ) ) ) ;
118+ }
119+ setAttemptsError ( null ) ;
120+ } )
121+ . catch ( ( err ) => {
122+ if ( ! controller . signal . aborted ) {
123+ setAttemptsError (
124+ err instanceof Error ? err . message : "Failed to load attempts" ,
125+ ) ;
126+ setAttemptSnapshots ( entry ? [ entry ] : [ ] ) ;
127+ }
128+ } )
129+ . finally ( ( ) => {
130+ if ( ! controller . signal . aborted ) {
131+ setAttemptsLoading ( false ) ;
132+ }
133+ } ) ;
78134
79- const attemptTimestamp =
80- entry . sessionEndedAt ?? entry . updatedAt ?? entry . createdAt ?? new Date ( ) ;
81-
82- const timeTakenLabel = formatDuration ( entry ) ;
83-
84- const baseQuestion : Question = {
85- title : entry . questionTitle || "Untitled Question" ,
86- body : "" ,
87- topics : entry . topics ?? [ ] ,
88- hints : [ ] ,
89- answer : "" ,
90- difficulty : entry . difficulty ?? "Unknown" ,
91- timeLimit :
92- typeof entry . timeLimit === "number"
93- ? `${ entry . timeLimit } min`
94- : ( entry . timeLimit ?? "—" ) ,
95- } ;
96-
97- const partners = entry . participants . filter (
98- ( participant ) => participant !== entry . userId ,
99- ) ;
100- const targets = partners . length > 0 ? partners : [ entry . userId ] ;
101-
102- return targets . map ( ( partner , index ) => ( {
103- id : `${ entry . id } -${ partner } -${ index } ` ,
104- question : baseQuestion ,
105- date : attemptTimestamp ,
106- partner,
107- timeTaken : timeTakenLabel ,
108- } ) ) ;
135+ return ( ) => controller . abort ( ) ;
109136 } , [ entry ] ) ;
110137
138+ const attemptEntries = useMemo < Attempt [ ] > ( ( ) => {
139+ if ( ! attemptSnapshots . length ) {
140+ return [ ] ;
141+ }
142+
143+ const sortedSnapshots = [ ...attemptSnapshots ] . sort ( ( a , b ) => {
144+ const timeA =
145+ ( a . sessionEndedAt ?? a . updatedAt ?? a . createdAt ) ?. getTime ( ) ?? 0 ;
146+ const timeB =
147+ ( b . sessionEndedAt ?? b . updatedAt ?? b . createdAt ) ?. getTime ( ) ?? 0 ;
148+ return timeB - timeA ;
149+ } ) ;
150+
151+ return sortedSnapshots . map ( ( snapshot ) => {
152+ const attemptTimestamp =
153+ snapshot . sessionEndedAt ??
154+ snapshot . updatedAt ??
155+ snapshot . createdAt ??
156+ new Date ( ) ;
157+
158+ const timeTakenLabel = formatDuration ( snapshot ) ;
159+
160+ const baseQuestion : Question = {
161+ title : snapshot . questionTitle || "Untitled Question" ,
162+ body : "" ,
163+ topics : snapshot . topics ?? [ ] ,
164+ hints : [ ] ,
165+ answer : "" ,
166+ difficulty : snapshot . difficulty ?? "Unknown" ,
167+ timeLimit :
168+ typeof snapshot . timeLimit === "number"
169+ ? `${ snapshot . timeLimit } min`
170+ : snapshot . timeLimit !== undefined
171+ ? String ( snapshot . timeLimit )
172+ : "—" ,
173+ } ;
174+
175+ const partner =
176+ snapshot . participants . find (
177+ ( participant ) => participant !== snapshot . userId ,
178+ ) ?? snapshot . userId ;
179+
180+ return {
181+ id : snapshot . id ,
182+ question : baseQuestion ,
183+ date : attemptTimestamp ,
184+ partner,
185+ timeTaken : timeTakenLabel ,
186+ } ;
187+ } ) ;
188+ } , [ attemptSnapshots ] ) ;
189+
111190 const handleAttemptSelect = ( attempt : Attempt ) => {
112- if ( ! entry ) {
191+ if ( ! attempt ?. id ) {
113192 return ;
114193 }
115- navigate ( `/history/${ entry . id } /attempt` , {
194+
195+ const snapshot = attemptSnapshots . find ( ( item ) => item . id === attempt . id ) ;
196+ if ( ! snapshot ) {
197+ return ;
198+ }
199+
200+ const attemptPartner =
201+ attempt . partner ??
202+ snapshot . participants . find (
203+ ( participant ) => participant !== snapshot . userId ,
204+ ) ??
205+ snapshot . userId ;
206+
207+ navigate ( `/history/${ snapshot . id } /attempt` , {
116208 state : {
117- entry,
118- attemptPartner : attempt . partner ?? entry . userId ,
209+ entry : snapshot ,
210+ attemptPartner,
119211 } ,
120212 } ) ;
121213 } ;
@@ -157,8 +249,8 @@ const HistoryDetailPage: React.FC = () => {
157249 errorMessage = "Attempt history unavailable."
158250 remoteProps = { {
159251 items : attemptEntries ,
160- isLoading : loading ,
161- error,
252+ isLoading : loading || attemptsLoading ,
253+ error : error ?? attemptsError ,
162254 emptyMessage : "No attempt history recorded." ,
163255 loadingMessage : "Loading attempt history…" ,
164256 onSelect : handleAttemptSelect ,
0 commit comments