-
Notifications
You must be signed in to change notification settings - Fork 0
feat(feedback): in‑app feedback with delegated SMTP OAuth; navbar icon order; seed + test scripts #7
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
feat(feedback): in‑app feedback with delegated SMTP OAuth; navbar icon order; seed + test scripts #7
Changes from all commits
c695d3b
745eb8f
17cb8ec
12d7c24
0527f0c
f455956
c9c8d8e
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 |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| .feedback-form { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 1.25rem; | ||
| } | ||
|
|
||
| .feedback-form label { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 0.5rem; | ||
| font-size: 0.95rem; | ||
| color: var(--mode-text-color, #f5f5f5); | ||
| } | ||
|
|
||
| .feedback-form select, | ||
| .feedback-form textarea, | ||
| .feedback-form input { | ||
| width: 100%; | ||
| padding: 0.75rem; | ||
| border-radius: 8px; | ||
| border: 1px solid rgba(245, 128, 37, 0.25); | ||
| background: var(--input-bg, rgba(18, 18, 18, 0.85)); | ||
| color: var(--mode-text-color, #f5f5f5); | ||
| font-family: inherit; | ||
| font-size: 0.95rem; | ||
| transition: border-color 0.2s ease, box-shadow 0.2s ease; | ||
| } | ||
|
|
||
| .feedback-form select:focus, | ||
| .feedback-form textarea:focus, | ||
| .feedback-form input:focus { | ||
| outline: none; | ||
| border-color: #F58025; | ||
| box-shadow: 0 0 0 2px rgba(245, 128, 37, 0.2); | ||
| } | ||
|
|
||
| .feedback-form textarea { | ||
| min-height: 160px; | ||
| resize: vertical; | ||
| } | ||
|
|
||
| .feedback-hint { | ||
| font-size: 0.8rem; | ||
| color: var(--subtle-text-color, rgba(255, 255, 255, 0.6)); | ||
| align-self: flex-end; | ||
| } | ||
|
|
||
| .feedback-actions { | ||
| display: flex; | ||
| justify-content: flex-end; | ||
| gap: 0.75rem; | ||
| } | ||
|
|
||
| .feedback-primary-button, | ||
| .feedback-secondary-button { | ||
| padding: 0.65rem 1.4rem; | ||
| border-radius: 6px; | ||
| font-weight: 600; | ||
| cursor: pointer; | ||
| transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; | ||
| border: none; | ||
| } | ||
|
|
||
| .feedback-primary-button { | ||
| background-color: #F58025; | ||
| color: #121212; | ||
| box-shadow: 0 2px 8px rgba(245, 128, 37, 0.35); | ||
| } | ||
|
|
||
| .feedback-primary-button:hover:not(:disabled) { | ||
| transform: translateY(-1px); | ||
| box-shadow: 0 4px 10px rgba(245, 128, 37, 0.4); | ||
| } | ||
|
|
||
| .feedback-secondary-button { | ||
| background: transparent; | ||
| border: 1px solid rgba(245, 128, 37, 0.5); | ||
| color: var(--mode-text-color, #f5f5f5); | ||
| } | ||
|
|
||
| .feedback-secondary-button:hover:not(:disabled) { | ||
| transform: translateY(-1px); | ||
| border-color: #F58025; | ||
| } | ||
|
|
||
| .feedback-primary-button:disabled, | ||
| .feedback-secondary-button:disabled { | ||
| opacity: 0.6; | ||
| cursor: not-allowed; | ||
| transform: none; | ||
| box-shadow: none; | ||
| } | ||
|
|
||
| .feedback-error { | ||
| margin: 0; | ||
| color: #ff7676; | ||
| font-size: 0.85rem; | ||
| } | ||
|
|
||
| .feedback-success { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 1rem; | ||
| font-size: 0.95rem; | ||
| color: var(--mode-text-color, #f5f5f5); | ||
| align-items: center; | ||
| text-align: center; | ||
| } | ||
|
|
||
| .feedback-success p { | ||
| margin: 0; | ||
| line-height: 1.5; | ||
| } | ||
|
|
||
| .feedback-success .feedback-primary-button { | ||
| min-width: 220px; | ||
| } | ||
|
|
||
| @media (max-width: 600px) { | ||
| .feedback-actions { | ||
| flex-direction: column; | ||
| align-items: stretch; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,172 @@ | ||||||||||||||||||
| import { useEffect, useState } from 'react'; | ||||||||||||||||||
| import PropTypes from 'prop-types'; | ||||||||||||||||||
| import Modal from './Modal'; | ||||||||||||||||||
| import './FeedbackModal.css'; | ||||||||||||||||||
| import { useAuth } from '../context/AuthContext'; | ||||||||||||||||||
|
|
||||||||||||||||||
| const CATEGORY_OPTIONS = [ | ||||||||||||||||||
| { value: 'feedback', label: 'General feedback' }, | ||||||||||||||||||
| { value: 'bug', label: 'Report a bug' }, | ||||||||||||||||||
| { value: 'idea', label: 'Feature request' }, | ||||||||||||||||||
| { value: 'other', label: 'Something else' } | ||||||||||||||||||
| ]; | ||||||||||||||||||
|
|
||||||||||||||||||
| function FeedbackModal({ isOpen, onClose }) { | ||||||||||||||||||
| const { authenticated, user } = useAuth(); | ||||||||||||||||||
| const [category, setCategory] = useState('feedback'); | ||||||||||||||||||
| const [message, setMessage] = useState(''); | ||||||||||||||||||
| const [contactInfo, setContactInfo] = useState(''); | ||||||||||||||||||
| const [submitting, setSubmitting] = useState(false); | ||||||||||||||||||
| const [error, setError] = useState(''); | ||||||||||||||||||
| const [submitted, setSubmitted] = useState(false); | ||||||||||||||||||
|
|
||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||
| if (isOpen) { | ||||||||||||||||||
| setCategory('feedback'); | ||||||||||||||||||
| setMessage(''); | ||||||||||||||||||
| setError(''); | ||||||||||||||||||
| setSubmitted(false); | ||||||||||||||||||
| if (authenticated && user?.netid) { | ||||||||||||||||||
| setContactInfo(`${user.netid}@princeton.edu`); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| setContactInfo(''); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| }, [isOpen, authenticated, user]); | ||||||||||||||||||
|
|
||||||||||||||||||
| const closeIfAllowed = () => { | ||||||||||||||||||
| if (!submitting) { | ||||||||||||||||||
| onClose(); | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const handleSubmit = async (event) => { | ||||||||||||||||||
| event.preventDefault(); | ||||||||||||||||||
| if (submitting) return; | ||||||||||||||||||
|
|
||||||||||||||||||
| const trimmedMessage = message.trim(); | ||||||||||||||||||
| if (trimmedMessage.length < 10) { | ||||||||||||||||||
| setError('Please include at least a few details so we can help.'); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| setSubmitting(true); | ||||||||||||||||||
| setError(''); | ||||||||||||||||||
|
|
||||||||||||||||||
| try { | ||||||||||||||||||
| const response = await fetch('/api/feedback', { | ||||||||||||||||||
| method: 'POST', | ||||||||||||||||||
| headers: { 'Content-Type': 'application/json' }, | ||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||
| category, | ||||||||||||||||||
| message: trimmedMessage, | ||||||||||||||||||
| contactInfo: contactInfo.trim() || null, | ||||||||||||||||||
| pagePath: typeof window !== 'undefined' ? window.location.pathname : null | ||||||||||||||||||
| }) | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||
| const data = await response.json().catch(() => ({})); | ||||||||||||||||||
| throw new Error(data.error || 'Unable to send feedback right now.'); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| setSubmitted(true); | ||||||||||||||||||
| setMessage(''); | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| setError(err.message || 'Unable to send feedback right now.'); | ||||||||||||||||||
| } finally { | ||||||||||||||||||
| setSubmitting(false); | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| return ( | ||||||||||||||||||
| <Modal | ||||||||||||||||||
| isOpen={isOpen} | ||||||||||||||||||
| onClose={closeIfAllowed} | ||||||||||||||||||
| title={submitted ? 'Thanks for your feedback!' : 'Send Feedback'} | ||||||||||||||||||
| showCloseButton | ||||||||||||||||||
| isLarge={!submitted} | ||||||||||||||||||
| > | ||||||||||||||||||
| {submitted ? ( | ||||||||||||||||||
| <div className="feedback-success"> | ||||||||||||||||||
| <p>We appreciate you taking the time to help improve TigerType.</p> | ||||||||||||||||||
| <button | ||||||||||||||||||
| type="button" | ||||||||||||||||||
| className="feedback-primary-button" | ||||||||||||||||||
| onClick={closeIfAllowed} | ||||||||||||||||||
| > | ||||||||||||||||||
| Close | ||||||||||||||||||
| </button> | ||||||||||||||||||
| </div> | ||||||||||||||||||
| ) : ( | ||||||||||||||||||
| <form className="feedback-form" onSubmit={handleSubmit}> | ||||||||||||||||||
| <label> | ||||||||||||||||||
| Category | ||||||||||||||||||
| <select | ||||||||||||||||||
| value={category} | ||||||||||||||||||
| onChange={(event) => setCategory(event.target.value)} | ||||||||||||||||||
| disabled={submitting} | ||||||||||||||||||
| > | ||||||||||||||||||
| {CATEGORY_OPTIONS.map(option => ( | ||||||||||||||||||
| <option key={option.value} value={option.value}> | ||||||||||||||||||
| {option.label} | ||||||||||||||||||
| </option> | ||||||||||||||||||
| ))} | ||||||||||||||||||
| </select> | ||||||||||||||||||
| </label> | ||||||||||||||||||
|
|
||||||||||||||||||
| <label> | ||||||||||||||||||
| Describe what happened | ||||||||||||||||||
| <textarea | ||||||||||||||||||
| value={message} | ||||||||||||||||||
| onChange={(event) => setMessage(event.target.value)} | ||||||||||||||||||
| disabled={submitting} | ||||||||||||||||||
| rows={8} | ||||||||||||||||||
| maxLength={2000} | ||||||||||||||||||
| placeholder="Share details, steps to reproduce, or anything else we should know." | ||||||||||||||||||
| /> | ||||||||||||||||||
| <span className="feedback-hint">{message.trim().length}/2000 characters</span> | ||||||||||||||||||
|
Comment on lines
+125
to
+128
|
||||||||||||||||||
| maxLength={2000} | |
| placeholder="Share details, steps to reproduce, or anything else we should know." | |
| /> | |
| <span className="feedback-hint">{message.trim().length}/2000 characters</span> | |
| maxLength={4000} | |
| placeholder="Share details, steps to reproduce, or anything else we should know." | |
| /> | |
| <span className="feedback-hint">{message.trim().length}/4000 characters</span> |
Copilot
AI
Nov 7, 2025
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.
The placeholder text "we'll follow up here if we need more info" should start with a capital letter for consistency with other UI text in the application.
Suggestion: Change to "We'll follow up here if we need more info"
| placeholder="we'll follow up here if we need more info" | |
| placeholder="We'll follow up here if we need more info" |
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.
The SMTP_OAUTH_CACHE environment variable has a comment in the value portion which is not valid .env syntax. Comments should be on their own line or after the value with a
#separator.Suggestion: Change to: