From 937a793a4fd3add9abc3f1ec881a9165ee3edf3c Mon Sep 17 00:00:00 2001 From: Pratik Date: Sun, 11 Jan 2026 18:06:47 +0530 Subject: [PATCH] feat: Implement Feedback system and Back to Top button --- backend/controllers/feedbackController.js | 31 ++++++ backend/models/Feedback.js | 30 ++++++ backend/routes/feedbackRoutes.js | 8 ++ backend/server.js | 1 + frontend/src/App.jsx | 2 + frontend/src/components/BackToTop.jsx | 44 +++++++++ frontend/src/components/Layout.jsx | 23 +++-- frontend/src/pages/FeedbackPage.jsx | 114 ++++++++++++++++++++++ 8 files changed, 244 insertions(+), 9 deletions(-) create mode 100644 backend/controllers/feedbackController.js create mode 100644 backend/models/Feedback.js create mode 100644 backend/routes/feedbackRoutes.js create mode 100644 frontend/src/components/BackToTop.jsx create mode 100644 frontend/src/pages/FeedbackPage.jsx diff --git a/backend/controllers/feedbackController.js b/backend/controllers/feedbackController.js new file mode 100644 index 0000000..a91f464 --- /dev/null +++ b/backend/controllers/feedbackController.js @@ -0,0 +1,31 @@ +const Feedback = require('../models/Feedback'); + +// @desc Submit feedback +// @route POST /api/feedback +// @access Private +const createFeedback = async (req, res) => { + try { + const { type, message } = req.body; + + if (!type || !message) { + return res.status(400).json({ message: 'Type and message are required' }); + } + + const feedback = new Feedback({ + user: req.user._id, + type, + message, + }); + + const savedFeedback = await feedback.save(); + + res.status(201).json(savedFeedback); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Server Error' }); + } +}; + +module.exports = { + createFeedback, +}; diff --git a/backend/models/Feedback.js b/backend/models/Feedback.js new file mode 100644 index 0000000..a4e071c --- /dev/null +++ b/backend/models/Feedback.js @@ -0,0 +1,30 @@ +const mongoose = require('mongoose'); + +const feedbackSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, + }, + type: { + type: String, + enum: ['bug', 'feature', 'thought'], + required: true, + }, + message: { + type: String, + required: true, + trim: true, + }, + status: { + type: String, + enum: ['pending', 'reviewed', 'resolved'], + default: 'pending', + }, +}, { + timestamps: true, +}); + +const Feedback = mongoose.model('Feedback', feedbackSchema); + +module.exports = Feedback; diff --git a/backend/routes/feedbackRoutes.js b/backend/routes/feedbackRoutes.js new file mode 100644 index 0000000..71adeed --- /dev/null +++ b/backend/routes/feedbackRoutes.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const { createFeedback } = require('../controllers/feedbackController'); +const { protect } = require('../middleware/authMiddleware'); + +router.post('/', protect, createFeedback); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index b083d9c..e373b58 100644 --- a/backend/server.js +++ b/backend/server.js @@ -47,6 +47,7 @@ app.use('/api/receipts', require('./routes/receiptRoutes')); app.use('/api/users', require('./routes/userRoutes')); app.use('/api/budgets', require('./routes/budgetRoutes')); app.use('/api/recurring', require('./routes/recurringTransactionRoutes')); +app.use('/api/feedback', require('./routes/feedbackRoutes')); // Serve static files from the uploads directory app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c45c3fc..aab5f92 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -16,6 +16,7 @@ import Layout from './components/Layout'; import ProtectedRoute from './components/ProtectedRoute'; import SetupProtectedRoute from './components/SetupProtectedRoute'; import RecurringTransactions from './pages/RecurringTransactions'; +import FeedbackPage from './pages/FeedbackPage'; function App() { return ( @@ -52,6 +53,7 @@ function App() { path="/recurring-transactions" element={} /> + } /> } /> diff --git a/frontend/src/components/BackToTop.jsx b/frontend/src/components/BackToTop.jsx new file mode 100644 index 0000000..97b5d83 --- /dev/null +++ b/frontend/src/components/BackToTop.jsx @@ -0,0 +1,44 @@ +import React, { useState, useEffect } from 'react'; +import { ArrowUp } from 'lucide-react'; + +const BackToTop = () => { + const [isVisible, setIsVisible] = useState(false); + + const toggleVisibility = () => { + if (window.scrollY > 300) { + setIsVisible(true); + } else { + setIsVisible(false); + } + }; + + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }; + + useEffect(() => { + window.addEventListener('scroll', toggleVisibility); + return () => { + window.removeEventListener('scroll', toggleVisibility); + }; + }, []); + + return ( + <> + {isVisible && ( + + )} + + ); +}; + +export default BackToTop; diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index eec812d..29f4f85 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -3,6 +3,7 @@ import { Outlet, NavLink, useNavigate } from 'react-router-dom'; // 1. Import Li import useAuth from '../hooks/useAuth'; import CurrencySelector from './CurrencySelector'; import ThemeToggle from './ThemeToggle'; +import BackToTop from './BackToTop'; const Layout = () => { const { logout } = useAuth(); @@ -18,7 +19,7 @@ const Layout = () => { }; const handleClick = (e) => { - navigate("/"); + navigate("/"); }; @@ -29,13 +30,13 @@ const Layout = () => {
{/* 2. Wrap the span in a Link to the dashboard */} - - Paisable - + + Paisable +
@@ -60,6 +61,9 @@ const Layout = () => { > Recurring Transactions + + Feedback +
@@ -82,7 +86,8 @@ const Layout = () => {
- + + ); }; diff --git a/frontend/src/pages/FeedbackPage.jsx b/frontend/src/pages/FeedbackPage.jsx new file mode 100644 index 0000000..d3d64e4 --- /dev/null +++ b/frontend/src/pages/FeedbackPage.jsx @@ -0,0 +1,114 @@ +import React, { useState } from 'react'; +import api from '../api/axios'; +import { toast } from 'react-toastify'; +import { MessageSquare, Bug, Lightbulb, Send } from 'lucide-react'; + +const FeedbackPage = () => { + const [formData, setFormData] = useState({ + type: 'thought', + message: '', + }); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!formData.message.trim()) { + toast.error('Please enter a message'); + return; + } + + setLoading(true); + try { + await api.post('/feedback', formData); + toast.success('Thank you for your feedback!'); + setFormData({ type: 'thought', message: '' }); + } catch (error) { + console.error('Error submitting feedback:', error); + toast.error(error.response?.data?.message || 'Failed to submit feedback'); + } finally { + setLoading(false); + } + }; + + const options = [ + { id: 'thought', label: 'Share Thoughts', icon: MessageSquare, color: 'text-blue-500', bg: 'bg-blue-100', border: 'border-blue-200' }, + { id: 'bug', label: 'Report Bug', icon: Bug, color: 'text-red-500', bg: 'bg-red-100', border: 'border-red-200' }, + { id: 'feature', label: 'Suggest Feature', icon: Lightbulb, color: 'text-yellow-500', bg: 'bg-yellow-100', border: 'border-yellow-200' }, + ]; + + return ( +
+
+
+
+

+ We Value Your Feedback +

+

+ Help us improve Paisable. Whether it's a bug, a feature request, or just some thoughts, we'd love to hear from you. +

+
+ +
+
+ {options.map((option) => ( +
setFormData({ ...formData, type: option.id })} + className={`cursor-pointer relative p-4 rounded-xl border-2 transition-all duration-300 transform hover:scale-105 ${formData.type === option.id + ? `${option.border} ${option.bg} ring-2 ring-offset-2 ring-indigo-500 dark:ring-offset-gray-800` + : 'border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-750 hover:bg-gray-100 dark:hover:bg-gray-700' + }`} + > +
+ + + {option.label} + +
+ {formData.type === option.id && ( +
+ )} +
+ ))} +
+ +
+ +