818 lines
25 KiB
Markdown
818 lines
25 KiB
Markdown
# Feedback Form Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Add a feedback form page where logged-in users can submit feedback that gets emailed to a configurable address via the existing Resend infrastructure.
|
|
|
|
**Architecture:** New `POST /feedback` backend endpoint validates input and sends an HTML email via the existing `EmailService`. Frontend is a single `FeedbackPage.tsx` form page accessible from the sidebar nav and account settings. No database storage — email-only.
|
|
|
|
**Tech Stack:** FastAPI + Pydantic (backend), React + TypeScript + Tailwind (frontend), Resend (email delivery), slowapi (rate limiting)
|
|
|
|
**Design doc:** `docs/plans/2026-02-18-feedback-form-design.md`
|
|
|
|
---
|
|
|
|
## Task 1: Backend Schema
|
|
|
|
**Files:**
|
|
- Create: `backend/app/schemas/feedback.py`
|
|
|
|
**Step 1: Create the Pydantic schema**
|
|
|
|
```python
|
|
from enum import Enum
|
|
from pydantic import BaseModel, EmailStr, Field
|
|
|
|
|
|
class FeedbackType(str, Enum):
|
|
BUG_REPORT = "Bug Report"
|
|
FEATURE_REQUEST = "Feature Request"
|
|
USABILITY_ISSUE = "Usability Issue"
|
|
DOCUMENTATION = "Documentation"
|
|
GENERAL = "General Feedback"
|
|
|
|
|
|
class FeedbackSubmission(BaseModel):
|
|
email: EmailStr
|
|
feedback_type: FeedbackType
|
|
message: str = Field(..., min_length=10, max_length=5000)
|
|
|
|
|
|
class FeedbackResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
```
|
|
|
|
**Step 2: Commit**
|
|
|
|
```bash
|
|
git add backend/app/schemas/feedback.py
|
|
git commit -m "feat: add feedback submission schema"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Config — Add FEEDBACK_EMAIL
|
|
|
|
**Files:**
|
|
- Modify: `backend/app/core/config.py`
|
|
|
|
**Step 1: Add the FEEDBACK_EMAIL setting**
|
|
|
|
In `backend/app/core/config.py`, add this line in the `Settings` class after the `FROM_EMAIL` line (line 57):
|
|
|
|
```python
|
|
FEEDBACK_EMAIL: Optional[str] = None
|
|
```
|
|
|
|
**Step 2: Commit**
|
|
|
|
```bash
|
|
git add backend/app/core/config.py
|
|
git commit -m "feat: add FEEDBACK_EMAIL config setting"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: Email Service — Add send_feedback_email
|
|
|
|
**Files:**
|
|
- Modify: `backend/app/core/email.py`
|
|
|
|
**Step 1: Add the send_feedback_email method**
|
|
|
|
Add this method to the `EmailService` class (after `send_account_invite_email`, before the helper functions):
|
|
|
|
```python
|
|
@staticmethod
|
|
async def send_feedback_email(
|
|
to_email: str,
|
|
reply_to_email: str,
|
|
feedback_type: str,
|
|
message: str,
|
|
user_email: str,
|
|
account_name: str | None = None,
|
|
account_code: str | None = None,
|
|
) -> bool:
|
|
if not settings.email_enabled:
|
|
logger.warning("Email not sent — RESEND_API_KEY not configured")
|
|
return False
|
|
|
|
try:
|
|
import resend
|
|
from datetime import datetime, timezone
|
|
|
|
resend.api_key = settings.RESEND_API_KEY
|
|
|
|
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
code_suffix = f" — {account_code}" if account_code else ""
|
|
subject = f"[ResolutionFlow Feedback] {feedback_type} — {date_str}{code_suffix}"
|
|
|
|
html = _render_feedback_html(
|
|
feedback_type=feedback_type,
|
|
message=message,
|
|
user_email=user_email,
|
|
account_name=account_name,
|
|
account_code=account_code,
|
|
)
|
|
|
|
resend.Emails.send(
|
|
{
|
|
"from": settings.FROM_EMAIL,
|
|
"to": [to_email],
|
|
"reply_to": reply_to_email,
|
|
"subject": subject,
|
|
"html": html,
|
|
}
|
|
)
|
|
logger.info("Feedback email sent from %s (type: %s)", user_email, feedback_type)
|
|
return True
|
|
|
|
except Exception:
|
|
logger.exception("Failed to send feedback email from %s", user_email)
|
|
return False
|
|
```
|
|
|
|
**Step 2: Add the HTML renderer**
|
|
|
|
Add this function at the bottom of the file (after the other `_render_*` functions):
|
|
|
|
```python
|
|
def _render_feedback_html(
|
|
feedback_type: str,
|
|
message: str,
|
|
user_email: str,
|
|
account_name: str | None,
|
|
account_code: str | None,
|
|
) -> str:
|
|
from datetime import datetime, timezone
|
|
import html
|
|
|
|
date_str = datetime.now(timezone.utc).strftime("%B %d, %Y")
|
|
safe_message = html.escape(message).replace("\n", "<br>")
|
|
|
|
account_line = ""
|
|
if account_name and account_code:
|
|
account_line = f"""
|
|
<tr><td style="padding:0 40px 8px;">
|
|
<p style="margin:0;color:#a0a0a0;font-size:14px;">
|
|
<strong style="color:#e0e0e0;">Account:</strong> {html.escape(account_name)} ({html.escape(account_code)})
|
|
</p>
|
|
</td></tr>"""
|
|
|
|
return f"""<!DOCTYPE html>
|
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
|
|
<body style="margin:0;padding:0;background:#000;font-family:'Inter',Helvetica,Arial,sans-serif;">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#000;padding:40px 0;">
|
|
<tr><td align="center">
|
|
<table width="560" cellpadding="0" cellspacing="0" style="background:#111;border:1px solid rgba(255,255,255,0.06);border-radius:16px;">
|
|
<tr><td style="padding:40px 40px 24px;text-align:center;">
|
|
<h1 style="margin:0;color:#fff;font-size:24px;font-weight:600;">ResolutionFlow Feedback</h1>
|
|
</td></tr>
|
|
<tr><td style="padding:0 40px 8px;">
|
|
<p style="margin:0;color:#a0a0a0;font-size:14px;">
|
|
<strong style="color:#e0e0e0;">Type:</strong> {html.escape(feedback_type)}
|
|
</p>
|
|
</td></tr>
|
|
<tr><td style="padding:0 40px 8px;">
|
|
<p style="margin:0;color:#a0a0a0;font-size:14px;">
|
|
<strong style="color:#e0e0e0;">From:</strong> {html.escape(user_email)}
|
|
</p>
|
|
</td></tr>
|
|
{account_line}
|
|
<tr><td style="padding:0 40px 8px;">
|
|
<p style="margin:0;color:#a0a0a0;font-size:14px;">
|
|
<strong style="color:#e0e0e0;">Date:</strong> {date_str}
|
|
</p>
|
|
</td></tr>
|
|
<tr><td style="padding:16px 40px 0;">
|
|
<div style="border-top:1px solid rgba(255,255,255,0.06);padding-top:16px;">
|
|
<p style="margin:0;color:#e0e0e0;font-size:15px;line-height:1.7;">{safe_message}</p>
|
|
</div>
|
|
</td></tr>
|
|
<tr><td style="padding:24px 40px 32px;">
|
|
<p style="margin:0;color:#666;font-size:12px;text-align:center;">
|
|
Reply directly to this email to respond to the user.
|
|
</p>
|
|
</td></tr>
|
|
</table>
|
|
</td></tr>
|
|
</table>
|
|
</body></html>"""
|
|
```
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add backend/app/core/email.py
|
|
git commit -m "feat: add send_feedback_email to EmailService"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Backend Endpoint
|
|
|
|
**Files:**
|
|
- Create: `backend/app/api/endpoints/feedback.py`
|
|
- Modify: `backend/app/api/router.py`
|
|
|
|
**Step 1: Create the endpoint**
|
|
|
|
Create `backend/app/api/endpoints/feedback.py`:
|
|
|
|
```python
|
|
import logging
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
|
|
from app.api.deps import get_current_active_user
|
|
from app.core.config import settings
|
|
from app.core.database import get_db
|
|
from app.core.email import EmailService
|
|
from app.core.rate_limit import limiter
|
|
from app.models.user import User
|
|
from app.models.account import Account
|
|
from app.schemas.feedback import FeedbackSubmission, FeedbackResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(tags=["feedback"])
|
|
|
|
|
|
@router.post("/feedback", response_model=FeedbackResponse)
|
|
@limiter.limit("1/minute")
|
|
async def submit_feedback(
|
|
request: Request,
|
|
data: FeedbackSubmission,
|
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
):
|
|
"""Submit user feedback via email."""
|
|
if not settings.FEEDBACK_EMAIL:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="Feedback submission is not configured",
|
|
)
|
|
|
|
# Get account info for the email
|
|
account_name = None
|
|
account_code = None
|
|
if current_user.account_id:
|
|
result = await db.execute(
|
|
select(Account).where(Account.id == current_user.account_id)
|
|
)
|
|
account = result.scalar_one_or_none()
|
|
if account:
|
|
account_name = account.name
|
|
account_code = account.display_code
|
|
|
|
sent = await EmailService.send_feedback_email(
|
|
to_email=settings.FEEDBACK_EMAIL,
|
|
reply_to_email=data.email,
|
|
feedback_type=data.feedback_type.value,
|
|
message=data.message,
|
|
user_email=current_user.email,
|
|
account_name=account_name,
|
|
account_code=account_code,
|
|
)
|
|
|
|
if not sent:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to send feedback. Please try again later.",
|
|
)
|
|
|
|
return FeedbackResponse(success=True, message="Thank you! Your feedback has been submitted.")
|
|
```
|
|
|
|
**Step 2: Register the router**
|
|
|
|
In `backend/app/api/router.py`, add the import and include:
|
|
|
|
Add to imports (line 6, after the existing imports):
|
|
```python
|
|
from app.api.endpoints import feedback
|
|
```
|
|
|
|
Add at the end of the router registrations:
|
|
```python
|
|
api_router.include_router(feedback.router)
|
|
```
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add backend/app/api/endpoints/feedback.py backend/app/api/router.py
|
|
git commit -m "feat: add POST /feedback endpoint"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Backend Test
|
|
|
|
**Files:**
|
|
- Create: `backend/tests/test_feedback.py`
|
|
|
|
**Step 1: Write the test**
|
|
|
|
```python
|
|
import pytest
|
|
from unittest.mock import patch, AsyncMock
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_submit_feedback(async_client, engineer_token, monkeypatch):
|
|
"""Test successful feedback submission."""
|
|
monkeypatch.setenv("FEEDBACK_EMAIL", "support@test.com")
|
|
# Reload settings to pick up the env var
|
|
from app.core.config import Settings
|
|
test_settings = Settings()
|
|
|
|
with patch("app.api.endpoints.feedback.settings") as mock_settings, \
|
|
patch("app.api.endpoints.feedback.EmailService") as mock_email:
|
|
mock_settings.FEEDBACK_EMAIL = "support@test.com"
|
|
mock_email.send_feedback_email = AsyncMock(return_value=True)
|
|
|
|
response = await async_client.post(
|
|
"/api/v1/feedback",
|
|
json={
|
|
"email": "engineer@resolutionflow.example.com",
|
|
"feedback_type": "Bug Report",
|
|
"message": "Something is broken in the tree editor when I try to save.",
|
|
},
|
|
headers={"Authorization": f"Bearer {engineer_token}"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert "submitted" in data["message"].lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_submit_feedback_not_configured(async_client, engineer_token):
|
|
"""Test 503 when FEEDBACK_EMAIL is not set."""
|
|
with patch("app.api.endpoints.feedback.settings") as mock_settings:
|
|
mock_settings.FEEDBACK_EMAIL = None
|
|
|
|
response = await async_client.post(
|
|
"/api/v1/feedback",
|
|
json={
|
|
"email": "engineer@resolutionflow.example.com",
|
|
"feedback_type": "General Feedback",
|
|
"message": "This is a general feedback message for testing.",
|
|
},
|
|
headers={"Authorization": f"Bearer {engineer_token}"},
|
|
)
|
|
|
|
assert response.status_code == 503
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_submit_feedback_validation(async_client, engineer_token):
|
|
"""Test validation — message too short."""
|
|
with patch("app.api.endpoints.feedback.settings") as mock_settings:
|
|
mock_settings.FEEDBACK_EMAIL = "support@test.com"
|
|
|
|
response = await async_client.post(
|
|
"/api/v1/feedback",
|
|
json={
|
|
"email": "engineer@resolutionflow.example.com",
|
|
"feedback_type": "Bug Report",
|
|
"message": "short",
|
|
},
|
|
headers={"Authorization": f"Bearer {engineer_token}"},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_submit_feedback_invalid_type(async_client, engineer_token):
|
|
"""Test validation — invalid feedback type."""
|
|
with patch("app.api.endpoints.feedback.settings") as mock_settings:
|
|
mock_settings.FEEDBACK_EMAIL = "support@test.com"
|
|
|
|
response = await async_client.post(
|
|
"/api/v1/feedback",
|
|
json={
|
|
"email": "engineer@resolutionflow.example.com",
|
|
"feedback_type": "Invalid Type",
|
|
"message": "This should fail because the type is invalid.",
|
|
},
|
|
headers={"Authorization": f"Bearer {engineer_token}"},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_submit_feedback_requires_auth(async_client):
|
|
"""Test that unauthenticated requests are rejected."""
|
|
response = await async_client.post(
|
|
"/api/v1/feedback",
|
|
json={
|
|
"email": "anon@example.com",
|
|
"feedback_type": "General Feedback",
|
|
"message": "This should fail because I'm not logged in.",
|
|
},
|
|
)
|
|
assert response.status_code == 401
|
|
```
|
|
|
|
**Step 2: Run the tests**
|
|
|
|
```bash
|
|
cd backend && pytest tests/test_feedback.py -v --override-ini="addopts="
|
|
```
|
|
|
|
Expected: All 5 tests pass. If any fail, debug and fix before proceeding.
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add backend/tests/test_feedback.py
|
|
git commit -m "test: add feedback endpoint tests"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Frontend API Client
|
|
|
|
**Files:**
|
|
- Create: `frontend/src/api/feedback.ts`
|
|
- Modify: `frontend/src/api/index.ts`
|
|
|
|
**Step 1: Create the API module**
|
|
|
|
Create `frontend/src/api/feedback.ts`:
|
|
|
|
```typescript
|
|
import { apiClient } from './client'
|
|
|
|
export interface FeedbackSubmission {
|
|
email: string
|
|
feedback_type: string
|
|
message: string
|
|
}
|
|
|
|
export interface FeedbackResponse {
|
|
success: boolean
|
|
message: string
|
|
}
|
|
|
|
export const feedbackApi = {
|
|
submit: async (data: FeedbackSubmission): Promise<FeedbackResponse> => {
|
|
const { data: response } = await apiClient.post('/feedback', data)
|
|
return response
|
|
},
|
|
}
|
|
|
|
export default feedbackApi
|
|
```
|
|
|
|
**Step 2: Export from index**
|
|
|
|
In `frontend/src/api/index.ts`, add at the end:
|
|
|
|
```typescript
|
|
export { default as feedbackApi } from './feedback'
|
|
```
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/api/feedback.ts frontend/src/api/index.ts
|
|
git commit -m "feat: add feedback API client"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: Frontend Page
|
|
|
|
**Files:**
|
|
- Create: `frontend/src/pages/FeedbackPage.tsx`
|
|
|
|
**Step 1: Create the page component**
|
|
|
|
```tsx
|
|
import { useState } from 'react'
|
|
import { MessageSquareText, Send, CheckCircle2 } from 'lucide-react'
|
|
import { useAuthStore } from '@/store/authStore'
|
|
import { feedbackApi } from '@/api'
|
|
import { cn } from '@/lib/utils'
|
|
import { toast } from '@/lib/toast'
|
|
|
|
const FEEDBACK_TYPES = [
|
|
'Bug Report',
|
|
'Feature Request',
|
|
'Usability Issue',
|
|
'Documentation',
|
|
'General Feedback',
|
|
] as const
|
|
|
|
export function FeedbackPage() {
|
|
const user = useAuthStore(s => s.user)
|
|
|
|
const [email, setEmail] = useState(user?.email ?? '')
|
|
const [feedbackType, setFeedbackType] = useState<string>('')
|
|
const [message, setMessage] = useState('')
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
const [submitted, setSubmitted] = useState(false)
|
|
|
|
const canSubmit = email.trim() && feedbackType && message.trim().length >= 10
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!canSubmit || isSubmitting) return
|
|
|
|
setIsSubmitting(true)
|
|
try {
|
|
const response = await feedbackApi.submit({
|
|
email: email.trim(),
|
|
feedback_type: feedbackType,
|
|
message: message.trim(),
|
|
})
|
|
if (response.success) {
|
|
setSubmitted(true)
|
|
setFeedbackType('')
|
|
setMessage('')
|
|
}
|
|
} catch (err: unknown) {
|
|
const error = err as { response?: { data?: { detail?: string } } }
|
|
toast.error(error.response?.data?.detail || 'Failed to submit feedback. Please try again.')
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
const handleNewFeedback = () => {
|
|
setSubmitted(false)
|
|
setEmail(user?.email ?? '')
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
|
{/* Page header */}
|
|
<div className="mb-8">
|
|
<div className="flex items-center gap-3">
|
|
<MessageSquareText className="h-8 w-8 text-muted-foreground" />
|
|
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Send Feedback</h1>
|
|
</div>
|
|
<p className="mt-2 text-muted-foreground">
|
|
Help us improve ResolutionFlow. Report bugs, request features, or share your thoughts.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="max-w-2xl">
|
|
{submitted ? (
|
|
<div className="bg-card border border-border rounded-xl p-8 text-center">
|
|
<CheckCircle2 className="mx-auto h-12 w-12 text-green-500 mb-4" />
|
|
<h2 className="text-xl font-semibold text-foreground mb-2">Thank you for your feedback!</h2>
|
|
<p className="text-muted-foreground mb-6">
|
|
We've received your submission and will review it shortly.
|
|
</p>
|
|
<button
|
|
onClick={handleNewFeedback}
|
|
className="bg-gradient-brand text-white font-medium px-6 py-2.5 rounded-lg shadow-lg shadow-primary/20 hover:opacity-90 transition-opacity"
|
|
>
|
|
Send More Feedback
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={handleSubmit} className="bg-card border border-border rounded-xl p-4 sm:p-6 space-y-5">
|
|
{/* Email */}
|
|
<div>
|
|
<label htmlFor="feedback-email" className="block text-sm font-medium text-foreground mb-1.5">
|
|
Email Address
|
|
</label>
|
|
<input
|
|
id="feedback-email"
|
|
type="email"
|
|
value={email}
|
|
onChange={e => setEmail(e.target.value)}
|
|
placeholder="your@email.com"
|
|
required
|
|
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none"
|
|
/>
|
|
<p className="mt-1 text-xs text-muted-foreground">We'll reply to this address if we need more details.</p>
|
|
</div>
|
|
|
|
{/* Feedback Type */}
|
|
<div>
|
|
<label htmlFor="feedback-type" className="block text-sm font-medium text-foreground mb-1.5">
|
|
Feedback Type
|
|
</label>
|
|
<select
|
|
id="feedback-type"
|
|
value={feedbackType}
|
|
onChange={e => setFeedbackType(e.target.value)}
|
|
required
|
|
className={cn(
|
|
"w-full rounded-lg border border-border bg-card px-3 py-2 focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none",
|
|
feedbackType ? "text-foreground" : "text-muted-foreground"
|
|
)}
|
|
>
|
|
<option value="" disabled>Select a type...</option>
|
|
{FEEDBACK_TYPES.map(type => (
|
|
<option key={type} value={type}>{type}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Message */}
|
|
<div>
|
|
<label htmlFor="feedback-message" className="block text-sm font-medium text-foreground mb-1.5">
|
|
Your Feedback
|
|
</label>
|
|
<textarea
|
|
id="feedback-message"
|
|
value={message}
|
|
onChange={e => setMessage(e.target.value)}
|
|
placeholder="Describe your feedback in detail..."
|
|
required
|
|
minLength={10}
|
|
rows={6}
|
|
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none resize-y"
|
|
/>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
{message.trim().length < 10
|
|
? `Minimum 10 characters (${message.trim().length}/10)`
|
|
: `${message.trim().length} characters`}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Submit */}
|
|
<div className="pt-2">
|
|
<button
|
|
type="submit"
|
|
disabled={!canSubmit || isSubmitting}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-lg px-6 py-2.5 font-medium text-white shadow-lg shadow-primary/20 transition-opacity",
|
|
canSubmit && !isSubmitting
|
|
? "bg-gradient-brand hover:opacity-90"
|
|
: "bg-gradient-brand opacity-50 cursor-not-allowed"
|
|
)}
|
|
>
|
|
<Send size={16} />
|
|
{isSubmitting ? 'Sending...' : 'Submit Feedback'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default FeedbackPage
|
|
```
|
|
|
|
**Step 2: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/pages/FeedbackPage.tsx
|
|
git commit -m "feat: add FeedbackPage component"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8: Router & Navigation
|
|
|
|
**Files:**
|
|
- Modify: `frontend/src/router.tsx`
|
|
- Modify: `frontend/src/components/layout/Sidebar.tsx`
|
|
- Modify: `frontend/src/pages/AccountSettingsPage.tsx`
|
|
|
|
**Step 1: Add the route**
|
|
|
|
In `frontend/src/router.tsx`:
|
|
|
|
Add to the lazy imports section (after the `MyAnalyticsPage` import, around line 32):
|
|
```typescript
|
|
const FeedbackPage = lazy(() => import('@/pages/FeedbackPage'))
|
|
```
|
|
|
|
Add as a child of the `'/'` route, after the `analytics/me` route (around line 228, before the admin routes comment):
|
|
```tsx
|
|
{
|
|
path: 'feedback',
|
|
element: (
|
|
<Suspense fallback={<PageLoader />}>
|
|
<FeedbackPage />
|
|
</Suspense>
|
|
),
|
|
},
|
|
```
|
|
|
|
**Step 2: Add the sidebar nav item**
|
|
|
|
In `frontend/src/components/layout/Sidebar.tsx`:
|
|
|
|
Add `MessageSquareText` to the lucide-react import (line 3):
|
|
```typescript
|
|
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Users, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText } from 'lucide-react'
|
|
```
|
|
|
|
In the collapsed sidebar section (around line 147, after the Analytics NavItem):
|
|
```tsx
|
|
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" collapsed />
|
|
```
|
|
|
|
In the expanded sidebar section, add in the footer area (around line 202, before the Team NavItem):
|
|
```tsx
|
|
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" />
|
|
```
|
|
|
|
**Step 3: Add the account settings link card**
|
|
|
|
In `frontend/src/pages/AccountSettingsPage.tsx`:
|
|
|
|
Add `MessageSquareText` to the lucide-react import.
|
|
|
|
Add a link card after the last existing link card (after the Target Lists card), outside any permission guard:
|
|
```tsx
|
|
{/* Feedback Link (all users) */}
|
|
<Link
|
|
to="/feedback"
|
|
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<MessageSquareText className="h-5 w-5 text-muted-foreground" />
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-foreground">Send Feedback</h2>
|
|
<p className="text-sm text-muted-foreground">Report bugs, request features, or share your thoughts</p>
|
|
</div>
|
|
</div>
|
|
<span className="text-muted-foreground group-hover:text-foreground transition-colors">→</span>
|
|
</Link>
|
|
```
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add frontend/src/router.tsx frontend/src/components/layout/Sidebar.tsx frontend/src/pages/AccountSettingsPage.tsx
|
|
git commit -m "feat: add feedback route, sidebar nav item, and account link card"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 9: Build Verification
|
|
|
|
**Step 1: Run the frontend build**
|
|
|
|
```bash
|
|
cd frontend && npm run build
|
|
```
|
|
|
|
Expected: Build succeeds with no errors. TypeScript type-checking is stricter in the build than `tsc --noEmit`.
|
|
|
|
**Step 2: Run the backend tests**
|
|
|
|
```bash
|
|
cd backend && pytest tests/test_feedback.py -v --override-ini="addopts="
|
|
```
|
|
|
|
Expected: All tests pass.
|
|
|
|
**Step 3: Set FEEDBACK_EMAIL in .env for local testing**
|
|
|
|
Add to `backend/.env`:
|
|
```
|
|
FEEDBACK_EMAIL=your-email@example.com
|
|
```
|
|
|
|
**Step 4: Manual smoke test**
|
|
|
|
1. Start backend: `cd backend && uvicorn app.main:app --reload`
|
|
2. Start frontend: `cd frontend && npm run dev`
|
|
3. Log in as any test user
|
|
4. Navigate to `/feedback` via sidebar
|
|
5. Verify form loads with email pre-filled
|
|
6. Submit feedback — verify success state
|
|
7. Check email inbox for the formatted feedback email
|
|
8. Verify the account settings page shows the "Send Feedback" link card
|
|
|
|
---
|
|
|
|
## Task 10: Final Commit & Cleanup
|
|
|
|
**Step 1: Run full backend test suite**
|
|
|
|
```bash
|
|
cd backend && pytest --override-ini="addopts="
|
|
```
|
|
|
|
Expected: All tests pass, no regressions.
|
|
|
|
**Step 2: Final frontend build check**
|
|
|
|
```bash
|
|
cd frontend && npm run build
|
|
```
|
|
|
|
Expected: Clean build.
|