From ed1500d86c816e992e5b8a7a901c6cc71250d365 Mon Sep 17 00:00:00 2001 From: amanlakhera16 Date: Tue, 7 Apr 2026 22:25:01 +0530 Subject: [PATCH 1/2] feat: Enhance UserBlogs component with improved layout and loading state - Refactored UserBlogs.js to include a more structured layout with an introduction section. - Added loading state handling while fetching user blogs. - Updated blog rendering to use a grid layout for better responsiveness. - Introduced a handleEdit function for navigating to the blog edit page. - Improved error handling in API requests. style: Update global styles and typography - Imported new fonts from Google Fonts for a modern look. - Defined CSS variables for consistent theming across the application. - Enhanced body styles for better visual appeal and responsiveness. fix: Update API response handling in blog and user controllers - Modified blog-controller.js to populate user data in blog responses. - Updated user-contoller.js to exclude password from user responses. - Improved error handling and response structure for better client-side integration. chore: Add project reports for frontend and backend - Created detailed reports outlining project structure, setup instructions, and core functionalities for both frontend and backend. build: Update server configuration for larger payloads - Increased JSON and URL-encoded body size limits in server.js to support larger blog content. test: Add environment variable configuration for MongoDB connection - Introduced .env file for MongoDB URI configuration to streamline local development setup. --- client/reportReadme.md | 301 +++++++++++++++++++ client/src/App.css | 309 ++++++++++++++++++++ client/src/App.js | 19 +- client/src/componets/AddBlogs.js | 397 +++++++++++++++++++------ client/src/componets/Blog.js | 194 +++++++++++-- client/src/componets/BlogDetail.js | 68 +++-- client/src/componets/Blogs.js | 78 +++-- client/src/componets/DeleteBlogs.js | 19 +- client/src/componets/Header.js | 223 ++++++++++---- client/src/componets/Login.js | 110 +++++-- client/src/componets/UserBlogs.js | 165 ++++++++--- client/src/componets/utils.js | 10 +- client/src/index.css | 199 ++++++++++++- client/src/index.js | 5 +- server/.env | 1 + server/controller/blog-controller.js | 16 +- server/controller/user-contoller.js | 12 +- server/reportReadme.md | 415 +++++++++++++++++++++++++++ server/routes/blog-routes.js | 12 +- server/server.js | 3 +- server/utils/ApiError.js | 2 +- server/utils/ApiResponse.js | 2 +- server/yarn.lock | 5 + 23 files changed, 2250 insertions(+), 315 deletions(-) create mode 100644 client/reportReadme.md create mode 100644 server/.env create mode 100644 server/reportReadme.md diff --git a/client/reportReadme.md b/client/reportReadme.md new file mode 100644 index 000000000..87f0d7132 --- /dev/null +++ b/client/reportReadme.md @@ -0,0 +1,301 @@ +# Frontend Project Report + +## 1. Project Overview + +The frontend of this Blog App is a React-based single-page application built to provide a clean, responsive, and modern interface for creating, viewing, updating, and deleting blog posts. It acts as the presentation and interaction layer of the MERN stack solution, communicating with the backend through REST APIs. + +The user interface is designed around two main usage patterns: + +- Public access to authentication pages. +- Authenticated access to blog browsing, personal blog management, and blog creation. + +The application emphasizes usability, visual clarity, and responsive design. It uses a modern card-based layout, a light/dark theme toggle, and live preview elements to improve the writing and reading experience. + +## 2. Purpose and Objectives + +The frontend was developed with the following objectives: + +- Provide a responsive and intuitive interface for blog readers and authors. +- Enable authentication-driven navigation with persistent login state. +- Support blog creation with preview and image handling. +- Present the blog feed in a readable and visually engaging format. +- Allow users to manage their own posts efficiently. +- Keep the UI modular so that features can be extended without major restructuring. + +## 3. Key Features and Functionalities + +- User login and signup flow. +- Persistent login state using `localStorage`. +- Blog feed page showing all blogs from the backend. +- Personal blog dashboard for the logged-in user. +- Blog creation form with live preview. +- Blog editing screen for updating title and description. +- Blog deletion with confirmation dialog. +- Theme switching between light and dark modes. +- Responsive layout using Material UI and custom CSS. + +## 4. Technology Stack + +### Core Technologies + +- **React 18**: Used to build the component-based user interface. +- **React Router DOM v6**: Handles client-side routing and navigation. +- **Redux Toolkit**: Manages application state such as authentication and theme settings. +- **React Redux**: Connects React components to the Redux store. +- **Axios**: Performs HTTP requests to the backend API. + +### UI and Styling + +- **Material UI (MUI)**: Provides ready-made UI components such as AppBar, Tabs, Cards, Dialogs, Buttons, and TextFields. +- **MUI Styles**: Used for some component-level styling helpers. +- **Styled Components**: Available in the project dependencies for CSS-in-JS styling support. +- **Custom CSS**: Implements the visual theme, layout system, glass effects, responsive behavior, and typography. + +### Why These Technologies + +- React is well suited for modular, reusable UI composition. +- React Router keeps navigation clean and declarative. +- Redux Toolkit simplifies global state handling for auth and theme. +- MUI speeds up implementation while keeping the interface consistent. +- Axios is easy to use for REST-based communication. + +## 5. Project Structure + +### Important Files and Directories + +- `src/App.js`: Main application shell and route definitions. +- `src/index.js`: React entry point that mounts the app, store, and router. +- `src/store/index.js`: Redux slices for auth and theme state. +- `src/config.js`: Central API base URL configuration. +- `src/utils/theme.js`: Theme constants used by styling logic. +- `src/componets/Header.js`: Top navigation bar, tabs, auth buttons, and theme toggle. +- `src/componets/Login.js`: Login and signup form handling. +- `src/componets/Blogs.js`: Displays the full blog feed. +- `src/componets/Blog.js`: Reusable card component for a single blog post. +- `src/componets/UserBlogs.js`: Shows blogs created by the current user. +- `src/componets/AddBlogs.js`: Blog creation page with live preview. +- `src/componets/BlogDetail.js`: Blog editing form for an existing post. +- `src/componets/DeleteBlogs.js`: Standalone delete button component. +- `src/componets/utils.js`: Shared formatting and styling helpers. +- `src/index.css`: Global styles, CSS variables, layout rules, and theme definitions. +- `src/App.css`: Page-specific styles for forms, preview panels, and blog composer layout. + +### Structure Summary + +The frontend follows a feature-oriented component structure. Route-level screens are separated from reusable UI elements, which improves maintainability and makes the application easier to extend. + +## 6. Setup and Installation + +### Prerequisites + +- Node.js and npm installed. +- Backend service running on `http://localhost:5001`. +- MongoDB running through the backend or Docker stack. + +### Local Setup + +1. Open the frontend directory: + + ```bash + cd client + ``` + +2. Install dependencies: + + ```bash + npm install + ``` + +3. Start the development server: + + ```bash + npm start + ``` + +4. Open the app in a browser: + + ```bash + http://localhost:3000 + ``` + +### Environment Variables and Configuration + +This frontend currently uses a static API base URL defined in `src/config.js`: + +```js +const config = { + BASE_URL: "http://localhost:5001", +}; +``` + +If the backend host changes, this value should be updated accordingly. + +## 7. Application Architecture and Code Flow + +The frontend is structured around a route-driven React application: + +1. `index.js` renders `App` inside `BrowserRouter` and `Provider`. +2. The Redux store is shared across the app. +3. `App.js` defines the main routes and checks local authentication state. +4. `Header.js` controls navigation, theme switching, and auth actions. +5. Screen components call backend APIs through Axios. +6. Responses are stored in component state and then rendered to the UI. + +### Data Flow + +- The user interacts with forms or navigation controls. +- The component triggers an API request using Axios. +- The backend returns structured JSON. +- The component stores the result in local state or Redux state. +- The UI re-renders based on updated state. + +### Request-Response Lifecycle Example + +When a user submits a blog: + +1. `AddBlogs.js` collects the title, description, and image. +2. The user ID is read from `localStorage`. +3. Axios sends a `POST` request to `/api/blogs/add`. +4. The backend saves the blog and returns a response object. +5. The frontend navigates back to the blog list after success. + +## 8. State Management and UI Flow + +The frontend uses Redux Toolkit for a small but important global state layer. + +### Auth State + +The `auth` slice stores whether the user is logged in: + +```js +const authSlice = createSlice({ + name: "auth", + initialState: { isLoggedIn: false }, + reducers: { + login(state) { + state.isLoggedIn = true; + }, + logout(state) { + localStorage.removeItem("userId"); + state.isLoggedIn = false; + }, + }, +}); +``` + +Login state is synchronized with `localStorage`. On app load, `App.js` checks whether a `userId` exists and dispatches `authActions.login()` if found. + +### Theme State + +The `theme` slice stores whether dark mode is enabled: + +```js +const themeSlice = createSlice({ + name: "theme", + initialState: { isDarkmode: false }, + reducers: { + setDarkmode: (state, action) => { + state.isDarkmode = action.payload; + }, + }, +}); +``` + +The theme value is persisted in `localStorage` and applied to the document body through a CSS class toggle. This keeps the visual theme consistent across page refreshes. + +### UI Flow + +- `Header` decides which navigation controls are visible based on auth state. +- `Login` switches between login and signup modes. +- `Blogs` renders the public feed. +- `UserBlogs` renders only the current user's posts. +- `AddBlogs` includes an editor and a preview panel side by side on larger screens. +- `BlogDetail` allows editing an existing post and then redirects back to personal blogs. + +### Example of Conditional Routing + +```js +}/> +``` + +This keeps unauthenticated users on the login page and sends authenticated users to the main feed. + +## 9. Authentication and Authorization + +The frontend handles authentication state, but it does not implement token-based security on its own. + +### How Authentication Works + +- The user signs up or logs in through `Login.js`. +- The backend returns a user object. +- The frontend stores the user ID in `localStorage`. +- Redux updates `isLoggedIn`. +- Protected UI elements such as tabs and logout controls are shown only when logged in. + +### Authorization Model + +There is no fine-grained role-based authorization in the frontend. Access control is based on the presence of a stored `userId` and whether the UI considers the user logged in. Blog edit/delete controls are shown only when the current blog belongs to the logged-in user. + +## 10. Core Functionalities + +### Login and Signup + +The `Login` component sends requests to `/api/users/login` or `/api/users/signup` and handles error messages gracefully. + +### Blog Listing + +`Blogs.js` requests all blogs from the server and renders them in a responsive grid of `Blog` cards. + +### Personal Blog Dashboard + +`UserBlogs.js` requests all blogs associated with the current user ID and deduplicates the results before rendering. + +### Blog Creation + +`AddBlogs.js` provides: + +- Title input +- Description editor +- Image URL support +- Local file upload preview +- Live preview of the finished post + +### Blog Editing + +`BlogDetail.js` loads the selected blog, populates the form, and submits updates to the backend. + +### Blog Deletion + +Deletion is supported through the `Blog` card and `DeleteBlogs.js`. A confirmation dialog is displayed before the request is completed. + +## 11. Challenges and Solutions + +- **Challenge: Persisting login across refreshes** + - Solution: The app uses `localStorage` to store the user ID and rehydrates auth state on load. + +- **Challenge: Maintaining a clear interface across screen sizes** + - Solution: Custom responsive CSS and MUI breakpoints are used to adapt layouts for mobile, tablet, and desktop. + +- **Challenge: Supporting both reading and writing workflows** + - Solution: Separate screens were designed for feed browsing, personal posts, and post composition with preview. + +- **Challenge: Handling theme consistency** + - Solution: Theme choice is stored in `localStorage` and reflected through CSS variables and a body class. + +## 12. Future Improvements + +- Add JWT-based authentication instead of local ID persistence. +- Introduce route guards for stronger client-side access control. +- Add search, filter, and pagination for larger blog collections. +- Improve rich text editing with markdown or WYSIWYG support. +- Add image upload to a storage service instead of relying on URLs or browser previews. +- Expand user profile and settings pages. +- Add client-side validation and form schemas. + +## 13. Conclusion + +The frontend delivers a modern and responsive interface for the Blog App while keeping the implementation modular and maintainable. It demonstrates practical use of React, Redux Toolkit, React Router, Material UI, and Axios in a real-world CRUD application. + +The overall impact of this frontend is a smooth content management experience for users, with clear navigation, a readable UI, and an organized flow from authentication to publishing. diff --git a/client/src/App.css b/client/src/App.css index e69de29bb..3df7c623c 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -0,0 +1,309 @@ +main { + flex: 1; +} + +.hero { + padding: 28px; + margin-bottom: 24px; + background: linear-gradient(135deg, rgba(110, 231, 255, 0.18), rgba(255, 106, 213, 0.14)); + border: 1px solid var(--border); + border-radius: 24px; + box-shadow: var(--shadow); + display: flex; + flex-direction: column; + gap: 10px; + transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease; +} + +.hero:hover { + transform: translateY(-4px); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.18); + border-color: var(--card-strong); +} + +.hero h1 { + font-family: "Space Grotesk", sans-serif; + font-size: clamp(28px, 4vw, 42px); + letter-spacing: -0.03em; + color: var(--title); +} + +.hero p { + color: var(--muted); + max-width: 700px; +} + +.form-card { + padding: 30px; + background: var(--card); + border: 1px solid var(--border); + border-radius: 24px; + box-shadow: var(--shadow); + width: 100%; + transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease; +} + +.form-card:hover { + transform: translateY(-3px); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.16); + border-color: var(--card-strong); +} + +.pill-button { + border-radius: 999px !important; + font-weight: 600 !important; + text-transform: none !important; +} + +.page-title { + font-family: "Space Grotesk", sans-serif; + font-weight: 800; + letter-spacing: -0.03em; + color: var(--title); +} + +.surface-panel { + background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)); + border: 1px solid var(--border); + border-radius: 24px; + box-shadow: var(--shadow); +} + +.composer-shell { + display: flex; + flex-direction: column; + gap: 20px; +} + +.composer-grid { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(300px, 0.8fr); + gap: 20px; + align-items: start; +} + +.composer-card, +.preview-card { + padding: 28px; + background: var(--card); + border: 1px solid var(--border); + border-radius: 28px; + box-shadow: var(--shadow); + color: var(--text); + transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease; +} + +.composer-card:hover, +.preview-card:hover { + transform: translateY(-4px); + box-shadow: 0 28px 56px rgba(0, 0, 0, 0.16); + border-color: var(--card-strong); +} + +.composer-header { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 18px; +} + +.composer-subtitle { + color: var(--muted); + max-width: 760px; + line-height: 1.65; +} + +.composer-mini { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.composer-pill { + padding: 8px 12px; + border-radius: 999px; + border: 1px solid var(--border); + background: rgba(255,255,255,0.04); + color: var(--muted); + font-size: 13px; + font-weight: 600; +} + +.field-group { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 18px; +} + +.field-label { + font-size: 0.95rem; + font-weight: 700; + color: var(--title); +} + +.field-note { + color: var(--muted); + font-size: 0.9rem; + line-height: 1.5; +} + +.upload-dropzone { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + margin-top: 4px; + padding: 16px 18px; + border-radius: 18px; + border: 1px dashed var(--border); + background: rgba(255,255,255,0.04); + color: var(--muted); + cursor: pointer; + transition: transform 160ms ease, border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); +} + +.upload-dropzone:hover { + transform: translateY(-1px); + border-color: var(--primary); + background: rgba(110,231,255,0.08); +} + +.upload-meta { + display: flex; + flex-direction: column; + gap: 4px; +} + +.upload-title { + font-weight: 700; + color: var(--title); +} + +.upload-subtitle { + font-size: 13px; + color: var(--muted); +} + +.preview-card { + position: sticky; + top: 96px; +} + +.preview-hero { + border-radius: 22px; + overflow: hidden; + border: 1px solid var(--border); + background: rgba(255,255,255,0.04); +} + +.preview-image { + width: 100%; + aspect-ratio: 16 / 10; + object-fit: cover; + display: block; +} + +.preview-empty { + display: grid; + place-items: center; + min-height: 220px; + padding: 24px; + text-align: center; + color: var(--muted); + background: linear-gradient(135deg, rgba(110,231,255,0.08), rgba(255,106,213,0.08)); +} + +.preview-title { + font-family: "Space Grotesk", sans-serif; + font-size: clamp(24px, 2.2vw, 34px); + font-weight: 800; + letter-spacing: -0.03em; + color: var(--title); +} + +.preview-desc { + color: var(--muted); + line-height: 1.7; + white-space: pre-wrap; + word-break: break-word; +} + +.preview-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 14px; +} + +.preview-chip { + padding: 8px 12px; + border-radius: 999px; + background: rgba(255,255,255,0.04); + border: 1px solid var(--border); + font-size: 13px; + color: var(--muted); + font-weight: 600; +} + +.publish-button { + margin-top: 24px !important; + padding: 12px 18px !important; +} + +body:not(.theme-light) .preview-card { + background: linear-gradient(180deg, rgba(20, 24, 36, 0.98), rgba(12, 14, 20, 0.98)); +} + +body:not(.theme-light) .preview-card .composer-subtitle, +body:not(.theme-light) .preview-card .preview-desc, +body:not(.theme-light) .preview-card .preview-chip, +body:not(.theme-light) .preview-card .muted, +body:not(.theme-light) .preview-card .field-note, +body:not(.theme-light) .preview-card .upload-subtitle { + color: #dbe7ff; +} + +body:not(.theme-light) .preview-card .preview-title, +body:not(.theme-light) .preview-card .page-title, +body:not(.theme-light) .preview-card .upload-title { + color: #ffffff; +} + +body:not(.theme-light) .preview-card .preview-chip { + background: rgba(255, 255, 255, 0.06); +} + +@media (max-width: 1024px) { + .composer-grid { + grid-template-columns: 1fr; + } + + .preview-card { + position: static; + top: auto; + } +} + +@media (max-width: 768px) { + .form-card { + padding: 20px; + } + .composer-card, + .preview-card { + padding: 20px; + border-radius: 22px; + } +} + +@media (max-width: 480px) { + .form-card { + padding: 16px; + } + .composer-card, + .preview-card { + padding: 16px; + } +} diff --git a/client/src/App.js b/client/src/App.js index e8d567a32..da176b90a 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,5 +1,5 @@ import './App.css'; -import { Route, Routes } from "react-router-dom"; +import { Navigate, Route, Routes } from "react-router-dom"; import Header from './componets/Header'; import React, { useEffect } from 'react'; import Login from './componets/Login'; @@ -7,13 +7,15 @@ import Blogs from './componets/Blogs'; import UserBlogs from './componets/UserBlogs' import AddBlogs from './componets/AddBlogs' import BlogDetail from './componets/BlogDetail' -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { authActions } from './store'; function App() { const dispatch = useDispatch(); + const isLoggedIn = useSelector((state) => state.auth.isLoggedIn); + const isDark = useSelector((state) => state.theme.isDarkmode); useEffect(()=>{ const userId = localStorage.getItem("userId"); @@ -21,13 +23,22 @@ function App() { dispatch(authActions.login()); } },[dispatch]); + + useEffect(() => { + document.body.classList.toggle("theme-light", !isDark); + }, [isDark]); return +
-
+
+ } + /> }> }> }> @@ -35,7 +46,7 @@ function App() { } />
- +
; } diff --git a/client/src/componets/AddBlogs.js b/client/src/componets/AddBlogs.js index 7f5d5c7a7..66aafa54f 100644 --- a/client/src/componets/AddBlogs.js +++ b/client/src/componets/AddBlogs.js @@ -1,111 +1,342 @@ -import { Box, Button, InputLabel, TextField, Typography } from "@mui/material"; -import axios from "axios"; +import { + Box, + Button, + Card, + CardContent, + Divider, + InputLabel, + TextField, + Typography, +} from "@mui/material"; import TextareaAutosize from "@mui/material/TextareaAutosize"; +import axios from "axios"; import config from "../config"; -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { useStyles } from "./utils"; -import placeholderImg from "../../src/placeholder.jpg" +import { formatTitle, useStyles } from "./utils"; +import placeholderImg from "../../src/placeholder.jpg"; + +const labelStyles = { + fontSize: "0.95rem", + fontWeight: 700, + color: "var(--title)", +}; -const labelStyles = { mb: 1, mt: 2, fontSize: "24px", fontWeight: "bold" }; const AddBlogs = () => { const classes = useStyles(); const navigate = useNavigate(); + const fileInputId = "blog-upload-input"; const [inputs, setInputs] = useState({ title: "", description: "", imageURL: "", }); + const [imageDataUrl, setImageDataUrl] = useState(""); + const handleChange = (e) => { setInputs((prevState) => ({ ...prevState, [e.target.name]: e.target.value, })); }; + + const handleImageChange = (e) => { + const file = e.target.files?.[0]; + if (!file) { + setImageDataUrl(""); + return; + } + const reader = new FileReader(); + reader.onloadend = () => { + setImageDataUrl(reader.result?.toString() || ""); + }; + reader.readAsDataURL(file); + }; + + const previewImage = useMemo(() => { + if (imageDataUrl) return imageDataUrl; + if (inputs.imageURL.trim()) return inputs.imageURL.trim(); + return placeholderImg; + }, [imageDataUrl, inputs.imageURL]); + + const previewTitle = useMemo(() => { + return formatTitle(inputs.title) || "Your blog title will appear here"; + }, [inputs.title]); + const sendRequest = async () => { - const res = await axios - .post(`${config.BASE_URL}/api/blogs/add`, { + const userId = localStorage.getItem("userId"); + if (!userId) { + return { error: true, message: "Please login before adding a blog." }; + } + try { + const res = await axios.post(`${config.BASE_URL}/api/blogs/add`, { title: inputs.title, desc: inputs.description, - img: inputs.imageURL.trim() === "" ? placeholderImg : inputs.imageURL, - user: localStorage.getItem("userId"), - }) - .catch((err) => console.log(err)); - const data = await res.data; - return data; + img: + imageDataUrl || + (inputs.imageURL.trim() === "" ? placeholderImg : inputs.imageURL), + user: userId, + }); + return res.data; + } catch (err) { + console.log(err); + const message = + err?.response?.data?.message || err?.message || "Request failed"; + return { error: true, message, data: err?.response?.data ?? null }; + } }; - const handleSubmit = (e) => { + + const handleSubmit = async (e) => { e.preventDefault(); - console.log(inputs); - sendRequest() - .then((data) => console.log(data)) - .then(() => navigate("/blogs")); + const data = await sendRequest(); + if (!data || data.error) { + console.error( + "Add blog failed:", + data?.message || "No response from server", + data + ); + return; + } + navigate("/blogs"); }; + return ( -
-
- - - Post Your Blog - - - Title - - - - Description - - - - ImageURL - - - - -
+
+
+

Create a new post

+

+ Shape your idea, add a cover image, and preview the final look before publishing. +

+
+ +
+
+ +
+ + Post Your Blog + + + Build your post in a clean editor with a live preview. Your content stays readable in both light and dark mode. + +
+ Live preview + Image upload + Mobile friendly +
+
+ +
+ + Title + + +
+ +
+ + Description + + + Write clearly and keep paragraphs short for a smoother reading experience. + + +
+ +
+ + Image URL + + + Paste a direct image link or use the upload box below. + + + {inputs.imageURL.trim() !== "" && !imageDataUrl && ( + + Preview { + e.currentTarget.style.display = "none"; + }} + /> + + )} +
+ +
+ + Upload from device + + + + {imageDataUrl && ( + + Preview + + )} +
+ + + + +
+
+ + + + + Preview + + + This is how your post will feel to readers before you publish it. + + +
+ {previewImage ? ( + Blog preview { + e.currentTarget.src = placeholderImg; + }} + /> + ) : ( +
+
+ + Add an image + + + A cover image gives the post more impact. + +
+
+ )} +
+ + + {previewTitle} + + +
+ Featured cover + Readable layout + Modern styling +
+ + + {inputs.description.trim() || + "Your description preview will appear here. Use this area to check spacing and tone before publishing."} + +
+
+
); }; diff --git a/client/src/componets/Blog.js b/client/src/componets/Blog.js index ae161a109..1beabc497 100644 --- a/client/src/componets/Blog.js +++ b/client/src/componets/Blog.js @@ -5,7 +5,12 @@ import { CardContent, CardHeader, CardMedia, + Dialog, + DialogActions, + DialogContent, + DialogTitle, IconButton, + Button, Typography, } from "@mui/material"; import React from "react"; @@ -13,47 +18,115 @@ import ModeEditOutlineIcon from "@mui/icons-material/ModeEditOutline"; import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; import { useNavigate } from "react-router-dom"; import axios from "axios"; -import { useStyles } from "./utils"; +import { formatTitle, useStyles } from "./utils"; import config from "../config"; -const Blogs = ({ title, desc, img, user, isUser, id }) => { +import placeholderImg from "../../src/placeholder.jpg"; + +const Blogs = ({ title, desc, img, user, isUser, id, date, onDelete, onEdit }) => { const classes = useStyles(); const navigate = useNavigate(); + const [openConfirm, setOpenConfirm] = React.useState(false); + const handleEdit = () => { + if (onEdit) { + onEdit(id); + return; + } navigate(`/myBlogs/${id}`); }; + const deleteRequest = async () => { - const res = await axios - .delete(`${config.BASE_URL}/api/blogs/${id}`) - .catch((err) => console.log(err)); - const data = await res.data; - return data; + try { + const res = await axios.delete(`${config.BASE_URL}/api/blogs/${id}`); + return res.data; + } catch (err) { + console.log(err); + return null; + } }; + const handleDelete = () => { + if (onDelete) { + onDelete(id); + return; + } deleteRequest() .then(() => navigate("/")) .then(() => navigate("/blogs")); }; + + const formattedTitle = formatTitle(title); + return (
- {" "} {isUser && ( - + - + setOpenConfirm(true)} + sx={{ + transition: "transform 160ms ease, background-color 160ms ease", + "&:hover": { + transform: "translateY(-2px)", + backgroundColor: "rgba(255,106,213,0.12)", + }, + }} + > @@ -61,29 +134,96 @@ const Blogs = ({ title, desc, img, user, isUser, id }) => { - {user ? user.charAt(0) : ""} + {user ? user.charAt(0).toUpperCase() : ""} } - title={title} + title={ + + {formattedTitle} + + } + subheader={ + + {user} {date ? `• ${date}` : ""} + + } + /> + { + e.currentTarget.src = placeholderImg; + }} /> - - -
-
- {user} {": "} {desc} + {desc}
+ setOpenConfirm(false)}> + Delete this blog? + + + This action cannot be undone. + + + + + + +
); }; diff --git a/client/src/componets/BlogDetail.js b/client/src/componets/BlogDetail.js index 29c282395..eb22442c5 100644 --- a/client/src/componets/BlogDetail.js +++ b/client/src/componets/BlogDetail.js @@ -5,7 +5,13 @@ import React, { useEffect, useState, useCallback } from "react"; import { useNavigate, useParams } from "react-router-dom"; import config from "../config"; -const labelStyles = { mb: 1, mt: 2, fontSize: "24px", fontWeight: "bold" }; +const labelStyles = { + mb: 1, + mt: 2, + fontSize: "18px", + fontWeight: "bold", + color: "var(--text)", +}; const BlogDetail = () => { const navigate = useNavigate(); @@ -26,10 +32,11 @@ const BlogDetail = () => { try { const res = await axios.get(`${config.BASE_URL}/api/blogs/${id}`); const data = res.data; - setBlog(data.blog); + const fetchedBlog = data?.data?.blog; + setBlog(fetchedBlog); setInputs({ - title: data.blog.title || "", - description: data.blog.description || "", + title: fetchedBlog?.title || "", + description: fetchedBlog?.desc || "", }); } catch (err) { console.error("Failed to fetch blog details:", err); @@ -44,7 +51,7 @@ const BlogDetail = () => { try { const res = await axios.put(`${config.BASE_URL}/api/blogs/update/${id}`, { title: inputs.title, - description: inputs.description, + desc: inputs.description, }); return res.data; } catch (err) { @@ -57,7 +64,7 @@ const BlogDetail = () => { sendRequest() .then((data) => { console.log("Blog updated:", data); - navigate("/myBlogs/"); + navigate("/myBlogs", { replace: true, state: { refresh: Date.now() } }); }); }; @@ -66,51 +73,66 @@ const BlogDetail = () => { {blog ? (
- - Update Blog - + + Update Blog + Title Description diff --git a/client/src/componets/Blogs.js b/client/src/componets/Blogs.js index 959d98cb3..24fecae54 100644 --- a/client/src/componets/Blogs.js +++ b/client/src/componets/Blogs.js @@ -1,35 +1,73 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import axios from "axios"; import Blog from "./Blog"; import config from "../config"; const Blogs = () => { - const [blogs, setBlogs] = useState(); + const [blogs, setBlogs] = useState([]); + const [errorMsg, setErrorMsg] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const hasFetched = useRef(false); const sendRequest = async () => { - const res = await axios - .get(`${config.BASE_URL}/api/blogs`) - .catch((err) => console.log(err)); - const data = await res.data; - return data; + try { + const res = await axios.get(`${config.BASE_URL}/api/blogs`); + return res.data; + } catch (err) { + console.log(err); + const message = + err?.response?.data?.message || err?.message || "Request failed"; + return { error: true, message, data: err?.response?.data ?? null }; + } }; useEffect(() => { - sendRequest().then((data) => setBlogs(data.blogs)); + if (hasFetched.current) return; + hasFetched.current = true; + sendRequest().then((data) => { + if (!data || data.error) { + setErrorMsg(data?.message || "No response from server"); + setBlogs([]); + setIsLoading(false); + return; + } + setBlogs(data?.data?.blogs || []); + setIsLoading(false); + }); }, []); console.log(blogs); return (
- {blogs && - blogs.map((blog, index) => ( - - ))} +
+

Discover what people are building.

+

+ A clean, modern space for your ideas. Share, explore, and connect + through blogs. +

+
+ {isLoading &&

Loading blogs...

} + {errorMsg &&

{errorMsg}

} +
+ {blogs && + blogs.map((blog) => ( + + setBlogs((prev) => prev.filter((item) => item._id !== id)) + } + /> + ))} +
+ {blogs?.length === 0 && !errorMsg && ( +

+ No blogs yet. Be the first to post. +

+ )}
); }; diff --git a/client/src/componets/DeleteBlogs.js b/client/src/componets/DeleteBlogs.js index 714db1f4b..dfd859877 100644 --- a/client/src/componets/DeleteBlogs.js +++ b/client/src/componets/DeleteBlogs.js @@ -1,6 +1,7 @@ import React from "react"; import axios from "axios"; import config from "../config"; +import { Button } from "@mui/material"; const DeleteButton = ({ blogId, onDelete }) => { const handleDelete = async () => { @@ -8,13 +9,27 @@ const DeleteButton = ({ blogId, onDelete }) => { // Send a delete request to your backend await axios.delete(`${config.BASE_URL}/api/blogs/${blogId}`); // Call the onDelete callback to update the UI - onDelete(); + onDelete(blogId); } catch (error) { console.error("Error deleting blog:", error); } }; - return ; + return ( + + ); }; export default DeleteButton; diff --git a/client/src/componets/Header.js b/client/src/componets/Header.js index 3e13facff..bc17ba739 100644 --- a/client/src/componets/Header.js +++ b/client/src/componets/Header.js @@ -13,16 +13,13 @@ import { import { useDispatch, useSelector } from "react-redux"; import DarkModeIcon from "@mui/icons-material/DarkMode"; import LightModeIcon from "@mui/icons-material/LightMode"; -import { lightTheme, darkTheme } from "../utils/theme"; const Header = () => { const dispatch = useDispatch(); const isDark = useSelector((state) => state.theme.isDarkmode); - const theme = isDark ? darkTheme : lightTheme; - const isLoggedIn = useSelector((state) => state.auth.isLoggedIn); - const [value, setValue] = useState(); + const [value, setValue] = useState(0); const location = useLocation(); const navigate = useNavigate(); @@ -35,7 +32,7 @@ const Header = () => { if (savedTheme !== null) { dispatch(setDarkmode(JSON.parse(savedTheme))); } - }, []); + }, [dispatch]); useEffect(() => { const path = location.pathname; if (path.startsWith("/blogs/add")) { @@ -69,15 +66,172 @@ const Header = () => { }; return ( - - - BlogsApp + + + + + BlogsApp + + + {!isLoggedIn && ( + <> + + + + )} + + {isLoggedIn && ( + + )} +
+ {isDark ? : } +
+
+
{isLoggedIn && ( - + @@ -85,57 +239,6 @@ const Header = () => { )} - - {!isLoggedIn && ( - <> - - - - )} - - {isLoggedIn && ( - - )} -
- {isDark ? : } -
-
); diff --git a/client/src/componets/Login.js b/client/src/componets/Login.js index 8f06606b0..1a50dc035 100644 --- a/client/src/componets/Login.js +++ b/client/src/componets/Login.js @@ -8,8 +8,8 @@ import config from "../config"; const Login = () => { const location = useLocation(); - const naviagte = useNavigate(); - const dispath = useDispatch(); + const navigate = useNavigate(); + const dispatch = useDispatch(); const { isSignupButtonPressed } = location.state || {}; const [inputs, setInputs] = useState({ @@ -17,6 +17,7 @@ const Login = () => { email: "", password: "", }); + const [errorMsg, setErrorMsg] = useState(""); const [isSignup, setIsSignup] = useState(isSignupButtonPressed || false); const handleChange = (e) => { setInputs((prevState) => ({ @@ -30,53 +31,81 @@ const Login = () => { const sendRequest = async (type = "login") => { console.log("inside send req"); console.log(`${config.BASE_URL}/api/users/${type}`); - const res = await axios - .post(`${config.BASE_URL}/api/users/${type}`, { + try { + const res = await axios.post(`${config.BASE_URL}/api/users/${type}`, { name: inputs.name, email: inputs.email, password: inputs.password, - }) - .catch((err) => console.log(err)); - - const data = await res.data; - console.log("return"); - console.log(data); - return data; + }); + console.log("return"); + console.log(res.data); + return res.data; + } catch (err) { + const status = err?.response?.status; + const serverMessage = err?.response?.data?.message; + const fallbackMessage = + status === 400 && type === "signup" + ? "User already exists. Please login." + : status === 404 && type === "login" + ? "User not found. Please signup first." + : "Request failed"; + const message = serverMessage || err?.message || fallbackMessage; + console.log(err); + return { error: true, message, data: err?.response?.data ?? null }; + } }; - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); console.log(inputs); - if (isSignup) { - sendRequest("signup") - .then((data) => localStorage.setItem("userId", data.user._id)) - .then(() => dispath(authActions.login())) - .then(() => naviagte("/blogs")); - } else { - sendRequest() - .then((data) => localStorage.setItem("userId", data.user._id)) - .then(() => dispath(authActions.login())) - .then(() => naviagte("/blogs")); + setErrorMsg(""); + const data = await sendRequest(isSignup ? "signup" : "login"); + if (!data || data.error) { + console.error( + "Login failed:", + data?.message || "No response from server", + data + ); + setErrorMsg(data?.message || "No response from server"); + return; } + const userId = data?.data?.user?._id; + if (!userId) { + console.error("Login failed: missing user id in response", data); + return; + } + localStorage.setItem("userId", userId); + dispatch(authActions.login()); + navigate("/blogs"); }; return (
- + {isSignup ? "Signup" : "Login"} + {errorMsg && ( + + {errorMsg} + + )} {isSignup && ( { value={inputs.name} placeholder="Name" margin="normal" + sx={{ + width: "100%", + input: { color: "var(--text)" }, + "& .MuiOutlinedInput-notchedOutline": { + borderColor: "var(--border)", + }, + }} /> )}{" "} { type={"email"} placeholder="Email" margin="normal" + sx={{ + width: "100%", + input: { color: "var(--text)" }, + "& .MuiOutlinedInput-notchedOutline": { + borderColor: "var(--border)", + }, + }} /> { type={"password"} placeholder="Password" margin="normal" + sx={{ + width: "100%", + input: { color: "var(--text)" }, + "& .MuiOutlinedInput-notchedOutline": { + borderColor: "var(--border)", + }, + }} /> diff --git a/client/src/componets/UserBlogs.js b/client/src/componets/UserBlogs.js index 34de299e5..0ae038f02 100644 --- a/client/src/componets/UserBlogs.js +++ b/client/src/componets/UserBlogs.js @@ -1,28 +1,69 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import axios from "axios"; -import Blogs from "./Blogs"; -import DeleteButton from "./DeleteBlogs"; +import Blog from "./Blog"; import { makeStyles } from "@mui/styles"; import config from "../config"; +import { useNavigate } from "react-router-dom"; +import { useLocation } from "react-router-dom"; const useStyles = makeStyles((theme) => ({ container: { display: "flex", flexDirection: "column", + alignItems: "stretch", + margin: "0 auto", + width: "100%", + }, + intro: { + marginBottom: "22px", + padding: "24px", + borderRadius: "22px", + background: "linear-gradient(135deg, rgba(110,231,255,0.14), rgba(255,106,213,0.12))", + border: "1px solid var(--border)", + boxShadow: "var(--shadow)", + }, + introTitle: { + fontFamily: "Space Grotesk, sans-serif", + fontSize: "clamp(26px, 3vw, 38px)", + fontWeight: 700, + letterSpacing: "-0.03em", + marginBottom: "8px", + }, + introText: { + color: "var(--muted)", + maxWidth: "760px", + lineHeight: 1.6, + }, + introMeta: { + marginTop: "14px", + display: "inline-flex", alignItems: "center", - margin: "20px auto", - width: "80%", + gap: "8px", + padding: "8px 14px", + borderRadius: "999px", + background: "rgba(255,255,255,0.05)", + border: "1px solid var(--border)", + color: "var(--text)", + fontSize: "14px", + fontWeight: 600, + }, + blogGrid: { + display: "grid", + gridTemplateColumns: "repeat(2, minmax(0, 1fr))", + gap: "22px", + width: "100%", + alignItems: "start", + "@media (max-width: 768px)": { + gridTemplateColumns: "1fr", + gap: "16px", + }, }, blogContainer: { display: "flex", flexDirection: "column", - alignItems: "center", - padding: "20px", - border: "1px solid #ccc", - borderRadius: "10px", - marginBottom: "20px", - boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.1)", + alignItems: "stretch", + width: "100%", }, blogImage: { width: "100%", @@ -50,49 +91,89 @@ const useStyles = makeStyles((theme) => ({ const UserBlogs = () => { const classes = useStyles(); + const navigate = useNavigate(); + const location = useLocation(); const [user, setUser] = useState(); + const [isLoading, setIsLoading] = useState(true); + const hasFetched = useRef(false); + const lastRefresh = useRef(0); const id = localStorage.getItem("userId"); const sendRequest = async () => { - const res = await axios - .get(`${config.BASE_URL}/api/blogs/user/${id}`) - .catch((err) => console.log(err)); - const data = await res?.data; - return data; + try { + const res = await axios.get(`${config.BASE_URL}/api/blogs/user/${id}`); + return res.data; + } catch (err) { + console.log(err); + return null; + } }; useEffect(() => { - sendRequest().then((data) => setUser(data?.user)); - }, []); - - const handleDelete = (blogId) => { - axios.delete(`${config.BASE_URL}/api/blogs/${blogId}`).then(() => { - sendRequest().then((data) => setUser(data.user)); + const refreshToken = location.state?.refresh || 0; + if (hasFetched.current && lastRefresh.current === refreshToken) return; + hasFetched.current = true; + lastRefresh.current = refreshToken; + if (!id) { + setUser(null); + setIsLoading(false); + return; + } + setIsLoading(true); + sendRequest().then((data) => { + setUser(data?.data?.user); + setIsLoading(false); }); + }, [id, location.state?.refresh]); + + const handleDelete = async (blogId) => { + if (!blogId) return; + try { + await axios.delete(`${config.BASE_URL}/api/blogs/${blogId}`); + const fresh = await sendRequest(); + setUser(fresh?.data?.user || null); + } catch (err) { + console.error("Delete failed:", err); + } + }; + const handleEdit = (blogId) => { + if (!blogId) return; + navigate(`/myBlogs/${blogId}`); }; return (
- {user && - user.blogs && - user.blogs.map((blog, index) => ( -
- - {blog.title} - -
- ))} +
+

My Blogs

+

+ Your personal writing space, organized for quick scanning and easy editing. + Posts are shown in a compact two-column layout on larger screens and a single + column on mobile. +

+
{user?.blogs?.length || 0} posts
+
+ {isLoading &&

Loading your blogs...

} + {user && user.blogs && ( +
+ {Array.from( + new Map(user.blogs.map((blog) => [blog._id, blog])).values() + ).map((blog) => ( +
+ +
+ ))} +
+ )}
); }; diff --git a/client/src/componets/utils.js b/client/src/componets/utils.js index d07e978ea..759496d05 100644 --- a/client/src/componets/utils.js +++ b/client/src/componets/utils.js @@ -1,6 +1,12 @@ import { makeStyles } from "@mui/styles"; export const useStyles = makeStyles({ font: { - fontFamily: "Roboto !important", + fontFamily: "Plus Jakarta Sans, sans-serif !important", }, -}); \ No newline at end of file +}); + +export const formatTitle = (title = "") => { + const trimmed = String(title).trim(); + if (!trimmed) return ""; + return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); +}; diff --git a/client/src/index.css b/client/src/index.css index 4d9942614..1a413a943 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,4 +1,199 @@ -*{ +@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"); + +* { margin: 0; padding: 0; -} \ No newline at end of file + box-sizing: border-box; +} + +:root { + --bg: #0f1115; + --bg-accent: #141824; + --surface: rgba(255, 255, 255, 0.06); + --surface-elevated: rgba(255, 255, 255, 0.1); + --card: rgba(255, 255, 255, 0.06); + --card-strong: rgba(255, 255, 255, 0.14); + --title: #f8fbff; + --text: #f5f7ff; + --muted: #b9c2d6; + --primary: #6ee7ff; + --secondary: #ff6ad5; + --accent-soft: rgba(110, 231, 255, 0.18); + --input-bg: rgba(255, 255, 255, 0.04); + --focus-ring: rgba(110, 231, 255, 0.45); + --shadow: 0 20px 40px rgba(0, 0, 0, 0.35); + --border: rgba(255, 255, 255, 0.2); +} + +body { + font-family: "Plus Jakarta Sans", system-ui, -apple-system, sans-serif; + background: radial-gradient(1000px 500px at 12% -10%, rgba(110, 231, 255, 0.16) 0%, transparent 60%), + radial-gradient(900px 520px at 88% -15%, rgba(255, 106, 213, 0.14) 0%, transparent 58%), + linear-gradient(180deg, var(--bg) 0%, #0b0d12 100%); + color: var(--text); + min-height: 100vh; + transition: background 220ms ease, color 220ms ease; +} + +body.theme-light { + --bg: #f4f6fb; + --bg-accent: #ffffff; + --surface: rgba(15, 17, 21, 0.04); + --surface-elevated: rgba(15, 17, 21, 0.07); + --card: rgba(15, 17, 21, 0.05); + --card-strong: rgba(15, 17, 21, 0.12); + --title: #111827; + --text: #111827; + --muted: #546174; + --primary: #0ea5e9; + --secondary: #ec4899; + --accent-soft: rgba(14, 165, 233, 0.12); + --input-bg: rgba(255, 255, 255, 0.88); + --focus-ring: rgba(14, 165, 233, 0.35); + --shadow: 0 20px 40px rgba(15, 17, 21, 0.08); + --border: rgba(15, 17, 21, 0.2); + background: radial-gradient(1000px 500px at 12% -10%, rgba(14, 165, 233, 0.12) 0%, transparent 60%), + radial-gradient(900px 520px at 88% -15%, rgba(236, 72, 153, 0.1) 0%, transparent 58%), + linear-gradient(180deg, #f9fafc 0%, #eef2ff 100%); +} + +a { + text-decoration: none; + color: inherit; + transition: color 160ms ease, opacity 160ms ease; +} + +a:hover { + color: var(--primary); +} + +.app-shell { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.page { + width: min(1100px, 92%); + margin: 32px auto 64px; +} + +.section-title { + font-family: "Space Grotesk", sans-serif; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 20px; + color: var(--title); +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 20px; +} + +.glass { + background: var(--card); + backdrop-filter: blur(14px); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 16px; + box-shadow: var(--shadow); + transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease, background 180ms ease; +} + +.glass:hover { + transform: translateY(-4px); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.18); + border-color: var(--card-strong); +} + +.muted { + color: var(--muted); +} + +.MuiButton-root, +.MuiTab-root, +.MuiIconButton-root, +.MuiPaper-root, +.MuiCard-root, +.MuiOutlinedInput-root, +.MuiInputBase-root { + transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background-color 160ms ease, color 160ms ease; +} + +.MuiButton-root:hover, +.MuiTab-root:hover, +.MuiIconButton-root:hover { + transform: translateY(-2px); +} + +.MuiButton-root { + border-radius: 999px !important; + text-transform: none !important; + font-weight: 700 !important; +} + +.MuiOutlinedInput-root { + background: var(--input-bg); + border-radius: 14px; +} + +.MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline { + border-color: var(--primary) !important; +} + +.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline { + border-color: var(--primary) !important; + box-shadow: 0 0 0 4px var(--focus-ring); +} + +.MuiInputBase-input, +.MuiInputBase-inputMultiline { + color: var(--text); +} + +.MuiPaper-root { + background-image: none; +} + +@media (max-width: 1024px) { + .page { + width: min(980px, 92%); + } + .grid { + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + } +} + +@media (min-width: 1280px) { + .page { + width: min(1400px, 88%); + } + .grid { + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 24px; + } +} + +@media (max-width: 768px) { + .page { + width: 92%; + margin: 20px auto 48px; + } + .grid { + grid-template-columns: 1fr; + } + .hero { + padding: 18px; + } +} + +@media (max-width: 480px) { + .page { + width: 94%; + margin: 16px auto 40px; + } + .hero h1 { + font-size: 26px; + } +} diff --git a/client/src/index.js b/client/src/index.js index a77935ac7..d13a4853f 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -9,7 +9,10 @@ import { store } from './store'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + diff --git a/server/.env b/server/.env new file mode 100644 index 000000000..4522065b4 --- /dev/null +++ b/server/.env @@ -0,0 +1 @@ +MONGO_URI = mongodb://127.0.0.1:27017/BlogApp \ No newline at end of file diff --git a/server/controller/blog-controller.js b/server/controller/blog-controller.js index 1cf77188f..93a3aba0f 100644 --- a/server/controller/blog-controller.js +++ b/server/controller/blog-controller.js @@ -6,9 +6,9 @@ const { ApiError } = require("../utils/ApiError"); const getAllBlogs = async (req, res, next) => { try { - const blogs = await Blog.find(); + const blogs = await Blog.find().populate("user"); if (!blogs || blogs.length === 0) { - return res.status(404).json(new ApiError(404, "No blogs found")); + return res.status(200).json(new ApiResponse(200, { blogs: [] }, "No blogs found")); } return res.status(200).json(new ApiResponse(200, { blogs }, "Blogs found")); } catch (e) { @@ -28,12 +28,10 @@ const addBlog = async (req, res, next) => { const blog = new Blog({ title, desc, img, user, date: currentDate }); - const session = await mongoose.startSession(); - session.startTransaction(); - await blog.save({ session }); + await blog.save(); + await blog.populate("user"); existingUser.blogs.push(blog); - await existingUser.save({ session }); - await session.commitTransaction(); + await existingUser.save(); return res.status(201).json(new ApiResponse(201, { blog }, "Blog created successfully")); } catch (e) { @@ -59,7 +57,7 @@ const updateBlog = async (req, res, next) => { const getById = async (req, res, next) => { const id = req.params.id; try { - const blog = await Blog.findById(id); + const blog = await Blog.findById(id).populate("user"); if (!blog) { return res.status(404).json(new ApiError(404, "Blog not found")); } @@ -100,4 +98,4 @@ const getByUserId = async (req, res, next) => { } }; -module.exports = { getAllBlogs, addBlog, updateBlog, getById, deleteBlog, getByUserId }; \ No newline at end of file +module.exports = { getAllBlogs, addBlog, updateBlog, getById, deleteBlog, getByUserId }; diff --git a/server/controller/user-contoller.js b/server/controller/user-contoller.js index c6387a7df..f34a010e1 100644 --- a/server/controller/user-contoller.js +++ b/server/controller/user-contoller.js @@ -34,7 +34,11 @@ const signUp = async (req, res, next) => { }); await user.save(); - return res.status(201).json(new ApiResponse(201, { user }, "User registered successfully")); + const safeUser = user.toObject(); + delete safeUser.password; + return res + .status(201) + .json(new ApiResponse(201, { user: safeUser }, "User registered successfully")); } catch (e) { console.error(e); return res.status(500).json(new ApiError(500, "Server error while signing up")); @@ -55,7 +59,11 @@ const logIn = async (req, res, next) => { return res.status(400).json(new ApiError(400, "Incorrect Password")); } - return res.status(200).json(new ApiResponse(200, { user: existingUser }, "Login successful")); + const safeUser = existingUser.toObject(); + delete safeUser.password; + return res + .status(200) + .json(new ApiResponse(200, { user: safeUser }, "Login successful")); } catch (err) { console.error(err); return res.status(500).json(new ApiError(500, "Server error while logging in")); diff --git a/server/reportReadme.md b/server/reportReadme.md new file mode 100644 index 000000000..5647903b0 --- /dev/null +++ b/server/reportReadme.md @@ -0,0 +1,415 @@ +# Backend Project Report + +## 1. Project Overview + +The backend of this Blog App is a Node.js and Express-based REST API that manages user accounts, blog creation, blog updates, blog retrieval, and blog deletion. It acts as the application's data and business logic layer, connecting the frontend to MongoDB through Mongoose models. + +The backend is intentionally lightweight and focused on core blog management operations. It exposes a small set of RESTful endpoints that support the full lifecycle of posts and user authentication through email/password login and signup. + +## 2. Purpose and Objectives + +The backend was built to: + +- Provide a reliable API for the frontend blog application. +- Store user and blog data in MongoDB. +- Support authentication-related operations such as signup and login. +- Manage relationships between users and their posts. +- Return structured success and error responses. +- Keep the service simple enough to run locally or in Docker. + +## 3. Key Features and Functionalities + +- User registration with password hashing. +- User login with password verification. +- Create, read, update, and delete blog posts. +- Retrieve all blogs or only blogs for a specific user. +- Populate user and blog references for richer responses. +- Consistent API response and error formatting. +- MongoDB integration through Mongoose schemas. + +## 4. Technology Stack + +### Core Technologies + +- **Node.js**: Runtime environment for the backend. +- **Express**: Web framework used to define routes and middleware. +- **MongoDB**: NoSQL database used to store users and blogs. +- **Mongoose**: Object data modeling library for MongoDB. + +### Security and Utility Libraries + +- **bcryptjs**: Used to hash passwords during signup and compare them during login. +- **cors**: Enables cross-origin requests from the frontend. +- **helmet**: Adds common security-related HTTP headers. +- **dotenv**: Loads environment variables from `.env`. +- **nodemon**: Reloads the server automatically during development. + +### Why These Technologies + +- Express keeps the API implementation simple and readable. +- MongoDB fits document-oriented blog and user data well. +- Mongoose provides validation, references, and easier query handling. +- bcryptjs is appropriate for password protection without adding unnecessary complexity. +- Helmet and CORS help harden and connect the API safely during development. + +## 5. Project Structure + +### Important Files and Directories + +- `server.js`: Main application bootstrap file. +- `config/db.js`: MongoDB connection setup. +- `controller/user-contoller.js`: User signup, login, and user listing logic. +- `controller/blog-controller.js`: Blog CRUD and lookup logic. +- `model/User.js`: User schema and database model. +- `model/Blog.js`: Blog schema and database model. +- `routes/user-routes.js`: Routes for authentication and user listing. +- `routes/blog-routes.js`: Routes for blog operations. +- `utils/ApiResponse.js`: Standard success response wrapper. +- `utils/ApiError.js`: Standard error response wrapper. +- `.env.example`: Example environment file for local setup. + +### Structure Summary + +The backend is organized using a conventional MVC-inspired pattern: + +- Routes map URLs to controllers. +- Controllers implement business logic. +- Models define database shape. +- Utilities standardize response payloads. + +## 6. Setup and Installation + +### Prerequisites + +- Node.js and npm installed. +- MongoDB available locally, in Docker, or via MongoDB Atlas. + +### Local Setup + +1. Open the backend directory: + + ```bash + cd server + ``` + +2. Install dependencies: + + ```bash + npm install + ``` + +3. Create a `.env` file in the `server` directory. + +4. Add the MongoDB connection string: + + ```env + MONGO_URI=mongodb://127.0.0.1:27017/BlogApp + ``` + +5. Start the backend server: + + ```bash + npm start + ``` + +6. The API will run on: + + ```bash + http://localhost:5001 + ``` + +### Docker Setup + +The repository also includes `docker-compose.yaml`, which starts: + +- the frontend container, +- the backend container, +- and a MongoDB container. + +## 7. Application Architecture and Code Flow + +The backend follows a clear request-processing flow: + +1. The client sends an HTTP request to an API endpoint. +2. Express matches the route in `routes/`. +3. The route forwards the request to a controller function. +4. The controller performs database operations using Mongoose models. +5. The controller wraps the result in `ApiResponse` or `ApiError`. +6. The JSON response is returned to the frontend. + +### Request-Response Lifecycle Example + +For a blog creation request: + +1. The frontend posts blog data to `/api/blogs/add`. +2. `blog-controller.js` validates the associated user ID. +3. A new `Blog` document is created. +4. The blog reference is pushed into the user's `blogs` array. +5. A success response is sent back. + +### Data Flow + +- **Frontend -> Backend**: JSON request payloads are sent using Axios. +- **Backend -> Database**: Mongoose reads and writes user/blog documents. +- **Database -> Backend**: Query results are populated and serialized. +- **Backend -> Frontend**: JSON responses are returned to update the UI. + +## 8. Core Functionalities + +### User Signup + +The signup flow checks for duplicate emails, hashes the password, and stores the new user. + +```js +const hashedPassword = bcrypt.hashSync(password, 10); +``` + +### User Login + +The login flow finds the user by email and compares the submitted password against the stored hash. + +### Blog Creation + +The backend verifies that the user exists before accepting a blog post. It then creates the blog and associates it with the user. + +### Blog Retrieval + +- All blogs can be retrieved with populated user details. +- A single blog can be fetched by ID. +- A user's blogs can be loaded by user ID. + +### Blog Update + +The update endpoint modifies blog title and description fields. + +### Blog Deletion + +The delete endpoint removes the blog from the database and also updates the owning user's blog list. + +## 9. API Documentation + +### Base URL + +```text +http://localhost:5001/api +``` + +### User Endpoints + +#### `GET /users` + +Returns all users. + +Response example: + +```json +{ + "statusCode": 200, + "data": { + "users": [] + }, + "message": "Users fetched successfully", + "success": true +} +``` + +#### `POST /users/signup` + +Creates a new user account. + +Request body: + +```json +{ + "name": "Alice", + "email": "alice@example.com", + "password": "secret123" +} +``` + +Response example: + +```json +{ + "statusCode": 201, + "data": { + "user": { + "_id": "..." + } + }, + "message": "User registered successfully", + "success": true +} +``` + +#### `POST /users/login` + +Authenticates a user using email and password. + +Request body: + +```json +{ + "email": "alice@example.com", + "password": "secret123" +} +``` + +### Blog Endpoints + +#### `GET /blogs` + +Returns all blogs with populated user references. + +#### `POST /blogs/add` + +Creates a new blog post. + +Request body: + +```json +{ + "title": "My First Blog", + "desc": "Blog content here", + "img": "https://example.com/image.jpg", + "user": "userObjectId" +} +``` + +#### `GET /blogs/user/:id` + +Returns blogs for a specific user. + +#### `GET /blogs/:id` + +Returns a single blog by its ID. + +#### `PUT /blogs/update/:id` + +Updates blog title and description. + +Request body: + +```json +{ + "title": "Updated Title", + "desc": "Updated description" +} +``` + +#### `DELETE /blogs/:id` + +Deletes a blog by its ID. + +### Response Pattern + +The backend uses a consistent wrapper: + +```js +class ApiResponse { + constructor(statusCode, data, message = "Success") { + this.statusCode = statusCode; + this.data = data; + this.message = message; + this.success = statusCode < 400; + } +} +``` + +Errors use `ApiError`, which adds a status code, message, and optional error details. + +## 10. Authentication and Authorization + +### Authentication + +Authentication is implemented at the application level using email/password login. + +- Passwords are hashed with `bcryptjs` during signup. +- Login compares the submitted password against the stored hash. +- On successful login, the API returns the user object without the password field. + +### Authorization + +The backend does not implement token-based authorization or role-based access control. Access decisions are currently based on whether the frontend provides a valid user ID in the request body or route parameters. + +### Security Note + +This design is functional for a coursework or prototype-level project, but in a production system it should be replaced with JWT/session-based authentication and server-side authorization checks. + +## 11. Database Design + +The backend uses two MongoDB collections: `users` and `blogs`. + +### `User` Schema + +```js +{ + name: String, + email: String, + password: String, + blogs: [ObjectId] +} +``` + +#### Fields + +- `name`: Required user name. +- `email`: Required and unique identifier for login. +- `password`: Required hashed password with minimum length validation. +- `blogs`: Array of blog references belonging to the user. + +### `Blog` Schema + +```js +{ + title: String, + desc: String, + img: String, + user: ObjectId, + date: Date +} +``` + +#### Fields + +- `title`: Required post title. +- `desc`: Required blog description. +- `img`: Required image URL or data URL. +- `user`: Required reference to the owning user. +- `date`: Automatically set when the blog is created. + +### Relationships + +- One user can have many blogs. +- Each blog belongs to exactly one user. +- Mongoose `populate()` is used to resolve references when reading data. + +## 12. Challenges and Solutions + +- **Challenge: Protecting passwords** + - Solution: Passwords are hashed with `bcryptjs` before storage. + +- **Challenge: Keeping user-blog relationships synchronized** + - Solution: When a blog is created, its reference is added to the user document; when deleted, the reference is removed from the user document. + +- **Challenge: Returning readable API data** + - Solution: `populate()` is used for blog and user lookups so the frontend gets meaningful nested data. + +- **Challenge: Consistent response formatting** + - Solution: `ApiResponse` and `ApiError` ensure a predictable JSON structure across endpoints. + +## 13. Future Improvements + +- Add JWT-based authentication and protected routes. +- Add middleware for authorization and ownership checks. +- Introduce request validation for all endpoints. +- Add pagination and filtering for blog listings. +- Implement centralized error-handling middleware. +- Replace manual user-ID checks with secure authenticated sessions. +- Add unit and integration tests for controllers and models. + +## 14. Conclusion + +The backend provides the essential data and business logic layer for the Blog App. It successfully handles user management, blog CRUD operations, and MongoDB integration in a concise and maintainable structure. + +Its impact is seen in the way it supports the full frontend experience while keeping the API design straightforward enough for local development, deployment with Docker, and future extension into a more secure production-grade service. diff --git a/server/routes/blog-routes.js b/server/routes/blog-routes.js index 549697a6e..e35e645fc 100644 --- a/server/routes/blog-routes.js +++ b/server/routes/blog-routes.js @@ -4,10 +4,10 @@ const { getAllBlogs , addBlog , updateBlog ,getById , deleteBlog , getByUserId} = require("../controller/blog-controller"); -blogRouter.get("/",getAllBlogs); -blogRouter.post('/add', addBlog); -blogRouter.put("/update/:id", updateBlog); +blogRouter.get("/", getAllBlogs); +blogRouter.post("/add", addBlog); +blogRouter.get("/user/:id", getByUserId); blogRouter.get("/:id", getById); -blogRouter.delete("/:id",deleteBlog); -blogRouter.get("/user/:id",getByUserId) -module.exports = blogRouter; \ No newline at end of file +blogRouter.put("/update/:id", updateBlog); +blogRouter.delete("/:id", deleteBlog); +module.exports = blogRouter; diff --git a/server/server.js b/server/server.js index d9e8b0485..79ee45cba 100644 --- a/server/server.js +++ b/server/server.js @@ -18,7 +18,8 @@ app.use(helmet( )); app.set("view engine", "ejs"); -app.use(express.json()); +app.use(express.json({ limit: "5mb" })); +app.use(express.urlencoded({ extended: true, limit: "5mb" })); app.use("/api/users", userRouter); app.use("/api/blogs", blogRouter); diff --git a/server/utils/ApiError.js b/server/utils/ApiError.js index 7fa485ff8..534742c2a 100644 --- a/server/utils/ApiError.js +++ b/server/utils/ApiError.js @@ -21,4 +21,4 @@ class ApiError extends Error { } } -export {ApiError} \ No newline at end of file +module.exports = { ApiError } diff --git a/server/utils/ApiResponse.js b/server/utils/ApiResponse.js index cb2a284af..86b4267ab 100644 --- a/server/utils/ApiResponse.js +++ b/server/utils/ApiResponse.js @@ -7,4 +7,4 @@ class ApiResponse { } } -export { ApiResponse } \ No newline at end of file +module.exports = { ApiResponse } diff --git a/server/yarn.lock b/server/yarn.lock index 6a5492980..399fef224 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1272,6 +1272,11 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +helmet@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz" + integrity sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg== + http-errors@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" From 6b484983eb46059846d91417ee3617396ec85ef2 Mon Sep 17 00:00:00 2001 From: amanlakhera16 Date: Wed, 29 Apr 2026 20:48:42 +0530 Subject: [PATCH 2/2] feat: Update server and client configurations for improved CORS handling and environment variable management --- client/.env.example | 1 + client/package.json | 3 +- client/public/_redirects | 1 + client/src/config.js | 3 +- server/.env.example | 3 +- server/package.json | 106 +++++++++------------------------------ server/server.js | 22 ++++++-- 7 files changed, 48 insertions(+), 91 deletions(-) create mode 100644 client/.env.example create mode 100644 client/public/_redirects diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 000000000..5d2e50998 --- /dev/null +++ b/client/.env.example @@ -0,0 +1 @@ +REACT_APP_API_BASE_URL=https://your-render-backend.onrender.com diff --git a/client/package.json b/client/package.json index d62a4dd8e..3d1ed2223 100644 --- a/client/package.json +++ b/client/package.json @@ -47,7 +47,6 @@ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" - ], - "proxy": "http://backend:5001" + ] } } diff --git a/client/public/_redirects b/client/public/_redirects new file mode 100644 index 000000000..7797f7c6a --- /dev/null +++ b/client/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/client/src/config.js b/client/src/config.js index f8c020e90..424361a25 100644 --- a/client/src/config.js +++ b/client/src/config.js @@ -1,4 +1,5 @@ const config = { - BASE_URL: "http://localhost:5001", + BASE_URL: process.env.REACT_APP_API_BASE_URL || "http://localhost:5001", }; + export default config; diff --git a/server/.env.example b/server/.env.example index 4522065b4..641a7f18c 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1 +1,2 @@ -MONGO_URI = mongodb://127.0.0.1:27017/BlogApp \ No newline at end of file +MONGO_URI = mongodb://127.0.0.1:27017/BlogApp +CLIENT_URL = https://your-netlify-site.netlify.app diff --git a/server/package.json b/server/package.json index 0bc1d46ff..72c48dcd4 100644 --- a/server/package.json +++ b/server/package.json @@ -1,83 +1,23 @@ -{ - - - "name": "blogapp", - - - "version": "1.0.0", - - - "description": "", - - - "main": "server.js", - - - "scripts": { - - - - - "start": "nodemon server.js", - - - - - "test": "echo \"Error: no test specified\" && exit 1" - - - }, - - - "author": "khushi patel", - - - "license": "ISC", - - - "devDependencies": { - - - - - "nodemon": "^2.0.16" - - - }, - - - "dependencies": { - - - - - "bcryptjs": "^2.4.3", - - - - - "cors": "^2.8.5", - - - - - "dotenv": "^16.5.0", - - - - - "express": "^4.18.1", - - - - - "helmet": "^8.1.0", - - - - - "mongoose": "^6.3.4" - - - } -} +{ + "name": "blogapp", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "start": "node server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Aman", + "license": "ISC", + "devDependencies": { + "nodemon": "^2.0.16" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^4.18.1", + "helmet": "^8.1.0", + "mongoose": "^6.3.4" + } +} \ No newline at end of file diff --git a/server/server.js b/server/server.js index 79ee45cba..fde8ce8ff 100644 --- a/server/server.js +++ b/server/server.js @@ -7,7 +7,22 @@ const cors = require("cors"); const app = express(); -app.use(cors()); +const allowedOrigins = [ + process.env.CLIENT_URL, + "http://localhost:3000", + "http://127.0.0.1:3000", +].filter(Boolean); + +app.use( + cors({ + origin: function (origin, callback) { + if (!origin || allowedOrigins.length === 0 || allowedOrigins.includes(origin)) { + return callback(null, true); + } + return callback(new Error("Not allowed by CORS")); + }, + }) +); //setting helmet middleware app.use(helmet( @@ -28,6 +43,5 @@ app.use("/api", (req, res, next) => { res.send("hello"); }); -//define port - -app.listen(5001, () => console.log("app started at 5001...")); +const PORT = process.env.PORT || 5001; +app.listen(PORT, "0.0.0.0", () => console.log(`app started at ${PORT}...`));