1- import { useCallback , useEffect , useMemo , useState } from "react" ;
1+ import { useState , useCallback , useMemo , useEffect } from "react" ;
22import { useParams , Link , useNavigate , Navigate } from "react-router" ;
33import { motion } from "framer-motion" ;
44import {
@@ -13,12 +13,44 @@ import {
1313 MessageSquare ,
1414} from "lucide-react" ;
1515import { sections , questions } from "./data" ;
16- import type { InterviewProgress , CodeExample } from "./data/types" ;
16+ import type { CodeExample } from "./data/types" ;
1717import { SEO } from "../../../components/SEO" ;
1818import { canonicalUrl } from "../../../lib/seo.utils" ;
1919import { useAuthStore } from "../../../lib/auth.store" ;
2020import { reportMilestone } from "../../../lib/milestone.utils" ;
21- import { useInterviewProgress } from "./interviewProgress" ;
21+
22+ async function getServerProgress ( ) {
23+ const res = await fetch ( "/api/interview-progress" , {
24+ method : "GET" ,
25+ credentials : "include" ,
26+ } ) ;
27+
28+ if ( ! res . ok ) {
29+ throw new Error ( "Failed to fetch progress" ) ;
30+ }
31+
32+ return res . json ( ) ;
33+ }
34+
35+ async function updateServerProgress (
36+ questionId : string ,
37+ action : "complete" | "uncomplete" | "visit"
38+ ) {
39+ const res = await fetch ( `/api/interview-progress` , {
40+ method : "PATCH" ,
41+ credentials : "include" ,
42+ headers : {
43+ "Content-Type" : "application/json" ,
44+ } ,
45+ body : JSON . stringify ( { questionId, action } ) ,
46+ } ) ;
47+
48+ if ( ! res . ok ) {
49+ throw new Error ( "Failed to update progress" ) ;
50+ }
51+
52+ return res . json ( ) ;
53+ }
2254
2355const DIFF_STYLE : Record < string , string > = {
2456 Beginner : "text-green-700 dark:text-green-400 border-green-300 dark:border-green-900/60" ,
@@ -115,7 +147,8 @@ export default function InterviewQuestionPage() {
115147 const navigate = useNavigate ( ) ;
116148 const basePath = "/learn/interview" ;
117149 const isAuthenticated = useAuthStore ( ( s ) => s . isAuthenticated ) ;
118- const { progress, toggleComplete, recordVisit } = useInterviewProgress ( ) ;
150+
151+ const [ completed , setCompleted ] = useState ( false ) ;
119152
120153 const section = sections . find ( ( s ) => s . id === sectionSlug ) ;
121154 const sectionQuestions = useMemo (
@@ -124,27 +157,93 @@ export default function InterviewQuestionPage() {
124157 ) ;
125158
126159 const question = sectionQuestions . find ( ( q ) => q . id === questionId ) ;
127- const completed = ! ! ( questionId && progress [ questionId ] ?. completed ) ;
128- const currentIndex = question ? sectionQuestions . findIndex ( ( q ) => q . id === question . id ) : - 1 ;
129- const prevQuestion = currentIndex > 0 ? sectionQuestions [ currentIndex - 1 ] : null ;
130- const nextQuestion = currentIndex < sectionQuestions . length - 1 ? sectionQuestions [ currentIndex + 1 ] : null ;
131-
132- const handleToggleComplete = useCallback ( ( ) => {
133- if ( ! questionId ) return ;
134- void toggleComplete ( questionId )
135- . then ( ( newVal ) => {
136- if ( newVal && isAuthenticated && sectionSlug ) {
137- const nextProgress : InterviewProgress = { ...progress , [ questionId ] : { completed : true } } ;
138- const allDone = sectionQuestions . every ( ( q ) => nextProgress [ q . id ] ?. completed ) ;
139- if ( allDone ) reportMilestone ( "INTERVIEW_SECTION_COMPLETE" , sectionSlug ) ;
140- }
141- } )
142- . catch ( ( ) => { } ) ;
143- } , [ questionId , toggleComplete , isAuthenticated , sectionSlug , sectionQuestions , progress ] ) ;
160+
161+ const currentIndex = question
162+ ? sectionQuestions . findIndex ( ( q ) => q . id === question . id )
163+ : - 1 ;
164+
165+ const prevQuestion =
166+ currentIndex > 0
167+ ? sectionQuestions [ currentIndex - 1 ]
168+ : null ;
169+
170+ const nextQuestion =
171+ currentIndex < sectionQuestions . length - 1
172+ ? sectionQuestions [ currentIndex + 1 ]
173+ : null ;
144174
145175 useEffect ( ( ) => {
146- if ( questionId ) recordVisit ( questionId ) ;
147- } , [ questionId , recordVisit ] ) ;
176+ if ( ! isAuthenticated || ! questionId ) return ;
177+
178+ const loadProgress = async ( ) => {
179+ try {
180+ const progress = await getServerProgress ( ) ;
181+
182+ setCompleted (
183+ progress . completedIds ?. includes ( questionId ) ?? false
184+ ) ;
185+ } catch ( err ) {
186+ console . error ( err ) ;
187+ }
188+ } ;
189+
190+ loadProgress ( ) ;
191+ } , [ isAuthenticated , questionId ] ) ;
192+
193+ useEffect ( ( ) => {
194+ if ( ! isAuthenticated || ! questionId ) return ;
195+
196+ const timeout = setTimeout ( ( ) => {
197+ updateServerProgress ( questionId , "visit" )
198+ . catch ( console . error ) ;
199+ } , 500 ) ;
200+
201+ return ( ) => clearTimeout ( timeout ) ;
202+ } , [ isAuthenticated , questionId ] ) ;
203+
204+ const handleToggleComplete = useCallback ( async ( ) => {
205+ if ( ! questionId || ! isAuthenticated ) return ;
206+
207+ try {
208+ const action =
209+ completed ? "uncomplete" : "complete" ;
210+
211+ const updatedProgress =
212+ await updateServerProgress (
213+ questionId ,
214+ action
215+ ) ;
216+
217+ const isNowCompleted =
218+ updatedProgress . completedIds . includes ( questionId ) ;
219+
220+ setCompleted ( isNowCompleted ) ;
221+
222+ if (
223+ isNowCompleted &&
224+ sectionSlug
225+ ) {
226+ const allDone = sectionQuestions . every ( ( q ) =>
227+ updatedProgress . completedIds . includes ( q . id )
228+ ) ;
229+
230+ if ( allDone ) {
231+ reportMilestone (
232+ "INTERVIEW_SECTION_COMPLETE" ,
233+ sectionSlug
234+ ) ;
235+ }
236+ }
237+ } catch ( err ) {
238+ console . error ( err ) ;
239+ }
240+ } , [
241+ questionId ,
242+ completed ,
243+ isAuthenticated ,
244+ sectionSlug ,
245+ sectionQuestions ,
246+ ] ) ;
148247
149248 if ( section && ! section . freeTier && ! isAuthenticated ) {
150249 return < Navigate to = { basePath } replace /> ;
0 commit comments