@@ -186,6 +186,10 @@ const PlanOverviewQuestionPage: React.FC = () => {
186186 const [ isSubmitting , setIsSubmitting ] = useState < boolean > ( false ) ;
187187 // Track whether there are unsaved changes
188188 const [ hasUnsavedChanges , setHasUnsavedChanges ] = useState < boolean > ( false ) ;
189+ const [ isAutoSaving , setIsAutoSaving ] = useState < boolean > ( false ) ;
190+ const [ lastSavedAt , setLastSavedAt ] = useState < Date | null > ( null ) ;
191+ const [ currentTime , setCurrentTime ] = useState ( new Date ( ) ) ;
192+ const autoSaveTimeoutRef = useRef < NodeJS . Timeout > ( ) ;
189193
190194 // Localization
191195 const Global = useTranslations ( 'Global' ) ;
@@ -366,6 +370,7 @@ const PlanOverviewQuestionPage: React.FC = () => {
366370 ...prev ,
367371 otherAffiliationName : value
368372 } ) ) ;
373+ setHasUnsavedChanges ( true ) ;
369374 } ;
370375
371376 // Update the selected radio value when user selects different option
@@ -750,7 +755,11 @@ const PlanOverviewQuestionPage: React.FC = () => {
750755 } ;
751756
752757 // Call Server Action updateAnswerAction or addAnswerAction to save answer
753- const addAnswer = async ( ) => {
758+ const addAnswer = async ( isAutoSave = false ) => {
759+ if ( isAutoSave ) {
760+ setIsAutoSaving ( true ) ;
761+ }
762+
754763 const jsonPayload = getAnswerJson ( ) ;
755764 // Check is answer already exists. If so, we want to call an update mutation rather than add
756765 const isUpdate = Boolean ( answerData ?. answerByVersionedQuestionId ) ;
@@ -785,6 +794,11 @@ const PlanOverviewQuestionPage: React.FC = () => {
785794 path : routePath ( 'projects.dmp.versionedQuestion.detail' , { projectId, dmpId, versionedSectionId, versionedQuestionId } )
786795 }
787796 } ) ;
797+ } finally {
798+ if ( isAutoSave ) {
799+ setIsAutoSaving ( false ) ;
800+ setHasUnsavedChanges ( false ) ;
801+ }
788802 }
789803 }
790804 return {
@@ -804,6 +818,11 @@ const PlanOverviewQuestionPage: React.FC = () => {
804818 if ( isSubmitting ) return ;
805819 setIsSubmitting ( true ) ;
806820
821+ // Clear any pending auto-save
822+ if ( autoSaveTimeoutRef . current ) {
823+ clearTimeout ( autoSaveTimeoutRef . current ) ;
824+ }
825+
807826 const result = await addAnswer ( ) ;
808827
809828 if ( ! result . success ) {
@@ -825,11 +844,32 @@ const PlanOverviewQuestionPage: React.FC = () => {
825844 // Show user a success message and redirect back to the Section page
826845 showSuccessToast ( ) ;
827846 router . push ( routePath ( 'projects.dmp.versionedSection' , { projectId, dmpId, versionedSectionId } ) )
828-
829847 }
830848 }
831849 } ;
832850
851+ // Helper function to format the last saved messaging
852+ const getLastSavedText = ( ) => {
853+
854+ if ( isAutoSaving ) {
855+ return `${ Global ( 'buttons.saving' ) } ...` ;
856+ }
857+
858+ if ( ! lastSavedAt ) {
859+ return hasUnsavedChanges ? t ( 'messages.unsavedChanges' ) : '' ;
860+ }
861+
862+ const diffInMinutes = Math . floor ( Math . abs ( currentTime . getTime ( ) - lastSavedAt . getTime ( ) ) / ( 1000 * 60 ) ) ;
863+
864+ if ( diffInMinutes === 0 ) {
865+ return t ( 'messages.savedJustNow' ) ;
866+ } else if ( diffInMinutes === 1 ) {
867+ return t ( 'messages.lastSavedOneMinuteAgo' ) ;
868+ } else {
869+ return t ( 'messages.lastSaves' , { minutes : diffInMinutes } ) ;
870+ }
871+ } ;
872+
833873 // Get parsed JSON from question, and set parsed, question and questionType in state
834874 useEffect ( ( ) => {
835875 if ( selectedQuestion ) {
@@ -934,20 +974,65 @@ const PlanOverviewQuestionPage: React.FC = () => {
934974
935975 } , [ answerData , questionType ] ) ;
936976
937- // Warn user of unsaved changes if they try to leave the page
977+
978+ // Auto-save logic
938979 useEffect ( ( ) => {
980+ if ( ! hasUnsavedChanges ) return ;
981+ if ( ! versionedQuestionId || ! versionedSectionId || ! question ) return ;
982+
983+ // Set a timeout to auto-save after 3 seconds of inactivity
984+ autoSaveTimeoutRef . current = setTimeout ( async ( ) => {
985+ const { success } = await addAnswer ( true ) ;
986+ if ( success ) {
987+ setLastSavedAt ( new Date ( ) ) ;
988+ setHasUnsavedChanges ( false ) ;
989+ }
990+ } , 3000 ) ;
991+
992+ return ( ) => clearTimeout ( autoSaveTimeoutRef . current ) ;
993+ } , [ formData , versionedQuestionId , versionedSectionId , question , hasUnsavedChanges ] ) ;
994+
995+
996+
997+ // Auto-save on window blur and before unload
998+ useEffect ( ( ) => {
999+ const handleWindowBlur = ( ) => {
1000+ if ( hasUnsavedChanges && ! isAutoSaving ) {
1001+ if ( autoSaveTimeoutRef . current ) {
1002+ clearTimeout ( autoSaveTimeoutRef . current ) ;
1003+ }
1004+ addAnswer ( true ) ;
1005+ }
1006+ } ;
1007+
9391008 const handleBeforeUnload = ( e : BeforeUnloadEvent ) => {
9401009 if ( hasUnsavedChanges ) {
9411010 e . preventDefault ( ) ;
9421011 e . returnValue = '' ; // Required for Chrome/Firefox to show the confirm dialog
9431012 }
9441013 } ;
9451014
1015+ window . addEventListener ( 'blur' , handleWindowBlur ) ;
9461016 window . addEventListener ( 'beforeunload' , handleBeforeUnload ) ;
1017+
9471018 return ( ) => {
1019+ window . removeEventListener ( 'blur' , handleWindowBlur ) ;
9481020 window . removeEventListener ( 'beforeunload' , handleBeforeUnload ) ;
1021+ if ( autoSaveTimeoutRef . current ) {
1022+ clearTimeout ( autoSaveTimeoutRef . current ) ;
1023+ }
9491024 } ;
950- } , [ hasUnsavedChanges ] ) ;
1025+ } , [ hasUnsavedChanges , isAutoSaving ] ) ;
1026+
1027+
1028+ // Set up an interval to update the current time every minute
1029+ useEffect ( ( ) => {
1030+ const interval = setInterval ( ( ) => {
1031+ setCurrentTime ( new Date ( ) ) ;
1032+ } , 60 * 1000 ) ; // Update every minute
1033+
1034+ return ( ) => clearInterval ( interval ) ;
1035+ } , [ ] ) ;
9511036
9521037 useEffect ( ( ) => {
9531038 // Set whether current user can add comments based on their role and plan data
@@ -1159,7 +1244,11 @@ const PlanOverviewQuestionPage: React.FC = () => {
11591244
11601245 </ div >
11611246 { parsed && questionField }
1162-
1247+ </ div >
1248+ < div className = "lastSaved mt-5"
1249+ aria-live = "polite"
1250+ role = "status" >
1251+ { getLastSavedText ( ) }
11631252 </ div >
11641253 </ Card >
11651254
0 commit comments