-
Notifications
You must be signed in to change notification settings - Fork 4
Implement downloading and copying of attendee names and IDs #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -48,6 +48,30 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $fetching = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const copyCSV = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const headers = ["Student ID", "Name"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const csvContent = [headers.join(","), ...rows].join("\n"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| navigator.clipboard.writeText(csvContent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const downloadCSV = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const headers = ["Student ID", "Name"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MattyTheHacker marked this conversation as resolved.
MattyTheHacker marked this conversation as resolved.
Comment on lines
+51
to
+60
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const copyCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`); | |
| const csvContent = [headers.join(","), ...rows].join("\n"); | |
| navigator.clipboard.writeText(csvContent); | |
| }; | |
| const downloadCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`); | |
| const escapeCSVField = (value: string) => { | |
| const neutralizedValue = /^[=+\-@]/.test(value) ? `'${value}` : value; | |
| return `"${neutralizedValue.replace(/"/g, '""')}"`; | |
| }; | |
| const copyCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map((u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`); | |
| const csvContent = [headers.join(","), ...rows].join("\n"); | |
| navigator.clipboard.writeText(csvContent); | |
| }; | |
| const downloadCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map((u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`); |
Copilot
AI
Apr 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CSV generation currently only escapes quotes in u.name, but not u.studentID, and it does not mitigate CSV formula injection (values starting with =, +, -, @) which can execute formulas when opened in spreadsheet tools. Consider a shared CSV-escaping helper applied to both fields, including formula-injection hardening.
| const copyCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`); | |
| const csvContent = [headers.join(","), ...rows].join("\n"); | |
| navigator.clipboard.writeText(csvContent); | |
| }; | |
| const downloadCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`); | |
| const escapeCSVField = (value: string) => { | |
| const hardenedValue = /^[=+\-@]/.test(value) ? `'${value}` : value; | |
| return `"${hardenedValue.replace(/"/g, '""')}"`; | |
| }; | |
| const copyCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map( | |
| (u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`, | |
| ); | |
| const csvContent = [headers.join(","), ...rows].join("\n"); | |
| navigator.clipboard.writeText(csvContent); | |
| }; | |
| const downloadCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map( | |
| (u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`, | |
| ); |
Copilot
AI
Apr 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
downloadCSV duplicates the header/row building logic from copyCSV. Extracting a single helper to build the CSV string will avoid the two paths drifting (especially once escaping/sanitization is improved).
| const copyCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`); | |
| const csvContent = [headers.join(","), ...rows].join("\n"); | |
| navigator.clipboard.writeText(csvContent); | |
| }; | |
| const downloadCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`); | |
| const csvContent = [headers.join(","), ...rows].join("\n"); | |
| const buildUsersCSV = () => { | |
| const headers = ["Student ID", "Name"]; | |
| const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`); | |
| return [headers.join(","), ...rows].join("\n"); | |
| }; | |
| const copyCSV = () => { | |
| const csvContent = buildUsersCSV(); | |
| navigator.clipboard.writeText(csvContent); | |
| }; | |
| const downloadCSV = () => { | |
| const csvContent = buildUsersCSV(); |
Copilot
AI
Apr 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
URL.revokeObjectURL(url) is called immediately after link.click(). In some browsers this can revoke the blob URL before the download has started, causing intermittent download failures. Revoke the URL asynchronously (e.g., in a setTimeout/next tick) after the click.
| URL.revokeObjectURL(url); | |
| setTimeout(() => URL.revokeObjectURL(url), 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
navigator.clipboard.writeTextreturns a Promise and can fail (permission/insecure context). As written, failures are ignored and the UI provides no feedback. MakecopyCSVasync,awaitthe call, and handle errors (e.g., set$erroror show a message).