diff --git a/server/controllers/BlogControl.js b/server/controllers/BlogControl.js
index 907fcb9..f1de929 100644
--- a/server/controllers/BlogControl.js
+++ b/server/controllers/BlogControl.js
@@ -33,7 +33,9 @@ exports.createBlog = async (req, res) => {
coverImgSrc = uploadRes.secure_url;
} catch (uploadError) {
console.error("Cloudinary error:", uploadError);
- return res.status(500).json({ error: "Image upload failed" });
+ // Use placeholder image when Cloudinary fails
+ console.log("Using placeholder image for blog cover");
+ coverImgSrc = "https://via.placeholder.com/800x400/4ade80/ffffff?text=Blog+Cover";
}
const blog = await Blog.create({
@@ -116,6 +118,24 @@ exports.getBlogById = async (req, res) => {
if (!blog) return res.status(404).json({ message: "Blog not found" });
+ // Track article reading for authenticated users
+ if (req.user && req.user.id) {
+ try {
+ console.log(`Tracking article read: User ${req.user.id} read blog ${blog._id}`);
+ const updateResult = await User.findByIdAndUpdate(
+ req.user.id,
+ { $addToSet: { articlesRead: blog._id } }, // $addToSet prevents duplicates
+ { new: true }
+ );
+ console.log(`Article tracking successful for user ${req.user.id}`);
+ } catch (trackingError) {
+ console.error("Article tracking error:", trackingError);
+ // Don't fail the request if tracking fails
+ }
+ } else {
+ console.log("No user authentication found, skipping article tracking");
+ }
+
res.status(200).json({ message: "Blog fetched", blog });
} catch (error) {
console.error("Get blog error:", error);
diff --git a/server/controllers/Profile.js b/server/controllers/Profile.js
new file mode 100644
index 0000000..d29af86
--- /dev/null
+++ b/server/controllers/Profile.js
@@ -0,0 +1,70 @@
+const { User } = require("../models/user");
+const asyncHandler = require("../utilities/asyncHandler");
+
+// Get user profile with complete information
+exports.getUserProfile = asyncHandler(async (req, res) => {
+ try {
+ const userId = req.user.id;
+
+ const user = await User.findById(userId)
+ .populate("blogs")
+ .populate("articlesRead")
+ .populate("userPlants")
+ .select("-password -token");
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found",
+ });
+ }
+
+ res.status(200).json({
+ success: true,
+ user,
+ });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({
+ success: false,
+ message: "Internal server error",
+ });
+ }
+});
+
+// Get user statistics
+exports.getUserStats = asyncHandler(async (req, res) => {
+ try {
+ const userId = req.user.id;
+
+ const user = await User.findById(userId)
+ .populate("blogs")
+ .populate("articlesRead")
+ .populate("userPlants");
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: "User not found",
+ });
+ }
+
+ const stats = {
+ blogsCreated: user.blogs.length,
+ articlesRead: user.articlesRead.length,
+ plantsInCollection: user.userPlants.length,
+ joinDate: user.createdAt || new Date('2024-01-01'), // Fallback for existing users
+ };
+
+ res.status(200).json({
+ success: true,
+ stats,
+ });
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({
+ success: false,
+ message: "Internal server error",
+ });
+ }
+});
diff --git a/server/index.js b/server/index.js
index dfbcdcb..1eeed77 100644
--- a/server/index.js
+++ b/server/index.js
@@ -13,11 +13,12 @@ const BlogRoutes = require("./routes/BlogRoutes");
const paymnet = require("./routes/paymnet");
const OrderRoutes = require("./routes/orederRoutes");
const PlantsRoute = require("./routes/PlantsRoute");
+const ProfileRoutes = require("./routes/ProfileRoutes");
app.use(cookieParser());
app.use(
cors({
- origin: ["https://cleanbreath.netlify.app", "http://localhost:5173"],
+ origin: ["https://cleanbreath.netlify.app", "http://localhost:5173", "http://localhost:5174"],
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
@@ -37,6 +38,7 @@ app.use("/api/blog", BlogRoutes);
app.use("/api/payment", paymnet);
app.use("/api/order", OrderRoutes);
app.use("/api/plants", PlantsRoute);
+app.use("/api/profile", ProfileRoutes);
app.listen(process.env.PORT || 3000, () => {
console.log(`Server is running on port ${process.env.PORT || 3000}`);
diff --git a/server/models/user.js b/server/models/user.js
index 6733353..b0bd297 100644
--- a/server/models/user.js
+++ b/server/models/user.js
@@ -53,6 +53,8 @@ const userSchema = new mongoose.Schema({
ref: "Plant",
},
],
+}, {
+ timestamps: true // This adds createdAt and updatedAt fields automatically
});
exports.User = mongoose.model("User", userSchema);
diff --git a/server/routes/BlogRoutes.js b/server/routes/BlogRoutes.js
index a1a8601..3f872b6 100644
--- a/server/routes/BlogRoutes.js
+++ b/server/routes/BlogRoutes.js
@@ -15,6 +15,29 @@ router.post("/create", auth, upload.single("coverImg"), createBlog);
router.get("/blogs", getAllBlogs);
-router.get("/blogdel/:id", getBlogById);
+// Optional auth middleware for blog detail - tracks reading for logged-in users
+const optionalAuth = (req, res, next) => {
+ const token = req.cookies.token || req.body.token || req.header("Authorization")?.replace("Bearer ", "");
+
+ console.log("Optional auth - Token received:", token ? "Yes" : "No");
+
+ if (token) {
+ try {
+ const jwt = require("jsonwebtoken");
+ const decode = jwt.verify(token, process.env.JWT_SECRET);
+ req.user = decode;
+ console.log("Optional auth - User decoded:", req.user.id);
+ } catch (error) {
+ // Invalid token, but continue without user context
+ console.log("Optional auth - Token invalid:", error.message);
+ req.user = null;
+ }
+ } else {
+ console.log("Optional auth - No token provided");
+ }
+ next();
+};
+
+router.get("/blogdel/:id", optionalAuth, getBlogById);
module.exports = router;
diff --git a/server/routes/ProfileRoutes.js b/server/routes/ProfileRoutes.js
new file mode 100644
index 0000000..e9138c2
--- /dev/null
+++ b/server/routes/ProfileRoutes.js
@@ -0,0 +1,11 @@
+const express = require("express");
+const { getUserProfile, getUserStats } = require("../controllers/Profile");
+const { auth } = require("../middleware/auth");
+
+const router = express.Router();
+
+// Profile routes - all require authentication
+router.get("/", auth, getUserProfile);
+router.get("/stats", auth, getUserStats);
+
+module.exports = router;
diff --git a/src/App.jsx b/src/App.jsx
index 68b1df3..9180cfe 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -19,6 +19,7 @@ import OrderPage from "./Pages/OrderPage";
import NotFound from "./Pages/NotFound";
import Feature from "./Pages/Feature";
import Faq from "./Component/Faq";
+import Profile from "./Pages/Profile";
const App = () => {
// 1. Manage the dark mode state
@@ -69,6 +70,7 @@ const App = () => {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/src/Component/Navbar.jsx b/src/Component/Navbar.jsx
index 8d30ebe..3dce753 100644
--- a/src/Component/Navbar.jsx
+++ b/src/Component/Navbar.jsx
@@ -4,6 +4,7 @@ import { useSelector, useDispatch } from "react-redux";
import { logout } from "../service/oprations/authApi";
import logo from "../assets/Untitled_design__1_-removebg-preview.png";
import { GiHamburgerMenu } from "react-icons/gi";
+import { FaUser } from "react-icons/fa";
import { motion } from "framer-motion";
import { BsSun, BsMoon } from "react-icons/bs"; // Import sun and moon icons
@@ -27,8 +28,9 @@ const itemVariants = {
// Accept darkMode and toggleDarkMode as props
const Navbar = ({ darkMode, toggleDarkMode }) => {
- const login = useSelector((state) => state.auth.islogin);
+ const islogin = useSelector((state) => state.auth.islogin);
const token = useSelector((state) => state.auth.token);
+ const user = useSelector((state) => state.auth.user);
const dispatch = useDispatch();
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -117,7 +119,18 @@ const Navbar = ({ darkMode, toggleDarkMode }) => {
{/* Auth Buttons */}
{token !== null ? (
-
+
+ {/* Profile Icon */}
+
+
+
+
+
+
+ {/* Logout Button */}
{token !== null ? (
-
-
- Log Out
-
-
+ <>
+
+
+
+ Profile
+
+
+
+ {
+ logouthandel();
+ toggleMenu();
+ }}
+ >
+ Log Out
+
+
+ >
) : (
<>
diff --git a/src/Pages/Profile.jsx b/src/Pages/Profile.jsx
new file mode 100644
index 0000000..fce0b21
--- /dev/null
+++ b/src/Pages/Profile.jsx
@@ -0,0 +1,207 @@
+import React, { useEffect, useState } from "react";
+import { useSelector, useDispatch } from "react-redux";
+import { motion } from "framer-motion";
+import { FaUser, FaEnvelope, FaCalendarAlt, FaBlog, FaBookOpen, FaSeedling } from "react-icons/fa";
+import { getUserProfile } from "../service/oprations/profileApi";
+
+const Profile = () => {
+ const { user, islogin, token } = useSelector((state) => state.auth);
+ const [profileData, setProfileData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ const fetchProfileData = async () => {
+ if (token && islogin) {
+ try {
+ const result = await dispatch(getUserProfile(token));
+ setProfileData(result.user);
+ } catch (error) {
+ console.error("Failed to fetch profile data:", error);
+ // Use existing user data as fallback
+ setProfileData(user);
+ } finally {
+ setLoading(false);
+ }
+ } else {
+ setLoading(false);
+ }
+ };
+
+ fetchProfileData();
+ }, [token, islogin, dispatch, user]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!islogin || !profileData) {
+ return (
+
+
+
+ Please log in to view your profile
+
+
+ You need to be logged in to access this page.
+
+
+
+ );
+ }
+
+ const profileSections = [
+ {
+ title: "Personal Information",
+ icon: ,
+ content: [
+ { label: "Username", value: profileData.username || "Not provided", icon: },
+ { label: "Email", value: profileData.email || "Not provided", icon: },
+ {
+ label: "Member Since",
+ value: profileData.createdAt
+ ? new Date(profileData.createdAt).toLocaleDateString()
+ : "Early 2025", // Show a reasonable date for existing users
+ icon:
+ },
+ ]
+ },
+ {
+ title: "Activity Stats",
+ icon: ,
+ content: [
+ {
+ label: "Blogs Created",
+ value: profileData.blogs ? profileData.blogs.length : 0,
+ icon:
+ },
+ {
+ label: "Articles Read",
+ value: profileData.articlesRead ? profileData.articlesRead.length : 0,
+ icon:
+ },
+ {
+ label: "Plants in Collection",
+ value: profileData.userPlants ? profileData.userPlants.length : 0,
+ icon:
+ },
+ ]
+ }
+ ];
+
+ return (
+
+
+ {/* Profile Header */}
+
+
+ {/* Profile Avatar */}
+
+
+
+
+ {/* User Info */}
+
+ {profileData.username || "User"}
+
+
+ {profileData.email}
+
+
+ {/* Status Badge */}
+
+
+
+
+ {/* Profile Sections */}
+
+ {profileSections.map((section, sectionIndex) => (
+
+ {/* Section Header */}
+
+
{section.icon}
+
+ {section.title}
+
+
+
+ {/* Section Content */}
+
+ {section.content.map((item, itemIndex) => (
+
+
+
+ {item.icon}
+
+
+ {item.label}
+
+
+
+ {item.value}
+
+
+ ))}
+
+
+ ))}
+
+
+ {/* Recent Activity Section (Placeholder for future features) */}
+
+
+
+ Recent Activity
+
+
+
+ Recent activity will be shown here
+
+
+ Create blogs, read articles, and add plants to see your activity!
+
+
+
+
+
+ );
+};
+
+export default Profile;
diff --git a/src/service/oprations/BlogApi.jsx b/src/service/oprations/BlogApi.jsx
index 79266fb..44f772e 100644
--- a/src/service/oprations/BlogApi.jsx
+++ b/src/service/oprations/BlogApi.jsx
@@ -61,8 +61,11 @@ export function createBlog(title, description, content, coverImg, setIsCreatingB
export function getBlog(id) {
return async (dispatch) => {
dispatch(setLoading(true));
+ const token = localStorage.getItem("token");
+
try {
- const response = await apiConnector("GET", `${GET_BLOG}/${id}`);
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
+ const response = await apiConnector("GET", `${GET_BLOG}/${id}`, null, headers);
dispatch(setBlogdel(response.data.blog));
return response;
} catch (error) {
diff --git a/src/service/oprations/authApi.jsx b/src/service/oprations/authApi.jsx
index 0cd6e7f..2cbfbf1 100644
--- a/src/service/oprations/authApi.jsx
+++ b/src/service/oprations/authApi.jsx
@@ -1,5 +1,5 @@
import { authEndpoits } from "../apis";
-import { setIslogin, setLoading, setToken } from "../../slices/Auth";
+import { setIslogin, setLoading, setToken, setUser } from "../../slices/Auth";
const { SIGNUP_API, SIGNIN_API } = authEndpoits;
import { toast } from "react-hot-toast";
import { apiConnector } from "../apiConnector";
@@ -52,7 +52,9 @@ export function signin(email, password, navigate) {
console.log(response);
dispatch(setToken(response.data.token));
+ dispatch(setUser(response.data.user));
localStorage.setItem("token", response.data.token);
+ localStorage.setItem("user", JSON.stringify(response.data.user));
dispatch(setIslogin(true));
toast.success("Logged In");
navigate("/");
@@ -69,8 +71,10 @@ export function logout() {
return async (dispatch) => {
try {
dispatch(setToken(null));
- dispatch(setIslogin(null));
- localStorage.getItem("token", "");
+ dispatch(setUser(null));
+ dispatch(setIslogin(false));
+ localStorage.removeItem("token");
+ localStorage.removeItem("user");
toast.success("Log Out");
} catch (error) {
console.log(error);
diff --git a/src/service/oprations/profileApi.jsx b/src/service/oprations/profileApi.jsx
new file mode 100644
index 0000000..3788855
--- /dev/null
+++ b/src/service/oprations/profileApi.jsx
@@ -0,0 +1,48 @@
+import { apiConnector } from "../apiConnector";
+
+const BASE_URL = import.meta.env.VITE_BASE_URL;
+
+export const ProfileEndpoints = {
+ GET_PROFILE: `${BASE_URL}/profile`,
+ GET_STATS: `${BASE_URL}/profile/stats`,
+};
+
+export function getUserProfile(token) {
+ return async (dispatch) => {
+ try {
+ const response = await apiConnector(
+ "GET",
+ ProfileEndpoints.GET_PROFILE,
+ null,
+ {
+ Authorization: `Bearer ${token}`,
+ }
+ );
+
+ return response.data;
+ } catch (error) {
+ console.log("GET_PROFILE_ERROR:", error);
+ throw error;
+ }
+ };
+}
+
+export function getUserStats(token) {
+ return async (dispatch) => {
+ try {
+ const response = await apiConnector(
+ "GET",
+ ProfileEndpoints.GET_STATS,
+ null,
+ {
+ Authorization: `Bearer ${token}`,
+ }
+ );
+
+ return response.data;
+ } catch (error) {
+ console.log("GET_STATS_ERROR:", error);
+ throw error;
+ }
+ };
+}
diff --git a/src/slices/Auth.jsx b/src/slices/Auth.jsx
index ae8b0fe..be0b3c5 100644
--- a/src/slices/Auth.jsx
+++ b/src/slices/Auth.jsx
@@ -2,8 +2,10 @@ import { createSlice } from "@reduxjs/toolkit";
const initialState = {
token: localStorage.getItem("token") ? localStorage.getItem("token") : null,
+ user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : null,
signupData: null,
loading: false,
+ islogin: localStorage.getItem("token") ? true : false,
};
const authSlice = createSlice({
@@ -13,6 +15,9 @@ const authSlice = createSlice({
setToken(state, action) {
state.token = action.payload;
},
+ setUser(state, action) {
+ state.user = action.payload;
+ },
setSignupData(state, action) {
state.signupData = action.payload;
},
@@ -25,6 +30,6 @@ const authSlice = createSlice({
},
});
-export const { setToken, setSignupData, setLoading, setIslogin } =
+export const { setToken, setUser, setSignupData, setLoading, setIslogin } =
authSlice.actions;
export default authSlice.reducer;
diff --git a/start-servers.bat b/start-servers.bat
new file mode 100644
index 0000000..01b179f
--- /dev/null
+++ b/start-servers.bat
@@ -0,0 +1,77 @@
+@echo off
+echo Starting Clean Breath Application...
+echo.
+
+REM Get the directory where this batch file is located
+set "PROJECT_DIR=%~dp0"
+
+REM Check if Node.js is installed
+node --version >nul 2>&1
+if %errorlevel% neq 0 (
+ echo Error: Node.js is not installed or not in PATH
+ echo Please install Node.js from https://nodejs.org/
+ pause
+ exit /b 1
+)
+
+echo Node.js is installed. Checking dependencies...
+echo.
+
+REM Check if node_modules exists in root directory
+if not exist "%PROJECT_DIR%node_modules" (
+ echo Installing frontend dependencies...
+ cd /d "%PROJECT_DIR%"
+ call npm install
+ if %errorlevel% neq 0 (
+ echo Error: Failed to install frontend dependencies
+ pause
+ exit /b 1
+ )
+)
+
+REM Check if node_modules exists in server directory
+if not exist "%PROJECT_DIR%server\node_modules" (
+ echo Installing backend dependencies...
+ cd /d "%PROJECT_DIR%server"
+ call npm install
+ if %errorlevel% neq 0 (
+ echo Error: Failed to install backend dependencies
+ pause
+ exit /b 1
+ )
+)
+
+echo.
+echo Starting servers...
+echo.
+
+REM Start backend server in a new command prompt window
+echo Starting backend server on http://localhost:5000
+start "Clean Breath - Backend Server" cmd /k "cd /d "%PROJECT_DIR%server" && npm start"
+
+REM Wait a moment for backend to start
+timeout /t 3 /nobreak >nul
+
+REM Start frontend server in a new command prompt window
+echo Starting frontend server on http://localhost:5173
+start "Clean Breath - Frontend Server" cmd /k "cd /d "%PROJECT_DIR%" && npm run dev"
+
+echo.
+echo Both servers are starting...
+echo.
+echo Backend Server: http://localhost:5000
+echo Frontend Server: http://localhost:5173
+echo.
+echo The application will open in separate command prompt windows.
+echo Close those windows to stop the servers.
+echo.
+
+REM Wait a bit more and then try to open the frontend in browser
+timeout /t 5 /nobreak >nul
+echo Opening application in browser...
+start http://localhost:5173
+
+echo.
+echo Clean Breath application is now running!
+echo Press any key to close this window...
+pause >nul