feedback tab
This commit is contained in:
75
frontend/src/components/FeedbackButton.tsx
Normal file
75
frontend/src/components/FeedbackButton.tsx
Normal 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;
|
||||
217
frontend/src/components/FeedbackModal.tsx
Normal file
217
frontend/src/components/FeedbackModal.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user