Skip to content

Commit ccc9ecd

Browse files
authored
Merge branch 'main' into main
2 parents b6ab63d + 5d57998 commit ccc9ecd

10 files changed

Lines changed: 426 additions & 14 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ jobs:
4444
run: npm ci
4545
- name: Build website
4646
run: npm run build
47+
env:
48+
GOOGLE_SCRIPT_URL: ${{ secrets.GOOGLE_SCRIPT_URL }}
4749

4850
- name: Upload Build Artifact
4951
uses: actions/upload-pages-artifact@v4

.github/workflows/static.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ jobs:
3535
- name: Build Docusaurus
3636
run: npm run build
3737
working-directory: ./website
38+
env:
39+
GOOGLE_SCRIPT_URL: ${{ secrets.GOOGLE_SCRIPT_URL }}
3840
- name: Setup Pages
3941
uses: actions/configure-pages@v5
4042
- name: Upload artifact

website/docusaurus.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { themes as prismThemes } from "prism-react-renderer";
22
import type { Config } from "@docusaurus/types";
33
import type * as Preset from "@docusaurus/preset-classic";
4+
import dotenv from 'dotenv';
5+
6+
dotenv.config({ path: '.env' });
47

58
const organizationName = "IntersectMBO";
69
const projectName = "developer-experience";
@@ -234,6 +237,10 @@ const config: Config = {
234237
darkTheme: prismThemes.dracula,
235238
},
236239
} satisfies Preset.ThemeConfig,
240+
241+
customFields: {
242+
googleScriptUrl: process.env.GOOGLE_SCRIPT_URL,
243+
},
237244
};
238245

239246
export default config;

website/google-apps-script.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* DO NOT DELETE THIS FILE
3+
* Google Apps Script to handle feedback submissions.
4+
*
5+
* INSTRUCTIONS:
6+
* 1. Open your Google Sheet.
7+
* 2. Go to Extensions > Apps Script.
8+
* 3. Paste this code into the editor.
9+
* 4. Save the project.
10+
* 5. Click "Deploy" > "New deployment".
11+
* 6. Select type: "Web app".
12+
* 7. Description: "Feedback Widget API".
13+
* 8. Execute as: "Me" (your email).
14+
* 9. Who has access: "Anyone" (IMPORTANT).
15+
* 10. Click "Deploy" and copy the "Web app URL".
16+
*/
17+
18+
19+
function doPost(e) {
20+
const lock = LockService.getScriptLock();
21+
lock.tryLock(10000);
22+
23+
try {
24+
const doc = SpreadsheetApp.getActiveSpreadsheet();
25+
const sheet = doc.getSheetByName('Feedback') || doc.insertSheet('Feedback');
26+
27+
// Add headers if they don't exist
28+
if (sheet.getLastRow() === 0) {
29+
sheet.appendRow(['Timestamp', 'Page URL', 'Vote', 'Comment', 'User Agent']);
30+
}
31+
32+
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
33+
const nextRow = sheet.getLastRow() + 1;
34+
35+
// Parse the request body
36+
const data = JSON.parse(e.postData.contents);
37+
38+
const newRow = [
39+
new Date(),
40+
data.url || 'Unknown',
41+
data.vote || 'Unknown',
42+
data.comment || '',
43+
data.userAgent || ''
44+
];
45+
46+
sheet.appendRow(newRow);
47+
48+
return ContentService
49+
.createTextOutput(JSON.stringify({ 'result': 'success', 'row': nextRow }))
50+
.setMimeType(ContentService.MimeType.JSON);
51+
52+
} catch (e) {
53+
return ContentService
54+
.createTextOutput(JSON.stringify({ 'result': 'error', 'error': e }))
55+
.setMimeType(ContentService.MimeType.JSON);
56+
} finally {
57+
lock.releaseLock();
58+
}
59+
}
60+
61+
// Handle CORS preflight requests
62+
function doOptions(e) {
63+
return ContentService.createTextOutput("")
64+
.setMimeType(ContentService.MimeType.JSON)
65+
.appendHeader("Access-Control-Allow-Origin", "*")
66+
.appendHeader("Access-Control-Allow-Methods", "POST")
67+
.appendHeader("Access-Control-Allow-Headers", "Content-Type");
68+
}

