docs: revise feedback implementation plan with enhancements

Adds: DB persistence, feedback type helper text, confirmation
email to submitter, and TODO breadcrumbs for post-session prompt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-18 17:27:14 -05:00
parent b0b77d6645
commit e995316d41

View File

@@ -2,11 +2,11 @@
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. > **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. **Goal:** Add a feedback form page where logged-in users can submit feedback that gets stored in the database and emailed to a configurable address via the existing Resend infrastructure. Includes confirmation email to the submitter, helper text on feedback types, and TODO breadcrumbs for future post-session contextual feedback.
**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. **Architecture:** New `POST /feedback` backend endpoint validates input, writes to a `feedback` table, sends an HTML notification email to the configured address, and fires a confirmation email back to the submitter. Email failures do NOT prevent the DB write from succeeding. Frontend is a single `FeedbackPage.tsx` form page with a custom feedback type selector (with descriptions) accessible from the sidebar nav and account settings.
**Tech Stack:** FastAPI + Pydantic (backend), React + TypeScript + Tailwind (frontend), Resend (email delivery), slowapi (rate limiting) **Tech Stack:** FastAPI + Pydantic + Alembic (backend), React + TypeScript + Tailwind (frontend), Resend (email delivery), slowapi (rate limiting)
**Design doc:** `docs/plans/2026-02-18-feedback-form-design.md` **Design doc:** `docs/plans/2026-02-18-feedback-form-design.md`
@@ -52,7 +52,117 @@ git commit -m "feat: add feedback submission schema"
--- ---
## Task 2: Config — Add FEEDBACK_EMAIL ## Task 2: Database Model & Migration
**Files:**
- Create: `backend/app/models/feedback.py`
- Modify: `backend/app/models/__init__.py`
- Create: Alembic migration (manual)
**Step 1: Create the SQLAlchemy model**
Create `backend/app/models/feedback.py`:
```python
import uuid
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import String, Text, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID
from app.core.database import Base
class Feedback(Base):
__tablename__ = "feedback"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
account_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="SET NULL"), nullable=True)
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=False)
email: Mapped[str] = mapped_column(String(255), nullable=False)
feedback_type: Mapped[str] = mapped_column(String(50), nullable=False)
message: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
```
**Step 2: Register the model**
In `backend/app/models/__init__.py`, add the import and export:
Add import:
```python
from .feedback import Feedback
```
Add `"Feedback"` to the `__all__` list.
**Step 3: Create the migration manually**
Run:
```bash
cd backend && alembic revision -m "add feedback table"
```
Then edit the generated migration file:
```python
"""add feedback table
Revision ID: <auto-generated>
Revises: 7e00fa3c75c9
Create Date: 2026-02-18
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '<auto-generated>'
down_revision = '7e00fa3c75c9'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
'feedback',
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column('account_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('accounts.id', ondelete='SET NULL'), nullable=True),
sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=False),
sa.Column('email', sa.String(255), nullable=False),
sa.Column('feedback_type', sa.String(50), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index('ix_feedback_account_id', 'feedback', ['account_id'])
op.create_index('ix_feedback_user_id', 'feedback', ['user_id'])
op.create_index('ix_feedback_created_at', 'feedback', ['created_at'])
def downgrade() -> None:
op.drop_index('ix_feedback_created_at', table_name='feedback')
op.drop_index('ix_feedback_user_id', table_name='feedback')
op.drop_index('ix_feedback_account_id', table_name='feedback')
op.drop_table('feedback')
```
**Step 4: Run the migration**
```bash
cd backend && alembic upgrade head
```
**Step 5: Commit**
```bash
git add backend/app/models/feedback.py backend/app/models/__init__.py backend/alembic/versions/*feedback*.py
git commit -m "feat: add feedback database model and migration"
```
---
## Task 3: Config — Add FEEDBACK_EMAIL
**Files:** **Files:**
- Modify: `backend/app/core/config.py` - Modify: `backend/app/core/config.py`
@@ -74,12 +184,12 @@ git commit -m "feat: add FEEDBACK_EMAIL config setting"
--- ---
## Task 3: Email Service — Add send_feedback_email ## Task 4: Email Service — Add feedback emails
**Files:** **Files:**
- Modify: `backend/app/core/email.py` - Modify: `backend/app/core/email.py`
**Step 1: Add the send_feedback_email method** **Step 1: Add the send_feedback_email method (admin notification)**
Add this method to the `EmailService` class (after `send_account_invite_email`, before the helper functions): Add this method to the `EmailService` class (after `send_account_invite_email`, before the helper functions):
@@ -133,9 +243,53 @@ Add this method to the `EmailService` class (after `send_account_invite_email`,
return False return False
``` ```
**Step 2: Add the HTML renderer** **Step 2: Add the send_feedback_confirmation_email method (user confirmation)**
Add this function at the bottom of the file (after the other `_render_*` functions): Add this method right after `send_feedback_email`:
```python
@staticmethod
async def send_feedback_confirmation_email(
to_email: str,
feedback_type: str,
message_preview: str,
) -> bool:
"""Send a thank-you confirmation to the feedback submitter. Fire-and-forget."""
if not settings.email_enabled:
logger.warning("Confirmation email not sent — RESEND_API_KEY not configured")
return False
try:
import resend
resend.api_key = settings.RESEND_API_KEY
subject = "Thanks for your feedback — ResolutionFlow"
html = _render_feedback_confirmation_html(
feedback_type=feedback_type,
message_preview=message_preview,
)
resend.Emails.send(
{
"from": settings.FROM_EMAIL,
"to": [to_email],
"subject": subject,
"html": html,
}
)
logger.info("Feedback confirmation email sent to %s", to_email)
return True
except Exception:
logger.exception("Failed to send feedback confirmation to %s", to_email)
return False
```
**Step 3: Add the HTML renderers**
Add these functions at the bottom of the file (after the other `_render_*` functions):
```python ```python
def _render_feedback_html( def _render_feedback_html(
@@ -199,18 +353,58 @@ def _render_feedback_html(
</td></tr> </td></tr>
</table> </table>
</body></html>""" </body></html>"""
def _render_feedback_confirmation_html(
feedback_type: str,
message_preview: str,
) -> str:
import html
safe_preview = html.escape(message_preview)
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</h1>
<p style="margin:8px 0 0;color:#a0a0a0;font-size:14px;">Thanks for your feedback!</p>
</td></tr>
<tr><td style="padding:0 40px 24px;">
<p style="margin:0;color:#e0e0e0;font-size:16px;line-height:1.6;">
We've received your <strong style="color:#fff;">{html.escape(feedback_type)}</strong> and our team will review it shortly.
</p>
</td></tr>
<tr><td style="padding:0 40px 24px;">
<div style="background:#000;border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:16px 20px;">
<p style="margin:0 0 4px;color:#a0a0a0;font-size:12px;text-transform:uppercase;letter-spacing:1px;">Your feedback</p>
<p style="margin:0;color:#e0e0e0;font-size:14px;line-height:1.5;font-style:italic;">"{safe_preview}"</p>
</div>
</td></tr>
<tr><td style="padding:0 40px 32px;">
<p style="margin:0;color:#666;font-size:12px;text-align:center;">
If we need more details, we'll reach out to you directly.
</p>
</td></tr>
</table>
</td></tr>
</table>
</body></html>"""
``` ```
**Step 3: Commit** **Step 4: Commit**
```bash ```bash
git add backend/app/core/email.py git add backend/app/core/email.py
git commit -m "feat: add send_feedback_email to EmailService" git commit -m "feat: add feedback notification and confirmation emails to EmailService"
``` ```
--- ---
## Task 4: Backend Endpoint ## Task 5: Backend Endpoint
**Files:** **Files:**
- Create: `backend/app/api/endpoints/feedback.py` - Create: `backend/app/api/endpoints/feedback.py`
@@ -235,12 +429,18 @@ from app.core.email import EmailService
from app.core.rate_limit import limiter from app.core.rate_limit import limiter
from app.models.user import User from app.models.user import User
from app.models.account import Account from app.models.account import Account
from app.models.feedback import Feedback
from app.schemas.feedback import FeedbackSubmission, FeedbackResponse from app.schemas.feedback import FeedbackSubmission, FeedbackResponse
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(tags=["feedback"]) router = APIRouter(tags=["feedback"])
# TODO: Post-session contextual feedback prompt — when building the post-session
# feedback flow, reuse this endpoint by adding optional session_id/tree_id fields
# to FeedbackSubmission. The Feedback model and email infrastructure are already
# in place. See design doc for details.
@router.post("/feedback", response_model=FeedbackResponse) @router.post("/feedback", response_model=FeedbackResponse)
@limiter.limit("1/minute") @limiter.limit("1/minute")
@@ -250,7 +450,7 @@ async def submit_feedback(
current_user: Annotated[User, Depends(get_current_active_user)], current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)], db: Annotated[AsyncSession, Depends(get_db)],
): ):
"""Submit user feedback via email.""" """Submit user feedback. Saves to DB and sends notification email."""
if not settings.FEEDBACK_EMAIL: if not settings.FEEDBACK_EMAIL:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
@@ -269,6 +469,18 @@ async def submit_feedback(
account_name = account.name account_name = account.name
account_code = account.display_code account_code = account.display_code
# Always persist to DB first — email failure should not lose feedback
feedback_record = Feedback(
account_id=current_user.account_id,
user_id=current_user.id,
email=data.email,
feedback_type=data.feedback_type.value,
message=data.message,
)
db.add(feedback_record)
await db.commit()
# Send notification email to admin (best-effort)
sent = await EmailService.send_feedback_email( sent = await EmailService.send_feedback_email(
to_email=settings.FEEDBACK_EMAIL, to_email=settings.FEEDBACK_EMAIL,
reply_to_email=data.email, reply_to_email=data.email,
@@ -280,10 +492,15 @@ async def submit_feedback(
) )
if not sent: if not sent:
raise HTTPException( logger.warning("Feedback saved to DB but notification email failed for user %s", current_user.email)
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send feedback. Please try again later.", # Send confirmation email to submitter (fire-and-forget)
) message_preview = data.message[:100] + ("..." if len(data.message) > 100 else "")
await EmailService.send_feedback_confirmation_email(
to_email=data.email,
feedback_type=data.feedback_type.value,
message_preview=message_preview,
)
return FeedbackResponse(success=True, message="Thank you! Your feedback has been submitted.") return FeedbackResponse(success=True, message="Thank you! Your feedback has been submitted.")
``` ```
@@ -292,7 +509,7 @@ async def submit_feedback(
In `backend/app/api/router.py`, add the import and include: In `backend/app/api/router.py`, add the import and include:
Add to imports (line 6, after the existing imports): Add to imports (after the existing import lines):
```python ```python
from app.api.endpoints import feedback from app.api.endpoints import feedback
``` ```
@@ -306,17 +523,19 @@ api_router.include_router(feedback.router)
```bash ```bash
git add backend/app/api/endpoints/feedback.py backend/app/api/router.py git add backend/app/api/endpoints/feedback.py backend/app/api/router.py
git commit -m "feat: add POST /feedback endpoint" git commit -m "feat: add POST /feedback endpoint with DB persistence and dual emails"
``` ```
--- ---
## Task 5: Backend Test ## Task 6: Backend Tests
**Files:** **Files:**
- Create: `backend/tests/test_feedback.py` - Create: `backend/tests/test_feedback.py`
**Step 1: Write the test** **Step 1: Write the tests**
These tests use the project's existing `client` and `auth_headers` fixtures from `conftest.py`.
```python ```python
import pytest import pytest
@@ -324,26 +543,22 @@ from unittest.mock import patch, AsyncMock
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_submit_feedback(async_client, engineer_token, monkeypatch): async def test_submit_feedback(client, auth_headers):
"""Test successful feedback submission.""" """Test successful feedback submission — saves to DB and sends emails."""
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, \ with patch("app.api.endpoints.feedback.settings") as mock_settings, \
patch("app.api.endpoints.feedback.EmailService") as mock_email: patch("app.api.endpoints.feedback.EmailService") as mock_email:
mock_settings.FEEDBACK_EMAIL = "support@test.com" mock_settings.FEEDBACK_EMAIL = "support@test.com"
mock_email.send_feedback_email = AsyncMock(return_value=True) mock_email.send_feedback_email = AsyncMock(return_value=True)
mock_email.send_feedback_confirmation_email = AsyncMock(return_value=True)
response = await async_client.post( response = await client.post(
"/api/v1/feedback", "/api/v1/feedback",
json={ json={
"email": "engineer@resolutionflow.example.com", "email": "test@example.com",
"feedback_type": "Bug Report", "feedback_type": "Bug Report",
"message": "Something is broken in the tree editor when I try to save.", "message": "Something is broken in the tree editor when I try to save.",
}, },
headers={"Authorization": f"Bearer {engineer_token}"}, headers=auth_headers,
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -351,68 +566,98 @@ async def test_submit_feedback(async_client, engineer_token, monkeypatch):
assert data["success"] is True assert data["success"] is True
assert "submitted" in data["message"].lower() assert "submitted" in data["message"].lower()
# Verify both emails were called
mock_email.send_feedback_email.assert_called_once()
mock_email.send_feedback_confirmation_email.assert_called_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_submit_feedback_not_configured(async_client, engineer_token): async def test_submit_feedback_saves_to_db_even_if_email_fails(client, auth_headers, test_db):
"""Test that feedback is persisted even when email sending fails."""
from sqlalchemy import select, func
from app.models.feedback import Feedback
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=False)
mock_email.send_feedback_confirmation_email = AsyncMock(return_value=False)
response = await client.post(
"/api/v1/feedback",
json={
"email": "test@example.com",
"feedback_type": "Feature Request",
"message": "Please add dark mode to the export preview screen.",
},
headers=auth_headers,
)
# Should still succeed — DB write happened
assert response.status_code == 200
assert response.json()["success"] is True
# Verify it was saved to the database
result = await test_db.execute(select(func.count()).select_from(Feedback))
count = result.scalar()
assert count >= 1
@pytest.mark.asyncio
async def test_submit_feedback_not_configured(client, auth_headers):
"""Test 503 when FEEDBACK_EMAIL is not set.""" """Test 503 when FEEDBACK_EMAIL is not set."""
with patch("app.api.endpoints.feedback.settings") as mock_settings: with patch("app.api.endpoints.feedback.settings") as mock_settings:
mock_settings.FEEDBACK_EMAIL = None mock_settings.FEEDBACK_EMAIL = None
response = await async_client.post( response = await client.post(
"/api/v1/feedback", "/api/v1/feedback",
json={ json={
"email": "engineer@resolutionflow.example.com", "email": "test@example.com",
"feedback_type": "General Feedback", "feedback_type": "General Feedback",
"message": "This is a general feedback message for testing.", "message": "This is a general feedback message for testing.",
}, },
headers={"Authorization": f"Bearer {engineer_token}"}, headers=auth_headers,
) )
assert response.status_code == 503 assert response.status_code == 503
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_submit_feedback_validation(async_client, engineer_token): async def test_submit_feedback_validation_message_too_short(client, auth_headers):
"""Test validation — message too short.""" """Test validation — message too short."""
with patch("app.api.endpoints.feedback.settings") as mock_settings: response = await client.post(
mock_settings.FEEDBACK_EMAIL = "support@test.com" "/api/v1/feedback",
json={
response = await async_client.post( "email": "test@example.com",
"/api/v1/feedback", "feedback_type": "Bug Report",
json={ "message": "short",
"email": "engineer@resolutionflow.example.com", },
"feedback_type": "Bug Report", headers=auth_headers,
"message": "short", )
},
headers={"Authorization": f"Bearer {engineer_token}"},
)
assert response.status_code == 422 assert response.status_code == 422
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_submit_feedback_invalid_type(async_client, engineer_token): async def test_submit_feedback_invalid_type(client, auth_headers):
"""Test validation — invalid feedback type.""" """Test validation — invalid feedback type."""
with patch("app.api.endpoints.feedback.settings") as mock_settings: response = await client.post(
mock_settings.FEEDBACK_EMAIL = "support@test.com" "/api/v1/feedback",
json={
response = await async_client.post( "email": "test@example.com",
"/api/v1/feedback", "feedback_type": "Invalid Type",
json={ "message": "This should fail because the type is invalid.",
"email": "engineer@resolutionflow.example.com", },
"feedback_type": "Invalid Type", headers=auth_headers,
"message": "This should fail because the type is invalid.", )
},
headers={"Authorization": f"Bearer {engineer_token}"},
)
assert response.status_code == 422 assert response.status_code == 422
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_submit_feedback_requires_auth(async_client): async def test_submit_feedback_requires_auth(client):
"""Test that unauthenticated requests are rejected.""" """Test that unauthenticated requests are rejected."""
response = await async_client.post( response = await client.post(
"/api/v1/feedback", "/api/v1/feedback",
json={ json={
"email": "anon@example.com", "email": "anon@example.com",
@@ -429,18 +674,18 @@ async def test_submit_feedback_requires_auth(async_client):
cd backend && pytest tests/test_feedback.py -v --override-ini="addopts=" cd backend && pytest tests/test_feedback.py -v --override-ini="addopts="
``` ```
Expected: All 5 tests pass. If any fail, debug and fix before proceeding. Expected: All 6 tests pass. If any fail, debug and fix before proceeding.
**Step 3: Commit** **Step 3: Commit**
```bash ```bash
git add backend/tests/test_feedback.py git add backend/tests/test_feedback.py
git commit -m "test: add feedback endpoint tests" git commit -m "test: add feedback endpoint tests including DB persistence"
``` ```
--- ---
## Task 6: Frontend API Client ## Task 7: Frontend API Client
**Files:** **Files:**
- Create: `frontend/src/api/feedback.ts` - Create: `frontend/src/api/feedback.ts`
@@ -491,27 +736,33 @@ git commit -m "feat: add feedback API client"
--- ---
## Task 7: Frontend Page ## Task 8: Frontend Page
**Files:** **Files:**
- Create: `frontend/src/pages/FeedbackPage.tsx` - Create: `frontend/src/pages/FeedbackPage.tsx`
**Step 1: Create the page component** **Step 1: Create the page component**
This version uses a custom feedback type selector with helper/description text instead of a plain `<select>`, so users can distinguish between types like "Usability Issue" vs "Bug Report."
```tsx ```tsx
import { useState } from 'react' import { useState } from 'react'
import { MessageSquareText, Send, CheckCircle2 } from 'lucide-react' import { MessageSquareText, Send, CheckCircle2, ChevronDown } from 'lucide-react'
import { useAuthStore } from '@/store/authStore' import { useAuthStore } from '@/store/authStore'
import { feedbackApi } from '@/api' import { feedbackApi } from '@/api'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
// TODO: Post-session contextual feedback prompt — after completing a troubleshooting
// session, show a subtle inline prompt like "How was this flow? Quick feedback →"
// that opens a lightweight version of this form pre-tagged with tree/session context.
const FEEDBACK_TYPES = [ const FEEDBACK_TYPES = [
'Bug Report', { value: 'Bug Report', description: 'Something is broken or not working as expected' },
'Feature Request', { value: 'Feature Request', description: "An idea for something new you'd like to see" },
'Usability Issue', { value: 'Usability Issue', description: 'Something works but is confusing or hard to use' },
'Documentation', { value: 'Documentation', description: 'Feedback on help docs, tooltips, or in-app guidance' },
'General Feedback', { value: 'General Feedback', description: 'Anything else — thoughts, impressions, suggestions' },
] as const ] as const
export function FeedbackPage() { export function FeedbackPage() {
@@ -522,9 +773,17 @@ export function FeedbackPage() {
const [message, setMessage] = useState('') const [message, setMessage] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false) const [submitted, setSubmitted] = useState(false)
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false)
const canSubmit = email.trim() && feedbackType && message.trim().length >= 10 const canSubmit = email.trim() && feedbackType && message.trim().length >= 10
const selectedType = FEEDBACK_TYPES.find(t => t.value === feedbackType)
const handleSelectType = (value: string) => {
setFeedbackType(value)
setTypeDropdownOpen(false)
}
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!canSubmit || isSubmitting) return if (!canSubmit || isSubmitting) return
@@ -573,7 +832,7 @@ export function FeedbackPage() {
<CheckCircle2 className="mx-auto h-12 w-12 text-green-500 mb-4" /> <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> <h2 className="text-xl font-semibold text-foreground mb-2">Thank you for your feedback!</h2>
<p className="text-muted-foreground mb-6"> <p className="text-muted-foreground mb-6">
We've received your submission and will review it shortly. We've received your submission and will review it shortly. Check your email for a confirmation.
</p> </p>
<button <button
onClick={handleNewFeedback} onClick={handleNewFeedback}
@@ -601,26 +860,42 @@ export function FeedbackPage() {
<p className="mt-1 text-xs text-muted-foreground">We'll reply to this address if we need more details.</p> <p className="mt-1 text-xs text-muted-foreground">We'll reply to this address if we need more details.</p>
</div> </div>
{/* Feedback Type */} {/* Feedback Type — custom selector with descriptions */}
<div> <div>
<label htmlFor="feedback-type" className="block text-sm font-medium text-foreground mb-1.5"> <label className="block text-sm font-medium text-foreground mb-1.5">
Feedback Type Feedback Type
</label> </label>
<select <div className="relative">
id="feedback-type" <button
value={feedbackType} type="button"
onChange={e => setFeedbackType(e.target.value)} onClick={() => setTypeDropdownOpen(!typeDropdownOpen)}
required className={cn(
className={cn( "w-full rounded-lg border border-border bg-card px-3 py-2 text-left flex items-center justify-between focus:border-primary focus:ring-1 focus:ring-primary/20 focus:outline-none",
"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"
feedbackType ? "text-foreground" : "text-muted-foreground" )}
>
<span>{selectedType?.value ?? 'Select a type...'}</span>
<ChevronDown size={16} className={cn("transition-transform", typeDropdownOpen && "rotate-180")} />
</button>
{typeDropdownOpen && (
<div className="absolute z-10 mt-1 w-full rounded-lg border border-border bg-card shadow-lg overflow-hidden">
{FEEDBACK_TYPES.map(type => (
<button
key={type.value}
type="button"
onClick={() => handleSelectType(type.value)}
className={cn(
"w-full text-left px-3 py-2.5 hover:bg-accent transition-colors",
feedbackType === type.value && "bg-accent"
)}
>
<div className="text-sm font-medium text-foreground">{type.value}</div>
<div className="text-xs text-muted-foreground mt-0.5">{type.description}</div>
</button>
))}
</div>
)} )}
> </div>
<option value="" disabled>Select a type...</option>
{FEEDBACK_TYPES.map(type => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div> </div>
{/* Message */} {/* Message */}
@@ -675,12 +950,12 @@ export default FeedbackPage
```bash ```bash
git add frontend/src/pages/FeedbackPage.tsx git add frontend/src/pages/FeedbackPage.tsx
git commit -m "feat: add FeedbackPage component" git commit -m "feat: add FeedbackPage with custom feedback type selector"
``` ```
--- ---
## Task 8: Router & Navigation ## Task 9: Router & Navigation
**Files:** **Files:**
- Modify: `frontend/src/router.tsx` - Modify: `frontend/src/router.tsx`
@@ -760,7 +1035,7 @@ git commit -m "feat: add feedback route, sidebar nav item, and account link card
--- ---
## Task 9: Build Verification ## Task 10: Build Verification
**Step 1: Run the frontend build** **Step 1: Run the frontend build**
@@ -776,7 +1051,7 @@ Expected: Build succeeds with no errors. TypeScript type-checking is stricter in
cd backend && pytest tests/test_feedback.py -v --override-ini="addopts=" cd backend && pytest tests/test_feedback.py -v --override-ini="addopts="
``` ```
Expected: All tests pass. Expected: All 6 tests pass.
**Step 3: Set FEEDBACK_EMAIL in .env for local testing** **Step 3: Set FEEDBACK_EMAIL in .env for local testing**
@@ -792,13 +1067,15 @@ FEEDBACK_EMAIL=your-email@example.com
3. Log in as any test user 3. Log in as any test user
4. Navigate to `/feedback` via sidebar 4. Navigate to `/feedback` via sidebar
5. Verify form loads with email pre-filled 5. Verify form loads with email pre-filled
6. Submit feedback — verify success state 6. Click the feedback type dropdown — verify descriptions appear under each option
7. Check email inbox for the formatted feedback email 7. Submit feedback — verify success state with confirmation email note
8. Verify the account settings page shows the "Send Feedback" link card 8. Check email inbox for: (a) admin notification email, (b) user confirmation email
9. Verify feedback row in DB: `docker exec -it patherly_postgres psql -U postgres -d patherly -c "SELECT * FROM feedback;"`
10. Verify the account settings page shows the "Send Feedback" link card
--- ---
## Task 10: Final Commit & Cleanup ## Task 11: Final Verification
**Step 1: Run full backend test suite** **Step 1: Run full backend test suite**