feedback tab

This commit is contained in:
jackiettran
2025-10-31 16:48:18 -04:00
parent 99aa0b3bdc
commit 16272ba373
13 changed files with 812 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ import Navbar from './components/Navbar';
import Footer from './components/Footer';
import AuthModal from './components/AuthModal';
import AlphaGate from './components/AlphaGate';
import FeedbackButton from './components/FeedbackButton';
import Home from './pages/Home';
import GoogleCallback from './pages/GoogleCallback';
import VerifyEmail from './pages/VerifyEmail';
@@ -32,7 +33,7 @@ import './App.css';
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5001';
const AppContent: React.FC = () => {
const { showAuthModal, authModalMode, closeAuthModal } = useAuth();
const { showAuthModal, authModalMode, closeAuthModal, user } = useAuth();
const [hasAlphaAccess, setHasAlphaAccess] = useState<boolean | null>(null);
const [checkingAccess, setCheckingAccess] = useState(true);
@@ -192,6 +193,9 @@ const AppContent: React.FC = () => {
onHide={closeAuthModal}
initialMode={authModalMode}
/>
{/* Show feedback button for authenticated users */}
{user && <FeedbackButton />}
</>
);
};

View File

@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import FeedbackModal from './FeedbackModal';
const FeedbackButton: React.FC = () => {
const [showPanel, setShowPanel] = useState(false);
return (
<>
<button
className="feedback-tab"
onClick={() => setShowPanel(true)}
title="Send Feedback"
aria-label="Send Feedback"
>
<div className="feedback-tab-text">FEEDBACK</div>
</button>
<FeedbackModal show={showPanel} onClose={() => setShowPanel(false)} />
<style>{`
.feedback-tab {
position: fixed;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 1000;
background-color: #0d6efd;
color: white;
border: none;
border-radius: 8px 0 0 8px;
padding: 16px 10px;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
writing-mode: vertical-rl;
text-orientation: mixed;
}
.feedback-tab:hover {
background-color: #0b5ed7;
padding-right: 14px;
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.2);
}
.feedback-tab:active {
background-color: #0a58ca;
}
.feedback-tab-text {
font-size: 14px;
font-weight: 600;
letter-spacing: 2px;
margin: 0;
transform: rotate(180deg);
}
@media (max-width: 768px) {
.feedback-tab {
padding: 12px 8px;
}
.feedback-tab-text {
font-size: 12px;
letter-spacing: 1px;
}
}
`}</style>
</>
);
};
export default FeedbackButton;

View File

@@ -0,0 +1,217 @@
import React, { useState, useEffect } from "react";
import { feedbackAPI } from "../services/api";
interface FeedbackModalProps {
show: boolean;
onClose: () => void;
}
const FeedbackModal: React.FC<FeedbackModalProps> = ({ show, onClose }) => {
const [feedbackText, setFeedbackText] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const MIN_LENGTH = 5;
const MAX_LENGTH = 5000;
const charCount = feedbackText.length;
const isValid = charCount >= MIN_LENGTH && charCount <= MAX_LENGTH;
useEffect(() => {
if (!show) {
// Reset form when modal closes
setTimeout(() => {
setFeedbackText("");
setError("");
setSuccess(false);
}, 300);
}
}, [show]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isValid) {
setError(
`Feedback must be between ${MIN_LENGTH} and ${MAX_LENGTH} characters`
);
return;
}
setLoading(true);
setError("");
try {
// Capture the current URL
const currentUrl = window.location.href;
await feedbackAPI.submitFeedback({
feedbackText: feedbackText.trim(),
url: currentUrl,
});
setSuccess(true);
setLoading(false);
} catch (err: any) {
setError(
err.response?.data?.error ||
"Failed to submit feedback. Please try again."
);
setLoading(false);
}
};
if (!show) return null;
return (
<>
<div className={`feedback-panel ${show ? "show" : ""}`}>
<div className="feedback-panel-content">
<div className="modal-header">
<h5 className="modal-title">Share Feedback</h5>
<button
type="button"
className="btn-close"
onClick={onClose}
disabled={loading}
></button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
{success ? (
<div className="alert alert-success" role="alert">
<h6 className="alert-heading">Thank you!</h6>
<p className="mb-0">
Your feedback has been submitted successfully! We appreciate
you making Community Rentals better!
</p>
</div>
) : (
<>
<p className="text-muted mb-3">
Share your thoughts, report bugs, or suggest improvements.
Your feedback helps us make RentAll better for everyone!
</p>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
<div className="mb-3">
<textarea
id="feedbackText"
className="form-control"
rows={6}
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
disabled={loading}
maxLength={MAX_LENGTH}
required
/>
{charCount > 0 && charCount < MIN_LENGTH && (
<div
className="text-danger mt-2"
style={{ fontSize: "0.875rem" }}
>
Minimum {MIN_LENGTH} characters required
</div>
)}
</div>
</>
)}
</div>
{!success && (
<div className="modal-footer" style={{ gap: "10px" }}>
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
disabled={loading || !isValid}
>
{loading ? (
<>
<span
className="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"
></span>
Sending...
</>
) : (
"Share"
)}
</button>
</div>
)}
</form>
</div>
</div>
<style>{`
.feedback-panel {
position: fixed;
top: 50%;
right: -400px;
transform: translateY(-50%);
height: auto;
max-height: calc(100vh - 40px);
width: 400px;
background-color: white;
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.2);
border-radius: 8px 0 0 8px;
z-index: 1050;
transition: right 0.3s ease;
overflow-y: auto;
}
.feedback-panel.show {
right: 0;
}
.feedback-panel-content {
height: 100%;
display: flex;
flex-direction: column;
}
.feedback-panel .modal-header {
border-bottom: none;
padding: 1rem 1.5rem 0.5rem 1.5rem;
flex-shrink: 0;
}
.feedback-panel .modal-body {
flex: 1;
padding: 0.5rem 1.5rem 1.5rem 1.5rem;
overflow-y: auto;
}
.feedback-panel .modal-footer {
border-top: none;
padding: 0 1.5rem 1rem 1.5rem;
flex-shrink: 0;
}
@media (max-width: 576px) {
.feedback-panel {
width: 100%;
right: -100%;
}
}
`}</style>
</>
);
};
export default FeedbackModal;

View File

@@ -293,4 +293,9 @@ export const conditionCheckAPI = {
getAvailableChecks: () => api.get("/condition-checks"),
};
export const feedbackAPI = {
submitFeedback: (data: { feedbackText: string; url?: string }) =>
api.post("/feedback", data),
};
export default api;

View File

@@ -293,3 +293,13 @@ export interface RefundPreview {
reason: string;
totalAmount: number;
}
export interface Feedback {
id: string;
userId: string;
feedbackText: string;
userAgent?: string;
url?: string;
createdAt: string;
updatedAt: string;
}