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
13 changes: 13 additions & 0 deletions backend/models/feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import mongoose from "mongoose";

const feedbackSchema = new mongoose.Schema({
userId: { type: String, default: null }, // null if anonymous
rating: { type: Number, required: true, min: 1, max: 5 },
comment: { type: String, required: true },
category: { type: String, default: "General" },
date: { type: Date, default: Date.now },
});

const Feedback = mongoose.model("Feedback", feedbackSchema);

export default Feedback;
30 changes: 30 additions & 0 deletions backend/routes/feedback.route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import express from "express";
import Feedback from "../models/feedback.js";

const router = express.Router();

// POST /api/feedback
router.post("/", async (req, res) => {
try {
const { userId, rating, comment, category } = req.body;

if (!rating || !comment) {
return res.status(400).json({ error: "Rating and comment required" });
}

const feedback = await Feedback.create({
userId,
rating,
comment,
category,
date: new Date()
});

res.status(201).json({ message: "Feedback saved", feedback });
} catch (err) {
console.error("Feedback error:", err);
res.status(500).json({ error: "Server error" });
}
});

export default router;
13 changes: 10 additions & 3 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ app.get("/", (req, res) => {
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server is up and running at http://localhost:${PORT} πŸš€`);
});
dbconnection()
.then(() => {
app.listen(PORT, () => {
console.log(`Server is up and running at http://localhost:${PORT} πŸš€`);
});
})
.catch((err) => {
console.error("❌ Failed to connect to MongoDB:", err.message);
process.exit(1); // stop server if DB fails
});
13 changes: 6 additions & 7 deletions frontend/src/Components/Navbar/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useEffect, useState } from "react";
import { Github, Home, Info, Sparkle, LogIn, UserPlus, UserCircle } from "lucide-react";
import { Github, Home, Info, Sparkle, LogIn, UserPlus, UserCircle, Phone } from "lucide-react";
import { FloatingNav } from "../ui/floating-navbar";
import { Phone } from "lucide-react";
import { Link } from "react-router-dom";
import FeedbackModal from "../ui/FeedbackModal";

