Skip to content

Commit 568c359

Browse files
committed
Fixes #536
1 parent 0d91eb9 commit 568c359

8 files changed

Lines changed: 378 additions & 58 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ application-local.yml
4949
jira.yml
5050
spieldata
5151
PlayGroundTest.java
52-
LOCAL_USERS.md
52+
LOCAL_USERS.md
53+
.opencode

client/src/components/ConfirmationDialog.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export default function ConfirmationDialog({
1313
children = null,
1414
confirmationTxt = null,
1515
largeWidth = false,
16-
confirmationHeader = null
16+
confirmationHeader = null,
17+
className = ""
1718
}) {
1819
const [busy, setBusy] = useState(false);
1920

@@ -34,6 +35,7 @@ export default function ConfirmationDialog({
3435
confirmationButtonLabel={confirmationTxt}
3536
confirmDisabled={disabledConfirm || (busy && cancel)}
3637
subTitle={null}
38+
className={className}
3739
full={largeWidth}/>
3840
);
3941

client/src/components/UserFeedbackWidget.jsx

Lines changed: 132 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,47 @@ const MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;
1414
export 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

Comments
 (0)