Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions backend/controllers/feedbackController.js
Original file line number Diff line number Diff line change
@@ -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,
};
30 changes: 30 additions & 0 deletions backend/models/Feedback.js
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 8 additions & 0 deletions backend/routes/feedbackRoutes.js
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')));
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -52,6 +53,7 @@ function App() {
path="/recurring-transactions"
element={<RecurringTransactions />}
/>
<Route path="/feedback" element={<FeedbackPage />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/components/BackToTop.jsx
Original file line number Diff line number Diff line change
@@ -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 && (
<button
onClick={scrollToTop}
className="fixed bottom-8 right-8 z-50 p-3 rounded-full bg-blue-600 text-white shadow-lg transition-all duration-300 hover:bg-blue-700 hover:scale-110 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 animate-bounce-subtle"
title="Back to Top"
>
<ArrowUp size={24} />
</button>
)}
</>
);
};

export default BackToTop;
23 changes: 14 additions & 9 deletions frontend/src/components/Layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -18,7 +19,7 @@ const Layout = () => {
};

const handleClick = (e) => {
navigate("/");
navigate("/");
};


Expand All @@ -29,13 +30,13 @@ const Layout = () => {
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
{/* 2. Wrap the span in a Link to the dashboard */}
<span
onClick={handleClick}
className="font-bold text-xl text-blue-600 dark:text-blue-400 cursor-pointer transition-all duration-500 hover:scale-105 hover:drop-shadow-lg hover:text-blue-500 dark:hover:text-blue-300"
title="Go to home"
>
Paisable
</span>
<span
onClick={handleClick}
className="font-bold text-xl text-blue-600 dark:text-blue-400 cursor-pointer transition-all duration-500 hover:scale-105 hover:drop-shadow-lg hover:text-blue-500 dark:hover:text-blue-300"
title="Go to home"
>
Paisable
</span>

<div className="hidden lg:block">
<div className="ml-10 flex items-baseline space-x-4">
Expand All @@ -60,6 +61,9 @@ const Layout = () => {
>
Recurring Transactions
</NavLink>
<NavLink to="/feedback" className={getNavLinkClass}>
Feedback
</NavLink>
</div>
</div>
</div>
Expand All @@ -82,7 +86,8 @@ const Layout = () => {
<Outlet />
</div>
</main>
</div>
<BackToTop />
</div >
);
};

Expand Down
114 changes: 114 additions & 0 deletions frontend/src/pages/FeedbackPage.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl overflow-hidden">
<div className="p-8 md:p-12">
<div className="text-center mb-10">
<h1 className="text-3xl md:text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600 dark:from-blue-400 dark:to-indigo-400 mb-4">
We Value Your Feedback
</h1>
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
Help us improve Paisable. Whether it's a bug, a feature request, or just some thoughts, we'd love to hear from you.
</p>
</div>

<form onSubmit={handleSubmit} className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{options.map((option) => (
<div
key={option.id}
onClick={() => 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'
}`}
>
<div className="flex flex-col items-center space-y-3">
<option.icon className={`w-8 h-8 ${option.color}`} />
<span className={`font-semibold ${formData.type === option.id ? 'text-gray-900 dark:text-gray-900' : 'text-gray-600 dark:text-gray-300'}`}>
{option.label}
</span>
</div>
{formData.type === option.id && (
<div className="absolute top-2 right-2 w-3 h-3 bg-indigo-500 rounded-full animate-pulse" />
)}
</div>
))}
</div>

<div className="relative">
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Your Message
</label>
<textarea
id="message"
rows={6}
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300 resize-none shadow-sm"
placeholder="Tell us what's on your mind..."
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
/>
</div>

<div className="flex justify-end">
<button
type="submit"
disabled={loading}
className={`flex items-center space-x-2 px-8 py-3 rounded-xl bg-gradient-to-r from-blue-600 to-indigo-600 text-white font-bold shadow-lg hover:shadow-indigo-500/30 transform hover:-translate-y-1 transition-all duration-300 ${loading ? 'opacity-70 cursor-not-allowed' : ''
}`}
>
{loading ? (
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<>
<span>Submit Feedback</span>
<Send size={20} />
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
);
};

export default FeedbackPage;