const navItems = [
{
Expand Down Expand Up @@ -44,21 +44,20 @@ const Navbar = () => {
return () => window.removeEventListener("scroll", handleScroll);
}, []);

const isAuthenticated = localStorage.getItem('token') !== null;
const isAuthenticated = localStorage.getItem("token") !== null;
const userId = localStorage.getItem("userId");

return (
<div className="w-full font-sans">
{!showFloating && (
<header className="fixed top-0 left-0 right-0 z-50 bg-gradient-to-b from-[#E4ECF1]/80 to-[#D2DEE7]/80 backdrop-blur-xl border-b border-[#C5D7E5] px-6 py-4 shadow-md">
<div className="mx-auto flex max-w-7xl items-center justify-between">
{/* Logo */}
<Link to="/">
<h1 className="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-[#2E3A59] to-[#2E3A59]">
DevSync
</h1>
</Link>

{/* Desktop Navigation */}
<nav className="hidden md:flex space-x-8 items-center">
{navItems.map((item) => (
<a
Expand Down Expand Up @@ -100,7 +99,6 @@ const Navbar = () => {
</div>
</nav>

{/* Mobile Menu Button */}
<div className="md:hidden">
<button
onClick={() => setMenuOpen(!menuOpen)}
Expand All @@ -111,7 +109,6 @@ const Navbar = () => {
</div>
</div>

{/* Mobile Navigation */}
{menuOpen && (
<div className="md:hidden mt-4 flex flex-col gap-3 px-4 pb-4">
{navItems.map((item) => (
Expand Down Expand Up @@ -158,6 +155,8 @@ const Navbar = () => {
)}

{showFloating && <FloatingNav navItems={navItems} />}

<FeedbackModal userId={userId} justLoggedIn={isAuthenticated} />
</div>
);
};
Expand Down
204 changes: 204 additions & 0 deletions frontend/src/Components/ui/FeedbackModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import React, { useEffect, useState } from "react";
import { motion } from "framer-motion"; // npm i framer-motion

const STORAGE_KEY = "devsync-feedback-lastshown";

export default function FeedbackModal({ userId = null, justLoggedIn = false, daysInterval = 5 }) {
const [open, setOpen] = useState(false);
const [rating, setRating] = useState(0);
const [comment, setComment] = useState("");
const [category, setCategory] = useState("");
const [allowAnonymous, setAllowAnonymous] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);

useEffect(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
const last = raw ? new Date(raw) : null;
const now = new Date();
const msInterval = daysInterval * 24 * 60 * 60 * 1000;

if (justLoggedIn) {
if (!last || now.getTime() - last.getTime() > 60 * 1000) {
setTimeout(() => setOpen(true), 800);
}
return;
}

if (!last || now.getTime() - last.getTime() >= msInterval) {
const t = setTimeout(() => setOpen(true), 2500);
return () => clearTimeout(t);
}
} catch (e) {
console.warn("FeedbackModal: localStorage issue", e);
}
}, [justLoggedIn, daysInterval]);

const handleClose = () => {
setOpen(false);
try {
localStorage.setItem(STORAGE_KEY, new Date().toISOString());
} catch {}
};

const validate = () => {
if (rating < 1 || rating > 5) {
setError("Please give a rating between 1 and 5 stars.");
return false;
}
if (!comment || comment.trim().length < 5) {
setError("Comment should be at least 5 characters.");
return false;
}
setError(null);
return true;
};

const submit = async () => {
if (!validate()) return;
setSubmitting(true);
setError(null);
setSuccess(null);

const payload = {
userId: allowAnonymous ? null : userId,
rating,
comment: comment.trim(),
category: category || null,
date: new Date().toISOString(),
};

try {
const res = await fetch("/api/feedback/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});

if (!res.ok) throw new Error("Failed to submit");

setSuccess("Thanks! Your feedback was recorded.");
setTimeout(() => handleClose(), 900);
} catch (err) {
setError(err.message || "Error submitting feedback");
} finally {
setSubmitting(false);
}
};

const Star = ({ filled, onClick, index }) => (
<button
aria-label={`${index + 1} star`}
onClick={onClick}
className="p-1 rounded hover:scale-110 transition-transform"
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill={filled ? "currentColor" : "none"}
stroke="currentColor"
className={`w-6 h-6 ${filled ? "text-yellow-400" : "text-slate-300"}`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.2}
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.964a1 1 0 00.95.69h4.173c.969 0 1.371 1.24.588 1.81l-3.374 2.455a1 1 0 00-.364 1.118l1.287 3.963c.3.922-.755 1.688-1.538 1.118L12 17.347l-3.374 2.455c-.783.57-1.838-.196-1.538-1.118l1.287-3.963a1 1 0 00-.364-1.118L4.637 9.39c-.783-.57-.38-1.81.588-1.81h4.173a1 1 0 00.95-.69l1.286-3.964z"
/>
</svg>
</button>
);

if (!open) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm"
onClick={handleClose}
/>
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ type: "spring", stiffness: 320, damping: 28 }}
className="relative bg-white dark:bg-slate-900 rounded-2xl shadow-2xl p-6 w-full max-w-xl z-10"
>
<div className="flex justify-between">
<h3 className="text-lg font-semibold">Share your feedback</h3>
<button onClick={handleClose} className="p-2 hover:bg-slate-200 rounded-full">
βœ•
</button>
</div>

<div className="mt-4 space-y-4">
<div>
<label className="text-sm font-medium">Rating</label>
<div className="flex gap-1 mt-1">
{[0, 1, 2, 3, 4].map((i) => (
<Star key={i} index={i} filled={i < rating} onClick={() => setRating(i + 1)} />
))}
<span className="ml-2 text-sm text-slate-500">{rating ? `${rating}/5` : "No rating"}</span>
</div>
</div>

<div>
<label className="text-sm font-medium">Feedback</label>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={3}
placeholder="What did you like? What can we improve?"
className="w-full border rounded-lg p-2 text-sm bg-white dark:bg-slate-800"
/>
</div>

<div>
<label className="text-sm font-medium">Category (optional)</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="mt-1 w-full border rounded-md p-2 text-sm"
>
<option value="">Select category</option>
<option value="UI">UI</option>
<option value="Features">Features</option>
<option value="Performance">Performance</option>
<option value="Bug">Bug</option>
<option value="Suggestion">Suggestion</option>
</select>
</div>

<div className="flex items-center gap-2">
<input
type="checkbox"
checked={allowAnonymous}
onChange={(e) => setAllowAnonymous(e.target.checked)}
/>
<label className="text-sm">Submit as anonymous</label>
</div>

{error && <div className="text-red-500 text-sm">{error}</div>}
{success && <div className="text-green-600 text-sm">{success}</div>}

<div className="flex justify-end gap-3">
<button onClick={handleClose} className="px-3 py-2 bg-slate-100 rounded-md">
Close
</button>
<button
onClick={submit}
disabled={submitting}
className="px-4 py-2 bg-indigo-600 text-white rounded-md"
>
{submitting ? "Submitting..." : "Submit"}
</button>
</div>
</div>
</motion.div>
</div>
);
}