website/package-lock.json

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@docusaurus/tsconfig": "^3.9.2",
3636
"@docusaurus/types": "^3.9.2",
3737
"@types/node": "^25.3.0",
38+
"dotenv": "^17.3.1",
3839
"ts-node": "^10.9.2",
3940
"typescript": "~5.9.3"
4041
},
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React, { useState } from 'react';
2+
import clsx from 'clsx';
3+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
4+
5+
export default function FeedbackWidget() {
6+
const { siteConfig } = useDocusaurusContext();
7+
const GOOGLE_SCRIPT_URL = siteConfig.customFields?.googleScriptUrl as string;
8+
9+
const [vote, setVote] = useState<null | 'yes' | 'no'>(null);
10+
const [comment, setComment] = useState('');
11+
const [submitted, setSubmitted] = useState(false);
12+
const [submitting, setSubmitting] = useState(false);
13+
14+
const handleVote = (value: 'yes' | 'no') => {
15+
setVote(value);
16+
if (value === 'yes') {
17+
submitFeedback(value, '');
18+
}
19+
};
20+
21+
const submitFeedback = async (voteValue: string, commentValue: string) => {
22+
setSubmitting(true);
23+
try {
24+
const data = {
25+
url: window.location.href,
26+
vote: voteValue,
27+
comment: commentValue,
28+
userAgent: navigator.userAgent,
29+
};
30+
31+
await fetch(GOOGLE_SCRIPT_URL, {
32+
method: 'POST',
33+
mode: 'no-cors',
34+
headers: {
35+
'Content-Type': 'text/plain;charset=utf-8',
36+
},
37+
body: JSON.stringify(data),
38+
});
39+
40+
setSubmitted(true);
41+
} catch (error) {
42+
setSubmitted(true);
43+
} finally {
44+
setSubmitting(false);
45+
}
46+
};
47+
48+
const handleCommentSubmit = (e: React.FormEvent) => {
49+
e.preventDefault();
50+
if (vote) {
51+
submitFeedback(vote, comment);
52+
}
53+
};
54+
55+
if (submitted) {
56+
return (
57+
<div className="feedback-container">
58+
<div className="feedback-widget">
59+
<p className="thank-you">Thanks for your feedback! 🙏🏽</p>
60+
</div>
61+
</div>
62+
);
63+
}
64+
65+
return (
66+
<div className="feedback-container">
67+
<div className="feedback-widget">
68+
{!vote ? (
69+
<div className="initial-state">
70+
<span className="question">Was this helpful?</span>
71+
<div className="buttons">
72+
<button
73+
className={clsx("button", "thumbs-up")}
74+
onClick={() => handleVote('yes')}
75+
aria-label="Yes"
76+
>
77+
👍 Yes
78+
</button>
79+
<button
80+
className={clsx("button", "thumbs-down")}
81+
onClick={() => setVote('no')}
82+
aria-label="No"
83+
>
84+
👎 No
85+
</button>
86+
</div>
87+
</div>
88+
) : (
89+
<form className="comment-form" onSubmit={handleCommentSubmit}>
90+
<p className="follow-up">
91+
{vote === 'yes' ? 'What was most helpful?' : 'How can we improve this page?'}
92+
</p>
93+
<textarea
94+
className="textarea"
95+
value={comment}
96+
onChange={(e) => setComment(e.target.value)}
97+
placeholder={vote === 'yes' ? "Optional comments..." : "Please tell us what's missing or unclear..."}
98+
rows={3}
99+
/>
100+
<div className="form-actions">
101+
<button
102+
type="button"
103+
className="cancel-button"
104+
onClick={() => setVote(null)}
105+
>
106+
Cancel
107+
</button>
108+
<button
109+
type="submit"
110+
className="submit-button"
111+
disabled={submitting}
112+
>
113+
{submitting ? 'Sending...' : 'Send Feedback'}
114+
</button>
115+
</div>
116+
</form>
117+
)}
118+
</div>
119+
</div>
120+
);
121+
}

0 commit comments

Comments
 (0)