@@ -14,32 +14,47 @@ const MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;
1414export const UserFeedbackWidget = ( ) => {
1515 const location = useLocation ( ) ;
1616 const setFlash = useAppStore ( state => state . setFlash ) ;
17+ const user = useAppStore ( state => state . user ) ;
1718 const [ open , setOpen ] = useState ( false ) ;
1819 const [ message , setMessage ] = useState ( "" ) ;
1920 const [ includeScreenshot , setIncludeScreenshot ] = useState ( true ) ;
2021 const [ submitting , setSubmitting ] = useState ( false ) ;
22+ const [ previewScreenshot , setPreviewScreenshot ] = useState ( null ) ;
2123 const inputRef = useRef ( null ) ;
2224
2325 const closeModal = ( ) => {
2426 setOpen ( false ) ;
2527 setMessage ( "" ) ;
2628 setIncludeScreenshot ( true ) ;
29+ setPreviewScreenshot ( null ) ;
2730 } ;
2831
29- const captureScreenshot = useCallback ( async ( ) => {
32+ const captureScreenshot = useCallback ( async ( hideModal = true ) => {
3033 window . scrollTo ( { top : 0 , behavior : "instant" } ) ;
3134 document . body . classList . add ( "feedback-capture" ) ;
35+ if ( hideModal ) {
36+ document . body . classList . add ( "feedback-capture--hide-modal" ) ;
37+ }
3238 const canvas = await html2canvas ( document . body , {
3339 backgroundColor : null ,
3440 useCORS : true ,
3541 scale : 1 ,
3642 windowWidth : document . body . clientWidth ,
3743 windowHeight : document . body . scrollHeight
3844 } ) ;
39- setTimeout ( ( ) => document . body . classList . remove ( "feedback-capture" ) , 825 ) ;
45+ document . body . classList . remove ( "feedback-capture" ) ;
46+ document . body . classList . remove ( "feedback-capture--hide-modal" ) ;
4047 return canvas . toDataURL ( "image/png" ) ;
4148 } , [ ] ) ;
4249
50+ const handleOpen = useCallback ( ( ) => {
51+ setOpen ( true ) ;
52+ setTimeout ( ( ) => inputRef . current ?. focus ( ) , 25 ) ;
53+ // Capture in background — the modal is hidden from html2canvas
54+ // via the feedback-capture--hide-modal CSS class.
55+ captureScreenshot ( true ) . then ( dataUrl => setPreviewScreenshot ( dataUrl ) ) ;
56+ } , [ captureScreenshot ] ) ;
57+
4358 const handleSubmit = useCallback ( async ( ) => {
4459 if ( ! message . trim ( ) ) {
4560 return ;
@@ -53,7 +68,7 @@ export const UserFeedbackWidget = () => {
5368 } ;
5469
5570 if ( includeScreenshot ) {
56- const dataUrl = await captureScreenshot ( ) ;
71+ const dataUrl = await captureScreenshot ( true ) ;
5772 const base64 = dataUrl . split ( "," ) [ 1 ] || "" ;
5873 const estimatedBytes = Math . ceil ( ( base64 . length * 3 ) / 4 ) ;
5974 if ( estimatedBytes > MAX_SCREENSHOT_BYTES ) {
@@ -65,58 +80,129 @@ export const UserFeedbackWidget = () => {
6580 }
6681 await sendFeedback ( payload ) ;
6782 closeModal ( ) ;
68- setTimeout ( ( ) => document . body . classList . remove ( "feedback-capture" ) , 625 ) ;
6983 setFlash ( I18n . t ( "feedback.flash" ) ) ;
7084 } finally {
7185 setSubmitting ( false ) ;
7286 }
7387 } , [ captureScreenshot , includeScreenshot , location . hash , location . pathname , location . search , message , setFlash ] ) ;
7488
89+ const renderMailPreview = ( ) => {
90+ const now = new Date ( ) ;
91+ const dateStr = now . toLocaleDateString ( I18n . locale , {
92+ weekday : "short" ,
93+ year : "numeric" ,
94+ month : "short" ,
95+ day : "numeric" ,
96+ hour : "2-digit" ,
97+ minute : "2-digit"
98+ } ) ;
99+
100+ return (
101+ < div className = "mail-preview" >
102+ < div className = "mail-preview__toolbar" >
103+ < div className = "mail-preview__dots" >
104+ < span className = "dot red" />
105+ < span className = "dot yellow" />
106+ < span className = "dot green" />
107+ </ div >
108+ < span className = "mail-preview__toolbar-title" >
109+ { I18n . t ( "feedback.preview.subjectLine" ) }
110+ </ span >
111+ </ div >
112+ < div className = "mail-preview__header" >
113+ < div className = "mail-preview__field" >
114+ < span className = "label" > { I18n . t ( "feedback.preview.to" ) } :</ span >
115+ < span className = "value" > support@surf.nl</ span >
116+ </ div >
117+ < div className = "mail-preview__field" >
118+ < span className = "label" > { I18n . t ( "feedback.preview.from" ) } :</ span >
119+ < span className = "value" > no-reply@surf.nl</ span >
120+ </ div >
121+ < div className = "mail-preview__field" >
122+ < span className = "label" > { I18n . t ( "feedback.preview.subject" ) } :</ span >
123+ < span className = "value" > { I18n . t ( "feedback.preview.subjectLine" ) } </ span >
124+ </ div >
125+ < div className = "mail-preview__field" >
126+ < span className = "label" > { I18n . t ( "feedback.preview.date" ) } :</ span >
127+ < span className = "value" > { dateStr } </ span >
128+ </ div >
129+ </ div >
130+ < div className = "mail-preview__body" >
131+ < p className = "greeting" > { I18n . t ( "feedback.preview.greeting" ) } </ p >
132+ < p className = "intro" > { I18n . t ( "feedback.preview.providedFeedback" , { name : user ?. name || "" } ) } </ p >
133+ < div className = "mail-preview__quote" >
134+ < p className = { message . trim ( ) ? "" : "placeholder" } >
135+ { message . trim ( ) || I18n . t ( "feedback.preview.messagePlaceholder" ) }
136+ </ p >
137+ </ div >
138+ { includeScreenshot && previewScreenshot && (
139+ < div className = "mail-preview__screenshot" >
140+ < img
141+ src = { previewScreenshot }
142+ alt = { I18n . t ( "feedback.preview.screenshotLabel" ) }
143+ />
144+ </ div >
145+ ) }
146+ { includeScreenshot && ! previewScreenshot && (
147+ < div className = "mail-preview__screenshot-loading" />
148+ ) }
149+ < p className = "follow-up" > { I18n . t ( "feedback.preview.followUp" , { email : user ?. email || "" } ) } </ p >
150+ </ div >
151+ </ div >
152+ ) ;
153+ } ;
75154
76155 const renderContent = ( ) => (
77- < div className = "user-feedback-widget__modal" >
78- < p > { I18n . t ( "feedback.info" ) } </ p >
79- < div className = "sds--text-area" >
80- < textarea
81- name = "feedback"
82- id = "feedback"
83- value = { message }
84- rows = "6"
85- ref = { inputRef }
86- onChange = { e => setMessage ( e . target . value ) }
87- />
156+ < div className = "user-feedback-widget__layout" >
157+ < div className = "user-feedback-widget__form" >
158+ < div className = "user-feedback-widget__modal" >
159+ < p > { I18n . t ( "feedback.info" ) } </ p >
160+ < div className = "sds--text-area" >
161+ < textarea
162+ name = "feedback"
163+ id = "feedback"
164+ value = { message }
165+ rows = "6"
166+ ref = { inputRef }
167+ onChange = { e => setMessage ( e . target . value ) }
168+ />
169+ </ div >
170+ < div className = "user-feedback-widget__options" >
171+ < Checkbox
172+ value = { includeScreenshot }
173+ name = { "includeScreenshot" }
174+ onChange = { ( ) => setIncludeScreenshot ( ! includeScreenshot ) }
175+ />
176+ < span > { I18n . t ( "feedback.includeScreenshot" ) } </ span >
177+ </ div >
178+ < section className = "disclaimer" >
179+ < span
180+ dangerouslySetInnerHTML = { {
181+ __html : DOMPurify . sanitize ( I18n . t ( "feedback.disclaimer" ) , {
182+ ADD_ATTR : [ "target" , "rel" ]
183+ } )
184+ } }
185+ />
186+ </ section >
187+ < section className = "help" >
188+ < h3
189+ className = "title"
190+ dangerouslySetInnerHTML = { {
191+ __html : DOMPurify . sanitize ( I18n . t ( "feedback.help" ) )
192+ } }
193+ />
194+ < span
195+ dangerouslySetInnerHTML = { {
196+ __html : DOMPurify . sanitize ( I18n . t ( "feedback.helpInfo" ) )
197+ } }
198+ />
199+ </ section >
200+ </ div >
88201 </ div >
89- < label className = "user-feedback-widget__options" >
90- < Checkbox
91- value = { includeScreenshot }
92- onChange = { ( ) => setIncludeScreenshot ( ! includeScreenshot ) }
93- />
94- < span > { I18n . t ( "feedback.includeScreenshot" ) } </ span >
95- </ label >
96- < section className = "disclaimer" >
97- < span
98- dangerouslySetInnerHTML = { {
99- __html : DOMPurify . sanitize ( I18n . t ( "feedback.disclaimer" ) , {
100- ADD_ATTR : [ "target" , "rel" ]
101- } )
102- } }
103- />
104- </ section >
105- < section className = "help" >
106- < h3
107- className = "title"
108- dangerouslySetInnerHTML = { {
109- __html : DOMPurify . sanitize ( I18n . t ( "feedback.help" ) )
110- } }
111- />
112- < span
113- dangerouslySetInnerHTML = { {
114- __html : DOMPurify . sanitize ( I18n . t ( "feedback.helpInfo" ) )
115- } }
116- />
117- </ section >
202+ { renderMailPreview ( ) }
118203 </ div >
119204 ) ;
205+
120206 if ( open ) {
121207 return (
122208 < ConfirmationDialog
@@ -127,17 +213,15 @@ export const UserFeedbackWidget = () => {
127213 disabledConfirm = { submitting || ! message . trim ( ) }
128214 children = { renderContent ( ) }
129215 largeWidth = { true }
216+ className = "feedback-preview-dialog"
130217 />
131218 ) ;
132219 }
133220 return (
134221 < div className = "user-feedback-widget" >
135222 < button
136223 className = "user-feedback-widget__trigger"
137- onClick = { ( ) => {
138- setOpen ( true ) ;
139- setTimeout ( ( ) => inputRef . current ?. focus ( ) , 25 ) ;
140- } }
224+ onClick = { handleOpen }
141225 type = "button"
142226 >
143227 { I18n . t ( "feedback.widgetLabel" ) }
0 commit comments