Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ DB_HOST=localhost
DB_PORT=5432
DB_NAME=tigertype

## Feedback Email
FEEDBACK_EMAIL_FROM=cs-tigertype@princeton.edu
FEEDBACK_EMAIL_TO_TEAM=cs-tigertype@princeton.edu,it.admin@tigerapps.org
FEEDBACK_REPLY_TO=cs-tigertype@princeton.edu,it.admin@tigerapps.org
SITE_URL=https://tigertype.tigerapps.org

# Scraping Configuration
OPENAI_API_KEY=your_api_key_here
PRINCETON_API_KEY=""
Expand Down Expand Up @@ -43,3 +49,11 @@ CHANGELOG_PUBLISH_TOKEN=your_shared_secret_token

# Start PostgreSQL service
# brew services start postgresql

# Delegated SMTP OAuth (device code, cached)
# Run: node server/scripts/seed_smtp_oauth_device_login.js (saves a cache file and prints JSON)
# For Heroku, set SMTP_OAUTH_CACHE to the printed JSON
AZURE_TENANT_ID=
AZURE_CLIENT_ID=
SMTP_SENDER=cs-tigertype@princeton.edu
SMTP_OAUTH_CACHE= (optional; JSON blob of MSAL cache for headless servers)
Copy link

Copilot AI Nov 7, 2025

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:

# Optional: JSON blob of MSAL cache for headless servers
SMTP_OAUTH_CACHE=
Suggested change
SMTP_OAUTH_CACHE= (optional; JSON blob of MSAL cache for headless servers)
# Optional: JSON blob of MSAL cache for headless servers
SMTP_OAUTH_CACHE=

Copilot uses AI. Check for mistakes.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ AGENTS.*
# Environment and Configuration
.env
.venv
server/.smtp_oauth_cache.json

# Dependencies
node_modules
Expand All @@ -34,4 +35,4 @@ __pycache__
.gitattributes

# Miscellaneous
.bak
.bak
124 changes: 124 additions & 0 deletions client/src/components/FeedbackModal.css
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;
}
}
172 changes: 172 additions & 0 deletions client/src/components/FeedbackModal.jsx
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
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client-side textarea has a maxLength of 2000 characters, but the server-side sanitizes messages to 4000 characters. This inconsistency could be confusing. Consider aligning these values - either increase the client-side limit to 4000 or decrease the server-side limit to 2000.

Suggested change
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 uses AI. Check for mistakes.
</label>

<label>
Contact (optional)
<input
type="email"
value={contactInfo}
onChange={(event) => setContactInfo(event.target.value)}
disabled={submitting}
placeholder="we'll follow up here if we need more info"
Copy link

Copilot AI Nov 7, 2025

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"

Suggested change
placeholder="we'll follow up here if we need more info"
placeholder="We'll follow up here if we need more info"

Copilot uses AI. Check for mistakes.
/>
</label>

{error && <p className="feedback-error">{error}</p>}

<div className="feedback-actions">
<button
type="button"
className="feedback-secondary-button"
onClick={closeIfAllowed}
disabled={submitting}
>
Cancel
</button>
<button
type="submit"
className="feedback-primary-button"
disabled={submitting}
>
{submitting ? 'Sending…' : 'Send feedback'}
</button>
</div>
</form>
)}
</Modal>
);
}

FeedbackModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired
};

export default FeedbackModal;
21 changes: 21 additions & 0 deletions client/src/components/Navbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@
gap: 0.5rem;
}

.navbar-feedback-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
color: var(--mode-text-color);
border-radius: 6px;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
background: none;
border: none;
cursor: pointer;
}

.navbar-feedback-icon:hover {
background-color: rgba(245, 128, 37, 0.1);
color: #F58025;
transform: translateY(-1px);
}

.settings-button-wrapper {
position: relative;
display: inline-flex;
Expand Down Expand Up @@ -104,6 +124,7 @@
transform: rotate(90deg);
}
.navbar-icons button:focus-visible,
.navbar-feedback-icon:focus-visible,
.navbar-github-icon:focus-visible,
.navbar-logo button:focus-visible {
outline: 2px solid #F58025;
Expand Down
Loading